/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "inline_bots/inline_bot_layout_internal.h" #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_file_origin.h" #include "styles/style_overview.h" #include "styles/style_history.h" #include "styles/style_chat_helpers.h" #include "styles/style_widgets.h" #include "inline_bots/inline_bot_result.h" #include "lottie/lottie_single_player.h" #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" #include "media/player/media_player_instance.h" #include "history/history_location_manager.h" #include "history/view/history_view_cursor_state.h" #include "storage/localstorage.h" #include "ui/image/image.h" #include "main/main_session.h" #include "apiwrap.h" #include "lang/lang_keys.h" #include "app.h" namespace InlineBots { namespace Layout { namespace internal { using TextState = HistoryView::TextState; FileBase::FileBase(not_null context, not_null result) : ItemBase(context, result) { } FileBase::FileBase(not_null context, DocumentData *document) : ItemBase(context, document) { } DocumentData *FileBase::getShownDocument() const { if (const auto result = getDocument()) { return result; } return getResultDocument(); } int FileBase::content_width() const { if (const auto document = getShownDocument()) { if (document->dimensions.width() > 0) { return document->dimensions.width(); } if (const auto thumb = document->thumbnail()) { return style::ConvertScale(thumb->width()); } } return 0; } int FileBase::content_height() const { if (const auto document = getShownDocument()) { if (document->dimensions.height() > 0) { return document->dimensions.height(); } if (const auto thumb = document->thumbnail()) { return style::ConvertScale(thumb->height()); } } return 0; } int FileBase::content_duration() const { if (const auto document = getShownDocument()) { if (document->getDuration() > 0) { return document->getDuration(); } } return getResultDuration(); } Image *FileBase::content_thumb() const { if (const auto document = getShownDocument()) { if (const auto thumb = document->thumbnail()) { return thumb; } } return getResultThumb(); } Gif::Gif(not_null context, Result *result) : FileBase(context, result) { } Gif::Gif(not_null context, DocumentData *document, bool hasDeleteButton) : FileBase(context, document) { if (hasDeleteButton) { _delete = std::make_shared(document); } } void Gif::initDimensions() { int32 w = content_width(), h = content_height(); if (w <= 0 || h <= 0) { _maxw = 0; } else { w = w * st::inlineMediaHeight / h; _maxw = qMax(w, int32(st::inlineResultsMinWidth)); } _minh = st::inlineMediaHeight + st::inlineResultsSkip; } void Gif::setPosition(int32 position) { ItemBase::setPosition(position); if (_position < 0) { _gif.reset(); } } void DeleteSavedGifClickHandler::onClickImpl() const { Auth().api().toggleSavedGif(_data, Data::FileOriginSavedGifs(), false); const auto index = Auth().data().savedGifs().indexOf(_data); if (index >= 0) { Auth().data().savedGifsRef().remove(index); Local::writeSavedGifs(); } Auth().data().notifySavedGifsUpdated(); } int Gif::resizeGetHeight(int width) { _width = width; _height = _minh; return _height; } void Gif::paint(Painter &p, const QRect &clip, const PaintContext *context) const { const auto document = getShownDocument(); document->automaticLoad(fileOrigin(), nullptr); bool loaded = document->loaded(), loading = document->loading(), displayLoading = document->displayLoading(); if (loaded && !_gif && !_gif.isBad()) { auto that = const_cast(this); that->_gif = Media::Clip::MakeReader(document, FullMsgId(), [that](Media::Clip::Notification notification) { that->clipCallback(notification); }); if (_gif) _gif->setAutoplay(); } const auto animating = (_gif && _gif->started()); if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(document->progress()); } } const auto radial = isRadialAnimation(); int32 height = st::inlineMediaHeight; QSize frame = countFrameSize(); QRect r(0, 0, _width, height); if (animating) { if (!_thumb.isNull()) _thumb = QPixmap(); auto pixmap = _gif->current(frame.width(), frame.height(), _width, height, ImageRoundRadius::None, RectPart::None, context->paused ? 0 : context->ms); p.drawPixmap(r.topLeft(), pixmap); } else { prepareThumbnail({ _width, height }, frame); if (_thumb.isNull()) { p.fillRect(r, st::overviewPhotoBg); } else { p.drawPixmap(r.topLeft(), _thumb); } } if (radial || _gif.isBad() || (!_gif && !loaded && !loading)) { auto radialOpacity = (radial && loaded) ? _animation->radial.opacity() : 1.; if (_animation && _animation->_a_over.animating()) { auto over = _animation->_a_over.value(1.); p.fillRect(r, anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, over)); } else { auto over = (_state & StateFlag::Over); p.fillRect(r, over ? st::msgDateImgBgOver : st::msgDateImgBg); } p.setOpacity(radialOpacity * p.opacity()); p.setOpacity(radialOpacity); auto icon = [&] { if (radial || loading) { return &st::historyFileInCancel; } else if (loaded) { return &st::historyFileInPlay; } return &st::historyFileInDownload; }(); QRect inner((_width - st::msgFileSize) / 2, (height - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize); icon->paintInCenter(p, inner); if (radial) { p.setOpacity(1); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, st::historyFileThumbRadialFg); } } if (_delete && (_state & StateFlag::Over)) { auto deleteSelected = (_state & StateFlag::DeleteOver); auto deletePos = QPoint(_width - st::stickerPanDeleteIconBg.width(), 0); p.setOpacity(deleteSelected ? st::stickerPanDeleteOpacityBgOver : st::stickerPanDeleteOpacityBg); st::stickerPanDeleteIconBg.paint(p, deletePos, width()); p.setOpacity(deleteSelected ? st::stickerPanDeleteOpacityFgOver : st::stickerPanDeleteOpacityFg); st::stickerPanDeleteIconFg.paint(p, deletePos, width()); p.setOpacity(1.); } } TextState Gif::getState( QPoint point, StateRequest request) const { if (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) { if (_delete && style::rtlpoint(point, _width).x() >= _width - st::stickerPanDeleteIconBg.width() && point.y() < st::stickerPanDeleteIconBg.height()) { return { nullptr, _delete }; } else { return { nullptr, _send }; } } return {}; } void Gif::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (!p) return; if (_delete && p == _delete) { bool wasactive = (_state & StateFlag::DeleteOver); if (active != wasactive) { auto from = active ? 0. : 1., to = active ? 1. : 0.; _a_deleteOver.start([this] { update(); }, from, to, st::stickersRowDuration); if (active) { _state |= StateFlag::DeleteOver; } else { _state &= ~StateFlag::DeleteOver; } } } if (p == _delete || p == _send) { bool wasactive = (_state & StateFlag::Over); if (active != wasactive) { if (!getShownDocument()->loaded()) { ensureAnimation(); auto from = active ? 0. : 1., to = active ? 1. : 0.; _animation->_a_over.start([this] { update(); }, from, to, st::stickersRowDuration); } if (active) { _state |= StateFlag::Over; } else { _state &= ~StateFlag::Over; } } } ItemBase::clickHandlerActiveChanged(p, active); } QSize Gif::countFrameSize() const { bool animating = (_gif && _gif->ready()); int32 framew = animating ? _gif->width() : content_width(), frameh = animating ? _gif->height() : content_height(), height = st::inlineMediaHeight; if (framew * height > frameh * _width) { if (framew < st::maxStickerSize || frameh > height) { if (frameh > height || (framew * height / frameh) <= st::maxStickerSize) { framew = framew * height / frameh; frameh = height; } else { frameh = int32(frameh * st::maxStickerSize) / framew; framew = st::maxStickerSize; } } } else { if (frameh < st::maxStickerSize || framew > _width) { if (framew > _width || (frameh * _width / framew) <= st::maxStickerSize) { frameh = frameh * _width / framew; framew = _width; } else { framew = int32(framew * st::maxStickerSize) / frameh; frameh = st::maxStickerSize; } } } return QSize(framew, frameh); } void Gif::validateThumbnail( Image *image, QSize size, QSize frame, bool good) const { if (!image || (_thumbGood && !good)) { return; } else if (!image->loaded()) { image->load(fileOrigin()); return; } else if ((_thumb.size() == size * cIntRetinaFactor()) && (_thumbGood || !good)) { return; } _thumbGood = good; _thumb = image->pixNoCache( fileOrigin(), frame.width() * cIntRetinaFactor(), frame.height() * cIntRetinaFactor(), (Images::Option::Smooth | (good ? Images::Option::None : Images::Option::Blurred)), size.width(), size.height()); } void Gif::prepareThumbnail(QSize size, QSize frame) const { if (const auto document = getShownDocument()) { validateThumbnail(document->thumbnail(), size, frame, true); validateThumbnail(document->thumbnailInline(), size, frame, false); } else { validateThumbnail(getResultThumb(), size, frame, true); } } void Gif::ensureAnimation() const { if (!_animation) { _animation = std::make_unique([=](crl::time now) { radialAnimationCallback(now); }); } } bool Gif::isRadialAnimation() const { if (_animation) { if (_animation->radial.animating()) { return true; } else if (getShownDocument()->loaded()) { _animation = nullptr; } } return false; } void Gif::radialAnimationCallback(crl::time now) const { const auto document = getShownDocument(); const auto updated = [&] { return _animation->radial.update( document->progress(), !document->loading() || document->loaded(), now); }(); if (!anim::Disabled() || updated) { update(); } if (!_animation->radial.animating() && document->loaded()) { _animation = nullptr; } } void Gif::clipCallback(Media::Clip::Notification notification) { using namespace Media::Clip; switch (notification) { case NotificationReinit: { if (_gif) { if (_gif->state() == State::Error) { _gif.setBad(); getShownDocument()->unload(); } else if (_gif->ready() && !_gif->started()) { auto height = st::inlineMediaHeight; auto frame = countFrameSize(); _gif->start(frame.width(), frame.height(), _width, height, ImageRoundRadius::None, RectPart::None); } else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) { _gif.reset(); getShownDocument()->unload(); } } update(); } break; case NotificationRepaint: { if (_gif && !_gif->currentDisplayed()) { update(); } } break; } } Sticker::Sticker(not_null context, Result *result) : FileBase(context, result) { } Sticker::~Sticker() = default; void Sticker::initDimensions() { _maxw = st::stickerPanSize.width(); _minh = st::stickerPanSize.height(); } void Sticker::preload() const { if (const auto document = getShownDocument()) { document->checkStickerSmall(); } else if (const auto thumb = getResultThumb()) { thumb->load(fileOrigin()); } } void Sticker::paint(Painter &p, const QRect &clip, const PaintContext *context) const { bool loaded = getShownDocument()->loaded(); auto over = _a_over.value(_active ? 1. : 0.); if (over > 0) { p.setOpacity(over); App::roundRect(p, QRect(QPoint(0, 0), st::stickerPanSize), st::emojiPanHover, StickerHoverCorners); p.setOpacity(1); } prepareThumbnail(); if (_lottie && _lottie->ready()) { const auto frame = _lottie->frame(); _lottie->markFrameShown(); const auto size = frame.size() / cIntRetinaFactor(); const auto pos = QPoint( (st::stickerPanSize.width() - size.width()) / 2, (st::stickerPanSize.height() - size.height()) / 2); p.drawImage( QRect(pos, size), frame); } else if (!_thumb.isNull()) { int w = _thumb.width() / cIntRetinaFactor(), h = _thumb.height() / cIntRetinaFactor(); QPoint pos = QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2); p.drawPixmap(pos, _thumb); } } TextState Sticker::getState( QPoint point, StateRequest request) const { if (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) { return { nullptr, _send }; } return {}; } void Sticker::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (!p) return; if (p == _send) { if (active != _active) { _active = active; auto from = active ? 0. : 1., to = active ? 1. : 0.; _a_over.start([this] { update(); }, from, to, st::stickersRowDuration); } } ItemBase::clickHandlerActiveChanged(p, active); } QSize Sticker::getThumbSize() const { int width = qMax(content_width(), 1), height = qMax(content_height(), 1); float64 coefw = (st::stickerPanSize.width() - st::buttonRadius * 2) / float64(width); float64 coefh = (st::stickerPanSize.height() - st::buttonRadius * 2) / float64(height); float64 coef = qMin(qMin(coefw, coefh), 1.); int w = qRound(coef * content_width()), h = qRound(coef * content_height()); return QSize(qMax(w, 1), qMax(h, 1)); } void Sticker::setupLottie(not_null document) const { _lottie = Stickers::LottiePlayerFromDocument( document, Stickers::LottieSize::InlineResults, QSize( st::stickerPanSize.width() - st::buttonRadius * 2, st::stickerPanSize.height() - st::buttonRadius * 2 ) * cIntRetinaFactor()); _lottie->updates( ) | rpl::start_with_next([=] { update(); }, _lifetime); } void Sticker::prepareThumbnail() const { if (const auto document = getShownDocument()) { if (document->sticker()->animated && !_lottie && document->loaded()) { setupLottie(document); } document->checkStickerSmall(); if (const auto sticker = document->getStickerSmall()) { if (!_lottie && !_thumbLoaded && sticker->loaded()) { const auto thumbSize = getThumbSize(); _thumb = sticker->pix( document->stickerSetOrigin(), thumbSize.width(), thumbSize.height()); _thumbLoaded = true; } } } else { const auto origin = fileOrigin(); if (const auto thumb = getResultThumb()) { if (thumb->loaded()) { if (!_thumbLoaded) { const auto thumbSize = getThumbSize(); _thumb = thumb->pix( origin, thumbSize.width(), thumbSize.height()); _thumbLoaded = true; } } else { thumb->load(origin); } } } } Photo::Photo(not_null context, Result *result) : ItemBase(context, result) { } void Photo::initDimensions() { const auto photo = getShownPhoto(); int32 w = photo->width(), h = photo->height(); if (w <= 0 || h <= 0) { _maxw = 0; } else { w = w * st::inlineMediaHeight / h; _maxw = qMax(w, int32(st::inlineResultsMinWidth)); } _minh = st::inlineMediaHeight + st::inlineResultsSkip; } void Photo::paint(Painter &p, const QRect &clip, const PaintContext *context) const { int32 height = st::inlineMediaHeight; QSize frame = countFrameSize(); QRect r(0, 0, _width, height); prepareThumbnail({ _width, height }, frame); if (_thumb.isNull()) { p.fillRect(r, st::overviewPhotoBg); } else { p.drawPixmap(r.topLeft(), _thumb); } } TextState Photo::getState( QPoint point, StateRequest request) const { if (QRect(0, 0, _width, st::inlineMediaHeight).contains(point)) { return { nullptr, _send }; } return {}; } PhotoData *Photo::getShownPhoto() const { if (PhotoData *result = getPhoto()) { return result; } return getResultPhoto(); } QSize Photo::countFrameSize() const { PhotoData *photo = getShownPhoto(); int32 framew = photo->width(), frameh = photo->height(), height = st::inlineMediaHeight; if (framew * height > frameh * _width) { if (framew < st::maxStickerSize || frameh > height) { if (frameh > height || (framew * height / frameh) <= st::maxStickerSize) { framew = framew * height / frameh; frameh = height; } else { frameh = int32(frameh * st::maxStickerSize) / framew; framew = st::maxStickerSize; } } } else { if (frameh < st::maxStickerSize || framew > _width) { if (framew > _width || (frameh * _width / framew) <= st::maxStickerSize) { frameh = frameh * _width / framew; framew = _width; } else { framew = int32(framew * st::maxStickerSize) / frameh; frameh = st::maxStickerSize; } } } return QSize(framew, frameh); } void Photo::validateThumbnail( Image *image, QSize size, QSize frame, bool good) const { if (!image || (_thumbGood && !good)) { return; } else if (!image->loaded()) { image->load(fileOrigin()); return; } else if ((_thumb.size() == size * cIntRetinaFactor()) && (_thumbGood || !good)) { return; } const auto origin = fileOrigin(); _thumb = image->pixNoCache( origin, frame.width() * cIntRetinaFactor(), frame.height() * cIntRetinaFactor(), Images::Option::Smooth | (good ? Images::Option(0) : Images::Option::Blurred), size.width(), size.height()); _thumbGood = good; } void Photo::prepareThumbnail(QSize size, QSize frame) const { if (const auto photo = getShownPhoto()) { validateThumbnail(photo->thumbnail(), size, frame, true); validateThumbnail(photo->thumbnailSmall(), size, frame, false); validateThumbnail(photo->thumbnailInline(), size, frame, false); } else if (const auto thumbnail = getResultThumb()) { validateThumbnail(thumbnail, size, frame, true); } } Video::Video(not_null context, Result *result) : FileBase(context, result) , _link(getResultPreviewHandler()) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { if (int duration = content_duration()) { _duration = formatDurationText(duration); _durationWidth = st::normalFont->width(_duration); } } void Video::initDimensions() { const auto withThumb = (content_thumb() != nullptr); _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; int32 textWidth = _maxw - (withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0); TextParseOptions titleOpts = { 0, _maxw, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto }; auto title = TextUtilities::SingleLine(_result->getLayoutTitle()); if (title.isEmpty()) { title = tr::lng_media_video(tr::now); } _title.setText(st::semiboldTextStyle, title, titleOpts); int32 titleHeight = qMin(_title.countHeight(_maxw), 2 * st::semiboldFont->height); int32 descriptionLines = withThumb ? (titleHeight > st::semiboldFont->height ? 1 : 2) : 3; TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto }; QString description = _result->getLayoutDescription(); if (description.isEmpty()) { description = _duration; } _description.setText(st::defaultTextStyle, description, descriptionOpts); int32 descriptionHeight = qMin(_description.countHeight(_maxw), descriptionLines * st::normalFont->height); _minh = st::inlineThumbSize; _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; } void Video::paint(Painter &p, const QRect &clip, const PaintContext *context) const { int left = st::inlineThumbSize + st::inlineThumbSkip; const auto withThumb = (content_thumb() != nullptr); if (withThumb) { prepareThumbnail({ st::inlineThumbSize, st::inlineThumbSize }); if (_thumb.isNull()) { p.fillRect(style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width), st::overviewPhotoBg); } else { p.drawPixmapLeft(0, st::inlineRowMargin, _width, _thumb); } } else { p.fillRect(style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width), st::overviewVideoBg); } if (!_duration.isEmpty()) { int durationTop = st::inlineRowMargin + st::inlineThumbSize - st::normalFont->height - st::inlineDurationMargin; int durationW = _durationWidth + 2 * st::msgDateImgPadding.x(), durationH = st::normalFont->height + 2 * st::msgDateImgPadding.y(); int durationX = (st::inlineThumbSize - durationW) / 2, durationY = st::inlineRowMargin + st::inlineThumbSize - durationH; App::roundRect(p, durationX, durationY - st::msgDateImgPadding.y(), durationW, durationH, st::msgDateImgBg, DateCorners); p.setPen(st::msgDateImgFg); p.setFont(st::normalFont); p.drawText(durationX + st::msgDateImgPadding.x(), durationTop + st::normalFont->ascent, _duration); } p.setPen(st::inlineTitleFg); _title.drawLeftElided(p, left, st::inlineRowMargin, _width - left, _width, 2); int32 titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2); p.setPen(st::inlineDescriptionFg); int32 descriptionLines = withThumb ? (titleHeight > st::semiboldFont->height ? 1 : 2) : 3; _description.drawLeftElided(p, left, st::inlineRowMargin + titleHeight, _width - left, _width, descriptionLines); if (!context->lastRow) { p.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg); } } TextState Video::getState( QPoint point, StateRequest request) const { if (QRect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize).contains(point)) { return { nullptr, _link }; } if (QRect(st::inlineThumbSize + st::inlineThumbSkip, 0, _width - st::inlineThumbSize - st::inlineThumbSkip, _height).contains(point)) { return { nullptr, _send }; } return {}; } void Video::prepareThumbnail(QSize size) const { Expects(content_thumb() != nullptr); const auto thumb = content_thumb(); const auto origin = fileOrigin(); if (thumb->loaded()) { if (_thumb.size() != size * cIntRetinaFactor()) { const auto width = size.width(); const auto height = size.height(); auto w = qMax(style::ConvertScale(thumb->width()), 1); auto h = qMax(style::ConvertScale(thumb->height()), 1); if (w * height > h * width) { if (height < h) { w = w * height / h; h = height; } } else { if (width < w) { h = h * width / w; w = width; } } _thumb = thumb->pixNoCache(origin, w * cIntRetinaFactor(), h * cIntRetinaFactor(), Images::Option::Smooth, width, height); } } else { thumb->load(origin); } } void OpenFileClickHandler::onClickImpl() const { _result->openFile(); } void CancelFileClickHandler::onClickImpl() const { _result->cancelFile(); } File::File(not_null context, not_null result) : FileBase(context, result) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::msgFileSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::msgFileSize - st::inlineThumbSkip) , _open(std::make_shared(result)) , _cancel(std::make_shared(result)) , _document(getShownDocument()) { updateStatusText(); // We have to save document, not read it from Result every time. // Because we first delete the Result and then delete this File. // So in destructor we have to remember _document, we can't read it. regDocumentItem(_document, this); } void File::initDimensions() { _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; int textWidth = _maxw - (st::msgFileSize + st::inlineThumbSkip); TextParseOptions titleOpts = { 0, _maxw, st::semiboldFont->height, Qt::LayoutDirectionAuto }; _title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts); TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, st::normalFont->height, Qt::LayoutDirectionAuto }; _description.setText(st::defaultTextStyle, _result->getLayoutDescription(), descriptionOpts); _minh = st::msgFileSize; _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; } void File::paint(Painter &p, const QRect &clip, const PaintContext *context) const { const auto left = st::msgFileSize + st::inlineThumbSkip; const auto loaded = _document->loaded(); const auto displayLoading = _document->displayLoading(); if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(_document->progress()); } } const auto showPause = updateStatusText(); const auto radial = isRadialAnimation(); auto inner = style::rtlrect(0, st::inlineRowMargin, st::msgFileSize, st::msgFileSize, _width); p.setPen(Qt::NoPen); if (isThumbAnimation()) { auto over = _animation->a_thumbOver.value(1.); p.setBrush(anim::brush(st::msgFileInBg, st::msgFileInBgOver, over)); } else { bool over = ClickHandler::showAsActive(_document->loading() ? _cancel : _open); p.setBrush(over ? st::msgFileInBgOver : st::msgFileInBg); } { PainterHighQualityEnabler hq(p); p.drawEllipse(inner); } if (radial) { auto radialCircle = inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)); _animation->radial.draw(p, radialCircle, st::msgFileRadialLine, st::historyFileInRadialFg); } auto icon = [&] { if (radial || _document->loading()) { return &st::historyFileInCancel; } else if (showPause) { return &st::historyFileInPause; } else if (true || _document->loaded()) { if (_document->isImage()) { return &st::historyFileInImage; } else if (_document->isVoiceMessage() || _document->isAudioFile()) { return &st::historyFileInPlay; } return &st::historyFileInDocument; } return &st::historyFileInDownload; }(); icon->paintInCenter(p, inner); int titleTop = st::inlineRowMargin + st::inlineRowFileNameTop; int descriptionTop = st::inlineRowMargin + st::inlineRowFileDescriptionTop; p.setPen(st::inlineTitleFg); _title.drawLeftElided(p, left, titleTop, _width - left, _width); p.setPen(st::inlineDescriptionFg); bool drawStatusSize = true; if (_statusSize == FileStatusSizeReady || _statusSize == FileStatusSizeLoaded || _statusSize == FileStatusSizeFailed) { if (!_description.isEmpty()) { _description.drawLeftElided(p, left, descriptionTop, _width - left, _width); drawStatusSize = false; } } if (drawStatusSize) { p.setFont(st::normalFont); p.drawTextLeft(left, descriptionTop, _width, _statusText); } if (!context->lastRow) { p.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg); } } TextState File::getState( QPoint point, StateRequest request) const { if (QRect(0, st::inlineRowMargin, st::msgFileSize, st::msgFileSize).contains(point)) { return { nullptr, _document->loading() ? _cancel : _open }; } else { auto left = st::msgFileSize + st::inlineThumbSkip; if (QRect(left, 0, _width - left, _height).contains(point)) { return { nullptr, _send }; } } return {}; } void File::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (p == _open || p == _cancel) { ensureAnimation(); _animation->a_thumbOver.start([this] { thumbAnimationCallback(); }, active ? 0. : 1., active ? 1. : 0., st::msgFileOverDuration); } } File::~File() { unregDocumentItem(_document, this); } void File::thumbAnimationCallback() { update(); } void File::radialAnimationCallback(crl::time now) const { const auto updated = [&] { return _animation->radial.update( _document->progress(), !_document->loading() || _document->loaded(), now); }(); if (!anim::Disabled() || updated) { update(); } if (!_animation->radial.animating()) { checkAnimationFinished(); } } void File::ensureAnimation() const { if (!_animation) { _animation = std::make_unique([=](crl::time now) { return radialAnimationCallback(now); }); } } void File::checkAnimationFinished() const { if (_animation && !_animation->a_thumbOver.animating() && !_animation->radial.animating()) { if (_document->loaded()) { _animation.reset(); } } } bool File::updateStatusText() const { bool showPause = false; int32 statusSize = 0, realDuration = 0; if (_document->status == FileDownloadFailed || _document->status == FileUploadFailed) { statusSize = FileStatusSizeFailed; } else if (_document->uploading()) { statusSize = _document->uploadingData->offset; } else if (_document->loading()) { statusSize = _document->loadOffset(); } else if (_document->loaded()) { statusSize = FileStatusSizeLoaded; } else { statusSize = FileStatusSizeReady; } if (_document->isVoiceMessage() || _document->isAudioFile()) { const auto type = _document->isVoiceMessage() ? AudioMsgId::Type::Voice : AudioMsgId::Type::Song; const auto state = Media::Player::instance()->getState(type); if (state.id == AudioMsgId(_document, FullMsgId(), state.id.externalPlayId()) && !Media::Player::IsStoppedOrStopping(state.state)) { statusSize = -1 - (state.position / state.frequency); realDuration = (state.length / state.frequency); showPause = Media::Player::ShowPauseIcon(state.state); } if (!showPause && (state.id == AudioMsgId(_document, FullMsgId(), state.id.externalPlayId())) && Media::Player::instance()->isSeeking(AudioMsgId::Type::Song)) { showPause = true; } } if (statusSize != _statusSize) { int32 duration = _document->isSong() ? _document->song()->duration : (_document->isVoiceMessage() ? _document->voice()->duration : -1); setStatusSize(statusSize, _document->size, duration, realDuration); } return showPause; } void File::setStatusSize(int32 newSize, int32 fullSize, int32 duration, qint64 realDuration) const { _statusSize = newSize; if (_statusSize == FileStatusSizeReady) { _statusText = (duration >= 0) ? formatDurationAndSizeText(duration, fullSize) : (duration < -1 ? formatGifAndSizeText(fullSize) : formatSizeText(fullSize)); } else if (_statusSize == FileStatusSizeLoaded) { _statusText = (duration >= 0) ? formatDurationText(duration) : (duration < -1 ? qsl("GIF") : formatSizeText(fullSize)); } else if (_statusSize == FileStatusSizeFailed) { _statusText = tr::lng_attach_failed(tr::now); } else if (_statusSize >= 0) { _statusText = formatDownloadText(_statusSize, fullSize); } else { _statusText = formatPlayedText(-_statusSize - 1, realDuration); } } Contact::Contact(not_null context, Result *result) : ItemBase(context, result) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { } void Contact::initDimensions() { _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; int32 textWidth = _maxw - (st::inlineThumbSize + st::inlineThumbSkip); TextParseOptions titleOpts = { 0, _maxw, st::semiboldFont->height, Qt::LayoutDirectionAuto }; _title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts); int32 titleHeight = qMin(_title.countHeight(_maxw), st::semiboldFont->height); TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, st::normalFont->height, Qt::LayoutDirectionAuto }; _description.setText(st::defaultTextStyle, _result->getLayoutDescription(), descriptionOpts); int32 descriptionHeight = qMin(_description.countHeight(_maxw), st::normalFont->height); _minh = st::msgFileSize; _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; } void Contact::paint(Painter &p, const QRect &clip, const PaintContext *context) const { int32 left = st::emojiPanHeaderLeft - st::inlineResultsLeft; left = st::msgFileSize + st::inlineThumbSkip; prepareThumbnail(st::msgFileSize, st::msgFileSize); QRect rthumb(style::rtlrect(0, st::inlineRowMargin, st::msgFileSize, st::msgFileSize, _width)); p.drawPixmapLeft(rthumb.topLeft(), _width, _thumb); int titleTop = st::inlineRowMargin + st::inlineRowFileNameTop; int descriptionTop = st::inlineRowMargin + st::inlineRowFileDescriptionTop; p.setPen(st::inlineTitleFg); _title.drawLeftElided(p, left, titleTop, _width - left, _width); p.setPen(st::inlineDescriptionFg); _description.drawLeftElided(p, left, descriptionTop, _width - left, _width); if (!context->lastRow) { p.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg); } } TextState Contact::getState( QPoint point, StateRequest request) const { if (!QRect(0, st::inlineRowMargin, st::msgFileSize, st::inlineThumbSize).contains(point)) { auto left = (st::msgFileSize + st::inlineThumbSkip); if (QRect(left, 0, _width - left, _height).contains(point)) { return { nullptr, _send }; } } return {}; } void Contact::prepareThumbnail(int width, int height) const { const auto thumb = getResultThumb(); if (!thumb) { if (_thumb.width() != width * cIntRetinaFactor() || _thumb.height() != height * cIntRetinaFactor()) { _thumb = getResultContactAvatar(width, height); } return; } const auto origin = fileOrigin(); if (thumb->loaded()) { if (_thumb.width() != width * cIntRetinaFactor() || _thumb.height() != height * cIntRetinaFactor()) { auto w = qMax(style::ConvertScale(thumb->width()), 1); auto h = qMax(style::ConvertScale(thumb->height()), 1); if (w * height > h * width) { if (height < h) { w = w * height / h; h = height; } } else { if (width < w) { h = h * width / w; w = width; } } _thumb = thumb->pixNoCache(origin, w * cIntRetinaFactor(), h * cIntRetinaFactor(), Images::Option::Smooth, width, height); } } else { thumb->load(origin); } } Article::Article(not_null context, Result *result, bool withThumb) : ItemBase(context, result) , _url(getResultUrlHandler()) , _link(getResultPreviewHandler()) , _withThumb(withThumb) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { if (!_link) { if (const auto point = result->getLocationPoint()) { _link = std::make_shared(*point); } } _thumbLetter = getResultThumbLetter(); } void Article::initDimensions() { _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; int32 textWidth = _maxw - (_withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0); TextParseOptions titleOpts = { 0, _maxw, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto }; _title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts); int32 titleHeight = qMin(_title.countHeight(_maxw), 2 * st::semiboldFont->height); int32 descriptionLines = (_withThumb || _url) ? 2 : 3; QString description = _result->getLayoutDescription(); TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto }; _description.setText(st::defaultTextStyle, description, descriptionOpts); int32 descriptionHeight = qMin(_description.countHeight(_maxw), descriptionLines * st::normalFont->height); _minh = titleHeight + descriptionHeight; if (_url) _minh += st::normalFont->height; if (_withThumb) _minh = qMax(_minh, int32(st::inlineThumbSize)); _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; } int32 Article::resizeGetHeight(int32 width) { _width = qMin(width, _maxw); if (_url) { _urlText = getResultUrl(); _urlWidth = st::normalFont->width(_urlText); if (_urlWidth > _width - st::inlineThumbSize - st::inlineThumbSkip) { _urlText = st::normalFont->elided(_urlText, _width - st::inlineThumbSize - st::inlineThumbSkip); _urlWidth = st::normalFont->width(_urlText); } } _height = _minh; return _height; } void Article::paint(Painter &p, const QRect &clip, const PaintContext *context) const { int32 left = st::emojiPanHeaderLeft - st::inlineResultsLeft; if (_withThumb) { left = st::inlineThumbSize + st::inlineThumbSkip; prepareThumbnail(st::inlineThumbSize, st::inlineThumbSize); QRect rthumb(style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width)); if (_thumb.isNull()) { const auto thumb = getResultThumb(); if (!thumb && !_thumbLetter.isEmpty()) { int32 index = (_thumbLetter.at(0).unicode() % 4); style::color colors[] = { st::msgFile3Bg, st::msgFile4Bg, st::msgFile2Bg, st::msgFile1Bg }; p.fillRect(rthumb, colors[index]); if (!_thumbLetter.isEmpty()) { p.setFont(st::linksLetterFont); p.setPen(st::linksLetterFg); p.drawText(rthumb, _thumbLetter, style::al_center); } } else { p.fillRect(rthumb, st::overviewPhotoBg); } } else { p.drawPixmapLeft(rthumb.topLeft(), _width, _thumb); } } p.setPen(st::inlineTitleFg); _title.drawLeftElided(p, left, st::inlineRowMargin, _width - left, _width, 2); int32 titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2); p.setPen(st::inlineDescriptionFg); int32 descriptionLines = (_withThumb || _url) ? 2 : 3; _description.drawLeftElided(p, left, st::inlineRowMargin + titleHeight, _width - left, _width, descriptionLines); if (_url) { int32 descriptionHeight = qMin(_description.countHeight(_width - left), st::normalFont->height * descriptionLines); p.drawTextLeft(left, st::inlineRowMargin + titleHeight + descriptionHeight, _width, _urlText, _urlWidth); } if (!context->lastRow) { p.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg); } } TextState Article::getState( QPoint point, StateRequest request) const { if (_withThumb && QRect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize).contains(point)) { return { nullptr, _link }; } auto left = _withThumb ? (st::inlineThumbSize + st::inlineThumbSkip) : 0; if (QRect(left, 0, _width - left, _height).contains(point)) { if (_url) { auto left = st::inlineThumbSize + st::inlineThumbSkip; auto titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2); auto descriptionLines = 2; auto descriptionHeight = qMin(_description.countHeight(_width - left), st::normalFont->height * descriptionLines); if (style::rtlrect(left, st::inlineRowMargin + titleHeight + descriptionHeight, _urlWidth, st::normalFont->height, _width).contains(point)) { return { nullptr, _url }; } } return { nullptr, _send }; } return {}; } void Article::prepareThumbnail(int width, int height) const { const auto thumb = getResultThumb(); if (!thumb) { if (_thumb.width() != width * cIntRetinaFactor() || _thumb.height() != height * cIntRetinaFactor()) { _thumb = getResultContactAvatar(width, height); } return; } const auto origin = fileOrigin(); if (thumb->loaded()) { if (_thumb.width() != width * cIntRetinaFactor() || _thumb.height() != height * cIntRetinaFactor()) { auto w = qMax(style::ConvertScale(thumb->width()), 1); auto h = qMax(style::ConvertScale(thumb->height()), 1); if (w * height > h * width) { if (height < h) { w = w * height / h; h = height; } } else { if (width < w) { h = h * width / w; w = width; } } _thumb = thumb->pixNoCache(origin, w * cIntRetinaFactor(), h * cIntRetinaFactor(), Images::Option::Smooth, width, height); } } else { thumb->load(origin); } } Game::Game(not_null context, Result *result) : ItemBase(context, result) , _title(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) , _description(st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft - st::inlineThumbSize - st::inlineThumbSkip) { countFrameSize(); } void Game::countFrameSize() { if (auto document = getResultDocument()) { if (document->isAnimation()) { auto documentSize = document->dimensions; if (documentSize.isEmpty()) { documentSize = QSize(st::inlineThumbSize, st::inlineThumbSize); } auto resizeByHeight1 = (documentSize.width() > documentSize.height()) && (documentSize.height() >= st::inlineThumbSize); auto resizeByHeight2 = (documentSize.height() >= documentSize.width()) && (documentSize.width() < st::inlineThumbSize); if (resizeByHeight1 || resizeByHeight2) { if (documentSize.height() > st::inlineThumbSize) { _frameSize = QSize((documentSize.width() * st::inlineThumbSize) / documentSize.height(), st::inlineThumbSize); } } else { if (documentSize.width() > st::inlineThumbSize) { _frameSize = QSize(st::inlineThumbSize, (documentSize.height() * st::inlineThumbSize) / documentSize.width()); } } if (!_frameSize.width()) { _frameSize.setWidth(1); } if (!_frameSize.height()) { _frameSize.setHeight(1); } } } } void Game::initDimensions() { _maxw = st::emojiPanWidth - st::emojiScroll.width - st::inlineResultsLeft; int32 textWidth = _maxw - (st::inlineThumbSize + st::inlineThumbSkip); TextParseOptions titleOpts = { 0, _maxw, 2 * st::semiboldFont->height, Qt::LayoutDirectionAuto }; _title.setText(st::semiboldTextStyle, TextUtilities::SingleLine(_result->getLayoutTitle()), titleOpts); int32 titleHeight = qMin(_title.countHeight(_maxw), 2 * st::semiboldFont->height); int32 descriptionLines = 2; QString description = _result->getLayoutDescription(); TextParseOptions descriptionOpts = { TextParseMultiline, _maxw, descriptionLines * st::normalFont->height, Qt::LayoutDirectionAuto }; _description.setText(st::defaultTextStyle, description, descriptionOpts); int32 descriptionHeight = qMin(_description.countHeight(_maxw), descriptionLines * st::normalFont->height); _minh = titleHeight + descriptionHeight; accumulate_max(_minh, st::inlineThumbSize); _minh += st::inlineRowMargin * 2 + st::inlineRowBorder; } void Game::setPosition(int32 position) { ItemBase::setPosition(position); if (_position < 0) { _gif.reset(); } } void Game::paint(Painter &p, const QRect &clip, const PaintContext *context) const { int32 left = st::emojiPanHeaderLeft - st::inlineResultsLeft; left = st::inlineThumbSize + st::inlineThumbSkip; auto rthumb = style::rtlrect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize, _width); // Gif thumb auto thumbDisplayed = false, radial = false; auto document = getResultDocument(); auto animatedThumb = document && document->isAnimation(); if (animatedThumb) { document->automaticLoad(fileOrigin(), nullptr); bool loaded = document->loaded(), loading = document->loading(), displayLoading = document->displayLoading(); if (loaded && !_gif && !_gif.isBad()) { auto that = const_cast(this); that->_gif = Media::Clip::MakeReader(document, FullMsgId(), [that](Media::Clip::Notification notification) { that->clipCallback(notification); }); if (_gif) _gif->setAutoplay(); } bool animating = (_gif && _gif->started()); if (displayLoading) { if (!_radial) { _radial = std::make_unique([=](crl::time now) { return radialAnimationCallback(now); }); } if (!_radial->animating()) { _radial->start(document->progress()); } } radial = isRadialAnimation(); if (animating) { if (!_thumb.isNull()) _thumb = QPixmap(); auto animationThumb = _gif->current(_frameSize.width(), _frameSize.height(), st::inlineThumbSize, st::inlineThumbSize, ImageRoundRadius::None, RectPart::None, context->paused ? 0 : context->ms); p.drawPixmapLeft(rthumb.topLeft(), _width, animationThumb); thumbDisplayed = true; } } if (!thumbDisplayed) { prepareThumbnail({ st::inlineThumbSize, st::inlineThumbSize }); if (_thumb.isNull()) { p.fillRect(rthumb, st::overviewPhotoBg); } else { p.drawPixmapLeft(rthumb.topLeft(), _width, _thumb); } } if (radial) { p.fillRect(rthumb, st::msgDateImgBg); QRect inner((st::inlineThumbSize - st::msgFileSize) / 2, (st::inlineThumbSize - st::msgFileSize) / 2, st::msgFileSize, st::msgFileSize); if (radial) { p.setOpacity(1); QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _radial->draw(p, rinner, st::msgFileRadialLine, st::historyFileThumbRadialFg); } } p.setPen(st::inlineTitleFg); _title.drawLeftElided(p, left, st::inlineRowMargin, _width - left, _width, 2); int32 titleHeight = qMin(_title.countHeight(_width - left), st::semiboldFont->height * 2); p.setPen(st::inlineDescriptionFg); int32 descriptionLines = 2; _description.drawLeftElided(p, left, st::inlineRowMargin + titleHeight, _width - left, _width, descriptionLines); if (!context->lastRow) { p.fillRect(style::rtlrect(left, _height - st::inlineRowBorder, _width - left, st::inlineRowBorder, _width), st::inlineRowBorderFg); } } TextState Game::getState( QPoint point, StateRequest request) const { int left = st::inlineThumbSize + st::inlineThumbSkip; if (QRect(0, st::inlineRowMargin, st::inlineThumbSize, st::inlineThumbSize).contains(point)) { return { nullptr, _send }; } if (QRect(left, 0, _width - left, _height).contains(point)) { return { nullptr, _send }; } return {}; } void Game::prepareThumbnail(QSize size) const { if (const auto photo = getResultPhoto()) { validateThumbnail(photo->thumbnail(), size, true); validateThumbnail(photo->thumbnailInline(), size, false); } else if (const auto document = getResultDocument()) { validateThumbnail(document->thumbnail(), size, true); validateThumbnail(document->thumbnailInline(), size, false); } } void Game::validateThumbnail(Image *image, QSize size, bool good) const { if (!image || (_thumbGood && !good)) { return; } else if (!image->loaded()) { image->load(fileOrigin()); return; } else if ((_thumb.size() == size * cIntRetinaFactor()) && (_thumbGood || !good)) { return; } const auto width = size.width(); const auto height = size.height(); auto w = qMax(style::ConvertScale(image->width()), 1); auto h = qMax(style::ConvertScale(image->height()), 1); auto resizeByHeight1 = (w * height > h * width) && (h >= height); auto resizeByHeight2 = (h * width >= w * height) && (w < width); if (resizeByHeight1 || resizeByHeight2) { if (h > height) { w = w * height / h; h = height; } } else { if (w > width) { h = h * width / w; w = width; } } _thumbGood = good; _thumb = image->pixNoCache( fileOrigin(), w * cIntRetinaFactor(), h * cIntRetinaFactor(), (Images::Option::Smooth | (good ? Images::Option::None : Images::Option::Blurred)), size.width(), size.height()); } bool Game::isRadialAnimation() const { if (_radial) { if (_radial->animating()) { return true; } else if (getResultDocument()->loaded()) { _radial = nullptr; } } return false; } void Game::radialAnimationCallback(crl::time now) const { const auto document = getResultDocument(); const auto updated = [&] { return _radial->update( document->progress(), !document->loading() || document->loaded(), now); }(); if (!anim::Disabled() || updated) { update(); } if (!_radial->animating() && document->loaded()) { _radial = nullptr; } } void Game::clipCallback(Media::Clip::Notification notification) { using namespace Media::Clip; switch (notification) { case NotificationReinit: { if (_gif) { if (_gif->state() == State::Error) { _gif.setBad(); getResultDocument()->unload(); } else if (_gif->ready() && !_gif->started()) { _gif->start(_frameSize.width(), _frameSize.height(), st::inlineThumbSize, st::inlineThumbSize, ImageRoundRadius::None, RectPart::None); } else if (_gif->autoPausedGif() && !context()->inlineItemVisible(this)) { _gif.reset(); getResultDocument()->unload(); } } update(); } break; case NotificationRepaint: { if (_gif && !_gif->currentDisplayed()) { update(); } } break; } } } // namespace internal } // namespace Layout } // namespace InlineBots