From 02440524eaa2b1bd6d1909ff4aa2ca207a282b2c Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 3 Oct 2023 13:06:56 +0400 Subject: [PATCH] Add per-line geometry / colors customization for Text. --- ui/basic.style | 2 +- ui/text/text.cpp | 301 ++++++++++-------- ui/text/text.h | 77 ++++- ui/text/text_block.cpp | 538 +++++++++++++++++++-------------- ui/text/text_block.h | 237 ++++++++------- ui/text/text_entity.cpp | 6 +- ui/text/text_entity.h | 14 +- ui/text/text_parser.cpp | 225 +++++++------- ui/text/text_parser.h | 19 +- ui/text/text_renderer.cpp | 462 ++++++++++++++-------------- ui/text/text_renderer.h | 26 +- ui/text/text_utilities.cpp | 10 +- ui/text/text_utilities.h | 8 +- ui/toast/toast_widget.cpp | 3 +- ui/widgets/checkbox.cpp | 4 +- ui/widgets/checkbox.h | 2 +- ui/widgets/labels.cpp | 16 +- ui/widgets/labels.h | 4 +- ui/widgets/side_bar_button.cpp | 8 +- 19 files changed, 1087 insertions(+), 875 deletions(-) diff --git a/ui/basic.style b/ui/basic.style index 9bcbd90..6073a2f 100644 --- a/ui/basic.style +++ b/ui/basic.style @@ -16,7 +16,7 @@ TextPalette { selectMonoFg: color; selectSpoilerFg: color; selectOverlay: color; - linkAlwaysActive: int; + linkAlwaysActive: bool; } TextStyle { diff --git a/ui/text/text.cpp b/ui/text/text.cpp index cb0ffab..41addf1 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -15,6 +15,12 @@ #include "base/platform/base_platform_info.h" #include "styles/style_basic.h" +namespace Ui { + +const QString kQEllipsis = u"..."_q; + +} // namespace Ui + namespace Ui::Text { namespace { @@ -125,6 +131,52 @@ not_null DefaultSpoilerCache() { return &data.cache; } +GeometryDescriptor SimpleGeometry( + int availableWidth, + int fontHeight, + int elisionHeight, + int elisionRemoveFromEnd, + bool elisionOneLine, + bool elisionBreakEverywhere) { + constexpr auto wrap = []( + Fn layout, + bool breakEverywhere = false) { + return GeometryDescriptor{ std::move(layout), breakEverywhere }; + }; + + // Try to minimize captured values (to minimize Fn allocations). + if (!elisionOneLine && !elisionHeight) { + return wrap([=](LineGeometry line, uint16 positino) { + line.width = availableWidth; + return line; + }); + } else if (elisionOneLine) { + return wrap([=](LineGeometry line, uint16 position) { + line.elided = true; + line.width = availableWidth - elisionRemoveFromEnd; + return line; + }, elisionBreakEverywhere); + } else if (!elisionRemoveFromEnd) { + return wrap([=](LineGeometry line, uint16 position) { + if (line.top + fontHeight * 2 > elisionHeight) { + line.elided = true; + } + line.width = availableWidth; + return line; + }); + } else { + return wrap([=](LineGeometry line, uint16 position) { + if (line.top + fontHeight * 2 > elisionHeight) { + line.elided = true; + line.width = availableWidth - elisionRemoveFromEnd; + } else { + line.width = availableWidth; + } + return line; + }); + } +}; + String::SpoilerDataWrap::SpoilerDataWrap() noexcept = default; String::SpoilerDataWrap::SpoilerDataWrap(SpoilerDataWrap &&other) noexcept @@ -188,7 +240,9 @@ void String::setText(const style::TextStyle &st, const QString &text, const Text recountNaturalSize(true, options.dir); } -void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { +void String::recountNaturalSize( + bool initial, + Qt::LayoutDirection optionsDirection) { NewlineBlock *lastNewline = 0; _maxWidth = _minHeight = 0; @@ -199,20 +253,23 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { auto b = block.get(); auto _btype = b->type(); auto blockHeight = CountBlockHeight(b, _st); - if (_btype == TextBlockTNewline) { + if (_btype == TextBlockType::Newline) { if (!lineHeight) lineHeight = blockHeight; if (initial) { - Qt::LayoutDirection dir = optionsDir; - if (dir == Qt::LayoutDirectionAuto) { - dir = StringDirection(_text, lastNewlineStart, b->from()); + Qt::LayoutDirection direction = optionsDirection; + if (direction == Qt::LayoutDirectionAuto) { + direction = StringDirection( + _text, + lastNewlineStart, + b->position()); } if (lastNewline) { - lastNewline->_nextDir = dir; + lastNewline->_nextDirection = direction; } else { - _startDir = dir; + _startDirection = direction; } } - lastNewlineStart = b->from(); + lastNewlineStart = b->position(); lastNewline = &block.unsafe(); _minHeight += lineHeight; @@ -244,14 +301,14 @@ void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { continue; } if (initial) { - Qt::LayoutDirection dir = optionsDir; - if (dir == Qt::LayoutDirectionAuto) { - dir = StringDirection(_text, lastNewlineStart, _text.size()); + Qt::LayoutDirection direction = optionsDirection; + if (direction == Qt::LayoutDirectionAuto) { + direction = StringDirection(_text, lastNewlineStart, _text.size()); } if (lastNewline) { - lastNewline->_nextDir = dir; + lastNewline->_nextDirection = direction; } else { - _startDir = dir; + _startDirection = direction; } } if (_width > 0) { @@ -269,7 +326,7 @@ int String::countMaxMonospaceWidth() const { for (auto &block : _blocks) { auto b = block.get(); auto _btype = b->type(); - if (_btype == TextBlockTNewline) { + if (_btype == TextBlockType::Newline) { last_rBearing = b->f_rbearing(); last_rPadding = b->f_rpadding(); @@ -283,8 +340,8 @@ int String::countMaxMonospaceWidth() const { _width = (b->f_width() - last_rBearing); continue; } - if (!(b->flags() & (TextBlockFPre | TextBlockFCode)) - && (b->type() != TextBlockTSkip)) { + if (!(b->flags() & (TextBlockFlag::Pre | TextBlockFlag::Code)) + && (b->type() != TextBlockType::Skip)) { fullMonospace = false; } auto b__f_rbearing = b->f_rbearing(); // cache @@ -347,9 +404,10 @@ void String::setMarkedText(const style::TextStyle &st, const TextWithEntities &t recountNaturalSize(true, options.dir); } -void String::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) { - if (!lnkIndex || lnkIndex > _links.size()) return; - _links[lnkIndex - 1] = lnk; +void String::setLink(uint16 index, const ClickHandlerPtr &link) { + if (index > 0 && index <= _links.size()) { + _links[index - 1] = link; + } } void String::setSpoilerRevealed(bool revealed, anim::type animated) { @@ -394,16 +452,17 @@ bool String::hasSpoilers() const { } bool String::hasSkipBlock() const { - return _blocks.empty() ? false : _blocks.back()->type() == TextBlockTSkip; + return !_blocks.empty() + && (_blocks.back()->type() == TextBlockType::Skip); } bool String::updateSkipBlock(int width, int height) { - if (!_blocks.empty() && _blocks.back()->type() == TextBlockTSkip) { + if (!_blocks.empty() && _blocks.back()->type() == TextBlockType::Skip) { const auto block = static_cast(_blocks.back().get()); - if (block->width() == width && block->height() == height) { + if (block->f_width().toInt() == width && block->height() == height) { return false; } - _text.resize(block->from()); + _text.resize(block->position()); _blocks.pop_back(); } _text.push_back('_'); @@ -420,10 +479,10 @@ bool String::updateSkipBlock(int width, int height) { } bool String::removeSkipBlock() { - if (_blocks.empty() || _blocks.back()->type() != TextBlockTSkip) { + if (_blocks.empty() || _blocks.back()->type() != TextBlockType::Skip) { return false; } - _text.resize(_blocks.back()->from()); + _text.resize(_blocks.back()->position()); _blocks.pop_back(); recountNaturalSize(false); return true; @@ -454,10 +513,21 @@ int String::countHeight(int width, bool breakEverywhere) const { return result; } -void String::countLineWidths(int width, QVector *lineWidths, bool breakEverywhere) const { - enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) { - lineWidths->push_back(lineWidth.ceil().toInt()); +std::vector String::countLineWidths(int width) const { + return countLineWidths(width, {}); +} + +std::vector String::countLineWidths( + int width, + LineWidthsOptions options) const { + auto result = std::vector(); + if (options.reserve) { + result.reserve(options.reserve); + } + enumerateLines(width, options.breakEverywhere, [&](QFixed lineWidth, int lineHeight) { + result.push_back(lineWidth.ceil().toInt()); }); + return result; } template @@ -475,7 +545,7 @@ void String::enumerateLines( auto _btype = b->type(); int blockHeight = CountBlockHeight(b.get(), _st); - if (_btype == TextBlockTNewline) { + if (_btype == TextBlockType::Newline) { if (!lineHeight) lineHeight = blockHeight; callback(width - widthLeft, lineHeight); @@ -500,7 +570,7 @@ void String::enumerateLines( continue; } - if (_btype == TextBlockTText) { + if (_btype == TextBlockType::Text) { 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(); @@ -577,6 +647,13 @@ void String::draw(QPainter &p, const PaintContext &context) const { Renderer(*this).draw(p, context); } +StateResult String::getState( + QPoint point, + GeometryDescriptor geometry, + StateRequest request) const { + return Renderer(*this).getState(point, std::move(geometry), request); +} + void String::draw(Painter &p, int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection, bool fullWidthSelection) const { // p.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug Renderer(*this).draw(p, { @@ -605,14 +682,16 @@ void String::drawElided(Painter &p, int32 left, int32 top, int32 w, int32 lines, .palette = &p.textPalette(), .paused = p.inactive(), .selection = selection, - .elisionLines = lines, + .elisionHeight = (lines > 1) ? (lines * _st->font->height) : 0, .elisionRemoveFromEnd = removeFromEnd, + .elisionOneLine = (lines == 1), }); } void String::drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const { Renderer(*this).draw(p, { .position = { left, top }, + //.outerWidth = outerw, .availableWidth = width, .align = align, .clip = (yTo >= 0 @@ -622,7 +701,6 @@ void String::drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 oute .paused = p.inactive(), .selection = selection, }); - draw(p, style::RightToLeft() ? (outerw - left - width) : left, top, width, align, yFrom, yTo, selection); } void String::drawLeftElided(Painter &p, int32 left, int32 top, int32 width, int32 outerw, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const { @@ -638,7 +716,10 @@ void String::drawRightElided(Painter &p, int32 right, int32 top, int32 width, in } StateResult String::getState(QPoint point, int width, StateRequest request) const { - return Renderer(*this).getState(point, width, request); + return Renderer(*this).getState( + point, + SimpleGeometry(width, _st->font->height, 0, 0, false, false), + request); } StateResult String::getStateLeft(QPoint point, int width, int outerw, StateRequest request) const { @@ -646,7 +727,14 @@ StateResult String::getStateLeft(QPoint point, int width, int outerw, StateReque } StateResult String::getStateElided(QPoint point, int width, StateRequestElided request) const { - return Renderer(*this).getStateElided(point, width, request); + return Renderer(*this).getState(point, SimpleGeometry( + width, + _st->font->height, + (request.lines > 1) ? (request.lines * _st->font->height) : 0, + request.removeFromEnd, + (request.lines == 1), + request.flags & StateRequest::Flag::BreakEverywhere + ), static_cast(request)); } StateResult String::getStateElidedLeft(QPoint point, int width, int outerw, StateRequestElided request) const { @@ -661,7 +749,7 @@ TextSelection String::adjustSelection(TextSelection selection, TextSelectType se // Full selection of monospace entity. for (const auto &b : _blocks) { - if (b->from() < from) { + if (b->position() < from) { continue; } if (!IsMono(b->flags())) { @@ -723,15 +811,15 @@ TextSelection String::adjustSelection(TextSelection selection, TextSelectType se } bool String::isEmpty() const { - return _blocks.empty() || _blocks[0]->type() == TextBlockTSkip; + return _blocks.empty() || _blocks[0]->type() == TextBlockType::Skip; } uint16 String::countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const { - return (i + 1 == e) ? _text.size() : (*(i + 1))->from(); + return (i + 1 == e) ? _text.size() : (*(i + 1))->position(); } uint16 String::countBlockLength(const String::TextBlocks::const_iterator &i, const String::TextBlocks::const_iterator &e) const { - return countBlockEnd(i, e) - (*i)->from(); + return countBlockEnd(i, e) - (*i)->position(); } template < @@ -749,92 +837,67 @@ void String::enumerateText( return; } - int lnkIndex = 0; - uint16 lnkFrom = 0; - - int spoilerIndex = 0; - uint16 spoilerFrom = 0; + int linkIndex = 0; + uint16 linkPosition = 0; int32 flags = 0; for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { - int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); - int blockSpoilerIndex = (i == e) ? 0 : (*i)->spoilerIndex(); - uint16 blockFrom = (i == e) ? _text.size() : (*i)->from(); - int32 blockFlags = (i == e) ? 0 : (*i)->flags(); - - if (IsMono(blockFlags)) { - blockLnkIndex = 0; - } - 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); + const auto blockPosition = (i == e) ? uint16(_text.size()) : (*i)->position(); + const auto blockFlags = (i == e) ? TextBlockFlags() : (*i)->flags(); + const auto blockLinkIndex = [&] { + if (IsMono(blockFlags) || (i == e)) { + return 0; + } + const auto result = (*i)->linkIndex(); + return (result && _links.at(result - 1)) ? result : 0; + }(); + if (blockLinkIndex != linkIndex) { + if (linkIndex) { + auto rangeFrom = qMax(selection.from, linkPosition); + auto rangeTo = qMin(selection.to, blockPosition); if (rangeTo > rangeFrom) { // handle click handler const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom); // Ignore links that are partially copied. - const auto handler = (lnkFrom != rangeFrom || blockFrom != rangeTo) + const auto handler = (linkPosition != rangeFrom || blockPosition != rangeTo) ? nullptr - : _links.at(lnkIndex - 1); + : _links.at(linkIndex - 1); const auto type = handler ? handler->getTextEntity().type : EntityType::Invalid; clickHandlerFinishCallback(r, handler, type); } } - lnkIndex = blockLnkIndex; - if (lnkIndex) { - lnkFrom = blockFrom; - const auto handler = _links.at(lnkIndex - 1); + linkIndex = blockLinkIndex; + if (linkIndex) { + linkPosition = blockPosition; + const auto handler = _links.at(linkIndex - 1); clickHandlerStartCallback(handler ? handler->getTextEntity().type : EntityType::Invalid); } } - if (blockSpoilerIndex != spoilerIndex) { - if (spoilerIndex) { - auto rangeFrom = qMax(selection.from, spoilerFrom); - auto rangeTo = qMin(selection.to, blockFrom); - if (rangeTo > rangeFrom) { // handle click handler - const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom); - // Ignore links that are partially copied. - const auto handler = (spoilerFrom != rangeFrom - || blockFrom != rangeTo - || !_spoiler.data) - ? nullptr - : _spoiler.data->link; - const auto type = EntityType::Spoiler; - clickHandlerFinishCallback(r, handler, type); - } - } - spoilerIndex = blockSpoilerIndex; - if (spoilerIndex) { - spoilerFrom = blockFrom; - clickHandlerStartCallback(EntityType::Spoiler); - } - } - const auto checkBlockFlags = (blockFrom >= selection.from) - && (blockFrom <= selection.to); + const auto checkBlockFlags = (blockPosition >= selection.from) + && (blockPosition <= selection.to); if (checkBlockFlags && blockFlags != flags) { flagsChangeCallback(flags, blockFlags); flags = blockFlags; } - if (i == e || (lnkIndex ? lnkFrom : blockFrom) >= selection.to) { + if (i == e || (linkIndex ? linkPosition : blockPosition) >= selection.to) { break; } const auto blockType = (*i)->type(); - if (blockType == TextBlockTSkip) continue; + if (blockType == TextBlockType::Skip) { + continue; + } - auto rangeFrom = qMax(selection.from, blockFrom); + auto rangeFrom = qMax(selection.from, blockPosition); auto rangeTo = qMin( selection.to, - uint16(blockFrom + countBlockLength(i, e))); + uint16(blockPosition + countBlockLength(i, e))); if (rangeTo > rangeFrom) { - const auto customEmojiData = (blockType == TextBlockTCustomEmoji) + const auto customEmojiData = (blockType == TextBlockType::CustomEmoji) ? static_cast(i->get())->_custom->entityData() : QString(); appendPartCallback( @@ -852,7 +915,7 @@ void String::unloadPersistentAnimation() { if (_hasCustomEmoji) { for (const auto &block : _blocks) { const auto raw = block.get(); - if (raw->type() == TextBlockTCustomEmoji) { + if (raw->type() == TextBlockType::CustomEmoji) { static_cast(raw)->_custom->unload(); } } @@ -871,12 +934,12 @@ OnlyCustomEmoji String::toOnlyCustomEmoji() const { result.lines.emplace_back(); for (const auto &block : _blocks) { const auto raw = block.get(); - if (raw->type() == TextBlockTCustomEmoji) { + if (raw->type() == TextBlockType::CustomEmoji) { const auto custom = static_cast(raw); result.lines.back().push_back({ .entityData = custom->_custom->entityData(), }); - } else if (raw->type() == TextBlockTNewline) { + } else if (raw->type() == TextBlockType::Newline) { result.lines.emplace_back(); } } @@ -925,17 +988,16 @@ TextForMimeData String::toText( result.rich.entities.insert(i, std::move(entity)); }; auto linkStart = 0; - auto spoilerStart = 0; auto markdownTrackers = composeEntities ? std::vector{ - { TextBlockFItalic, EntityType::Italic }, - { TextBlockFBold, EntityType::Bold }, - { TextBlockFSemibold, EntityType::Semibold }, - { TextBlockFUnderline, EntityType::Underline }, - { TextBlockFPlainLink, EntityType::PlainLink }, - { TextBlockFStrikeOut, EntityType::StrikeOut }, - { TextBlockFCode, EntityType::Code }, // #TODO entities - { TextBlockFPre, EntityType::Pre }, + { TextBlockFlag::Italic, EntityType::Italic }, + { TextBlockFlag::Bold, EntityType::Bold }, + { TextBlockFlag::Semibold, EntityType::Semibold }, + { TextBlockFlag::Underline, EntityType::Underline }, + { TextBlockFlag::Spoiler, EntityType::Spoiler }, + { TextBlockFlag::StrikeOut, EntityType::StrikeOut }, + { TextBlockFlag::Code, EntityType::Code }, // #TODO entities + { TextBlockFlag::Pre, EntityType::Pre }, } : std::vector(); const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) { if (!composeEntities) { @@ -954,25 +1016,12 @@ TextForMimeData String::toText( } }; const auto clickHandlerStartCallback = [&](EntityType type) { - if (type == EntityType::Spoiler) { - spoilerStart = result.rich.text.size(); - } else { - linkStart = result.rich.text.size(); - } + linkStart = result.rich.text.size(); }; const auto clickHandlerFinishCallback = [&]( QStringView inText, const ClickHandlerPtr &handler, EntityType type) { - if (type == EntityType::Spoiler) { - insertEntity({ - type, - spoilerStart, - int(result.rich.text.size() - spoilerStart), - QString(), - }); - return; - } if (!handler || (!composeExpanded && !composeEntities)) { return; } @@ -1061,21 +1110,21 @@ IsolatedEmoji String::toIsolatedEmoji() const { } auto result = IsolatedEmoji(); const auto skip = (_blocks.empty() - || _blocks.back()->type() != TextBlockTSkip) ? 0 : 1; + || _blocks.back()->type() != TextBlockType::Skip) ? 0 : 1; if ((_blocks.size() > kIsolatedEmojiLimit + skip) || hasSpoilers()) { return {}; } auto index = 0; for (const auto &block : _blocks) { const auto type = block->type(); - if (block->lnkIndex()) { + if (block->linkIndex()) { return {}; - } else if (type == TextBlockTEmoji) { + } else if (type == TextBlockType::Emoji) { result.items[index++] = block.unsafe()._emoji; - } else if (type == TextBlockTCustomEmoji) { + } else if (type == TextBlockType::CustomEmoji) { result.items[index++] = block.unsafe()._custom->entityData(); - } else if (type != TextBlockTSkip) { + } else if (type != TextBlockType::Skip) { return {}; } } @@ -1092,7 +1141,7 @@ void String::clearFields() { _links.clear(); _spoiler.data = nullptr; _maxWidth = _minHeight = 0; - _startDir = Qt::LayoutDirectionAuto; + _startDirection = Qt::LayoutDirectionAuto; } bool IsBad(QChar ch) { @@ -1186,7 +1235,7 @@ bool IsSpace(QChar ch) { || (ch == QChar(8203)/*Zero width space.*/); } -bool IsDiac(QChar ch) { // diac and variation selectors +bool IsDiacritic(QChar ch) { // diacritic and variation selectors return (ch.category() == QChar::Mark_NonSpacing) || (ch == 1652) || (ch >= 64606 && ch <= 64611); diff --git a/ui/text/text.h b/ui/text/text.h index 809f926..2b32bb8 100644 --- a/ui/text/text.h +++ b/ui/text/text.h @@ -26,11 +26,9 @@ struct TextPalette; } // namespace style namespace Ui { -static const auto kQEllipsis = QStringLiteral("..."); +extern const QString kQEllipsis; } // namespace Ui -static const QChar TextCommand(0x0010); - struct TextParseOptions { int32 flags; int32 maxw; @@ -125,16 +123,42 @@ private: }; +struct SpecialColor { + const QPen *pen = nullptr; + const QPen *penSelected = nullptr; +}; + +struct LineGeometry { + int left = 0; + int top = 0; + int width = 0; + bool elided = false; +}; +struct GeometryDescriptor { + Fn layout; + bool breakEverywhere = false; +}; + [[nodiscard]] not_null DefaultSpoilerCache(); +[[nodiscard]] GeometryDescriptor SimpleGeometry( + int availableWidth, + int fontHeight, + int elisionHeight, + int elisionRemoveFromEnd, + bool elisionOneLine, + bool elisionBreakEverywhere); + struct PaintContext { QPoint position; int outerWidth = 0; // For automatic RTL Ui inversion. int availableWidth = 0; + GeometryDescriptor geometry; // By default is SimpleGeometry. style::align align = style::al_left; QRect clip; const style::TextPalette *palette = nullptr; + std::span colors; SpoilerMessCache *spoiler = nullptr; crl::time now = 0; bool paused = false; @@ -144,8 +168,9 @@ struct PaintContext { TextSelection selection; bool fullWidthSelection = true; - int elisionLines = 0; + int elisionHeight = 0; int elisionRemoveFromEnd = 0; + bool elisionOneLine = false; bool elisionBreakEverywhere = false; }; @@ -167,14 +192,42 @@ public: String &operator=(String &&other); ~String(); - [[nodiscard]] int countWidth(int width, bool breakEverywhere = false) const; - [[nodiscard]] int countHeight(int width, bool breakEverywhere = false) const; - void countLineWidths(int width, QVector *lineWidths, bool breakEverywhere = false) const; + [[nodiscard]] int countWidth( + int width, + bool breakEverywhere = false) const; + [[nodiscard]] int countHeight( + int width, + bool breakEverywhere = false) const; + + struct LineWidthsOptions { + bool breakEverywhere = false; + int reserve = 0; + }; + [[nodiscard]] std::vector countLineWidths(int width) const; + [[nodiscard]] std::vector countLineWidths( + int width, + LineWidthsOptions options) const; + + struct DimensionsResult { + int width = 0; + int height = 0; + std::vector lineWidths; + }; + struct DimensionsRequest { + bool lineWidths = false; + int reserve = 0; + }; + [[nodiscard]] DimensionsResult countDimensions( + GeometryDescriptor geometry) const; + [[nodiscard]] DimensionsResult countDimensions( + GeometryDescriptor geometry, + DimensionsRequest request) const; + void setText(const style::TextStyle &st, const QString &text, const TextParseOptions &options = kDefaultTextOptions); void setMarkedText(const style::TextStyle &st, const TextWithEntities &textWithEntities, const TextParseOptions &options = kMarkupTextOptions, const std::any &context = {}); [[nodiscard]] bool hasLinks() const; - void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk); + void setLink(uint16 index, const ClickHandlerPtr &lnk); [[nodiscard]] bool hasSpoilers() const; void setSpoilerRevealed(bool revealed, anim::type animated); @@ -193,6 +246,10 @@ public: [[nodiscard]] int countMaxMonospaceWidth() const; void draw(QPainter &p, const PaintContext &context) const; + [[nodiscard]] StateResult getState( + QPoint point, + GeometryDescriptor geometry, + StateRequest request = StateRequest()) const; void draw(Painter &p, int32 left, int32 top, int32 width, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) const; void drawElided(Painter &p, int32 left, int32 top, int32 width, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false, TextSelection selection = { 0, 0 }) const; @@ -298,7 +355,7 @@ private: TextBlocks _blocks; TextLinks _links; - Qt::LayoutDirection _startDir = Qt::LayoutDirectionAuto; + Qt::LayoutDirection _startDirection = Qt::LayoutDirectionAuto; SpoilerDataWrap _spoiler; @@ -313,7 +370,7 @@ private: [[nodiscard]] bool IsLinkEnd(QChar ch); [[nodiscard]] bool IsNewline(QChar ch); [[nodiscard]] bool IsSpace(QChar ch); -[[nodiscard]] bool IsDiac(QChar ch); +[[nodiscard]] bool IsDiacritic(QChar ch); [[nodiscard]] bool IsReplacedBySpace(QChar ch); [[nodiscard]] bool IsTrimmed(QChar ch); diff --git a/ui/text/text_block.cpp b/ui/text/text_block.cpp index 54ccc74..f7ad545 100644 --- a/ui/text/text_block.cpp +++ b/ui/text/text_block.cpp @@ -47,7 +47,8 @@ struct LineBreakHelper { 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. + // So we don't need caching / optimizations referred to + // delayed right bearing calculations. //static const QFixed RightBearingNotCalculated; @@ -156,15 +157,6 @@ void addNextCluster( } while (glyphPosition < current.num_glyphs && !glyphs.attributes[glyphPosition].clusterStart); - if (!((pos == end && glyphPosition == current.num_glyphs) || logClusters[pos] == glyphPosition)) { - auto str = QStringList(); - for (auto i = 0; i < pos; ++i) { - str.append(QString::number(logClusters[i])); - } - LOG(("text: %1 (from: %2, length: %3) part: %4").arg(DebugCurrentParsingString).arg(DebugCurrentParsingFrom).arg(DebugCurrentParsingLength).arg(DebugCurrentParsingPart)); - LOG(("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); @@ -176,32 +168,36 @@ void addNextCluster( class BlockParser { public: BlockParser( - QTextEngine *e, - TextBlock *b, + TextBlock &block, + QTextEngine &engine, QFixed minResizeWidth, - int blockFrom, - const QString &str); + int blockPosition, + const QString &text); private: void parseWords(QFixed minResizeWidth, int blockFrom); - bool isLineBreak(const QCharAttributes *attributes, int index); - bool isSpaceBreak(const QCharAttributes *attributes, int index); + [[nodiscard]] bool isLineBreak( + const QCharAttributes *attributes, + int index) const; + [[nodiscard]] bool isSpaceBreak( + const QCharAttributes *attributes, + int index) const; - TextBlock *block; - QTextEngine *eng; - const QString &str; + TextBlock █ + QTextEngine &engine; + const QString &text; }; BlockParser::BlockParser( - QTextEngine *e, - TextBlock *b, + TextBlock &block, + QTextEngine &engine, QFixed minResizeWidth, int blockFrom, - const QString &str) -: block(b) -, eng(e) -, str(str) { + const QString &text) +: block(block) +, engine(engine) +, text(text) { parseWords(minResizeWidth, blockFrom); } @@ -221,15 +217,15 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { // LOG(("Text: %1, chars: %2").arg(str).arg(debugChars)); int item = -1; - int newItem = eng->findItem(0); + int newItem = engine.findItem(0); - const QCharAttributes *attributes = eng->attributes(); + const QCharAttributes *attributes = engine.attributes(); if (!attributes) return; int end = 0; - lbh.logClusters = eng->layoutData->logClustersPtr; + lbh.logClusters = engine.layoutData->logClustersPtr; - block->_words.clear(); + block._words.clear(); int wordStart = lbh.currentPosition; @@ -237,26 +233,26 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { int lastGraphemeBoundaryPosition = -1; ScriptLine lastGraphemeBoundaryLine; - while (newItem < eng->layoutData->items.size()) { + while (newItem < engine.layoutData->items.size()) { if (newItem != item) { item = newItem; - const QScriptItem ¤t = eng->layoutData->items[item]; + const QScriptItem ¤t = engine.layoutData->items[item]; if (!current.num_glyphs) { - eng->shape(item); - attributes = eng->attributes(); + engine.shape(item); + attributes = engine.attributes(); if (!attributes) return; - lbh.logClusters = eng->layoutData->logClustersPtr; + lbh.logClusters = engine.layoutData->logClustersPtr; } lbh.currentPosition = current.position; - end = current.position + eng->length(item); - lbh.glyphs = eng->shapedGlyphs(¤t); - QFontEngine *fontEngine = eng->fontEngine(current); + end = current.position + engine.length(item); + lbh.glyphs = engine.shapedGlyphs(¤t); + QFontEngine *fontEngine = engine.fontEngine(current); if (lbh.fontEngine != fontEngine) { lbh.fontEngine = fontEngine; } } - const QScriptItem ¤t = eng->layoutData->items[item]; + const QScriptItem ¤t = engine.layoutData->items[item]; const auto atSpaceBreak = [&] { for (auto index = lbh.currentPosition; index < end; ++index) { @@ -269,15 +265,25 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { return false; }(); if (atSpaceBreak) { - while (lbh.currentPosition < end && attributes[lbh.currentPosition].whiteSpace) - addNextCluster(lbh.currentPosition, end, lbh.spaceData, lbh.glyphCount, - current, lbh.logClusters, lbh.glyphs); + 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())); + 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; + block._words.back().add_rpadding(lbh.spaceData.textWidth); + block._width += lbh.spaceData.textWidth; lbh.spaceData.length = 0; lbh.spaceData.textWidth = 0; @@ -288,15 +294,24 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { lastGraphemeBoundaryLine = ScriptLine(); } else { do { - addNextCluster(lbh.currentPosition, end, lbh.tmpData, lbh.glyphCount, - current, lbh.logClusters, lbh.glyphs); + addNextCluster( + lbh.currentPosition, + end, + lbh.tmpData, + lbh.glyphCount, + current, + lbh.logClusters, + lbh.glyphs); - if (lbh.currentPosition >= eng->layoutData->string.length() + if (lbh.currentPosition >= engine.layoutData->string.length() || isSpaceBreak(attributes, lbh.currentPosition) || isLineBreak(attributes, lbh.currentPosition)) { lbh.calculateRightBearing(); - block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing())); - block->_width += lbh.tmpData.textWidth; + 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; @@ -305,8 +320,11 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { 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; + 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; @@ -315,8 +333,11 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { } if (addingEachGrapheme) { lbh.calculateRightBearing(); - block->_words.push_back(TextWord(wordStart + blockFrom, -lbh.tmpData.textWidth, -lbh.negativeRightBearing())); - block->_width += lbh.tmpData.textWidth; + 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; @@ -331,54 +352,87 @@ void BlockParser::parseWords(QFixed minResizeWidth, int blockFrom) { 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(); + 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) { + int index) const { // Don't break after / in links. return attributes[index].lineBreak - && (block->lnkIndex() <= 0 - || index <= 0 - || str[index - 1] != '/'); + && (block.linkIndex() <= 0 || index <= 0 || text[index - 1] != '/'); } bool BlockParser::isSpaceBreak( const QCharAttributes *attributes, - int index) { + int index) const { // Don't break on   - return attributes[index].whiteSpace - && (str[index] != QChar::Nbsp); + return attributes[index].whiteSpace && (text[index] != QChar::Nbsp); +} + +style::font WithFlags( + const style::font &font, + TextBlockFlags flags, + uint32 fontFlags) { + using namespace style::internal; + + if (!flags && !fontFlags) { + return font; + } else if (IsMono(flags) || (fontFlags & FontMonospace)) { + return font->monospace(); + } + auto result = font; + if ((flags & TextBlockFlag::Bold) || (fontFlags & FontBold)) { + result = result->bold(); + } else if ((flags & TextBlockFlag::Semibold) + || (fontFlags & FontSemibold)) { + result = result->semibold(); + } + if ((flags & TextBlockFlag::Italic) || (fontFlags & FontItalic)) { + result = result->italic(); + } + if ((flags & TextBlockFlag::Underline) || (fontFlags & FontUnderline)) { + result = result->underline(); + } + if ((flags & TextBlockFlag::StrikeOut) || (fontFlags & FontStrikeOut)) { + result = result->strikeout(); + } + if (flags & TextBlockFlag::Tilde) { // Tilde fix in OpenSans. + result = result->semibold(); + } + return result; } AbstractBlock::AbstractBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, + TextBlockType type, uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex) -: _flags((flags & 0b1111111111) | ((lnkIndex & 0xFFFF) << 14)) -, _from(from) -, _spoilerIndex(spoilerIndex) { + uint16 linkIndex, + uint16 colorIndex) +: _position(position) +, _type(static_cast(type)) +, _flags(flags) +, _linkIndex(linkIndex) +, _colorIndex(colorIndex) { } -uint16 AbstractBlock::from() const { - return _from; +uint16 AbstractBlock::position() const { + return _position; } -int AbstractBlock::width() const { - return _width.toInt(); +TextBlockType AbstractBlock::type() const { + return static_cast(_type); } -int AbstractBlock::rpadding() const { - return _rpadding.toInt(); +TextBlockFlags AbstractBlock::flags() const { + return TextBlockFlags::from_raw(_flags); } QFixed AbstractBlock::f_width() const { @@ -389,77 +443,54 @@ QFixed AbstractBlock::f_rpadding() const { return _rpadding; } -uint16 AbstractBlock::lnkIndex() const { - return (_flags >> 14) & 0xFFFF; +uint16 AbstractBlock::linkIndex() const { + return _linkIndex; } -void AbstractBlock::setLnkIndex(uint16 lnkIndex) { - _flags = (_flags & ~(0xFFFF << 14)) | (lnkIndex << 14); +uint16 AbstractBlock::colorIndex() const { + return _colorIndex; } -uint16 AbstractBlock::spoilerIndex() const { - return _spoilerIndex; -} - -void AbstractBlock::setSpoilerIndex(uint16 spoilerIndex) { - _spoilerIndex = spoilerIndex; -} - -TextBlockType AbstractBlock::type() const { - return TextBlockType((_flags >> 10) & 0x0F); -} - -int32 AbstractBlock::flags() const { - return (_flags & 0b1111111111); +void AbstractBlock::setLinkIndex(uint16 index) { + _linkIndex = index; } QFixed AbstractBlock::f_rbearing() const { - return (type() == TextBlockTText) + return (type() == TextBlockType::Text) ? static_cast(this)->real_f_rbearing() : 0; } TextBlock::TextBlock( const style::font &font, - const QString &str, - QFixed minResizeWidth, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex) -: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) { - _flags |= ((TextBlockTText & 0x0F) << 10); - if (length) { - style::font blockFont = font; - if (!flags && lnkIndex) { - // should use TextStyle lnkFlags somehow... not supported - } - - if ((flags & TextBlockFPre) || (flags & TextBlockFCode)) { - blockFont = blockFont->monospace(); - } else { - if (flags & TextBlockFBold) { - blockFont = blockFont->bold(); - } else if (flags & TextBlockFSemibold) { - blockFont = blockFont->semibold(); - } - if (flags & TextBlockFItalic) blockFont = blockFont->italic(); - if (flags & TextBlockFUnderline) blockFont = blockFont->underline(); - if (flags & TextBlockFStrikeOut) blockFont = blockFont->strikeout(); - if (flags & TextBlockFTilde) { // tilde fix in OpenSans - blockFont = blockFont->semibold(); - } - } - - DebugCurrentParsingString = str; - DebugCurrentParsingFrom = _from; - DebugCurrentParsingLength = length; - const auto part = DebugCurrentParsingPart = str.mid(_from, length); - - QStackTextEngine engine(part, blockFont->f); - BlockParser parser(&engine, this, minResizeWidth, _from, part); + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, + QFixed minResizeWidth) +: AbstractBlock( + font, + text, + position, + length, + TextBlockType::Text, + flags, + linkIndex, + colorIndex) { + if (!length) { + return; } + const auto blockFont = WithFlags(font, flags); + if (!flags && linkIndex) { + // should use TextStyle lnkFlags somehow... not supported + } + + const auto part = text.mid(_position, length); + + QStackTextEngine engine(part, blockFont->f); + BlockParser parser(*this, engine, minResizeWidth, _position, part); } QFixed TextBlock::real_f_rbearing() const { @@ -468,20 +499,27 @@ QFixed TextBlock::real_f_rbearing() const { EmojiBlock::EmojiBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, EmojiPtr emoji) -: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) +: AbstractBlock( + font, + text, + position, + length, + TextBlockType::Emoji, + flags, + linkIndex, + colorIndex) , _emoji(emoji) { - _flags |= ((TextBlockTEmoji & 0x0F) << 10); _width = int(st::emojiSize + 2 * st::emojiPadding); _rpadding = 0; for (auto i = length; i != 0;) { - auto ch = str[_from + (--i)]; + auto ch = text[_position + (--i)]; if (ch.unicode() == QChar::Space) { _rpadding += font->spacew; } else { @@ -492,20 +530,27 @@ EmojiBlock::EmojiBlock( CustomEmojiBlock::CustomEmojiBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, std::unique_ptr custom) -: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) +: AbstractBlock( + font, + text, + position, + length, + TextBlockType::CustomEmoji, + flags, + linkIndex, + colorIndex) , _custom(std::move(custom)) { - _flags |= ((TextBlockTCustomEmoji & 0x0F) << 10); _width = int(st::emojiSize + 2 * st::emojiPadding); _rpadding = 0; for (auto i = length; i != 0;) { - auto ch = str[_from + (--i)]; + auto ch = text[_position + (--i)]; if (ch.unicode() == QChar::Space) { _rpadding += font->spacew; } else { @@ -516,32 +561,46 @@ CustomEmojiBlock::CustomEmojiBlock( NewlineBlock::NewlineBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex) -: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) { - _flags |= ((TextBlockTNewline & 0x0F) << 10); + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex) +: AbstractBlock( + font, + text, + position, + length, + TextBlockType::Newline, + flags, + linkIndex, + colorIndex) { } Qt::LayoutDirection NewlineBlock::nextDirection() const { - return _nextDir; + return _nextDirection; } SkipBlock::SkipBlock( const style::font &font, - const QString &str, - uint16 from, - int32 w, - int32 h, - uint16 lnkIndex, - uint16 spoilerIndex) -: AbstractBlock(font, str, from, 1, 0, lnkIndex, spoilerIndex) -, _height(h) { - _flags |= ((TextBlockTSkip & 0x0F) << 10); - _width = w; + const QString &text, + uint16 position, + int32 width, + int32 height, + uint16 linkIndex, + uint16 colorIndex) +: AbstractBlock( + font, + text, + position, + 1, + TextBlockType::Skip, + 0, + linkIndex, + colorIndex) +, _height(height) { + _width = width; } int SkipBlock::height() const { @@ -550,11 +609,11 @@ int SkipBlock::height() const { TextWord::TextWord( - uint16 from, + uint16 position, QFixed width, QFixed rbearing, QFixed rpadding) -: _from(from) +: _position(position) , _rbearing((rbearing.value() > 0x7FFF) ? 0x7FFF : (rbearing.value() < -0x7FFF ? -0x7FFF : rbearing.value())) @@ -562,8 +621,8 @@ TextWord::TextWord( , _rpadding(rpadding) { } -uint16 TextWord::from() const { - return _from; +uint16 TextWord::position() const { + return _position; } QFixed TextWord::f_rbearing() const { @@ -588,19 +647,20 @@ Block::Block() { Block::Block(Block &&other) { switch (other->type()) { - case TextBlockTNewline: + case TextBlockType::Newline: emplace(std::move(other.unsafe())); break; - case TextBlockTText: + case TextBlockType::Text: emplace(std::move(other.unsafe())); break; - case TextBlockTEmoji: + case TextBlockType::Emoji: emplace(std::move(other.unsafe())); break; - case TextBlockTCustomEmoji: - emplace(std::move(other.unsafe())); + case TextBlockType::CustomEmoji: + emplace( + std::move(other.unsafe())); break; - case TextBlockTSkip: + case TextBlockType::Skip: emplace(std::move(other.unsafe())); break; default: @@ -614,19 +674,20 @@ Block &Block::operator=(Block &&other) { } destroy(); switch (other->type()) { - case TextBlockTNewline: + case TextBlockType::Newline: emplace(std::move(other.unsafe())); break; - case TextBlockTText: + case TextBlockType::Text: emplace(std::move(other.unsafe())); break; - case TextBlockTEmoji: + case TextBlockType::Emoji: emplace(std::move(other.unsafe())); break; - case TextBlockTCustomEmoji: - emplace(std::move(other.unsafe())); + case TextBlockType::CustomEmoji: + emplace( + std::move(other.unsafe())); break; - case TextBlockTSkip: + case TextBlockType::Skip: emplace(std::move(other.unsafe())); break; default: @@ -641,91 +702,98 @@ Block::~Block() { Block Block::Newline( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex) { + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex) { return New( font, - str, - from, + text, + position, length, flags, - lnkIndex, - spoilerIndex); + linkIndex, + colorIndex); } Block Block::Text( const style::font &font, - const QString &str, - QFixed minResizeWidth, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex) { + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, + QFixed minResizeWidth) { return New( font, - str, - minResizeWidth, - from, + text, + position, length, flags, - lnkIndex, - spoilerIndex); + linkIndex, + colorIndex, + minResizeWidth); } Block Block::Emoji( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, EmojiPtr emoji) { return New( font, - str, - from, + text, + position, length, flags, - lnkIndex, - spoilerIndex, + linkIndex, + colorIndex, emoji); } Block Block::CustomEmoji( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, std::unique_ptr custom) { return New( font, - str, - from, + text, + position, length, flags, - lnkIndex, - spoilerIndex, + linkIndex, + colorIndex, std::move(custom)); } Block Block::Skip( const style::font &font, - const QString &str, - uint16 from, - int32 w, - int32 h, - uint16 lnkIndex, - uint16 spoilerIndex) { - return New(font, str, from, w, h, lnkIndex, spoilerIndex); + const QString &text, + uint16 position, + int32 width, + int32 height, + uint16 linkIndex, + uint16 colorIndex) { + return New( + font, + text, + position, + width, + height, + linkIndex, + colorIndex); } AbstractBlock *Block::get() { @@ -754,19 +822,19 @@ const AbstractBlock &Block::operator*() const { void Block::destroy() { switch (get()->type()) { - case TextBlockTNewline: + case TextBlockType::Newline: unsafe().~NewlineBlock(); break; - case TextBlockTText: + case TextBlockType::Text: unsafe().~TextBlock(); break; - case TextBlockTEmoji: + case TextBlockType::Emoji: unsafe().~EmojiBlock(); break; - case TextBlockTCustomEmoji: + case TextBlockType::CustomEmoji: unsafe().~CustomEmojiBlock(); break; - case TextBlockTSkip: + case TextBlockType::Skip: unsafe().~SkipBlock(); break; default: @@ -777,7 +845,7 @@ void Block::destroy() { int CountBlockHeight( const AbstractBlock *block, const style::TextStyle *st) { - return (block->type() == TextBlockTSkip) + return (block->type() == TextBlockType::Skip) ? static_cast(block)->height() : (st->lineHeight > st->font->height) ? st->lineHeight diff --git a/ui/text/text_block.h b/ui/text/text_block.h index b5a140a..dfb7ba5 100644 --- a/ui/text/text_block.h +++ b/ui/text/text_block.h @@ -6,6 +6,7 @@ // #pragma once +#include "base/flags.h" #include "ui/text/text_custom_emoji.h" #include "ui/style/style_core.h" #include "ui/emoji_config.h" @@ -20,59 +21,64 @@ struct TextStyle; namespace Ui::Text { -enum TextBlockType { - TextBlockTNewline = 0x01, - TextBlockTText = 0x02, - TextBlockTEmoji = 0x03, - TextBlockTCustomEmoji = 0x04, - TextBlockTSkip = 0x05, +enum class TextBlockType : uint16 { + Newline = 0x01, + Text = 0x02, + Emoji = 0x03, + CustomEmoji = 0x04, + Skip = 0x05, }; -enum TextBlockFlags { - TextBlockFBold = 0x01, - TextBlockFItalic = 0x02, - TextBlockFUnderline = 0x04, - TextBlockFStrikeOut = 0x08, - TextBlockFTilde = 0x10, // tilde fix in OpenSans - TextBlockFSemibold = 0x20, - TextBlockFCode = 0x40, - TextBlockFPre = 0x80, - TextBlockFPlainLink = 0x100, +enum class TextBlockFlag : uint16 { + Bold = 0x001, + Italic = 0x002, + Underline = 0x004, + StrikeOut = 0x008, + Tilde = 0x010, // Tilde fix in OpenSans. + Semibold = 0x020, + Code = 0x040, + Pre = 0x080, + Spoiler = 0x100, }; +inline constexpr bool is_flag_type(TextBlockFlag) { return true; } +using TextBlockFlags = base::flags; + +[[nodiscard]] style::font WithFlags( + const style::font &font, + TextBlockFlags flags, + uint32 fontFlags = 0); class AbstractBlock { public: - uint16 from() const; - int width() const; - int rpadding() const; - QFixed f_width() const; - QFixed f_rpadding() const; + [[nodiscard]] uint16 position() const; + [[nodiscard]] TextBlockType type() const; + [[nodiscard]] TextBlockFlags flags() const; + [[nodiscard]] uint16 colorIndex() const; + [[nodiscard]] uint16 linkIndex() const; + void setLinkIndex(uint16 index); + + [[nodiscard]] QFixed f_width() const; + [[nodiscard]] QFixed f_rpadding() const; // Should be virtual, but optimized throught type() call. - QFixed f_rbearing() const; - - uint16 lnkIndex() const; - void setLnkIndex(uint16 lnkIndex); - - uint16 spoilerIndex() const; - void setSpoilerIndex(uint16 spoilerIndex); - - TextBlockType type() const; - int32 flags() const; + [[nodiscard]] QFixed f_rbearing() const; protected: AbstractBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, + TextBlockType type, uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex); + uint16 linkIndex, + uint16 colorIndex); - uint32 _flags = 0; // 2 bits empty, 16 bits lnkIndex, 4 bits type, 10 bits flags - uint16 _from = 0; - uint16 _spoilerIndex = 0; + uint16 _position = 0; + uint16 _type : 4 = 0; + uint16 _flags : 12 = 0; + uint16 _linkIndex = 0; + uint16 _colorIndex = 0; QFixed _width = 0; @@ -90,16 +96,16 @@ public: NewlineBlock( const style::font &font, const QString &str, - uint16 from, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex); + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex); - Qt::LayoutDirection nextDirection() const; + [[nodiscard]] Qt::LayoutDirection nextDirection() const; private: - Qt::LayoutDirection _nextDir = Qt::LayoutDirectionAuto; + Qt::LayoutDirection _nextDirection = Qt::LayoutDirectionAuto; friend class String; friend class Parser; @@ -110,17 +116,24 @@ private: class TextWord final { public: TextWord() = default; - TextWord(uint16 from, QFixed width, QFixed rbearing, QFixed rpadding = 0); - uint16 from() const; - QFixed f_rbearing() const; - QFixed f_width() const; - QFixed f_rpadding() const; + TextWord( + uint16 position, + QFixed width, + QFixed rbearing, + QFixed rpadding = 0); + + [[nodiscard]] uint16 position() const; + [[nodiscard]] QFixed f_rbearing() const; + [[nodiscard]] QFixed f_width() const; + [[nodiscard]] QFixed f_rpadding() const; + void add_rpadding(QFixed padding); private: - uint16 _from = 0; + uint16 _position = 0; int16 _rbearing = 0; - QFixed _width, _rpadding; + QFixed _width; + QFixed _rpadding; }; @@ -128,16 +141,16 @@ class TextBlock final : public AbstractBlock { public: TextBlock( const style::font &font, - const QString &str, - QFixed minResizeWidth, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex); + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, + QFixed minResizeWidth); private: - QFixed real_f_rbearing() const; + [[nodiscard]] QFixed real_f_rbearing() const; QVector _words; @@ -153,12 +166,12 @@ class EmojiBlock final : public AbstractBlock { public: EmojiBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, EmojiPtr emoji); private: @@ -174,12 +187,12 @@ class CustomEmojiBlock final : public AbstractBlock { public: CustomEmojiBlock( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, std::unique_ptr custom); private: @@ -195,14 +208,14 @@ class SkipBlock final : public AbstractBlock { public: SkipBlock( const style::font &font, - const QString &str, - uint16 from, - int32 w, - int32 h, - uint16 lnkIndex, - uint16 spoilerIndex); + const QString &text, + uint16 position, + int32 width, + int32 height, + uint16 linkIndex, + uint16 colorIndex); - int height() const; + [[nodiscard]] int height() const; private: int _height = 0; @@ -221,52 +234,52 @@ public: ~Block(); [[nodiscard]] static Block Newline( - const style::font &font, - const QString &str, - uint16 from, - uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex); + const style::font &font, + const QString &text, + uint16 position, + uint16 length, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex); [[nodiscard]] static Block Text( - const style::font &font, - const QString &str, - QFixed minResizeWidth, - uint16 from, - uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex); + const style::font &font, + const QString &text, + uint16 position, + uint16 length, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, + QFixed minResizeWidth); [[nodiscard]] static Block Emoji( - const style::font &font, - const QString &str, - uint16 from, - uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, - EmojiPtr emoji); + const style::font &font, + const QString &text, + uint16 position, + uint16 length, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, + EmojiPtr emoji); [[nodiscard]] static Block CustomEmoji( const style::font &font, - const QString &str, - uint16 from, + const QString &text, + uint16 position, uint16 length, - uint16 flags, - uint16 lnkIndex, - uint16 spoilerIndex, + TextBlockFlags flags, + uint16 linkIndex, + uint16 colorIndex, std::unique_ptr custom); [[nodiscard]] static Block Skip( - const style::font &font, - const QString &str, - uint16 from, - int32 w, - int32 h, - uint16 lnkIndex, - uint16 spoilerIndex); + const style::font &font, + const QString &text, + uint16 position, + int32 width, + int32 height, + uint16 linkIndex, + uint16 colorIndex); template [[nodiscard]] FinalBlock &unsafe() { @@ -325,8 +338,8 @@ private: const AbstractBlock *block, const style::TextStyle *st); -[[nodiscard]] inline bool IsMono(int32 flags) { - return (flags & TextBlockFPre) || (flags & TextBlockFCode); +[[nodiscard]] inline bool IsMono(TextBlockFlags flags) { + return (flags & TextBlockFlag::Pre) || (flags & TextBlockFlag::Code); } } // namespace Ui::Text diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index 3945774..05633ec 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -1358,7 +1358,7 @@ QString RemoveAccents(const QString &text) { if (copying) result[i] = *ch; continue; } - if (IsDiac(*ch)) { + if (IsDiacritic(*ch)) { copying = true; --i; continue; @@ -2047,9 +2047,9 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) { auto offset = 0; auto state = State(); - auto notClosedEntities = QVector(); // Stack of indices. + auto notClosedEntities = std::vector(); // Stack of indices. const auto closeOne = [&] { - Expects(!notClosedEntities.isEmpty()); + Expects(!notClosedEntities.empty()); auto &entity = result[notClosedEntities.back()]; entity = { diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 07e281c..336d136 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -26,7 +26,7 @@ enum class EntityType : uchar { CustomEmoji, BotCommand, MediaTimestamp, - PlainLink, // Senders in chat list, attachements in chat list, etc. + Colorized, // Senders in chat list, attachments in chat list, etc. Bold, Semibold, @@ -61,16 +61,16 @@ public: int length, const QString &data = QString()); - EntityType type() const { + [[nodiscard]] EntityType type() const { return _type; } - int offset() const { + [[nodiscard]] int offset() const { return _offset; } - int length() const { + [[nodiscard]] int length() const { return _length; } - QString data() const { + [[nodiscard]] QString data() const { return _data; } @@ -103,7 +103,7 @@ public: } } - static int FirstMonospaceOffset( + [[nodiscard]] static int FirstMonospaceOffset( const EntitiesInText &entities, int textLength); @@ -253,7 +253,7 @@ enum { TextParseHashtags = 0x008, TextParseBotCommands = 0x010, TextParseMarkdown = 0x020, - TextParsePlainLinks = 0x040, + TextParseColorized = 0x040, }; struct TextWithTags { diff --git a/ui/text/text_parser.cpp b/ui/text/text_parser.cpp index 41c5da8..8eb925a 100644 --- a/ui/text/text_parser.cpp +++ b/ui/text/text_parser.cpp @@ -26,8 +26,8 @@ constexpr auto kMaxDiacAfterSymbol = 2; auto result = text; const auto &preparsed = text.entities; const bool parseLinks = (options.flags & TextParseLinks); - const bool parsePlainLinks = (options.flags & TextParsePlainLinks); - if (!preparsed.isEmpty() && (parseLinks || parsePlainLinks)) { + const bool parseColorized = (options.flags & TextParseColorized); + if (!preparsed.isEmpty() && (parseLinks || parseColorized)) { bool parseMentions = (options.flags & TextParseMentions); bool parseHashtags = (options.flags & TextParseHashtags); bool parseBotCommands = (options.flags & TextParseBotCommands); @@ -41,9 +41,6 @@ constexpr auto kMaxDiacAfterSymbol = 2; if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) || (type == EntityType::Hashtag && !parseHashtags) || (type == EntityType::Cashtag && !parseHashtags) || - (type == EntityType::PlainLink - && !parsePlainLinks - && !parseMarkdown) || (!parseLinks && (type == EntityType::Url || type == EntityType::CustomUrl)) || @@ -53,6 +50,8 @@ constexpr auto kMaxDiacAfterSymbol = 2; || type == EntityType::Italic || type == EntityType::Underline || type == EntityType::StrikeOut + || type == EntityType::Colorized + || type == EntityType::Spoiler || type == EntityType::Code || type == EntityType::Pre))) { continue; @@ -72,7 +71,7 @@ constexpr auto kMaxDiacAfterSymbol = 2; : QFIXED_MAX; } -// Open Sans tilde fix. +// Tilde fix in OpenSans. [[nodiscard]] bool ComputeCheckTilde(const style::TextStyle &st) { const auto &font = st.font; return (font->size() * style::DevicePixelRatio() == 13) @@ -83,7 +82,7 @@ constexpr auto kMaxDiacAfterSymbol = 2; } // namespace Parser::StartedEntity::StartedEntity(TextBlockFlags flags) -: _value(flags) +: _value(flags.value()) , _type(Type::Flags) { Expects(_value >= 0 && _value < int(kStringLinkIndexShift)); } @@ -102,12 +101,12 @@ Parser::StartedEntity::Type Parser::StartedEntity::type() const { std::optional Parser::StartedEntity::flags() const { if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) { - return TextBlockFlags(_value); + return TextBlockFlags::from_raw(uint16(_value)); } return std::nullopt; } -std::optional Parser::StartedEntity::lnkIndex() const { +std::optional Parser::StartedEntity::linkIndex() const { if ((_value < int(kStringLinkIndexShift) && (_type == Type::IndexedLink)) || (_value >= int(kStringLinkIndexShift) && (_type == Type::Link))) { return uint16(_value); @@ -115,8 +114,8 @@ std::optional Parser::StartedEntity::lnkIndex() const { return std::nullopt; } -std::optional Parser::StartedEntity::spoilerIndex() const { - if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) { +std::optional Parser::StartedEntity::colorIndex() const { + if (_type == Type::Colorized) { return uint16(_value); } return std::nullopt; @@ -163,47 +162,61 @@ void Parser::blockCreated() { } void Parser::createBlock(int32 skipBack) { - if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) { - _maxLnkIndex = _lnkIndex; + if (_linkIndex < kStringLinkIndexShift && _linkIndex > _maxLinkIndex) { + _maxLinkIndex = _linkIndex; } - if (_lnkIndex > kStringLinkIndexShift) { - _maxShiftedLnkIndex = std::max( - uint16(_lnkIndex - kStringLinkIndexShift), - _maxShiftedLnkIndex); + if (_linkIndex > kStringLinkIndexShift) { + _maxShiftedLinkIndex = std::max( + uint16(_linkIndex - kStringLinkIndexShift), + _maxShiftedLinkIndex); } - 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); - } - } - const auto lnkIndex = _monoIndex ? _monoIndex : _lnkIndex; - auto custom = _customEmojiData.isEmpty() - ? nullptr - : Integration::Instance().createCustomEmoji( - _customEmojiData, - _context); - if (custom) { - _t->_blocks.push_back(Block::CustomEmoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, std::move(custom))); - } else if (_emoji) { - _t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, _emoji)); - } else if (newline) { - _t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex)); - } else { - _t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, lnkIndex, _spoilerIndex)); - } - // Diacritic can't attach from the next block to this one. - _allowDiacritic = false; - _blockStart += len; - _customEmojiData = QByteArray(); - _emoji = nullptr; - blockCreated(); + const auto length = int32(_t->_text.size()) + skipBack - _blockStart; + if (length <= 0) { + return; } + const auto newline = !_emoji + && (length == 1) + && (_t->_text.at(_blockStart) == QChar::LineFeed); + if (_newlineAwaited) { + _newlineAwaited = false; + if (!newline) { + _t->_text.insert(_blockStart, QChar::LineFeed); + createBlock(skipBack - length); + } + } + const auto linkIndex = _monoIndex ? _monoIndex : _linkIndex; + auto custom = _customEmojiData.isEmpty() + ? nullptr + : Integration::Instance().createCustomEmoji( + _customEmojiData, + _context); + const auto push = [&](auto &&factory, auto &&...args) { + _t->_blocks.push_back(factory( + _t->_st->font, + _t->_text, + _blockStart, + length, + _flags, + linkIndex, + _colorIndex, + std::forward(args)...)); + }; + if (custom) { + push(&Block::CustomEmoji, std::move(custom)); + } else if (_emoji) { + push(&Block::Emoji, _emoji); + } else if (newline) { + push(&Block::Newline); + } else { + push(&Block::Text, _t->_minResizeWidth); + } + // Diacritic can't attach from the next block to this one. + _allowDiacritic = false; + _blockStart += length; + _customEmojiData = QByteArray(); + _emoji = nullptr; + blockCreated(); } void Parser::createNewlineBlock() { @@ -226,24 +239,24 @@ void Parser::finishEntities() { if (_flags & (*flags)) { createBlock(); _flags &= ~(*flags); - if (((*flags) & TextBlockFPre) + if (((*flags) & TextBlockFlag::Pre) && !_t->_blocks.empty() - && _t->_blocks.back()->type() != TextBlockTNewline) { + && _t->_blocks.back()->type() != TextBlockType::Newline) { _newlineAwaited = true; } if (IsMono(*flags)) { _monoIndex = 0; } } - } else if (const auto lnkIndex = list.back().lnkIndex()) { - if (_lnkIndex == *lnkIndex) { + } else if (const auto linkIndex = list.back().linkIndex()) { + if (_linkIndex == *linkIndex) { createBlock(); - _lnkIndex = 0; + _linkIndex = 0; } - } else if (const auto spoilerIndex = list.back().spoilerIndex()) { - if (_spoilerIndex == *spoilerIndex && (_spoilerIndex != 0)) { + } else if (const auto colorIndex = list.back().colorIndex()) { + if (_colorIndex == *colorIndex) { createBlock(); - _spoilerIndex = 0; + _colorIndex = 0; } } list.pop_back(); @@ -289,26 +302,26 @@ bool Parser::checkEntities() { _customEmojiData = _waitingEntity->data(); _startedEntities[entityEnd].emplace_back(0, Type::CustomEmoji); } else if (entityType == EntityType::Bold) { - flags = TextBlockFBold; + flags = TextBlockFlag::Bold; } else if (entityType == EntityType::Semibold) { - flags = TextBlockFSemibold; + flags = TextBlockFlag::Semibold; } else if (entityType == EntityType::Italic) { - flags = TextBlockFItalic; + flags = TextBlockFlag::Italic; } else if (entityType == EntityType::Underline) { - flags = TextBlockFUnderline; - } else if (entityType == EntityType::PlainLink) { - flags = TextBlockFPlainLink; + flags = TextBlockFlag::Underline; + } else if (entityType == EntityType::Spoiler) { + flags = TextBlockFlag::Spoiler; } else if (entityType == EntityType::StrikeOut) { - flags = TextBlockFStrikeOut; + flags = TextBlockFlag::StrikeOut; } else if ((entityType == EntityType::Code) // #TODO entities || (entityType == EntityType::Pre)) { if (entityType == EntityType::Code) { - flags = TextBlockFCode; + flags = TextBlockFlag::Code; } else { - flags = TextBlockFPre; + flags = TextBlockFlag::Pre; createBlock(); if (!_t->_blocks.empty() - && _t->_blocks.back()->type() != TextBlockTNewline + && _t->_blocks.back()->type() != TextBlockType::Newline && _customEmojiData.isEmpty()) { createNewlineBlock(); } @@ -342,6 +355,14 @@ bool Parser::checkEntities() { } } else if (entityType == EntityType::MentionName) { pushComplexUrl(); + } else if (entityType == EntityType::Colorized) { + createBlock(); + + const auto data = _waitingEntity->data(); + _colorIndex = data.isEmpty() ? 1 : (data.front().unicode() + 1); + _startedEntities[entityEnd].emplace_back( + _colorIndex, + Type::Colorized); } if (link.type != EntityType::Invalid) { @@ -350,9 +371,9 @@ bool Parser::checkEntities() { _links.push_back(link); const auto tempIndex = _links.size(); const auto useCustom = processCustomIndex(tempIndex); - _lnkIndex = tempIndex + (useCustom ? 0 : kStringLinkIndexShift); + _linkIndex = tempIndex + (useCustom ? 0 : kStringLinkIndexShift); _startedEntities[entityEnd].emplace_back( - _lnkIndex, + _linkIndex, useCustom ? Type::IndexedLink : Type::Link); } else if (flags) { if (!(_flags & flags)) { @@ -361,18 +382,6 @@ bool Parser::checkEntities() { _startedEntities[entityEnd].emplace_back(flags); _monoIndex = monoIndex; } - } else if (entityType == EntityType::Spoiler) { - createBlock(); - - _spoilers.push_back(EntityLinkData{ - .data = QString::number(_spoilers.size() + 1), - .type = entityType, - }); - _spoilerIndex = _spoilers.size(); - - _startedEntities[entityEnd].emplace_back( - _spoilerIndex, - Type::Spoiler); } ++_waitingEntity; @@ -423,7 +432,7 @@ void Parser::parseCurrentChar() { const auto inCustomEmoji = !_customEmojiData.isEmpty(); const auto isNewLine = !inCustomEmoji && _multiline && IsNewline(_ch); const auto replaceWithSpace = IsSpace(_ch) && (_ch != QChar::Nbsp); - const auto isDiac = IsDiac(_ch); + const auto isDiacritic = IsDiacritic(_ch); const auto isTilde = !inCustomEmoji && _checkTilde && (_ch == '~'); const auto skip = [&] { if (IsBad(_ch) || _ch.isLowSurrogate()) { @@ -431,8 +440,10 @@ void Parser::parseCurrentChar() { } else if (_ch == 0xFE0F && Platform::IsMac()) { // Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :( return true; - } else if (isDiac) { - if (!_allowDiacritic || _emoji || ++_diacs > kMaxDiacAfterSymbol) { + } else if (isDiacritic) { + if (!_allowDiacritic + || _emoji + || ++_diacritics > kMaxDiacAfterSymbol) { return true; } } else if (_ch.isHighSurrogate()) { @@ -474,15 +485,15 @@ void Parser::parseCurrentChar() { _ch = 0; _allowDiacritic = false; } else { - if (isTilde) { // tilde fix in OpenSans - if (!(_flags & TextBlockFTilde)) { + if (isTilde) { // Tilde fix in OpenSans. + if (!(_flags & TextBlockFlag::Tilde)) { createBlock(-_emojiLookback); - _flags |= TextBlockFTilde; + _flags |= TextBlockFlag::Tilde; } } else { - if (_flags & TextBlockFTilde) { + if (_flags & TextBlockFlag::Tilde) { createBlock(-_emojiLookback); - _flags &= ~TextBlockFTilde; + _flags &= ~TextBlockFlag::Tilde; } } if (isNewLine) { @@ -497,8 +508,8 @@ void Parser::parseCurrentChar() { _t->_text.push_back(_ch); _allowDiacritic = true; } - if (!isDiac) { - _diacs = 0; + if (!isDiacritic) { + _diacritics = 0; } } } @@ -594,7 +605,7 @@ void Parser::trimSourceRange() { // } void Parser::finalize(const TextParseOptions &options) { - _t->_links.resize(_maxLnkIndex + _maxShiftedLnkIndex); + _t->_links.resize(_maxLinkIndex + _maxShiftedLinkIndex); auto counterCustomIndex = uint16(0); auto currentIndex = uint16(0); // Current the latest index of _t->_links. struct { @@ -614,21 +625,21 @@ void Parser::finalize(const TextParseOptions &options) { auto spacesCheckFrom = uint16(-1); const auto length = int(_t->_text.size()); for (auto &block : _t->_blocks) { - if (block->type() == TextBlockTCustomEmoji) { + if (block->type() == TextBlockType::CustomEmoji) { _t->_hasCustomEmoji = true; - } else if (block->type() != TextBlockTNewline - && block->type() != TextBlockTSkip) { + } else if (block->type() != TextBlockType::Newline + && block->type() != TextBlockType::Skip) { _t->_isOnlyCustomEmoji = false; - } else if (block->lnkIndex()) { + } else if (block->linkIndex()) { _t->_isOnlyCustomEmoji = _t->_isIsolatedEmoji = false; } if (!_t->_hasNotEmojiAndSpaces) { - if (block->type() == TextBlockTText) { + if (block->type() == TextBlockType::Text) { if (spacesCheckFrom == uint16(-1)) { - spacesCheckFrom = block->from(); + spacesCheckFrom = block->position(); } } else if (spacesCheckFrom != uint16(-1)) { - const auto checkTill = block->from(); + const auto checkTill = block->position(); for (auto i = spacesCheckFrom; i != checkTill; ++i) { Assert(i < length); if (!_t->_text[i].isSpace()) { @@ -640,35 +651,35 @@ void Parser::finalize(const TextParseOptions &options) { } } if (_t->_isIsolatedEmoji) { - if (block->type() == TextBlockTCustomEmoji - || block->type() == TextBlockTEmoji) { + if (block->type() == TextBlockType::CustomEmoji + || block->type() == TextBlockType::Emoji) { if (++isolatedEmojiCount > kIsolatedEmojiLimit) { _t->_isIsolatedEmoji = false; } - } else if (block->type() != TextBlockTSkip) { + } else if (block->type() != TextBlockType::Skip) { _t->_isIsolatedEmoji = false; } } - if (block->spoilerIndex()) { + if (block->flags() & TextBlockFlag::Spoiler) { if (!_t->_spoiler.data) { _t->_spoiler.data = std::make_unique( Integration::Instance().createSpoilerRepaint(_context)); } } - const auto shiftedIndex = block->lnkIndex(); + const auto shiftedIndex = block->linkIndex(); auto useCustomIndex = false; if (shiftedIndex <= kStringLinkIndexShift) { if (IsMono(block->flags()) && shiftedIndex) { const auto monoIndex = shiftedIndex; if (lastHandlerIndex.mono == monoIndex) { - block->setLnkIndex(currentIndex); + block->setLinkIndex(currentIndex); continue; // Optimization. } else { currentIndex++; } avoidIntersectionsWithCustom(); - block->setLnkIndex(currentIndex); + block->setLinkIndex(currentIndex); const auto handler = Integration::Instance().createLinkHandler( _monos[monoIndex - 1], _context); @@ -693,7 +704,7 @@ void Parser::finalize(const TextParseOptions &options) { ? shiftedIndex : (shiftedIndex - kStringLinkIndexShift); if (lastHandlerIndex.lnk == realIndex) { - block->setLnkIndex(usedIndex()); + block->setLinkIndex(usedIndex()); continue; // Optimization. } else { (useCustomIndex ? counterCustomIndex : currentIndex)++; @@ -701,7 +712,7 @@ void Parser::finalize(const TextParseOptions &options) { if (!useCustomIndex) { avoidIntersectionsWithCustom(); } - block->setLnkIndex(usedIndex()); + block->setLinkIndex(usedIndex()); _t->_links.resize(std::max(usedIndex(), uint16(_t->_links.size()))); const auto handler = Integration::Instance().createLinkHandler( diff --git a/ui/text/text_parser.h b/ui/text/text_parser.h index 528fc5a..eb4f3a7 100644 --- a/ui/text/text_parser.h +++ b/ui/text/text_parser.h @@ -28,8 +28,8 @@ private: Flags, Link, IndexedLink, - Spoiler, CustomEmoji, + Colorized, }; explicit StartedEntity(TextBlockFlags flags); @@ -37,8 +37,8 @@ private: [[nodiscard]] Type type() const; [[nodiscard]] std::optional flags() const; - [[nodiscard]] std::optional lnkIndex() const; - [[nodiscard]] std::optional spoilerIndex() const; + [[nodiscard]] std::optional linkIndex() const; + [[nodiscard]] std::optional colorIndex() const; private: const int _value = 0; @@ -96,23 +96,22 @@ private: std::vector _linksIndexes; std::vector _links; - std::vector _spoilers; std::vector _monos; base::flat_map< const QChar*, std::vector> _startedEntities; - uint16 _maxLnkIndex = 0; - uint16 _maxShiftedLnkIndex = 0; + uint16 _maxLinkIndex = 0; + uint16 _maxShiftedLinkIndex = 0; // current state - int32 _flags = 0; - uint16 _lnkIndex = 0; - uint16 _spoilerIndex = 0; + TextBlockFlags _flags; + uint16 _linkIndex = 0; + uint16 _colorIndex = 0; uint16 _monoIndex = 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 + int32 _diacritics = 0; // diacritic chars skipped without good char QFixed _sumWidth; bool _sumFinished = false; bool _newlineAwaited = false; diff --git a/ui/text/text_renderer.cpp b/ui/text/text_renderer.cpp index 89074df..db95239 100644 --- a/ui/text/text_renderer.cpp +++ b/ui/text/text_renderer.cpp @@ -173,6 +173,7 @@ void Renderer::draw(QPainter &p, const PaintContext &context) { _p = &p; _p->setFont(_t->_st->font); _palette = context.palette ? context.palette : &st::defaultTextPalette; + _colors = context.colors; _originalPen = _p->pen(); _originalPenSelected = (_palette->selectFg->c.alphaF() == 0) ? _originalPen @@ -184,18 +185,19 @@ void Renderer::draw(QPainter &p, const PaintContext &context) { _yTo = context.clip.isNull() ? -1 : (context.clip.y() + context.clip.height()); - if (const auto lines = context.elisionLines) { - if (_yTo < 0 || (_y + (lines - 1) * _t->_st->font->height) < _yTo) { - _yTo = _y + (lines * _t->_st->font->height); - _elideLast = true; - _elideRemoveFromEnd = context.elisionRemoveFromEnd; - } - _breakEverywhere = context.elisionBreakEverywhere; - } + _geometry = context.geometry.layout + ? context.geometry + : SimpleGeometry( + context.availableWidth, + _t->_st->font->height, + context.elisionHeight, + context.elisionRemoveFromEnd, + context.elisionOneLine, + context.elisionBreakEverywhere); + _breakEverywhere = _geometry.breakEverywhere; _spoilerCache = context.spoiler; _selection = context.selection; _fullWidthSelection = context.fullWidthSelection; - _w = context.availableWidth; _align = context.align; _cachedNow = context.now; _pausedEmoji = context.paused || context.pausedEmoji; @@ -209,34 +211,26 @@ void Renderer::draw(QPainter &p, const PaintContext &context) { void Renderer::enumerate() { _blocksSize = _t->_blocks.size(); - _wLeft = _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(); + const 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(); + if (_yFrom < clip.y()) { + _yFrom = clip.y(); + } + if (_yTo < 0 || _yTo > clip.y() + clip.height()) { + _yTo = clip.y() + clip.height(); + } } } - _parDirection = _t->_startDir; - if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); - if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) { - initNextParagraph(_t->_blocks.cbegin()); - } + _startLeft = _x.toInt(); + _startTop = _y; - _lineStart = 0; - _lineStartBlock = 0; + if ((*_t->_blocks.cbegin())->type() != TextBlockType::Newline) { + initNextParagraph(_t->_blocks.cbegin(), _t->_startDirection); + } _lineHeight = 0; _fontHeight = _t->_st->font->height; @@ -257,27 +251,23 @@ void Renderer::enumerate() { auto _btype = b->type(); auto blockHeight = CountBlockHeight(b, _t->_st); - if (_btype == TextBlockTNewline) { - if (!_lineHeight) _lineHeight = blockHeight; - if (!drawLine((*i)->from(), i, e)) { + if (_btype == TextBlockType::Newline) { + if (!_lineHeight) { + _lineHeight = blockHeight; + } + if (!drawLine((*i)->position(), 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; - } + last_rBearing = 0; + _last_rPadding = 0; - _parDirection = static_cast(b)->nextDirection(); - if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); - initNextParagraph(i + 1); + initNextParagraph( + i + 1, + static_cast(b)->nextDirection()); longWordLine = true; continue; @@ -296,7 +286,7 @@ void Renderer::enumerate() { continue; } - if (_btype == TextBlockTText) { + if (_btype == TextBlockType::Text) { 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(); @@ -332,10 +322,8 @@ void Renderer::enumerate() { continue; } - auto elidedLineHeight = qMax(_lineHeight, blockHeight); - auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); - if (elidedLine) { - _lineHeight = elidedLineHeight; + if (_elidedLine) { + _lineHeight = qMax(_lineHeight, blockHeight); } 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; @@ -343,20 +331,24 @@ void Renderer::enumerate() { _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)) { + const auto lineEnd = !_elidedLine + ? j->position() + : (j + 1 != en) + ? (j + 1)->position() + : _t->countBlockEnd(i, e); + if (!drawLine(lineEnd, i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); - _lineStart = j->from(); + _lineStart = j->position(); _lineStartBlock = blockIndex; + _paragraphWidthRemaining -= (_lineWidth - _wLeft) - _last_rPadding + last_rBearing; + initNextLine(); 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; - } + _wLeft -= j_width - last_rBearing; longWordLine = !wordEndsHere; f = j + 1; @@ -366,31 +358,33 @@ void Renderer::enumerate() { continue; } - auto elidedLineHeight = qMax(_lineHeight, blockHeight); - auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); - if (elidedLine) { - _lineHeight = elidedLineHeight; + if (_elidedLine) { + _lineHeight = qMax(_lineHeight, blockHeight); } - if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) { + const auto lineEnd = !_elidedLine + ? b->position() + : _t->countBlockEnd(i, e); + if (!drawLine(lineEnd, i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); - _lineStart = b->from(); + _lineStart = b->position(); _lineStartBlock = blockIndex; + _paragraphWidthRemaining -= (_lineWidth - _wLeft) - _last_rPadding + last_rBearing; + initNextLine(); 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; - } + _wLeft -= b->f_width() - last_rBearing; longWordLine = true; continue; } if (_lineStart < _t->_text.size()) { - if (!drawLine(_t->_text.size(), e, e)) return; + if (!drawLine(_t->_text.size(), e, e)) { + return; + } } if (!_p && _lookupSymbol) { _lookupResult.symbol = _t->_text.size(); @@ -398,7 +392,10 @@ void Renderer::enumerate() { } } -StateResult Renderer::getState(QPoint point, int w, StateRequest request) { +StateResult Renderer::getState( + QPoint point, + GeometryDescriptor geometry, + StateRequest request) { if (_t->isEmpty() || point.y() < 0) { return {}; } @@ -406,41 +403,13 @@ StateResult Renderer::getState(QPoint point, int w, StateRequest 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)) { + if (!_lookupSymbol && _lookupX < 0) { return {}; } - _w = w; - _yFrom = _lookupY; - _yTo = _lookupY + 1; - _align = _lookupRequest.align; - enumerate(); - return _lookupResult; -} - -StateResult Renderer::getStateElided(QPoint point, int w, StateRequestElided request) { - if (_t->isEmpty() || point.y() < 0 || request.lines <= 0) { - return {}; - } - _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)) { - return {}; - } - 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; - } - _w = w; + _geometry = std::move(geometry); + _breakEverywhere = _geometry.breakEverywhere; _yFrom = _lookupY; _yTo = _lookupY + 1; _align = _lookupRequest.align; @@ -455,26 +424,58 @@ crl::time Renderer::now() const { return _cachedNow; } -void Renderer::initNextParagraph(String::TextBlocks::const_iterator i) { +void Renderer::initNextParagraph( + String::TextBlocks::const_iterator i, + Qt::LayoutDirection direction) { + _parDirection = (direction == Qt::LayoutDirectionAuto) + ? style::LayoutDirection() + : direction; _parStartBlock = i; + _paragraphWidthRemaining = 0; const auto e = _t->_blocks.cend(); if (i == e) { - _parStart = _t->_text.size(); + _lineStart = _parStart = _t->_text.size(); + _lineStartBlock = _t->_blocks.size(); _parLength = 0; } else { - _parStart = (*i)->from(); + _lineStart = _parStart = (*i)->position(); + _lineStartBlock = i - _t->_blocks.cbegin(); + + auto last_rPadding = QFixed(0); + auto last_rBearing = QFixed(0); for (; i != e; ++i) { - if ((*i)->type() == TextBlockTNewline) { + if ((*i)->type() == TextBlockType::Newline) { break; } + const auto rBearing = (*i)->f_rbearing(); + _paragraphWidthRemaining += last_rBearing + + last_rPadding + + (*i)->f_width() + - rBearing; + last_rBearing = rBearing; } - _parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart; + _parLength = ((i == e) ? _t->_text.size() : (*i)->position()) - _parStart; } _parAnalysis.resize(0); + initNextLine(); +} + +void Renderer::initNextLine() { + const auto line = _geometry.layout({ + .left = 0, + .top = (_y - _startTop), + .width = _paragraphWidthRemaining.ceil().toInt(), + }, _lineStart); + _x = _startLeft + line.left; + _y = _startTop + line.top; + _lineWidth = _wLeft = line.width; + _elidedLine = line.elided; } void Renderer::initParagraphBidi() { - if (!_parLength || !_parAnalysis.isEmpty()) return; + if (!_parLength || !_parAnalysis.isEmpty()) { + return; + } String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; @@ -486,13 +487,13 @@ void Renderer::initParagraphBidi() { const ushort *curr = start; const ushort *end = start + _parLength; while (curr < end) { - while (n != e && (*n)->from() <= _parStart + (curr - start)) { + while (n != e && (*n)->position() <= _parStart + (curr - start)) { i = n; ++n; } const auto type = (*i)->type(); - if (type != TextBlockTEmoji - && type != TextBlockTCustomEmoji + if (type != TextBlockType::Emoji + && type != TextBlockType::CustomEmoji && *curr >= 0x590) { ignore = false; break; @@ -521,13 +522,15 @@ void Renderer::initParagraphBidi() { bool Renderer::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 (_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; + return !_elidedLine; } // Trimming pending spaces, because they sometimes don't fit on the line. @@ -542,16 +545,15 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get(); - auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide); - if (elidedLine) { + 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) { + if (_endBlock && _endBlock->type() == TextBlockType::Skip) { _endBlock = nullptr; } if (!_endBlock) { - elidedLine = false; + _elidedLine = false; } } @@ -559,11 +561,11 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato 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) + const auto extendLeft = (currentBlock->position() < _lineStart) + ? qMin(_lineStart - currentBlock->position(), 2) : 0; _localFrom = _lineStart - extendLeft; - const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine) + const auto extendedLineEnd = (_endBlock && _endBlock->position() < trimmedLineEnd && !_elidedLine) ? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end)) : trimmedLineEnd; @@ -571,7 +573,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato auto lineStart = extendLeft; auto lineLength = trimmedLineEnd - _lineStart; - if (elidedLine) { + if (_elidedLine) { initParagraphBidi(); prepareElidedLine(lineText, lineStart, lineLength, _endBlock); } @@ -589,7 +591,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato 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; + // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockType::Skip)) ? true : false; } else { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; @@ -601,7 +603,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } _lookupResult.uponSymbol = false; return false; - } else if (_lookupX >= x + (_w - _wLeft)) { + } else if (_lookupX >= x + (_lineWidth - _wLeft)) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; @@ -609,7 +611,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } 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; + // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockType::Skip)) ? true : false; } if (_lookupLink) { _lookupResult.link = nullptr; @@ -626,7 +628,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato const auto selectTillEnd = (_selection.to > trimmedLineEnd) && (trimmedLineEnd < _t->_text.size()) && (_selection.from <= trimmedLineEnd) - && (!_endBlock || _endBlock->type() != TextBlockTSkip); + && (!_endBlock || _endBlock->type() != TextBlockType::Skip); if ((selectFromStart && _parDirection == Qt::LeftToRight) || (selectTillEnd && _parDirection == Qt::RightToLeft)) { @@ -637,15 +639,17 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato if ((selectTillEnd && _parDirection == Qt::LeftToRight) || (selectFromStart && _parDirection == Qt::RightToLeft)) { if (x < _x + _wLeft) { - fillSelectRange({ x + _w - _wLeft, _x + _w }); + fillSelectRange({ x + _lineWidth - _wLeft, _x + _lineWidth }); } } } - if (trimmedLineEnd == _lineStart && !elidedLine) { + if (trimmedLineEnd == _lineStart && !_elidedLine) { return true; } - if (!elidedLine) initParagraphBidi(); // if was not inited + if (!_elidedLine) { + initParagraphBidi(); // if was not inited + } _f = _t->_st->font; QStackTextEngine engine(lineText, _f->f); @@ -662,7 +666,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato 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; + return !_elidedLine; } int skipIndex = -1; @@ -670,23 +674,23 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato QVarLengthArray levels(nItems); for (int i = 0; i < nItems; ++i) { auto &si = engine.layoutData->items[firstItem + i]; - while (nextBlock && nextBlock->from() <= _localFrom + si.position) { + while (nextBlock && nextBlock->position() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); - if (_type == TextBlockTSkip) { + if (_type == TextBlockType::Skip) { levels[i] = si.analysis.bidiLevel = 0; skipIndex = i; } else { levels[i] = si.analysis.bidiLevel; } if (si.analysis.flags == QScriptAnalysis::Object) { - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip) { + if (_type == TextBlockType::Emoji + || _type == TextBlockType::CustomEmoji + || _type == TextBlockType::Skip) { si.width = currentBlock->f_width() - + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) + + (nextBlock == _endBlock && (!nextBlock || nextBlock->position() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding()); } @@ -714,12 +718,12 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato const auto &si = engine.layoutData->items.at(item); const auto rtl = (si.analysis.bidiLevel % 2); - while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) { + while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->position() > _localFrom + si.position) { nextBlock = currentBlock; currentBlock = _t->_blocks[--blockIndex - 1].get(); applyBlockProperties(currentBlock); } - while (nextBlock && nextBlock->from() <= _localFrom + si.position) { + while (nextBlock && nextBlock->position() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; applyBlockProperties(currentBlock); @@ -734,11 +738,11 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } } } - if (_type != TextBlockTSkip) { + if (_type != TextBlockType::Skip) { _lookupResult.uponSymbol = true; } if (_lookupSymbol) { - if (_type == TextBlockTSkip) { + if (_type == TextBlockType::Skip) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; @@ -750,8 +754,8 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } // Emoji with spaces after symbol lookup - auto chFrom = _str + currentBlock->from(); - auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); + auto chFrom = _str + currentBlock->position(); + auto chTo = chFrom + ((nextBlock ? nextBlock->position() : _t->_text.size()) - currentBlock->position()); auto spacesWidth = (si.width - currentBlock->f_width()); auto spacesCount = 0; while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) { @@ -780,7 +784,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } } return false; - } else if (_p && (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji)) { + } else if (_p && (_type == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji)) { auto glyphX = x; auto spacesWidth = (si.width - currentBlock->f_width()); if (rtl) { @@ -791,8 +795,8 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato if (_background.selectActiveBlock) { fillSelect = { x, x + si.width }; } else if (_localFrom + si.position < _selection.to) { - auto chFrom = _str + currentBlock->from(); - auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); + auto chFrom = _str + currentBlock->position(); + auto chTo = chFrom + ((nextBlock ? nextBlock->position() : _t->_text.size()) - currentBlock->position()); if (_localFrom + si.position >= _selection.from) { // could be without space if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) { fillSelect = { x, x + si.width }; @@ -820,7 +824,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato } const auto x = (glyphX + st::emojiPadding).toInt(); const auto y = _y + _yDelta + emojiY; - if (_type == TextBlockTEmoji) { + if (_type == TextBlockType::Emoji) { Emoji::Draw( *_p, static_cast(currentBlock)->_emoji, @@ -1015,7 +1019,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato : QRegion(); if (complexClipping) { const auto elided = (_indexOfElidedBlock == blockIndex) - ? (_elideRemoveFromEnd + _f->elidew) + ? _f->elidew : 0; _p->setClipRect( QRect( @@ -1046,9 +1050,9 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato const auto externalClipping = clippingEnabled ? clippingRegion : QRegion(QRect( - (_x - _w).toInt(), + (_x - _lineWidth).toInt(), _y - _lineHeight, - (_x + 2 * _w).toInt(), + (_x + 2 * _lineWidth).toInt(), _y + 2 * _lineHeight)); _p->setClipRegion(externalClipping - selectedRect); _p->setPen(*_currentPen); @@ -1089,7 +1093,7 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato x += itemWidth; } fillSpoilerRects(); - return true; + return !_elidedLine; } void Renderer::fillSelectRange(FixedRange range) { @@ -1108,9 +1112,7 @@ void Renderer::pushSpoilerRange( if (!_background.spoiler || !_spoiler) { return; } - const auto elided = isElidedItem - ? (_elideRemoveFromEnd + _f->elidew) - : 0; + const auto elided = isElidedItem ? _f->elidew : 0; range.till -= elided; if (range.empty()) { return; @@ -1204,9 +1206,18 @@ void Renderer::elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, _elideSavedIndex = blockIndex; auto mutableText = const_cast(_t); _elideSavedBlock = std::move(mutableText->_blocks[blockIndex]); - mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex(), (*_elideSavedBlock)->spoilerIndex()); + mutableText->_blocks[blockIndex] = Block::Text( + _t->_st->font, + _t->_text, + elideStart, 0, + (*_elideSavedBlock)->flags(), + (*_elideSavedBlock)->linkIndex(), + (*_elideSavedBlock)->colorIndex(), + QFIXED_MAX); _blocksSize = blockIndex + 1; - _endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr); + _endBlock = (blockIndex + 1 < _t->_blocks.size()) + ? _t->_blocks[blockIndex + 1].get() + : nullptr; } void Renderer::setElideBidi(int32 elideStart, int32 elideLen) { @@ -1237,40 +1248,40 @@ void Renderer::prepareElidedLine(QString &lineText, int32 lineStart, int32 &line eShapeLine(line); auto elideWidth = _f->elidew; - _wLeft = _w - elideWidth - _elideRemoveFromEnd; + _wLeft = _lineWidth - elideWidth; 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) { + while (nextBlock && nextBlock->position() <= _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 == TextBlockTCustomEmoji - || _type == TextBlockTSkip) { + if (_type == TextBlockType::Emoji + || _type == TextBlockType::CustomEmoji + || _type == TextBlockType::Skip) { si.width = currentBlock->f_width() + currentBlock->f_rpadding(); } } - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip - || _type == TextBlockTNewline) { + if (_type == TextBlockType::Emoji + || _type == TextBlockType::CustomEmoji + || _type == TextBlockType::Skip + || _type == TextBlockType::Newline) { if (_wLeft < si.width) { - lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis; - lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart; - _selection.to = qMin(_selection.to, currentBlock->from()); + lineText = lineText.mid(0, currentBlock->position() - _localFrom) + kQEllipsis; + lineLength = currentBlock->position() + kQEllipsis.size() - _lineStart; + _selection.to = qMin(_selection.to, currentBlock->position()); _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); - setElideBidi(currentBlock->from(), kQEllipsis.size()); - elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth); + setElideBidi(currentBlock->position(), kQEllipsis.size()); + elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->position(), elideWidth); return; } _wLeft -= si.width; - } else if (_type == TextBlockTText) { + } else if (_type == TextBlockType::Text) { unsigned short *logClusters = engine.logClusters(&si); QGlyphLayout glyphs = engine.shapedGlyphs(&si); @@ -1325,7 +1336,7 @@ void Renderer::prepareElidedLine(QString &lineText, int32 lineStart, int32 &line lineLength += kQEllipsis.size(); if (!repeat) { - for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) { + for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->position() < elideStart; ++blockIndex) { } if (blockIndex < _blocksSize) { elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth); @@ -1371,65 +1382,46 @@ void Renderer::eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, co void Renderer::eShapeLine(const QScriptLine &line) { int item = _e->findItem(line.from); - if (item == -1) + if (item == -1) { return; + } auto end = _e->findItem(line.from + line.length - 1, item); auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); - auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + 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) { + while (nextBlock && nextBlock->position() <= _localFrom + si.position) { currentBlock = nextBlock; - nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; + nextBlock = (++blockIndex < _blocksSize) + ? _t->_blocks[blockIndex].get() + : nullptr; eSetFont(currentBlock); } _e->shape(item); } } -style::font Renderer::applyFlags(int32 flags, const style::font &f) { - if (!flags) { - return f; - } - auto result = f; - if (IsMono(flags)) { - result = result->monospace(); - } else { - if (flags & TextBlockFBold) { - result = result->bold(); - } else if (flags & TextBlockFSemibold) { - result = result->semibold(); - } - 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 = result->semibold(); - } - } - return result; -} - void Renderer::eSetFont(const AbstractBlock *block) { const auto flags = block->flags(); const auto usedFont = [&] { - if (const auto index = block->lnkIndex()) { - const auto active = ClickHandler::showAsActive( - _t->_links.at(index - 1) - ) || (_palette && _palette->linkAlwaysActive > 0); + if (const auto index = block->linkIndex()) { + const auto active = (_palette && _palette->linkAlwaysActive) + || ClickHandler::showAsActive(_t->_links.at(index - 1)); return active ? _t->_st->linkFontOver : _t->_st->linkFont; } return _t->_st->font; }(); - const auto newFont = applyFlags(flags, usedFont); + const auto newFont = WithFlags(usedFont, flags); if (newFont != _f) { _f = (newFont->family() == _t->_st->font->family()) - ? applyFlags(flags | newFont->flags(), _t->_st->font) + ? WithFlags(_t->_st->font, flags, newFont->flags()) : newFont; _e->fnt = _f->f; _e->resetFontEngineCache(); @@ -1479,14 +1471,14 @@ void Renderer::eItemize() { auto start = string; auto end = start + length; while (start < end) { - while (nextBlock && nextBlock->from() <= _localFrom + (start - string)) { + while (nextBlock && nextBlock->position() <= _localFrom + (start - string)) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); - if (_type == TextBlockTEmoji - || _type == TextBlockTCustomEmoji - || _type == TextBlockTSkip) { + if (_type == TextBlockType::Emoji + || _type == TextBlockType::CustomEmoji + || _type == TextBlockType::Skip) { analysis->script = QChar::Script_Common; analysis->flags = QScriptAnalysis::Object; } else { @@ -1515,7 +1507,7 @@ void Renderer::eItemize() { auto start = 0; auto end = start + length; for (int i = start + 1; i < end; ++i) { - while (nextBlock && nextBlock->from() <= _localFrom + i) { + while (nextBlock && nextBlock->position() <= _localFrom + i) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } @@ -1545,20 +1537,23 @@ void Renderer::eItemize() { } } -QChar::Direction Renderer::eSkipBoundryNeutrals(QScriptAnalysis *analysis, - const ushort *unicode, - int &sor, int &eor, BidiControl &control, - String::TextBlocks::const_iterator i) { +QChar::Direction Renderer::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) { + while (i != _parStartBlock && (*i)->position() > _parStart + sor) { n = i; --i; } - while (n != e && (*n)->from() <= _parStart + sor) { + while (n != e && (*n)->position() <= _parStart + sor) { i = n; ++n; } @@ -1566,10 +1561,10 @@ QChar::Direction Renderer::eSkipBoundryNeutrals(QScriptAnalysis *analysis, TextBlockType _itype = (*i)->type(); if (eor == _parLength) dir = control.basicDirection(); - else if (_itype == TextBlockTEmoji - || _itype == TextBlockTCustomEmoji) + else if (_itype == TextBlockType::Emoji + || _itype == TextBlockType::CustomEmoji) dir = QChar::DirCS; - else if (_itype == TextBlockTSkip) + else if (_itype == TextBlockType::Skip) dir = QChar::DirCS; else dir = QChar::direction(unicode[sor]); @@ -1602,9 +1597,9 @@ bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { QChar::Direction sdir; TextBlockType _stype = (*_parStartBlock)->type(); - if (_stype == TextBlockTEmoji || _stype == TextBlockTCustomEmoji) + if (_stype == TextBlockType::Emoji || _stype == TextBlockType::CustomEmoji) sdir = QChar::DirCS; - else if (_stype == TextBlockTSkip) + else if (_stype == TextBlockType::Skip) sdir = QChar::DirCS; else sdir = QChar::direction(*unicode); @@ -1619,7 +1614,7 @@ bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { status.dir = sdir; while (current <= _parLength) { - while (n != e && (*n)->from() <= _parStart + current) { + while (n != e && (*n)->position() <= _parStart + current) { i = n; ++n; } @@ -1628,10 +1623,10 @@ bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { TextBlockType _itype = (*i)->type(); if (current == (int)_parLength) dirCurrent = control.basicDirection(); - else if (_itype == TextBlockTEmoji - || _itype == TextBlockTCustomEmoji) + else if (_itype == TextBlockType::Emoji + || _itype == TextBlockType::CustomEmoji) dirCurrent = QChar::DirCS; - else if (_itype == TextBlockTSkip) + else if (_itype == TextBlockType::Skip) dirCurrent = QChar::DirCS; else dirCurrent = QChar::direction(unicode[current]); @@ -2009,23 +2004,34 @@ bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { void Renderer::applyBlockProperties(const AbstractBlock *block) { eSetFont(block); if (_p) { - const auto isMono = IsMono(block->flags()); + const auto flags = block->flags(); + const auto isMono = IsMono(flags); _background = {}; - if (block->spoilerIndex() && _spoiler) { + if ((flags & TextBlockFlag::Spoiler) && _spoiler) { _background.spoiler = true; } if (isMono - && block->lnkIndex() + && block->linkIndex() && (!_background.spoiler || _spoiler->revealed)) { _background.selectActiveBlock = ClickHandler::showAsPressed( - _t->_links.at(block->lnkIndex() - 1)); + _t->_links.at(block->linkIndex() - 1)); } - if (isMono) { + if (const auto color = block->colorIndex()) { + if (color == 1) { + _currentPen = &_palette->linkFg->p; + _currentPenSelected = &_palette->selectLinkFg->p; + } else if (color - 1 <= _colors.size()) { + _currentPen = _colors[color - 2].pen; + _currentPenSelected = _colors[color - 2].penSelected; + } else { + _currentPen = &_originalPen; + _currentPenSelected = &_originalPenSelected; + } + } else if (isMono) { _currentPen = &_palette->monoFg->p; _currentPenSelected = &_palette->selectMonoFg->p; - } else if (block->lnkIndex() - || (block->flags() & TextBlockFPlainLink)) { + } else if (block->linkIndex()) { _currentPen = &_palette->linkFg->p; _currentPenSelected = &_palette->selectLinkFg->p; } else { @@ -2038,12 +2044,12 @@ void Renderer::applyBlockProperties(const AbstractBlock *block) { ClickHandlerPtr Renderer::lookupLink(const AbstractBlock *block) const { const auto spoilerLink = (_spoiler && !_spoiler->revealed - && block->spoilerIndex()) + && (block->flags() & TextBlockFlag::Spoiler)) ? _spoiler->link : ClickHandlerPtr(); - return (spoilerLink || !block->lnkIndex()) + return (spoilerLink || !block->linkIndex()) ? spoilerLink - : _t->_links.at(block->lnkIndex() - 1); + : _t->_links.at(block->linkIndex() - 1); } } // namespace Ui::Text diff --git a/ui/text/text_renderer.h b/ui/text/text_renderer.h index b89254b..184c1f3 100644 --- a/ui/text/text_renderer.h +++ b/ui/text/text_renderer.h @@ -37,12 +37,8 @@ public: void draw(QPainter &p, const PaintContext &context); [[nodiscard]] StateResult getState( QPoint point, - int w, + GeometryDescriptor geometry, StateRequest request); - [[nodiscard]] StateResult getStateElided( - QPoint point, - int w, - StateRequestElided request); private: static constexpr int kSpoilersRectsSize = 512; @@ -52,7 +48,10 @@ private: void enumerate(); [[nodiscard]] crl::time now() const; - void initNextParagraph(String::TextBlocks::const_iterator i); + void initNextParagraph( + String::TextBlocks::const_iterator i, + Qt::LayoutDirection direction); + void initNextLine(); void initParagraphBidi(); bool drawLine( uint16 _lineEnd, @@ -94,7 +93,6 @@ private: const BidiControl &control, QChar::Direction dir); void eShapeLine(const QScriptLine &line); - [[nodiscard]] style::font applyFlags(int32 flags, const style::font &f); void eSetFont(const AbstractBlock *block); void eItemize(); QChar::Direction eSkipBoundryNeutrals( @@ -111,13 +109,12 @@ private: const AbstractBlock *block) const; const String *_t = nullptr; + GeometryDescriptor _geometry; SpoilerData *_spoiler = nullptr; SpoilerMessCache *_spoilerCache = nullptr; QPainter *_p = nullptr; const style::TextPalette *_palette = nullptr; - bool _elideLast = false; - bool _breakEverywhere = false; - int _elideRemoveFromEnd = 0; + std::span _colors; bool _pausedEmoji = false; bool _pausedSpoiler = false; style::align _align = style::al_topleft; @@ -131,7 +128,6 @@ private: } _background; int _yFrom = 0; int _yTo = 0; - int _yToElide = 0; TextSelection _selection = { 0, 0 }; bool _fullWidthSelection = true; const QChar *_str = nullptr; @@ -158,11 +154,15 @@ private: // current line data QTextEngine *_e = nullptr; style::font _f; - QFixed _x, _w, _wLeft, _last_rPadding; + int _startLeft = 0; + int _startTop = 0; + QFixed _x, _wLeft, _last_rPadding; int _y = 0; int _yDelta = 0; int _lineHeight = 0; int _fontHeight = 0; + bool _breakEverywhere = false; + bool _elidedLine = false; // elided hack support int _blocksSize = 0; @@ -172,6 +172,8 @@ private: int _lineStart = 0; int _localFrom = 0; int _lineStartBlock = 0; + QFixed _lineWidth = 0; + QFixed _paragraphWidthRemaining = 0; // link and symbol resolve QFixed _lookupX = 0; diff --git a/ui/text/text_utilities.cpp b/ui/text/text_utilities.cpp index c2c5114..35d4f35 100644 --- a/ui/text/text_utilities.cpp +++ b/ui/text/text_utilities.cpp @@ -54,12 +54,14 @@ TextWithEntities Link(TextWithEntities text, int index) { return Link(std::move(text), u"internal:index"_q + QChar(index)); } -TextWithEntities PlainLink(const QString &text) { - return WithSingleEntity(text, EntityType::PlainLink); +TextWithEntities Colorized(const QString &text, int index) { + const auto data = index ? QString(QChar(index)) : QString(); + return WithSingleEntity(text, EntityType::Colorized, data); } -TextWithEntities PlainLink(TextWithEntities text) { - return Wrapped(std::move(text), EntityType::PlainLink, QString()); +TextWithEntities Colorized(TextWithEntities text, int index) { + const auto data = index ? QString(QChar(index)) : QString(); + return Wrapped(std::move(text), EntityType::Colorized, data); } TextWithEntities Wrapped( diff --git a/ui/text/text_utilities.h b/ui/text/text_utilities.h index e493cec..1391759 100644 --- a/ui/text/text_utilities.h +++ b/ui/text/text_utilities.h @@ -35,8 +35,12 @@ inline constexpr auto Upper = details::ToUpperType{}; TextWithEntities text, const QString &url = u"internal:action"_q); [[nodiscard]] TextWithEntities Link(TextWithEntities text, int index); -[[nodiscard]] TextWithEntities PlainLink(const QString &text); -[[nodiscard]] TextWithEntities PlainLink(TextWithEntities text); +[[nodiscard]] TextWithEntities Colorized( + const QString &text, + int index = 0); +[[nodiscard]] TextWithEntities Colorized( + TextWithEntities text, + int index = 0); [[nodiscard]] TextWithEntities Wrapped( TextWithEntities text, EntityType type, diff --git a/ui/toast/toast_widget.cpp b/ui/toast/toast_widget.cpp index c676776..6849046 100644 --- a/ui/toast/toast_widget.cpp +++ b/ui/toast/toast_widget.cpp @@ -173,14 +173,13 @@ void Widget::paintEvent(QPaintEvent *e) { width()); } - const auto lines = _maxTextHeight / _st->style.font->height; p.setPen(st::toastFg); _text.draw(p, { .position = { _st->padding.left(), _textTop }, .availableWidth = _textWidth + 1, .palette = &_st->palette, .spoiler = Ui::Text::DefaultSpoilerCache(), - .elisionLines = lines, + .elisionHeight = _maxTextHeight, }); } diff --git a/ui/widgets/checkbox.cpp b/ui/widgets/checkbox.cpp index c589dcf..8aabbc5 100644 --- a/ui/widgets/checkbox.cpp +++ b/ui/widgets/checkbox.cpp @@ -587,8 +587,8 @@ void Checkbox::setTextBreakEverywhere(bool allow) { _textBreakEverywhere = allow; } -void Checkbox::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) { - _text.setLink(lnkIndex, lnk); +void Checkbox::setLink(uint16 index, const ClickHandlerPtr &lnk) { + _text.setLink(index, lnk); } void Checkbox::setLinksTrusted() { diff --git a/ui/widgets/checkbox.h b/ui/widgets/checkbox.h index 79fa5f4..06da21b 100644 --- a/ui/widgets/checkbox.h +++ b/ui/widgets/checkbox.h @@ -185,7 +185,7 @@ public: void setAllowTextLines(int lines = 0); void setTextBreakEverywhere(bool allow = true); - void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk); + void setLink(uint16 index, const ClickHandlerPtr &lnk); void setLinksTrusted(); using ClickHandlerFilter = Fn; diff --git a/ui/widgets/labels.cpp b/ui/widgets/labels.cpp index daf48c9..8b6dcaa 100644 --- a/ui/widgets/labels.cpp +++ b/ui/widgets/labels.cpp @@ -368,8 +368,8 @@ void FlatLabel::refreshSize() { resize(fullWidth, fullHeight); } -void FlatLabel::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) { - _text.setLink(lnkIndex, lnk); +void FlatLabel::setLink(uint16 index, const ClickHandlerPtr &lnk) { + _text.setLink(index, lnk); } void FlatLabel::setLinksTrusted() { @@ -795,7 +795,9 @@ CrossFadeAnimation::Data FlatLabel::crossFadeData( auto result = CrossFadeAnimation::Data(); result.full = GrabWidgetToImage(this, QRect(), bg->c); const auto textWidth = width() - _st.margin.left() - _st.margin.right(); - _text.countLineWidths(textWidth, &result.lineWidths, _breakEverywhere); + result.lineWidths = _text.countLineWidths(textWidth, { + .breakEverywhere = _breakEverywhere, + }); result.lineHeight = _st.style.font->height; const auto addedHeight = (_st.style.lineHeight - result.lineHeight); if (addedHeight > 0) { @@ -961,11 +963,11 @@ void FlatLabel::paintEvent(QPaintEvent *e) { && (_st.maxHeight < _fullTextHeight || textWidth < _text.maxWidth()); const auto renderElided = _breakEverywhere || heightExceeded; const auto lineHeight = qMax(_st.style.lineHeight, _st.style.font->height); - const auto lines = !renderElided + const auto elisionHeight = !renderElided ? 0 : _st.maxHeight - ? qMax(_st.maxHeight / lineHeight, 1) - : ((height() / lineHeight) + 2); + ? qMax(_st.maxHeight, lineHeight) + : height(); const auto paused = _animationsPausedCallback ? _animationsPausedCallback() : WhichAnimationsPaused::None; @@ -982,7 +984,7 @@ void FlatLabel::paintEvent(QPaintEvent *e) { .pausedSpoiler = (paused == WhichAnimationsPaused::Spoiler || paused == WhichAnimationsPaused::All), .selection = selection, - .elisionLines = lines, + .elisionHeight = elisionHeight, .elisionBreakEverywhere = renderElided && _breakEverywhere, }); } diff --git a/ui/widgets/labels.h b/ui/widgets/labels.h index 46552d4..cb66f5c 100644 --- a/ui/widgets/labels.h +++ b/ui/widgets/labels.h @@ -26,7 +26,7 @@ class CrossFadeAnimation { public: struct Data { QImage full; - QVector lineWidths; + std::vector lineWidths; QPoint position; style::align align; style::font font; @@ -143,7 +143,7 @@ public: int naturalWidth() const override; QMargins getMargins() const override; - void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk); + void setLink(uint16 index, const ClickHandlerPtr &lnk); void setLinksTrusted(); using ClickHandlerFilter = Fn; diff --git a/ui/widgets/side_bar_button.cpp b/ui/widgets/side_bar_button.cpp index c3d5d81..470d94a 100644 --- a/ui/widgets/side_bar_button.cpp +++ b/ui/widgets/side_bar_button.cpp @@ -166,10 +166,10 @@ void SideBarButton::paintEvent(QPaintEvent *e) { } if (_lock.locked) { - auto lineWidths = QVector(); - lineWidths.reserve(kMaxLabelLines); - _text.countLineWidths(width() - 2 * _st.textSkip, &lineWidths); - if (lineWidths.isEmpty()) { + const auto lineWidths = _text.countLineWidths( + width() - 2 * _st.textSkip, + { .reserve = kMaxLabelLines }); + if (lineWidths.empty()) { return; } validateLockIconCache();