From 0dd1b4eae6e90ef4d554c7ff61bcb53008876ef4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 2 Jul 2019 16:29:26 +0200 Subject: [PATCH] Support animated stickers in suggestions. --- .../chat_helpers/field_autocomplete.cpp | 184 +++++++++++++++--- .../chat_helpers/field_autocomplete.h | 55 ++++-- .../SourceFiles/chat_helpers/stickers.cpp | 7 +- Telegram/SourceFiles/chat_helpers/stickers.h | 3 +- .../SourceFiles/lottie/lottie_animation.cpp | 4 + .../SourceFiles/lottie/lottie_animation.h | 3 + .../lottie/lottie_multi_player.cpp | 4 - .../SourceFiles/lottie/lottie_multi_player.h | 2 - 8 files changed, 210 insertions(+), 52 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 46b7d06c8..ad91949ab 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "apiwrap.h" #include "storage/localstorage.h" +#include "lottie/lottie_single_player.h" #include "ui/widgets/scroll_area.h" #include "ui/image/image.h" #include "auth_session.h" @@ -23,11 +24,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_widgets.h" #include "styles/style_chat_helpers.h" -FieldAutocomplete::FieldAutocomplete(QWidget *parent) : TWidget(parent) +FieldAutocomplete::FieldAutocomplete(QWidget *parent) +: RpWidget(parent) , _scroll(this, st::mentionScroll) { _scroll->setGeometry(rect()); - _inner = _scroll->setOwnedWidget(object_ptr(this, &_mrows, &_hrows, &_brows, &_srows)); + _inner = _scroll->setOwnedWidget( + object_ptr( + this, + &_mrows, + &_hrows, + &_brows, + &_srows)); _inner->setGeometry(rect()); connect(_inner, SIGNAL(mentionChosen(UserData*, FieldAutocomplete::ChooseMethod)), this, SIGNAL(mentionChosen(UserData*, FieldAutocomplete::ChooseMethod))); @@ -44,6 +52,8 @@ FieldAutocomplete::FieldAutocomplete(QWidget *parent) : TWidget(parent) connect(_scroll, SIGNAL(geometryChanged()), _inner, SLOT(onParentGeometryChanged())); } +FieldAutocomplete::~FieldAutocomplete() = default; + void FieldAutocomplete::paintEvent(QPaintEvent *e) { Painter p(this); @@ -70,7 +80,12 @@ void FieldAutocomplete::showFiltered( _channel = peer->asChannel(); if (query.isEmpty()) { _type = Type::Mentions; - rowsUpdated(internal::MentionRows(), internal::HashtagRows(), internal::BotCommandRows(), _srows, false); + rowsUpdated( + internal::MentionRows(), + internal::HashtagRows(), + internal::BotCommandRows(), + base::take(_srows), + false); return; } @@ -108,7 +123,12 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) { _emoji = emoji; _type = Type::Stickers; if (!emoji) { - rowsUpdated(_mrows, _hrows, _brows, internal::StickerRows(), false); + rowsUpdated( + base::take(_mrows), + base::take(_hrows), + base::take(_brows), + internal::StickerRows(), + false); return; } @@ -137,6 +157,31 @@ inline int indexOfInFirstN(const T &v, const U &elem, int last) { } } +internal::StickerRows FieldAutocomplete::getStickerSuggestions() { + const auto list = Stickers::GetListByEmoji( + _emoji, + _stickersSeed + ); + auto result = ranges::view::all( + list + ) | ranges::view::transform([](not_null sticker) { + return internal::StickerSuggestion{ sticker }; + }) | ranges::to_vector; + for (auto &suggestion : _srows) { + if (!suggestion.animated) { + continue; + } + const auto i = ranges::find( + result, + suggestion.document, + &internal::StickerSuggestion::document); + if (i != end(result)) { + i->animated = std::move(suggestion.animated); + } + } + return result; +} + void FieldAutocomplete::updateFiltered(bool resetScroll) { int32 now = unixtime(), recentInlineBots = 0; internal::MentionRows mrows; @@ -144,7 +189,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { internal::BotCommandRows brows; internal::StickerRows srows; if (_emoji) { - srows = Stickers::GetListByEmoji(_emoji, _stickersSeed); + srows = getStickerSuggestions(); } else if (_type == Type::Mentions) { int maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0; if (_chat) { @@ -326,11 +371,21 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } } } - rowsUpdated(mrows, hrows, brows, srows, resetScroll); + rowsUpdated( + std::move(mrows), + std::move(hrows), + std::move(brows), + std::move(srows), + resetScroll); _inner->setRecentInlineBotsInRows(recentInlineBots); } -void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const internal::StickerRows &srows, bool resetScroll) { +void FieldAutocomplete::rowsUpdated( + internal::MentionRows &&mrows, + internal::HashtagRows &&hrows, + internal::BotCommandRows &&brows, + internal::StickerRows &&srows, + bool resetScroll) { if (mrows.isEmpty() && hrows.isEmpty() && brows.isEmpty() && srows.empty()) { if (!isHidden()) { hideAnimated(); @@ -341,10 +396,10 @@ void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const in _brows.clear(); _srows.clear(); } else { - _mrows = mrows; - _hrows = hrows; - _brows = brows; - _srows = srows; + _mrows = std::move(mrows); + _hrows = std::move(hrows); + _brows = std::move(brows); + _srows = std::move(srows); bool hidden = _hiding || isHidden(); if (hidden) { @@ -358,6 +413,7 @@ void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const in showAnimated(); } } + _inner->rowsUpdated(); } void FieldAutocomplete::setBoundings(QRect boundings) { @@ -505,12 +561,14 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { return QWidget::eventFilter(obj, e); } -FieldAutocomplete::~FieldAutocomplete() { -} - namespace internal { -FieldAutocompleteInner::FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerRows *srows) +FieldAutocompleteInner::FieldAutocompleteInner( + not_null parent, + not_null mrows, + not_null hrows, + not_null brows, + not_null srows) : _parent(parent) , _mrows(mrows) , _hrows(hrows) @@ -551,9 +609,16 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { int32 index = row * _stickersPerRow + col; if (index >= _srows->size()) break; - const auto document = _srows->at(index); + auto &sticker = (*_srows)[index]; + const auto document = sticker.document; if (!document->sticker()) continue; + if (document->sticker()->animated + && !sticker.animated + && document->loaded()) { + setupLottie(sticker); + } + QPoint pos(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height()); if (_sel == index) { QPoint tl(pos); @@ -562,14 +627,23 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { } document->checkStickerSmall(); - - float64 coef = qMin((st::stickerPanSize.width() - st::buttonRadius * 2) / float64(document->dimensions.width()), (st::stickerPanSize.height() - st::buttonRadius * 2) / float64(document->dimensions.height())); - if (coef > 1) coef = 1; - int32 w = qRound(coef * document->dimensions.width()), h = qRound(coef * document->dimensions.height()); - if (w < 1) w = 1; - if (h < 1) h = 1; - QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2); - if (const auto image = document->getStickerSmall()) { + if (sticker.animated && sticker.animated->ready()) { + const auto frame = sticker.animated->frame(); + sticker.animated->markFrameShown(); + const auto size = frame.size() / cIntRetinaFactor(); + const auto ppos = pos + QPoint( + (st::stickerPanSize.width() - size.width()) / 2, + (st::stickerPanSize.height() - size.height()) / 2); + p.drawImage( + QRect(ppos, size), + frame); + } else if (const auto image = document->getStickerSmall()) { + float64 coef = qMin((st::stickerPanSize.width() - st::buttonRadius * 2) / float64(document->dimensions.width()), (st::stickerPanSize.height() - st::buttonRadius * 2) / float64(document->dimensions.height())); + if (coef > 1) coef = 1; + int32 w = qRound(coef * document->dimensions.width()), h = qRound(coef * document->dimensions.height()); + if (w < 1) w = 1; + if (h < 1) h = 1; + QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2); p.drawPixmapLeft(ppos, width(), image->pix(document->stickerSetOrigin(), w, h)); } } @@ -742,7 +816,7 @@ bool FieldAutocompleteInner::moveSel(int key) { bool FieldAutocompleteInner::chooseSelected(FieldAutocomplete::ChooseMethod method) const { if (!_srows->empty()) { if (_sel >= 0 && _sel < _srows->size()) { - emit stickerChosen((*_srows)[_sel], method); + emit stickerChosen((*_srows)[_sel].document, method); return true; } } else if (!_mrows->isEmpty()) { @@ -872,6 +946,58 @@ void FieldAutocompleteInner::setSel(int sel, bool scroll) { } } +void FieldAutocompleteInner::rowsUpdated() { + if (_srows->empty()) { + _stickersLifetime.destroy(); + } +} + +auto FieldAutocompleteInner::getLottieRenderer() +-> std::shared_ptr { + if (auto result = _lottieRenderer.lock()) { + return result; + } + auto result = Lottie::MakeFrameRenderer(); + _lottieRenderer = result; + return result; +} + +void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) { + const auto document = suggestion.document; + suggestion.animated = Stickers::LottiePlayerFromDocument( + document, + Stickers::LottieSize::InlineResults, + QSize( + st::stickerPanSize.width() - st::buttonRadius * 2, + st::stickerPanSize.height() - st::buttonRadius * 2 + ) * cIntRetinaFactor(), + getLottieRenderer()); + + suggestion.animated->updates( + ) | rpl::start_with_next([=] { + repaintSticker(document); + }, _stickersLifetime); +} + +void FieldAutocompleteInner::repaintSticker( + not_null document) { + const auto i = ranges::find( + *_srows, + document, + &StickerSuggestion::document); + if (i == end(*_srows)) { + return; + } + const auto index = (i - begin(*_srows)); + const auto row = (index / _stickersPerRow); + const auto col = (index % _stickersPerRow); + update( + st::stickerPanPadding + col * st::stickerPanSize.width(), + st::stickerPanPadding + row * st::stickerPanSize.height(), + st::stickerPanSize.width(), + st::stickerPanSize.height()); +} + void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) { _mouseSelection = true; _lastMousePosition = globalPosition; @@ -905,8 +1031,8 @@ void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) { _down = _sel; if (_down >= 0 && _down < _srows->size()) { Ui::showMediaPreview( - (*_srows)[_down]->stickerSetOrigin(), - (*_srows)[_down]); + (*_srows)[_down].document->stickerSetOrigin(), + (*_srows)[_down].document); } } } @@ -925,8 +1051,8 @@ void FieldAutocompleteInner::onParentGeometryChanged() { void FieldAutocompleteInner::showPreview() { if (_down >= 0 && _down < _srows->size()) { Ui::showMediaPreview( - (*_srows)[_down]->stickerSetOrigin(), - (*_srows)[_down]); + (*_srows)[_down].document->stickerSetOrigin(), + (*_srows)[_down].document); _previewShown = true; } } diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 36af635cb..071b628ae 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "ui/effects/animations.h" -#include "ui/twidget.h" +#include "ui/rp_widget.h" #include "base/timer.h" #include "chat_helpers/stickers.h" @@ -16,22 +16,33 @@ namespace Ui { class ScrollArea; } // namespace Ui +namespace Lottie { +class SinglePlayer; +class FrameRenderer; +} // namespace Lottie; + namespace internal { +struct StickerSuggestion { + not_null document; + std::unique_ptr animated; +}; + using MentionRows = QList; using HashtagRows = QList; using BotCommandRows = QList>; -using StickerRows = std::vector>; +using StickerRows = std::vector; class FieldAutocompleteInner; } // namespace internal -class FieldAutocomplete final : public TWidget { +class FieldAutocomplete final : public Ui::RpWidget { Q_OBJECT public: FieldAutocomplete(QWidget *parent); + ~FieldAutocomplete(); bool clearFilteredBotCommands(); void showFiltered( @@ -70,8 +81,6 @@ public: void hideFast(); - ~FieldAutocomplete(); - signals: void mentionChosen(UserData *user, FieldAutocomplete::ChooseMethod method) const; void hashtagChosen(QString hashtag, FieldAutocomplete::ChooseMethod method) const; @@ -93,6 +102,7 @@ private: void updateFiltered(bool resetScroll = false); void recount(bool resetScroll = false); + internal::StickerRows getStickerSuggestions(); QPixmap _cache; internal::MentionRows _mrows; @@ -100,7 +110,12 @@ private: internal::BotCommandRows _brows; internal::StickerRows _srows; - void rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const internal::StickerRows &srows, bool resetScroll); + void rowsUpdated( + internal::MentionRows &&mrows, + internal::HashtagRows &&hrows, + internal::BotCommandRows &&brows, + internal::StickerRows &&srows, + bool resetScroll); object_ptr _scroll; QPointer _inner; @@ -132,17 +147,25 @@ private: namespace internal { -class FieldAutocompleteInner final : public TWidget, private base::Subscriber { +class FieldAutocompleteInner final + : public Ui::RpWidget + , private base::Subscriber { Q_OBJECT public: - FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerRows *srows); + FieldAutocompleteInner( + not_null parent, + not_null mrows, + not_null hrows, + not_null brows, + not_null srows); void clearSel(bool hidden = false); bool moveSel(int key); bool chooseSelected(FieldAutocomplete::ChooseMethod method) const; void setRecentInlineBotsInRows(int32 bots); + void rowsUpdated(); signals: void mentionChosen(UserData *user, FieldAutocomplete::ChooseMethod method) const; @@ -170,11 +193,17 @@ private: void showPreview(); void selectByMouse(QPoint global); - FieldAutocomplete *_parent = nullptr; - MentionRows *_mrows = nullptr; - HashtagRows *_hrows = nullptr; - BotCommandRows *_brows = nullptr; - StickerRows *_srows = nullptr; + void setupLottie(StickerSuggestion &suggestion); + void repaintSticker(not_null document); + std::shared_ptr getLottieRenderer(); + + not_null _parent; + not_null _mrows; + not_null _hrows; + not_null _brows; + not_null _srows; + rpl::lifetime _stickersLifetime; + std::weak_ptr _lottieRenderer; int _stickersPerRow = 1; int _recentInlineBotsInRows = 0; int _sel = -1; diff --git a/Telegram/SourceFiles/chat_helpers/stickers.cpp b/Telegram/SourceFiles/chat_helpers/stickers.cpp index aa96392f4..ff026da6a 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers.cpp @@ -1154,10 +1154,11 @@ auto LottieFromDocument( std::unique_ptr LottiePlayerFromDocument( not_null document, LottieSize sizeTag, - QSize box) { - const auto method = [](auto &&...args) { + QSize box, + std::shared_ptr renderer) { + const auto method = [&](auto &&...args) { return std::make_unique( - std::forward(args)...); + std::forward(args)..., std::move(renderer)); }; return LottieFromDocument(method, document, sizeTag, box); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers.h b/Telegram/SourceFiles/chat_helpers/stickers.h index d2d0bc9d0..993635b86 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers.h +++ b/Telegram/SourceFiles/chat_helpers/stickers.h @@ -132,7 +132,8 @@ enum class LottieSize : uchar { [[nodiscard]] std::unique_ptr LottiePlayerFromDocument( not_null document, LottieSize sizeTag, - QSize box); + QSize box, + std::shared_ptr renderer = nullptr); [[nodiscard]] not_null LottieAnimationFromDocument( not_null player, not_null document, diff --git a/Telegram/SourceFiles/lottie/lottie_animation.cpp b/Telegram/SourceFiles/lottie/lottie_animation.cpp index ad5c1bda3..d8e8b34ec 100644 --- a/Telegram/SourceFiles/lottie/lottie_animation.cpp +++ b/Telegram/SourceFiles/lottie/lottie_animation.cpp @@ -129,6 +129,10 @@ std::unique_ptr CreateFromContent( } // namespace details +std::shared_ptr MakeFrameRenderer() { + return FrameRenderer::CreateIndependent(); +} + QImage ReadThumbnail(const QByteArray &content) { return Init(content, FrameRequest()).match([]( const std::unique_ptr &state) { diff --git a/Telegram/SourceFiles/lottie/lottie_animation.h b/Telegram/SourceFiles/lottie/lottie_animation.h index 178b98a7c..0e3c4b185 100644 --- a/Telegram/SourceFiles/lottie/lottie_animation.h +++ b/Telegram/SourceFiles/lottie/lottie_animation.h @@ -22,6 +22,9 @@ namespace Lottie { class Player; class SharedState; +class FrameRenderer; + +std::shared_ptr MakeFrameRenderer(); QImage ReadThumbnail(const QByteArray &content); diff --git a/Telegram/SourceFiles/lottie/lottie_multi_player.cpp b/Telegram/SourceFiles/lottie/lottie_multi_player.cpp index c46bff833..3620d1417 100644 --- a/Telegram/SourceFiles/lottie/lottie_multi_player.cpp +++ b/Telegram/SourceFiles/lottie/lottie_multi_player.cpp @@ -15,10 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Lottie { -std::shared_ptr MakeFrameRenderer() { - return FrameRenderer::CreateIndependent(); -} - MultiPlayer::MultiPlayer(std::shared_ptr renderer) : _timer([=] { checkNextFrameRender(); }) , _renderer(renderer ? std::move(renderer) : FrameRenderer::Instance()) { diff --git a/Telegram/SourceFiles/lottie/lottie_multi_player.h b/Telegram/SourceFiles/lottie/lottie_multi_player.h index ba7e7818e..849f28cb8 100644 --- a/Telegram/SourceFiles/lottie/lottie_multi_player.h +++ b/Telegram/SourceFiles/lottie/lottie_multi_player.h @@ -27,8 +27,6 @@ struct MultiUpdate { // std::pair> data; }; -std::shared_ptr MakeFrameRenderer(); - class MultiPlayer final : public Player { public: explicit MultiPlayer(std::shared_ptr renderer = nullptr);