From 487fa9728a8943c298e8b94183c96312e9e67197 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 14 May 2024 14:07:38 +0400 Subject: [PATCH] Fade in/out effect preview. --- .../data/data_message_reactions.cpp | 8 +- .../SourceFiles/data/data_message_reactions.h | 4 + .../history_view_reactions_selector.cpp | 16 +- .../history_view_reactions_selector.h | 6 +- .../media/stories/media_stories_reactions.cpp | 3 +- Telegram/SourceFiles/menu/menu_send.cpp | 182 ++++++++++++------ 6 files changed, 155 insertions(+), 64 deletions(-) diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 7a834dcc16..f1175a4734 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -575,7 +575,9 @@ void Reactions::preloadReactionImageFor(const ReactionId &emoji) { } void Reactions::preloadEffectImageFor(EffectId id) { - preloadImageFor({ DocumentId(id) }); + if (id != kFakeEffectId) { + preloadImageFor({ DocumentId(id) }); + } } void Reactions::preloadImageFor(const ReactionId &id) { @@ -651,7 +653,9 @@ QImage Reactions::resolveReactionImageFor(const ReactionId &emoji) { } QImage Reactions::resolveEffectImageFor(EffectId id) { - return resolveImageFor({ DocumentId(id) }); + return (id == kFakeEffectId) + ? QImage() + : resolveImageFor({ DocumentId(id) }); } QImage Reactions::resolveImageFor(const ReactionId &id) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index 98aab8d851..793fcc1981 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -119,6 +119,10 @@ public: void preloadReactionImageFor(const ReactionId &emoji); [[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji); + // This is used to reserve space for the effect in BottomInfo but not + // actually paint anything, used in case we want to paint icon ourselves. + static constexpr auto kFakeEffectId = EffectId(1); + void preloadEffectImageFor(EffectId id); [[nodiscard]] QImage resolveEffectImageFor(EffectId id); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp index 7173d3aed4..dcac6a69e7 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.cpp @@ -203,6 +203,7 @@ Selector::Selector( TextWithEntities about, Fn close, IconFactory iconFactory, + Fn paused, bool child) : Selector( parent, @@ -216,8 +217,9 @@ Selector::Selector( : ChatHelpers::EmojiListMode::MessageEffects), {}, std::move(about), - iconFactory, - close, + std::move(iconFactory), + std::move(paused), + std::move(close), child) { } @@ -253,6 +255,7 @@ Selector::Selector( std::vector recent, TextWithEntities about, IconFactory iconFactory, + Fn paused, Fn close, bool child) : RpWidget(parent) @@ -261,6 +264,7 @@ Selector::Selector( , _reactions(reactions) , _recent(std::move(recent)) , _listMode(mode) +, _paused(std::move(paused)) , _jumpedToPremium([=] { close(false); }) , _cachedRound( QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight), @@ -1005,7 +1009,7 @@ void Selector::createList() { object_ptr(lists, EmojiListDescriptor{ .show = _show, .mode = _listMode, - .paused = [] { return false; }, + .paused = _paused ? _paused : [] { return false; }, .customRecentList = std::move(recentList), .customRecentFactory = _unifiedFactoryOwner->factory(), .freeEffects = std::move(freeEffects), @@ -1026,7 +1030,7 @@ void Selector::createList() { StickersListDescriptor{ .show = _show, .mode = StickersListMode::MessageEffects, - .paused = [] { return false; }, + .paused = _paused ? _paused : [] { return false; }, .customRecentList = std::move(descriptors), .st = st, })); @@ -1352,7 +1356,8 @@ auto AttachSelectorToMenu( std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, TextWithEntities about, - IconFactory iconFactory) + IconFactory iconFactory, + Fn paused) -> base::expected, AttachSelectorResult> { if (reactions.recent.empty()) { return base::make_unexpected(AttachSelectorResult::Skipped); @@ -1366,6 +1371,7 @@ auto AttachSelectorToMenu( std::move(about), [=](bool fast) { menu->hideMenu(fast); }, std::move(iconFactory), + std::move(paused), false); // child if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { return base::make_unexpected(AttachSelectorResult::Failed); diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h index d5b1e05d5e..56f5210b78 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_selector.h @@ -85,6 +85,7 @@ public: TextWithEntities about, Fn close, IconFactory iconFactory = nullptr, + Fn paused = nullptr, bool child = false); #if 0 // not ready Selector( @@ -149,6 +150,7 @@ private: std::vector recent, TextWithEntities about, IconFactory iconFactory, + Fn paused, Fn close, bool child); @@ -187,6 +189,7 @@ private: const Data::PossibleItemReactions _reactions; const std::vector _recent; const ChatHelpers::EmojiListMode _listMode; + const Fn _paused; Fn _jumpedToPremium; Ui::RoundAreaWithShadow _cachedRound; std::unique_ptr _strip; @@ -274,7 +277,8 @@ AttachSelectorResult AttachSelectorToMenu( std::shared_ptr show, const Data::PossibleItemReactionsRef &reactions, TextWithEntities about, - IconFactory iconFactory = nullptr + IconFactory iconFactory = nullptr, + Fn paused = nullptr ) -> base::expected, AttachSelectorResult>; [[nodiscard]] TextWithEntities ItemReactionsAbout( diff --git a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp index 30055ad0b7..af45d91507 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reactions.cpp @@ -668,7 +668,8 @@ void Reactions::Panel::create() { ? tr::lng_stories_reaction_as_message(tr::now) : QString()) }, [=](bool fast) { hide(mode); }, - nullptr, + nullptr, // iconFactory + nullptr, // paused true); _selector->chosen( diff --git a/Telegram/SourceFiles/menu/menu_send.cpp b/Telegram/SourceFiles/menu/menu_send.cpp index 450c50d554..98645e79f1 100644 --- a/Telegram/SourceFiles/menu/menu_send.cpp +++ b/Telegram/SourceFiles/menu/menu_send.cpp @@ -59,6 +59,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace SendMenu { namespace { +constexpr auto kToggleDuration = crl::time(400); + class Delegate final : public HistoryView::DefaultElementDelegate { public: Delegate(not_null pathGradient) @@ -90,6 +92,8 @@ public: Fn action, Fn done); + void hideAnimated(); + private: void paintEvent(QPaintEvent *e) override; void mousePressEvent(QMouseEvent *e) override; @@ -104,8 +108,12 @@ private: void setupSend(Details details); void createLottie(); + [[nodiscard]] bool ready() const; + void paintLoading(QPainter &p); + void paintLottie(QPainter &p); bool checkIconBecameLoaded(); - [[nodiscard]] bool checkReady(); + [[nodiscard]] bool checkLoaded(); + void toggle(bool shown); const EffectId _effectId = 0; const Data::Reaction _effect; @@ -135,6 +143,10 @@ private: QRect _iconRect; std::unique_ptr _loading; + Ui::Animations::Simple _shownAnimation; + QPixmap _bottomCache; + bool _hiding = false; + rpl::lifetime _readyCheckLifetime; }; @@ -248,7 +260,7 @@ EffectPreview::EffectPreview( _history->peer->id, _replyTo->data()->fullId(), tr::lng_settings_chat_message_reply(tr::now), - _effectId)) + Data::Reactions::kFakeEffectId)) , _send(canSend() ? std::make_unique( this, @@ -271,68 +283,87 @@ EffectPreview::EffectPreview( , _close(done) , _actionWithEffect(ComposeActionWithEffect(action, _effectId, done)) { setupGeometry(position); - setupBackground(); setupItem(); + setupBackground(); setupLottie(); setupSend(details); + + toggle(true); } void EffectPreview::paintEvent(QPaintEvent *e) { - auto p = Painter(this); + checkIconBecameLoaded(); + + const auto progress = _shownAnimation.value(_hiding ? 0. : 1.); + if (!progress) { + return; + } + + auto p = QPainter(this); + p.setOpacity(progress); p.drawImage(0, 0, _bg); - p.setClipRect(_inner); - p.translate(_itemShift); - auto rect = QRect(0, 0, st::windowMinWidth, _inner.height()); - auto context = _theme->preparePaintContext( - _chatStyle.get(), - rect, - rect, - false); - context.outbg = _item->hasOutLayout(); - _item->draw(p, context); - p.translate(-_itemShift); + if (!_bottomCache.isNull()) { + p.drawPixmap(_bottom->pos(), _bottomCache); + } - checkIconBecameLoaded(); - if (_icon.isNull()) { - if (!_loading) { - _loading = std::make_unique([=] { - update(); - }, st::effectPreviewLoading); - _loading->start(st::defaultInfiniteRadialAnimation.linearPeriod); - } - const auto loading = _iconRect.marginsRemoved( - { st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth }); - auto hq = PainterHighQualityEnabler(p); - Ui::InfiniteRadialAnimation::Draw( - p, - _loading->computeState(), - loading.topLeft(), - loading.size(), - width(), - _chatStyle->msgInDateFg(), - st::effectPreviewLoading.thickness); + if (!ready()) { + paintLoading(p); } else { _loading = nullptr; - } - if (_lottie && _lottie->ready()) { - const auto factor = style::DevicePixelRatio(); - auto request = Lottie::FrameRequest(); - request.box = _inner.size() * factor; - const auto rightAligned = _item->hasRightLayout(); - if (!rightAligned) { - request.mirrorHorizontal = true; + p.drawImage(_iconRect, _icon); + if (!_hiding) { + p.setOpacity(1.); } - const auto frame = _lottie->frameInfo(request); - p.drawImage( - QRect(_inner.topLeft(), frame.image.size() / factor), - frame.image); - _lottie->markFrameShown(); + paintLottie(p); } } +bool EffectPreview::ready() const { + return !_icon.isNull() && _lottie && _lottie->ready(); +} + +void EffectPreview::paintLoading(QPainter &p) { + if (!_loading) { + _loading = std::make_unique([=] { + update(); + }, st::effectPreviewLoading); + _loading->start(st::defaultInfiniteRadialAnimation.linearPeriod); + } + const auto loading = _iconRect.marginsRemoved( + { st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth }); + auto hq = PainterHighQualityEnabler(p); + Ui::InfiniteRadialAnimation::Draw( + p, + _loading->computeState(), + loading.topLeft(), + loading.size(), + width(), + _chatStyle->msgInDateFg(), + st::effectPreviewLoading.thickness); +} + +void EffectPreview::paintLottie(QPainter &p) { + const auto factor = style::DevicePixelRatio(); + auto request = Lottie::FrameRequest(); + request.box = _inner.size() * factor; + const auto rightAligned = _item->hasRightLayout(); + if (!rightAligned) { + request.mirrorHorizontal = true; + } + const auto frame = _lottie->frameInfo(request); + p.drawImage( + QRect(_inner.topLeft(), frame.image.size() / factor), + frame.image); + _lottie->markFrameShown(); +} + +void EffectPreview::hideAnimated() { + toggle(false); +} + void EffectPreview::mousePressEvent(QMouseEvent *e) { - delete this; + hideAnimated(); } void EffectPreview::setupGeometry(QPoint position) { @@ -402,7 +433,7 @@ void EffectPreview::repaintBackground() { bg.setDevicePixelRatio(ratio); { - auto p = QPainter(&bg); + auto p = Painter(&bg); Window::SectionWidget::PaintBackground( p, _theme.get(), @@ -411,6 +442,18 @@ void EffectPreview::repaintBackground() { p.fillRect( QRect(0, _inner.height(), _inner.width(), _bottom->height()), st::previewMarkRead.bgColor); + + p.translate(_itemShift - _inner.topLeft()); + auto rect = QRect(0, 0, st::windowMinWidth, _inner.height()); + auto context = _theme->preparePaintContext( + _chatStyle.get(), + rect, + rect, + false); + context.outbg = _item->hasOutLayout(); + _item->draw(p, context); + p.translate(_inner.topLeft() - _itemShift); + auto hq = PainterHighQualityEnabler(p); p.setCompositionMode(QPainter::CompositionMode_DestinationIn); auto roundRect = Ui::RoundRect(st::previewMenu.radius, st::menuBg); @@ -438,7 +481,7 @@ void EffectPreview::setupLottie() { rpl::single(rpl::empty) | rpl::then( _show->session().downloaderTaskFinished() ) | rpl::start_with_next([=] { - if (checkReady()) { + if (checkLoaded()) { _readyCheckLifetime.destroy(); createLottie(); } @@ -495,10 +538,14 @@ bool EffectPreview::checkIconBecameLoaded() { } const auto reactions = &_show->session().data().reactions(); _icon = reactions->resolveEffectImageFor(_effect.id.custom()); - return !_icon.isNull(); + if (_icon.isNull()) { + return false; + } + repaintBackground(); + return true; } -bool EffectPreview::checkReady() { +bool EffectPreview::checkLoaded() { if (checkIconBecameLoaded()) { update(); } @@ -511,6 +558,29 @@ bool EffectPreview::checkReady() { return !_icon.isNull() && (!_bytes.isEmpty() || !_filepath.isEmpty()); } +void EffectPreview::toggle(bool shown) { + if (!shown && _hiding) { + return; + } + _hiding = !shown; + if (_bottomCache.isNull()) { + _bottomCache = Ui::GrabWidget(_bottom); + _bottom->hide(); + } + _shownAnimation.start([=] { + update(); + if (!_shownAnimation.animating()) { + if (_hiding) { + delete this; + } else { + _bottomCache = QPixmap(); + _bottom->show(); + } + } + }, shown ? 0. : 1., shown ? 1. : 0., kToggleDuration, anim::easeOutCirc); + show(); +} + } // namespace Fn DefaultCallback( @@ -571,6 +641,7 @@ FillMenuResult FillSendMenu( } using namespace HistoryView::Reactions; + const auto effect = std::make_shared>(); const auto position = desiredPositionOverride.value_or(QCursor::pos()); const auto selector = (showForEffect && details.effectAllowed) ? AttachSelectorToMenu( @@ -579,7 +650,9 @@ FillMenuResult FillSendMenu( st::reactPanelEmojiPan, showForEffect, LookupPossibleEffects(&showForEffect->session()), - { tr::lng_effect_add_title(tr::now) }) + { tr::lng_effect_add_title(tr::now) }, + nullptr, // iconFactory + [=] { return (*effect) != nullptr; }) // paused : base::make_unexpected(AttachSelectorResult::Skipped); if (!selector) { if (selector.error() == AttachSelectorResult::Failed) { @@ -589,7 +662,6 @@ FillMenuResult FillSendMenu( return FillMenuResult::Prepared; } - const auto effect = std::make_shared>(); (*selector)->chosen( ) | rpl::start_with_next([=](ChosenReaction chosen) { const auto &reactions = showForEffect->session().data().reactions(); @@ -597,7 +669,7 @@ FillMenuResult FillSendMenu( const auto i = ranges::find(effects, chosen.id, &Data::Reaction::id); if (i != end(effects)) { if (const auto strong = effect->data()) { - delete strong; + strong->hideAnimated(); } const auto weak = Ui::MakeWeak(menu); const auto done = [=] {