diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index db8a9e57d..e1f6a2551 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -13,27 +13,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #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 "chat_helpers/stickers_lottie.h" #include "ui/text/text_block.h" +#include "apiwrap.h" namespace Data { -namespace { struct CustomEmojiId { StickerSetIdentifier set; uint64 id = 0; }; + +namespace { + [[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) { - const auto &info = id.set; - const auto set = info.id - ? (QString::number(info.id) + ':' + QString::number(info.accessHash)) - : info.shortName; - return QString::number(id.id) + '@' + set; + return QString::number(id.id) + + '@' + + QString::number(id.set.id) + + ':' + + QString::number(id.set.accessHash); } -[[nodiscard]] CustomEmojiId ParseCustomEmojiData(const QString &data) { +[[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) { const auto parts = data.split('@'); if (parts.size() != 2) { return {}; @@ -43,23 +47,20 @@ struct CustomEmojiId { return {}; } const auto second = parts[1].split(':'); - if (const auto set = second[0].toULongLong()) { - return { - .set = {.id = set, .accessHash = second[1].toULongLong() }, - .id = id - }; - } return { - .set = {.shortName = second[1] }, + .set = { + .id = second[0].toULongLong(), + .accessHash = second[1].toULongLong(), + }, .id = id }; } -class CustomEmojiWithData : public Ui::Text::CustomEmoji { +class CustomEmojiWithData { public: explicit CustomEmojiWithData(const QString &data); - QString entityData() final override; + QString entityData(); private: const QString _data; @@ -80,7 +81,7 @@ public: not_null document, Fn update); - void paint(QPainter &p, int x, int y) override; + void paint(QPainter &p, int x, int y, const QColor &preview); private: not_null _document; @@ -100,7 +101,7 @@ DocumentCustomEmoji::DocumentCustomEmoji( , _update(update) { } -void DocumentCustomEmoji::paint(QPainter &p, int x, int y) { +void DocumentCustomEmoji::paint(QPainter &p, int x, int y, const QColor &preview) { if (!_media) { _media = _document->createMediaView(); _media->automaticLoad(_document->stickerSetOrigin(), nullptr); @@ -128,32 +129,112 @@ void DocumentCustomEmoji::paint(QPainter &p, int x, int y) { } } -class ResolvingCustomEmoji final : public CustomEmojiWithData { -public: - ResolvingCustomEmoji(const QString &data, Fn update); +} // namespace - void paint(QPainter &p, int x, int y) override; +class CustomEmojiLoader final + : public Ui::CustomEmoji::Loader + , public base::has_weak_ptr { +public: + CustomEmojiLoader(not_null owner, const CustomEmojiId id); + + [[nodiscard]] bool resolving() const; + void resolved(not_null document); + + void load(Fn ready) override; + void cancel() override; + Ui::CustomEmoji::Preview preview() override; private: - std::optional _resolved; - Fn _update; + struct Resolve { + Fn requested; + }; + struct Process { + std::shared_ptr media; + Fn callback; + rpl::lifetime lifetime; + }; + struct Load { + not_null document; + std::unique_ptr process; + }; + + [[nodiscard]] static std::variant InitialState( + not_null owner, + const CustomEmojiId &id); + + std::variant _state; }; -ResolvingCustomEmoji::ResolvingCustomEmoji( - const QString &data, - Fn update) -: CustomEmojiWithData(data) -, _update(update) { +CustomEmojiLoader::CustomEmojiLoader( + not_null owner, + const CustomEmojiId id) +: _state(InitialState(owner, id)) { } -void ResolvingCustomEmoji::paint(QPainter &p, int x, int y) { - if (_resolved) { - _resolved->paint(p, x, y); +bool CustomEmojiLoader::resolving() const { + return v::is(_state); +} + +void CustomEmojiLoader::resolved(not_null document) { + Expects(resolving()); + + auto requested = std::move(v::get(_state).requested); + _state = Load{ document }; + if (requested) { + load(std::move(requested)); } } -} // namespace +void CustomEmojiLoader::load(Fn ready) { + if (const auto resolve = std::get_if(&_state)) { + resolve->requested = std::move(ready); + } 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), + }); + load->process->media->checkStickerLarge(); + } else { + load->process->callback = std::move(ready); + } + } +} + +auto CustomEmojiLoader::InitialState( + not_null owner, + const CustomEmojiId &id) +-> std::variant { + const auto document = owner->document(id.id); + if (!document->isNull()) { + return Load{ document }; + } + return Resolve(); +} + +void CustomEmojiLoader::cancel() { + if (const auto load = std::get_if(&_state)) { + if (base::take(load->process)) { + load->document->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 }; + } + } + return {}; +} CustomEmojiManager::CustomEmojiManager(not_null owner) : _owner(owner) { @@ -165,14 +246,80 @@ std::unique_ptr CustomEmojiManager::create( const QString &data, Fn update) { const auto parsed = ParseCustomEmojiData(data); - if (!parsed.id) { + if (!parsed.id || !parsed.set.id) { return nullptr; } - const auto document = _owner->document(parsed.id); - if (!document->isNull()) { - return std::make_unique(data, document, update); + auto i = _instances.find(parsed.set.id); + if (i == end(_instances)) { + i = _instances.emplace(parsed.set.id).first; } - return std::make_unique(data, update); + auto j = i->second.find(parsed.id); + if (j == end(i->second)) { + using Loading = Ui::CustomEmoji::Loading; + auto loader = std::make_unique(_owner, parsed); + if (loader->resolving()) { + _loaders[parsed.id].push_back(base::make_weak(loader.get())); + } + j = i->second.emplace( + parsed.id, + std::make_unique(data, Loading{ + std::move(loader), + Ui::CustomEmoji::Preview() + })).first; + } + requestSetIfNeeded(parsed); + + return std::make_unique(j->second.get()); +} + +void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) { + const auto setId = id.set.id; + auto i = _sets.find(setId); + if (i == end(_sets)) { + i = _sets.emplace(setId).first; + } + if (i->second.documents.contains(id.id)) { + return; + } else if (!i->second.waiting.emplace(id.id).second + || i->second.requestId) { + return; + } + const auto api = &_owner->session().api(); + i->second.requestId = api->request(MTPmessages_GetStickerSet( + InputStickerSet(id.set), + MTP_int(i->second.hash) + )).done([=](const MTPmessages_StickerSet &result) { + const auto i = _sets.find(setId); + Assert(i != end(_sets)); + i->second.requestId = 0; + result.match([&](const MTPDmessages_stickerSet &data) { + data.vset().match([&](const MTPDstickerSet &data) { + i->second.hash = data.vhash().v; + }); + for (const auto &entry : data.vdocuments().v) { + const auto document = _owner->processDocument(entry); + const auto id = document->id; + i->second.documents.emplace(id); + i->second.waiting.remove(id); + if (const auto loaders = _loaders.take(id)) { + for (const auto &weak : *loaders) { + if (const auto strong = weak.get()) { + strong->resolved(document); + } + } + } + } + }, [&](const MTPDmessages_stickerSetNotModified &) { + }); + for (const auto &id : base::take(i->second.waiting)) { + DEBUG_LOG(("API Error: Sticker '%1' not found for emoji.").arg(id)); + } + }).fail([=] { + const auto i = _sets.find(setId); + Assert(i != end(_sets)); + i->second.requestId = 0; + LOG(("API Error: Failed getting set '%1' for emoji.").arg(setId)); + }).send(); } Main::Session &CustomEmojiManager::session() const { @@ -192,17 +339,18 @@ void FillTestCustomEmoji( auto length = 0; if (const auto emoji = Ui::Emoji::Find(ch, end, &length)) { if (const auto found = pack->stickerForEmoji(emoji)) { - Expects(found.document->sticker() != nullptr); - - text.entities.push_back({ - EntityType::CustomEmoji, - (ch - begin), - length, - SerializeCustomEmojiId({ - found.document->sticker()->set, - found.document->id, - }), - }); + Assert(found.document->sticker() != nullptr); + if (found.document->sticker()->set.id) { + text.entities.push_back({ + EntityType::CustomEmoji, + (ch - begin), + length, + SerializeCustomEmojiId({ + found.document->sticker()->set, + found.document->id, + }), + }); + } } ch += length; } else if (ch->isHighSurrogate() diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h index 757b171f3..9d2c76cdb 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.h +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.h @@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/text/custom_emoji_instance.h" + +struct StickerSetIdentifier; + namespace Main { class Session; } // namespace Main @@ -14,6 +18,8 @@ class Session; namespace Data { class Session; +struct CustomEmojiId; +class CustomEmojiLoader; class CustomEmojiManager final { public: @@ -28,8 +34,25 @@ public: [[nodiscard]] Session &owner() const; private: + struct Set { + int32 hash = 0; + mtpRequestId requestId = 0; + base::flat_set documents; + base::flat_set waiting; + }; + + void requestSetIfNeeded(const CustomEmojiId &id); + const not_null _owner; + base::flat_map>> _instances; + base::flat_map _sets; + base::flat_map< + uint64, + std::vector>> _loaders; + }; void FillTestCustomEmoji( diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index e3cc1d5fa..4066e0c4c 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1242,6 +1242,10 @@ void Message::paintText( 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()) { + _heavyCustomEmoji = true; + history()->owner().registerHeavyViewPart(const_cast(this)); + } } PointState Message::pointState(QPoint point) const { @@ -1368,12 +1372,16 @@ void Message::toggleCommentsButtonRipple(bool pressed) { } bool Message::hasHeavyPart() const { - return _comments || Element::hasHeavyPart(); + return _heavyCustomEmoji || _comments || Element::hasHeavyPart(); } void Message::unloadHeavyPart() { Element::unloadHeavyPart(); _comments = nullptr; + if (_heavyCustomEmoji) { + _heavyCustomEmoji = false; + message()->_text.unloadCustomEmoji(); + } } bool Message::showForwardsFromSender( diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 1bbf929d2..d6687e3e1 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -252,6 +252,7 @@ private: Ui::Text::String _rightBadge; int _bubbleWidthLimit = 0; + mutable bool _heavyCustomEmoji = false; BottomInfo _bottomInfo; diff --git a/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp b/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp new file mode 100644 index 000000000..c4491c37b --- /dev/null +++ b/Telegram/SourceFiles/ui/text/custom_emoji_instance.cpp @@ -0,0 +1,206 @@ +/* +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 "ui/text/custom_emoji_instance.h" + +class QPainter; + +namespace Ui::CustomEmoji { + +Preview::Preview(QPainterPath path, float64 scale) +: _data(ScaledPath{ std::move(path), scale }) { +} + +Preview::Preview(QImage image) : _data(std::move(image)) { +} + +void Preview::paint(QPainter &p, int x, int y, const QColor &preview) { + if (const auto path = std::get_if(&_data)) { + paintPath(p, x, y, preview, *path); + } else if (const auto image = std::get_if(&_data)) { + p.drawImage(x, y, *image); + } +} + +void Preview::paintPath( + QPainter &p, + int x, + int y, + const QColor &preview, + const ScaledPath &path) { + auto hq = PainterHighQualityEnabler(p); + p.setBrush(preview); + p.setPen(Qt::NoPen); + const auto scale = path.scale; + const auto required = (scale != 1.); + if (required) { + p.save(); + } + p.translate(x, y); + if (required) { + p.scale(scale, scale); + } + p.drawPath(path.path); + if (required) { + p.restore(); + } else { + p.translate(-x, -y); + } +} + +Cache::Cache(QSize size) : _size(size) { +} + +int Cache::frames() const { + return _frames; +} + +QImage Cache::frame(int index) const { + return QImage(); +} + +void Cache::reserve(int frames) { +} + +Cached::Cached(std::unique_ptr unloader, Cache cache) +: _unloader(std::move(unloader)) +, _cache(cache) { +} + +void Cached::paint(QPainter &p, int x, int y) { + p.drawImage(x, y, _cache.frame(0)); +} + +Loading Cached::unload() { + return Loading(_unloader->unload(), Preview(_cache.frame(0))); +} + +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); + } +} + +std::optional Caching::ready() { + return _cacher->ready(); +} + +Loading Caching::cancel() { + return Loading(_cacher->cancel(), std::move(_preview)); +} + +Loading::Loading(std::unique_ptr loader, Preview preview) +: _loader(std::move(loader)) +, _preview(std::move(preview)) { +} + +void Loading::load(Fn done) { + _loader->load(crl::guard(this, std::move(done))); +} + +void Loading::paint(QPainter &p, int x, int y, const QColor &preview) { + if (!_preview) { + if (auto preview = _loader->preview()) { + _preview = std::move(preview); + } + } + _preview.paint(p, x, y, preview); +} + +void Loading::cancel() { + _loader->cancel(); + invalidate_weak_ptrs(this); +} + +Instance::Instance(const QString &entityData, Loading loading) +: _state(std::move(loading)) +, _entityData(entityData) { +} + +QString Instance::entityData() const { + return _entityData; +} + +void Instance::paint(QPainter &p, int x, int y, const QColor &preview) { + if (const auto loading = std::get_if(&_state)) { + loading->paint(p, x, y, preview); + loading->load([=](Caching caching) { + _state = std::move(caching); + }); + } else if (const auto caching = std::get_if(&_state)) { + caching->paint(p, x, y, preview); + if (auto cached = caching->ready()) { + _state = std::move(*cached); + } + } else if (const auto cached = std::get_if(&_state)) { + cached->paint(p, x, y); + } +} + +void Instance::incrementUsage() { + ++_usage; +} + +void Instance::decrementUsage() { + Expects(_usage > 0); + + if (--_usage > 0) { + return; + } + if (const auto loading = std::get_if(&_state)) { + loading->cancel(); + } else if (const auto caching = std::get_if(&_state)) { + _state = caching->cancel(); + } else if (const auto cached = std::get_if(&_state)) { + _state = cached->unload(); + } +} + +Object::Object(not_null instance) +: _instance(instance) { +} + +Object::~Object() { + unload(); +} + +QString Object::entityData() { + return _instance->entityData(); +} + +void Object::paint(QPainter &p, int x, int y, const QColor &preview) { + if (!_using) { + _using = true; + _instance->incrementUsage(); + } + _instance->paint(p, x, y, preview); +} + +void Object::unload() { + if (_using) { + _using = false; + _instance->decrementUsage(); + } +} + +} // namespace Ui::CustomEmoji diff --git a/Telegram/SourceFiles/ui/text/custom_emoji_instance.h b/Telegram/SourceFiles/ui/text/custom_emoji_instance.h new file mode 100644 index 000000000..2d917b94c --- /dev/null +++ b/Telegram/SourceFiles/ui/text/custom_emoji_instance.h @@ -0,0 +1,181 @@ +/* +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 +*/ +#pragma once + +#include "ui/text/text_block.h" +#include "base/weak_ptr.h" +#include "base/bytes.h" + +class QColor; +class QPainter; + +namespace Ui::CustomEmoji { + +class Preview final { +public: + Preview() = default; + Preview(QImage image); + Preview(QPainterPath path, float64 scale); + + void paint(QPainter &p, int x, int y, const QColor &preview); + + [[nodiscard]] explicit operator bool() const { + return !v::is_null(_data); + } + +private: + struct ScaledPath { + QPainterPath path; + float64 scale = 1.; + }; + + void paintPath( + QPainter &p, + int x, + int y, + const QColor &preview, + const ScaledPath &path); + + std::variant _data; + +}; + +class Cache final { +public: + Cache(QSize size); + + [[nodiscard]] int frames() const; + [[nodiscard]] QImage frame(int index) const; + void reserve(int frames); + +private: + static constexpr auto kPerRow = 30; + + std::vector _bytes; + std::vector _durations; + QSize _size; + int _frames = 0; + +}; + +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); + + void paint(QPainter &p, int x, int y); + [[nodiscard]] Loading unload(); + +private: + std::unique_ptr _unloader; + Cache _cache; + +}; + +class Cacher { +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; + +protected: + void reserve(int frames); + void add(crl::time duration, QImage frame); + + [[nodiscard]] Cache takeCache(); + +private: + Cache _cache; + +}; + +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; + +}; + +class Loader { +public: + virtual void load(Fn ready) = 0; + virtual void cancel() = 0; + [[nodiscard]] virtual Preview preview() = 0; + virtual ~Loader() = default; +}; + +class Loading final : public base::has_weak_ptr { +public: + Loading(std::unique_ptr loader, Preview preview); + + void load(Fn done); + void paint(QPainter &p, int x, int y, const QColor &preview); + void cancel(); + +private: + std::unique_ptr _loader; + Preview _preview; + +}; + +class Instance final { +public: + Instance(const QString &entityData, Loading loading); + + [[nodiscard]] QString entityData() const; + void paint(QPainter &p, int x, int y, const QColor &preview); + + void incrementUsage(); + void decrementUsage(); + +private: + std::variant _state; + QString _entityData; + + int _usage = 0; + +}; + +class Delegate { +public: + [[nodiscard]] virtual bool paused() = 0; + virtual ~Delegate() = default; +}; + +class Object final : public Ui::Text::CustomEmoji { +public: + Object(not_null instance); + ~Object(); + + QString entityData() override; + void paint(QPainter &p, int x, int y, const QColor &preview) override; + void unload() override; + +private: + const not_null _instance; + bool _using = false; + +}; + +} // namespace Ui::CustomEmoji diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 584630f3c..8813ecd3c 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -232,6 +232,8 @@ PRIVATE ui/effects/round_checkbox.h ui/effects/scroll_content_shadow.cpp ui/effects/scroll_content_shadow.h + ui/text/custom_emoji_instance.cpp + ui/text/custom_emoji_instance.h ui/text/format_song_name.cpp ui/text/format_song_name.h ui/text/format_values.cpp