// This file is part of Desktop App Toolkit, // a set of libraries for developing nice desktop applications. // // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // #include "ui/text/text_renderer.h" #include "ui/text/text_extended_data.h" #include "styles/style_basic.h" #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #endif // Qt < 6.0.0 namespace Ui::Text { namespace { // COPIED FROM qtextengine.cpp AND MODIFIED struct BidiStatus { BidiStatus() { eor = QChar::DirON; lastStrong = QChar::DirON; last = QChar::DirON; dir = QChar::DirON; } QChar::Direction eor; QChar::Direction lastStrong; QChar::Direction last; QChar::Direction dir; }; enum { _MaxBidiLevel = 61 }; enum { _MaxItemLength = 4096 }; void InitTextItemWithScriptItem(QTextItemInt &ti, const QScriptItem &si) { // explicitly initialize flags so that initFontAttributes can be called // multiple times on the same TextItem ti.flags = { }; if (si.analysis.bidiLevel % 2) ti.flags |= QTextItem::RightToLeft; ti.ascent = si.ascent; ti.descent = si.descent; if (ti.charFormat.hasProperty(QTextFormat::TextUnderlineStyle)) { ti.underlineStyle = ti.charFormat.underlineStyle(); } else if (ti.charFormat.boolProperty(QTextFormat::FontUnderline) || ti.f->underline()) { ti.underlineStyle = QTextCharFormat::SingleUnderline; } // compat if (ti.underlineStyle == QTextCharFormat::SingleUnderline) ti.flags |= QTextItem::Underline; if (ti.f->overline() || ti.charFormat.fontOverline()) ti.flags |= QTextItem::Overline; if (ti.f->strikeOut() || ti.charFormat.fontStrikeOut()) ti.flags |= QTextItem::StrikeOut; } void AppendRange( QVarLengthArray &ranges, FixedRange range) { for (auto i = ranges.begin(); i != ranges.end(); ++i) { if (range.till < i->from) { ranges.insert(i, range); return; } else if (!Distinct(range, *i)) { *i = United(*i, range); for (auto j = i + 1; j != ranges.end(); ++j) { if (j->from > i->till) { ranges.erase(i + 1, j); return; } else { *i = United(*i, *j); } } ranges.erase(i + 1, ranges.end()); return; } } ranges.push_back(range); } } // namespace struct Renderer::BidiControl { inline BidiControl(bool rtl) : base(rtl ? 1 : 0), level(rtl ? 1 : 0) {} inline void embed(bool rtl, bool o = false) { unsigned int toAdd = 1; if ((level % 2 != 0) == rtl) { ++toAdd; } if (level + toAdd <= _MaxBidiLevel) { ctx[cCtx].level = level; ctx[cCtx].override = override; cCtx++; override = o; level += toAdd; } } inline bool canPop() const { return cCtx != 0; } inline void pdf() { Q_ASSERT(cCtx); --cCtx; level = ctx[cCtx].level; override = ctx[cCtx].override; } inline QChar::Direction basicDirection() const { return (base ? QChar::DirR : QChar::DirL); } inline unsigned int baseLevel() const { return base; } inline QChar::Direction direction() const { return ((level % 2) ? QChar::DirR : QChar::DirL); } struct { unsigned int level = 0; bool override = false; } ctx[_MaxBidiLevel]; unsigned int cCtx = 0; const unsigned int base; unsigned int level; bool override = false; }; FixedRange Intersected(FixedRange a, FixedRange b) { return { .from = std::max(a.from, b.from), .till = std::min(a.till, b.till), }; } bool Intersects(FixedRange a, FixedRange b) { return (a.till > b.from) && (b.till > a.from); } FixedRange United(FixedRange a, FixedRange b) { return { .from = std::min(a.from, b.from), .till = std::max(a.till, b.till), }; } bool Distinct(FixedRange a, FixedRange b) { return (a.till < b.from) || (b.till < a.from); } Renderer::Renderer(const Ui::Text::String &t) : _t(&t) , _spoiler(_t->_extended ? _t->_extended->spoiler.get() : nullptr) { } Renderer::~Renderer() { restoreAfterElided(); if (_p) { _p->setPen(_originalPen); } } void Renderer::draw(QPainter &p, const PaintContext &context) { if (_t->isEmpty()) { return; } _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 : _palette->selectFg->p; _x = _startLeft = context.position.x(); _y = _startTop = context.position.y(); _yFrom = context.clip.isNull() ? 0 : context.clip.y(); _yTo = context.clip.isNull() ? -1 : (context.clip.y() + context.clip.height()); _geometry = context.geometry.layout ? context.geometry : SimpleGeometry( context.availableWidth, (context.elisionLines ? context.elisionLines : (context.elisionHeight / _t->_st->font->height)), context.elisionRemoveFromEnd, context.elisionBreakEverywhere); _breakEverywhere = _geometry.breakEverywhere; _spoilerCache = context.spoiler; _selection = context.selection; _highlight = context.highlight; _fullWidthSelection = context.fullWidthSelection; _align = context.align; _cachedNow = context.now; _pausedEmoji = context.paused || context.pausedEmoji; _pausedSpoiler = context.paused || context.pausedSpoiler; _spoilerOpacity = _spoiler ? (1. - _spoiler->revealAnimation.value( _spoiler->revealed ? 1. : 0.)) : 0.; _quotePreCache = context.pre; _quoteBlockquoteCache = context.blockquote; enumerate(); } void Renderer::enumerate() { Expects(!_geometry.outElided); _blocksSize = _t->_blocks.size(); _str = _t->_text.unicode(); if (_p) { 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 ((*_t->_blocks.cbegin())->type() != TextBlockType::Newline) { initNextParagraph( _t->_blocks.cbegin(), _t->_startQuoteIndex, UnpackParagraphDirection( _t->_startParagraphLTR, _t->_startParagraphRTL)); } _lineHeight = 0; _fontHeight = _t->_st->font->height; auto last_rBearing = QFixed(0); _last_rPadding = QFixed(0); const auto guard = gsl::finally([&] { if (_p) { paintSpoilerRects(); } if (_highlight) { composeHighlightPath(); } }); auto blockIndex = 0; bool longWordLine = true; auto e = _t->_blocks.cend(); for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) { auto b = i->get(); auto _btype = b->type(); auto blockHeight = CountBlockHeight(b, _t->_st); if (_btype == TextBlockType::Newline) { if (!_lineHeight) { _lineHeight = blockHeight; } const auto qindex = static_cast(b)->quoteIndex(); const auto changed = (_quoteIndex != qindex); fillParagraphBg(changed ? _quotePadding.bottom() : 0); if (!drawLine((*i)->position(), i, e)) { return; } _y += _lineHeight; _lineHeight = 0; last_rBearing = 0; _last_rPadding = 0; initNextParagraph( i + 1, qindex, static_cast(b)->paragraphDirection()); longWordLine = true; continue; } auto b__f_rbearing = b->f_rbearing(); auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing); if (newWidthLeft >= 0) { last_rBearing = b__f_rbearing; _last_rPadding = b->f_rpadding(); _wLeft = newWidthLeft; _lineHeight = qMax(_lineHeight, blockHeight); longWordLine = false; continue; } if (_btype == 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(); _lineHeight = qMax(_lineHeight, blockHeight); longWordLine = false; continue; } auto f_wLeft = _wLeft; // vars for saving state of the last word start auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) { auto wordEndsHere = (j->f_width() >= 0); auto j_width = wordEndsHere ? j->f_width() : -j->f_width(); auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing()); if (newWidthLeft >= 0) { last_rBearing = j->f_rbearing(); _last_rPadding = j->f_rpadding(); _wLeft = newWidthLeft; _lineHeight = qMax(_lineHeight, blockHeight); if (wordEndsHere) { longWordLine = false; } if (wordEndsHere || longWordLine) { f = j + 1; f_wLeft = _wLeft; f_lineHeight = _lineHeight; } continue; } 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; _wLeft = f_wLeft; _lineHeight = f_lineHeight; j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width(); } const auto lineEnd = !_elidedLine ? j->position() : (j + 1 != en) ? (j + 1)->position() : _t->countBlockEnd(i, e); fillParagraphBg(0); if (!drawLine(lineEnd, i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); _lineStart = j->position(); _lineStartBlock = blockIndex; initNextLine(); last_rBearing = j->f_rbearing(); _last_rPadding = j->f_rpadding(); _wLeft -= j_width - last_rBearing; longWordLine = !wordEndsHere; f = j + 1; f_wLeft = _wLeft; f_lineHeight = _lineHeight; } continue; } if (_elidedLine) { _lineHeight = qMax(_lineHeight, blockHeight); } const auto lineEnd = !_elidedLine ? b->position() : _t->countBlockEnd(i, e); fillParagraphBg(0); if (!drawLine(lineEnd, i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); _lineStart = b->position(); _lineStartBlock = blockIndex; initNextLine(); last_rBearing = b__f_rbearing; _last_rPadding = b->f_rpadding(); _wLeft -= b->f_width() - last_rBearing; longWordLine = true; continue; } if (_lineStart < _t->_text.size()) { fillParagraphBg(_quotePadding.bottom()); if (!drawLine(_t->_text.size(), e, e)) { return; } } if (!_p && _lookupSymbol) { _lookupResult.symbol = _t->_text.size(); _lookupResult.afterSymbol = false; } } void Renderer::fillParagraphBg(int paddingBottom) { if (_quote) { const auto &st = _t->quoteStyle(_quote); const auto skip = st.verticalSkip; const auto isTop = (_y != _quoteLineTop); const auto isBottom = (paddingBottom != 0); const auto left = _startLeft + _quoteShift; const auto start = _quoteTop + skip; const auto top = _quoteLineTop + (isTop ? skip : 0); const auto fill = _y + _lineHeight + paddingBottom - top - (isBottom ? skip : 0); const auto rect = QRect(left, top, _startLineWidth, fill); const auto cache = (!_p || !_quote) ? nullptr : _quote->pre ? _quotePreCache : _quote->blockquote ? _quoteBlockquoteCache : nullptr; if (cache) { auto &valid = _quote->pre ? _quotePreValid : _quoteBlockquoteValid; if (!valid) { valid = true; ValidateQuotePaintCache(*cache, st); } FillQuotePaint(*_p, rect, *cache, st, { .skippedTop = uint32(top - start), .skipBottom = !isBottom, }); } if (isTop && st.header > 0) { if (_p) { const auto font = _t->_st->font->monospace(); const auto topleft = rect.topLeft(); const auto position = topleft + st.headerPosition; const auto lbaseline = position + QPoint(0, font->ascent); _p->setFont(font); _p->setPen(_palette->monoFg->p); _p->drawText(lbaseline, _t->quoteHeaderText(_quote)); } else if (_lookupX >= left && _lookupX < left + _startLineWidth && _lookupY >= top && _lookupY < top + st.header) { if (_lookupLink) { _lookupResult.link = _quote->copy; } if (_lookupSymbol) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; } } } } _quoteLineTop = _y + _lineHeight + paddingBottom; } StateResult Renderer::getState( QPoint point, GeometryDescriptor geometry, StateRequest request) { if (_t->isEmpty() || point.y() < 0) { return {}; } _lookupRequest = request; _lookupX = point.x(); _lookupY = point.y(); _lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol); _lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink); if (!_lookupSymbol && _lookupX < 0) { return {}; } _geometry = std::move(geometry); _breakEverywhere = _geometry.breakEverywhere; _yFrom = _lookupY; _yTo = _lookupY + 1; _align = _lookupRequest.align; enumerate(); return _lookupResult; } crl::time Renderer::now() const { if (!_cachedNow) { _cachedNow = crl::now(); } return _cachedNow; } void Renderer::initNextParagraph( String::TextBlocks::const_iterator i, int16 paragraphIndex, Qt::LayoutDirection direction) { _paragraphDirection = (direction == Qt::LayoutDirectionAuto) ? style::LayoutDirection() : direction; _paragraphStartBlock = i; if (_quoteIndex != paragraphIndex) { _y += _quotePadding.bottom(); _quoteIndex = paragraphIndex; _quote = _t->quoteByIndex(paragraphIndex); _quotePadding = _t->quotePadding(_quote); _quoteTop = _quoteLineTop = _y; _y += _quotePadding.top(); _quotePadding.setTop(0); _quoteDirection = _paragraphDirection; } const auto e = _t->_blocks.cend(); if (i == e) { _lineStart = _paragraphStart = _t->_text.size(); _lineStartBlock = _t->_blocks.size(); _paragraphLength = 0; } else { _lineStart = _paragraphStart = (*i)->position(); _lineStartBlock = i - _t->_blocks.cbegin(); for (; i != e; ++i) { if ((*i)->type() == TextBlockType::Newline) { break; } } _paragraphLength = ((i == e) ? _t->_text.size() : (*i)->position()) - _paragraphStart; } _paragraphAnalysis.resize(0); initNextLine(); } void Renderer::initNextLine() { const auto line = _geometry.layout(_lineIndex++); _x = _startLeft + line.left + _quotePadding.left(); _startLineWidth = line.width; _quoteShift = 0; if (_quote && _quote->maxWidth < _startLineWidth) { const auto delta = _startLineWidth - _quote->maxWidth; _startLineWidth = _quote->maxWidth; if (_align & Qt::AlignHCenter) { _quoteShift = delta / 2; } else if (((_align & Qt::AlignLeft) && (_quoteDirection == Qt::RightToLeft)) || ((_align & Qt::AlignRight) && (_quoteDirection == Qt::LeftToRight))) { _quoteShift = delta; } _x += _quoteShift; } _lineWidth = _startLineWidth - _quotePadding.left() - _quotePadding.right(); _wLeft = _lineWidth; _elidedLine = line.elided; } void Renderer::initParagraphBidi() { if (!_paragraphLength || !_paragraphAnalysis.isEmpty()) { return; } String::TextBlocks::const_iterator i = _paragraphStartBlock, e = _t->_blocks.cend(), n = i + 1; bool ignore = false; bool rtl = (_paragraphDirection == Qt::RightToLeft); if (!ignore && !rtl) { ignore = true; const ushort *start = reinterpret_cast(_str) + _paragraphStart; const ushort *curr = start; const ushort *end = start + _paragraphLength; while (curr < end) { while (n != e && (*n)->position() <= _paragraphStart + (curr - start)) { i = n; ++n; } const auto type = (*i)->type(); if (type != TextBlockType::Emoji && type != TextBlockType::CustomEmoji && *curr >= 0x590) { ignore = false; break; } ++curr; } } _paragraphAnalysis.resize(_paragraphLength); QScriptAnalysis *analysis = _paragraphAnalysis.data(); BidiControl control(rtl); _paragraphHasBidi = false; if (ignore) { memset(analysis, 0, _paragraphLength * sizeof(QScriptAnalysis)); if (rtl) { for (int i = 0; i < _paragraphLength; ++i) analysis[i].bidiLevel = 1; _paragraphHasBidi = true; } } else { _paragraphHasBidi = eBidiItemize(analysis, control); } } 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 (_y + _yDelta + _fontHeight <= _yFrom) { if (_lookupSymbol) { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; } return !_elidedLine; } // Trimming pending spaces, because they sometimes don't fit on the line. // They also are not counted in the line width, they're in the right padding. // Line width is a sum of block / word widths and paddings between them, without trailing one. auto trimmedLineEnd = _lineEnd; for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { auto ch = _t->_text[trimmedLineEnd - 1]; if (ch != QChar::Space && ch != QChar::LineFeed) { break; } } auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get(); 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() == TextBlockType::Skip) { _endBlock = nullptr; } if (!_endBlock) { _elidedLine = false; } } auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; const auto extendLeft = (currentBlock->position() < _lineStart) ? qMin(_lineStart - currentBlock->position(), 2) : 0; _localFrom = _lineStart - extendLeft; const auto extendedLineEnd = (_endBlock && _endBlock->position() < trimmedLineEnd && !_elidedLine) ? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end)) : trimmedLineEnd; auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom); auto lineStart = extendLeft; auto lineLength = trimmedLineEnd - _lineStart; if (_elidedLine) { initParagraphBidi(); prepareElidedLine(lineText, lineStart, lineLength, _endBlock); } auto x = _x; if (_align & Qt::AlignHCenter) { x += (_wLeft / 2).toInt(); } else if (((_align & Qt::AlignLeft) && (_paragraphDirection == Qt::RightToLeft)) || ((_align & Qt::AlignRight) && (_paragraphDirection == Qt::LeftToRight))) { x += _wLeft; } if (!_p) { if (_lookupX < x) { if (_lookupSymbol) { if (_paragraphDirection == 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() != TextBlockType::Skip)) ? true : false; } else { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false; } } if (_lookupLink) { _lookupResult.link = nullptr; } _lookupResult.uponSymbol = false; return false; } else if (_lookupX >= x + (_lineWidth - _wLeft)) { if (_paragraphDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false; } else { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockType::Skip)) ? true : false; } if (_lookupLink) { _lookupResult.link = nullptr; } _lookupResult.uponSymbol = false; return false; } } if (_fullWidthSelection) { const auto selectFromStart = (_selection.to > _lineStart) && (_lineStart > 0) && (_selection.from <= _lineStart); const auto selectTillEnd = (_selection.to > trimmedLineEnd) && (trimmedLineEnd < _t->_text.size()) && (_selection.from <= trimmedLineEnd) && (!_endBlock || _endBlock->type() != TextBlockType::Skip); if ((selectFromStart && _paragraphDirection == Qt::LeftToRight) || (selectTillEnd && _paragraphDirection == Qt::RightToLeft)) { if (x > _x) { fillSelectRange({ _x, x }); } } if ((selectTillEnd && _paragraphDirection == Qt::LeftToRight) || (selectFromStart && _paragraphDirection == Qt::RightToLeft)) { if (x < _x + _wLeft) { fillSelectRange({ x + _lineWidth - _wLeft, _x + _lineWidth }); } } } if (trimmedLineEnd == _lineStart && !_elidedLine) { return true; } if (!_elidedLine) { initParagraphBidi(); // if was not inited } _f = _t->_st->font; QStackTextEngine engine(lineText, _f->f); engine.option.setTextDirection(_paragraphDirection); _e = &engine; eItemize(); QScriptLine line; line.from = lineStart; line.length = lineLength; eShapeLine(line); int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0; if (!nItems) { return !_elidedLine; } int skipIndex = -1; QVarLengthArray visualOrder(nItems); QVarLengthArray levels(nItems); for (int i = 0; i < nItems; ++i) { auto &si = engine.layoutData->items[firstItem + i]; while (nextBlock && nextBlock->position() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); 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 == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji || _type == TextBlockType::Skip) { si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->position() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding()); } } } QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); if (style::RightToLeft() && skipIndex == nItems - 1) { for (int32 i = nItems; i > 1;) { --i; visualOrder[i] = visualOrder[i - 1]; } visualOrder[0] = skipIndex; } blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2; applyBlockProperties(currentBlock); for (int i = 0; i < nItems; ++i) { const auto item = firstItem + visualOrder[i]; const auto isLastItem = (item == lastItem); const auto &si = engine.layoutData->items.at(item); const auto rtl = (si.analysis.bidiLevel % 2); 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->position() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; applyBlockProperties(currentBlock); } if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { TextBlockType _type = currentBlock->type(); if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest if (_lookupLink) { if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { if (const auto link = lookupLink(currentBlock)) { _lookupResult.link = link; } } } if (_type != TextBlockType::Skip) { _lookupResult.uponSymbol = true; } if (_lookupSymbol) { if (_type == TextBlockType::Skip) { if (_paragraphDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; } else { _lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false; } return false; } // Emoji with spaces after symbol lookup auto chFrom = _str + currentBlock->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) { ++spacesCount; --chTo; } if (spacesCount > 0) { // Check if we're over a space. if (rtl) { if (_lookupX < x + spacesWidth) { _lookupResult.symbol = (chTo - _str); // up to a space, included, rtl _lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false; return false; } } else if (_lookupX >= x + si.width - spacesWidth) { _lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr _lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false; return false; } } if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) { _lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str; _lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false; } else { _lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str; _lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true; } } return false; } else if (_p && (_type == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji)) { auto glyphX = x; auto spacesWidth = (si.width - currentBlock->f_width()); if (rtl) { glyphX += spacesWidth; } const auto fillSelect = _background.selectActiveBlock ? FixedRange{ x, x + si.width } : findSelectEmojiRange( si, currentBlock, nextBlock, x, glyphX, _selection); fillSelectRange(fillSelect); if (_highlight) { pushHighlightRange(findSelectEmojiRange( si, currentBlock, nextBlock, x, glyphX, _highlight->range)); } const auto hasSpoiler = _background.spoiler && (_spoilerOpacity > 0.); const auto fillSpoiler = hasSpoiler ? FixedRange{ x, x + si.width } : FixedRange(); const auto opacity = _p->opacity(); if (!hasSpoiler || _spoilerOpacity < 1.) { if (hasSpoiler) { _p->setOpacity(opacity * (1. - _spoilerOpacity)); } const auto x = (glyphX + st::emojiPadding).toInt(); const auto y = _y + _yDelta + emojiY; if (_type == TextBlockType::Emoji) { Emoji::Draw( *_p, static_cast(currentBlock)->_emoji, Emoji::GetSizeNormal(), x, y); } else if (const auto custom = static_cast(currentBlock)->_custom.get()) { const auto selected = (fillSelect.from <= glyphX) && (fillSelect.till > glyphX); const auto color = (selected ? _currentPenSelected : _currentPen)->color(); if (!_customEmojiContext) { _customEmojiContext = CustomEmoji::Context{ .textColor = color, .now = now(), .paused = _pausedEmoji, }; _customEmojiSkip = (st::emojiSize - AdjustCustomEmojiSize(st::emojiSize)) / 2; } else { _customEmojiContext->textColor = color; } _customEmojiContext->position = { x + _customEmojiSkip, y + _customEmojiSkip, }; custom->paint(*_p, *_customEmojiContext); } if (hasSpoiler) { _p->setOpacity(opacity); } } if (hasSpoiler) { // Elided item should be a text item // with '...' at the end, so this should not be it. const auto isElidedItem = false; pushSpoilerRange(fillSpoiler, fillSelect, isElidedItem); } //} else if (_p && currentBlock->type() == TextBlockSkip) { // debug // _p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast(currentBlock)->height()), QColor(0, 0, 0, 32)); } x += si.width; continue; } unsigned short *logClusters = engine.logClusters(&si); QGlyphLayout glyphs = engine.shapedGlyphs(&si); int itemStart = qMax(line.from, si.position), itemEnd; int itemLength = engine.length(item); int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; if (line.from + line.length < si.position + itemLength) { itemEnd = line.from + line.length; glyphsEnd = logClusters[itemEnd - si.position]; } else { itemEnd = si.position + itemLength; glyphsEnd = si.num_glyphs; } QFixed itemWidth = 0; for (int g = glyphsStart; g < glyphsEnd; ++g) itemWidth += glyphs.effectiveAdvance(g); if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest if (_lookupLink) { if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { if (const auto link = lookupLink(currentBlock)) { _lookupResult.link = link; } } } _lookupResult.uponSymbol = true; if (_lookupSymbol) { QFixed tmpx = rtl ? (x + itemWidth) : x; for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) { g = logClusters[itemStart - si.position + ch]; QFixed gwidth = glyphs.effectiveAdvance(g); // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes int ch2 = ch + 1; while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { ++ch2; } for (int charsCount = (ch2 - ch); ch < ch2; ++ch) { QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount), shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount); if ((rtl && _lookupX >= tmpx - shift1) || (!rtl && _lookupX < tmpx + shift1)) { _lookupResult.symbol = _localFrom + itemStart + ch; if ((rtl && _lookupX >= tmpx - shift2) || (!rtl && _lookupX < tmpx + shift2)) { _lookupResult.afterSymbol = false; } else { _lookupResult.afterSymbol = true; } return false; } } if (rtl) { tmpx -= gwidth; } else { tmpx += gwidth; } } if (itemEnd > itemStart) { _lookupResult.symbol = _localFrom + itemEnd - 1; _lookupResult.afterSymbol = true; } else { _lookupResult.symbol = _localFrom + itemStart; _lookupResult.afterSymbol = false; } } return false; } else if (_p) { QTextItemInt gf; gf.glyphs = glyphs.mid(glyphsStart, glyphsEnd - glyphsStart); gf.f = &_e->fnt; gf.chars = engine.layoutData->string.unicode() + itemStart; gf.num_chars = itemEnd - itemStart; gf.fontEngine = engine.fontEngine(si); gf.logClusters = logClusters + itemStart - si.position; gf.width = itemWidth; gf.justified = false; InitTextItemWithScriptItem(gf, si); const auto itemRange = FixedRange{ x, x + itemWidth }; auto selectedRect = QRect(); auto fillSelect = itemRange; if (!_background.selectActiveBlock) { fillSelect = findSelectTextRange( si, itemStart, itemEnd, x, itemWidth, gf, _selection); const auto from = fillSelect.from.toInt(); selectedRect = QRect( from, _y + _yDelta, fillSelect.till.toInt() - from, _fontHeight); } const auto hasSelected = !fillSelect.empty(); const auto hasNotSelected = (fillSelect.from != itemRange.from) || (fillSelect.till != itemRange.till); fillSelectRange(fillSelect); if (_highlight) { pushHighlightRange(findSelectTextRange( si, itemStart, itemEnd, x, itemWidth, gf, _highlight->range)); } const auto hasSpoiler = _background.spoiler && (_spoilerOpacity > 0.); const auto opacity = _p->opacity(); const auto isElidedItem = (_indexOfElidedBlock == blockIndex) && isLastItem; const auto complexClipping = hasSpoiler && isElidedItem && (_spoilerOpacity == 1.); if (!hasSpoiler || (_spoilerOpacity < 1.) || isElidedItem) { const auto complexClippingEnabled = complexClipping && _p->hasClipping(); const auto complexClippingRegion = complexClipping ? _p->clipRegion() : QRegion(); if (complexClipping) { const auto elided = (_indexOfElidedBlock == blockIndex) ? _f->elidew : 0; _p->setClipRect( QRect( (x + itemWidth).toInt() - elided, _y - _lineHeight, elided, _y + 2 * _lineHeight), Qt::IntersectClip); } else if (hasSpoiler && !isElidedItem) { _p->setOpacity(opacity * (1. - _spoilerOpacity)); } if (Q_UNLIKELY(hasSelected)) { if (Q_UNLIKELY(hasNotSelected)) { // There is a bug in retina QPainter clipping stack. // You can see glitches in rendering in such text: // aA // Aa // Where selection is both 'A'-s. // I can't debug it right now, this is a workaround. #ifdef Q_OS_MAC _p->save(); #endif // Q_OS_MAC const auto clippingEnabled = _p->hasClipping(); const auto clippingRegion = _p->clipRegion(); _p->setClipRect(selectedRect, Qt::IntersectClip); _p->setPen(*_currentPenSelected); _p->drawTextItem(QPointF(x.toReal(), textY), gf); const auto externalClipping = clippingEnabled ? clippingRegion : QRegion(QRect( (_x - _lineWidth).toInt(), _y - _lineHeight, (_x + 2 * _lineWidth).toInt(), _y + 2 * _lineHeight)); _p->setClipRegion(externalClipping - selectedRect); _p->setPen(*_currentPen); _p->drawTextItem(QPointF(x.toReal(), textY), gf); #ifdef Q_OS_MAC _p->restore(); #else // Q_OS_MAC if (clippingEnabled) { _p->setClipRegion(clippingRegion); } else { _p->setClipping(false); } #endif // Q_OS_MAC } else { _p->setPen(*_currentPenSelected); _p->drawTextItem(QPointF(x.toReal(), textY), gf); } } else { _p->setPen(*_currentPen); _p->drawTextItem(QPointF(x.toReal(), textY), gf); } if (complexClipping) { if (complexClippingEnabled) { _p->setClipRegion(complexClippingRegion); } else { _p->setClipping(false); } } else if (hasSpoiler && !isElidedItem) { _p->setOpacity(opacity); } } if (hasSpoiler) { pushSpoilerRange(itemRange, fillSelect, isElidedItem); } } x += itemWidth; } fillRectsFromRanges(); return !_elidedLine; } FixedRange Renderer::findSelectEmojiRange( const QScriptItem &si, const Ui::Text::AbstractBlock *currentBlock, const Ui::Text::AbstractBlock *nextBlock, QFixed x, QFixed glyphX, TextSelection selection) const { if (_localFrom + si.position >= selection.to) { return {}; } 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)) { return { x, x + si.width }; } else { // or with space return { glyphX, glyphX + currentBlock->f_width() }; } } else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= selection.from) { const auto rtl = (si.analysis.bidiLevel % 2); if (rtl) { // rtl space only return { x, glyphX }; } else { // ltr space only return { x + currentBlock->f_width(), x + si.width }; } } return {}; } FixedRange Renderer::findSelectTextRange( const QScriptItem &si, int itemStart, int itemEnd, QFixed x, QFixed itemWidth, const QTextItemInt &gf, TextSelection selection) const { if (_localFrom + itemStart >= selection.to || _localFrom + itemEnd <= selection.from) { return {}; } auto selX = x; auto selWidth = itemWidth; const auto rtl = (si.analysis.bidiLevel % 2); if (_localFrom + itemStart < selection.from || _localFrom + itemEnd > selection.to) { selWidth = 0; const auto itemL = itemEnd - itemStart; const auto selStart = std::max( selection.from - (_localFrom + itemStart), 0); const auto selEnd = std::min( selection.to - (_localFrom + itemStart), itemL); const auto lczero = gf.logClusters[0]; for (int ch = 0, g; ch < selEnd;) { g = gf.logClusters[ch]; const auto gwidth = gf.glyphs.effectiveAdvance(g - lczero); // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes int ch2 = ch + 1; while ((ch2 < itemL) && (g == gf.logClusters[ch2])) { ++ch2; } if (ch2 <= selStart) { selX += gwidth; } else if (ch >= selStart && ch2 <= selEnd) { selWidth += gwidth; } else { int sStart = ch, sEnd = ch2; if (ch < selStart) { sStart = selStart; selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch); } if (ch2 >= selEnd) { sEnd = selEnd; selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); break; } selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); } ch = ch2; } } if (rtl) selX = x + itemWidth - (selX - x) - selWidth; return { selX, selX + selWidth }; } void Renderer::fillSelectRange(FixedRange range) { if (range.empty()) { return; } const auto left = range.from.toInt(); const auto width = range.till.toInt() - left; _p->fillRect(left, _y + _yDelta, width, _fontHeight, _palette->selectBg); } void Renderer::pushHighlightRange(FixedRange range) { if (range.empty()) { return; } AppendRange(_highlightRanges, range); } void Renderer::pushSpoilerRange( FixedRange range, FixedRange selected, bool isElidedItem) { if (!_background.spoiler || !_spoiler) { return; } const auto elided = isElidedItem ? _f->elidew : 0; range.till -= elided; if (range.empty()) { return; } else if (selected.empty() || !Intersects(range, selected)) { AppendRange(_spoilerRanges, range); } else { AppendRange(_spoilerRanges, { range.from, selected.from }); AppendRange(_spoilerSelectedRanges, Intersected(range, selected)); AppendRange(_spoilerRanges, { selected.till, range.till }); } } void Renderer::fillRectsFromRanges() { fillRectsFromRanges(_spoilerRects, _spoilerRanges); fillRectsFromRanges(_spoilerSelectedRects, _spoilerSelectedRanges); fillRectsFromRanges(_highlightRects, _highlightRanges); } void Renderer::fillRectsFromRanges( QVarLengthArray &rects, QVarLengthArray &ranges) { if (ranges.empty()) { return; } auto lastTill = ranges.front().from.toInt() - 1; const auto y = _y + _yDelta; for (const auto &range : ranges) { auto from = range.from.toInt(); auto till = range.till.toInt(); if (from <= lastTill) { auto &last = rects.back(); from = std::min(from, last.x()); till = std::max(till, last.x() + last.width()); last = { from, y, till - from, _fontHeight }; } else { rects.push_back({ from, y, till - from, _fontHeight }); } lastTill = till; } ranges.clear(); } void Renderer::paintSpoilerRects() { Expects(_p != nullptr); if (!_spoiler) { return; } const auto opacity = _p->opacity(); if (_spoilerOpacity < 1.) { _p->setOpacity(opacity * _spoilerOpacity); } const auto index = _spoiler->animation.index(now(), _pausedSpoiler); paintSpoilerRects( _spoilerRects, _palette->spoilerFg, index); paintSpoilerRects( _spoilerSelectedRects, _palette->selectSpoilerFg, index); if (_spoilerOpacity < 1.) { _p->setOpacity(opacity); } } void Renderer::paintSpoilerRects( const QVarLengthArray &rects, const style::color &color, int index) { if (rects.empty()) { return; } const auto frame = _spoilerCache->lookup(color->c)->frame(index); if (_spoilerCache) { for (const auto &rect : rects) { Ui::FillSpoilerRect(*_p, rect, frame, -rect.topLeft()); } } else { // Show forgotten spoiler context part. for (const auto &rect : rects) { _p->fillRect(rect, Qt::red); } } } void Renderer::composeHighlightPath() { Expects(_highlight != nullptr); Expects(_highlight->outPath != nullptr); if (_highlight->interpolateProgress >= 1.) { _highlight->outPath->addRect(_highlight->interpolateTo); } else if (_highlight->interpolateProgress <= 0.) { for (const auto &rect : _highlightRects) { _highlight->outPath->addRect(rect); } } else { const auto to = _highlight->interpolateTo; const auto progress = _highlight->interpolateProgress; const auto lerp = [=](int from, int to) { return from + (to - from) * progress; }; for (const auto &rect : _highlightRects) { _highlight->outPath->addRect( lerp(rect.x(), to.x()), lerp(rect.y(), to.y()), lerp(rect.width(), to.width()), lerp(rect.height(), to.height())); } } } void Renderer::elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { if (_elideSavedBlock) { restoreAfterElided(); } _elideSavedIndex = blockIndex; auto mutableText = const_cast(_t); _elideSavedBlock = std::move(mutableText->_blocks[blockIndex]); mutableText->_blocks[blockIndex] = 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; } void Renderer::setElideBidi(int32 elideStart, int32 elideLen) { int32 newParLength = elideStart + elideLen - _paragraphStart; if (newParLength > _paragraphAnalysis.size()) { _paragraphAnalysis.resize(newParLength); } for (int32 i = elideLen; i > 0; --i) { _paragraphAnalysis[newParLength - i].bidiLevel = (_paragraphDirection == Qt::RightToLeft) ? 1 : 0; } } void Renderer::prepareElidedLine( QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat) { _f = _t->_st->font; QStackTextEngine engine(lineText, _f->f); engine.option.setTextDirection(_paragraphDirection); _e = &engine; eItemize(); auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; QScriptLine line; line.from = lineStart; line.length = lineLength; eShapeLine(line); auto elideWidth = _f->elidew; _wLeft = _lineWidth - _quotePadding.left() - _quotePadding.right() - 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->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 == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji || _type == TextBlockType::Skip) { si.width = currentBlock->f_width() + currentBlock->f_rpadding(); } } if (_type == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji || _type == TextBlockType::Skip || _type == TextBlockType::Newline) { if (_wLeft < si.width) { 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->position(), kQEllipsis.size()); elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->position(), elideWidth); return; } _wLeft -= si.width; } else if (_type == TextBlockType::Text) { unsigned short *logClusters = engine.logClusters(&si); QGlyphLayout glyphs = engine.shapedGlyphs(&si); int itemStart = qMax(line.from, si.position), itemEnd; int itemLength = engine.length(firstItem + i); int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; if (line.from + line.length < si.position + itemLength) { itemEnd = line.from + line.length; glyphsEnd = logClusters[itemEnd - si.position]; } else { itemEnd = si.position + itemLength; glyphsEnd = si.num_glyphs; } for (auto g = glyphsStart; g < glyphsEnd; ++g) { auto adv = glyphs.effectiveAdvance(g); if (_wLeft < adv) { auto pos = itemStart; while (pos < itemEnd && logClusters[pos - si.position] < g) { ++pos; } if (lineText.size() <= pos || repeat > 3) { lineText += kQEllipsis; lineLength = _localFrom + pos + kQEllipsis.size() - _lineStart; _selection.to = qMin(_selection.to, uint16(_localFrom + pos)); _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); setElideBidi(_localFrom + pos, kQEllipsis.size()); _blocksSize = blockIndex; _endBlock = nextBlock; } else { lineText = lineText.mid(0, pos); lineLength = _localFrom + pos - _lineStart; _blocksSize = blockIndex; _endBlock = nextBlock; prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1); } return; } else { _wLeft -= adv; } } } } int32 elideStart = _localFrom + lineText.size(); _selection.to = qMin(_selection.to, uint16(elideStart)); _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); setElideBidi(elideStart, kQEllipsis.size()); lineText += kQEllipsis; lineLength += kQEllipsis.size(); if (!repeat) { for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->position() < elideStart; ++blockIndex) { } if (blockIndex < _blocksSize) { elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth); } } } void Renderer::restoreAfterElided() { if (_elideSavedBlock) { const_cast(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock); } } // COPIED FROM qtextengine.cpp AND MODIFIED void Renderer::eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) { if (start > stop) return; int level = control.level; if(dir != QChar::DirON && !control.override) { // add level of run (cases I1 & I2) if(level % 2) { if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN) level++; } else { if(dir == QChar::DirR) level++; else if(dir == QChar::DirAN || dir == QChar::DirEN) level += 2; } } QScriptAnalysis *s = analysis + start; const QScriptAnalysis *e = analysis + stop; while (s <= e) { s->bidiLevel = level; ++s; } ++stop; start = stop; } void Renderer::eShapeLine(const QScriptLine &line) { int item = _e->findItem(line.from); 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; eSetFont(currentBlock); for (; item <= end; ++item) { QScriptItem &si = _e->layoutData->items[item]; while (nextBlock && nextBlock->position() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; eSetFont(currentBlock); } _e->shape(item); } } void Renderer::eSetFont(const AbstractBlock *block) { const auto flags = block->flags(); const auto usedFont = [&] { if (const auto index = block->linkIndex()) { const auto underline = _t->_st->linkUnderline; const auto underlined = (underline == st::kLinkUnderlineNever) ? false : (underline == st::kLinkUnderlineActive) ? ((_palette && _palette->linkAlwaysActive) || ClickHandler::showAsActive(_t->_extended ? _t->_extended->links[index - 1] : nullptr)) : true; return underlined ? _t->_st->font->underline() : _t->_st->font; } return _t->_st->font; }(); const auto newFont = WithFlags(usedFont, flags); if (newFont != _f) { _f = (newFont->family() == _t->_st->font->family()) ? WithFlags(_t->_st->font, flags, newFont->flags()) : newFont; _e->fnt = _f->f; _e->resetFontEngineCache(); } } void Renderer::eItemize() { _e->validate(); if (_e->layoutData->items.size()) return; int length = _e->layoutData->string.length(); if (!length) return; const ushort *string = reinterpret_cast(_e->layoutData->string.unicode()); auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; _e->layoutData->hasBidi = _paragraphHasBidi; auto analysis = _paragraphAnalysis.data() + (_localFrom - _paragraphStart); { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QUnicodeTools::ScriptItemArray scriptItems; QUnicodeTools::initScripts(_e->layoutData->string, &scriptItems); for (int i = 0; i < scriptItems.length(); ++i) { const auto &item = scriptItems.at(i); int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length; for (int j = item.position; j < end; ++j) analysis[j].script = item.script; } #else // Qt >= 6.0.0 QVarLengthArray scripts(length); QUnicodeTools::initScripts(string, length, scripts.data()); for (int i = 0; i < length; ++i) analysis[i].script = scripts.at(i); #endif // Qt < 6.0.0 } blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; auto start = string; auto end = start + length; while (start < end) { while (nextBlock && nextBlock->position() <= _localFrom + (start - string)) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); if (_type == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji || _type == TextBlockType::Skip) { analysis->script = QChar::Script_Common; analysis->flags = QScriptAnalysis::Object; } else { analysis->flags = QScriptAnalysis::None; } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior #endif // Qt < 6.0.0 ++start; ++analysis; } { auto i_string = &_e->layoutData->string; auto i_analysis = _paragraphAnalysis.data() + (_localFrom - _paragraphStart); auto i_items = &_e->layoutData->items; blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; auto startBlock = currentBlock; if (!length) { return; } auto start = 0; auto end = start + length; for (int i = start + 1; i < end; ++i) { while (nextBlock && nextBlock->position() <= _localFrom + i) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } // According to the unicode spec we should be treating characters in the Common script // (punctuation, spaces, etc) as being the same script as the surrounding text for the // purpose of splitting up text. This is important because, for example, a fullstop // (0x2E) can be used to indicate an abbreviation and so must be treated as part of a // word. Thus it must be passed along with the word in languages that have to calculate // word breaks. For example the thai word "[lookup-in-git]." has no word breaks // but the word "[lookup-too]" does. // Unfortuntely because we split up the strings for both wordwrapping and for setting // the font and because Japanese and Chinese are also aliases of the script "Common", // doing this would break too many things. So instead we only pass the full stop // along, and nothing else. if (currentBlock == startBlock && i_analysis[i].bidiLevel == i_analysis[start].bidiLevel && i_analysis[i].flags == i_analysis[start].flags && (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.')) // && i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs && i - start < _MaxItemLength) continue; i_items->append(QScriptItem(start, i_analysis[start])); start = i; startBlock = currentBlock; } i_items->append(QScriptItem(start, i_analysis[start])); } } QChar::Direction 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 <= _paragraphLength) { while (i != _paragraphStartBlock && (*i)->position() > _paragraphStart + sor) { n = i; --i; } while (n != e && (*n)->position() <= _paragraphStart + sor) { i = n; ++n; } TextBlockType _itype = (*i)->type(); if (eor == _paragraphLength) dir = control.basicDirection(); else if (_itype == TextBlockType::Emoji || _itype == TextBlockType::CustomEmoji) dir = QChar::DirCS; else if (_itype == TextBlockType::Skip) dir = QChar::DirCS; else dir = QChar::direction(unicode[sor]); // Keep skipping DirBN as if it doesn't exist if (dir != QChar::DirBN) break; analysis[sor++].bidiLevel = level; } eor = sor; return dir; } // creates the next QScript items. bool Renderer::eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) { bool rightToLeft = (control.basicDirection() == 1); bool hasBidi = rightToLeft; int sor = 0; int eor = -1; const ushort *unicode = reinterpret_cast(_t->_text.unicode()) + _paragraphStart; int current = 0; QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL; BidiStatus status; String::TextBlocks::const_iterator i = _paragraphStartBlock, e = _t->_blocks.cend(), n = i + 1; QChar::Direction sdir; TextBlockType _stype = (*_paragraphStartBlock)->type(); if (_stype == TextBlockType::Emoji || _stype == TextBlockType::CustomEmoji) sdir = QChar::DirCS; else if (_stype == TextBlockType::Skip) sdir = QChar::DirCS; else sdir = QChar::direction(*unicode); if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN) sdir = QChar::DirON; else dir = QChar::DirON; status.eor = sdir; status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL; status.last = status.lastStrong; status.dir = sdir; while (current <= _paragraphLength) { while (n != e && (*n)->position() <= _paragraphStart + current) { i = n; ++n; } QChar::Direction dirCurrent; TextBlockType _itype = (*i)->type(); if (current == (int)_paragraphLength) dirCurrent = control.basicDirection(); else if (_itype == TextBlockType::Emoji || _itype == TextBlockType::CustomEmoji) dirCurrent = QChar::DirCS; else if (_itype == TextBlockType::Skip) dirCurrent = QChar::DirCS; else dirCurrent = QChar::direction(unicode[current]); switch (dirCurrent) { // embedding and overrides (X1-X9 in the BiDi specs) case QChar::DirRLE: case QChar::DirRLO: case QChar::DirLRE: case QChar::DirLRO: { bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO); hasBidi |= rtl; bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO); unsigned int level = control.level + 1; if ((level % 2 != 0) == rtl) ++level; if (level < _MaxBidiLevel) { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); eor = current; control.embed(rtl, override); QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL); dir = status.eor = edir; status.lastStrong = edir; } break; } case QChar::DirPDF: { if (control.canPop()) { if (dir != control.direction()) { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); dir = control.direction(); } eor = current; eAppendItems(analysis, sor, eor, control, dir); control.pdf(); dir = QChar::DirON; status.eor = QChar::DirON; status.last = control.direction(); if (control.override) dir = control.direction(); else dir = QChar::DirON; status.lastStrong = control.direction(); } break; } // strong types case QChar::DirL: if (dir == QChar::DirON) dir = QChar::DirL; switch (status.last) { case QChar::DirL: eor = current; status.eor = QChar::DirL; break; case QChar::DirR: case QChar::DirAL: case QChar::DirEN: case QChar::DirAN: if (eor >= 0) { eAppendItems(analysis, sor, eor, control, dir); status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); } else { eor = current; status.eor = dir; } break; case QChar::DirES: case QChar::DirET: case QChar::DirCS: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (dir != QChar::DirL) { //last stuff takes embedding dir if (control.direction() == QChar::DirR) { if (status.eor != QChar::DirR) { // AN or EN eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirR; } eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); } else { if (status.eor != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirL; } else { eor = current; status.eor = QChar::DirL; break; } } } else { eor = current; status.eor = QChar::DirL; } default: break; } status.lastStrong = QChar::DirL; break; case QChar::DirAL: case QChar::DirR: hasBidi = true; if (dir == QChar::DirON) dir = QChar::DirR; switch (status.last) { case QChar::DirL: case QChar::DirEN: case QChar::DirAN: if (eor >= 0) eAppendItems(analysis, sor, eor, control, dir); // fall through case QChar::DirR: case QChar::DirAL: dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break; case QChar::DirES: case QChar::DirET: case QChar::DirCS: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (status.eor != QChar::DirR && status.eor != QChar::DirAL) { //last stuff takes embedding dir if (control.direction() == QChar::DirR || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { eAppendItems(analysis, sor, eor, control, dir); dir = QChar::DirR; status.eor = QChar::DirON; eor = current; } else { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); dir = QChar::DirR; status.eor = QChar::DirON; } } else { eor = current; status.eor = QChar::DirR; } default: break; } status.lastStrong = dirCurrent; break; // weak types: case QChar::DirNSM: if (eor == current - 1) eor = current; break; case QChar::DirEN: // if last strong was AL change EN to AN if (status.lastStrong != QChar::DirAL) { if (dir == QChar::DirON) { if (status.lastStrong == QChar::DirL) dir = QChar::DirL; else dir = QChar::DirEN; } switch (status.last) { case QChar::DirET: if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirAN; } [[fallthrough]]; case QChar::DirEN: case QChar::DirL: eor = current; status.eor = dirCurrent; break; case QChar::DirR: case QChar::DirAL: case QChar::DirAN: if (eor >= 0) eAppendItems(analysis, sor, eor, control, dir); else eor = current; status.eor = QChar::DirEN; dir = QChar::DirAN; break; case QChar::DirES: case QChar::DirCS: if (status.eor == QChar::DirEN || dir == QChar::DirAN) { eor = current; break; } [[fallthrough]]; case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (status.eor == QChar::DirR) { // neutrals go to R eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirEN; dir = QChar::DirAN; } else if (status.eor == QChar::DirL || (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { eor = current; status.eor = dirCurrent; } else { // numbers on both sides, neutrals get right to left direction if (dir != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; eor = current - 1; dir = QChar::DirR; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirAN; } else { eor = current; status.eor = dirCurrent; } } [[fallthrough]]; default: break; } break; } [[fallthrough]]; case QChar::DirAN: hasBidi = true; dirCurrent = QChar::DirAN; if (dir == QChar::DirON) dir = QChar::DirAN; switch (status.last) { case QChar::DirL: case QChar::DirAN: eor = current; status.eor = QChar::DirAN; break; case QChar::DirR: case QChar::DirAL: case QChar::DirEN: if (eor >= 0) { eAppendItems(analysis, sor, eor, control, dir); } else { eor = current; } dir = QChar::DirAN; status.eor = QChar::DirAN; break; case QChar::DirCS: if (status.eor == QChar::DirAN) { eor = current; break; } [[fallthrough]]; case QChar::DirES: case QChar::DirET: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (status.eor == QChar::DirR) { // neutrals go to R eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirAN; dir = QChar::DirAN; } else if (status.eor == QChar::DirL || (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { eor = current; status.eor = dirCurrent; } else { // numbers on both sides, neutrals get right to left direction if (dir != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; eor = current - 1; dir = QChar::DirR; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirAN; dir = QChar::DirAN; } else { eor = current; status.eor = dirCurrent; } } [[fallthrough]]; default: break; } break; case QChar::DirES: case QChar::DirCS: break; case QChar::DirET: if (status.last == QChar::DirEN) { dirCurrent = QChar::DirEN; eor = current; status.eor = dirCurrent; } break; // boundary neutrals should be ignored case QChar::DirBN: break; // neutrals case QChar::DirB: // ### what do we do with newline and paragraph separators that come to here? break; case QChar::DirS: // ### implement rule L1 break; case QChar::DirWS: case QChar::DirON: break; default: break; } if (current >= (int)_paragraphLength) break; // set status.last as needed. switch (dirCurrent) { case QChar::DirET: case QChar::DirES: case QChar::DirCS: case QChar::DirS: case QChar::DirWS: case QChar::DirON: switch (status.last) { case QChar::DirL: case QChar::DirR: case QChar::DirAL: case QChar::DirEN: case QChar::DirAN: status.last = dirCurrent; break; default: status.last = QChar::DirON; } break; case QChar::DirNSM: case QChar::DirBN: // ignore these break; case QChar::DirLRO: case QChar::DirLRE: status.last = QChar::DirL; break; case QChar::DirRLO: case QChar::DirRLE: status.last = QChar::DirR; break; case QChar::DirEN: if (status.last == QChar::DirL) { status.last = QChar::DirL; break; } [[fallthrough]]; default: status.last = dirCurrent; } ++current; } eor = current - 1; // remove dummy char if (sor <= eor) eAppendItems(analysis, sor, eor, control, dir); return hasBidi; } void Renderer::applyBlockProperties(const AbstractBlock *block) { eSetFont(block); if (_p) { const auto flags = block->flags(); const auto isMono = IsMono(flags); _background = {}; if ((flags & TextBlockFlag::Spoiler) && _spoiler) { _background.spoiler = true; } if (isMono && block->linkIndex() && (!_background.spoiler || _spoiler->revealed)) { const auto pressed = ClickHandler::showAsPressed(_t->_extended ? _t->_extended->links[block->linkIndex() - 1] : nullptr); _background.selectActiveBlock = pressed; } if (const auto color = block->colorIndex()) { if (color == 1) { if (_quote && _quote->blockquote && _quoteBlockquoteCache) { _quoteLinkPenOverride = QPen(_quoteBlockquoteCache->outlines[0]); _currentPen = &_quoteLinkPenOverride; _currentPenSelected = &_quoteLinkPenOverride; } else { _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->linkIndex()) { if (_quote && _quote->blockquote && _quoteBlockquoteCache) { _quoteLinkPenOverride = QPen(_quoteBlockquoteCache->outlines[0]); _currentPen = &_quoteLinkPenOverride; _currentPenSelected = &_quoteLinkPenOverride; } else { _currentPen = &_palette->linkFg->p; _currentPenSelected = &_palette->selectLinkFg->p; } } else { _currentPen = &_originalPen; _currentPenSelected = &_originalPenSelected; } } } ClickHandlerPtr Renderer::lookupLink(const AbstractBlock *block) const { const auto spoilerLink = (_spoiler && !_spoiler->revealed && (block->flags() & TextBlockFlag::Spoiler)) ? _spoiler->link : ClickHandlerPtr(); return (spoilerLink || !block->linkIndex() || !_t->_extended) ? spoilerLink : _t->_extended->links[block->linkIndex() - 1]; } } // namespace Ui::Text