/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/text/text.h" #include "core/click_handler_types.h" #include "core/crash_reports.h" #include "ui/text/text_block.h" #include "ui/emoji_config.h" #include "lang/lang_keys.h" #include "platform/platform_info.h" #include "boxes/confirm_box.h" #include "mainwindow.h" #include #include namespace Ui { namespace Text { namespace { constexpr auto kStringLinkIndexShift = uint16(0x8000); Qt::LayoutDirection StringDirection(const QString &str, int32 from, int32 to) { const ushort *p = reinterpret_cast(str.unicode()) + from; const ushort *end = p + (to - from); while (p < end) { uint ucs4 = *p; if (QChar::isHighSurrogate(ucs4) && p < end - 1) { ushort low = p[1]; if (QChar::isLowSurrogate(low)) { ucs4 = QChar::surrogateToUcs4(ucs4, low); ++p; } } switch (QChar::direction(ucs4)) { case QChar::DirL: return Qt::LeftToRight; case QChar::DirR: case QChar::DirAL: return Qt::RightToLeft; default: break; } ++p; } return Qt::LayoutDirectionAuto; } TextWithEntities PrepareRichFromPlain( const QString &text, const TextParseOptions &options) { auto result = TextWithEntities{ text }; if (options.flags & TextParseLinks) { TextUtilities::ParseEntities( result, options.flags, (options.flags & TextParseRichText)); } return result; } TextWithEntities PrepareRichFromRich( const TextWithEntities &text, const TextParseOptions &options) { auto result = text; const auto &preparsed = text.entities; if ((options.flags & TextParseLinks) && !preparsed.isEmpty()) { bool parseMentions = (options.flags & TextParseMentions); bool parseHashtags = (options.flags & TextParseHashtags); bool parseBotCommands = (options.flags & TextParseBotCommands); bool parseMarkdown = (options.flags & TextParseMarkdown); if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) { int32 i = 0, l = preparsed.size(); result.entities.clear(); result.entities.reserve(l); const QChar s = result.text.size(); for (; i < l; ++i) { auto type = preparsed.at(i).type(); if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) || (type == EntityType::Hashtag && !parseHashtags) || (type == EntityType::Cashtag && !parseHashtags) || (type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities (!parseMarkdown && (type == EntityType::Bold || type == EntityType::Italic || type == EntityType::Underline || type == EntityType::StrikeOut || type == EntityType::Code || type == EntityType::Pre))) { continue; } result.entities.push_back(preparsed.at(i)); } } } return result; } QFixed ComputeStopAfter(const TextParseOptions &options, const style::TextStyle &st) { return (options.maxw > 0 && options.maxh > 0) ? ((options.maxh / st.font->height) + 1) * options.maxw : QFIXED_MAX; } // Open Sans tilde fix. bool ComputeCheckTilde(const style::TextStyle &st) { const auto &font = st.font; return (font->size() * cIntRetinaFactor() == 13) && (font->flags() == 0) && (font->f.family() == qstr("Open Sans")); } } // namespace } // namespace Text } // namespace Ui bool chIsBad(QChar ch) { return (ch == 0) || (ch >= 8232 && ch < 8237) || (ch >= 65024 && ch < 65040 && ch != 65039) || (ch >= 127 && ch < 160 && ch != 156) || (Platform::IsMac() && !Platform::IsMac10_7OrGreater() && (ch == 8207 || ch == 8206 || ch == 8288)) // qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551 || (Platform::IsMac() && ch == 6158) // tmp hack see https://bugreports.qt.io/browse/QTBUG-48910 || (Platform::IsMac10_11OrGreater() && !Platform::IsMac10_12OrGreater() && ch >= 0x0B00 && ch <= 0x0B7F && chIsDiac(ch)); } QString textcmdSkipBlock(ushort w, ushort h) { static QString cmd(5, TextCommand); cmd[1] = QChar(TextCommandSkipBlock); cmd[2] = QChar(w); cmd[3] = QChar(h); return cmd; } QString textcmdStartLink(ushort lnkIndex) { static QString cmd(4, TextCommand); cmd[1] = QChar(TextCommandLinkIndex); cmd[2] = QChar(lnkIndex); return cmd; } QString textcmdStartLink(const QString &url) { if (url.size() >= 4096) return QString(); QString result; result.reserve(url.size() + 4); return result.append(TextCommand).append(QChar(TextCommandLinkText)).append(QChar(url.size())).append(url).append(TextCommand); } QString textcmdStopLink() { return textcmdStartLink(0); } QString textcmdLink(ushort lnkIndex, const QString &text) { QString result; result.reserve(4 + text.size() + 4); return result.append(textcmdStartLink(lnkIndex)).append(text).append(textcmdStopLink()); } QString textcmdLink(const QString &url, const QString &text) { QString result; result.reserve(4 + url.size() + text.size() + 4); return result.append(textcmdStartLink(url)).append(text).append(textcmdStopLink()); } QString textcmdStartSemibold() { QString result; result.reserve(3); return result.append(TextCommand).append(QChar(TextCommandSemibold)).append(TextCommand); } QString textcmdStopSemibold() { QString result; result.reserve(3); return result.append(TextCommand).append(QChar(TextCommandNoSemibold)).append(TextCommand); } const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink) { const QChar *result = from + 1; if (*from != TextCommand || result >= end) return from; ushort cmd = result->unicode(); ++result; if (result >= end) return from; switch (cmd) { case TextCommandBold: case TextCommandNoBold: case TextCommandSemibold: case TextCommandNoSemibold: case TextCommandItalic: case TextCommandNoItalic: case TextCommandUnderline: case TextCommandNoUnderline: break; case TextCommandLinkIndex: if (result->unicode() > 0x7FFF) return from; ++result; break; case TextCommandLinkText: { ushort len = result->unicode(); if (len >= 4096 || !canLink) return from; result += len + 1; } break; case TextCommandSkipBlock: result += 2; break; case TextCommandLangTag: result += 1; break; } return (result < end && *result == TextCommand) ? (result + 1) : from; } const TextParseOptions _defaultOptions = { TextParseLinks | TextParseMultiline, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; const TextParseOptions _textPlainOptions = { TextParseMultiline, // flags 0, // maxw 0, // maxh Qt::LayoutDirectionAuto, // dir }; namespace Ui { namespace Text { class Parser { public: Parser( not_null string, const QString &text, const TextParseOptions &options); Parser( not_null string, const TextWithEntities &textWithEntities, const TextParseOptions &options); private: struct ReadyToken { }; enum LinkDisplayStatus { LinkDisplayedFull, LinkDisplayedElided, }; struct TextLinkData { TextLinkData() = default; TextLinkData( EntityType type, const QString &text, const QString &data, LinkDisplayStatus displayStatus); EntityType type = EntityType::Invalid; QString text, data; LinkDisplayStatus displayStatus = LinkDisplayedFull; }; class StartedEntity { public: explicit StartedEntity(TextBlockFlags flags); explicit StartedEntity(uint16 lnkIndex); std::optional flags() const; std::optional lnkIndex() const; private: int _value = 0; }; Parser( not_null string, TextWithEntities &&source, const TextParseOptions &options, ReadyToken); void trimSourceRange(); void blockCreated(); void createBlock(int32 skipBack = 0); void createSkipBlock(int32 w, int32 h); void createNewlineBlock(); bool checkCommand(); // Returns true if at least one entity was parsed in the current position. bool checkEntities(); bool readSkipBlockCommand(); bool readCommand(); void parseCurrentChar(); void parseEmojiFromCurrent(); void checkForElidedSkipBlock(); void finalize(const TextParseOptions &options); void finishEntities(); void skipPassedEntities(); void skipBadEntities(); bool isInvalidEntity(const EntityInText &entity) const; bool isLinkEntity(const EntityInText &entity) const; void parse(const TextParseOptions &options); void computeLinkText( const QString &linkData, QString *outLinkText, LinkDisplayStatus *outDisplayStatus); static ClickHandlerPtr CreateHandlerForLink( const TextLinkData &link, const TextParseOptions &options); const not_null _t; const TextWithEntities _source; const QChar * const _start = nullptr; const QChar *_end = nullptr; // mutable, because we trim by decrementing. const QChar *_ptr = nullptr; const EntitiesInText::const_iterator _entitiesEnd; EntitiesInText::const_iterator _waitingEntity; const bool _rich = false; const bool _multiline = false; const QFixed _stopAfterWidth; // summary width of all added words const bool _checkTilde = false; // do we need a special text block for tilde symbol std::vector _links; base::flat_map< const QChar*, std::vector> _startedEntities; uint16 _maxLnkIndex = 0; // current state int32 _flags = 0; uint16 _lnkIndex = 0; EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero int32 _blockStart = 0; // offset in result, from which current parsed block is started int32 _diacs = 0; // diac chars skipped without good char QFixed _sumWidth; bool _sumFinished = false; bool _newlineAwaited = false; // current char data QChar _ch; // current char (low surrogate, if current char is surrogate pair) int32 _emojiLookback = 0; // how far behind the current ptr to look for current emoji bool _lastSkipped = false; // did we skip current char }; Parser::TextLinkData::TextLinkData( EntityType type, const QString &text, const QString &data, LinkDisplayStatus displayStatus) : type(type) , text(text) , data(data) , displayStatus(displayStatus) { } Parser::StartedEntity::StartedEntity(TextBlockFlags flags) : _value(flags) { Expects(_value >= 0 && _value < int(kStringLinkIndexShift)); } Parser::StartedEntity::StartedEntity(uint16 lnkIndex) : _value(lnkIndex) { Expects(_value >= kStringLinkIndexShift); } std::optional Parser::StartedEntity::flags() const { if (_value < int(kStringLinkIndexShift)) { return TextBlockFlags(_value); } return std::nullopt; } std::optional Parser::StartedEntity::lnkIndex() const { if (_value >= int(kStringLinkIndexShift)) { return uint16(_value); } return std::nullopt; } Parser::Parser( not_null string, const QString &text, const TextParseOptions &options) : Parser( string, PrepareRichFromPlain(text, options), options, ReadyToken()) { } Parser::Parser( not_null string, const TextWithEntities &textWithEntities, const TextParseOptions &options) : Parser( string, PrepareRichFromRich(textWithEntities, options), options, ReadyToken()) { } Parser::Parser( not_null string, TextWithEntities &&source, const TextParseOptions &options, ReadyToken) : _t(string) , _source(std::move(source)) , _start(_source.text.constData()) , _end(_start + _source.text.size()) , _ptr(_start) , _entitiesEnd(_source.entities.end()) , _waitingEntity(_source.entities.begin()) , _rich(options.flags & TextParseRichText) , _multiline(options.flags & TextParseMultiline) , _stopAfterWidth(ComputeStopAfter(options, *_t->_st)) , _checkTilde(ComputeCheckTilde(*_t->_st)) { parse(options); } void Parser::blockCreated() { _sumWidth += _t->_blocks.back()->f_width(); if (_sumWidth.floor().toInt() > _stopAfterWidth) { _sumFinished = true; } } void Parser::createBlock(int32 skipBack) { if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) { _maxLnkIndex = _lnkIndex; } int32 len = int32(_t->_text.size()) + skipBack - _blockStart; if (len > 0) { bool newline = !_emoji && (len == 1 && _t->_text.at(_blockStart) == QChar::LineFeed); if (_newlineAwaited) { _newlineAwaited = false; if (!newline) { _t->_text.insert(_blockStart, QChar::LineFeed); createBlock(skipBack - len); } } _lastSkipped = false; if (_emoji) { _t->_blocks.push_back(std::make_unique(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _emoji)); _emoji = nullptr; _lastSkipped = true; } else if (newline) { _t->_blocks.push_back(std::make_unique(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex)); } else { _t->_blocks.push_back(std::make_unique(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex)); } _blockStart += len; blockCreated(); } } void Parser::createSkipBlock(int32 w, int32 h) { createBlock(); _t->_text.push_back('_'); _t->_blocks.push_back(std::make_unique(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex)); blockCreated(); } void Parser::createNewlineBlock() { createBlock(); _t->_text.push_back(QChar::LineFeed); createBlock(); } bool Parser::checkCommand() { bool result = false; for (QChar c = ((_ptr < _end) ? *_ptr : 0); c == TextCommand; c = ((_ptr < _end) ? *_ptr : 0)) { if (!readCommand()) { break; } result = true; } return result; } void Parser::finishEntities() { while (!_startedEntities.empty() && (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) { auto list = std::move(_startedEntities.begin()->second); _startedEntities.erase(_startedEntities.begin()); while (!list.empty()) { if (const auto flags = list.back().flags()) { if (_flags & (*flags)) { createBlock(); _flags &= ~(*flags); if (((*flags) & TextBlockFPre) && !_t->_blocks.empty() && _t->_blocks.back()->type() != TextBlockTNewline) { _newlineAwaited = true; } } } else if (const auto lnkIndex = list.back().lnkIndex()) { if (_lnkIndex == *lnkIndex) { createBlock(); _lnkIndex = 0; } } list.pop_back(); } } } // Returns true if at least one entity was parsed in the current position. bool Parser::checkEntities() { finishEntities(); skipPassedEntities(); if (_waitingEntity == _entitiesEnd || _ptr < _start + _waitingEntity->offset()) { return false; } auto flags = TextBlockFlags(); auto link = TextLinkData(); const auto entityType = _waitingEntity->type(); const auto entityLength = _waitingEntity->length(); const auto entityBegin = _start + _waitingEntity->offset(); const auto entityEnd = entityBegin + entityLength; if (entityType == EntityType::Bold) { flags = TextBlockFSemibold; } else if (entityType == EntityType::Italic) { flags = TextBlockFItalic; } else if (entityType == EntityType::Underline) { flags = TextBlockFUnderline; } else if (entityType == EntityType::StrikeOut) { flags = TextBlockFStrikeOut; } else if (entityType == EntityType::Code) { // #TODO entities flags = TextBlockFCode; } else if (entityType == EntityType::Pre) { flags = TextBlockFPre; createBlock(); if (!_t->_blocks.empty() && _t->_blocks.back()->type() != TextBlockTNewline) { createNewlineBlock(); } } else if (entityType == EntityType::Url || entityType == EntityType::Email || entityType == EntityType::Mention || entityType == EntityType::Hashtag || entityType == EntityType::Cashtag || entityType == EntityType::BotCommand) { link.type = entityType; link.data = QString(entityBegin, entityLength); if (link.type == EntityType::Url) { computeLinkText(link.data, &link.text, &link.displayStatus); } else { link.text = link.data; } } else if (entityType == EntityType::CustomUrl || entityType == EntityType::MentionName) { link.type = entityType; link.data = _waitingEntity->data(); link.text = QString(_start + _waitingEntity->offset(), _waitingEntity->length()); } if (link.type != EntityType::Invalid) { createBlock(); _links.push_back(link); _lnkIndex = kStringLinkIndexShift + _links.size(); _startedEntities[entityEnd].emplace_back(_lnkIndex); } else if (flags) { if (!(_flags & flags)) { createBlock(); _flags |= flags; _startedEntities[entityEnd].emplace_back(flags); } } ++_waitingEntity; skipBadEntities(); return true; } void Parser::skipPassedEntities() { while (_waitingEntity != _entitiesEnd && _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) { ++_waitingEntity; } } void Parser::skipBadEntities() { if (_links.size() >= 0x7FFF) { while (_waitingEntity != _entitiesEnd && (isLinkEntity(*_waitingEntity) || isInvalidEntity(*_waitingEntity))) { ++_waitingEntity; } } else { while (_waitingEntity != _entitiesEnd && isInvalidEntity(*_waitingEntity)) { ++_waitingEntity; } } } bool Parser::readSkipBlockCommand() { const QChar *afterCmd = textSkipCommand(_ptr, _end, _links.size() < 0x7FFF); if (afterCmd == _ptr) { return false; } ushort cmd = (++_ptr)->unicode(); ++_ptr; switch (cmd) { case TextCommandSkipBlock: createSkipBlock(_ptr->unicode(), (_ptr + 1)->unicode()); break; } _ptr = afterCmd; return true; } bool Parser::readCommand() { const QChar *afterCmd = textSkipCommand(_ptr, _end, _links.size() < 0x7FFF); if (afterCmd == _ptr) { return false; } ushort cmd = (++_ptr)->unicode(); ++_ptr; switch (cmd) { case TextCommandBold: if (!(_flags & TextBlockFBold)) { createBlock(); _flags |= TextBlockFBold; } break; case TextCommandNoBold: if (_flags & TextBlockFBold) { createBlock(); _flags &= ~TextBlockFBold; } break; case TextCommandSemibold: if (!(_flags & TextBlockFSemibold)) { createBlock(); _flags |= TextBlockFSemibold; } break; case TextCommandNoSemibold: if (_flags & TextBlockFSemibold) { createBlock(); _flags &= ~TextBlockFSemibold; } break; case TextCommandItalic: if (!(_flags & TextBlockFItalic)) { createBlock(); _flags |= TextBlockFItalic; } break; case TextCommandNoItalic: if (_flags & TextBlockFItalic) { createBlock(); _flags &= ~TextBlockFItalic; } break; case TextCommandUnderline: if (!(_flags & TextBlockFUnderline)) { createBlock(); _flags |= TextBlockFUnderline; } break; case TextCommandNoUnderline: if (_flags & TextBlockFUnderline) { createBlock(); _flags &= ~TextBlockFUnderline; } break; case TextCommandStrikeOut: if (!(_flags & TextBlockFStrikeOut)) { createBlock(); _flags |= TextBlockFStrikeOut; } break; case TextCommandNoStrikeOut: if (_flags & TextBlockFStrikeOut) { createBlock(); _flags &= ~TextBlockFStrikeOut; } break; case TextCommandLinkIndex: if (_ptr->unicode() != _lnkIndex) { createBlock(); _lnkIndex = _ptr->unicode(); } break; case TextCommandLinkText: { createBlock(); int32 len = _ptr->unicode(); _links.emplace_back(EntityType::CustomUrl, QString(), QString(++_ptr, len), LinkDisplayedFull); _lnkIndex = kStringLinkIndexShift + _links.size(); } break; case TextCommandSkipBlock: createSkipBlock(_ptr->unicode(), (_ptr + 1)->unicode()); break; } _ptr = afterCmd; return true; } void Parser::parseCurrentChar() { _ch = ((_ptr < _end) ? *_ptr : 0); _emojiLookback = 0; const auto isNewLine = _multiline && chIsNewline(_ch); const auto isSpace = chIsSpace(_ch); const auto isDiac = chIsDiac(_ch); const auto isTilde = _checkTilde && (_ch == '~'); const auto skip = [&] { if (chIsBad(_ch) || _ch.isLowSurrogate()) { return true; } else if (_ch == 0xFE0F && Platform::IsMac()) { // Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :( return true; } else if (isDiac) { if (_lastSkipped || _emoji || ++_diacs > chMaxDiacAfterSymbol()) { return true; } } else if (_ch.isHighSurrogate()) { if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) { return true; } } return false; }(); if (_ch.isHighSurrogate() && !skip) { _t->_text.push_back(_ch); ++_ptr; _ch = *_ptr; _emojiLookback = 1; } _lastSkipped = skip; if (skip) { _ch = 0; } else { if (isTilde) { // tilde fix in OpenSans if (!(_flags & TextBlockFTilde)) { createBlock(-_emojiLookback); _flags |= TextBlockFTilde; } } else { if (_flags & TextBlockFTilde) { createBlock(-_emojiLookback); _flags &= ~TextBlockFTilde; } } if (isNewLine) { createNewlineBlock(); } else if (isSpace) { _t->_text.push_back(QChar::Space); } else { if (_emoji) { createBlock(-_emojiLookback); } _t->_text.push_back(_ch); } if (!isDiac) _diacs = 0; } } void Parser::parseEmojiFromCurrent() { int len = 0; auto e = Ui::Emoji::Find(_ptr - _emojiLookback, _end, &len); if (!e) return; for (int l = len - _emojiLookback - 1; l > 0; --l) { _t->_text.push_back(*++_ptr); } if (e->hasPostfix()) { Assert(!_t->_text.isEmpty()); const auto last = _t->_text[_t->_text.size() - 1]; if (last.unicode() != Ui::Emoji::kPostfix) { _t->_text.push_back(QChar(Ui::Emoji::kPostfix)); ++len; } } createBlock(-len); _emoji = e; } bool Parser::isInvalidEntity(const EntityInText &entity) const { const auto length = entity.length(); return (_start + entity.offset() + length > _end) || (length <= 0); } bool Parser::isLinkEntity(const EntityInText &entity) const { const auto type = entity.type(); const auto urls = { EntityType::Url, EntityType::CustomUrl, EntityType::Email, EntityType::Hashtag, EntityType::Cashtag, EntityType::Mention, EntityType::MentionName, EntityType::BotCommand }; return ranges::find(urls, type) != std::end(urls); } void Parser::parse(const TextParseOptions &options) { skipBadEntities(); trimSourceRange(); _t->_text.resize(0); _t->_text.reserve(_end - _ptr); for (; _ptr <= _end; ++_ptr) { while (checkEntities() || (_rich && checkCommand())) { } parseCurrentChar(); parseEmojiFromCurrent(); if (_sumFinished || _t->_text.size() >= 0x8000) { break; // 32k max } } createBlock(); checkForElidedSkipBlock(); finalize(options); } void Parser::trimSourceRange() { const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset( _source.entities, _end - _start); while (_ptr != _end && chIsTrimmed(*_ptr, _rich) && _ptr != _start + firstMonospaceOffset) { ++_ptr; } while (_ptr != _end && chIsTrimmed(*(_end - 1), _rich)) { --_end; } } void Parser::checkForElidedSkipBlock() { if (!_sumFinished || !_rich) { return; } // We could've skipped the final skip block command. for (; _ptr < _end; ++_ptr) { if (*_ptr == TextCommand && readSkipBlockCommand()) { break; } } } void Parser::finalize(const TextParseOptions &options) { _t->_links.resize(_maxLnkIndex); for (const auto &block : _t->_blocks) { const auto b = block.get(); const auto shiftedIndex = b->lnkIndex(); if (shiftedIndex <= kStringLinkIndexShift) { continue; } const auto realIndex = (shiftedIndex - kStringLinkIndexShift); const auto index = _maxLnkIndex + realIndex; b->setLnkIndex(index); if (_t->_links.size() >= index) { continue; } _t->_links.resize(index); const auto handler = CreateHandlerForLink( _links[realIndex - 1], options); if (handler) { _t->setLink(index, handler); } } _t->_links.squeeze(); _t->_blocks.shrink_to_fit(); _t->_text.squeeze(); } void Parser::computeLinkText(const QString &linkData, QString *outLinkText, LinkDisplayStatus *outDisplayStatus) { auto url = QUrl(linkData); auto good = QUrl(url.isValid() ? url.toEncoded() : QByteArray()); auto readable = good.isValid() ? good.toDisplayString() : linkData; *outLinkText = _t->_st->font->elided(readable, st::linkCropLimit); *outDisplayStatus = (*outLinkText == readable) ? LinkDisplayedFull : LinkDisplayedElided; } ClickHandlerPtr Parser::CreateHandlerForLink( const TextLinkData &link, const TextParseOptions &options) { switch (link.type) { case EntityType::CustomUrl: return !link.data.isEmpty() ? std::make_shared(link.data) : nullptr; case EntityType::Email: case EntityType::Url: return std::make_shared( link.data, link.displayStatus == LinkDisplayedFull); case EntityType::BotCommand: return std::make_shared(link.data); case EntityType::Hashtag: if (options.flags & TextTwitterMentions) { return std::make_shared( (qsl("https://twitter.com/hashtag/") + link.data.mid(1) + qsl("?src=hash")), true); } else if (options.flags & TextInstagramMentions) { return std::make_shared( (qsl("https://instagram.com/explore/tags/") + link.data.mid(1) + '/'), true); } return std::make_shared(link.data); case EntityType::Cashtag: return std::make_shared(link.data); case EntityType::Mention: if (options.flags & TextTwitterMentions) { return std::make_shared( qsl("https://twitter.com/") + link.data.mid(1), true); } else if (options.flags & TextInstagramMentions) { return std::make_shared( qsl("https://instagram.com/") + link.data.mid(1) + '/', true); } return std::make_shared(link.data); case EntityType::MentionName: { auto fields = TextUtilities::MentionNameDataToFields(link.data); if (fields.userId) { return std::make_shared( link.text, fields.userId, fields.accessHash); } else { LOG(("Bad mention name: %1").arg(link.data)); } } break; } return nullptr; } namespace { // COPIED FROM qtextengine.cpp AND MODIFIED struct BidiStatus { BidiStatus() { eor = QChar::DirON; lastStrong = QChar::DirON; last = QChar:: DirON; dir = QChar::DirON; } QChar::Direction eor; QChar::Direction lastStrong; QChar::Direction last; QChar::Direction dir; }; enum { _MaxBidiLevel = 61 }; enum { _MaxItemLength = 4096 }; struct BidiControl { inline BidiControl(bool rtl) : base(rtl ? 1 : 0), level(rtl ? 1 : 0) {} inline void embed(bool rtl, bool o = false) { unsigned int toAdd = 1; if((level%2 != 0) == rtl ) { ++toAdd; } if (level + toAdd <= _MaxBidiLevel) { ctx[cCtx].level = level; ctx[cCtx].override = override; cCtx++; override = o; level += toAdd; } } inline bool canPop() const { return cCtx != 0; } inline void pdf() { Q_ASSERT(cCtx); --cCtx; level = ctx[cCtx].level; override = ctx[cCtx].override; } inline QChar::Direction basicDirection() const { return (base ? QChar::DirR : QChar:: DirL); } inline unsigned int baseLevel() const { return base; } inline QChar::Direction direction() const { return ((level%2) ? QChar::DirR : QChar:: DirL); } struct { unsigned int level = 0; bool override = false; } ctx[_MaxBidiLevel]; unsigned int cCtx = 0; const unsigned int base; unsigned int level; bool override = false; }; static void eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) { if (start > stop) return; int level = control.level; if(dir != QChar::DirON && !control.override) { // add level of run (cases I1 & I2) if(level % 2) { if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN) level++; } else { if(dir == QChar::DirR) level++; else if(dir == QChar::DirAN || dir == QChar::DirEN) level += 2; } } QScriptAnalysis *s = analysis + start; const QScriptAnalysis *e = analysis + stop; while (s <= e) { s->bidiLevel = level; ++s; } ++stop; start = stop; } inline int32 countBlockHeight(const AbstractBlock *b, const style::TextStyle *st) { return (b->type() == TextBlockTSkip) ? static_cast(b)->height() : (st->lineHeight > st->font->height) ? st->lineHeight : st->font->height; } } // namespace class Renderer { public: Renderer(Painter *p, const String *t) : _p(p) , _t(t) , _originalPen(p ? p->pen() : QPen()) { } ~Renderer() { restoreAfterElided(); if (_p) { _p->setPen(_originalPen); } } void draw(int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) { if (_t->isEmpty()) return; _blocksSize = _t->_blocks.size(); if (_p) { _p->setFont(_t->_st->font); _textPalette = &_p->textPalette(); _originalPenSelected = (_textPalette->selectFg->c.alphaF() == 0) ? _originalPen : _textPalette->selectFg->p; } _x = left; _y = top; _yFrom = yFrom + top; _yTo = (yTo < 0) ? -1 : (yTo + top); _selection = selection; _fullWidthSelection = fullWidthSelection; _wLeft = _w = w; if (_elideLast) { _yToElide = _yTo; if (_elideRemoveFromEnd > 0 && !_t->_blocks.empty()) { int firstBlockHeight = countBlockHeight(_t->_blocks.front().get(), _t->_st); if (_y + firstBlockHeight >= _yToElide) { _wLeft -= _elideRemoveFromEnd; } } } _str = _t->_text.unicode(); if (_p) { auto clip = _p->hasClipping() ? _p->clipBoundingRect() : QRect(); if (clip.width() > 0 || clip.height() > 0) { if (_yFrom < clip.y()) _yFrom = clip.y(); if (_yTo < 0 || _yTo > clip.y() + clip.height()) _yTo = clip.y() + clip.height(); } } _align = align; _parDirection = _t->_startDir; if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = cLangDir(); if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) { initNextParagraph(_t->_blocks.cbegin()); } _lineStart = 0; _lineStartBlock = 0; _lineHeight = 0; _fontHeight = _t->_st->font->height; auto last_rBearing = QFixed(0); _last_rPadding = QFixed(0); auto blockIndex = 0; bool longWordLine = true; auto e = _t->_blocks.cend(); for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) { auto b = i->get(); auto _btype = b->type(); auto blockHeight = countBlockHeight(b, _t->_st); if (_btype == TextBlockTNewline) { if (!_lineHeight) _lineHeight = blockHeight; if (!drawLine((*i)->from(), i, e)) { return; } _y += _lineHeight; _lineHeight = 0; _lineStart = _t->countBlockEnd(i, e); _lineStartBlock = blockIndex + 1; last_rBearing = b->f_rbearing(); _last_rPadding = b->f_rpadding(); _wLeft = _w - (b->f_width() - last_rBearing); if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { _wLeft -= _elideRemoveFromEnd; } _parDirection = static_cast(b)->nextDirection(); if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = cLangDir(); initNextParagraph(i + 1); longWordLine = true; continue; } auto b__f_rbearing = b->f_rbearing(); auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing); if (newWidthLeft >= 0) { last_rBearing = b__f_rbearing; _last_rPadding = b->f_rpadding(); _wLeft = newWidthLeft; _lineHeight = qMax(_lineHeight, blockHeight); longWordLine = false; continue; } if (_btype == TextBlockTText) { auto t = static_cast(b); if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line _last_rPadding += b->f_rpadding(); _lineHeight = qMax(_lineHeight, blockHeight); longWordLine = false; continue; } auto f_wLeft = _wLeft; // vars for saving state of the last word start auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) { auto wordEndsHere = (j->f_width() >= 0); auto j_width = wordEndsHere ? j->f_width() : -j->f_width(); auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing()); if (newWidthLeft >= 0) { last_rBearing = j->f_rbearing(); _last_rPadding = j->f_rpadding(); _wLeft = newWidthLeft; _lineHeight = qMax(_lineHeight, blockHeight); if (wordEndsHere) { longWordLine = false; } if (wordEndsHere || longWordLine) { f = j + 1; f_wLeft = _wLeft; f_lineHeight = _lineHeight; } continue; } auto elidedLineHeight = qMax(_lineHeight, blockHeight); auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); if (elidedLine) { _lineHeight = elidedLineHeight; } else if (f != j && !_breakEverywhere) { // word did not fit completely, so we roll back the state to the beginning of this long word j = f; _wLeft = f_wLeft; _lineHeight = f_lineHeight; j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width(); } if (!drawLine(elidedLine ? ((j + 1 == en) ? _t->countBlockEnd(i, e) : (j + 1)->from()) : j->from(), i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); _lineStart = j->from(); _lineStartBlock = blockIndex; last_rBearing = j->f_rbearing(); _last_rPadding = j->f_rpadding(); _wLeft = _w - (j_width - last_rBearing); if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { _wLeft -= _elideRemoveFromEnd; } longWordLine = true; f = j + 1; f_wLeft = _wLeft; f_lineHeight = _lineHeight; } continue; } auto elidedLineHeight = qMax(_lineHeight, blockHeight); auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); if (elidedLine) { _lineHeight = elidedLineHeight; } if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); _lineStart = b->from(); _lineStartBlock = blockIndex; last_rBearing = b__f_rbearing; _last_rPadding = b->f_rpadding(); _wLeft = _w - (b->f_width() - last_rBearing); if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { _wLeft -= _elideRemoveFromEnd; } longWordLine = true; continue; } if (_lineStart < _t->_text.size()) { if (!drawLine(_t->_text.size(), e, e)) return; } if (!_p && _lookupSymbol) { _lookupResult.symbol = _t->_text.size(); _lookupResult.afterSymbol = false; } } void drawElided(int32 left, int32 top, int32 w, style::align align, int32 lines, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) { if (lines <= 0 || _t->isNull()) return; if (yTo < 0 || (lines - 1) * _t->_st->font->height < yTo) { yTo = lines * _t->_st->font->height; _elideLast = true; _elideRemoveFromEnd = removeFromEnd; } _breakEverywhere = breakEverywhere; draw(left, top, w, align, yFrom, yTo, selection); } StateResult getState(QPoint point, int w, StateRequest request) { if (!_t->isNull() && point.y() >= 0) { _lookupRequest = request; _lookupX = point.x(); _lookupY = point.y(); _breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere); _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) { draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1); } } return _lookupResult; } StateResult getStateElided(QPoint point, int w, StateRequestElided request) { if (!_t->isNull() && point.y() >= 0 && request.lines > 0) { _lookupRequest = request; _lookupX = point.x(); _lookupY = point.y(); _breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere); _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) { int yTo = _lookupY + 1; if (yTo < 0 || (request.lines - 1) * _t->_st->font->height < yTo) { yTo = request.lines * _t->_st->font->height; _elideLast = true; _elideRemoveFromEnd = request.removeFromEnd; } draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1); } } return _lookupResult; } private: void initNextParagraph(String::TextBlocks::const_iterator i) { _parStartBlock = i; const auto e = _t->_blocks.cend(); if (i == e) { _parStart = _t->_text.size(); _parLength = 0; } else { _parStart = (*i)->from(); for (; i != e; ++i) { if ((*i)->type() == TextBlockTNewline) { break; } } _parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart; } _parAnalysis.resize(0); } void initParagraphBidi() { if (!_parLength || !_parAnalysis.isEmpty()) return; String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; bool ignore = false; bool rtl = (_parDirection == Qt::RightToLeft); if (!ignore && !rtl) { ignore = true; const ushort *start = reinterpret_cast(_str) + _parStart; const ushort *curr = start; const ushort *end = start + _parLength; while (curr < end) { while (n != e && (*n)->from() <= _parStart + (curr - start)) { i = n; ++n; } if ((*i)->type() != TextBlockTEmoji && *curr >= 0x590) { ignore = false; break; } ++curr; } } _parAnalysis.resize(_parLength); QScriptAnalysis *analysis = _parAnalysis.data(); BidiControl control(rtl); _parHasBidi = false; if (ignore) { memset(analysis, 0, _parLength * sizeof(QScriptAnalysis)); if (rtl) { for (int i = 0; i < _parLength; ++i) analysis[i].bidiLevel = 1; _parHasBidi = true; } } else { _parHasBidi = eBidiItemize(analysis, control); } } bool drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_end) { _yDelta = (_lineHeight - _fontHeight) / 2; if (_yTo >= 0 && (_y + _yDelta >= _yTo || _y >= _yTo)) return false; if (_y + _yDelta + _fontHeight <= _yFrom) { if (_lookupSymbol) { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; } return true; } // Trimming pending spaces, because they sometimes don't fit on the line. // They also are not counted in the line width, they're in the right padding. // Line width is a sum of block / word widths and paddings between them, without trailing one. auto trimmedLineEnd = _lineEnd; for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { auto ch = _t->_text[trimmedLineEnd - 1]; if (ch != QChar::Space && ch != QChar::LineFeed) { break; } } auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get(); auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide); if (elidedLine) { // If we decided to draw the last line elided only because of the skip block // that did not fit on this line, we just draw the line till the very end. // Skip block is ignored in the elided lines, instead "removeFromEnd" is used. if (_endBlock && _endBlock->type() == TextBlockTSkip) { _endBlock = nullptr; } if (!_endBlock) { elidedLine = false; } } auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; const auto extendLeft = (currentBlock->from() < _lineStart) ? qMin(_lineStart - currentBlock->from(), 2) : 0; _localFrom = _lineStart - extendLeft; const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine) ? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end)) : trimmedLineEnd; auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom); auto lineStart = extendLeft; auto lineLength = trimmedLineEnd - _lineStart; if (elidedLine) { initParagraphBidi(); prepareElidedLine(lineText, lineStart, lineLength, _endBlock); } auto x = _x; if (_align & Qt::AlignHCenter) { x += (_wLeft / 2).toInt(); } else if (((_align & Qt::AlignLeft) && _parDirection == Qt::RightToLeft) || ((_align & Qt::AlignRight) && _parDirection == Qt::LeftToRight)) { x += _wLeft; } if (!_p) { if (_lookupX < x) { if (_lookupSymbol) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; } else { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false; } } if (_lookupLink) { _lookupResult.link = nullptr; } _lookupResult.uponSymbol = false; return false; } else if (_lookupX >= x + (_w - _wLeft)) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false; } else { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; } if (_lookupLink) { _lookupResult.link = nullptr; } _lookupResult.uponSymbol = false; return false; } } if (_fullWidthSelection) { const auto selectFromStart = (_selection.to > _lineStart) && (_lineStart > 0) && (_selection.from <= _lineStart); const auto selectTillEnd = (_selection.to > trimmedLineEnd) && (trimmedLineEnd < _t->_text.size()) && (_selection.from <= trimmedLineEnd) && (!_endBlock || _endBlock->type() != TextBlockTSkip); if ((selectFromStart && _parDirection == Qt::LeftToRight) || (selectTillEnd && _parDirection == Qt::RightToLeft)) { if (x > _x) { fillSelectRange(_x, x); } } if ((selectTillEnd && _parDirection == Qt::LeftToRight) || (selectFromStart && _parDirection == Qt::RightToLeft)) { if (x < _x + _wLeft) { fillSelectRange(x + _w - _wLeft, _x + _w); } } } if (trimmedLineEnd == _lineStart && !elidedLine) { return true; } if (!elidedLine) initParagraphBidi(); // if was not inited _f = _t->_st->font; QStackTextEngine engine(lineText, _f->f); engine.option.setTextDirection(_parDirection); _e = &engine; eItemize(); QScriptLine line; line.from = lineStart; line.length = lineLength; eShapeLine(line); int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0; if (!nItems) { return true; } int skipIndex = -1; QVarLengthArray visualOrder(nItems); QVarLengthArray levels(nItems); for (int i = 0; i < nItems; ++i) { auto &si = engine.layoutData->items[firstItem + i]; while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); if (_type == TextBlockTSkip) { levels[i] = si.analysis.bidiLevel = 0; skipIndex = i; } else { 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()); } } } QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); if (rtl() && skipIndex == nItems - 1) { for (int32 i = nItems; i > 1;) { --i; visualOrder[i] = visualOrder[i - 1]; } visualOrder[0] = skipIndex; } blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2; applyBlockProperties(currentBlock); for (int i = 0; i < nItems; ++i) { int item = firstItem + visualOrder[i]; const QScriptItem &si = engine.layoutData->items.at(item); bool rtl = (si.analysis.bidiLevel % 2); while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) { nextBlock = currentBlock; currentBlock = _t->_blocks[--blockIndex - 1].get(); applyBlockProperties(currentBlock); } while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; applyBlockProperties(currentBlock); } if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { TextBlockType _type = currentBlock->type(); if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest if (_lookupLink) { if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { _lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1); } } if (_type != TextBlockTSkip) { _lookupResult.uponSymbol = true; } if (_lookupSymbol) { if (_type == TextBlockTSkip) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; } else { _lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false; } return false; } // Emoji with spaces after symbol lookup auto chFrom = _str + currentBlock->from(); auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); auto spacesWidth = (si.width - currentBlock->f_width()); auto spacesCount = 0; while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) { ++spacesCount; --chTo; } if (spacesCount > 0) { // Check if we're over a space. if (rtl) { if (_lookupX < x + spacesWidth) { _lookupResult.symbol = (chTo - _str); // up to a space, included, rtl _lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false; return false; } } else if (_lookupX >= x + si.width - spacesWidth) { _lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr _lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false; return false; } } if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) { _lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str; _lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false; } else { _lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str; _lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true; } } return false; } else if (_p && _type == TextBlockTEmoji) { auto glyphX = x; auto spacesWidth = (si.width - currentBlock->f_width()); if (rtl) { glyphX += spacesWidth; } if (_localFrom + si.position < _selection.to) { auto chFrom = _str + currentBlock->from(); auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); if (_localFrom + si.position >= _selection.from) { // could be without space if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) { fillSelectRange(x, x + si.width); } else { // or with space fillSelectRange(glyphX, glyphX + currentBlock->f_width()); } } else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) { if (rtl) { // rtl space only fillSelectRange(x, glyphX); } else { // ltr space only fillSelectRange(x + currentBlock->f_width(), x + si.width); } } } Ui::Emoji::Draw( *_p, static_cast(currentBlock)->emoji, Ui::Emoji::GetSizeNormal(), (glyphX + st::emojiPadding).toInt(), _y + _yDelta + emojiY); // } else if (_p && currentBlock->type() == TextBlockSkip) { // debug // _p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast(currentBlock)->height()), QColor(0, 0, 0, 32)); } x += si.width; continue; } unsigned short *logClusters = engine.logClusters(&si); QGlyphLayout glyphs = engine.shapedGlyphs(&si); int itemStart = qMax(line.from, si.position), itemEnd; int itemLength = engine.length(item); int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; if (line.from + line.length < si.position + itemLength) { itemEnd = line.from + line.length; glyphsEnd = logClusters[itemEnd - si.position]; } else { itemEnd = si.position + itemLength; glyphsEnd = si.num_glyphs; } QFixed itemWidth = 0; for (int g = glyphsStart; g < glyphsEnd; ++g) itemWidth += glyphs.effectiveAdvance(g); if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest if (_lookupLink) { if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { _lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1); } } _lookupResult.uponSymbol = true; if (_lookupSymbol) { QFixed tmpx = rtl ? (x + itemWidth) : x; for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) { g = logClusters[itemStart - si.position + ch]; QFixed gwidth = glyphs.effectiveAdvance(g); // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes int ch2 = ch + 1; while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { ++ch2; } for (int charsCount = (ch2 - ch); ch < ch2; ++ch) { QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount), shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount); if ((rtl && _lookupX >= tmpx - shift1) || (!rtl && _lookupX < tmpx + shift1)) { _lookupResult.symbol = _localFrom + itemStart + ch; if ((rtl && _lookupX >= tmpx - shift2) || (!rtl && _lookupX < tmpx + shift2)) { _lookupResult.afterSymbol = false; } else { _lookupResult.afterSymbol = true; } return false; } } if (rtl) { tmpx -= gwidth; } else { tmpx += gwidth; } } if (itemEnd > itemStart) { _lookupResult.symbol = _localFrom + itemEnd - 1; _lookupResult.afterSymbol = true; } else { _lookupResult.symbol = _localFrom + itemStart; _lookupResult.afterSymbol = false; } } return false; } else if (_p) { QTextCharFormat format; QTextItemInt gf(glyphs.mid(glyphsStart, glyphsEnd - glyphsStart), &_e->fnt, engine.layoutData->string.unicode() + itemStart, itemEnd - itemStart, engine.fontEngine(si), format); gf.logClusters = logClusters + itemStart - si.position; gf.width = itemWidth; gf.justified = false; gf.initWithScriptItem(si); auto hasSelected = false; auto hasNotSelected = true; auto selectedRect = QRect(); if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) { hasSelected = true; auto selX = x; auto selWidth = itemWidth; if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) { hasNotSelected = false; } else { selWidth = 0; int itemL = itemEnd - itemStart; int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart); if (selStart < 0) selStart = 0; if (selEnd > itemL) selEnd = itemL; for (int ch = 0, g; ch < selEnd;) { g = logClusters[itemStart - si.position + ch]; QFixed gwidth = glyphs.effectiveAdvance(g); // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes int ch2 = ch + 1; while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { ++ch2; } if (ch2 <= selStart) { selX += gwidth; } else if (ch >= selStart && ch2 <= selEnd) { selWidth += gwidth; } else { int sStart = ch, sEnd = ch2; if (ch < selStart) { sStart = selStart; selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch); } if (ch2 >= selEnd) { sEnd = selEnd; selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); break; } selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); } ch = ch2; } } if (rtl) selX = x + itemWidth - (selX - x) - selWidth; selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight); fillSelectRange(selX, selX + selWidth); } if (Q_UNLIKELY(hasSelected)) { if (Q_UNLIKELY(hasNotSelected)) { auto clippingEnabled = _p->hasClipping(); auto clippingRegion = _p->clipRegion(); _p->setClipRect(selectedRect, Qt::IntersectClip); _p->setPen(*_currentPenSelected); _p->drawTextItem(QPointF(x.toReal(), textY), gf); auto externalClipping = clippingEnabled ? clippingRegion : QRegion(QRect((_x - _w).toInt(), _y - _lineHeight, (_x + 2 * _w).toInt(), _y + 2 * _lineHeight)); _p->setClipRegion(externalClipping - selectedRect); _p->setPen(*_currentPen); _p->drawTextItem(QPointF(x.toReal(), textY), gf); if (clippingEnabled) { _p->setClipRegion(clippingRegion); } else { _p->setClipping(false); } } else { _p->setPen(*_currentPenSelected); _p->drawTextItem(QPointF(x.toReal(), textY), gf); } } else { _p->setPen(*_currentPen); _p->drawTextItem(QPointF(x.toReal(), textY), gf); } } x += itemWidth; } return true; } void fillSelectRange(QFixed from, QFixed to) { auto left = from.toInt(); auto width = to.toInt() - left; _p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg); } void elideSaveBlock(int32 blockIndex, AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { if (_elideSavedBlock) { restoreAfterElided(); } _elideSavedIndex = blockIndex; auto mutableText = const_cast(_t); _elideSavedBlock = std::move(mutableText->_blocks[blockIndex]); mutableText->_blocks[blockIndex] = std::make_unique(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, _elideSavedBlock->flags(), _elideSavedBlock->lnkIndex()); _blocksSize = blockIndex + 1; _endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr); } void setElideBidi(int32 elideStart, int32 elideLen) { int32 newParLength = elideStart + elideLen - _parStart; if (newParLength > _parAnalysis.size()) { _parAnalysis.resize(newParLength); } for (int32 i = elideLen; i > 0; --i) { _parAnalysis[newParLength - i].bidiLevel = (_parDirection == Qt::RightToLeft) ? 1 : 0; } } void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, AbstractBlock *&_endBlock, int repeat = 0) { static const QString _Elide = qsl("..."); _f = _t->_st->font; QStackTextEngine engine(lineText, _f->f); engine.option.setTextDirection(_parDirection); _e = &engine; eItemize(); auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; QScriptLine line; line.from = lineStart; line.length = lineLength; eShapeLine(line); auto elideWidth = _f->elidew; _wLeft = _w - elideWidth - _elideRemoveFromEnd; int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i; for (i = 0; i < nItems; ++i) { QScriptItem &si(engine.layoutData->items[firstItem + i]); while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } TextBlockType _type = currentBlock->type(); if (si.analysis.flags == QScriptAnalysis::Object) { if (_type == TextBlockTEmoji || _type == TextBlockTSkip) { si.width = currentBlock->f_width() + currentBlock->f_rpadding(); } } if (_type == TextBlockTEmoji || _type == TextBlockTSkip || _type == TextBlockTNewline) { if (_wLeft < si.width) { lineText = lineText.mid(0, currentBlock->from() - _localFrom) + _Elide; lineLength = currentBlock->from() + _Elide.size() - _lineStart; _selection.to = qMin(_selection.to, currentBlock->from()); setElideBidi(currentBlock->from(), _Elide.size()); elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth); return; } _wLeft -= si.width; } else if (_type == TextBlockTText) { unsigned short *logClusters = engine.logClusters(&si); QGlyphLayout glyphs = engine.shapedGlyphs(&si); int itemStart = qMax(line.from, si.position), itemEnd; int itemLength = engine.length(firstItem + i); int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; if (line.from + line.length < si.position + itemLength) { itemEnd = line.from + line.length; glyphsEnd = logClusters[itemEnd - si.position]; } else { itemEnd = si.position + itemLength; glyphsEnd = si.num_glyphs; } for (auto g = glyphsStart; g < glyphsEnd; ++g) { auto adv = glyphs.effectiveAdvance(g); if (_wLeft < adv) { auto pos = itemStart; while (pos < itemEnd && logClusters[pos - si.position] < g) { ++pos; } if (lineText.size() <= pos || repeat > 3) { lineText += _Elide; lineLength = _localFrom + pos + _Elide.size() - _lineStart; _selection.to = qMin(_selection.to, uint16(_localFrom + pos)); setElideBidi(_localFrom + pos, _Elide.size()); _blocksSize = blockIndex; _endBlock = nextBlock; } else { lineText = lineText.mid(0, pos); lineLength = _localFrom + pos - _lineStart; _blocksSize = blockIndex; _endBlock = nextBlock; prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1); } return; } else { _wLeft -= adv; } } } } int32 elideStart = _localFrom + lineText.size(); _selection.to = qMin(_selection.to, uint16(elideStart)); setElideBidi(elideStart, _Elide.size()); lineText += _Elide; lineLength += _Elide.size(); if (!repeat) { for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) { } if (blockIndex < _blocksSize) { elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth); } } } void restoreAfterElided() { if (_elideSavedBlock) { const_cast(_t)->_blocks[_elideSavedIndex] = std::move(_elideSavedBlock); } } // COPIED FROM qtextengine.cpp AND MODIFIED void eShapeLine(const QScriptLine &line) { int item = _e->findItem(line.from); if (item == -1) return; #ifdef OS_MAC_OLD auto end = _e->findItem(line.from + line.length - 1); #else // OS_MAC_OLD auto end = _e->findItem(line.from + line.length - 1, item); #endif // OS_MAC_OLD auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; eSetFont(currentBlock); for (; item <= end; ++item) { QScriptItem &si = _e->layoutData->items[item]; while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; eSetFont(currentBlock); } _e->shape(item); } } style::font applyFlags(int32 flags, const style::font &f) { if (!flags) { return f; } auto result = f; if ((flags & TextBlockFPre) || (flags & TextBlockFCode)) { result = App::monofont(); if (result->size() != f->size() || result->flags() != f->flags()) { result = style::font(f->size(), f->flags(), result->family()); } } else { if (flags & TextBlockFBold) { result = result->bold(); } else if (flags & TextBlockFSemibold) { result = st::semiboldFont; if (result->size() != f->size() || result->flags() != f->flags()) { result = style::font(f->size(), f->flags(), result->family()); } } if (flags & TextBlockFItalic) result = result->italic(); if (flags & TextBlockFUnderline) result = result->underline(); if (flags & TextBlockFStrikeOut) result = result->strikeout(); if (flags & TextBlockFTilde) { // tilde fix in OpenSans result = st::semiboldFont; } } return result; } void eSetFont(AbstractBlock *block) { const auto flags = block->flags(); const auto usedFont = [&] { if (const auto index = block->lnkIndex()) { return ClickHandler::showAsActive(_t->_links.at(index - 1)) ? _t->_st->linkFontOver : _t->_st->linkFont; } return _t->_st->font; }(); const auto newFont = applyFlags(flags, usedFont); if (newFont != _f) { _f = (newFont->family() == _t->_st->font->family()) ? applyFlags(flags | newFont->flags(), _t->_st->font) : newFont; _e->fnt = _f->f; _e->resetFontEngineCache(); } } void eItemize() { _e->validate(); if (_e->layoutData->items.size()) return; int length = _e->layoutData->string.length(); if (!length) return; const ushort *string = reinterpret_cast(_e->layoutData->string.unicode()); auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; _e->layoutData->hasBidi = _parHasBidi; auto analysis = _parAnalysis.data() + (_localFrom - _parStart); { QVarLengthArray scripts(length); QUnicodeTools::initScripts(string, length, scripts.data()); for (int i = 0; i < length; ++i) analysis[i].script = scripts.at(i); } blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; auto start = string; auto end = start + length; while (start < end) { while (nextBlock && nextBlock->from() <= _localFrom + (start - string)) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); if (_type == TextBlockTEmoji || _type == TextBlockTSkip) { analysis->script = QChar::Script_Common; analysis->flags = QScriptAnalysis::Object; } else { analysis->flags = QScriptAnalysis::None; } analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior ++start; ++analysis; } { auto i_string = &_e->layoutData->string; auto i_analysis = _parAnalysis.data() + (_localFrom - _parStart); auto i_items = &_e->layoutData->items; blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; auto startBlock = currentBlock; if (!length) { return; } auto start = 0; auto end = start + length; for (int i = start + 1; i < end; ++i) { while (nextBlock && nextBlock->from() <= _localFrom + i) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } // According to the unicode spec we should be treating characters in the Common script // (punctuation, spaces, etc) as being the same script as the surrounding text for the // purpose of splitting up text. This is important because, for example, a fullstop // (0x2E) can be used to indicate an abbreviation and so must be treated as part of a // word. Thus it must be passed along with the word in languages that have to calculate // word breaks. For example the thai word "ครม." has no word breaks but the word "ครม" // does. // Unfortuntely because we split up the strings for both wordwrapping and for setting // the font and because Japanese and Chinese are also aliases of the script "Common", // doing this would break too many things. So instead we only pass the full stop // along, and nothing else. if (currentBlock == startBlock && i_analysis[i].bidiLevel == i_analysis[start].bidiLevel && i_analysis[i].flags == i_analysis[start].flags && (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.')) // && i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs && i - start < _MaxItemLength) continue; i_items->append(QScriptItem(start, i_analysis[start])); start = i; startBlock = currentBlock; } i_items->append(QScriptItem(start, i_analysis[start])); } } QChar::Direction eSkipBoundryNeutrals(QScriptAnalysis *analysis, const ushort *unicode, int &sor, int &eor, BidiControl &control, String::TextBlocks::const_iterator i) { String::TextBlocks::const_iterator e = _t->_blocks.cend(), n = i + 1; QChar::Direction dir = control.basicDirection(); int level = sor > 0 ? analysis[sor - 1].bidiLevel : control.level; while (sor <= _parLength) { while (i != _parStartBlock && (*i)->from() > _parStart + sor) { n = i; --i; } while (n != e && (*n)->from() <= _parStart + sor) { i = n; ++n; } TextBlockType _itype = (*i)->type(); if (eor == _parLength) dir = control.basicDirection(); else if (_itype == TextBlockTEmoji) dir = QChar::DirCS; else if (_itype == TextBlockTSkip) dir = QChar::DirCS; else dir = QChar::direction(unicode[sor]); // Keep skipping DirBN as if it doesn't exist if (dir != QChar::DirBN) break; analysis[sor++].bidiLevel = level; } eor = sor; return dir; } // creates the next QScript items. bool eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { bool rightToLeft = (control.basicDirection() == 1); bool hasBidi = rightToLeft; int sor = 0; int eor = -1; const ushort *unicode = reinterpret_cast(_t->_text.unicode()) + _parStart; int current = 0; QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL; BidiStatus status; String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; QChar::Direction sdir; TextBlockType _stype = (*_parStartBlock)->type(); if (_stype == TextBlockTEmoji) sdir = QChar::DirCS; else if (_stype == TextBlockTSkip) sdir = QChar::DirCS; else sdir = QChar::direction(*unicode); if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN) sdir = QChar::DirON; else dir = QChar::DirON; status.eor = sdir; status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL; status.last = status.lastStrong; status.dir = sdir; while (current <= _parLength) { while (n != e && (*n)->from() <= _parStart + current) { i = n; ++n; } QChar::Direction dirCurrent; TextBlockType _itype = (*i)->type(); if (current == (int)_parLength) dirCurrent = control.basicDirection(); else if (_itype == TextBlockTEmoji) dirCurrent = QChar::DirCS; else if (_itype == TextBlockTSkip) dirCurrent = QChar::DirCS; else dirCurrent = QChar::direction(unicode[current]); switch (dirCurrent) { // embedding and overrides (X1-X9 in the BiDi specs) case QChar::DirRLE: case QChar::DirRLO: case QChar::DirLRE: case QChar::DirLRO: { bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO); hasBidi |= rtl; bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO); unsigned int level = control.level+1; if ((level%2 != 0) == rtl) ++level; if (level < _MaxBidiLevel) { eor = current-1; eAppendItems(analysis, sor, eor, control, dir); eor = current; control.embed(rtl, override); QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL); dir = status.eor = edir; status.lastStrong = edir; } break; } case QChar::DirPDF: { if (control.canPop()) { if (dir != control.direction()) { eor = current-1; eAppendItems(analysis, sor, eor, control, dir); dir = control.direction(); } eor = current; eAppendItems(analysis, sor, eor, control, dir); control.pdf(); dir = QChar::DirON; status.eor = QChar::DirON; status.last = control.direction(); if (control.override) dir = control.direction(); else dir = QChar::DirON; status.lastStrong = control.direction(); } break; } // strong types case QChar::DirL: if(dir == QChar::DirON) dir = QChar::DirL; switch(status.last) { case QChar::DirL: eor = current; status.eor = QChar::DirL; break; case QChar::DirR: case QChar::DirAL: case QChar::DirEN: case QChar::DirAN: if (eor >= 0) { eAppendItems(analysis, sor, eor, control, dir); status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); } else { eor = current; status.eor = dir; } break; case QChar::DirES: case QChar::DirET: case QChar::DirCS: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if(dir != QChar::DirL) { //last stuff takes embedding dir if(control.direction() == QChar::DirR) { if(status.eor != QChar::DirR) { // AN or EN eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirR; } eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); } else { if(status.eor != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirL; } else { eor = current; status.eor = QChar::DirL; break; } } } else { eor = current; status.eor = QChar::DirL; } default: break; } status.lastStrong = QChar::DirL; break; case QChar::DirAL: case QChar::DirR: hasBidi = true; if(dir == QChar::DirON) dir = QChar::DirR; switch(status.last) { case QChar::DirL: case QChar::DirEN: case QChar::DirAN: if (eor >= 0) eAppendItems(analysis, sor, eor, control, dir); // fall through case QChar::DirR: case QChar::DirAL: dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break; case QChar::DirES: case QChar::DirET: case QChar::DirCS: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if(status.eor != QChar::DirR && status.eor != QChar::DirAL) { //last stuff takes embedding dir if(control.direction() == QChar::DirR || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { eAppendItems(analysis, sor, eor, control, dir); dir = QChar::DirR; status.eor = QChar::DirON; eor = current; } else { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); dir = QChar::DirR; status.eor = QChar::DirON; } } else { eor = current; status.eor = QChar::DirR; } default: break; } status.lastStrong = dirCurrent; break; // weak types: case QChar::DirNSM: if (eor == current-1) eor = current; break; case QChar::DirEN: // if last strong was AL change EN to AN if(status.lastStrong != QChar::DirAL) { if(dir == QChar::DirON) { if(status.lastStrong == QChar::DirL) dir = QChar::DirL; else dir = QChar::DirEN; } switch(status.last) { case QChar::DirET: if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirAN; } [[fallthrough]]; case QChar::DirEN: case QChar::DirL: eor = current; status.eor = dirCurrent; break; case QChar::DirR: case QChar::DirAL: case QChar::DirAN: if (eor >= 0) eAppendItems(analysis, sor, eor, control, dir); else eor = current; status.eor = QChar::DirEN; dir = QChar::DirAN; break; case QChar::DirES: case QChar::DirCS: if(status.eor == QChar::DirEN || dir == QChar::DirAN) { eor = current; break; } [[fallthrough]]; case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if(status.eor == QChar::DirR) { // neutrals go to R eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirEN; dir = QChar::DirAN; } else if(status.eor == QChar::DirL || (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { eor = current; status.eor = dirCurrent; } else { // numbers on both sides, neutrals get right to left direction if(dir != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; eor = current - 1; dir = QChar::DirR; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirAN; } else { eor = current; status.eor = dirCurrent; } } [[fallthrough]]; default: break; } break; } [[fallthrough]]; case QChar::DirAN: hasBidi = true; dirCurrent = QChar::DirAN; if(dir == QChar::DirON) dir = QChar::DirAN; switch(status.last) { case QChar::DirL: case QChar::DirAN: eor = current; status.eor = QChar::DirAN; break; case QChar::DirR: case QChar::DirAL: case QChar::DirEN: if (eor >= 0){ eAppendItems(analysis, sor, eor, control, dir); } else { eor = current; } dir = QChar::DirAN; status.eor = QChar::DirAN; break; case QChar::DirCS: if(status.eor == QChar::DirAN) { eor = current; break; } [[fallthrough]]; case QChar::DirES: case QChar::DirET: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if(status.eor == QChar::DirR) { // neutrals go to R eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirAN; dir = QChar::DirAN; } else if(status.eor == QChar::DirL || (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { eor = current; status.eor = dirCurrent; } else { // numbers on both sides, neutrals get right to left direction if(dir != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; eor = current - 1; dir = QChar::DirR; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirAN; dir = QChar::DirAN; } else { eor = current; status.eor = dirCurrent; } } [[fallthrough]]; default: break; } break; case QChar::DirES: case QChar::DirCS: break; case QChar::DirET: if(status.last == QChar::DirEN) { dirCurrent = QChar::DirEN; eor = current; status.eor = dirCurrent; } break; // boundary neutrals should be ignored case QChar::DirBN: break; // neutrals case QChar::DirB: // ### what do we do with newline and paragraph separators that come to here? break; case QChar::DirS: // ### implement rule L1 break; case QChar::DirWS: case QChar::DirON: break; default: break; } if(current >= (int)_parLength) break; // set status.last as needed. switch(dirCurrent) { case QChar::DirET: case QChar::DirES: case QChar::DirCS: case QChar::DirS: case QChar::DirWS: case QChar::DirON: switch(status.last) { case QChar::DirL: case QChar::DirR: case QChar::DirAL: case QChar::DirEN: case QChar::DirAN: status.last = dirCurrent; break; default: status.last = QChar::DirON; } break; case QChar::DirNSM: case QChar::DirBN: // ignore these break; case QChar::DirLRO: case QChar::DirLRE: status.last = QChar::DirL; break; case QChar::DirRLO: case QChar::DirRLE: status.last = QChar::DirR; break; case QChar::DirEN: if (status.last == QChar::DirL) { status.last = QChar::DirL; break; } [[fallthrough]]; default: status.last = dirCurrent; } ++current; } eor = current - 1; // remove dummy char if (sor <= eor) eAppendItems(analysis, sor, eor, control, dir); return hasBidi; } private: void applyBlockProperties(AbstractBlock *block) { eSetFont(block); if (_p) { if (block->lnkIndex()) { _currentPen = &_textPalette->linkFg->p; _currentPenSelected = &_textPalette->selectLinkFg->p; } else if ((block->flags() & TextBlockFCode) || (block->flags() & TextBlockFPre)) { _currentPen = &_textPalette->monoFg->p; _currentPenSelected = &_textPalette->selectMonoFg->p; } else { _currentPen = &_originalPen; _currentPenSelected = &_originalPenSelected; } } } Painter *_p = nullptr; const style::TextPalette *_textPalette = nullptr; const String *_t = nullptr; bool _elideLast = false; bool _breakEverywhere = false; int _elideRemoveFromEnd = 0; style::align _align = style::al_topleft; const QPen _originalPen; QPen _originalPenSelected; const QPen *_currentPen = nullptr; const QPen *_currentPenSelected = nullptr; int _yFrom = 0; int _yTo = 0; int _yToElide = 0; TextSelection _selection = { 0, 0 }; bool _fullWidthSelection = true; const QChar *_str = nullptr; // current paragraph data String::TextBlocks::const_iterator _parStartBlock; Qt::LayoutDirection _parDirection; int _parStart = 0; int _parLength = 0; bool _parHasBidi = false; QVarLengthArray _parAnalysis; // current line data QTextEngine *_e = nullptr; style::font _f; QFixed _x, _w, _wLeft, _last_rPadding; int32 _y, _yDelta, _lineHeight, _fontHeight; // elided hack support int _blocksSize = 0; int _elideSavedIndex = 0; std::unique_ptr _elideSavedBlock; int _lineStart = 0; int _localFrom = 0; int _lineStartBlock = 0; // link and symbol resolve QFixed _lookupX = 0; int _lookupY = 0; bool _lookupSymbol = false; bool _lookupLink = false; StateRequest _lookupRequest; StateResult _lookupResult; }; String::String(int32 minResizeWidth) : _minResizeWidth(minResizeWidth) { } String::String(const style::TextStyle &st, const QString &text, const TextParseOptions &options, int32 minResizeWidth, bool richText) : _minResizeWidth(minResizeWidth) { if (richText) { setRichText(st, text, options); } else { setText(st, text, options); } } String::String(const String &other) : _minResizeWidth(other._minResizeWidth) , _maxWidth(other._maxWidth) , _minHeight(other._minHeight) , _text(other._text) , _st(other._st) , _links(other._links) , _startDir(other._startDir) { _blocks.reserve(other._blocks.size()); for (auto &block : other._blocks) { _blocks.push_back(block->clone()); } } String::String(String &&other) : _minResizeWidth(other._minResizeWidth) , _maxWidth(other._maxWidth) , _minHeight(other._minHeight) , _text(other._text) , _st(other._st) , _blocks(std::move(other._blocks)) , _links(other._links) , _startDir(other._startDir) { other.clearFields(); } String &String::operator=(const String &other) { _minResizeWidth = other._minResizeWidth; _maxWidth = other._maxWidth; _minHeight = other._minHeight; _text = other._text; _st = other._st; _blocks = TextBlocks(other._blocks.size()); _links = other._links; _startDir = other._startDir; for (int32 i = 0, l = _blocks.size(); i < l; ++i) { _blocks[i] = other._blocks.at(i)->clone(); } return *this; } String &String::operator=(String &&other) { _minResizeWidth = other._minResizeWidth; _maxWidth = other._maxWidth; _minHeight = other._minHeight; _text = other._text; _st = other._st; _blocks = std::move(other._blocks); _links = other._links; _startDir = other._startDir; other.clearFields(); return *this; } void String::setText(const style::TextStyle &st, const QString &text, const TextParseOptions &options) { _st = &st; clear(); { Parser parser(this, text, options); } recountNaturalSize(true, options.dir); } void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { NewlineBlock *lastNewline = 0; _maxWidth = _minHeight = 0; int32 lineHeight = 0; int32 result = 0, lastNewlineStart = 0; QFixed _width = 0, last_rBearing = 0, last_rPadding = 0; for (auto i = _blocks.cbegin(), e = _blocks.cend(); i != e; ++i) { auto b = i->get(); auto _btype = b->type(); auto blockHeight = countBlockHeight(b, _st); if (_btype == TextBlockTNewline) { if (!lineHeight) lineHeight = blockHeight; if (initial) { Qt::LayoutDirection dir = optionsDir; if (dir == Qt::LayoutDirectionAuto) { dir = StringDirection(_text, lastNewlineStart, b->from()); } if (lastNewline) { lastNewline->_nextDir = dir; } else { _startDir = dir; } } lastNewlineStart = b->from(); lastNewline = static_cast(b); _minHeight += lineHeight; lineHeight = 0; last_rBearing = b->f_rbearing(); last_rPadding = b->f_rpadding(); accumulate_max(_maxWidth, _width); _width = (b->f_width() - last_rBearing); continue; } auto b__f_rbearing = b->f_rbearing(); // cache // We need to accumulate max width after each block, because // some blocks have width less than -1 * previous right bearing. // In that cases the _width gets _smaller_ after moving to the next block. // // But when we layout block and we're sure that _maxWidth is enough // for all the blocks to fit on their line we check each block, even the // intermediate one with a large negative right bearing. accumulate_max(_maxWidth, _width); _width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing); lineHeight = qMax(lineHeight, blockHeight); last_rBearing = b__f_rbearing; last_rPadding = b->f_rpadding(); continue; } if (initial) { Qt::LayoutDirection dir = optionsDir; if (dir == Qt::LayoutDirectionAuto) { dir = StringDirection(_text, lastNewlineStart, _text.size()); } if (lastNewline) { lastNewline->_nextDir = dir; } else { _startDir = dir; } } if (_width > 0) { if (!lineHeight) lineHeight = countBlockHeight(_blocks.back().get(), _st); _minHeight += lineHeight; accumulate_max(_maxWidth, _width); } } void String::setMarkedText(const style::TextStyle &st, const TextWithEntities &textWithEntities, const TextParseOptions &options) { _st = &st; clear(); { // utf codes of the text display for emoji extraction // auto text = textWithEntities.text; // auto newText = QString(); // newText.reserve(8 * text.size()); // newText.append("\t{ "); // for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { // if (*ch == TextCommand) { // break; // } else if (chIsNewline(*ch)) { // newText.append("},").append(*ch).append("\t{ "); // } else { // if (ch->isHighSurrogate() || ch->isLowSurrogate()) { // if (ch->isHighSurrogate() && (ch + 1 != e) && ((ch + 1)->isLowSurrogate())) { // newText.append("0x").append(QString::number((uint32(ch->unicode()) << 16) | uint32((ch + 1)->unicode()), 16).toUpper()).append("U, "); // ++ch; // } else { // newText.append("BADx").append(QString::number(ch->unicode(), 16).toUpper()).append("U, "); // } // } else { // newText.append("0x").append(QString::number(ch->unicode(), 16).toUpper()).append("U, "); // } // } // } // newText.append("},\n\n").append(text); // Parser parser(this, { newText, EntitiesInText() }, options); Parser parser(this, textWithEntities, options); } recountNaturalSize(true, options.dir); } void String::setRichText(const style::TextStyle &st, const QString &text, TextParseOptions options) { options.flags |= TextParseRichText; setText(st, text, options); } void String::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) { if (!lnkIndex || lnkIndex > _links.size()) return; _links[lnkIndex - 1] = lnk; } bool String::hasLinks() const { return !_links.isEmpty(); } bool String::hasSkipBlock() const { return _blocks.empty() ? false : _blocks.back()->type() == TextBlockTSkip; } bool String::updateSkipBlock(int width, int height) { if (!_blocks.empty() && _blocks.back()->type() == TextBlockTSkip) { const auto block = static_cast(_blocks.back().get()); if (block->width() == width && block->height() == height) { return false; } _text.resize(block->from()); _blocks.pop_back(); } _text.push_back('_'); _blocks.push_back(std::make_unique( _st->font, _text, _text.size() - 1, width, height, 0)); recountNaturalSize(false); return true; } bool String::removeSkipBlock() { if (_blocks.empty() || _blocks.back()->type() != TextBlockTSkip) { return false; } _text.resize(_blocks.back()->from()); _blocks.pop_back(); recountNaturalSize(false); return true; } int String::countWidth(int width) const { if (QFixed(width) >= _maxWidth) { return _maxWidth.ceil().toInt(); } QFixed maxLineWidth = 0; enumerateLines(width, [&maxLineWidth](QFixed lineWidth, int lineHeight) { if (lineWidth > maxLineWidth) { maxLineWidth = lineWidth; } }); return maxLineWidth.ceil().toInt(); } int String::countHeight(int width) const { if (QFixed(width) >= _maxWidth) { return _minHeight; } int result = 0; enumerateLines(width, [&result](QFixed lineWidth, int lineHeight) { result += lineHeight; }); return result; } void String::countLineWidths(int width, QVector *lineWidths) const { enumerateLines(width, [lineWidths](QFixed lineWidth, int lineHeight) { lineWidths->push_back(lineWidth.ceil().toInt()); }); } template void String::enumerateLines(int w, Callback callback) const { QFixed width = w; if (width < _minResizeWidth) width = _minResizeWidth; int lineHeight = 0; QFixed widthLeft = width, last_rBearing = 0, last_rPadding = 0; bool longWordLine = true; for (auto &b : _blocks) { auto _btype = b->type(); int blockHeight = countBlockHeight(b.get(), _st); if (_btype == TextBlockTNewline) { if (!lineHeight) lineHeight = blockHeight; callback(width - widthLeft, lineHeight); lineHeight = 0; last_rBearing = b->f_rbearing(); last_rPadding = b->f_rpadding(); widthLeft = width - (b->f_width() - last_rBearing); longWordLine = true; continue; } auto b__f_rbearing = b->f_rbearing(); auto newWidthLeft = widthLeft - last_rBearing - (last_rPadding + b->f_width() - b__f_rbearing); if (newWidthLeft >= 0) { last_rBearing = b__f_rbearing; last_rPadding = b->f_rpadding(); widthLeft = newWidthLeft; lineHeight = qMax(lineHeight, blockHeight); longWordLine = false; continue; } if (_btype == TextBlockTText) { auto t = static_cast(b.get()); if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line last_rPadding += b->f_rpadding(); lineHeight = qMax(lineHeight, blockHeight); longWordLine = false; continue; } auto f_wLeft = widthLeft; int f_lineHeight = lineHeight; for (auto j = t->_words.cbegin(), e = t->_words.cend(), f = j; j != e; ++j) { bool wordEndsHere = (j->f_width() >= 0); auto j_width = wordEndsHere ? j->f_width() : -j->f_width(); auto newWidthLeft = widthLeft - last_rBearing - (last_rPadding + j_width - j->f_rbearing()); if (newWidthLeft >= 0) { last_rBearing = j->f_rbearing(); last_rPadding = j->f_rpadding(); widthLeft = newWidthLeft; lineHeight = qMax(lineHeight, blockHeight); if (wordEndsHere) { longWordLine = false; } if (wordEndsHere || longWordLine) { f_wLeft = widthLeft; f_lineHeight = lineHeight; f = j + 1; } continue; } if (f != j) { j = f; widthLeft = f_wLeft; lineHeight = f_lineHeight; j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width(); } callback(width - widthLeft, lineHeight); lineHeight = qMax(0, blockHeight); last_rBearing = j->f_rbearing(); last_rPadding = j->f_rpadding(); widthLeft = width - (j_width - last_rBearing); longWordLine = true; f = j + 1; f_wLeft = widthLeft; f_lineHeight = lineHeight; } continue; } callback(width - widthLeft, lineHeight); lineHeight = qMax(0, blockHeight); last_rBearing = b__f_rbearing; last_rPadding = b->f_rpadding(); widthLeft = width - (b->f_width() - last_rBearing); longWordLine = true; continue; } if (widthLeft < width) { callback(width - widthLeft, lineHeight); } } void String::draw(Painter &painter, int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection, bool fullWidthSelection) const { // painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug Renderer p(&painter, this); p.draw(left, top, w, align, yFrom, yTo, selection, fullWidthSelection); } void String::drawElided(Painter &painter, int32 left, int32 top, int32 w, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const { // painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug Renderer p(&painter, this); p.drawElided(left, top, w, align, lines, yFrom, yTo, removeFromEnd, breakEverywhere, selection); } StateResult String::getState(QPoint point, int width, StateRequest request) const { return Renderer(nullptr, this).getState(point, width, request); } StateResult String::getStateElided(QPoint point, int width, StateRequestElided request) const { return Renderer(nullptr, this).getStateElided(point, width, request); } TextSelection String::adjustSelection(TextSelection selection, TextSelectType selectType) const { uint16 from = selection.from, to = selection.to; if (from < _text.size() && from <= to) { if (to > _text.size()) to = _text.size(); if (selectType == TextSelectType::Paragraphs) { if (!chIsParagraphSeparator(_text.at(from))) { while (from > 0 && !chIsParagraphSeparator(_text.at(from - 1))) { --from; } } if (to < _text.size()) { if (chIsParagraphSeparator(_text.at(to))) { ++to; } else { while (to < _text.size() && !chIsParagraphSeparator(_text.at(to))) { ++to; } } } } else if (selectType == TextSelectType::Words) { if (!chIsWordSeparator(_text.at(from))) { while (from > 0 && !chIsWordSeparator(_text.at(from - 1))) { --from; } } if (to < _text.size()) { if (chIsWordSeparator(_text.at(to))) { ++to; } else { while (to < _text.size() && !chIsWordSeparator(_text.at(to))) { ++to; } } } } } return { from, to }; } bool String::isEmpty() const { return _blocks.empty() || _blocks[0]->type() == TextBlockTSkip; } uint16 String::countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const { return (i + 1 == e) ? _text.size() : (*(i + 1))->from(); } uint16 String::countBlockLength(const String::TextBlocks::const_iterator &i, const String::TextBlocks::const_iterator &e) const { return countBlockEnd(i, e) - (*i)->from(); } template void String::enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const { if (isEmpty() || selection.empty()) { return; } int lnkIndex = 0; uint16 lnkFrom = 0; int32 flags = 0; for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); uint16 blockFrom = (i == e) ? _text.size() : (*i)->from(); int32 blockFlags = (i == e) ? 0 : (*i)->flags(); if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links blockLnkIndex = 0; } if (blockLnkIndex != lnkIndex) { if (lnkIndex) { auto rangeFrom = qMax(selection.from, lnkFrom); auto rangeTo = qMin(selection.to, blockFrom); if (rangeTo > rangeFrom) { // handle click handler QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); if (lnkFrom != rangeFrom || blockFrom != rangeTo) { appendPartCallback(r); } else { clickHandlerFinishCallback(r, _links.at(lnkIndex - 1)); } } } lnkIndex = blockLnkIndex; if (lnkIndex) { lnkFrom = blockFrom; clickHandlerStartCallback(); } } const auto checkBlockFlags = (blockFrom >= selection.from) && (blockFrom <= selection.to); if (checkBlockFlags && blockFlags != flags) { flagsChangeCallback(flags, blockFlags); flags = blockFlags; } if (i == e || blockFrom >= selection.to) { break; } if ((*i)->type() == TextBlockTSkip) continue; if (!blockLnkIndex) { auto rangeFrom = qMax(selection.from, blockFrom); auto rangeTo = qMin(selection.to, uint16(blockFrom + countBlockLength(i, e))); if (rangeTo > rangeFrom) { appendPartCallback(_text.midRef(rangeFrom, rangeTo - rangeFrom)); } } } } QString String::toString(TextSelection selection) const { return toText(selection, false, false).rich.text; } TextWithEntities String::toTextWithEntities(TextSelection selection) const { return toText(selection, false, true).rich; } TextForMimeData String::toTextForMimeData(TextSelection selection) const { return toText(selection, true, true); } TextForMimeData String::toText( TextSelection selection, bool composeExpanded, bool composeEntities) const { struct MarkdownTagTracker { TextBlockFlags flag = TextBlockFlags(); EntityType type = EntityType(); int start = 0; }; auto result = TextForMimeData(); result.rich.text.reserve(_text.size()); if (composeExpanded) { result.expanded.reserve(_text.size()); } auto linkStart = 0; auto markdownTrackers = composeEntities ? std::vector{ { TextBlockFItalic, EntityType::Italic }, { TextBlockFSemibold, EntityType::Bold }, { TextBlockFUnderline, EntityType::Underline }, { TextBlockFStrikeOut, EntityType::StrikeOut }, { TextBlockFCode, EntityType::Code }, // #TODO entities { TextBlockFPre, EntityType::Pre } } : std::vector(); const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) { if (!composeEntities) { return; } for (auto &tracker : markdownTrackers) { const auto flag = tracker.flag; if ((oldFlags & flag) && !(newFlags & flag)) { result.rich.entities.push_back({ tracker.type, tracker.start, result.rich.text.size() - tracker.start }); } else if ((newFlags & flag) && !(oldFlags & flag)) { tracker.start = result.rich.text.size(); } } }; const auto clickHandlerStartCallback = [&] { linkStart = result.rich.text.size(); }; const auto clickHandlerFinishCallback = [&]( const QStringRef &part, const ClickHandlerPtr &handler) { const auto entity = handler->getTextEntity(); const auto plainUrl = (entity.type == EntityType::Url) || (entity.type == EntityType::Email); const auto full = plainUrl ? entity.data.midRef(0, entity.data.size()) : part; result.rich.text.append(full); if (!composeExpanded && !composeEntities) { return; } if (composeExpanded) { result.expanded.append(full); if (entity.type == EntityType::CustomUrl) { const auto &url = entity.data; result.expanded.append(qstr(" (")).append(url).append(')'); } } if (composeEntities) { result.rich.entities.push_back({ entity.type, linkStart, full.size(), plainUrl ? QString() : entity.data }); } }; const auto appendPartCallback = [&](const QStringRef &part) { result.rich.text += part; if (composeExpanded) { result.expanded += part; } }; enumerateText( selection, appendPartCallback, clickHandlerStartCallback, clickHandlerFinishCallback, flagsChangeCallback); return result; } void String::clear() { clearFields(); _text.clear(); } void String::clearFields() { _blocks.clear(); _links.clear(); _maxWidth = _minHeight = 0; _startDir = Qt::LayoutDirectionAuto; } String::~String() = default; } // namespace Text } // namespace Ui