Add initial support for custom emoji.

This commit is contained in:
John Preston 2022-06-23 15:06:37 +04:00
parent b90d7ee27a
commit 87cd0b6127
7 changed files with 212 additions and 83 deletions

View file

@ -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<ClickHandler> Integration::createLinkHandler(
return nullptr;
}
std::unique_ptr<Text::CustomEmoji> Integration::createCustomEmoji(
const QString &data,
const std::any &context) {
return nullptr;
}
bool Integration::handleUrlClick(
const QString &url,
const QVariant &context) {

View file

@ -27,6 +27,10 @@ namespace Emoji {
class One;
} // namespace Emoji
namespace Text {
class CustomEmoji;
} // namespace Text
class Integration {
public:
static void Set(not_null<Integration*> 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<Text::CustomEmoji>;
[[nodiscard]] virtual rpl::producer<> forcePopupMenuHideRequests();

View file

@ -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<const EmojiBlock*>(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<const EmojiBlock*>(currentBlock)->_emoji,
Emoji::GetSizeNormal(),
x,
y);
} else if (const auto custom = static_cast<const CustomEmojiBlock*>(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 <typename AppendPartCallback, typename ClickHandlerStartCallback, typename ClickHandlerFinishCallback, typename FlagsChangeCallback>
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<const CustomEmojiBlock*>(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(

View file

@ -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<Block>;
using TextBlocks = std::vector<Block>;
using TextLinks = QVector<ClickHandlerPtr>;
uint16 countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const;

View file

@ -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<CustomEmoji> 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<NewlineBlock>(other.unsafe<NewlineBlock>());
break;
case TextBlockTText:
emplace<TextBlock>(other.unsafe<TextBlock>());
break;
case TextBlockTEmoji:
emplace<EmojiBlock>(other.unsafe<EmojiBlock>());
break;
case TextBlockTSkip:
emplace<SkipBlock>(other.unsafe<SkipBlock>());
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<EmojiBlock>(std::move(other.unsafe<EmojiBlock>()));
break;
case TextBlockTCustomEmoji:
emplace<CustomEmojiBlock>(std::move(other.unsafe<CustomEmojiBlock>()));
break;
case TextBlockTSkip:
emplace<SkipBlock>(std::move(other.unsafe<SkipBlock>()));
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<NewlineBlock>(other.unsafe<NewlineBlock>());
break;
case TextBlockTText:
emplace<TextBlock>(other.unsafe<TextBlock>());
break;
case TextBlockTEmoji:
emplace<EmojiBlock>(other.unsafe<EmojiBlock>());
break;
case TextBlockTSkip:
emplace<SkipBlock>(other.unsafe<SkipBlock>());
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<EmojiBlock>(std::move(other.unsafe<EmojiBlock>()));
break;
case TextBlockTCustomEmoji:
emplace<CustomEmojiBlock>(std::move(other.unsafe<CustomEmojiBlock>()));
break;
case TextBlockTSkip:
emplace<SkipBlock>(std::move(other.unsafe<SkipBlock>()));
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<Text::CustomEmoji> custom) {
return New<CustomEmojiBlock>(
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>().~EmojiBlock();
break;
case TextBlockTCustomEmoji:
unsafe<CustomEmojiBlock>().~CustomEmojiBlock();
break;
case TextBlockTSkip:
unsafe<SkipBlock>().~SkipBlock();
break;

View file

@ -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<CustomEmoji> custom);
private:
std::unique_ptr<CustomEmoji> _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<CustomEmoji> 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*));

View file

@ -23,6 +23,7 @@ enum class EntityType : uchar {
Cashtag,
Mention,
MentionName,
CustomEmoji,
BotCommand,
MediaTimestamp,
PlainLink, // Senders in chat list, attachements in chat list, etc.