From 6db3a0ec98d70eb269e0943f2fc23d5f45ddf75d Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Jun 2022 17:13:20 +0400 Subject: [PATCH] Support optimized lottie emoji. --- .../chat_helpers/stickers_lottie.cpp | 9 +- .../chat_helpers/stickers_lottie.h | 5 +- .../data/stickers/data_custom_emoji.cpp | 414 +++++++++++++----- .../data/stickers/data_custom_emoji.h | 23 +- .../history/view/history_view_message.cpp | 22 +- .../ui/text/custom_emoji_instance.cpp | 403 ++++++++++++++--- .../ui/text/custom_emoji_instance.h | 163 +++++-- .../window/window_session_controller.cpp | 1 + Telegram/lib_lottie | 2 +- Telegram/lib_ui | 2 +- 10 files changed, 831 insertions(+), 213 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp index b4f16f34b..9e3d23ce2 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp @@ -27,6 +27,10 @@ constexpr auto kDontCacheLottieAfterArea = 512 * 512; } // namespace +uint8 LottieCacheKeyShift(uint8 replacementsTag, StickerLottieSize sizeTag) { + return ((replacementsTag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F); +} + template auto LottieCachedFromContent( Method &&method, @@ -115,8 +119,9 @@ std::unique_ptr LottiePlayerFromDocument( replacements, std::move(renderer)); }; - const auto tag = replacements ? replacements->tag : uint8(0); - const auto keyShift = ((tag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F); + const auto keyShift = LottieCacheKeyShift( + replacements ? replacements->tag : uint8(0), + sizeTag); return LottieFromDocument(method, media, uint8(keyShift), box); } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h index c5fd606a0..37d499842 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_lottie.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_lottie.h @@ -49,7 +49,7 @@ using StickersSetFlags = base::flags; namespace ChatHelpers { -enum class StickerLottieSize : uchar { +enum class StickerLottieSize : uint8 { MessageHistory, StickerSet, StickersPanel, @@ -66,6 +66,9 @@ enum class StickerLottieSize : uchar { EmojiInteractionReserved7, PremiumReactionPreview, }; +[[nodiscard]] uint8 LottieCacheKeyShift( + uint8 replacementsTag, + StickerLottieSize sizeTag); [[nodiscard]] std::unique_ptr LottiePlayerFromDocument( not_null media, diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index e1f6a2551..782c5b561 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -15,9 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_file_origin.h" #include "data/stickers/data_stickers_set.h" #include "lottie/lottie_common.h" -#include "lottie/lottie_single_player.h" +#include "lottie/lottie_emoji.h" #include "chat_helpers/stickers_lottie.h" #include "ui/text/text_block.h" +#include "ui/ui_utility.h" #include "apiwrap.h" namespace Data { @@ -29,6 +30,25 @@ struct CustomEmojiId { namespace { +using SizeTag = CustomEmojiManager::SizeTag; + +[[nodiscard]] ChatHelpers::StickerLottieSize LottieSizeFromTag(SizeTag tag) { + using LottieSize = ChatHelpers::StickerLottieSize; + switch (tag) { + case SizeTag::Normal: return LottieSize::MessageHistory; + case SizeTag::Large: return LottieSize::EmojiInteraction; + } + Unexpected("SizeTag value in CustomEmojiManager-LottieSizeFromTag."); +} + +[[nodiscard]] int SizeFromTag(SizeTag tag) { + switch (tag) { + case SizeTag::Normal: return Ui::Emoji::GetSizeNormal(); + case SizeTag::Large: return Ui::Emoji::GetSizeLarge(); + } + Unexpected("SizeTag value in CustomEmojiManager-SizeFromTag."); +} + [[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) { return QString::number(id.id) + '@' @@ -37,6 +57,15 @@ namespace { + QString::number(id.set.accessHash); } +[[nodiscard]] QString SerializeCustomEmojiId( + not_null document) { + const auto sticker = document->sticker(); + return SerializeCustomEmojiId({ + sticker ? sticker->set : StickerSetIdentifier(), + document->id, + }); +} + [[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) { const auto parts = data.split('@'); if (parts.size() != 2) { @@ -56,120 +85,83 @@ namespace { }; } -class CustomEmojiWithData { -public: - explicit CustomEmojiWithData(const QString &data); - - QString entityData(); - -private: - const QString _data; - -}; - -CustomEmojiWithData::CustomEmojiWithData(const QString &data) : _data(data) { -} - -QString CustomEmojiWithData::entityData() { - return _data; -} - -class DocumentCustomEmoji final : public CustomEmojiWithData { -public: - DocumentCustomEmoji( - const QString &data, - not_null document, - Fn update); - - void paint(QPainter &p, int x, int y, const QColor &preview); - -private: - not_null _document; - std::shared_ptr _media; - std::unique_ptr _lottie; - Fn _update; - rpl::lifetime _lifetime; - -}; - -DocumentCustomEmoji::DocumentCustomEmoji( - const QString &data, - not_null document, - Fn update) -: CustomEmojiWithData(data) -, _document(document) -, _update(update) { -} - -void DocumentCustomEmoji::paint(QPainter &p, int x, int y, const QColor &preview) { - if (!_media) { - _media = _document->createMediaView(); - _media->automaticLoad(_document->stickerSetOrigin(), nullptr); - } - if (_media->loaded() && !_lottie) { - const auto size = Ui::Emoji::GetSizeNormal(); - _lottie = ChatHelpers::LottiePlayerFromDocument( - _media.get(), - nullptr, - ChatHelpers::StickerLottieSize::MessageHistory, - QSize(size, size), - Lottie::Quality::High); - _lottie->updates() | rpl::start_with_next(_update, _lifetime); - } - if (_lottie && _lottie->ready()) { - const auto frame = _lottie->frame(); - p.drawImage( - QRect( - x, - y, - frame.width() / frame.devicePixelRatio(), - frame.height() / frame.devicePixelRatio()), - frame); - _lottie->markFrameShown(); - } -} - } // namespace class CustomEmojiLoader final : public Ui::CustomEmoji::Loader , public base::has_weak_ptr { public: - CustomEmojiLoader(not_null owner, const CustomEmojiId id); + CustomEmojiLoader( + not_null owner, + const CustomEmojiId id, + SizeTag tag); + CustomEmojiLoader(not_null document, SizeTag tag); [[nodiscard]] bool resolving() const; void resolved(not_null document); - void load(Fn ready) override; + QString entityData() override; + + void load(Fn loaded) override; + bool loading() override; void cancel() override; Ui::CustomEmoji::Preview preview() override; private: struct Resolve { - Fn requested; + Fn requested; + QString entityData; }; struct Process { std::shared_ptr media; - Fn callback; + Fn loaded; + base::has_weak_ptr guard; rpl::lifetime lifetime; }; - struct Load { + struct Requested { not_null document; std::unique_ptr process; }; + struct Lookup : Requested { + }; + struct Load : Requested { + }; - [[nodiscard]] static std::variant InitialState( + void check(); + [[nodiscard]] Storage::Cache::Key cacheKey( + not_null document) const; + void startCacheLookup( + not_null lookup, + Fn loaded); + void lookupDone( + not_null lookup, + std::optional result); + void loadNoCache( + not_null document, + Fn loaded); + + [[nodiscard]] static std::variant InitialState( not_null owner, const CustomEmojiId &id); - std::variant _state; + std::variant _state; + SizeTag _tag = SizeTag::Normal; }; CustomEmojiLoader::CustomEmojiLoader( not_null owner, - const CustomEmojiId id) -: _state(InitialState(owner, id)) { + const CustomEmojiId id, + SizeTag tag) +: _state(InitialState(owner, id)) +, _tag(tag) { +} + +CustomEmojiLoader::CustomEmojiLoader( + not_null document, + SizeTag tag) +: _state(Lookup{ document }) +, _tag(tag) { } bool CustomEmojiLoader::resolving() const { @@ -180,41 +172,179 @@ void CustomEmojiLoader::resolved(not_null document) { Expects(resolving()); auto requested = std::move(v::get(_state).requested); - _state = Load{ document }; + _state = Lookup{ document }; if (requested) { load(std::move(requested)); } } -void CustomEmojiLoader::load(Fn ready) { +void CustomEmojiLoader::load(Fn loaded) { if (const auto resolve = std::get_if(&_state)) { - resolve->requested = std::move(ready); + resolve->requested = std::move(loaded); + } else if (const auto lookup = std::get_if(&_state)) { + if (!lookup->process) { + startCacheLookup(lookup, std::move(loaded)); + } else { + lookup->process->loaded = std::move(loaded); + } } else if (const auto load = std::get_if(&_state)) { if (!load->process) { load->process = std::make_unique(Process{ .media = load->document->createMediaView(), - .callback = std::move(ready), + .loaded = std::move(loaded), }); load->process->media->checkStickerLarge(); + if (load->process->media->loaded()) { + check(); + } else { + load->document->session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + check(); + }, load->process->lifetime); + } } else { - load->process->callback = std::move(ready); + load->process->loaded = std::move(loaded); } } } +QString CustomEmojiLoader::entityData() { + if (const auto resolve = std::get_if(&_state)) { + return resolve->entityData; + } else if (const auto lookup = std::get_if(&_state)) { + return SerializeCustomEmojiId(lookup->document); + } else if (const auto load = std::get_if(&_state)) { + return SerializeCustomEmojiId(load->document); + } + Unexpected("State in CustomEmojiLoader::entityData."); +} + +bool CustomEmojiLoader::loading() { + if (const auto resolve = std::get_if(&_state)) { + return (resolve->requested != nullptr); + } else if (const auto lookup = std::get_if(&_state)) { + return (lookup->process != nullptr); + } else if (const auto load = std::get_if(&_state)) { + return (load->process != nullptr); + } + return false; +} + +Storage::Cache::Key CustomEmojiLoader::cacheKey( + not_null document) const { + const auto baseKey = document->bigFileBaseCacheKey(); + if (!baseKey) { + return {}; + } + return Storage::Cache::Key{ + baseKey.high, + baseKey.low + ChatHelpers::LottieCacheKeyShift( + 0x0F, + LottieSizeFromTag(_tag)), + }; +} + +void CustomEmojiLoader::startCacheLookup( + not_null lookup, + Fn loaded) { + const auto document = lookup->document; + const auto key = cacheKey(document); + if (!key) { + loadNoCache(document, std::move(loaded)); + return; + } + lookup->process = std::make_unique(Process{ + .loaded = std::move(loaded), + }); + const auto weak = base::make_weak(&lookup->process->guard); + document->owner().cacheBigFile().get(key, [=](QByteArray value) { + auto cache = Ui::CustomEmoji::Cache::FromSerialized(value); + crl::on_main(weak, [=, result = std::move(cache)]() mutable { + lookupDone(lookup, std::move(result)); + }); + }); +} + +void CustomEmojiLoader::lookupDone( + not_null lookup, + std::optional result) { + const auto document = lookup->document; + if (!result) { + loadNoCache(document, std::move(lookup->process->loaded)); + return; + } + const auto tag = _tag; + auto loader = [=] { + return std::make_unique(document, tag); + }; + lookup->process->loaded(Ui::CustomEmoji::Cached( + SerializeCustomEmojiId(document), + std::move(loader), + std::move(*result))); +} + +void CustomEmojiLoader::loadNoCache( + not_null document, + Fn loaded) { + _state = Load{ document }; + load(std::move(loaded)); +} + +void CustomEmojiLoader::check() { + using namespace Ui::CustomEmoji; + + const auto load = std::get_if(&_state); + Assert(load != nullptr); + Assert(load->process != nullptr); + + const auto media = load->process->media.get(); + const auto document = media->owner(); + const auto data = media->bytes(); + const auto filepath = document->filepath(); + if (data.isEmpty() && filepath.isEmpty()) { + return; + } + load->process->lifetime.destroy(); + + const auto tag = _tag; + const auto size = SizeFromTag(_tag); + auto bytes = Lottie::ReadContent(data, filepath); + auto loader = [=] { + return std::make_unique(document, tag); + }; + auto put = [=, key = cacheKey(document)](QByteArray value) { + document->owner().cacheBigFile().put(key, std::move(value)); + }; + auto generator = [=, bytes = Lottie::ReadContent(data, filepath)]() { + return std::make_unique(bytes); + }; + auto renderer = std::make_unique(RendererDescriptor{ + .generator = std::move(generator), + .put = std::move(put), + .loader = std::move(loader), + .size = SizeFromTag(_tag), + }); + base::take(load->process)->loaded(Caching{ + std::move(renderer), + SerializeCustomEmojiId(document), + }); +} + auto CustomEmojiLoader::InitialState( not_null owner, const CustomEmojiId &id) --> std::variant { +-> std::variant { const auto document = owner->document(id.id); if (!document->isNull()) { - return Load{ document }; + return Lookup{ document }; } return Resolve(); } void CustomEmojiLoader::cancel() { - if (const auto load = std::get_if(&_state)) { + if (const auto lookup = std::get_if(&_state)) { + base::take(lookup->process); + } else if (const auto load = std::get_if(&_state)) { if (base::take(load->process)) { load->document->cancel(); } @@ -222,22 +352,28 @@ void CustomEmojiLoader::cancel() { } Ui::CustomEmoji::Preview CustomEmojiLoader::preview() { - if (const auto load = std::get_if(&_state)) { - if (const auto process = load->process.get()) { - const auto dimensions = load->document->dimensions; - if (!dimensions.width()) { - return {}; - } - const auto scale = (Ui::Emoji::GetSizeNormal() * 1.) - / (style::DevicePixelRatio() * dimensions.width()); - return { process->media->thumbnailPath(), scale }; + using Preview = Ui::CustomEmoji::Preview; + const auto make = [&](not_null document) -> Preview { + const auto dimensions = document->dimensions; + if (!document->inlineThumbnailIsPath() + || !dimensions.width()) { + return {}; } + const auto scale = (SizeFromTag(_tag) * 1.) + / (style::DevicePixelRatio() * dimensions.width()); + return { document->createMediaView()->thumbnailPath(), scale }; + }; + if (const auto lookup = std::get_if(&_state)) { + return make(lookup->document); + } else if (const auto load = std::get_if(&_state)) { + return make(load->document); } return {}; } CustomEmojiManager::CustomEmojiManager(not_null owner) -: _owner(owner) { +: _owner(owner) +, _repaintTimer([=] { invokeRepaints(); }) { } CustomEmojiManager::~CustomEmojiManager() = default; @@ -256,20 +392,30 @@ std::unique_ptr CustomEmojiManager::create( auto j = i->second.find(parsed.id); if (j == end(i->second)) { using Loading = Ui::CustomEmoji::Loading; - auto loader = std::make_unique(_owner, parsed); + auto loader = std::make_unique( + _owner, + parsed, + SizeTag::Normal); if (loader->resolving()) { _loaders[parsed.id].push_back(base::make_weak(loader.get())); } + const auto repaint = [=]( + not_null instance, + Ui::CustomEmoji::RepaintRequest request) { + repaintLater(instance, request); + }; j = i->second.emplace( parsed.id, - std::make_unique(data, Loading{ + std::make_unique(Loading{ std::move(loader), Ui::CustomEmoji::Preview() - })).first; + }, std::move(repaint))).first; } requestSetIfNeeded(parsed); - return std::make_unique(j->second.get()); + return std::make_unique( + j->second.get(), + std::move(update)); } void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) { @@ -322,6 +468,64 @@ void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) { }).send(); } +void CustomEmojiManager::repaintLater( + not_null instance, + Ui::CustomEmoji::RepaintRequest request) { + auto &bunch = _repaints[request.duration]; + if (bunch.when < request.when) { + bunch.when = request.when; + } + bunch.instances.emplace_back(instance); + scheduleRepaintTimer(); +} + +void CustomEmojiManager::scheduleRepaintTimer() { + if (_repaintTimerScheduled) { + return; + } + _repaintTimerScheduled = true; + Ui::PostponeCall(this, [=] { + _repaintTimerScheduled = false; + + auto next = crl::time(); + for (const auto &[duration, bunch] : _repaints) { + if (!next || next > bunch.when) { + next = bunch.when; + } + } + if (next && (!_repaintNext || _repaintNext > next)) { + const auto now = crl::now(); + if (now >= next) { + _repaintNext = 0; + _repaintTimer.cancel(); + invokeRepaints(); + } else { + _repaintNext = next; + _repaintTimer.callOnce(next - now); + } + } + }); +} + +void CustomEmojiManager::invokeRepaints() { + _repaintNext = 0; + const auto now = crl::now(); + for (auto i = begin(_repaints); i != end(_repaints);) { + if (i->second.when > now) { + ++i; + continue; + } + auto bunch = std::move(i->second); + i = _repaints.erase(i); + for (const auto &weak : bunch.instances) { + if (const auto strong = weak.get()) { + strong->repaint(); + } + } + } + scheduleRepaintTimer(); +} + Main::Session &CustomEmojiManager::session() const { return _owner->session(); } diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index 9d2c76cdb..9b8bcbde4 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "ui/text/custom_emoji_instance.h" +#include "base/timer.h" +#include "base/weak_ptr.h" struct StickerSetIdentifier; @@ -21,8 +23,13 @@ class Session; struct CustomEmojiId; class CustomEmojiLoader; -class CustomEmojiManager final { +class CustomEmojiManager final : public base::has_weak_ptr { public: + enum class SizeTag { + Normal, + Large, + }; + CustomEmojiManager(not_null owner); ~CustomEmojiManager(); @@ -40,8 +47,17 @@ private: base::flat_set documents; base::flat_set waiting; }; + struct RepaintBunch { + crl::time when = 0; + std::vector> instances; + }; void requestSetIfNeeded(const CustomEmojiId &id); + void repaintLater( + not_null instance, + Ui::CustomEmoji::RepaintRequest request); + void scheduleRepaintTimer(); + void invokeRepaints(); const not_null _owner; @@ -53,6 +69,11 @@ private: uint64, std::vector>> _loaders; + base::flat_map _repaints; + crl::time _repaintNext = 0; + base::Timer _repaintTimer; + bool _repaintTimerScheduled = false; + }; void FillTestCustomEmoji( diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 4066e0c4c..d41f5352a 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -275,8 +275,12 @@ Message::Message( } Message::~Message() { - if (_comments) { + if (_comments || _heavyCustomEmoji) { _comments = nullptr; + if (_heavyCustomEmoji) { + _heavyCustomEmoji = false; + message()->_text.unloadCustomEmoji(); + } checkHeavyPart(); } } @@ -1241,8 +1245,20 @@ void Message::paintText( const auto stm = context.messageStyle(); p.setPen(stm->historyTextFg); p.setFont(st::msgFont); - item->_text.draw(p, trect.x(), trect.y(), trect.width(), style::al_left, 0, -1, context.selection); - if (!_heavyCustomEmoji && item->_text.hasCustomEmoji()) { + const auto custom = item->_text.hasCustomEmoji(); + if (custom) { + p.setInactive(delegate()->elementIsGifPaused()); + } + item->_text.draw( + p, + trect.x(), + trect.y(), + trect.width(), + style::al_left, + 0, + -1, + context.selection); + if (!_heavyCustomEmoji && custom) { _heavyCustomEmoji = true; history()->owner().registerHeavyViewPart(const_cast(this)); } diff --git a/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp b/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp index c4491c37b..18b18917d 100644 --- a/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp +++ b/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp @@ -7,9 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/text/custom_emoji_instance.h" +#include "ui/effects/frame_generator.h" + +#include + class QPainter; namespace Ui::CustomEmoji { +namespace { + +constexpr auto kMaxFrameDuration = 86400 * crl::time(1000); + +struct CacheHelper { + int version = 0; + int size = 0; + int frames = 0; + int length = 0; +}; + +} // namespace Preview::Preview(QPainterPath path, float64 scale) : _data(ScaledPath{ std::move(path), scale }) { @@ -26,6 +42,10 @@ void Preview::paint(QPainter &p, int x, int y, const QColor &preview) { } } +bool Preview::image() const { + return v::is(_data); +} + void Preview::paintPath( QPainter &p, int x, @@ -52,7 +72,15 @@ void Preview::paintPath( } } -Cache::Cache(QSize size) : _size(size) { +Cache::Cache(int size) : _size(size) { +} + +std::optional Cache::FromSerialized(const QByteArray &serialized) { + return {}; +} + +QByteArray Cache::serialize() { + return {}; } int Cache::frames() const { @@ -60,53 +88,247 @@ int Cache::frames() const { } QImage Cache::frame(int index) const { - return QImage(); + Expects(index < _frames); + + const auto row = index / kPerRow; + const auto inrow = index % kPerRow; + const auto bytes = _bytes[row].data() + inrow * frameByteSize(); + const auto data = reinterpret_cast(bytes); + return QImage(data, _size, _size, QImage::Format_ARGB32_Premultiplied); +} + +int Cache::size() const { + return _size; +} + +Preview Cache::makePreview() const { + Expects(_frames > 0); + + auto image = frame(0); + image.detach(); + return { std::move(image) }; } void Cache::reserve(int frames) { + const auto rows = (frames + kPerRow - 1) / kPerRow; + if (const auto add = rows - int(_bytes.size()); add > 0) { + _bytes.resize(rows); + for (auto e = end(_bytes), i = e - add; i != e; ++i) { + i->resize(kPerRow * frameByteSize()); + } + } + _durations.reserve(frames); } -Cached::Cached(std::unique_ptr unloader, Cache cache) -: _unloader(std::move(unloader)) -, _cache(cache) { +int Cache::frameRowByteSize() const { + return _size * 4; } -void Cached::paint(QPainter &p, int x, int y) { - p.drawImage(x, y, _cache.frame(0)); +int Cache::frameByteSize() const { + return _size * frameRowByteSize(); } -Loading Cached::unload() { - return Loading(_unloader->unload(), Preview(_cache.frame(0))); +void Cache::add(crl::time duration, const QImage &frame) { + Expects(duration < kMaxFrameDuration); + Expects(frame.size() == QSize(_size, _size)); + Expects(frame.format() == QImage::Format_ARGB32_Premultiplied); + + const auto rowSize = frameRowByteSize(); + const auto frameSize = frameByteSize(); + const auto row = (_frames / kPerRow); + const auto inrow = (_frames % kPerRow); + const auto rows = row + 1; + while (_bytes.size() < rows) { + _bytes.emplace_back(); + _bytes.back().resize(kPerRow * frameSize); + } + const auto perLine = frame.bytesPerLine(); + auto dst = _bytes[row].data() + inrow * frameSize; + auto src = frame.constBits(); + for (auto y = 0; y != _size; ++y) { + memcpy(dst, src, rowSize); + dst += rowSize; + src += perLine; + } + ++_frames; + _durations.push_back(duration); } -void Cacher::reserve(int frames) { - _cache.reserve(frames); -} - -void Cacher::add(crl::time duration, QImage frame) { -} - -Cache Cacher::takeCache() { - return std::move(_cache); -} - -Caching::Caching(std::unique_ptr cacher, Preview preview) -: _cacher(std::move(cacher)) -, _preview(std::move(preview)) { -} - -void Caching::paint(QPainter &p, int x, int y, const QColor &preview) { - if (!_cacher->paint(p, x, y)) { - _preview.paint(p, x, y, preview); +void Cache::finish() { + _finished = true; + if (_frame == _frames) { + _frame = 0; } } -std::optional Caching::ready() { - return _cacher->ready(); +PaintFrameResult Cache::paintCurrentFrame( + QPainter &p, + int x, + int y, + crl::time now) { + if (!_frames) { + return {}; + } + const auto finishes = now ? currentFrameFinishes() : 0; + if (finishes && now >= finishes) { + ++_frame; + if (_finished && _frame == _frames) { + _frame = 0; + } + _shown = now; + } else if (!_shown) { + _shown = now; + } + p.drawImage( + QRect(x, y, _size, _size), + frame(std::min(_frame, _frames - 1))); + const auto next = currentFrameFinishes(); + const auto duration = next ? (next - _shown) : 0; + return { + .painted = true, + .next = currentFrameFinishes(), + .duration = duration, + }; } -Loading Caching::cancel() { - return Loading(_cacher->cancel(), std::move(_preview)); +int Cache::currentFrame() const { + return _frame; +} + +crl::time Cache::currentFrameFinishes() const { + if (!_shown || _frame >= _durations.size()) { + return 0; + } else if (const auto duration = _durations[_frame]) { + return _shown + duration; + } + return 0; +} + +Cached::Cached( + const QString &entityData, + Fn()> unloader, + Cache cache) +: _unloader(std::move(unloader)) +, _cache(std::move(cache)) +, _entityData(entityData) { +} + +QString Cached::entityData() const { + return _entityData; +} + +PaintFrameResult Cached::paint(QPainter &p, int x, int y, crl::time now) { + return _cache.paintCurrentFrame(p, x, y, now); +} + +Loading Cached::unload() { + return Loading(_unloader(), _cache.makePreview()); +} + +Renderer::Renderer(RendererDescriptor &&descriptor) +: _cache(descriptor.size) +, _put(std::move(descriptor.put)) +, _loader(std::move(descriptor.loader)) { + Expects(_loader != nullptr); + + const auto size = _cache.size(); + const auto guard = base::make_weak(this); + crl::async([=, factory = std::move(descriptor.generator)]() mutable { + auto generator = factory(); + auto rendered = generator->renderNext( + QImage(), + QSize(size, size) * style::DevicePixelRatio(), + Qt::KeepAspectRatio); + if (rendered.image.isNull()) { + return; + } + crl::on_main(guard, [ + =, + frame = std::move(rendered), + generator = std::move(generator) + ]() mutable { + frameReady( + std::move(generator), + frame.duration, + std::move(frame.image)); + }); + }); +} + +void Renderer::frameReady( + std::unique_ptr generator, + crl::time duration, + QImage frame) { + if (frame.isNull()) { + finish(); + return; + } + if (const auto count = generator->count()) { + if (!_cache.frames()) { + _cache.reserve(count); + } + } + const auto explicitRepaint = (_cache.frames() == _cache.currentFrame()); + _cache.add(duration, frame); + const auto size = _cache.size(); + const auto guard = base::make_weak(this); + crl::async([ + =, + frame = std::move(frame), + generator = std::move(generator) + ]() mutable { + auto rendered = generator->renderNext( + std::move(frame), + QSize(size, size) * style::DevicePixelRatio(), + Qt::KeepAspectRatio); + crl::on_main(guard, [ + =, + frame = std::move(rendered), + generator = std::move(generator) + ]() mutable { + frameReady( + std::move(generator), + frame.duration, + std::move(frame.image)); + }); + }); + if (explicitRepaint && _repaint) { + _repaint(); + } +} + +void Renderer::finish() { + _finished = true; + _cache.finish(); + if (_put) { + _put(_cache.serialize()); + } +} + +PaintFrameResult Renderer::paint(QPainter &p, int x, int y, crl::time now) { + return _cache.paintCurrentFrame(p, x, y, now); +} + +std::optional Renderer::ready(const QString &entityData) { + return _finished + ? Cached{ entityData, std::move(_loader), std::move(_cache) } + : std::optional(); +} + +std::unique_ptr Renderer::cancel() { + return _loader(); +} + +Preview Renderer::makePreview() const { + return _cache.makePreview(); +} + +void Renderer::setRepaintCallback(Fn repaint) { + _repaint = std::move(repaint); +} + +Cache Renderer::takeCache() { + return std::move(_cache); } Loading::Loading(std::unique_ptr loader, Preview preview) @@ -114,8 +336,24 @@ Loading::Loading(std::unique_ptr loader, Preview preview) , _preview(std::move(preview)) { } -void Loading::load(Fn done) { - _loader->load(crl::guard(this, std::move(done))); +QString Loading::entityData() const { + return _loader->entityData(); +} + +void Loading::load(Fn done) { + _loader->load(crl::guard(this, [this, done = std::move(done)]( + Loader::LoadResult result) mutable { + if (const auto caching = std::get_if(&result)) { + caching->preview = _preview + ? std::move(_preview) + : _loader->preview(); + } + done(std::move(result)); + })); +} + +bool Loading::loading() const { + return _loader->loading(); } void Loading::paint(QPainter &p, int x, int y, const QColor &preview) { @@ -132,52 +370,97 @@ void Loading::cancel() { invalidate_weak_ptrs(this); } -Instance::Instance(const QString &entityData, Loading loading) +Instance::Instance( + Loading loading, + Fn, RepaintRequest)> repaintLater) : _state(std::move(loading)) -, _entityData(entityData) { +, _repaintLater(std::move(repaintLater)) { } QString Instance::entityData() const { - return _entityData; + if (const auto loading = std::get_if(&_state)) { + return loading->entityData(); + } else if (const auto caching = std::get_if(&_state)) { + return caching->entityData; + } else if (const auto cached = std::get_if(&_state)) { + return cached->entityData(); + } + Unexpected("State in Instance::entityData."); } -void Instance::paint(QPainter &p, int x, int y, const QColor &preview) { +void Instance::paint( + QPainter &p, + int x, + int y, + crl::time now, + const QColor &preview, + bool paused) { if (const auto loading = std::get_if(&_state)) { loading->paint(p, x, y, preview); - loading->load([=](Caching caching) { - _state = std::move(caching); + loading->load([=](Loader::LoadResult result) { + if (auto caching = std::get_if(&result)) { + caching->renderer->setRepaintCallback([=] { repaint(); }); + _state = std::move(*caching); + } else if (auto cached = std::get_if(&result)) { + _state = std::move(*cached); + } else { + Unexpected("Value in Loader::LoadResult."); + } }); } else if (const auto caching = std::get_if(&_state)) { - caching->paint(p, x, y, preview); - if (auto cached = caching->ready()) { + auto result = caching->renderer->paint(p, x, y, paused ? 0 : now); + if (!result.painted) { + caching->preview.paint(p, x, y, preview); + } else { + if (!caching->preview.image()) { + caching->preview = caching->renderer->makePreview(); + } + if (result.next > now) { + _repaintLater(this, { result.next, result.duration }); + } + } + if (auto cached = caching->renderer->ready(caching->entityData)) { _state = std::move(*cached); } } else if (const auto cached = std::get_if(&_state)) { - cached->paint(p, x, y); + const auto result = cached->paint(p, x, y, paused ? 0 : now); + if (result.next > now) { + _repaintLater(this, { result.next, result.duration }); + } } } -void Instance::incrementUsage() { - ++_usage; +void Instance::repaint() { + for (const auto &object : _usage) { + object->repaint(); + } } -void Instance::decrementUsage() { - Expects(_usage > 0); +void Instance::incrementUsage(not_null object) { + _usage.emplace(object); +} - if (--_usage > 0) { +void Instance::decrementUsage(not_null object) { + _usage.remove(object); + if (!_usage.empty()) { return; } if (const auto loading = std::get_if(&_state)) { loading->cancel(); } else if (const auto caching = std::get_if(&_state)) { - _state = caching->cancel(); + _state = Loading{ + caching->renderer->cancel(), + std::move(caching->preview), + }; } else if (const auto cached = std::get_if(&_state)) { _state = cached->unload(); } + _repaintLater(this, RepaintRequest()); } -Object::Object(not_null instance) -: _instance(instance) { +Object::Object(not_null instance, Fn repaint) +: _instance(instance) +, _repaint(std::move(repaint)) { } Object::~Object() { @@ -188,19 +471,29 @@ QString Object::entityData() { return _instance->entityData(); } -void Object::paint(QPainter &p, int x, int y, const QColor &preview) { +void Object::paint( + QPainter &p, + int x, + int y, + crl::time now, + const QColor &preview, + bool paused) { if (!_using) { _using = true; - _instance->incrementUsage(); + _instance->incrementUsage(this); } - _instance->paint(p, x, y, preview); + _instance->paint(p, x, y, now, preview, paused); } void Object::unload() { if (_using) { _using = false; - _instance->decrementUsage(); + _instance->decrementUsage(this); } } +void Object::repaint() { + _repaint(); +} + } // namespace Ui::CustomEmoji diff --git a/Telegram/SourceFiles/ui/text/custom_emoji_instance.h b/Telegram/SourceFiles/ui/text/custom_emoji_instance.h index 2d917b94c..c708dfca7 100644 --- a/Telegram/SourceFiles/ui/text/custom_emoji_instance.h +++ b/Telegram/SourceFiles/ui/text/custom_emoji_instance.h @@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class QColor; class QPainter; +namespace Ui { +class FrameGenerator; +} // namespace Ui + namespace Ui::CustomEmoji { class Preview final { @@ -23,6 +27,7 @@ public: Preview(QPainterPath path, float64 scale); void paint(QPainter &p, int x, int y, const QColor &preview); + [[nodiscard]] bool image() const; [[nodiscard]] explicit operator bool() const { return !v::is_null(_data); @@ -45,81 +50,124 @@ private: }; +struct PaintFrameResult { + bool painted = false; + crl::time next = 0; + crl::time duration = 0; +}; + class Cache final { public: - Cache(QSize size); + Cache(int size); + [[nodiscard]] static std::optional FromSerialized( + const QByteArray &serialized); + [[nodiscard]] QByteArray serialize(); + + [[nodiscard]] int size() const; [[nodiscard]] int frames() const; [[nodiscard]] QImage frame(int index) const; void reserve(int frames); + void add(crl::time duration, const QImage &frame); + void finish(); + + [[nodiscard]] Preview makePreview() const; + + PaintFrameResult paintCurrentFrame( + QPainter &p, + int x, + int y, + crl::time now); + [[nodiscard]] int currentFrame() const; private: - static constexpr auto kPerRow = 30; + static constexpr auto kPerRow = 16; + + [[nodiscard]] int frameRowByteSize() const; + [[nodiscard]] int frameByteSize() const; + [[nodiscard]] crl::time currentFrameFinishes() const; std::vector _bytes; std::vector _durations; - QSize _size; + crl::time _shown = 0; + int _frame = 0; + int _size = 0; int _frames = 0; + bool _finished = false; }; class Loader; class Loading; -class Unloader { -public: - [[nodiscard]] virtual std::unique_ptr unload() = 0; - virtual ~Unloader() = default; -}; - class Cached final { public: - Cached(std::unique_ptr unloader, Cache cache); + Cached( + const QString &entityData, + Fn()> unloader, + Cache cache); - void paint(QPainter &p, int x, int y); + [[nodiscard]] QString entityData() const; + + PaintFrameResult paint(QPainter &p, int x, int y, crl::time now); [[nodiscard]] Loading unload(); private: - std::unique_ptr _unloader; + Fn()> _unloader; Cache _cache; + QString _entityData; }; -class Cacher { +struct RendererDescriptor { + Fn()> generator; + Fn put; + Fn()> loader; + int size = 0; +}; + +class Renderer final : public base::has_weak_ptr { public: - virtual bool paint(QPainter &p, int x, int y) = 0; - [[nodiscard]] virtual std::optional ready() = 0; - [[nodiscard]] virtual std::unique_ptr cancel() = 0; - virtual ~Cacher() = default; + explicit Renderer(RendererDescriptor &&descriptor); + virtual ~Renderer() = default; -protected: - void reserve(int frames); - void add(crl::time duration, QImage frame); + PaintFrameResult paint(QPainter &p, int x, int y, crl::time now); + [[nodiscard]] std::optional ready(const QString &entityData); + [[nodiscard]] std::unique_ptr cancel(); + [[nodiscard]] Preview makePreview() const; + + void setRepaintCallback(Fn repaint); [[nodiscard]] Cache takeCache(); private: + void frameReady( + std::unique_ptr generator, + crl::time duration, + QImage frame); + void finish(); + Cache _cache; + std::unique_ptr _generator; + Fn _put; + Fn _repaint; + Fn()> _loader; + bool _finished = false; }; -class Caching final { -public: - Caching(std::unique_ptr cacher, Preview preview); - void paint(QPainter &p, int x, int y, const QColor &preview); - - [[nodiscard]] std::optional ready(); - [[nodiscard]] Loading cancel(); - -private: - std::unique_ptr _cacher; - Preview _preview; - +struct Caching { + std::unique_ptr renderer; + QString entityData; + Preview preview; }; class Loader { public: - virtual void load(Fn ready) = 0; + using LoadResult = std::variant; + [[nodiscard]] virtual QString entityData() = 0; + virtual void load(Fn loaded) = 0; + [[nodiscard]] virtual bool loading() = 0; virtual void cancel() = 0; [[nodiscard]] virtual Preview preview() = 0; virtual ~Loader() = default; @@ -129,7 +177,10 @@ class Loading final : public base::has_weak_ptr { public: Loading(std::unique_ptr loader, Preview preview); - void load(Fn done); + [[nodiscard]] QString entityData() const; + + void load(Fn done); + [[nodiscard]] bool loading() const; void paint(QPainter &p, int x, int y, const QColor &preview); void cancel(); @@ -139,21 +190,36 @@ private: }; -class Instance final { +struct RepaintRequest { + crl::time when = 0; + crl::time duration = 0; +}; + +class Object; +class Instance final : public base::has_weak_ptr { public: - Instance(const QString &entityData, Loading loading); + Instance( + Loading loading, + Fn, RepaintRequest)> repaintLater); [[nodiscard]] QString entityData() const; - void paint(QPainter &p, int x, int y, const QColor &preview); + void paint( + QPainter &p, + int x, + int y, + crl::time now, + const QColor &preview, + bool paused); - void incrementUsage(); - void decrementUsage(); + void incrementUsage(not_null object); + void decrementUsage(not_null object); + + void repaint(); private: std::variant _state; - QString _entityData; - - int _usage = 0; + base::flat_set> _usage; + Fn that, RepaintRequest)> _repaintLater; }; @@ -165,15 +231,24 @@ public: class Object final : public Ui::Text::CustomEmoji { public: - Object(not_null instance); + Object(not_null instance, Fn repaint); ~Object(); QString entityData() override; - void paint(QPainter &p, int x, int y, const QColor &preview) override; + void paint( + QPainter &p, + int x, + int y, + crl::time now, + const QColor &preview, + bool paused) override; void unload() override; + void repaint(); + private: const not_null _instance; + Fn _repaint; bool _using = false; }; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index d1593bd1b..8b6b2512b 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -841,6 +841,7 @@ void SessionController::setupPremiumToast() { }) | rpl::distinct_until_changed() | rpl::skip( 1 ) | rpl::filter([=](bool premium) { + session().mtp().requestConfig(); return premium; }) | rpl::start_with_next([=] { Ui::Toast::Show( diff --git a/Telegram/lib_lottie b/Telegram/lib_lottie index 7ae2b9480..301708287 160000 --- a/Telegram/lib_lottie +++ b/Telegram/lib_lottie @@ -1 +1 @@ -Subproject commit 7ae2b9480ef61e222fd46e7806e46875d523502c +Subproject commit 301708287d619c0ba01058d151d46aba2a15ec64 diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 187110f43..6ef5ec341 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 187110f43820a70d20b77e4063286b692d53294d +Subproject commit 6ef5ec3410ef5db33cb6413a7f05cc87a2c970bd