Display emoji correctly in folder tags.

This commit is contained in:
John Preston 2024-12-31 12:17:34 +04:00
parent 51b81dba87
commit acfd92e2e6
8 changed files with 362 additions and 93 deletions

View file

@ -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<QImage>();
const auto previewAlpha = preview->lifetime().make_state<float64>(1);
struct TagState {
Ui::Animations::Simple animation;
Ui::ChatsFilterTagContext context;
QImage frame;
float64 alpha = 1.;
};
const auto tag = preview->lifetime().make_state<TagState>();
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<not_null<UserpicBuilder::CircleButton*>>();
const auto animation
= line->lifetime().make_state<Ui::Animations::Simple>();
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<Data::ChatFilter> {
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();

View file

@ -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<Ui::Text::CustomEmoji> CustomEmojiManager::create(
Fn<void()> 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();

View file

@ -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;

View file

@ -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() {

View file

@ -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<FilterId, int> _chatsFilterScrollStates;
std::unordered_map<ChatsFilterTagsKey, QImage> _chatsFilterTags;
std::unordered_map<ChatsFilterTagsKey, TagCache> _chatsFilterTags;
bool _waitingAllChatListEntryRefreshesForTags = false;
rpl::lifetime _handleChatListEntryTagRefreshesLifetime;

View file

@ -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<Ui::Text::CustomEmoji> 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<Ui::Text::CustomEmoji> _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<Ui::Text::CustomEmoji> 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<EmojiReplacement>();
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<Text::CustomEmoji> MakeScaledSimpleEmoji(EmojiPtr emoji) {
return std::make_unique<ScaledSimpleEmoji>(emoji);
}
std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(
std::unique_ptr<Text::CustomEmoji> wrapped) {
return std::make_unique<ScaledCustomEmoji>(std::move(wrapped));
}
} // namespace Ui

View file

@ -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<QString, std::unique_ptr<Text::CustomEmoji>> emoji;
std::any textContext;
QColor color;
bool active = false;
bool loading = false;
};
[[nodiscard]] QImage ChatsFilterTag(
const TextWithEntities &text,
ChatsFilterTagContext &context);
[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(
EmojiPtr emoji);
[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(
std::unique_ptr<Text::CustomEmoji> wrapped);
} // namespace Ui

@ -1 +1 @@
Subproject commit c1ea8aef5073785ce6e35db9eda830604f81ed62
Subproject commit 8cb06a75d981d7a1a2f2a5df420ef20ff4c0b097