diff --git a/ui/basic_click_handlers.cpp b/ui/basic_click_handlers.cpp index 21b55d9..ca59447 100644 --- a/ui/basic_click_handlers.cpp +++ b/ui/basic_click_handlers.cpp @@ -71,13 +71,13 @@ void UrlClickHandler::Open(QString url, QVariant context) { bool UrlClickHandler::IsSuspicious(const QString &url) { static const auto Check1 = QRegularExpression( - "^(https?://)?([^/#\\:]+)([/#\\:]|$)", + "^((https?|s?ftp)://)?([^/#\\:]+)([/#\\:]|$)", QRegularExpression::CaseInsensitiveOption); const auto match1 = Check1.match(url); if (!match1.hasMatch()) { return false; } - const auto domain = match1.capturedRef(2); + const auto domain = match1.capturedRef(3); static const auto Check2 = QRegularExpression("^(.*)\\.[a-zA-Z]+$"); const auto match2 = Check2.match(domain); if (!match2.hasMatch()) { diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 60a8a52..ca49fdc 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -21,10 +21,14 @@ namespace Text { namespace { constexpr auto kStringLinkIndexShift = uint16(0x8000); +constexpr auto kMaxDiacAfterSymbol = 2; -Qt::LayoutDirection StringDirection(const QString &str, int32 from, int32 to) { - const ushort *p = reinterpret_cast(str.unicode()) + from; - const ushort *end = p + (to - from); +Qt::LayoutDirection StringDirection( + const QString &str, + int from, + int to) { + auto p = reinterpret_cast(str.unicode()) + from; + const auto end = p + (to - from); while (p < end) { uint ucs4 = *p; if (QChar::isHighSurrogate(ucs4) && p < end - 1) { @@ -98,7 +102,9 @@ TextWithEntities PrepareRichFromRich( return result; } -QFixed ComputeStopAfter(const TextParseOptions &options, const style::TextStyle &st) { +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; @@ -112,11 +118,17 @@ bool ComputeCheckTilde(const style::TextStyle &st) { && (font->f.family() == qstr("DAOpenSansRegular")); } -} // namespace -} // namespace Text -} // namespace Ui +bool IsParagraphSeparator(QChar ch) { + switch (ch.unicode()) { + case QChar::LineFeed: + return true; + default: + break; + } + return false; +} -bool chIsBad(QChar ch) { +bool IsBad(QChar ch) { return (ch == 0) || (ch >= 8232 && ch < 8237) || (ch >= 65024 && ch < 65040 && ch != 65039) @@ -130,9 +142,13 @@ bool chIsBad(QChar ch) { && !Platform::IsMac10_12OrGreater() && ch >= 0x0B00 && ch <= 0x0B7F - && chIsDiac(ch)); + && IsDiac(ch)); } +} // namespace +} // namespace Text +} // namespace Ui + QString textcmdSkipBlock(ushort w, ushort h) { static QString cmd(5, TextCommand); cmd[1] = QChar(TextCommandSkipBlock); @@ -461,13 +477,13 @@ void Parser::createBlock(int32 skipBack) { } _lastSkipped = false; if (_emoji) { - _t->_blocks.push_back(std::make_unique(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _emoji)); + _t->_blocks.push_back(Block::Emoji(_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)); + _t->_blocks.push_back(Block::Newline(_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)); + _t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex)); } _blockStart += len; blockCreated(); @@ -477,7 +493,7 @@ void Parser::createBlock(int32 skipBack) { 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)); + _t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex)); blockCreated(); } @@ -745,18 +761,18 @@ bool Parser::readCommand() { 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 isNewLine = _multiline && IsNewline(_ch); + const auto isSpace = IsSpace(_ch); + const auto isDiac = IsDiac(_ch); const auto isTilde = _checkTilde && (_ch == '~'); const auto skip = [&] { - if (chIsBad(_ch) || _ch.isLowSurrogate()) { + if (IsBad(_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()) { + if (_lastSkipped || _emoji || ++_diacs > kMaxDiacAfterSymbol) { return true; } } else if (_ch.isHighSurrogate()) { @@ -891,10 +907,10 @@ void Parser::trimSourceRange() { _source.entities, _end - _start); - while (_ptr != _end && chIsTrimmed(*_ptr, _rich) && _ptr != _start + firstMonospaceOffset) { + while (_ptr != _end && IsTrimmed(*_ptr, _rich) && _ptr != _start + firstMonospaceOffset) { ++_ptr; } - while (_ptr != _end && chIsTrimmed(*(_end - 1), _rich)) { + while (_ptr != _end && IsTrimmed(*(_end - 1), _rich)) { --_end; } } @@ -913,15 +929,14 @@ void Parser::checkForElidedSkipBlock() { 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(); + for (auto &block : _t->_blocks) { + const auto shiftedIndex = block->lnkIndex(); if (shiftedIndex <= kStringLinkIndexShift) { continue; } const auto realIndex = (shiftedIndex - kStringLinkIndexShift); const auto index = _maxLnkIndex + realIndex; - b->setLnkIndex(index); + block->setLnkIndex(index); if (_t->_links.size() >= index) { continue; } @@ -935,7 +950,7 @@ void Parser::finalize(const TextParseOptions &options) { } } _t->_links.squeeze(); - _t->_blocks.shrink_to_fit(); + _t->_blocks.squeeze(); _t->_text.squeeze(); } @@ -1165,7 +1180,7 @@ public: _wLeft -= _elideRemoveFromEnd; } - _parDirection = static_cast(b)->nextDirection(); + _parDirection = static_cast(b)->nextDirection(); if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); initNextParagraph(i + 1); @@ -1187,7 +1202,7 @@ public: } if (_btype == TextBlockTText) { - auto t = static_cast(b); + 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(); @@ -1678,7 +1693,7 @@ private: } Emoji::Draw( *_p, - static_cast(currentBlock)->emoji, + static_cast(currentBlock)->_emoji, Emoji::GetSizeNormal(), (glyphX + st::emojiPadding).toInt(), _y + _yDelta + emojiY); @@ -1847,7 +1862,7 @@ private: _p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg); } - void elideSaveBlock(int32 blockIndex, AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { + void elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { if (_elideSavedBlock) { restoreAfterElided(); } @@ -1855,7 +1870,7 @@ private: _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()); + mutableText->_blocks[blockIndex] = Block::Text(_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); } @@ -1870,7 +1885,7 @@ private: } } - void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, AbstractBlock *&_endBlock, int repeat = 0) { + void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat = 0) { static const auto _Elide = QString::fromLatin1("..."); _f = _t->_st->font; @@ -1980,7 +1995,7 @@ private: void restoreAfterElided() { if (_elideSavedBlock) { - const_cast(_t)->_blocks[_elideSavedIndex] = std::move(_elideSavedBlock); + const_cast(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock); } } @@ -2034,7 +2049,7 @@ private: return result; } - void eSetFont(AbstractBlock *block) { + void eSetFont(const AbstractBlock *block) { const auto flags = block->flags(); const auto usedFont = [&] { if (const auto index = block->lnkIndex()) { @@ -2609,7 +2624,7 @@ private: } private: - void applyBlockProperties(AbstractBlock *block) { + void applyBlockProperties(const AbstractBlock *block) { eSetFont(block); if (_p) { if (block->lnkIndex()) { @@ -2660,7 +2675,7 @@ private: // elided hack support int _blocksSize = 0; int _elideSavedIndex = 0; - std::unique_ptr _elideSavedBlock; + std::optional _elideSavedBlock; int _lineStart = 0; int _localFrom = 0; @@ -2679,7 +2694,8 @@ private: String::String(int32 minResizeWidth) : _minResizeWidth(minResizeWidth) { } -String::String(const style::TextStyle &st, const QString &text, const TextParseOptions &options, int32 minResizeWidth, bool richText) : _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 { @@ -2687,60 +2703,6 @@ String::String(const style::TextStyle &st, const QString &text, const TextParseO } } -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(); @@ -2757,8 +2719,8 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { int32 lineHeight = 0; int32 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(); + for (auto &block : _blocks) { + auto b = block.get(); auto _btype = b->type(); auto blockHeight = countBlockHeight(b, _st); if (_btype == TextBlockTNewline) { @@ -2775,7 +2737,7 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { } } lastNewlineStart = b->from(); - lastNewline = static_cast(b); + lastNewline = &block.unsafe(); _minHeight += lineHeight; lineHeight = 0; @@ -2824,19 +2786,19 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { } int String::countMaxMonospaceWidth() const { - NewlineBlock *lastNewline = 0; + const NewlineBlock *lastNewline = nullptr; auto result = QFixed(); auto paragraphWidth = QFixed(); auto lastNewlineStart = 0; auto fullMonospace = true; QFixed _width = 0, last_rBearing = 0, last_rPadding = 0; - for (auto i = _blocks.cbegin(), e = _blocks.cend(); i != e; ++i) { - auto b = i->get(); + for (auto &block : _blocks) { + auto b = block.get(); auto _btype = b->type(); if (_btype == TextBlockTNewline) { lastNewlineStart = b->from(); - lastNewline = static_cast(b); + lastNewline = &block.unsafe(); last_rBearing = b->f_rbearing(); last_rPadding = b->f_rpadding(); @@ -2892,7 +2854,7 @@ void String::setMarkedText(const style::TextStyle &st, const TextWithEntities &t // for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) { // if (*ch == TextCommand) { // break; -// } else if (chIsNewline(*ch)) { +// } else if (IsNewline(*ch)) { // newText.append("},").append(*ch).append("\t{ "); // } else { // if (ch->isHighSurrogate() || ch->isLowSurrogate()) { @@ -2943,7 +2905,7 @@ bool String::updateSkipBlock(int width, int height) { _blocks.pop_back(); } _text.push_back('_'); - _blocks.push_back(std::make_unique( + _blocks.push_back(Block::Skip( _st->font, _text, _text.size() - 1, @@ -3036,7 +2998,7 @@ void String::enumerateLines( } if (_btype == TextBlockTText) { - auto t = static_cast(b.get()); + const auto t = &b.unsafe(); if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line last_rPadding += b->f_rpadding(); @@ -3157,31 +3119,31 @@ TextSelection String::adjustSelection(TextSelection selection, TextSelectType se 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))) { + if (!IsParagraphSeparator(_text.at(from))) { + while (from > 0 && !IsParagraphSeparator(_text.at(from - 1))) { --from; } } if (to < _text.size()) { - if (chIsParagraphSeparator(_text.at(to))) { + if (IsParagraphSeparator(_text.at(to))) { ++to; } else { - while (to < _text.size() && !chIsParagraphSeparator(_text.at(to))) { + while (to < _text.size() && !IsParagraphSeparator(_text.at(to))) { ++to; } } } } else if (selectType == TextSelectType::Words) { - if (!chIsWordSeparator(_text.at(from))) { - while (from > 0 && !chIsWordSeparator(_text.at(from - 1))) { + if (!IsWordSeparator(_text.at(from))) { + while (from > 0 && !IsWordSeparator(_text.at(from - 1))) { --from; } } if (to < _text.size()) { - if (chIsWordSeparator(_text.at(to))) { + if (IsWordSeparator(_text.at(to))) { ++to; } else { - while (to < _text.size() && !chIsWordSeparator(_text.at(to))) { + while (to < _text.size() && !IsWordSeparator(_text.at(to))) { ++to; } } @@ -3389,7 +3351,7 @@ IsolatedEmoji String::toIsolatedEmoji() const { if (block->lnkIndex()) { return IsolatedEmoji(); } else if (type == TextBlockTEmoji) { - result.items[index++] = static_cast(block.get())->emoji; + result.items[index++] = block.unsafe()._emoji; } else if (type != TextBlockTSkip) { return IsolatedEmoji(); } @@ -3409,7 +3371,113 @@ void String::clearFields() { _startDir = Qt::LayoutDirectionAuto; } -String::~String() = default; +bool IsWordSeparator(QChar ch) { + switch (ch.unicode()) { + case QChar::Space: + case QChar::LineFeed: + case '.': + case ',': + case '?': + case '!': + case '@': + case '#': + case '$': + case ':': + case ';': + case '-': + case '<': + case '>': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + case '=': + case '/': + case '+': + case '%': + case '&': + case '^': + case '*': + case '\'': + case '"': + case '`': + case '~': + case '|': + return true; + default: + break; + } + return false; +} + +bool IsAlmostLinkEnd(QChar ch) { + switch (ch.unicode()) { + case '?': + case ',': + case '.': + case '"': + case ':': + case '!': + case '\'': + return true; + default: + break; + } + return false; +} + +bool IsLinkEnd(QChar ch) { + return (ch == TextCommand) + || IsBad(ch) + || IsSpace(ch) + || IsNewline(ch) + || ch.isLowSurrogate() + || ch.isHighSurrogate(); +} + +bool IsNewline(QChar ch) { + return (ch == QChar::LineFeed) + || (ch == 156); +} + +bool IsSpace(QChar ch, bool rich) { + return ch.isSpace() + || (ch < 32 && !(rich && ch == TextCommand)) + || (ch == QChar::ParagraphSeparator) + || (ch == QChar::LineSeparator) + || (ch == QChar::ObjectReplacementCharacter) + || (ch == QChar::CarriageReturn) + || (ch == QChar::Tabulation) + || (ch == QChar(8203)/*Zero width space.*/); +} + +bool IsDiac(QChar ch) { // diac and variation selectors + return (ch.category() == QChar::Mark_NonSpacing) + || (ch == 1652) + || (ch >= 64606 && ch <= 64611); +} + +bool IsReplacedBySpace(QChar ch) { + // \xe2\x80[\xa8 - \xac\xad] // 8232 - 8237 + // QString from1 = QString::fromUtf8("\xe2\x80\xa8"), to1 = QString::fromUtf8("\xe2\x80\xad"); + // \xcc[\xb3\xbf\x8a] // 819, 831, 778 + // QString bad1 = QString::fromUtf8("\xcc\xb3"), bad2 = QString::fromUtf8("\xcc\xbf"), bad3 = QString::fromUtf8("\xcc\x8a"); + // [\x00\x01\x02\x07\x08\x0b-\x1f] // '\t' = 0x09 + return (/*code >= 0x00 && */ch <= 0x02) + || (ch >= 0x07 && ch <= 0x09) + || (ch >= 0x0b && ch <= 0x1f) + || (ch == 819) + || (ch == 831) + || (ch == 778) + || (ch >= 8232 && ch <= 8237); +} + +bool IsTrimmed(QChar ch, bool rich) { + return (!rich || ch != TextCommand) + && (IsSpace(ch) || IsBad(ch)); +} } // namespace Text } // namespace Ui diff --git a/ui/text/text.h b/ui/text/text.h index 709f22a..9019990 100644 --- a/ui/text/text.h +++ b/ui/text/text.h @@ -7,6 +7,7 @@ #pragma once #include "ui/text/text_entity.h" +#include "ui/text/text_block.h" #include "ui/painter.h" #include "ui/click_handler.h" #include "base/flags.h" @@ -47,19 +48,20 @@ enum class TextSelectType { }; struct TextSelection { - constexpr TextSelection() : from(0), to(0) { - } + constexpr TextSelection() = default; constexpr TextSelection(uint16 from, uint16 to) : from(from), to(to) { } constexpr bool empty() const { return from == to; } - uint16 from; - uint16 to; + uint16 from = 0; + uint16 to = 0; }; + inline bool operator==(TextSelection a, TextSelection b) { return a.from == b.from && a.to == b.to; } + inline bool operator!=(TextSelection a, TextSelection b) { return !(a == b); } @@ -69,7 +71,6 @@ static constexpr TextSelection AllTextSelection = { 0, 0xFFFF }; namespace Ui { namespace Text { -class AbstractBlock; struct IsolatedEmoji; struct StateRequest { @@ -108,11 +109,17 @@ struct StateRequestElided : public StateRequest { class String { public: String(int32 minResizeWidth = QFIXED_MAX); - String(const style::TextStyle &st, const QString &text, const TextParseOptions &options = _defaultOptions, int32 minResizeWidth = QFIXED_MAX, bool richText = false); - String(const String &other); - String(String &&other); - String &operator=(const String &other); - String &operator=(String &&other); + String( + const style::TextStyle &st, + const QString &text, + const TextParseOptions &options = _defaultOptions, + int32 minResizeWidth = QFIXED_MAX, + bool richText = false); + String(const String &other) = default; + String(String &&other) = default; + String &operator=(const String &other) = default; + String &operator=(String &&other) = default; + ~String() = default; int countWidth(int width, bool breakEverywhere = false) const; int countHeight(int width, bool breakEverywhere = false) const; @@ -168,34 +175,14 @@ public: TextSelection selection = AllTextSelection) const; IsolatedEmoji toIsolatedEmoji() const; - bool lastDots(int32 dots, int32 maxdots = 3) { // hack for typing animation - if (_text.size() < maxdots) return false; - - int32 nowDots = 0, from = _text.size() - maxdots, to = _text.size(); - for (int32 i = from; i < to; ++i) { - if (_text.at(i) == QChar('.')) { - ++nowDots; - } - } - if (nowDots == dots) return false; - for (int32 j = from; j < from + dots; ++j) { - _text[j] = QChar('.'); - } - for (int32 j = from + dots; j < to; ++j) { - _text[j] = QChar(' '); - } - return true; - } - const style::TextStyle *style() const { return _st; } void clear(); - ~String(); private: - using TextBlocks = std::vector>; + using TextBlocks = QVector; using TextLinks = QVector; uint16 countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const; @@ -239,6 +226,15 @@ private: }; +[[nodiscard]] bool IsWordSeparator(QChar ch); +[[nodiscard]] bool IsAlmostLinkEnd(QChar ch); +[[nodiscard]] bool IsLinkEnd(QChar ch); +[[nodiscard]] bool IsNewline(QChar ch); +[[nodiscard]] bool IsSpace(QChar ch, bool rich = false); +[[nodiscard]] bool IsDiac(QChar ch); +[[nodiscard]] bool IsReplacedBySpace(QChar ch); +[[nodiscard]] bool IsTrimmed(QChar ch, bool rich = false); + } // namespace Text } // namespace Ui @@ -268,120 +264,3 @@ QString textcmdLink(const QString &url, const QString &text); QString textcmdStartSemibold(); QString textcmdStopSemibold(); const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink = true); - -inline bool chIsSpace(QChar ch, bool rich = false) { - return ch.isSpace() || (ch < 32 && !(rich && ch == TextCommand)) || (ch == QChar::ParagraphSeparator) || (ch == QChar::LineSeparator) || (ch == QChar::ObjectReplacementCharacter) || (ch == QChar::CarriageReturn) || (ch == QChar::Tabulation) || (ch == QChar(8203)/*Zero width space.*/); -} -inline bool chIsDiac(QChar ch) { // diac and variation selectors - return (ch.category() == QChar::Mark_NonSpacing) || (ch == 1652) || (ch >= 64606 && ch <= 64611); -} - -bool chIsBad(QChar ch); - -inline bool chIsTrimmed(QChar ch, bool rich = false) { - return (!rich || ch != TextCommand) && (chIsSpace(ch) || chIsBad(ch)); -} -inline bool chReplacedBySpace(QChar ch) { - // \xe2\x80[\xa8 - \xac\xad] // 8232 - 8237 - // QString from1 = QString::fromUtf8("\xe2\x80\xa8"), to1 = QString::fromUtf8("\xe2\x80\xad"); - // \xcc[\xb3\xbf\x8a] // 819, 831, 778 - // QString bad1 = QString::fromUtf8("\xcc\xb3"), bad2 = QString::fromUtf8("\xcc\xbf"), bad3 = QString::fromUtf8("\xcc\x8a"); - // [\x00\x01\x02\x07\x08\x0b-\x1f] // '\t' = 0x09 - return (/*code >= 0x00 && */ch <= 0x02) || (ch >= 0x07 && ch <= 0x09) || (ch >= 0x0b && ch <= 0x1f) || - (ch == 819) || (ch == 831) || (ch == 778) || (ch >= 8232 && ch <= 8237); -} -inline int32 chMaxDiacAfterSymbol() { - return 2; -} -inline bool chIsNewline(QChar ch) { - return (ch == QChar::LineFeed || ch == 156); -} -inline bool chIsLinkEnd(QChar ch) { - return ch == TextCommand || chIsBad(ch) || chIsSpace(ch) || chIsNewline(ch) || ch.isLowSurrogate() || ch.isHighSurrogate(); -} -inline bool chIsAlmostLinkEnd(QChar ch) { - switch (ch.unicode()) { - case '?': - case ',': - case '.': - case '"': - case ':': - case '!': - case '\'': - return true; - default: - break; - } - return false; -} -inline bool chIsWordSeparator(QChar ch) { - switch (ch.unicode()) { - case QChar::Space: - case QChar::LineFeed: - case '.': - case ',': - case '?': - case '!': - case '@': - case '#': - case '$': - case ':': - case ';': - case '-': - case '<': - case '>': - case '[': - case ']': - case '(': - case ')': - case '{': - case '}': - case '=': - case '/': - case '+': - case '%': - case '&': - case '^': - case '*': - case '\'': - case '"': - case '`': - case '~': - case '|': - return true; - default: - break; - } - return false; -} -inline bool chIsSentenceEnd(QChar ch) { - switch (ch.unicode()) { - case '.': - case '?': - case '!': - return true; - default: - break; - } - return false; -} -inline bool chIsSentencePartEnd(QChar ch) { - switch (ch.unicode()) { - case ',': - case ':': - case ';': - return true; - default: - break; - } - return false; -} -inline bool chIsParagraphSeparator(QChar ch) { - switch (ch.unicode()) { - case QChar::LineFeed: - return true; - default: - break; - } - return false; -} diff --git a/ui/text/text_block.cpp b/ui/text/text_block.cpp index c2a3f11..d62a114 100644 --- a/ui/text/text_block.cpp +++ b/ui/text/text_block.cpp @@ -17,89 +17,34 @@ namespace Text { namespace { struct ScriptLine { - ScriptLine() : length(0), textWidth(0) { - } - - int32 length; + int length = 0; QFixed textWidth; }; // All members finished with "_" are internal. -struct LineBreakHelper -{ - LineBreakHelper() - : glyphCount(0), maxGlyphs(INT_MAX), currentPosition(0), fontEngine(0), logClusters(0) - { - } - - +struct LineBreakHelper { ScriptLine tmpData; ScriptLine spaceData; QGlyphLayout glyphs; - int glyphCount; - int maxGlyphs; - int currentPosition; + int glyphCount = 0; + int maxGlyphs = INT_MAX; + int currentPosition = 0; glyph_t previousGlyph_ = 0; QFontEngine *previousFontEngine_ = nullptr; QFixed rightBearing; - QFontEngine *fontEngine; - const unsigned short *logClusters; + QFontEngine *fontEngine = nullptr; + const unsigned short *logClusters = nullptr; - inline glyph_t currentGlyph() const - { - Q_ASSERT(currentPosition > 0); - Q_ASSERT(logClusters[currentPosition - 1] < glyphs.numGlyphs); - - return glyphs.glyphs[logClusters[currentPosition - 1]]; - } - - inline void saveCurrentGlyph() - { - if (currentPosition > 0 && - logClusters[currentPosition - 1] < glyphs.numGlyphs) { - previousGlyph_ = currentGlyph(); // needed to calculate right bearing later - previousFontEngine_ = fontEngine; - } else { - previousGlyph_ = 0; - previousFontEngine_ = nullptr; - } - } - - inline void calculateRightBearing(QFontEngine *engine, glyph_t glyph) - { - qreal rb; - engine->getGlyphBearings(glyph, 0, &rb); - - // We only care about negative right bearings, so we limit the range - // of the bearing here so that we can assume it's negative in the rest - // of the code, as well ase use QFixed(1) as a sentinel to represent - // the state where we have yet to compute the right bearing. - rightBearing = qMin(QFixed::fromReal(rb), QFixed(0)); - } - - inline void calculateRightBearing() - { - if (currentPosition > 0 && - logClusters[currentPosition - 1] < glyphs.numGlyphs) { - calculateRightBearing(fontEngine, currentGlyph()); - } else { - rightBearing = 0; - } - } - - inline void calculateRightBearingForPreviousGlyph() - { - if (previousGlyph_ > 0) { - calculateRightBearing(previousFontEngine_, previousGlyph_); - } else { - rightBearing = 0; - } - } + glyph_t currentGlyph() const; + void saveCurrentGlyph(); + void calculateRightBearing(QFontEngine *engine, glyph_t glyph); + void calculateRightBearing(); + void calculateRightBearingForPreviousGlyph(); // We always calculate the right bearing right before it is needed. // So we don't need caching / optimizations referred to delayed right bearing calculations. @@ -113,26 +58,92 @@ struct LineBreakHelper // We express the negative right bearing as an absolute number // so that it can be applied to the width using addition. - inline QFixed negativeRightBearing() const - { - //if (rightBearing == RightBearingNotCalculated) - // return QFixed(0); - - return qAbs(rightBearing); - } + QFixed negativeRightBearing() const; }; //const QFixed LineBreakHelper::RightBearingNotCalculated = QFixed(1); +glyph_t LineBreakHelper::currentGlyph() const { + Q_ASSERT(currentPosition > 0); + Q_ASSERT(logClusters[currentPosition - 1] < glyphs.numGlyphs); + + return glyphs.glyphs[logClusters[currentPosition - 1]]; +} + +void LineBreakHelper::saveCurrentGlyph() { + if (currentPosition > 0 + && logClusters[currentPosition - 1] < glyphs.numGlyphs) { + // needed to calculate right bearing later + previousGlyph_ = currentGlyph(); + previousFontEngine_ = fontEngine; + } else { + previousGlyph_ = 0; + previousFontEngine_ = nullptr; + } +} + +void LineBreakHelper::calculateRightBearing( + QFontEngine *engine, + glyph_t glyph) { + qreal rb; + engine->getGlyphBearings(glyph, 0, &rb); + + // We only care about negative right bearings, so we limit the range + // of the bearing here so that we can assume it's negative in the rest + // of the code, as well ase use QFixed(1) as a sentinel to represent + // the state where we have yet to compute the right bearing. + rightBearing = qMin(QFixed::fromReal(rb), QFixed(0)); +} + +void LineBreakHelper::calculateRightBearing() { + if (currentPosition > 0 + && logClusters[currentPosition - 1] < glyphs.numGlyphs) { + calculateRightBearing(fontEngine, currentGlyph()); + } else { + rightBearing = 0; + } +} + +void LineBreakHelper::calculateRightBearingForPreviousGlyph() { + if (previousGlyph_ > 0) { + calculateRightBearing(previousFontEngine_, previousGlyph_); + } else { + rightBearing = 0; + } +} + +// We always calculate the right bearing right before it is needed. +// So we don't need caching / optimizations referred to delayed right bearing calculations. + +//static const QFixed RightBearingNotCalculated; + +//inline void resetRightBearing() +//{ +// rightBearing = RightBearingNotCalculated; +//} + +// We express the negative right bearing as an absolute number +// so that it can be applied to the width using addition. +QFixed LineBreakHelper::negativeRightBearing() const { + //if (rightBearing == RightBearingNotCalculated) + // return QFixed(0); + + return qAbs(rightBearing); +} + QString DebugCurrentParsingString, DebugCurrentParsingPart; int DebugCurrentParsingFrom = 0; int DebugCurrentParsingLength = 0; -static inline void addNextCluster(int &pos, int end, ScriptLine &line, int &glyphCount, - const QScriptItem ¤t, const unsigned short *logClusters, - const QGlyphLayout &glyphs) -{ +void addNextCluster( + int &pos, + int end, + ScriptLine &line, + int &glyphCount, + const QScriptItem ¤t, + const unsigned short *logClusters, + const QGlyphLayout &glyphs) { int glyphPosition = logClusters[pos]; do { // got to the first next cluster ++pos; @@ -142,7 +153,8 @@ static inline void addNextCluster(int &pos, int end, ScriptLine &line, int &glyp if (!glyphs.attributes[glyphPosition].dontPrint) line.textWidth += glyphs.advances[glyphPosition]; ++glyphPosition; - } while (glyphPosition < current.num_glyphs && !glyphs.attributes[glyphPosition].clusterStart); + } while (glyphPosition < current.num_glyphs + && !glyphs.attributes[glyphPosition].clusterStart); if (!((pos == end && glyphPosition == current.num_glyphs) || logClusters[pos] == glyphPosition)) { auto str = QStringList(); @@ -153,7 +165,8 @@ static inline void addNextCluster(int &pos, int end, ScriptLine &line, int &glyp base::Integration::Instance().logAssertionViolation(QString("pos: %1, end: %2, glyphPosition: %3, glyphCount: %4, lineLength: %5, num_glyphs: %6, logClusters[0..pos]: %7").arg(pos).arg(end).arg(glyphPosition).arg(glyphCount).arg(line.length).arg(current.num_glyphs).arg(str.join(","))); Unexpected("Values in addNextCluster()"); } - Q_ASSERT((pos == end && glyphPosition == current.num_glyphs) || logClusters[pos] == glyphPosition); + Q_ASSERT((pos == end && glyphPosition == current.num_glyphs) + || logClusters[pos] == glyphPosition); ++glyphCount; } @@ -162,146 +175,16 @@ static inline void addNextCluster(int &pos, int end, ScriptLine &line, int &glyp class BlockParser { public: - - BlockParser(QTextEngine *e, TextBlock *b, QFixed minResizeWidth, int32 blockFrom, const QString &str) - : block(b), eng(e), str(str) { - parseWords(minResizeWidth, blockFrom); - } - - void parseWords(QFixed minResizeWidth, int32 blockFrom) { - LineBreakHelper lbh; - - // Helper for debugging crashes in text processing. - // - // auto debugChars = QString(); - // debugChars.reserve(str.size() * 7); - // for (const auto ch : str) { - // debugChars.append( - // "0x").append( - // QString::number(ch.unicode(), 16).toUpper()).append( - // ' '); - // } - // LOG(("Text: %1, chars: %2").arg(str).arg(debugChars)); - - int item = -1; - int newItem = eng->findItem(0); - - style::align alignment = eng->option.alignment(); - - const QCharAttributes *attributes = eng->attributes(); - if (!attributes) - return; - int end = 0; - lbh.logClusters = eng->layoutData->logClustersPtr; - - block->_words.clear(); - - int wordStart = lbh.currentPosition; - - bool addingEachGrapheme = false; - int lastGraphemeBoundaryPosition = -1; - ScriptLine lastGraphemeBoundaryLine; - - while (newItem < eng->layoutData->items.size()) { - if (newItem != item) { - item = newItem; - const QScriptItem ¤t = eng->layoutData->items[item]; - if (!current.num_glyphs) { - eng->shape(item); - attributes = eng->attributes(); - if (!attributes) - return; - lbh.logClusters = eng->layoutData->logClustersPtr; - } - lbh.currentPosition = current.position; - end = current.position + eng->length(item); - lbh.glyphs = eng->shapedGlyphs(¤t); - QFontEngine *fontEngine = eng->fontEngine(current); - if (lbh.fontEngine != fontEngine) { - lbh.fontEngine = fontEngine; - } - } - const QScriptItem ¤t = eng->layoutData->items[item]; - - if (attributes[lbh.currentPosition].whiteSpace) { - while (lbh.currentPosition < end && attributes[lbh.currentPosition].whiteSpace) - addNextCluster(lbh.currentPosition, end, lbh.spaceData, lbh.glyphCount, - current, lbh.logClusters, lbh.glyphs); - - if (block->_words.isEmpty()) { - block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing())); - } - block->_words.back().add_rpadding(lbh.spaceData.textWidth); - block->_width += lbh.spaceData.textWidth; - lbh.spaceData.length = 0; - lbh.spaceData.textWidth = 0; - - wordStart = lbh.currentPosition; - - addingEachGrapheme = false; - lastGraphemeBoundaryPosition = -1; - lastGraphemeBoundaryLine = ScriptLine(); - } else { - do { - addNextCluster(lbh.currentPosition, end, lbh.tmpData, lbh.glyphCount, - current, lbh.logClusters, lbh.glyphs); - - if (lbh.currentPosition >= eng->layoutData->string.length() - || attributes[lbh.currentPosition].whiteSpace - || isLineBreak(attributes, lbh.currentPosition)) { - lbh.calculateRightBearing(); - block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing())); - block->_width += lbh.tmpData.textWidth; - lbh.tmpData.textWidth = 0; - lbh.tmpData.length = 0; - wordStart = lbh.currentPosition; - break; - } else if (attributes[lbh.currentPosition].graphemeBoundary) { - if (!addingEachGrapheme && lbh.tmpData.textWidth > minResizeWidth) { - if (lastGraphemeBoundaryPosition >= 0) { - lbh.calculateRightBearingForPreviousGlyph(); - block->_words.push_back(TextWord(wordStart + blockFrom, -lastGraphemeBoundaryLine.textWidth, -lbh.negativeRightBearing())); - block->_width += lastGraphemeBoundaryLine.textWidth; - lbh.tmpData.textWidth -= lastGraphemeBoundaryLine.textWidth; - lbh.tmpData.length -= lastGraphemeBoundaryLine.length; - wordStart = lastGraphemeBoundaryPosition; - } - addingEachGrapheme = true; - } - if (addingEachGrapheme) { - lbh.calculateRightBearing(); - block->_words.push_back(TextWord(wordStart + blockFrom, -lbh.tmpData.textWidth, -lbh.negativeRightBearing())); - block->_width += lbh.tmpData.textWidth; - lbh.tmpData.textWidth = 0; - lbh.tmpData.length = 0; - wordStart = lbh.currentPosition; - } else { - lastGraphemeBoundaryPosition = lbh.currentPosition; - lastGraphemeBoundaryLine = lbh.tmpData; - lbh.saveCurrentGlyph(); - } - } - } while (lbh.currentPosition < end); - } - if (lbh.currentPosition == end) - newItem = item + 1; - } - if (!block->_words.isEmpty()) { - block->_rpadding = block->_words.back().f_rpadding(); - block->_width -= block->_rpadding; - block->_words.squeeze(); - } - } - - bool isLineBreak(const QCharAttributes *attributes, int32 index) { - bool lineBreak = attributes[index].lineBreak; - if (lineBreak && block->lnkIndex() > 0 && index > 0 && str.at(index - 1) == '/') { - return false; // don't break after / in links - } - return lineBreak; - } + BlockParser( + QTextEngine *e, + TextBlock *b, + QFixed minResizeWidth, + int blockFrom, + const QString &str); private: + void parseWords(QFixed minResizeWidth, int blockFrom); + bool isLineBreak(const QCharAttributes *attributes, int index); TextBlock *block; QTextEngine *eng; @@ -309,11 +192,171 @@ private: }; -QFixed AbstractBlock::f_rbearing() const { - return (type() == TextBlockTText) ? static_cast(this)->real_f_rbearing() : 0; +BlockParser::BlockParser( + QTextEngine *e, + TextBlock *b, + QFixed minResizeWidth, + int blockFrom, + const QString &str) +: block(b) +, eng(e) +, str(str) { + parseWords(minResizeWidth, blockFrom); } -TextBlock::TextBlock(const style::font &font, const QString &str, QFixed minResizeWidth, uint16 from, uint16 length, uchar flags, uint16 lnkIndex) : AbstractBlock(font, str, from, length, flags, lnkIndex) { +void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { + LineBreakHelper lbh; + + // Helper for debugging crashes in text processing. + // + // auto debugChars = QString(); + // debugChars.reserve(str.size() * 7); + // for (const auto ch : str) { + // debugChars.append( + // "0x").append( + // QString::number(ch.unicode(), 16).toUpper()).append( + // ' '); + // } + // LOG(("Text: %1, chars: %2").arg(str).arg(debugChars)); + + int item = -1; + int newItem = eng->findItem(0); + + style::align alignment = eng->option.alignment(); + + const QCharAttributes *attributes = eng->attributes(); + if (!attributes) + return; + int end = 0; + lbh.logClusters = eng->layoutData->logClustersPtr; + + block->_words.clear(); + + int wordStart = lbh.currentPosition; + + bool addingEachGrapheme = false; + int lastGraphemeBoundaryPosition = -1; + ScriptLine lastGraphemeBoundaryLine; + + while (newItem < eng->layoutData->items.size()) { + if (newItem != item) { + item = newItem; + const QScriptItem ¤t = eng->layoutData->items[item]; + if (!current.num_glyphs) { + eng->shape(item); + attributes = eng->attributes(); + if (!attributes) + return; + lbh.logClusters = eng->layoutData->logClustersPtr; + } + lbh.currentPosition = current.position; + end = current.position + eng->length(item); + lbh.glyphs = eng->shapedGlyphs(¤t); + QFontEngine *fontEngine = eng->fontEngine(current); + if (lbh.fontEngine != fontEngine) { + lbh.fontEngine = fontEngine; + } + } + const QScriptItem ¤t = eng->layoutData->items[item]; + + if (attributes[lbh.currentPosition].whiteSpace) { + while (lbh.currentPosition < end && attributes[lbh.currentPosition].whiteSpace) + addNextCluster(lbh.currentPosition, end, lbh.spaceData, lbh.glyphCount, + current, lbh.logClusters, lbh.glyphs); + + if (block->_words.isEmpty()) { + block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing())); + } + block->_words.back().add_rpadding(lbh.spaceData.textWidth); + block->_width += lbh.spaceData.textWidth; + lbh.spaceData.length = 0; + lbh.spaceData.textWidth = 0; + + wordStart = lbh.currentPosition; + + addingEachGrapheme = false; + lastGraphemeBoundaryPosition = -1; + lastGraphemeBoundaryLine = ScriptLine(); + } else { + do { + addNextCluster(lbh.currentPosition, end, lbh.tmpData, lbh.glyphCount, + current, lbh.logClusters, lbh.glyphs); + + if (lbh.currentPosition >= eng->layoutData->string.length() + || attributes[lbh.currentPosition].whiteSpace + || isLineBreak(attributes, lbh.currentPosition)) { + lbh.calculateRightBearing(); + block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing())); + block->_width += lbh.tmpData.textWidth; + lbh.tmpData.textWidth = 0; + lbh.tmpData.length = 0; + wordStart = lbh.currentPosition; + break; + } else if (attributes[lbh.currentPosition].graphemeBoundary) { + if (!addingEachGrapheme && lbh.tmpData.textWidth > minResizeWidth) { + if (lastGraphemeBoundaryPosition >= 0) { + lbh.calculateRightBearingForPreviousGlyph(); + block->_words.push_back(TextWord(wordStart + blockFrom, -lastGraphemeBoundaryLine.textWidth, -lbh.negativeRightBearing())); + block->_width += lastGraphemeBoundaryLine.textWidth; + lbh.tmpData.textWidth -= lastGraphemeBoundaryLine.textWidth; + lbh.tmpData.length -= lastGraphemeBoundaryLine.length; + wordStart = lastGraphemeBoundaryPosition; + } + addingEachGrapheme = true; + } + if (addingEachGrapheme) { + lbh.calculateRightBearing(); + block->_words.push_back(TextWord(wordStart + blockFrom, -lbh.tmpData.textWidth, -lbh.negativeRightBearing())); + block->_width += lbh.tmpData.textWidth; + lbh.tmpData.textWidth = 0; + lbh.tmpData.length = 0; + wordStart = lbh.currentPosition; + } else { + lastGraphemeBoundaryPosition = lbh.currentPosition; + lastGraphemeBoundaryLine = lbh.tmpData; + lbh.saveCurrentGlyph(); + } + } + } while (lbh.currentPosition < end); + } + if (lbh.currentPosition == end) + newItem = item + 1; + } + if (!block->_words.isEmpty()) { + block->_rpadding = block->_words.back().f_rpadding(); + block->_width -= block->_rpadding; + block->_words.squeeze(); + } +} + +bool BlockParser::isLineBreak( + const QCharAttributes *attributes, + int index) { + bool lineBreak = attributes[index].lineBreak; + if (lineBreak + && block->lnkIndex() > 0 + && index > 0 + && str.at(index - 1) == '/') { + return false; // don't break after / in links + } + return lineBreak; +} + +QFixed AbstractBlock::f_rbearing() const { + return (type() == TextBlockTText) + ? static_cast(this)->real_f_rbearing() + : 0; +} + +TextBlock::TextBlock( + const style::font &font, + const QString &str, + QFixed minResizeWidth, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex) +: AbstractBlock(font, str, from, length, flags, lnkIndex) { _flags |= ((TextBlockTText & 0x0F) << 8); if (length) { style::font blockFont = font; @@ -347,11 +390,18 @@ TextBlock::TextBlock(const style::font &font, const QString &str, QFixed minResi } } -EmojiBlock::EmojiBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, uint16 lnkIndex, EmojiPtr emoji) : AbstractBlock(font, str, from, length, flags, lnkIndex) -, emoji(emoji) { +EmojiBlock::EmojiBlock( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex, + EmojiPtr emoji) +: AbstractBlock(font, str, from, length, flags, lnkIndex) +, _emoji(emoji) { _flags |= ((TextBlockTEmoji & 0x0F) << 8); _width = int(st::emojiSize + 2 * st::emojiPadding); - _rpadding = 0; for (auto i = length; i != 0;) { auto ch = str[_from + (--i)]; @@ -363,10 +413,5 @@ EmojiBlock::EmojiBlock(const style::font &font, const QString &str, uint16 from, } } -SkipBlock::SkipBlock(const style::font &font, const QString &str, uint16 from, int32 w, int32 h, uint16 lnkIndex) : AbstractBlock(font, str, from, 1, 0, lnkIndex), _height(h) { - _flags |= ((TextBlockTSkip & 0x0F) << 8); - _width = w; -} - } // namespace Text } // namespace Ui diff --git a/ui/text/text_block.h b/ui/text/text_block.h index 0bd5953..5f99108 100644 --- a/ui/text/text_block.h +++ b/ui/text/text_block.h @@ -34,16 +34,13 @@ enum TextBlockFlags { class AbstractBlock { public: - AbstractBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, uint16 lnkIndex) : _from(from), _flags((flags & 0xFF) | ((lnkIndex & 0xFFFF) << 12)) { - } - uint16 from() const { return _from; } - int32 width() const { + int width() const { return _width.toInt(); } - int32 rpadding() const { + int rpadding() const { return _rpadding.toInt(); } QFixed f_width() const { @@ -70,11 +67,18 @@ public: return (_flags & 0xFF); } - virtual std::unique_ptr clone() const = 0; - virtual ~AbstractBlock() { +protected: + AbstractBlock( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex) + : _from(from) + , _flags((flags & 0xFF) | ((lnkIndex & 0xFFFF) << 12)) { } -protected: uint16 _from = 0; uint32 _flags = 0; // 4 bits empty, 16 bits lnkIndex, 4 bits type, 8 bits flags @@ -90,9 +94,16 @@ protected: }; -class NewlineBlock : public AbstractBlock { +class NewlineBlock final : public AbstractBlock { public: - NewlineBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, uint16 lnkIndex) : AbstractBlock(font, str, from, length, flags, lnkIndex), _nextDir(Qt::LayoutDirectionAuto) { + NewlineBlock( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex) + : AbstractBlock(font, str, from, length, flags, lnkIndex) { _flags |= ((TextBlockTNewline & 0x0F) << 8); } @@ -100,12 +111,8 @@ public: return _nextDir; } - std::unique_ptr clone() const override { - return std::make_unique(*this); - } - private: - Qt::LayoutDirection _nextDir; + Qt::LayoutDirection _nextDir = Qt::LayoutDirectionAuto; friend class String; friend class Parser; @@ -113,14 +120,16 @@ private: }; -class TextWord { +class TextWord final { public: TextWord() = default; TextWord(uint16 from, QFixed width, QFixed rbearing, QFixed rpadding = 0) - : _from(from) - , _width(width) - , _rpadding(rpadding) - , _rbearing(rbearing.value() > 0x7FFF ? 0x7FFF : (rbearing.value() < -0x7FFF ? -0x7FFF : rbearing.value())) { + : _from(from) + , _rbearing((rbearing.value() > 0x7FFF) + ? 0x7FFF + : (rbearing.value() < -0x7FFF ? -0x7FFF : rbearing.value())) + , _width(width) + , _rpadding(rpadding) { } uint16 from() const { return _from; @@ -140,45 +149,50 @@ public: private: uint16 _from = 0; - QFixed _width, _rpadding; int16 _rbearing = 0; + QFixed _width, _rpadding; }; -class TextBlock : public AbstractBlock { +class TextBlock final : public AbstractBlock { public: - TextBlock(const style::font &font, const QString &str, QFixed minResizeWidth, uint16 from, uint16 length, uchar flags, uint16 lnkIndex); - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } + TextBlock( + const style::font &font, + const QString &str, + QFixed minResizeWidth, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex); private: - friend class AbstractBlock; QFixed real_f_rbearing() const { return _words.isEmpty() ? 0 : _words.back().f_rbearing(); } - using TextWords = QVector; - TextWords _words; + QVector _words; friend class String; friend class Parser; friend class Renderer; friend class BlockParser; + friend class AbstractBlock; }; -class EmojiBlock : public AbstractBlock { +class EmojiBlock final : public AbstractBlock { public: - EmojiBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, uint16 lnkIndex, EmojiPtr emoji); - - std::unique_ptr clone() const override { - return std::make_unique(*this); - } + EmojiBlock( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex, + EmojiPtr emoji); private: - EmojiPtr emoji = nullptr; + EmojiPtr _emoji = nullptr; friend class String; friend class Parser; @@ -186,20 +200,27 @@ private: }; -class SkipBlock : public AbstractBlock { +class SkipBlock final : public AbstractBlock { public: - SkipBlock(const style::font &font, const QString &str, uint16 from, int32 w, int32 h, uint16 lnkIndex); + SkipBlock( + const style::font &font, + const QString &str, + uint16 from, + int32 w, + int32 h, + uint16 lnkIndex) + : AbstractBlock(font, str, from, 1, 0, lnkIndex) + , _height(h) { + _flags |= ((TextBlockTSkip & 0x0F) << 8); + _width = w; + } - int32 height() const { + int height() const { return _height; } - std::unique_ptr clone() const override { - return std::make_unique(*this); - } - private: - int32 _height; + int _height = 0; friend class String; friend class Parser; @@ -207,5 +228,235 @@ private: }; +class Block final { +public: + Block() { + Unexpected("Should not be called."); + } + Block(const Block &other) { + switch (other->type()) { + case TextBlockTNewline: + emplace(other.unsafe()); + break; + case TextBlockTText: + emplace(other.unsafe()); + break; + case TextBlockTEmoji: + emplace(other.unsafe()); + break; + case TextBlockTSkip: + emplace(other.unsafe()); + break; + default: + Unexpected("Bad text block type in Block(const Block&)."); + } + } + Block(Block &&other) { + switch (other->type()) { + case TextBlockTNewline: + emplace(std::move(other.unsafe())); + break; + case TextBlockTText: + emplace(std::move(other.unsafe())); + break; + case TextBlockTEmoji: + emplace(std::move(other.unsafe())); + break; + case TextBlockTSkip: + emplace(std::move(other.unsafe())); + break; + default: + Unexpected("Bad text block type in Block(Block&&)."); + } + } + Block &operator=(const Block &other) { + if (&other == this) { + return *this; + } + destroy(); + switch (other->type()) { + case TextBlockTNewline: + emplace(other.unsafe()); + break; + case TextBlockTText: + emplace(other.unsafe()); + break; + case TextBlockTEmoji: + emplace(other.unsafe()); + break; + case TextBlockTSkip: + emplace(other.unsafe()); + break; + default: + Unexpected("Bad text block type in operator=(const Block&)."); + } + return *this; + } + Block &operator=(Block &&other) { + if (&other == this) { + return *this; + } + destroy(); + switch (other->type()) { + case TextBlockTNewline: + emplace(std::move(other.unsafe())); + break; + case TextBlockTText: + emplace(std::move(other.unsafe())); + break; + case TextBlockTEmoji: + emplace(std::move(other.unsafe())); + break; + case TextBlockTSkip: + emplace(std::move(other.unsafe())); + break; + default: + Unexpected("Bad text block type in operator=(Block&&)."); + } + return *this; + } + ~Block() { + destroy(); + } + + [[nodiscard]] static Block Newline( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex) { + return New(font, str, from, length, flags, lnkIndex); + } + + [[nodiscard]] static Block Text( + const style::font &font, + const QString &str, + QFixed minResizeWidth, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex) { + return New( + font, + str, + minResizeWidth, + from, + length, + flags, + lnkIndex); + } + + [[nodiscard]] static Block Emoji( + const style::font &font, + const QString &str, + uint16 from, + uint16 length, + uchar flags, + uint16 lnkIndex, + EmojiPtr emoji) { + return New( + font, + str, + from, + length, + flags, + lnkIndex, + emoji); + } + + [[nodiscard]] static Block Skip( + const style::font &font, + const QString &str, + uint16 from, + int32 w, + int32 h, + uint16 lnkIndex) { + return New(font, str, from, w, h, lnkIndex); + } + + template + [[nodiscard]] FinalBlock &unsafe() { + return *reinterpret_cast(&_data); + } + + template + [[nodiscard]] const FinalBlock &unsafe() const { + return *reinterpret_cast(&_data); + } + + [[nodiscard]] AbstractBlock *get() { + return &unsafe(); + } + + [[nodiscard]] const AbstractBlock *get() const { + return &unsafe(); + } + + [[nodiscard]] AbstractBlock *operator->() { + return get(); + } + + [[nodiscard]] const AbstractBlock *operator->() const { + return get(); + } + + [[nodiscard]] AbstractBlock &operator*() { + return *get(); + } + + [[nodiscard]] const AbstractBlock &operator*() const { + return *get(); + } + +private: + struct Tag { + }; + + explicit Block(const Tag &) { + } + + template + [[nodiscard]] static Block New(Args &&...args) { + auto result = Block(Tag{}); + result.emplace(std::forward(args)...); + return result; + } + + template + void emplace(Args &&...args) { + new (&_data) FinalType(std::forward(args)...); + } + + void destroy() { + switch (get()->type()) { + case TextBlockTNewline: + unsafe().~NewlineBlock(); + break; + case TextBlockTText: + unsafe().~TextBlock(); + break; + case TextBlockTEmoji: + unsafe().~EmojiBlock(); + break; + case TextBlockTSkip: + unsafe().~SkipBlock(); + break; + default: + Unexpected("Bad text block type in Block(Block&&)."); + } + } + + static_assert(sizeof(NewlineBlock) <= sizeof(TextBlock)); + static_assert(alignof(NewlineBlock) <= alignof(void*)); + static_assert(sizeof(EmojiBlock) <= sizeof(TextBlock)); + static_assert(alignof(EmojiBlock) <= alignof(void*)); + static_assert(sizeof(SkipBlock) <= sizeof(TextBlock)); + static_assert(alignof(SkipBlock) <= alignof(void*)); + + std::aligned_storage_t _data; + +}; + } // namespace Text } // namespace Ui diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index 995c72d..087fa77 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -21,6 +21,8 @@ namespace TextUtilities { namespace { +using namespace Ui::Text; + QString ExpressionMailNameAtEnd() { // Matches email first part (before '@') at the end of the string. // First we find a domain without protocol (like "gmail.com"), then @@ -1189,6 +1191,18 @@ std::unique_ptr MimeDataFromText( return result; } +bool IsSentencePartEnd(QChar ch) { + return (ch == ',') + || (ch == ':') + || (ch == ';'); +} + +bool IsSentenceEnd(QChar ch) { + return (ch == '.') + || (ch == '?') + || (ch == '!'); +} + } // namespace const QRegularExpression &RegExpMailNameAtEnd() { @@ -1303,10 +1317,10 @@ QString SingleLine(const QString &text) { auto s = text.unicode(), ch = s, e = text.unicode() + text.size(); // Trim. - while (s < e && chIsTrimmed(*s)) { + while (s < e && IsTrimmed(*s)) { ++s; } - while (s < e && chIsTrimmed(*(e - 1))) { + while (s < e && IsTrimmed(*(e - 1))) { --e; } if (e - s != text.size()) { @@ -1314,7 +1328,7 @@ QString SingleLine(const QString &text) { } for (auto ch = s; ch != e; ++ch) { - if (chIsNewline(*ch) || *ch == TextCommand) { + if (IsNewline(*ch) || *ch == TextCommand) { result[int(ch - s)] = QChar::Space; } } @@ -1336,7 +1350,7 @@ QString RemoveAccents(const QString &text) { if (copying) result[i] = *ch; continue; } - if (chIsDiac(*ch)) { + if (IsDiac(*ch)) { copying = true; --i; continue; @@ -1434,14 +1448,14 @@ bool CutPart(TextWithEntities &sending, TextWithEntities &left, int32 limit) { if (inEntity && !canBreakEntity) { markGoodAsLevel(0); } else { - if (chIsNewline(*ch)) { + if (IsNewline(*ch)) { if (inEntity) { - if (ch + 1 < end && chIsNewline(*(ch + 1))) { + if (ch + 1 < end && IsNewline(*(ch + 1))) { markGoodAsLevel(12); } else { markGoodAsLevel(11); } - } else if (ch + 1 < end && chIsNewline(*(ch + 1))) { + } else if (ch + 1 < end && IsNewline(*(ch + 1))) { markGoodAsLevel(15); } else if (currentEntity < entityCount && ch + 1 == start + left.entities[currentEntity].offset() && left.entities[currentEntity].type() == EntityType::Pre) { markGoodAsLevel(14); @@ -1450,15 +1464,15 @@ bool CutPart(TextWithEntities &sending, TextWithEntities &left, int32 limit) { } else { markGoodAsLevel(13); } - } else if (chIsSpace(*ch)) { - if (chIsSentenceEnd(*(ch - 1))) { + } else if (IsSpace(*ch)) { + if (IsSentenceEnd(*(ch - 1))) { markGoodAsLevel(9 + noEntityLevel); - } else if (chIsSentencePartEnd(*(ch - 1))) { + } else if (IsSentencePartEnd(*(ch - 1))) { markGoodAsLevel(7 + noEntityLevel); } else { markGoodAsLevel(5 + noEntityLevel); } - } else if (chIsWordSeparator(*(ch - 1))) { + } else if (IsWordSeparator(*(ch - 1))) { markGoodAsLevel(3 + noEntityLevel); } else { markGoodAsLevel(1 + noEntityLevel); @@ -1761,13 +1775,14 @@ void ParseEntities(TextWithEntities &result, int32 flags, bool rich) { const QChar *domainEnd = start + mDomain.capturedEnd(), *p = domainEnd; for (; p < end; ++p) { QChar ch(*p); - if (chIsLinkEnd(ch)) break; // link finished - if (chIsAlmostLinkEnd(ch)) { + if (IsLinkEnd(ch)) { + break; // link finished + } else if (IsAlmostLinkEnd(ch)) { const QChar *endTest = p + 1; - while (endTest < end && chIsAlmostLinkEnd(*endTest)) { + while (endTest < end && IsAlmostLinkEnd(*endTest)) { ++endTest; } - if (endTest >= end || chIsLinkEnd(*endTest)) { + if (endTest >= end || IsLinkEnd(*endTest)) { break; // link finished at p } p = endTest; @@ -1879,7 +1894,7 @@ void ApplyServerCleaning(TextWithEntities &result) { if (ch->unicode() == '\r') { MovePartAndGoForward(result, to, from, (ch - start) - from); ++from; - } else if (chReplacedBySpace(*ch)) { + } else if (IsReplacedBySpace(*ch)) { *ch = ' '; } } @@ -1893,7 +1908,7 @@ void Trim(TextWithEntities &result) { // right trim for (auto s = result.text.data(), e = s + result.text.size(), ch = e; ch != s;) { --ch; - if (!chIsTrimmed(*ch)) { + if (!IsTrimmed(*ch)) { if (ch + 1 < e) { auto l = ch + 1 - s; for (auto &entity : result.entities) { @@ -1916,7 +1931,7 @@ void Trim(TextWithEntities &result) { // left trim for (auto s = result.text.data(), ch = s, e = s + result.text.size(); ch != e; ++ch) { - if (!chIsTrimmed(*ch) || (ch - s) == firstMonospaceOffset) { + if (!IsTrimmed(*ch) || (ch - s) == firstMonospaceOffset) { if (ch > s) { auto l = ch - s; for (auto &entity : result.entities) { diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 5041f83..a5aae43 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -107,12 +107,12 @@ QString GetFullSimpleTextTag(const TextWithTags &textWithTags) { auto from = 0; auto till = int(text.size()); for (; from != till; ++from) { - if (!IsNewline(text[from]) && !chIsSpace(text[from])) { + if (!IsNewline(text[from]) && !Text::IsSpace(text[from])) { break; } } while (till != from) { - if (!IsNewline(text[till - 1]) && !chIsSpace(text[till - 1])) { + if (!IsNewline(text[till - 1]) && !Text::IsSpace(text[till - 1])) { break; } --till; @@ -3129,7 +3129,7 @@ bool InputField::commitMarkdownReplacement( const auto ch = outer.at(check); if (IsNewline(ch)) { return check + 1; - } else if (!chIsSpace(ch)) { + } else if (!Text::IsSpace(ch)) { break; } }