From 87cd0b6127845b93e2a4db3433816b9ea305b14d Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 23 Jun 2022 15:06:37 +0400 Subject: [PATCH] Add initial support for custom emoji. --- ui/integration.cpp | 7 +++ ui/integration.h | 7 +++ ui/text/text.cpp | 126 +++++++++++++++++++++++++++++++---------- ui/text/text.h | 4 +- ui/text/text_block.cpp | 100 +++++++++++++++++--------------- ui/text/text_block.h | 50 ++++++++++++++-- ui/text/text_entity.h | 1 + 7 files changed, 212 insertions(+), 83 deletions(-) diff --git a/ui/integration.cpp b/ui/integration.cpp index e34243d..d0694e7 100644 --- a/ui/integration.cpp +++ b/ui/integration.cpp @@ -8,6 +8,7 @@ #include "ui/gl/gl_detection.h" #include "ui/text/text_entity.h" +#include "ui/text/text_block.h" #include "ui/basic_click_handlers.h" #include "base/platform/base_platform_info.h" @@ -69,6 +70,12 @@ std::shared_ptr Integration::createLinkHandler( return nullptr; } +std::unique_ptr Integration::createCustomEmoji( + const QString &data, + const std::any &context) { + return nullptr; +} + bool Integration::handleUrlClick( const QString &url, const QVariant &context) { diff --git a/ui/integration.h b/ui/integration.h index 67a953e..701d8e3 100644 --- a/ui/integration.h +++ b/ui/integration.h @@ -27,6 +27,10 @@ namespace Emoji { class One; } // namespace Emoji +namespace Text { +class CustomEmoji; +} // namespace Text + class Integration { public: static void Set(not_null instance); @@ -56,6 +60,9 @@ public: [[nodiscard]] virtual QString convertTagToMimeTag(const QString &tagId); [[nodiscard]] virtual const Emoji::One *defaultEmojiVariant( const Emoji::One *emoji); + [[nodiscard]] virtual auto createCustomEmoji( + const QString &data, + const std::any &context) -> std::unique_ptr; [[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests(); diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 06e1073..2073ea8 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -272,6 +272,7 @@ private: const QChar *_ptr = nullptr; const EntitiesInText::const_iterator _entitiesEnd; EntitiesInText::const_iterator _waitingEntity; + QString _customEmojiData; const bool _multiline = false; const QFixed _stopAfterWidth; // summary width of all added words @@ -406,9 +407,16 @@ void Parser::createBlock(int32 skipBack) { } _lastSkipped = false; const auto lnkIndex = _monoIndex ? _monoIndex : _lnkIndex; - if (_emoji) { + auto custom = _customEmojiData.isEmpty() + ? nullptr + : Integration::Instance().createCustomEmoji( + _customEmojiData, + _context); + if (custom) { + _t->_blocks.push_back(Block::CustomEmoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, std::move(custom))); + _lastSkipped = true; + } else if (_emoji) { _t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, _emoji)); - _emoji = nullptr; _lastSkipped = true; } else if (newline) { _t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex)); @@ -416,6 +424,8 @@ void Parser::createBlock(int32 skipBack) { _t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, lnkIndex, _spoilerIndex)); } _blockStart += len; + _customEmojiData = QByteArray(); + _emoji = nullptr; blockCreated(); } } @@ -499,7 +509,10 @@ bool Parser::checkEntities() { link.data = _waitingEntity->data(); link.text = QString(entityBegin, entityLength); }; - if (entityType == EntityType::Bold) { + if (entityType == EntityType::CustomEmoji) { + createBlock(); + _customEmojiData = _waitingEntity->data(); + } else if (entityType == EntityType::Bold) { flags = TextBlockFBold; } else if (entityType == EntityType::Semibold) { flags = TextBlockFSemibold; @@ -519,7 +532,8 @@ bool Parser::checkEntities() { flags = TextBlockFPre; createBlock(); if (!_t->_blocks.empty() - && _t->_blocks.back()->type() != TextBlockTNewline) { + && _t->_blocks.back()->type() != TextBlockTNewline + && _customEmojiData.isEmpty()) { createNewlineBlock(); } } @@ -632,10 +646,11 @@ void Parser::skipBadEntities() { void Parser::parseCurrentChar() { _ch = ((_ptr < _end) ? *_ptr : 0); _emojiLookback = 0; - const auto isNewLine = _multiline && IsNewline(_ch); + const auto inCustomEmoji = !_customEmojiData.isEmpty(); + const auto isNewLine = !inCustomEmoji && _multiline && IsNewline(_ch); const auto isSpace = IsSpace(_ch); const auto isDiac = IsDiac(_ch); - const auto isTilde = _checkTilde && (_ch == '~'); + const auto isTilde = !inCustomEmoji && _checkTilde && (_ch == '~'); const auto skip = [&] { if (IsBad(_ch) || _ch.isLowSurrogate()) { return true; @@ -711,6 +726,9 @@ void Parser::parseCurrentChar() { } void Parser::parseEmojiFromCurrent() { + if (!_customEmojiData.isEmpty()) { + return; + } int len = 0; auto e = Emoji::Find(_ptr - _emojiLookback, _end, &len); if (!e) return; @@ -878,7 +896,7 @@ void Parser::finalize(const TextParseOptions &options) { } _t->_links.squeeze(); _t->_spoilers.squeeze(); - _t->_blocks.squeeze(); + _t->_blocks.shrink_to_fit(); _t->_text.squeeze(); } @@ -1301,7 +1319,10 @@ private: i = n; ++n; } - if ((*i)->type() != TextBlockTEmoji && *curr >= 0x590) { + const auto type = (*i)->type(); + if (type != TextBlockTEmoji + && type != TextBlockTCustomEmoji + && *curr >= 0x590) { ignore = false; break; } @@ -1490,8 +1511,13 @@ private: levels[i] = si.analysis.bidiLevel; } if (si.analysis.flags == QScriptAnalysis::Object) { - if (_type == TextBlockTEmoji || _type == TextBlockTSkip) { - si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding()); + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip) { + si.width = currentBlock->f_width() + + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) + ? 0 + : currentBlock->f_rpadding()); } } } @@ -1586,7 +1612,7 @@ private: } } return false; - } else if (_p && _type == TextBlockTEmoji) { + } else if (_p && (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji)) { auto glyphX = x; auto spacesWidth = (si.width - currentBlock->f_width()); if (rtl) { @@ -1636,12 +1662,18 @@ private: if (hasSpoiler) { _p->setOpacity(opacity * (1. - spoilerOpacity)); } - Emoji::Draw( - *_p, - static_cast(currentBlock)->_emoji, - Emoji::GetSizeNormal(), - (glyphX + st::emojiPadding).toInt(), - _y + _yDelta + emojiY); + const auto x = (glyphX + st::emojiPadding).toInt(); + const auto y = _y + _yDelta + emojiY; + if (_type == TextBlockTEmoji) { + Emoji::Draw( + *_p, + static_cast(currentBlock)->_emoji, + Emoji::GetSizeNormal(), + x, + y); + } else if (const auto custom = static_cast(currentBlock)->_custom.get()) { + custom->paint(*_p, x, y); + } } if (hasSpoiler) { _p->setOpacity(opacity * spoilerOpacity); @@ -2014,11 +2046,16 @@ private: } TextBlockType _type = currentBlock->type(); if (si.analysis.flags == QScriptAnalysis::Object) { - if (_type == TextBlockTEmoji || _type == TextBlockTSkip) { + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip) { si.width = currentBlock->f_width() + currentBlock->f_rpadding(); } } - if (_type == TextBlockTEmoji || _type == TextBlockTSkip || _type == TextBlockTNewline) { + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip + || _type == TextBlockTNewline) { if (_wLeft < si.width) { lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis; lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart; @@ -2214,7 +2251,9 @@ private: nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); - if (_type == TextBlockTEmoji || _type == TextBlockTSkip) { + if (_type == TextBlockTEmoji + || _type == TextBlockTCustomEmoji + || _type == TextBlockTSkip) { analysis->script = QChar::Script_Common; analysis->flags = QScriptAnalysis::Object; } else { @@ -2294,7 +2333,8 @@ private: TextBlockType _itype = (*i)->type(); if (eor == _parLength) dir = control.basicDirection(); - else if (_itype == TextBlockTEmoji) + else if (_itype == TextBlockTEmoji + || _itype == TextBlockTCustomEmoji) dir = QChar::DirCS; else if (_itype == TextBlockTSkip) dir = QChar::DirCS; @@ -2329,7 +2369,7 @@ private: QChar::Direction sdir; TextBlockType _stype = (*_parStartBlock)->type(); - if (_stype == TextBlockTEmoji) + if (_stype == TextBlockTEmoji || _stype == TextBlockTCustomEmoji) sdir = QChar::DirCS; else if (_stype == TextBlockTSkip) sdir = QChar::DirCS; @@ -2355,7 +2395,8 @@ private: TextBlockType _itype = (*i)->type(); if (current == (int)_parLength) dirCurrent = control.basicDirection(); - else if (_itype == TextBlockTEmoji) + else if (_itype == TextBlockTEmoji + || _itype == TextBlockTCustomEmoji) dirCurrent = QChar::DirCS; else if (_itype == TextBlockTSkip) dirCurrent = QChar::DirCS; @@ -3356,8 +3397,17 @@ uint16 String::countBlockLength(const String::TextBlocks::const_iterator &i, con return countBlockEnd(i, e) - (*i)->from(); } -template -void String::enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const { +template < + typename AppendPartCallback, + typename ClickHandlerStartCallback, + typename ClickHandlerFinishCallback, + typename FlagsChangeCallback> +void String::enumerateText( + TextSelection selection, + AppendPartCallback appendPartCallback, + ClickHandlerStartCallback clickHandlerStartCallback, + ClickHandlerFinishCallback clickHandlerFinishCallback, + FlagsChangeCallback flagsChangeCallback) const { if (isEmpty() || selection.empty()) { return; } @@ -3437,12 +3487,20 @@ void String::enumerateText(TextSelection selection, AppendPartCallback appendPar break; } - if ((*i)->type() == TextBlockTSkip) continue; + const auto blockType = (*i)->type(); + if (blockType == TextBlockTSkip) continue; auto rangeFrom = qMax(selection.from, blockFrom); - auto rangeTo = qMin(selection.to, uint16(blockFrom + countBlockLength(i, e))); + auto rangeTo = qMin( + selection.to, + uint16(blockFrom + countBlockLength(i, e))); if (rangeTo > rangeFrom) { - appendPartCallback(base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom)); + const auto customEmojiData = (blockType == TextBlockTCustomEmoji) + ? static_cast(i->get())->_custom->entityData() + : QString(); + appendPartCallback( + base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom), + customEmojiData); } } } @@ -3562,11 +3620,21 @@ TextForMimeData String::toText( plainUrl ? QString() : entity.data }); } }; - const auto appendPartCallback = [&](QStringView part) { + const auto appendPartCallback = [&]( + QStringView part, + const QString &customEmojiData) { result.rich.text += part; if (composeExpanded) { result.expanded += part; } + if (composeEntities && !customEmojiData.isEmpty()) { + insertEntity({ + EntityType::CustomEmoji, + result.rich.text.size() - part.size(), + part.size(), + customEmojiData, + }); + } }; enumerateText( diff --git a/ui/text/text.h b/ui/text/text.h index 3927652..fc36995 100644 --- a/ui/text/text.h +++ b/ui/text/text.h @@ -106,9 +106,7 @@ public: const QString &text, const TextParseOptions &options = kDefaultTextOptions, int32 minResizeWidth = QFIXED_MAX); - String(const String &other) = default; String(String &&other) = default; - String &operator=(const String &other) = default; String &operator=(String &&other) = default; ~String() = default; @@ -177,7 +175,7 @@ public: void clear(); private: - using TextBlocks = QVector; + using TextBlocks = std::vector; using TextLinks = QVector; uint16 countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const; diff --git a/ui/text/text_block.cpp b/ui/text/text_block.cpp index a110c15..f3d2275 100644 --- a/ui/text/text_block.cpp +++ b/ui/text/text_block.cpp @@ -348,8 +348,8 @@ AbstractBlock::AbstractBlock( uint16 flags, uint16 lnkIndex, uint16 spoilerIndex) -: _from(from) -, _flags((flags & 0b1111111111) | ((lnkIndex & 0xFFFF) << 14)) +: _flags((flags & 0b1111111111) | ((lnkIndex & 0xFFFF) << 14)) +, _from(from) , _spoilerIndex(spoilerIndex) { } @@ -474,6 +474,30 @@ EmojiBlock::EmojiBlock( } } +CustomEmojiBlock::CustomEmojiBlock( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uint16 flags, + uint16 lnkIndex, + uint16 spoilerIndex, + std::unique_ptr custom) +: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) +, _custom(std::move(custom)) { + _flags |= ((TextBlockTCustomEmoji & 0x0F) << 10); + _width = int(st::emojiSize + 2 * st::emojiPadding); + _rpadding = 0; + for (auto i = length; i != 0;) { + auto ch = str[_from + (--i)]; + if (ch.unicode() == QChar::Space) { + _rpadding += font->spacew; + } else { + break; + } + } +} + NewlineBlock::NewlineBlock( const style::font &font, const QString &str, @@ -546,25 +570,6 @@ Block::Block() { Unexpected("Should not be called."); } -Block::Block(const Block &other) { - switch (other->type()) { - case TextBlockTNewline: - emplace(other.unsafe()); - break; - case TextBlockTText: - emplace(other.unsafe()); - break; - case TextBlockTEmoji: - emplace(other.unsafe()); - break; - case TextBlockTSkip: - emplace(other.unsafe()); - break; - default: - Unexpected("Bad text block type in Block(const Block&)."); - } -} - Block::Block(Block &&other) { switch (other->type()) { case TextBlockTNewline: @@ -576,6 +581,9 @@ Block::Block(Block &&other) { case TextBlockTEmoji: emplace(std::move(other.unsafe())); break; + case TextBlockTCustomEmoji: + emplace(std::move(other.unsafe())); + break; case TextBlockTSkip: emplace(std::move(other.unsafe())); break; @@ -584,30 +592,6 @@ Block::Block(Block &&other) { } } -Block &Block::operator=(const Block &other) { - if (&other == this) { - return *this; - } - destroy(); - switch (other->type()) { - case TextBlockTNewline: - emplace(other.unsafe()); - break; - case TextBlockTText: - emplace(other.unsafe()); - break; - case TextBlockTEmoji: - emplace(other.unsafe()); - break; - case TextBlockTSkip: - emplace(other.unsafe()); - break; - default: - Unexpected("Bad text block type in operator=(const Block&)."); - } - return *this; -} - Block &Block::operator=(Block &&other) { if (&other == this) { return *this; @@ -623,6 +607,9 @@ Block &Block::operator=(Block &&other) { case TextBlockTEmoji: emplace(std::move(other.unsafe())); break; + case TextBlockTCustomEmoji: + emplace(std::move(other.unsafe())); + break; case TextBlockTSkip: emplace(std::move(other.unsafe())); break; @@ -694,6 +681,26 @@ Block Block::Emoji( emoji); } +Block Block::CustomEmoji( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uint16 flags, + uint16 lnkIndex, + uint16 spoilerIndex, + std::unique_ptr custom) { + return New( + font, + str, + from, + length, + flags, + lnkIndex, + spoilerIndex, + std::move(custom)); +} + Block Block::Skip( const style::font &font, const QString &str, @@ -740,6 +747,9 @@ void Block::destroy() { case TextBlockTEmoji: unsafe().~EmojiBlock(); break; + case TextBlockTCustomEmoji: + unsafe().~CustomEmojiBlock(); + break; case TextBlockTSkip: unsafe().~SkipBlock(); break; diff --git a/ui/text/text_block.h b/ui/text/text_block.h index c02d68c..249bc91 100644 --- a/ui/text/text_block.h +++ b/ui/text/text_block.h @@ -18,7 +18,8 @@ enum TextBlockType { TextBlockTNewline = 0x01, TextBlockTText = 0x02, TextBlockTEmoji = 0x03, - TextBlockTSkip = 0x04, + TextBlockTCustomEmoji = 0x04, + TextBlockTSkip = 0x05, }; enum TextBlockFlags { @@ -63,10 +64,8 @@ protected: uint16 lnkIndex, uint16 spoilerIndex); - uint16 _from = 0; - uint32 _flags = 0; // 2 bits empty, 16 bits lnkIndex, 4 bits type, 10 bits flags - + uint16 _from = 0; uint16 _spoilerIndex = 0; QFixed _width = 0; @@ -165,6 +164,35 @@ private: }; +class CustomEmoji { +public: + virtual ~CustomEmoji() = default; + [[nodiscard]] virtual QString entityData() = 0; + virtual void paint(QPainter &p, int x, int y) = 0; + +}; + +class CustomEmojiBlock final : public AbstractBlock { +public: + CustomEmojiBlock( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uint16 flags, + uint16 lnkIndex, + uint16 spoilerIndex, + std::unique_ptr custom); + +private: + std::unique_ptr _custom; + + friend class String; + friend class Parser; + friend class Renderer; + +}; + class SkipBlock final : public AbstractBlock { public: SkipBlock( @@ -190,9 +218,7 @@ private: class Block final { public: Block(); - Block(const Block &other); Block(Block &&other); - Block &operator=(const Block &other); Block &operator=(Block &&other); ~Block(); @@ -225,6 +251,16 @@ public: uint16 spoilerIndex, EmojiPtr emoji); + [[nodiscard]] static Block CustomEmoji( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uint16 flags, + uint16 lnkIndex, + uint16 spoilerIndex, + std::unique_ptr custom); + [[nodiscard]] static Block Skip( const style::font &font, const QString &str, @@ -278,6 +314,8 @@ private: static_assert(alignof(NewlineBlock) <= alignof(void*)); static_assert(sizeof(EmojiBlock) <= sizeof(TextBlock)); static_assert(alignof(EmojiBlock) <= alignof(void*)); + static_assert(sizeof(CustomEmojiBlock) <= sizeof(TextBlock)); + static_assert(alignof(CustomEmojiBlock) <= alignof(void*)); static_assert(sizeof(SkipBlock) <= sizeof(TextBlock)); static_assert(alignof(SkipBlock) <= alignof(void*)); diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 7c794ec..fc406ab 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -23,6 +23,7 @@ enum class EntityType : uchar { Cashtag, Mention, MentionName, + CustomEmoji, BotCommand, MediaTimestamp, PlainLink, // Senders in chat list, attachements in chat list, etc.