diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 1e6a60182..119d4f6a7 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -38,81 +38,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace ChatHelpers { namespace { -constexpr auto kFakeEmojiDocumentIdBase = 0x7777'FFFF'FFFF'0000ULL; - using Core::RecentEmojiId; using Core::RecentEmojiDocument; -[[nodiscard]] DocumentId FakeEmojiDocumentId(EmojiPtr emoji) { - return kFakeEmojiDocumentIdBase + emoji->index(); -} - -class DefaultEmojiLoader final : public Ui::CustomEmoji::Loader { -public: - DefaultEmojiLoader(EmojiPtr emoji, int size); - - QString entityData() override; - - void load(Fn loaded) override; - bool loading() override; - void cancel() override; - Ui::CustomEmoji::Preview preview() override; - -private: - void validateImage(); - - EmojiPtr _emoji = nullptr; - QImage _image; - int _size = 0; - -}; - -DefaultEmojiLoader::DefaultEmojiLoader(EmojiPtr emoji, int size) -: _emoji(emoji) -, _size(size) { -} - -void DefaultEmojiLoader::load(Fn loaded) { - validateImage(); - const auto data = entityData(); - const auto unloader = [emoji = _emoji, size = _size] { - return std::make_unique(emoji, size); - }; - auto cache = Ui::CustomEmoji::Cache(_size); - cache.add(0, _image); - cache.finish(); - loaded(Ui::CustomEmoji::Cached(data, unloader, std::move(cache))); -} - -void DefaultEmojiLoader::validateImage() { - if (!_image.isNull()) { - return; - } - _image = QImage( - { _size, _size }, - QImage::Format_ARGB32_Premultiplied); - _image.setDevicePixelRatio(style::DevicePixelRatio()); - _image.fill(Qt::transparent); - QPainter p(&_image); - Ui::Emoji::Draw(p, _emoji, _size, 0, 0); -} - -QString DefaultEmojiLoader::entityData() { - return "default-emoji://" + _emoji->id(); -} - -bool DefaultEmojiLoader::loading() { - return false; -} - -void DefaultEmojiLoader::cancel() { -} - -Ui::CustomEmoji::Preview DefaultEmojiLoader::preview() { - validateImage(); - return { _image }; -} - } // namespace class EmojiColorPicker : public Ui::RpWidget { @@ -181,7 +109,7 @@ struct EmojiListWidget::CustomInstance { }; struct EmojiListWidget::RecentOne { - not_null instance; + CustomInstance *instance = nullptr; RecentEmojiId id; }; @@ -881,7 +809,7 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) { if (info.section == int(Section::Recent)) { drawRecent(p, w, now, paused, index); } else if (info.section < kEmojiSectionCount) { - drawEmoji(p, w, info.section, index); + drawEmoji(p, w, _emoji[info.section][index]); } else { const auto set = info.section - kEmojiSectionCount; drawCustom(p, w, now, paused, set, index); @@ -919,24 +847,29 @@ void EmojiListWidget::drawRecent( int index) { const auto size = (_esize / cIntRetinaFactor()); _recentPainted = true; - _recent[index].instance->object.paint( - p, - position.x() + (_singleSize.width() - size) / 2, - position.y() + (_singleSize.height() - size) / 2, - now, - st::windowBgRipple->c, - paused); + if (const auto emoji = std::get_if(&_recent[index].id.data)) { + drawEmoji(p, position, *emoji); + } else { + Assert(_recent[index].instance != nullptr); + + _recent[index].instance->object.paint( + p, + position.x() + (_singleSize.width() - size) / 2, + position.y() + (_singleSize.height() - size) / 2, + now, + st::windowBgRipple->c, + paused); + } } void EmojiListWidget::drawEmoji( QPainter &p, QPoint position, - int section, - int index) { + EmojiPtr emoji) { const auto size = (_esize / cIntRetinaFactor()); Ui::Emoji::Draw( p, - _emoji[section][index], + emoji, _esize, position.x() + (_singleSize.width() - size) / 2, position.y() + (_singleSize.height() - size) / 2); @@ -1385,7 +1318,7 @@ auto EmojiListWidget::resolveCustomInstance( setId); if (recentOnly) { for (auto &recent : _recent) { - if (recent.instance == i->second.get()) { + if (recent.instance && recent.instance == i->second.get()) { recent.instance = instance.get(); } } @@ -1399,35 +1332,16 @@ auto EmojiListWidget::resolveCustomInstance( auto EmojiListWidget::resolveCustomInstance( RecentEmojiId customId) --> not_null { +-> CustomInstance* { const auto &data = customId.data; if (const auto document = std::get_if(&data)) { return resolveCustomInstance(document->id); } else if (const auto emoji = std::get_if(&data)) { - return resolveCustomInstance(FakeEmojiDocumentId(*emoji), *emoji); + return nullptr; } Unexpected("Custom recent emoji id."); } -auto EmojiListWidget::resolveCustomInstance( - DocumentId fakeId, - EmojiPtr emoji) --> not_null { - const auto i = _instances.find(fakeId); - if (i != end(_instances)) { - return i->second.get(); - } - return _instances.emplace( - fakeId, - std::make_unique( - std::make_unique( - emoji, - Ui::Emoji::GetSizeLarge()), - [](const auto&, const auto&) {}, - [] {}, - true)).first->second.get(); -} - auto EmojiListWidget::resolveCustomInstance( DocumentId documentId) -> not_null { diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 19158ef10..0c7c03403 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -196,8 +196,7 @@ private: void drawEmoji( QPainter &p, QPoint position, - int section, - int index); + EmojiPtr emoji); void drawCustom( QPainter &p, QPoint position, @@ -234,11 +233,8 @@ private: [[nodiscard]] not_null resolveCustomInstance( not_null document, uint64 setId); - [[nodiscard]] not_null resolveCustomInstance( + [[nodiscard]] CustomInstance *resolveCustomInstance( Core::RecentEmojiId customId); - [[nodiscard]] not_null resolveCustomInstance( - DocumentId fakeId, - EmojiPtr emoji); [[nodiscard]] not_null resolveCustomInstance( DocumentId documentId); [[nodiscard]] std::unique_ptr customInstanceWithLoader( diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp index 58c8512fe..1007ca950 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp @@ -22,6 +22,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "base/event_filter.h" #include "main/main_session.h" +#include "data/data_session.h" +#include "data/data_document.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" #include "styles/style_chat_helpers.h" #include @@ -37,8 +41,36 @@ constexpr auto kAnimationDuration = crl::time(120); } // namespace -SuggestionsWidget::SuggestionsWidget(QWidget *parent) +struct SuggestionsWidget::CustomInstance { + CustomInstance( + std::unique_ptr loader, + Fn, + Ui::CustomEmoji::RepaintRequest)> repaintLater, + Fn repaint); + + Ui::CustomEmoji::Instance emoji; + Ui::CustomEmoji::Object object; +}; + +SuggestionsWidget::CustomInstance::CustomInstance( + std::unique_ptr loader, + Fn, + Ui::CustomEmoji::RepaintRequest)> repaintLater, + Fn repaint) +: emoji( + Ui::CustomEmoji::Loading(std::move(loader), Ui::CustomEmoji::Preview()), + std::move(repaintLater)) +, object(&emoji, std::move(repaint)) { +} + +SuggestionsWidget::SuggestionsWidget( + QWidget *parent, + not_null session) : RpWidget(parent) +, _session(session) +, _repaintTimer([=] { invokeRepaints(); }) , _oneWidth(st::emojiSuggestionSize) , _padding(st::emojiSuggestionsPadding) { resize( @@ -47,11 +79,13 @@ SuggestionsWidget::SuggestionsWidget(QWidget *parent) setMouseTracking(true); } +SuggestionsWidget::~SuggestionsWidget() = default; + rpl::producer SuggestionsWidget::toggleAnimated() const { return _toggleAnimated.events(); } -rpl::producer SuggestionsWidget::triggered() const { +auto SuggestionsWidget::triggered() const -> rpl::producer { return _triggered.events(); } @@ -60,7 +94,7 @@ void SuggestionsWidget::showWithQuery(const QString &query, bool force) { return; } _query = query; - auto rows = getRowsByQuery(); + auto rows = prependCustom(getRowsByQuery()); if (rows.empty()) { _toggleAnimated.fire(false); } @@ -83,6 +117,142 @@ void SuggestionsWidget::selectFirstResult() { } } +auto SuggestionsWidget::prependCustom(std::vector rows) +-> std::vector { + if (rows.empty()) { + return {}; + } + struct Custom { + not_null document; + not_null emoji; + QString replacement; + }; + auto custom = base::flat_multi_map(); + const auto premium = _session->premium(); + const auto stickers = &_session->data().stickers(); + for (const auto setId : stickers->emojiSetsOrder()) { + const auto i = stickers->sets().find(setId); + if (i == end(stickers->sets())) { + continue; + } + for (const auto &document : i->second->stickers) { + if (!premium && document->isPremiumEmoji()) { + // Skip the whole premium emoji set. + break; + } + if (const auto sticker = document->sticker()) { + if (const auto emoji = Ui::Emoji::Find(sticker->alt)) { + const auto j = ranges::find( + rows, + not_null{ emoji }, + &Row::emoji); + if (j != end(rows)) { + custom.emplace(int(j - begin(rows)), Custom{ + .document = document, + .emoji = j->emoji, + .replacement = j->replacement, + }); + } + } + } + } + } + if (custom.empty()) { + return rows; + } + auto result = std::vector(); + result.reserve(custom.size() + rows.size()); + for (const auto &[position, one] : custom) { + result.push_back(Row(one.emoji, one.replacement)); + result.back().document = one.document; + result.back().instance = resolveCustomInstance(one.document); + } + for (auto &row : rows) { + result.push_back(std::move(row)); + } + return result; +} + +auto SuggestionsWidget::resolveCustomInstance( + not_null document) +-> not_null { + const auto i = _instances.find(document); + if (i != end(_instances)) { + return i->second.get(); + } + const auto repaintDelayed = [=]( + not_null instance, + Ui::CustomEmoji::RepaintRequest request) { + if (_instances.empty() || !request.when) { + return; + } + auto &when = _repaints[request.duration]; + if (when < request.when) { + when = request.when; + } + if (_repaintTimerScheduled) { + return; + } + scheduleRepaintTimer(); + }; + const auto repaintNow = [=] { + update(); + }; + auto instance = std::make_unique( + _session->data().customEmojiManager().createLoader( + document, + Data::CustomEmojiManager::SizeTag::Large), + std::move(repaintDelayed), + std::move(repaintNow)); + return _instances.emplace( + document, + std::move(instance) + ).first->second.get(); +} + +void SuggestionsWidget::scheduleRepaintTimer() { + _repaintTimerScheduled = true; + Ui::PostponeCall(this, [=] { + _repaintTimerScheduled = false; + + auto next = crl::time(); + for (const auto &[duration, when] : _repaints) { + if (!next || next > when) { + next = 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 SuggestionsWidget::invokeRepaints() { + _repaintNext = 0; + auto invoke = false; + const auto now = crl::now(); + for (auto i = begin(_repaints); i != end(_repaints);) { + if (i->second > now) { + ++i; + continue; + } + invoke = true; + i = _repaints.erase(i); + } + if (invoke) { + update(); + } + scheduleRepaintTimer(); +} + SuggestionsWidget::Row::Row( not_null emoji, const QString &replacement) @@ -230,18 +400,20 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) { Ui::StickerHoverCorners); } + const auto now = crl::now(); + const auto preview = st::windowBgOver->c; for (auto i = from; i != till; ++i) { const auto &row = _rows[i]; const auto emoji = row.emoji; const auto esize = Ui::Emoji::GetSizeLarge(); - const auto x = i * _oneWidth; - const auto y = 0; - Ui::Emoji::Draw( - p, - emoji, - esize, - x + (_oneWidth - (esize / cIntRetinaFactor())) / 2, - y + (_oneWidth - (esize / cIntRetinaFactor())) / 2); + const auto size = esize / style::DevicePixelRatio(); + const auto x = i * _oneWidth + (_oneWidth - size) / 2; + const auto y = (_oneWidth - size) / 2; + if (row.instance) { + row.instance->object.paint(p, x, y, now, preview, false); + } else { + Ui::Emoji::Draw(p, emoji, esize, x, y); + } } paintFadings(p); } @@ -496,7 +668,10 @@ bool SuggestionsWidget::triggerSelectedRow() const { } void SuggestionsWidget::triggerRow(const Row &row) const { - _triggered.fire(row.emoji->text()); + _triggered.fire({ + row.emoji->text(), + row.document ? Data::SerializeCustomEmojiId(row.document) : QString() + }); } void SuggestionsWidget::enterEventHook(QEnterEvent *e) { @@ -525,7 +700,9 @@ SuggestionsController::SuggestionsController( st::emojiSuggestionsDropdown); _container->setAutoHiding(false); _suggestions = _container->setOwnedWidget( - object_ptr(_container)); + object_ptr( + _container, + session)); setReplaceCallback(nullptr); @@ -559,8 +736,8 @@ SuggestionsController::SuggestionsController( suggestionsUpdated(visible); }, _lifetime); _suggestions->triggered( - ) | rpl::start_with_next([=](QString replacement) { - replaceCurrent(replacement); + ) | rpl::start_with_next([=](const SuggestionsWidget::Chosen &chosen) { + replaceCurrent(chosen.emoji, chosen.customData); }, _lifetime); Core::App().emojiKeywords().refreshed( ) | rpl::start_with_next([=] { @@ -589,8 +766,13 @@ SuggestionsController *SuggestionsController::Init( result->setReplaceCallback([=]( int from, int till, - const QString &replacement) { - field->commitInstantReplacement(from, till, replacement); + const QString &replacement, + const QString &customEmojiData) { + field->commitInstantReplacement( + from, + till, + replacement, + customEmojiData); }); return result; } @@ -599,11 +781,16 @@ void SuggestionsController::setReplaceCallback( Fn callback) { + const QString &replacement, + const QString &customEmojiData)> callback) { if (callback) { _replaceCallback = std::move(callback); } else { - _replaceCallback = [=](int from, int till, const QString &replacement) { + _replaceCallback = [=]( + int from, + int till, + const QString &replacement, + const QString &customEmojiData) { auto cursor = _field->textCursor(); cursor.setPosition(from); cursor.setPosition(till, QTextCursor::KeepAnchor); @@ -667,7 +854,9 @@ QString SuggestionsController::getEmojiQuery() { if (from >= position || till < position) { continue; } - if (fragment.charFormat().isImageFormat()) { + const auto format = fragment.charFormat(); + if (format.isImageFormat() + || format.objectType() == InputField::kCustomEmojiFormat) { continue; } _queryStartPosition = from; @@ -714,7 +903,9 @@ QString SuggestionsController::getEmojiQuery() { return text; } -void SuggestionsController::replaceCurrent(const QString &replacement) { +void SuggestionsController::replaceCurrent( + const QString &replacement, + const QString &customEmojiData) { const auto suggestion = getEmojiQuery(); if (suggestion.isEmpty()) { showWithQuery(QString()); @@ -722,7 +913,7 @@ void SuggestionsController::replaceCurrent(const QString &replacement) { const auto cursor = _field->textCursor(); const auto position = cursor.position(); const auto from = position - suggestion.size(); - _replaceCallback(from, position, replacement); + _replaceCallback(from, position, replacement, customEmojiData); } } diff --git a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h index a580cc8c3..bc29bfc74 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h @@ -27,19 +27,28 @@ namespace Emoji { class SuggestionsWidget final : public Ui::RpWidget { public: - SuggestionsWidget(QWidget *parent); + SuggestionsWidget(QWidget *parent, not_null session); + ~SuggestionsWidget(); void showWithQuery(const QString &query, bool force = false); void selectFirstResult(); bool handleKeyEvent(int key); - rpl::producer toggleAnimated() const; - rpl::producer triggered() const; + [[nodiscard]] rpl::producer toggleAnimated() const; + + struct Chosen { + QString emoji; + QString customData; + }; + [[nodiscard]] rpl::producer triggered() const; private: + struct CustomInstance; struct Row { Row(not_null emoji, const QString &replacement); + CustomInstance *instance = nullptr; + DocumentData *document = nullptr; not_null emoji; QString replacement; }; @@ -56,7 +65,8 @@ private: void scrollByWheelEvent(not_null e); void paintFadings(Painter &p) const; - std::vector getRowsByQuery() const; + [[nodiscard]] std::vector getRowsByQuery() const; + [[nodiscard]] std::vector prependCustom(std::vector rows); void resizeToRows(); void setSelected( int selected, @@ -77,8 +87,21 @@ private: void scrollTo(int value, anim::type animated = anim::type::instant); void stopAnimations(); + [[nodiscard]] not_null resolveCustomInstance( + not_null document); + void scheduleRepaintTimer(); + void invokeRepaints(); + + const not_null _session; QString _query; std::vector _rows; + base::flat_map< + not_null, + std::unique_ptr> _instances; + base::flat_map _repaints; + bool _repaintTimerScheduled = false; + base::Timer _repaintTimer; + crl::time _repaintNext = 0; std::optional _lastMousePosition; bool _mouseSelection = false; @@ -96,7 +119,7 @@ private: int _dragScrollStart = -1; rpl::event_stream _toggleAnimated; - rpl::event_stream _triggered; + rpl::event_stream _triggered; }; @@ -119,7 +142,8 @@ public: void setReplaceCallback(Fn callback); + const QString &replacement, + const QString &customEmojiData)> callback); static SuggestionsController *Init( not_null outer, @@ -135,7 +159,9 @@ private: void suggestionsUpdated(bool visible); void updateGeometry(); void updateForceHidden(); - void replaceCurrent(const QString &replacement); + void replaceCurrent( + const QString &replacement, + const QString &customEmojiData); bool fieldFilter(not_null event); bool outerFilter(not_null event); @@ -149,7 +175,8 @@ private: Fn _replaceCallback; + const QString &replacement, + const QString &customEmojiData)> _replaceCallback; base::unique_qptr _container; QPointer _suggestions; base::unique_qptr _fieldFilter; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 1d34c64da..0daf3d4ac 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 1d34c64da8bc234c4d5dd8ebaff7f249d897c7d7 +Subproject commit 0daf3d4ac70e587c80abe7685e7ad7512f6f39cf