diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp index c6a483667..d202d6e6d 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_box.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/message_field.h" #include "core/application.h" #include "core/core_settings.h" +#include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_chat_filters.h" #include "data/data_peer.h" @@ -443,6 +444,13 @@ void EditFilterBox( nameEditing->settingDefault = false; } }; + const auto nameWithEntities = [=](bool upper = false) { + const auto entered = name->getTextWithTags(); + return TextWithEntities{ + (upper ? entered.text.toUpper() : entered.text), + TextUtilities::ConvertTextTagsToEntities(entered.tags), + }; + }; const auto outer = box->getDelegate()->outerContainer(); CreateIconSelector( @@ -546,18 +554,28 @@ void EditFilterBox( colors->width(), h); }, preview->lifetime()); - const auto previewTag = preview->lifetime().make_state(); - const auto previewAlpha = preview->lifetime().make_state(1); + + struct TagState { + Ui::Animations::Simple animation; + Ui::ChatsFilterTagContext context; + QImage frame; + float64 alpha = 1.; + }; + const auto tag = preview->lifetime().make_state(); + tag->context.textContext = Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [] {}, + }; preview->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(preview); - p.setOpacity(*previewAlpha); - const auto size = previewTag->size() / style::DevicePixelRatio(); + p.setOpacity(tag->alpha); + const auto size = tag->frame.size() / style::DevicePixelRatio(); const auto rect = QRect( preview->width() - size.width() - st::boxRowPadding.right(), (st::normalFont->height - size.height()) / 2, size.width(), size.height()); - p.drawImage(rect.topLeft(), *previewTag); + p.drawImage(rect.topLeft(), tag->frame); if (p.opacity() < 1) { p.setOpacity(1. - p.opacity()); p.setFont(st::normalFont); @@ -574,16 +592,14 @@ void EditFilterBox( Ui::CreateSkipWidget(colors, side), st::boxRowPadding); auto buttons = std::vector>(); - const auto animation - = line->lifetime().make_state(); const auto palette = [](int i) { return Ui::EmptyUserpic::UserpicColor(i).color2; }; name->changes() | rpl::start_with_next([=] { - *previewTag = Ui::ChatsFilterTag( - name->getLastText().toUpper(), - palette(state->colorIndex.current())->c, - false); + tag->context.color = palette(state->colorIndex.current())->c; + tag->frame = Ui::ChatsFilterTag( + nameWithEntities(true), + tag->context); preview->update(); }, preview->lifetime()); for (auto i = 0; i < kColorsCount; ++i) { @@ -597,12 +613,12 @@ void EditFilterBox( const auto color = palette(i); button->setBrush(color); if (progress == 1) { - *previewTag = Ui::ChatsFilterTag( - name->getLastText().toUpper(), - color->c, - false); + tag->context.color = color->c; + tag->frame = Ui::ChatsFilterTag( + nameWithEntities(true), + tag->context); if (i == kNoTag) { - *previewAlpha = 0.; + tag->alpha = 0.; } } buttons.push_back(button); @@ -617,17 +633,17 @@ void EditFilterBox( const auto c2 = palette(now); const auto a1 = (was == kNoTag) ? 0. : 1.; const auto a2 = (now == kNoTag) ? 0. : 1.; - animation->stop(); - animation->start([=](float64 progress) { + tag->animation.stop(); + tag->animation.start([=](float64 progress) { if (was >= 0) { buttons[was]->setSelectedProgress(1. - progress); } buttons[now]->setSelectedProgress(progress); - *previewTag = Ui::ChatsFilterTag( - name->getLastText().toUpper(), - anim::color(c1, c2, progress), - false); - *previewAlpha = anim::interpolateF(a1, a2, progress); + tag->context.color = anim::color(c1, c2, progress); + tag->frame = Ui::ChatsFilterTag( + nameWithEntities(true), + tag->context); + tag->alpha = anim::interpolateF(a1, a2, progress); preview->update(); }, 0., 1., st::universalDuration); } @@ -673,11 +689,7 @@ void EditFilterBox( } const auto collect = [=]() -> std::optional { - const auto entered = name->getTextWithTags(); - const auto title = TextWithEntities{ - entered.text, - TextUtilities::ConvertTextTagsToEntities(entered.tags), - }; + const auto title = nameWithEntities(); const auto rules = data->current(); if (title.empty()) { name->showError(); diff --git a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp index 2ed3baef8..ed6f36515 100644 --- a/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp +++ b/Telegram/SourceFiles/data/stickers/data_custom_emoji.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ffmpeg/ffmpeg_frame_generator.h" #include "chat_helpers/stickers_lottie.h" #include "storage/file_download.h" // kMaxFileInMemory +#include "ui/chat/chats_filter_tag.h" #include "ui/effects/credits_graphics.h" #include "ui/widgets/fields/input_field.h" #include "ui/text/custom_emoji_instance.h" @@ -104,6 +105,14 @@ private: return u"userpic:"_q; } +[[nodiscard]] QString ScaledSimplePrefix() { + return u"scaled-simple:"_q; +} + +[[nodiscard]] QString ScaledCustomPrefix() { + return u"scaled-custom:"_q; +} + [[nodiscard]] QString InternalPadding(QMargins value) { return value.isNull() ? QString() : QString(",%1,%2,%3,%4" ).arg(value.left() @@ -536,7 +545,16 @@ std::unique_ptr CustomEmojiManager::create( Fn update, SizeTag tag, int sizeOverride) { - if (data.startsWith(InternalPrefix())) { + if (data.startsWith(ScaledSimplePrefix())) { + const auto text = data.mid(ScaledSimplePrefix().size()); + const auto emoji = Ui::Emoji::Find(text); + Assert(emoji != nullptr); + return Ui::MakeScaledSimpleEmoji(emoji); + } else if (data.startsWith(ScaledCustomPrefix())) { + const auto original = data.mid(ScaledCustomPrefix().size()); + return Ui::MakeScaledCustomEmoji( + create(original, std::move(update), SizeTag::Large)); + } else if (data.startsWith(InternalPrefix())) { return internal(data); } else if (data.startsWith(UserpicEmojiPrefix())) { const auto ratio = style::DevicePixelRatio(); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 8df32991b..2914e3a8e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -112,8 +112,10 @@ taggedForumDialogRow: DialogRow(forumDialogRow) { height: 96px; tagTop: 77px; } -dialogRowFilterTagSkip : 4px; -dialogRowFilterTagFont : font(10px); +dialogRowFilterTagSkip: 4px; +dialogRowFilterTagStyle: TextStyle(defaultTextStyle) { + font: font(10px); +} dialogRowOpenBotTextStyle: semiboldTextStyle; dialogRowOpenBotHeight: 20px; dialogRowOpenBotRight: 10px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 4d9e013f4..f581d421a 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/click_handler_types.h" #include "core/shortcuts.h" +#include "core/ui_integration.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" @@ -220,6 +221,11 @@ struct InnerWidget::PeerSearchResult { BasicRow row; }; +struct InnerWidget::TagCache { + Ui::ChatsFilterTagContext context; + QImage frame; +}; + Key InnerWidget::FilterResult::key() const { return row->key(); } @@ -4161,32 +4167,41 @@ QImage *InnerWidget::cacheChatsFilterTag( return nullptr; } const auto key = SerializeFilterTagsKey(filter.id(), more, active); - { - const auto it = _chatsFilterTags.find(key); - if (it != end(_chatsFilterTags)) { - return &it->second; + auto &entry = _chatsFilterTags[key]; + if (!entry.frame.isNull()) { + if (!entry.context.loading) { + return &entry.frame; + } + for (const auto &[k, emoji] : entry.context.emoji) { + if (!emoji->ready()) { + return &entry.frame; // Still waiting for emoji. + } } } - auto roundedText = QString(); + auto roundedText = TextWithEntities(); auto colorIndex = -1; if (filter.id()) { - roundedText = filter.title().text.toUpper(); // todo filter emoji + roundedText = filter.title(); + roundedText.text = roundedText.text.toUpper(); if (filter.colorIndex()) { colorIndex = *(filter.colorIndex()); } } else if (more > 0) { - roundedText = QChar('+') + QString::number(more); + roundedText.text = QChar('+') + QString::number(more); colorIndex = st::colorIndexBlue; } - if (roundedText.isEmpty() || colorIndex < 0) { + if (roundedText.empty() || colorIndex < 0) { return nullptr; } - return &_chatsFilterTags.emplace( - key, - Ui::ChatsFilterTag( - std::move(roundedText), - Ui::EmptyUserpic::UserpicColor(colorIndex).color2->c, - active)).first->second; + const auto color = Ui::EmptyUserpic::UserpicColor(colorIndex).color2; + entry.context.color = color->c; + entry.context.active = active; + entry.context.textContext = Core::MarkedTextContext{ + .session = &session(), + .customEmojiRepaint = [] {}, + }; + entry.frame = Ui::ChatsFilterTag(roundedText, entry.context); + return &entry.frame; } bool InnerWidget::chooseHashtag() { diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 56defd4dd..48c409234 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -221,6 +221,7 @@ private: struct CollapsedRow; struct HashtagResult; struct PeerSearchResult; + struct TagCache; enum class JumpSkip { PreviousOrBegin, @@ -579,7 +580,7 @@ private: base::flat_map _chatsFilterScrollStates; - std::unordered_map _chatsFilterTags; + std::unordered_map _chatsFilterTags; bool _waitingAllChatListEntryRefreshesForTags = false; rpl::lifetime _handleChatListEntryTagRefreshesLifetime; diff --git a/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp b/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp index aa1b4bff2..077de4f32 100644 --- a/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp +++ b/Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp @@ -7,59 +7,246 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/chat/chats_filter_tag.h" +#include "ui/text/text_custom_emoji.h" #include "ui/emoji_config.h" +#include "ui/integration.h" #include "ui/painter.h" #include "styles/style_dialogs.h" namespace Ui { +namespace { -QImage ChatsFilterTag(QString roundedText, QColor color, bool active) { - const auto &roundedFont = st::dialogRowFilterTagFont; - const auto additionalWidth = roundedFont->spacew * 3; - struct EmojiReplacement final { - QPixmap pixmap; - int from = -1; - int length = 0; - float64 x = -1; +class ScaledSimpleEmoji final : public Ui::Text::CustomEmoji { +public: + ScaledSimpleEmoji(EmojiPtr emoji); + + int width() override; + QString entityData() override; + void paint(QPainter &p, const Context &context) override; + void unload() override; + bool ready() override; + bool readyInDefaultState() override; + +private: + const EmojiPtr _emoji; + QImage _frame; + QPoint _shift; + +}; + +class ScaledCustomEmoji final : public Ui::Text::CustomEmoji { +public: + ScaledCustomEmoji(std::unique_ptr wrapped); + + int width() override; + QString entityData() override; + void paint(QPainter &p, const Context &context) override; + void unload() override; + bool ready() override; + bool readyInDefaultState() override; + +private: + const std::unique_ptr _wrapped; + QImage _frame; + QPoint _shift; + +}; + +[[nodiscard]] int ScaledSize() { + return st::dialogRowFilterTagStyle.font->height - 2 * st::lineWidth; +} + +ScaledSimpleEmoji::ScaledSimpleEmoji(EmojiPtr emoji) +: _emoji(emoji) { +} + +int ScaledSimpleEmoji::width() { + return ScaledSize(); +} + +QString ScaledSimpleEmoji::entityData() { + return u"scaled-simple:"_q + _emoji->text(); +} + +void ScaledSimpleEmoji::paint(QPainter &p, const Context &context) { + if (_frame.isNull()) { + const auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize); + const auto xskip = (st::emojiSize - adjusted) / 2; + const auto yskip = xskip + (width() - st::emojiSize) / 2; + _shift = { xskip, yskip }; + + const auto ratio = style::DevicePixelRatio(); + const auto large = Emoji::GetSizeLarge(); + const auto size = QSize(large, large); + _frame = QImage(size, QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + _frame.fill(Qt::transparent); + + auto p = QPainter(&_frame); + Emoji::Draw(p, _emoji, large, 0, 0); + p.end(); + + _frame = _frame.scaled( + QSize(width(), width()) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + } + + p.drawImage(context.position - _shift, _frame); +} + +void ScaledSimpleEmoji::unload() { +} + +bool ScaledSimpleEmoji::ready() { + return true; +} + +bool ScaledSimpleEmoji::readyInDefaultState() { + return true; +} + +ScaledCustomEmoji::ScaledCustomEmoji( + std::unique_ptr wrapped) +: _wrapped(std::move(wrapped)) { +} + +int ScaledCustomEmoji::width() { + return ScaledSize(); +} + +QString ScaledCustomEmoji::entityData() { + return u"scaled-custom:"_q + _wrapped->entityData(); +} + +void ScaledCustomEmoji::paint(QPainter &p, const Context &context) { + if (_frame.isNull()) { + if (!_wrapped->ready()) { + return; + } + const auto ratio = style::DevicePixelRatio(); + const auto large = Emoji::GetSizeLarge(); + const auto largeadjust = Text::AdjustCustomEmojiSize(large / ratio); + const auto size = QSize(largeadjust, largeadjust) * ratio; + _frame = QImage(size, QImage::Format_ARGB32_Premultiplied); + _frame.setDevicePixelRatio(ratio); + _frame.fill(Qt::transparent); + + auto p = QPainter(&_frame); + p.translate(-context.position); + const auto was = context.internal.forceFirstFrame; + context.internal.forceFirstFrame = true; + _wrapped->paint(p, context); + context.internal.forceFirstFrame = was; + p.end(); + + const auto smalladjust = Text::AdjustCustomEmojiSize(width()); + _frame = _frame.scaled( + QSize(smalladjust, smalladjust) * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + _wrapped->unload(); + + const auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize); + const auto xskip = (st::emojiSize - adjusted) / 2; + const auto yskip = xskip + (width() - st::emojiSize) / 2; + + const auto add = (width() - smalladjust) / 2; + _shift = QPoint(xskip, yskip) - QPoint(add, add); + } + p.drawImage(context.position - _shift, _frame); +} + +void ScaledCustomEmoji::unload() { + _wrapped->unload(); +} + +bool ScaledCustomEmoji::ready() { + return !_frame.isNull() || _wrapped->ready(); +} + +bool ScaledCustomEmoji::readyInDefaultState() { + return !_frame.isNull() || _wrapped->ready(); +} + +[[nodiscard]] TextWithEntities PrepareSmallEmojiText( + TextWithEntities text, + ChatsFilterTagContext &context) { + auto i = text.entities.begin(); + auto ch = text.text.constData(); + auto &integration = Integration::Instance(); + context.loading = false; + const auto end = text.text.constData() + text.text.size(); + const auto adjust = [&](EntityInText &entity) { + if (entity.type() != EntityType::CustomEmoji) { + return; + } + const auto data = entity.data(); + if (data.startsWith(u"scaled-simple:"_q)) { + return; + } + auto &emoji = context.emoji[data]; + if (!emoji) { + emoji = integration.createCustomEmoji( + data, + context.textContext); + } + if (!emoji->ready()) { + context.loading = true; + } + entity = EntityInText( + entity.type(), + entity.offset(), + entity.length(), + u"scaled-custom:"_q + entity.data()); + }; + const auto till = [](EntityInText &entity) { + return entity.offset() + entity.length(); }; - auto emojiReplacements = std::vector(); - auto ch = roundedText.constData(); - const auto end = ch + roundedText.size(); while (ch != end) { auto emojiLength = 0; if (const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength)) { - const auto factor = style::DevicePixelRatio(); - emojiReplacements.push_back({ - .pixmap = Ui::Emoji::SinglePixmap( - emoji, - st::normalFont->height * factor).scaledToHeight( - roundedFont->ascent * factor, - Qt::SmoothTransformation), - .from = int(ch - roundedText.constData()), - .length = emojiLength, - }); + const auto f = int(ch - text.text.constData()); + const auto l = f + emojiLength; + while (i != text.entities.end() && till(*i) <= f) { + adjust(*i); + ++i; + } + ch += emojiLength; + if (i != text.entities.end() && i->offset() < l) { + continue; + } + i = text.entities.insert(i, EntityInText{ + EntityType::CustomEmoji, + f, + emojiLength, + u"scaled-simple:"_q + emoji->text(), + }); } else { - ch++; + ++ch; } } - if (!emojiReplacements.empty()) { - auto addedChars = 0; - for (auto &e : emojiReplacements) { - const auto pixmapWidth = e.pixmap.width() - / style::DevicePixelRatio(); - const auto spaces = 1 + pixmapWidth / roundedFont->spacew; - const auto placeholder = QString(spaces, ' '); - const auto from = e.from + addedChars; - e.x = roundedFont->width(roundedText.mid(0, from)) - + additionalWidth / 2. - + (roundedFont->width(placeholder) - pixmapWidth) / 2.; - roundedText.replace(from, e.length, placeholder); - addedChars += spaces - e.length; - } + for (; i != text.entities.end(); ++i) { + adjust(*i); } - const auto roundedWidth = roundedFont->width(roundedText) - + additionalWidth; + return text; +} + +} // namespace + +QImage ChatsFilterTag( + const TextWithEntities &text, + ChatsFilterTagContext &context) { + const auto &roundedFont = st::dialogRowFilterTagStyle.font; + const auto additionalWidth = roundedFont->spacew * 3; + auto rich = Text::String( + st::dialogRowFilterTagStyle, + PrepareSmallEmojiText(text, context), + kMarkupTextOptions, + kQFixedMax, + context.textContext); + const auto roundedWidth = rich.maxWidth() + additionalWidth; const auto rect = QRect(0, 0, roundedWidth, roundedFont->height); auto cache = QImage( rect.size() * style::DevicePixelRatio(), @@ -68,9 +255,11 @@ QImage ChatsFilterTag(QString roundedText, QColor color, bool active) { cache.fill(Qt::transparent); { auto p = QPainter(&cache); - const auto pen = QPen(active ? st::dialogsBgActive->c : color); + const auto pen = QPen(context.active + ? st::dialogsBgActive->c + : context.color); p.setPen(Qt::NoPen); - p.setBrush(active + p.setBrush(context.active ? st::dialogsTextFgActive->c : anim::with_alpha(pen.color(), .15)); { @@ -80,13 +269,23 @@ QImage ChatsFilterTag(QString roundedText, QColor color, bool active) { } p.setPen(pen); p.setFont(roundedFont); - p.drawText(rect, roundedText, style::al_center); - for (const auto &e : emojiReplacements) { - const auto h = e.pixmap.height() / style::DevicePixelRatio(); - p.drawPixmap(QPointF(e.x, (rect.height() - h) / 2), e.pixmap); - } + const auto dx = (rect.width() - rich.maxWidth()) / 2; + const auto dy = (rect.height() - roundedFont->height) / 2; + rich.draw(p, { + .position = rect.topLeft() + QPoint(dx, dy), + .availableWidth = rich.maxWidth(), + }); } return cache; } +std::unique_ptr MakeScaledSimpleEmoji(EmojiPtr emoji) { + return std::make_unique(emoji); +} + +std::unique_ptr MakeScaledCustomEmoji( + std::unique_ptr wrapped) { + return std::make_unique(std::move(wrapped)); +} + } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/chats_filter_tag.h b/Telegram/SourceFiles/ui/chat/chats_filter_tag.h index aa07bf4c5..28da74c80 100644 --- a/Telegram/SourceFiles/ui/chat/chats_filter_tag.h +++ b/Telegram/SourceFiles/ui/chat/chats_filter_tag.h @@ -7,8 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "emoji.h" + +namespace Ui::Text { +class CustomEmoji; +} // namespace Ui::Text + namespace Ui { -[[nodiscard]] QImage ChatsFilterTag(QString text, QColor color, bool active); +struct ChatsFilterTagContext { + base::flat_map> emoji; + std::any textContext; + QColor color; + bool active = false; + bool loading = false; +}; + +[[nodiscard]] QImage ChatsFilterTag( + const TextWithEntities &text, + ChatsFilterTagContext &context); + +[[nodiscard]] std::unique_ptr MakeScaledSimpleEmoji( + EmojiPtr emoji); + +[[nodiscard]] std::unique_ptr MakeScaledCustomEmoji( + std::unique_ptr wrapped); } // namespace Ui diff --git a/Telegram/lib_ui b/Telegram/lib_ui index c1ea8aef5..8cb06a75d 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit c1ea8aef5073785ce6e35db9eda830604f81ed62 +Subproject commit 8cb06a75d981d7a1a2f2a5df420ef20ff4c0b097