// 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_spoiler_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->_spoiler.data.get()) { } 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; _originalPen = _p->pen(); _originalPenSelected = (_palette->selectFg->c.alphaF() == 0) ? _originalPen : _palette->selectFg->p; _x = context.position.x(); _y = context.position.y(); _yFrom = context.clip.isNull() ? 0 : context.clip.y(); _yTo = context.clip.isNull() ? -1 : (context.clip.y() + context.clip.height()); if (const auto lines = context.elisionLines) { if (context.clip.isNull()) 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; } _spoilerCache = context.spoiler; _selection = context.selection; _fullWidthSelection = context.fullWidthSelection; _w = context.availableWidth; _align = context.align; _cachedNow = context.now; _paused = context.paused; _spoilerOpacity = _spoiler ? (1. - _spoiler->revealAnimation.value( _spoiler->revealed ? 1. : 0.)) : 0.; enumerate(); } 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(); 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(); } } _parDirection = _t->_startDir; if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) { initNextParagraph(_t->_blocks.cbegin()); } _lineStart = 0; _lineStartBlock = 0; _lineHeight = 0; _fontHeight = _t->_st->font->height; auto last_rBearing = QFixed(0); _last_rPadding = QFixed(0); const auto guard = gsl::finally([&] { if (_p) { paintSpoilerRects(); } }); auto blockIndex = 0; bool longWordLine = true; auto e = _t->_blocks.cend(); for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) { auto b = i->get(); auto _btype = b->type(); auto blockHeight = CountBlockHeight(b, _t->_st); if (_btype == TextBlockTNewline) { if (!_lineHeight) _lineHeight = blockHeight; if (!drawLine((*i)->from(), i, e)) { return; } _y += _lineHeight; _lineHeight = 0; _lineStart = _t->countBlockEnd(i, e); _lineStartBlock = blockIndex + 1; last_rBearing = b->f_rbearing(); _last_rPadding = b->f_rpadding(); _wLeft = _w - (b->f_width() - last_rBearing); if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { _wLeft -= _elideRemoveFromEnd; } _parDirection = static_cast(b)->nextDirection(); if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection(); initNextParagraph(i + 1); longWordLine = true; continue; } auto b__f_rbearing = b->f_rbearing(); auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing); if (newWidthLeft >= 0) { last_rBearing = b__f_rbearing; _last_rPadding = b->f_rpadding(); _wLeft = newWidthLeft; _lineHeight = qMax(_lineHeight, blockHeight); longWordLine = false; continue; } if (_btype == TextBlockTText) { auto t = static_cast(b); if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line _last_rPadding += b->f_rpadding(); _lineHeight = qMax(_lineHeight, blockHeight); longWordLine = false; continue; } auto f_wLeft = _wLeft; // vars for saving state of the last word start auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) { auto wordEndsHere = (j->f_width() >= 0); auto j_width = wordEndsHere ? j->f_width() : -j->f_width(); auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing()); if (newWidthLeft >= 0) { last_rBearing = j->f_rbearing(); _last_rPadding = j->f_rpadding(); _wLeft = newWidthLeft; _lineHeight = qMax(_lineHeight, blockHeight); if (wordEndsHere) { longWordLine = false; } if (wordEndsHere || longWordLine) { f = j + 1; f_wLeft = _wLeft; f_lineHeight = _lineHeight; } continue; } auto elidedLineHeight = qMax(_lineHeight, blockHeight); auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); if (elidedLine) { _lineHeight = elidedLineHeight; } else if (f != j && !_breakEverywhere) { // word did not fit completely, so we roll back the state to the beginning of this long word j = f; _wLeft = f_wLeft; _lineHeight = f_lineHeight; j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width(); } if (!drawLine(elidedLine ? ((j + 1 == en) ? _t->countBlockEnd(i, e) : (j + 1)->from()) : j->from(), i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); _lineStart = j->from(); _lineStartBlock = blockIndex; last_rBearing = j->f_rbearing(); _last_rPadding = j->f_rpadding(); _wLeft = _w - (j_width - last_rBearing); if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { _wLeft -= _elideRemoveFromEnd; } longWordLine = !wordEndsHere; f = j + 1; f_wLeft = _wLeft; f_lineHeight = _lineHeight; } continue; } auto elidedLineHeight = qMax(_lineHeight, blockHeight); auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide); if (elidedLine) { _lineHeight = elidedLineHeight; } if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) { return; } _y += _lineHeight; _lineHeight = qMax(0, blockHeight); _lineStart = b->from(); _lineStartBlock = blockIndex; last_rBearing = b__f_rbearing; _last_rPadding = b->f_rpadding(); _wLeft = _w - (b->f_width() - last_rBearing); if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) { _wLeft -= _elideRemoveFromEnd; } longWordLine = true; continue; } if (_lineStart < _t->_text.size()) { if (!drawLine(_t->_text.size(), e, e)) return; } if (!_p && _lookupSymbol) { _lookupResult.symbol = _t->_text.size(); _lookupResult.afterSymbol = false; } } StateResult Renderer::getState(QPoint point, int w, StateRequest request) { if (_t->isEmpty() || point.y() < 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 {}; } _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; _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) { _parStartBlock = i; const auto e = _t->_blocks.cend(); if (i == e) { _parStart = _t->_text.size(); _parLength = 0; } else { _parStart = (*i)->from(); for (; i != e; ++i) { if ((*i)->type() == TextBlockTNewline) { break; } } _parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart; } _parAnalysis.resize(0); } void Renderer::initParagraphBidi() { if (!_parLength || !_parAnalysis.isEmpty()) return; String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; bool ignore = false; bool rtl = (_parDirection == Qt::RightToLeft); if (!ignore && !rtl) { ignore = true; const ushort *start = reinterpret_cast(_str) + _parStart; const ushort *curr = start; const ushort *end = start + _parLength; while (curr < end) { while (n != e && (*n)->from() <= _parStart + (curr - start)) { i = n; ++n; } const auto type = (*i)->type(); if (type != TextBlockTEmoji && type != TextBlockTCustomEmoji && *curr >= 0x590) { ignore = false; break; } ++curr; } } _parAnalysis.resize(_parLength); QScriptAnalysis *analysis = _parAnalysis.data(); BidiControl control(rtl); _parHasBidi = false; if (ignore) { memset(analysis, 0, _parLength * sizeof(QScriptAnalysis)); if (rtl) { for (int i = 0; i < _parLength; ++i) analysis[i].bidiLevel = 1; _parHasBidi = true; } } else { _parHasBidi = eBidiItemize(analysis, control); } } bool 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 true; } // Trimming pending spaces, because they sometimes don't fit on the line. // They also are not counted in the line width, they're in the right padding. // Line width is a sum of block / word widths and paddings between them, without trailing one. auto trimmedLineEnd = _lineEnd; for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) { auto ch = _t->_text[trimmedLineEnd - 1]; if (ch != QChar::Space && ch != QChar::LineFeed) { break; } } auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get(); auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide); if (elidedLine) { // If we decided to draw the last line elided only because of the skip block // that did not fit on this line, we just draw the line till the very end. // Skip block is ignored in the elided lines, instead "removeFromEnd" is used. if (_endBlock && _endBlock->type() == TextBlockTSkip) { _endBlock = nullptr; } if (!_endBlock) { elidedLine = false; } } auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; const auto extendLeft = (currentBlock->from() < _lineStart) ? qMin(_lineStart - currentBlock->from(), 2) : 0; _localFrom = _lineStart - extendLeft; const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine) ? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end)) : trimmedLineEnd; auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom); auto lineStart = extendLeft; auto lineLength = trimmedLineEnd - _lineStart; if (elidedLine) { initParagraphBidi(); prepareElidedLine(lineText, lineStart, lineLength, _endBlock); } auto x = _x; if (_align & Qt::AlignHCenter) { x += (_wLeft / 2).toInt(); } else if (((_align & Qt::AlignLeft) && _parDirection == Qt::RightToLeft) || ((_align & Qt::AlignRight) && _parDirection == Qt::LeftToRight)) { x += _wLeft; } if (!_p) { if (_lookupX < x) { if (_lookupSymbol) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; } else { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; // _lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false; } } if (_lookupLink) { _lookupResult.link = nullptr; } _lookupResult.uponSymbol = false; return false; } else if (_lookupX >= x + (_w - _wLeft)) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false; } else { _lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false; // _lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false; } if (_lookupLink) { _lookupResult.link = nullptr; } _lookupResult.uponSymbol = false; return false; } } if (_fullWidthSelection) { const auto selectFromStart = (_selection.to > _lineStart) && (_lineStart > 0) && (_selection.from <= _lineStart); const auto selectTillEnd = (_selection.to > trimmedLineEnd) && (trimmedLineEnd < _t->_text.size()) && (_selection.from <= trimmedLineEnd) && (!_endBlock || _endBlock->type() != TextBlockTSkip); if ((selectFromStart && _parDirection == Qt::LeftToRight) || (selectTillEnd && _parDirection == Qt::RightToLeft)) { if (x > _x) { fillSelectRange({ _x, x }); } } if ((selectTillEnd && _parDirection == Qt::LeftToRight) || (selectFromStart && _parDirection == Qt::RightToLeft)) { if (x < _x + _wLeft) { fillSelectRange({ x + _w - _wLeft, _x + _w }); } } } if (trimmedLineEnd == _lineStart && !elidedLine) { return true; } if (!elidedLine) initParagraphBidi(); // if was not inited _f = _t->_st->font; QStackTextEngine engine(lineText, _f->f); engine.option.setTextDirection(_parDirection); _e = &engine; eItemize(); QScriptLine line; line.from = lineStart; line.length = lineLength; eShapeLine(line); int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0; if (!nItems) { return true; } int skipIndex = -1; QVarLengthArray visualOrder(nItems); QVarLengthArray levels(nItems); for (int i = 0; i < nItems; ++i) { auto &si = engine.layoutData->items[firstItem + i]; while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); if (_type == TextBlockTSkip) { levels[i] = si.analysis.bidiLevel = 0; skipIndex = i; } else { levels[i] = si.analysis.bidiLevel; } if (si.analysis.flags == QScriptAnalysis::Object) { if (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji || _type == TextBlockTSkip) { si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding()); } } } QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); if (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) { int item = firstItem + visualOrder[i]; const QScriptItem &si = engine.layoutData->items.at(item); bool rtl = (si.analysis.bidiLevel % 2); while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) { nextBlock = currentBlock; currentBlock = _t->_blocks[--blockIndex - 1].get(); applyBlockProperties(currentBlock); } while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; applyBlockProperties(currentBlock); } if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { TextBlockType _type = currentBlock->type(); if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest if (_lookupLink) { if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) { if (const auto link = lookupLink(currentBlock)) { _lookupResult.link = link; } } } if (_type != TextBlockTSkip) { _lookupResult.uponSymbol = true; } if (_lookupSymbol) { if (_type == TextBlockTSkip) { if (_parDirection == Qt::RightToLeft) { _lookupResult.symbol = _lineStart; _lookupResult.afterSymbol = false; } else { _lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart; _lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false; } return false; } // Emoji with spaces after symbol lookup auto chFrom = _str + currentBlock->from(); auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from()); auto spacesWidth = (si.width - currentBlock->f_width()); auto spacesCount = 0; while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) { ++spacesCount; --chTo; } if (spacesCount > 0) { // Check if we're over a space. if (rtl) { if (_lookupX < x + spacesWidth) { _lookupResult.symbol = (chTo - _str); // up to a space, included, rtl _lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false; return false; } } else if (_lookupX >= x + si.width - spacesWidth) { _lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr _lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false; return false; } } if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) { _lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str; _lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false; } else { _lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str; _lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true; } } return false; } else if (_p && (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji)) { auto glyphX = x; auto spacesWidth = (si.width - currentBlock->f_width()); if (rtl) { glyphX += spacesWidth; } FixedRange fillSelect; FixedRange fillSpoiler; 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()); 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 }; } else { // or with space fillSelect = { glyphX, glyphX + currentBlock->f_width() }; } } else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) { if (rtl) { // rtl space only fillSelect = { x, glyphX }; } else { // ltr space only fillSelect = { x + currentBlock->f_width(), x + si.width }; } } } const auto hasSpoiler = _background.spoiler && (_spoilerOpacity > 0.); if (hasSpoiler) { fillSpoiler = { x, x + si.width }; } fillSelectRange(fillSelect); 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 == TextBlockTEmoji) { Emoji::Draw( *_p, static_cast(currentBlock)->_emoji, Emoji::GetSizeNormal(), x, y); } else if (const auto custom = static_cast(currentBlock)->_custom.get()) { if (!_customEmojiSize) { _customEmojiSize = AdjustCustomEmojiSize(st::emojiSize); _customEmojiSkip = (st::emojiSize - _customEmojiSize) / 2; } custom->paint(*_p, { .preview = _palette->spoilerFg->c, .now = now(), .position = { x + _customEmojiSkip, y + _customEmojiSkip }, .paused = _paused, }); } if (hasSpoiler) { _p->setOpacity(opacity); } } if (hasSpoiler) { pushSpoilerRange(fillSpoiler, fillSelect, blockIndex); } //} 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); auto itemRange = FixedRange{ x, x + itemWidth }; auto fillSelect = FixedRange(); auto hasSelected = false; auto hasNotSelected = true; auto selectedRect = QRect(); if (_background.selectActiveBlock) { fillSelect = itemRange; fillSelectRange(fillSelect); } else if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) { hasSelected = true; auto selX = x; auto selWidth = itemWidth; if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) { hasNotSelected = false; } else { selWidth = 0; int itemL = itemEnd - itemStart; int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart); if (selStart < 0) selStart = 0; if (selEnd > itemL) selEnd = itemL; for (int ch = 0, g; ch < selEnd;) { g = logClusters[itemStart - si.position + ch]; QFixed gwidth = glyphs.effectiveAdvance(g); // ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes int ch2 = ch + 1; while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) { ++ch2; } if (ch2 <= selStart) { selX += gwidth; } else if (ch >= selStart && ch2 <= selEnd) { selWidth += gwidth; } else { int sStart = ch, sEnd = ch2; if (ch < selStart) { sStart = selStart; selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch); } if (ch2 >= selEnd) { sEnd = selEnd; selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); break; } selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch); } ch = ch2; } } if (rtl) selX = x + itemWidth - (selX - x) - selWidth; selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight); fillSelect = { selX, selX + selWidth }; fillSelectRange(fillSelect); } const auto hasSpoiler = _background.spoiler && (_spoilerOpacity > 0.); const auto opacity = _p->opacity(); const auto isElidedBlock = !rtl && (_indexOfElidedBlock == blockIndex); const auto complexClipping = hasSpoiler && isElidedBlock && (_spoilerOpacity == 1.); if (!hasSpoiler || (_spoilerOpacity < 1.) || isElidedBlock) { const auto complexClippingEnabled = complexClipping && _p->hasClipping(); const auto complexClippingRegion = complexClipping ? _p->clipRegion() : QRegion(); if (complexClipping) { const auto elided = (_indexOfElidedBlock == blockIndex) ? (_elideRemoveFromEnd + _f->elidew) : 0; _p->setClipRect( QRect( (x + itemWidth).toInt() - elided, _y - _lineHeight, elided, _y + 2 * _lineHeight), Qt::IntersectClip); } else if (hasSpoiler && !isElidedBlock) { _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 - _w).toInt(), _y - _lineHeight, (_x + 2 * _w).toInt(), _y + 2 * _lineHeight)); _p->setClipRegion(externalClipping - selectedRect); _p->setPen(*_currentPen); _p->drawTextItem(QPointF(x.toReal(), textY), gf); #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 && !isElidedBlock) { _p->setOpacity(opacity); } } if (hasSpoiler) { pushSpoilerRange(itemRange, fillSelect, blockIndex); } } x += itemWidth; } fillSpoilerRects(); return true; } 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::pushSpoilerRange( FixedRange range, FixedRange selected, int currentBlockIndex) { if (!_background.spoiler || !_spoiler) { return; } const auto elided = (_indexOfElidedBlock == currentBlockIndex) ? (_elideRemoveFromEnd + _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::fillSpoilerRects() { fillSpoilerRects(_spoilerRects, _spoilerRanges); fillSpoilerRects(_spoilerSelectedRects, _spoilerSelectedRanges); } void Renderer::fillSpoilerRects( 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(), _paused); 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::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, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex(), (*_elideSavedBlock)->spoilerIndex()); _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 - _parStart; if (newParLength > _parAnalysis.size()) { _parAnalysis.resize(newParLength); } for (int32 i = elideLen; i > 0; --i) { _parAnalysis[newParLength - i].bidiLevel = (_parDirection == Qt::RightToLeft) ? 1 : 0; } } void 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(_parDirection); _e = &engine; eItemize(); auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; QScriptLine line; line.from = lineStart; line.length = lineLength; eShapeLine(line); auto elideWidth = _f->elidew; _wLeft = _w - elideWidth - _elideRemoveFromEnd; int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1); int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i; for (i = 0; i < nItems; ++i) { QScriptItem &si(engine.layoutData->items[firstItem + i]); while (nextBlock && nextBlock->from() <= _localFrom + si.position) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } TextBlockType _type = currentBlock->type(); if (si.analysis.flags == QScriptAnalysis::Object) { if (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji || _type == TextBlockTSkip) { si.width = currentBlock->f_width() + currentBlock->f_rpadding(); } } if (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji || _type == TextBlockTSkip || _type == TextBlockTNewline) { if (_wLeft < si.width) { lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis; lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart; _selection.to = qMin(_selection.to, currentBlock->from()); _indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0); setElideBidi(currentBlock->from(), kQEllipsis.size()); elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth); return; } _wLeft -= si.width; } else if (_type == TextBlockTText) { unsigned short *logClusters = engine.logClusters(&si); QGlyphLayout glyphs = engine.shapedGlyphs(&si); int itemStart = qMax(line.from, si.position), itemEnd; int itemLength = engine.length(firstItem + i); int glyphsStart = logClusters[itemStart - si.position], glyphsEnd; if (line.from + line.length < si.position + itemLength) { itemEnd = line.from + line.length; glyphsEnd = logClusters[itemEnd - si.position]; } else { itemEnd = si.position + itemLength; glyphsEnd = si.num_glyphs; } for (auto g = glyphsStart; g < glyphsEnd; ++g) { auto adv = glyphs.effectiveAdvance(g); if (_wLeft < adv) { auto pos = itemStart; while (pos < itemEnd && logClusters[pos - si.position] < g) { ++pos; } if (lineText.size() <= pos || repeat > 3) { lineText += 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]->from() < 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->from() <= _localFrom + si.position) { currentBlock = nextBlock; 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); return active ? _t->_st->linkFontOver : _t->_st->linkFont; } return _t->_st->font; }(); const auto newFont = applyFlags(flags, usedFont); if (newFont != _f) { _f = (newFont->family() == _t->_st->font->family()) ? applyFlags(flags | newFont->flags(), _t->_st->font) : newFont; _e->fnt = _f->f; _e->resetFontEngineCache(); } } void 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 = _parHasBidi; auto analysis = _parAnalysis.data() + (_localFrom - _parStart); { #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->from() <= _localFrom + (start - string)) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } auto _type = currentBlock->type(); if (_type == TextBlockTEmoji || _type == TextBlockTCustomEmoji || _type == TextBlockTSkip) { 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 = _parAnalysis.data() + (_localFrom - _parStart); auto i_items = &_e->layoutData->items; blockIndex = _lineStartBlock; currentBlock = _t->_blocks[blockIndex].get(); nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; auto startBlock = currentBlock; if (!length) { return; } auto start = 0; auto end = start + length; for (int i = start + 1; i < end; ++i) { while (nextBlock && nextBlock->from() <= _localFrom + i) { currentBlock = nextBlock; nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; } // According to the unicode spec we should be treating characters in the Common script // (punctuation, spaces, etc) as being the same script as the surrounding text for the // purpose of splitting up text. This is important because, for example, a fullstop // (0x2E) can be used to indicate an abbreviation and so must be treated as part of a // word. Thus it must be passed along with the word in languages that have to calculate // word breaks. For example the thai word "[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 <= _parLength) { while (i != _parStartBlock && (*i)->from() > _parStart + sor) { n = i; --i; } while (n != e && (*n)->from() <= _parStart + sor) { i = n; ++n; } TextBlockType _itype = (*i)->type(); if (eor == _parLength) dir = control.basicDirection(); else if (_itype == TextBlockTEmoji || _itype == TextBlockTCustomEmoji) dir = QChar::DirCS; else if (_itype == TextBlockTSkip) dir = QChar::DirCS; else dir = QChar::direction(unicode[sor]); // Keep skipping DirBN as if it doesn't exist if (dir != QChar::DirBN) break; analysis[sor++].bidiLevel = level; } eor = sor; return dir; } // creates the next QScript items. bool 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()) + _parStart; int current = 0; QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL; BidiStatus status; String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1; QChar::Direction sdir; TextBlockType _stype = (*_parStartBlock)->type(); if (_stype == TextBlockTEmoji || _stype == TextBlockTCustomEmoji) sdir = QChar::DirCS; else if (_stype == TextBlockTSkip) sdir = QChar::DirCS; else sdir = QChar::direction(*unicode); if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN) sdir = QChar::DirON; else dir = QChar::DirON; status.eor = sdir; status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL; status.last = status.lastStrong; status.dir = sdir; while (current <= _parLength) { while (n != e && (*n)->from() <= _parStart + current) { i = n; ++n; } QChar::Direction dirCurrent; TextBlockType _itype = (*i)->type(); if (current == (int)_parLength) dirCurrent = control.basicDirection(); else if (_itype == TextBlockTEmoji || _itype == TextBlockTCustomEmoji) dirCurrent = QChar::DirCS; else if (_itype == TextBlockTSkip) dirCurrent = QChar::DirCS; else dirCurrent = QChar::direction(unicode[current]); switch (dirCurrent) { // embedding and overrides (X1-X9 in the BiDi specs) case QChar::DirRLE: case QChar::DirRLO: case QChar::DirLRE: case QChar::DirLRO: { bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO); hasBidi |= rtl; bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO); unsigned int level = control.level + 1; if ((level % 2 != 0) == rtl) ++level; if (level < _MaxBidiLevel) { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); eor = current; control.embed(rtl, override); QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL); dir = status.eor = edir; status.lastStrong = edir; } break; } case QChar::DirPDF: { if (control.canPop()) { if (dir != control.direction()) { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); dir = control.direction(); } eor = current; eAppendItems(analysis, sor, eor, control, dir); control.pdf(); dir = QChar::DirON; status.eor = QChar::DirON; status.last = control.direction(); if (control.override) dir = control.direction(); else dir = QChar::DirON; status.lastStrong = control.direction(); } break; } // strong types case QChar::DirL: if (dir == QChar::DirON) dir = QChar::DirL; switch (status.last) { case QChar::DirL: eor = current; status.eor = QChar::DirL; break; case QChar::DirR: case QChar::DirAL: case QChar::DirEN: case QChar::DirAN: if (eor >= 0) { eAppendItems(analysis, sor, eor, control, dir); status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); } else { eor = current; status.eor = dir; } break; case QChar::DirES: case QChar::DirET: case QChar::DirCS: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (dir != QChar::DirL) { //last stuff takes embedding dir if (control.direction() == QChar::DirR) { if (status.eor != QChar::DirR) { // AN or EN eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirR; } eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i); } else { if (status.eor != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirL; } else { eor = current; status.eor = QChar::DirL; break; } } } else { eor = current; status.eor = QChar::DirL; } default: break; } status.lastStrong = QChar::DirL; break; case QChar::DirAL: case QChar::DirR: hasBidi = true; if (dir == QChar::DirON) dir = QChar::DirR; switch (status.last) { case QChar::DirL: case QChar::DirEN: case QChar::DirAN: if (eor >= 0) eAppendItems(analysis, sor, eor, control, dir); // fall through case QChar::DirR: case QChar::DirAL: dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break; case QChar::DirES: case QChar::DirET: case QChar::DirCS: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (status.eor != QChar::DirR && status.eor != QChar::DirAL) { //last stuff takes embedding dir if (control.direction() == QChar::DirR || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { eAppendItems(analysis, sor, eor, control, dir); dir = QChar::DirR; status.eor = QChar::DirON; eor = current; } else { eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); dir = QChar::DirR; status.eor = QChar::DirON; } } else { eor = current; status.eor = QChar::DirR; } default: break; } status.lastStrong = dirCurrent; break; // weak types: case QChar::DirNSM: if (eor == current - 1) eor = current; break; case QChar::DirEN: // if last strong was AL change EN to AN if (status.lastStrong != QChar::DirAL) { if (dir == QChar::DirON) { if (status.lastStrong == QChar::DirL) dir = QChar::DirL; else dir = QChar::DirEN; } switch (status.last) { case QChar::DirET: if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirAN; } [[fallthrough]]; case QChar::DirEN: case QChar::DirL: eor = current; status.eor = dirCurrent; break; case QChar::DirR: case QChar::DirAL: case QChar::DirAN: if (eor >= 0) eAppendItems(analysis, sor, eor, control, dir); else eor = current; status.eor = QChar::DirEN; dir = QChar::DirAN; break; case QChar::DirES: case QChar::DirCS: if (status.eor == QChar::DirEN || dir == QChar::DirAN) { eor = current; break; } [[fallthrough]]; case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (status.eor == QChar::DirR) { // neutrals go to R eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirEN; dir = QChar::DirAN; } else if (status.eor == QChar::DirL || (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { eor = current; status.eor = dirCurrent; } else { // numbers on both sides, neutrals get right to left direction if (dir != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; eor = current - 1; dir = QChar::DirR; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; dir = QChar::DirAN; } else { eor = current; status.eor = dirCurrent; } } [[fallthrough]]; default: break; } break; } [[fallthrough]]; case QChar::DirAN: hasBidi = true; dirCurrent = QChar::DirAN; if (dir == QChar::DirON) dir = QChar::DirAN; switch (status.last) { case QChar::DirL: case QChar::DirAN: eor = current; status.eor = QChar::DirAN; break; case QChar::DirR: case QChar::DirAL: case QChar::DirEN: if (eor >= 0) { eAppendItems(analysis, sor, eor, control, dir); } else { eor = current; } dir = QChar::DirAN; status.eor = QChar::DirAN; break; case QChar::DirCS: if (status.eor == QChar::DirAN) { eor = current; break; } [[fallthrough]]; case QChar::DirES: case QChar::DirET: case QChar::DirBN: case QChar::DirB: case QChar::DirS: case QChar::DirWS: case QChar::DirON: if (status.eor == QChar::DirR) { // neutrals go to R eor = current - 1; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirAN; dir = QChar::DirAN; } else if (status.eor == QChar::DirL || (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) { eor = current; status.eor = dirCurrent; } else { // numbers on both sides, neutrals get right to left direction if (dir != QChar::DirL) { eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirON; eor = current - 1; dir = QChar::DirR; eAppendItems(analysis, sor, eor, control, dir); status.eor = QChar::DirAN; dir = QChar::DirAN; } else { eor = current; status.eor = dirCurrent; } } [[fallthrough]]; default: break; } break; case QChar::DirES: case QChar::DirCS: break; case QChar::DirET: if (status.last == QChar::DirEN) { dirCurrent = QChar::DirEN; eor = current; status.eor = dirCurrent; } break; // boundary neutrals should be ignored case QChar::DirBN: break; // neutrals case QChar::DirB: // ### what do we do with newline and paragraph separators that come to here? break; case QChar::DirS: // ### implement rule L1 break; case QChar::DirWS: case QChar::DirON: break; default: break; } if (current >= (int)_parLength) break; // set status.last as needed. switch (dirCurrent) { case QChar::DirET: case QChar::DirES: case QChar::DirCS: case QChar::DirS: case QChar::DirWS: case QChar::DirON: switch (status.last) { case QChar::DirL: case QChar::DirR: case QChar::DirAL: case QChar::DirEN: case QChar::DirAN: status.last = dirCurrent; break; default: status.last = QChar::DirON; } break; case QChar::DirNSM: case QChar::DirBN: // ignore these break; case QChar::DirLRO: case QChar::DirLRE: status.last = QChar::DirL; break; case QChar::DirRLO: case QChar::DirRLE: status.last = QChar::DirR; break; case QChar::DirEN: if (status.last == QChar::DirL) { status.last = QChar::DirL; break; } [[fallthrough]]; default: status.last = dirCurrent; } ++current; } eor = current - 1; // remove dummy char if (sor <= eor) eAppendItems(analysis, sor, eor, control, dir); return hasBidi; } void Renderer::applyBlockProperties(const AbstractBlock *block) { eSetFont(block); if (_p) { const auto isMono = IsMono(block->flags()); _background = {}; if (block->spoilerIndex() && _spoiler) { _background.spoiler = true; } if (isMono && block->lnkIndex() && (!_background.spoiler || _spoiler->revealed)) { _background.selectActiveBlock = ClickHandler::showAsPressed( _t->_links.at(block->lnkIndex() - 1)); } if (isMono) { _currentPen = &_palette->monoFg->p; _currentPenSelected = &_palette->selectMonoFg->p; } else if (block->lnkIndex() || (block->flags() & TextBlockFPlainLink)) { _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->spoilerIndex()) ? _spoiler->link : ClickHandlerPtr(); return (spoilerLink || !block->lnkIndex()) ? spoilerLink : _t->_links.at(block->lnkIndex() - 1); } } // namespace Ui::Text