Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qquicktextnodeengine_p.h"
++
++ #include <QtCore/qpoint.h>
++ #include <QtGui/qabstracttextdocumentlayout.h>
++ #include <QtGui/qrawfont.h>
++ #include <QtGui/qtextdocument.h>
++ #include <QtGui/qtextlayout.h>
++ #include <QtGui/qtextobject.h>
++ #include <QtGui/qtexttable.h>
++ #include <QtGui/qtextlist.h>
++ #include <QtGui/qimageiohandler.h>
++
++ #include <private/qquicktext_p.h>
++ #include <private/qtextdocumentlayout_p.h>
++ #include <private/qtextimagehandler_p.h>
++ #include <private/qrawfont_p.h>
++ #include <private/qglyphrun_p.h>
++ #include <private/qquickitem_p.h>
++ #include <private/qsgdistancefieldglyphnode_p.h>
++
++ #include <QtGui/private/qguiapplication_p.h>
++ #include <qpa/qplatformtheme.h>
++
++ QT_BEGIN_NAMESPACE
++
++ // Resolves SpellCheckUnderline to the platform-specific style and normalizes
++ // unknown styles to SingleUnderline.
++ static QTextCharFormat::UnderlineStyle resolveUnderlineStyle(QTextCharFormat::UnderlineStyle style)
++ {
++ if (style == QTextCharFormat::SpellCheckUnderline) {
++ QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme();
++ if (theme) {
++ style = QTextCharFormat::UnderlineStyle(
++ theme->themeHint(QPlatformTheme::SpellCheckUnderlineStyle).toInt());
++ }
++ if (style == QTextCharFormat::SpellCheckUnderline)
++ style = QTextCharFormat::WaveUnderline;
++ }
++
++ switch (style) {
++ case QTextCharFormat::NoUnderline:
++ case QTextCharFormat::SingleUnderline:
++ case QTextCharFormat::DashUnderline:
++ case QTextCharFormat::DotLine:
++ case QTextCharFormat::DashDotLine:
++ case QTextCharFormat::DashDotDotLine:
++ case QTextCharFormat::WaveUnderline:
++ return style;
++ default:
++ qWarning("QQuickTextNodeEngine: unknown underline style %d, falling back to SingleUnderline",
++ int(style));
++ return QTextCharFormat::SingleUnderline;
++ }
++ }
++
++ QQuickTextNodeEngine::BinaryTreeNodeKey::BinaryTreeNodeKey(BinaryTreeNode *node)
++ : fontEngine(QRawFontPrivate::get(node->glyphRun.rawFont())->fontEngine)
++ , clipNode(node->clipNode)
++ , color(node->color.rgba())
++ , selectionState(node->selectionState)
++ {
++ }
++
++ QQuickTextNodeEngine::BinaryTreeNode::BinaryTreeNode(const QGlyphRun &g,
++ SelectionState selState,
++ const QRectF &brect,
++ const Decorations &decs,
++ QTextCharFormat::UnderlineStyle us,
++ const QColor &c,
++ const QColor &bc, const QColor &dc,
++ const QPointF &pos, qreal a)
++ : glyphRun(g)
++ , boundingRect(brect)
++ , selectionState(selState)
++ , clipNode(nullptr)
++ , decorations(decs)
++ , underlineStyle(us)
++ , color(c)
++ , backgroundColor(bc)
++ , decorationColor(dc)
++ , position(pos)
++ , ascent(a)
++ , leftChildIndex(-1)
++ , rightChildIndex(-1)
++ {
++ QGlyphRunPrivate *d = QGlyphRunPrivate::get(g);
++ ranges.append(std::make_pair(d->textRangeStart, d->textRangeEnd));
++ }
++
++ void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray<BinaryTreeNode, 16> *binaryTree, const QGlyphRun &glyphRun, SelectionState selectionState,
++ Decorations decorations, QTextCharFormat::UnderlineStyle underlineStyle, const QColor &textColor,
++ const QColor &backgroundColor, const QColor &decorationColor, const QPointF &position)
++ {
++ QRectF searchRect = glyphRun.boundingRect();
++ searchRect.translate(position);
++
++ if (qFuzzyIsNull(searchRect.width()) || qFuzzyIsNull(searchRect.height()))
++ return;
++
++ if (glyphRun.underline() || underlineStyle != QTextCharFormat::NoUnderline)
++ decorations |= Decoration::Underline;
++ decorations |= (glyphRun.overline() ? Decoration::Overline : Decoration::NoDecoration);
++ decorations |= (glyphRun.strikeOut() ? Decoration::StrikeOut : Decoration::NoDecoration);
++ decorations |= (backgroundColor.isValid() ? Decoration::Background : Decoration::NoDecoration);
++
++ qreal ascent = glyphRun.rawFont().ascent();
++ insert(binaryTree, BinaryTreeNode(glyphRun,
++ selectionState,
++ searchRect,
++ decorations,
++ underlineStyle,
++ textColor,
++ backgroundColor,
++ decorationColor,
++ position,
++ ascent));
++ }
++
++ void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray<BinaryTreeNode, 16> *binaryTree, const BinaryTreeNode &binaryTreeNode)
++ {
++ int newIndex = binaryTree->size();
++ binaryTree->append(binaryTreeNode);
++ if (newIndex == 0)
++ return;
++
++ int searchIndex = 0;
++ forever {
++ BinaryTreeNode *node = binaryTree->data() + searchIndex;
++ if (binaryTreeNode.boundingRect.left() < node->boundingRect.left()) {
++ if (node->leftChildIndex < 0) {
++ node->leftChildIndex = newIndex;
++ break;
++ } else {
++ searchIndex = node->leftChildIndex;
++ }
++ } else {
++ if (node->rightChildIndex < 0) {
++ node->rightChildIndex = newIndex;
++ break;
++ } else {
++ searchIndex = node->rightChildIndex;
++ }
++ }
++ }
++ }
++
++ void QQuickTextNodeEngine::BinaryTreeNode::inOrder(const QVarLengthArray<BinaryTreeNode, 16> &binaryTree,
++ QVarLengthArray<int> *sortedIndexes, int currentIndex)
++ {
++ Q_ASSERT(currentIndex < binaryTree.size());
++
++ const BinaryTreeNode *node = binaryTree.data() + currentIndex;
++ if (node->leftChildIndex >= 0)
++ inOrder(binaryTree, sortedIndexes, node->leftChildIndex);
++
++ sortedIndexes->append(currentIndex);
++
++ if (node->rightChildIndex >= 0)
++ inOrder(binaryTree, sortedIndexes, node->rightChildIndex);
++ }
++
++
++ int QQuickTextNodeEngine::addText(const QTextBlock &block,
++ const QTextCharFormat &charFormat,
++ const QColor &textColor,
++ const QVarLengthArray<QTextLayout::FormatRange> &colorChanges,
++ int textPos, int fragmentEnd,
++ int selectionStart, int selectionEnd)
++ {
++ if (charFormat.foreground().style() != Qt::NoBrush)
++ setTextColor(charFormat.foreground().color());
++ else
++ setTextColor(textColor);
++
++ while (textPos < fragmentEnd) {
++ int blockRelativePosition = textPos - block.position();
++ QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition);
++ if (!line.isValid())
++ break;
++ if (!currentLine().isValid()
++ || line.lineNumber() != currentLine().lineNumber()) {
++ setCurrentLine(line);
++ }
++
++ Q_ASSERT(line.textLength() > 0);
++ int lineEnd = line.textStart() + block.position() + line.textLength();
++
++ int len = qMin(lineEnd - textPos, fragmentEnd - textPos);
++ Q_ASSERT(len > 0);
++
++ int currentStepEnd = textPos + len;
++
++ addGlyphsForRanges(colorChanges,
++ textPos - block.position(),
++ currentStepEnd - block.position(),
++ selectionStart - block.position(),
++ selectionEnd - block.position());
++
++ textPos = currentStepEnd;
++ }
++ return textPos;
++ }
++
++ void QQuickTextNodeEngine::addTextDecorations(QSpan<const TextDecoration> textDecorations,
++ qreal offset, qreal thickness)
++ {
++ for (auto textDecoration : textDecorations) {
++ QRectF &rect = textDecoration.rect;
++ rect.setY(qRound(rect.y() + m_currentLine.ascent() + offset));
++ rect.setHeight(thickness);
++
++ m_lines.append(textDecoration);
++ }
++ }
++
++ void QQuickTextNodeEngine::processCurrentLine()
++ {
++ // No glyphs, do nothing
++ if (m_currentLineTree.isEmpty())
++ return;
++
++ // 1. Go through current line and get correct decoration position for each node based on
++ // neighbouring decorations. Add decoration to global list
++ // 2. Create clip nodes for all selected text. Try to merge as many as possible within
++ // the line.
++ // 3. Add QRects to a list of selection rects.
++ // 4. Add all nodes to a global processed list
++ QVarLengthArray<int> sortedIndexes; // Indexes in tree sorted by x position
++ BinaryTreeNode::inOrder(m_currentLineTree, &sortedIndexes);
++
++ Q_ASSERT(sortedIndexes.size() == m_currentLineTree.size());
++
++ SelectionState currentSelectionState = Unselected;
++ QRectF currentRect;
++
++ Decorations currentDecorations = Decoration::NoDecoration;
++ qreal underlineOffset = 0.0;
++ qreal underlineThickness = 0.0;
++
++ qreal overlineOffset = 0.0;
++ qreal overlineThickness = 0.0;
++
++ qreal strikeOutOffset = 0.0;
++ qreal strikeOutThickness = 0.0;
++
++ QRectF decorationRect = currentRect;
++
++ QColor lastColor;
++ QColor lastBackgroundColor;
++ QColor lastDecorationColor;
++ QTextCharFormat::UnderlineStyle lastUnderlineStyle = QTextCharFormat::NoUnderline;
++
++ QVarLengthArray<TextDecoration, 16> pendingUnderlines;
++ QVarLengthArray<TextDecoration, 16> pendingOverlines;
++ QVarLengthArray<TextDecoration, 16> pendingStrikeOuts;
++ if (!sortedIndexes.isEmpty()) {
++ QQuickDefaultClipNode *currentClipNode = m_hasSelection ? new QQuickDefaultClipNode(QRectF()) : nullptr;
++ bool currentClipNodeUsed = false;
++ for (int i=0; i<=sortedIndexes.size(); ++i) {
++ BinaryTreeNode *node = nullptr;
++ if (i < sortedIndexes.size()) {
++ int sortedIndex = sortedIndexes.at(i);
++ Q_ASSERT(sortedIndex < m_currentLineTree.size());
++
++ node = m_currentLineTree.data() + sortedIndex;
++ if (i == 0)
++ currentSelectionState = node->selectionState;
++ }
++
++ // Update decorations
++ if (currentDecorations != Decoration::NoDecoration) {
++ decorationRect.setY(m_position.y() + m_currentLine.y());
++ decorationRect.setHeight(m_currentLine.height());
++
++ // Extend decoration rect to the next node only if the next node continues the
++ // same decoration, or if it is directly adjacent. Don't extend across gaps
++ // (e.g. tab characters) to a node that does not share the same decoration, as
++ // that would draw the decoration through invisible content.
++ if (node != nullptr) {
++ const bool continuesDecoration = node->decorations & currentDecorations;
++ if (continuesDecoration || node->boundingRect.left() < decorationRect.right())
++ decorationRect.setRight(node->boundingRect.left());
++ }
++
++ TextDecoration textDecoration(currentSelectionState, decorationRect, lastColor);
++ if (lastDecorationColor.isValid() &&
++ (currentDecorations.testFlag(Decoration::Underline) ||
++ currentDecorations.testFlag(Decoration::Overline) ||
++ currentDecorations.testFlag(Decoration::StrikeOut)))
++ textDecoration.color = lastDecorationColor;
++
++ if (currentDecorations & Decoration::Underline) {
++ textDecoration.underlineStyle = lastUnderlineStyle;
++ // Try to merge with previous underline if style, color, and selection match
++ if (!pendingUnderlines.isEmpty()) {
++ TextDecoration &last = pendingUnderlines.last();
++ // Half-pixel threshold accounts for floating-point gaps between adjacent glyph runs
++ constexpr qreal mergeThreshold = 0.5;
++ if (last.underlineStyle == lastUnderlineStyle
++ && last.color == textDecoration.color
++ && last.selectionState == textDecoration.selectionState
++ && qAbs(last.rect.right() - textDecoration.rect.left()) < mergeThreshold) {
++ // Extend previous range instead of creating a new one
++ last.rect.setRight(textDecoration.rect.right());
++ } else {
++ pendingUnderlines.append(textDecoration);
++ }
++ } else {
++ pendingUnderlines.append(textDecoration);
++ }
++ }
++
++ if (currentDecorations & Decoration::Overline)
++ pendingOverlines.append(textDecoration);
++
++ if (currentDecorations & Decoration::StrikeOut)
++ pendingStrikeOuts.append(textDecoration);
++
++ if (currentDecorations & Decoration::Background)
++ m_backgrounds.append(std::make_pair(decorationRect, lastBackgroundColor));
++ }
++
++ // If we've reached an unselected node from a selected node, we add the
++ // selection rect to the graph, and we add decoration every time the
++ // selection state changes, because that means the text color changes
++ if (node == nullptr || node->selectionState != currentSelectionState) {
++ currentRect.setY(m_position.y() + m_currentLine.y());
++ currentRect.setHeight(m_currentLine.height());
++
++ if (currentSelectionState == Selected)
++ m_selectionRects.append(currentRect);
++
++ if (currentClipNode != nullptr) {
++ if (!currentClipNodeUsed) {
++ delete currentClipNode;
++ } else {
++ currentClipNode->setIsRectangular(true);
++ currentClipNode->setRect(currentRect);
++ currentClipNode->update();
++ }
++ }
++
++ if (node != nullptr && m_hasSelection)
++ currentClipNode = new QQuickDefaultClipNode(QRectF());
++ else
++ currentClipNode = nullptr;
++ currentClipNodeUsed = false;
++
++ if (node != nullptr) {
++ currentSelectionState = node->selectionState;
++ currentRect = node->boundingRect;
++
++ // Make sure currentRect is valid, otherwise the unite won't work
++ if (currentRect.isNull())
++ currentRect.setSize(QSizeF(1, 1));
++ }
++ } else {
++ if (currentRect.isNull())
++ currentRect = node->boundingRect;
++ else
++ currentRect = currentRect.united(node->boundingRect);
++ }
++
++ if (node != nullptr) {
++ if (node->selectionState == Selected) {
++ node->clipNode = currentClipNode;
++ currentClipNodeUsed = true;
++ }
++
++ decorationRect = node->boundingRect;
++
++ // If previous item(s) had underline and current does not, then we add the
++ // pending lines to the lists and likewise for overlines and strikeouts
++ if (!pendingUnderlines.isEmpty()
++ && !(node->decorations & Decoration::Underline)) {
++ addTextDecorations(pendingUnderlines, underlineOffset, underlineThickness);
++
++ pendingUnderlines.clear();
++
++ underlineOffset = 0.0;
++ underlineThickness = 0.0;
++ }
++
++ // ### Add pending when overlineOffset/thickness changes to minimize number of
++ // nodes
++ if (!pendingOverlines.isEmpty()) {
++ addTextDecorations(pendingOverlines, overlineOffset, overlineThickness);
++
++ pendingOverlines.clear();
++
++ overlineOffset = 0.0;
++ overlineThickness = 0.0;
++ }
++
++ // ### Add pending when overlineOffset/thickness changes to minimize number of
++ // nodes
++ if (!pendingStrikeOuts.isEmpty()) {
++ addTextDecorations(pendingStrikeOuts, strikeOutOffset, strikeOutThickness);
++
++ pendingStrikeOuts.clear();
++
++ strikeOutOffset = 0.0;
++ strikeOutThickness = 0.0;
++ }
++
++ // Merge current values with previous. Prefer greatest thickness
++ QRawFont rawFont = node->glyphRun.rawFont();
++ if (node->decorations & Decoration::Underline) {
++ if (rawFont.lineThickness() > underlineThickness) {
++ underlineThickness = rawFont.lineThickness();
++ underlineOffset = rawFont.underlinePosition();
++ }
++ }
++
++ if (node->decorations & Decoration::Overline) {
++ overlineOffset = -rawFont.ascent();
++ overlineThickness = rawFont.lineThickness();
++ }
++
++ if (node->decorations & Decoration::StrikeOut) {
++ strikeOutThickness = rawFont.lineThickness();
++ strikeOutOffset = rawFont.ascent() / -3.0;
++ }
++
++ currentDecorations = node->decorations;
++ lastColor = node->color;
++ lastBackgroundColor = node->backgroundColor;
++ lastDecorationColor = node->decorationColor;
++ lastUnderlineStyle = node->underlineStyle;
++ m_processedNodes.append(*node);
++ }
++ }
++
++ if (!pendingUnderlines.isEmpty())
++ addTextDecorations(pendingUnderlines, underlineOffset, underlineThickness);
++
++ if (!pendingOverlines.isEmpty())
++ addTextDecorations(pendingOverlines, overlineOffset, overlineThickness);
++
++ if (!pendingStrikeOuts.isEmpty())
++ addTextDecorations(pendingStrikeOuts, strikeOutOffset, strikeOutThickness);
++ }
++
++ m_currentLineTree.clear();
++ m_currentLine = QTextLine();
++ m_hasSelection = false;
++ }
++
++ void QQuickTextNodeEngine::addImage(const QRectF &rect, const QImage &image, qreal ascent,
++ SelectionState selectionState,
++ QTextFrameFormat::Position layoutPosition)
++ {
++ QRectF searchRect = rect;
++ if (layoutPosition == QTextFrameFormat::InFlow) {
++ if (m_currentLineTree.isEmpty()) {
++ qreal y = m_currentLine.ascent() - ascent;
++ if (m_currentTextDirection == Qt::RightToLeft)
++ searchRect.moveTopRight(m_position + m_currentLine.rect().topRight() + QPointF(0, y));
++ else
++ searchRect.moveTopLeft(m_position + m_currentLine.position() + QPointF(0, y));
++ } else {
++ const BinaryTreeNode *lastNode = m_currentLineTree.data() + m_currentLineTree.size() - 1;
++ if (lastNode->glyphRun.isRightToLeft()) {
++ QPointF lastPos = lastNode->boundingRect.topLeft();
++ searchRect.moveTopRight(lastPos - QPointF(0, ascent - lastNode->ascent));
++ } else {
++ QPointF lastPos = lastNode->boundingRect.topRight();
++ searchRect.moveTopLeft(lastPos - QPointF(0, ascent - lastNode->ascent));
++ }
++ }
++ }
++
++ BinaryTreeNode::insert(&m_currentLineTree, searchRect, image, ascent, selectionState);
++ m_hasContents = true;
++ }
++
++ void QQuickTextNodeEngine::addTextObject(const QTextBlock &block, const QPointF &position, const QTextCharFormat &format,
++ SelectionState selectionState,
++ QTextDocument *textDocument, int pos,
++ QTextFrameFormat::Position layoutPosition)
++ {
++ QTextObjectInterface *handler = textDocument->documentLayout()->handlerForObject(format.objectType());
++ if (handler != nullptr) {
++ QImage image;
++ QSizeF size = handler->intrinsicSize(textDocument, pos, format);
++
++ if (format.objectType() == QTextFormat::ImageObject) {
++ QTextImageFormat imageFormat = format.toImageFormat();
++ QTextImageHandler *imageHandler = static_cast<QTextImageHandler *>(handler);
++ image = imageHandler->image(textDocument, imageFormat);
++ }
++
++ if (image.isNull()) {
++ if (QImageIOHandler::allocateImage((size * m_devicePixelRatio).toSize(),
++ QImage::Format_ARGB32_Premultiplied,
++ &image)) {
++ image.setDevicePixelRatio(m_devicePixelRatio);
++ image.fill(Qt::transparent);
++ {
++ QPainter painter(&image);
++ handler->drawObject(&painter, QRectF({}, size), textDocument, pos, format);
++ }
++ }
++ }
++
++ // The top/bottom positions are supposed to be higher/lower than the text and reference
++ // the line height, not the text height (using QFontMetrics)
++ qreal ascent;
++ QTextLine line = block.layout()->lineForTextPosition(pos - block.position());
++ switch (format.verticalAlignment())
++ {
++ case QTextCharFormat::AlignTop:
++ ascent = line.ascent();
++ break;
++ case QTextCharFormat::AlignMiddle:
++ // Middlepoint of line (height - descent) + Half object height
++ ascent = (line.ascent() + line.descent()) / 2 - line.descent() + size.height() / 2;
++ break;
++ case QTextCharFormat::AlignBottom:
++ ascent = size.height() - line.descent();
++ break;
++ case QTextCharFormat::AlignBaseline: {
++ QFontMetricsF m(format.font());
++ ascent = size.height() - m.descent();
++ break;
++ }
++ default:
++ ascent = size.height();
++ }
++
++ addImage(QRectF(position, size), image, ascent, selectionState, layoutPosition);
++ }
++ }
++
++ void QQuickTextNodeEngine::addUnselectedGlyphs(const QGlyphRun &glyphRun)
++ {
++ BinaryTreeNode::insert(&m_currentLineTree,
++ glyphRun,
++ Unselected,
++ Decoration::NoDecoration,
++ m_underlineStyle,
++ m_textColor,
++ m_backgroundColor,
++ m_decorationColor,
++ m_position);
++ }
++
++ void QQuickTextNodeEngine::addSelectedGlyphs(const QGlyphRun &glyphRun)
++ {
++ int currentSize = m_currentLineTree.size();
++ BinaryTreeNode::insert(&m_currentLineTree,
++ glyphRun,
++ Selected,
++ Decoration::NoDecoration,
++ m_underlineStyle,
++ m_textColor,
++ m_backgroundColor,
++ m_decorationColor,
++ m_position);
++ m_hasSelection = m_hasSelection || m_currentLineTree.size() > currentSize;
++ }
++
++ void QQuickTextNodeEngine::addGlyphsForRanges(const QVarLengthArray<QTextLayout::FormatRange> &ranges,
++ int start, int end,
++ int selectionStart, int selectionEnd)
++ {
++ int currentPosition = start;
++ int remainingLength = end - start;
++ for (const QTextLayout::FormatRange &range : ranges) {
++ if (range.start + range.length > currentPosition
++ && range.start < currentPosition + remainingLength) {
++
++ if (range.start > currentPosition) {
++ addGlyphsInRange(currentPosition, range.start - currentPosition,
++ QTextCharFormat::NoUnderline,
++ QColor(), QColor(), QColor(), selectionStart, selectionEnd);
++ }
++ int rangeEnd = qMin(range.start + range.length, currentPosition + remainingLength);
++ QColor rangeColor;
++ if (range.format.hasProperty(QTextFormat::ForegroundBrush))
++ rangeColor = range.format.foreground().color();
++ else if (range.format.isAnchor())
++ rangeColor = m_anchorColor;
++ QColor rangeBackgroundColor = range.format.hasProperty(QTextFormat::BackgroundBrush)
++ ? range.format.background().color()
++ : QColor();
++
++ QColor rangeDecorationColor = range.format.hasProperty(QTextFormat::TextUnderlineColor)
++ ? range.format.underlineColor()
++ : QColor();
++
++ QTextCharFormat::UnderlineStyle rangeUnderlineStyle = QTextCharFormat::NoUnderline;
++ if (range.format.hasProperty(QTextFormat::TextUnderlineStyle))
++ rangeUnderlineStyle = range.format.underlineStyle();
++
++ addGlyphsInRange(range.start, rangeEnd - range.start, rangeUnderlineStyle,
++ rangeColor, rangeBackgroundColor,
++ rangeDecorationColor, selectionStart, selectionEnd);
++
++ currentPosition = range.start + range.length;
++ remainingLength = end - currentPosition;
++
++ } else if (range.start > currentPosition + remainingLength || remainingLength <= 0) {
++ break;
++ }
++ }
++
++ if (remainingLength > 0) {
++ addGlyphsInRange(currentPosition, remainingLength, QTextCharFormat::NoUnderline,
++ QColor(), QColor(), QColor(),
++ selectionStart, selectionEnd);
++ }
++
++ }
++
++ void QQuickTextNodeEngine::addGlyphsInRange(int rangeStart, int rangeLength,
++ QTextCharFormat::UnderlineStyle underlineStyle,
++ const QColor &color, const QColor &backgroundColor, const QColor &decorationColor,
++ int selectionStart, int selectionEnd)
++ {
++ QColor oldColor;
++ if (color.isValid()) {
++ oldColor = m_textColor;
++ m_textColor = color;
++ }
++
++ QColor oldBackgroundColor = m_backgroundColor;
++ if (backgroundColor.isValid()) {
++ oldBackgroundColor = m_backgroundColor;
++ m_backgroundColor = backgroundColor;
++ }
++
++ QColor oldDecorationColor = m_decorationColor;
++ if (decorationColor.isValid()) {
++ oldDecorationColor = m_decorationColor;
++ m_decorationColor = decorationColor;
++ }
++
++ QTextCharFormat::UnderlineStyle oldUnderlineStyle = m_underlineStyle;
++ m_underlineStyle = underlineStyle;
++
++ bool hasSelection = selectionEnd >= 0
++ && selectionStart <= selectionEnd;
++
++ QTextLine &line = m_currentLine;
++ int rangeEnd = rangeStart + rangeLength;
++ if (!hasSelection || (selectionStart > rangeEnd || selectionEnd < rangeStart)) {
++ const QList<QGlyphRun> glyphRuns = line.glyphRuns(rangeStart, rangeLength);
++ for (const QGlyphRun &glyphRun : glyphRuns)
++ addUnselectedGlyphs(glyphRun);
++ } else {
++ if (rangeStart < selectionStart) {
++ int length = qMin(selectionStart - rangeStart, rangeLength);
++ const QList<QGlyphRun> glyphRuns = line.glyphRuns(rangeStart, length);
++ for (const QGlyphRun &glyphRun : glyphRuns)
++ addUnselectedGlyphs(glyphRun);
++ }
++
++ if (rangeEnd > selectionStart) {
++ int start = qMax(selectionStart, rangeStart);
++ int length = qMin(selectionEnd - start + 1, rangeEnd - start);
++ const QList<QGlyphRun> glyphRuns = line.glyphRuns(start, length);
++ for (const QGlyphRun &glyphRun : glyphRuns)
++ addSelectedGlyphs(glyphRun);
++ }
++
++ if (selectionEnd >= rangeStart && selectionEnd < rangeEnd) {
++ int start = selectionEnd + 1;
++ int length = rangeEnd - selectionEnd - 1;
++ const QList<QGlyphRun> glyphRuns = line.glyphRuns(start, length);
++ for (const QGlyphRun &glyphRun : glyphRuns)
++ addUnselectedGlyphs(glyphRun);
++ }
++ }
++
++ if (decorationColor.isValid())
++ m_decorationColor = oldDecorationColor;
++
++ if (backgroundColor.isValid())
++ m_backgroundColor = oldBackgroundColor;
++
++ if (oldColor.isValid())
++ m_textColor = oldColor;
++
++ m_underlineStyle = oldUnderlineStyle;
++ }
++
++ void QQuickTextNodeEngine::addBorder(const QRectF &rect, qreal border,
++ QTextFrameFormat::BorderStyle borderStyle,
++ const QBrush &borderBrush)
++ {
++ const QColor &color = borderBrush.color();
++
++ // Currently we don't support other styles than solid
++ Q_UNUSED(borderStyle);
++
++ m_backgrounds.append(std::make_pair(QRectF(rect.left(), rect.top(), border, rect.height() + border), color));
++ m_backgrounds.append(std::make_pair(QRectF(rect.left() + border, rect.top(), rect.width(), border), color));
++ m_backgrounds.append(std::make_pair(QRectF(rect.right(), rect.top() + border, border, rect.height() - border), color));
++ m_backgrounds.append(std::make_pair(QRectF(rect.left() + border, rect.bottom(), rect.width(), border), color));
++ }
++
++ void QQuickTextNodeEngine::addFrameDecorations(QTextDocument *document, QTextFrame *frame)
++ {
++ QTextDocumentLayout *documentLayout = qobject_cast<QTextDocumentLayout *>(document->documentLayout());
++ if (Q_UNLIKELY(!documentLayout))
++ return;
++
++ QTextFrameFormat frameFormat = frame->format().toFrameFormat();
++ QTextTable *table = qobject_cast<QTextTable *>(frame);
++
++ QRectF boundingRect = table == nullptr
++ ? documentLayout->frameBoundingRect(frame)
++ : documentLayout->tableBoundingRect(table);
++
++ QBrush bg = frame->frameFormat().background();
++ if (bg.style() != Qt::NoBrush)
++ m_backgrounds.append(std::make_pair(boundingRect, bg.color()));
++
++ if (!frameFormat.hasProperty(QTextFormat::FrameBorder))
++ return;
++
++ qreal borderWidth = frameFormat.border();
++ if (qFuzzyIsNull(borderWidth))
++ return;
++
++ QBrush borderBrush = frameFormat.borderBrush();
++ QTextFrameFormat::BorderStyle borderStyle = frameFormat.borderStyle();
++ if (borderStyle == QTextFrameFormat::BorderStyle_None)
++ return;
++
++ const auto collapsed = table->format().borderCollapse();
++
++ if (!collapsed) {
++ addBorder(boundingRect.adjusted(frameFormat.leftMargin(), frameFormat.topMargin(),
++ -frameFormat.rightMargin() - borderWidth,
++ -frameFormat.bottomMargin() - borderWidth),
++ borderWidth, borderStyle, borderBrush);
++ }
++ if (table != nullptr) {
++ int rows = table->rows();
++ int columns = table->columns();
++
++ for (int row=0; row<rows; ++row) {
++ for (int column=0; column<columns; ++column) {
++ QTextTableCell cell = table->cellAt(row, column);
++
++ QRectF cellRect = documentLayout->tableCellBoundingRect(table, cell);
++ addBorder(cellRect.adjusted(-borderWidth, -borderWidth, collapsed ? -borderWidth : 0, collapsed ? -borderWidth : 0), borderWidth,
++ borderStyle, borderBrush);
++ }
++ }
++ }
++ }
++
++ size_t qHash(const QQuickTextNodeEngine::BinaryTreeNodeKey &key, size_t seed = 0)
++ {
++ return qHashMulti(seed, key.fontEngine, key.clipNode, key.color, key.selectionState);
++ }
++
++ void QQuickTextNodeEngine::mergeProcessedNodes(QVarLengthArray<BinaryTreeNode *, 8> *regularNodes,
++ QVarLengthArray<BinaryTreeNode *, 8> *imageNodes)
++ {
++ QHash<BinaryTreeNodeKey, QVarLengthArray<BinaryTreeNode *, 2> > map;
++
++ for (int i = 0; i < m_processedNodes.size(); ++i) {
++ BinaryTreeNode *node = m_processedNodes.data() + i;
++
++ if (node->image.isNull()) {
++ if (node->glyphRun.isEmpty())
++ continue;
++
++ BinaryTreeNodeKey key(node);
++
++ QVarLengthArray<BinaryTreeNode *, 2> &nodes = map[key];
++ if (nodes.isEmpty())
++ regularNodes->append(node);
++
++ nodes.append(node);
++ } else {
++ imageNodes->append(node);
++ }
++ }
++
++ for (int i = 0; i < regularNodes->size(); ++i) {
++ BinaryTreeNode *primaryNode = regularNodes->at(i);
++ BinaryTreeNodeKey key(primaryNode);
++
++ const auto it = map.constFind(key);
++ Q_ASSERT(it != map.constEnd());
++ const QVarLengthArray<BinaryTreeNode *, 2> &nodes = it.value();
++ Q_ASSERT(nodes.first() == primaryNode);
++
++ int count = 0;
++ for (int j = 0; j < nodes.size(); ++j)
++ count += nodes.at(j)->glyphRun.glyphIndexes().size();
++
++ if (count != primaryNode->glyphRun.glyphIndexes().size()) {
++ QGlyphRun &glyphRun = primaryNode->glyphRun;
++ QList<quint32> glyphIndexes = glyphRun.glyphIndexes();
++ glyphIndexes.reserve(count);
++
++ QList<QPointF> glyphPositions = glyphRun.positions();
++ glyphPositions.reserve(count);
++
++ QRectF glyphBoundingRect = glyphRun.boundingRect();
++
++ for (int j = 1; j < nodes.size(); ++j) {
++ BinaryTreeNode *otherNode = nodes.at(j);
++ glyphIndexes += otherNode->glyphRun.glyphIndexes();
++ primaryNode->ranges += otherNode->ranges;
++ glyphBoundingRect = glyphBoundingRect.united(otherNode->boundingRect);
++
++ QList<QPointF> otherPositions = otherNode->glyphRun.positions();
++ for (int k = 0; k < otherPositions.size(); ++k)
++ glyphPositions += otherPositions.at(k) + (otherNode->position - primaryNode->position);
++ }
++
++ Q_ASSERT(glyphPositions.size() == count);
++ Q_ASSERT(glyphIndexes.size() == count);
++
++ glyphRun.setGlyphIndexes(glyphIndexes);
++ glyphRun.setPositions(glyphPositions);
++ glyphRun.setBoundingRect(glyphBoundingRect);
++ }
++ }
++ }
++
++ void QQuickTextNodeEngine::addToSceneGraph(QSGInternalTextNode *parentNode,
++ QSGInternalTextNode::RecycleBin *recycleBin,
++ QQuickText::TextStyle style,
++ const QColor &styleColor)
++ {
++ if (m_currentLine.isValid())
++ processCurrentLine();
++
++ QVarLengthArray<BinaryTreeNode *, 8> nodes;
++ QVarLengthArray<BinaryTreeNode *, 8> imageNodes;
++ mergeProcessedNodes(&nodes, &imageNodes);
++
++ for (int i = 0; i < m_backgrounds.size(); ++i) {
++ const QRectF &rect = m_backgrounds.at(i).first;
++ const QColor &color = m_backgrounds.at(i).second;
++ if (color.alpha() != 0)
++ parentNode->addRectangleNode(rect, color, recycleBin);
++ }
++
++ // Add all text with unselected color first
++ for (int i = 0; i < nodes.size(); ++i) {
++ const BinaryTreeNode *node = nodes.at(i);
++ parentNode->addGlyphs(node->position, node->glyphRun, node->color, recycleBin, style, styleColor, nullptr);
++ }
++
++ for (int i = 0; i < imageNodes.size(); ++i) {
++ const BinaryTreeNode *node = imageNodes.at(i);
++ if (node->selectionState == Unselected)
++ parentNode->addImage(node->boundingRect, node->image, recycleBin);
++ }
++
++ for (int i = 0; i < m_selectionRects.size(); ++i) {
++ const QRectF &rect = m_selectionRects.at(i);
++ if (m_selectionColor.alpha() != 0)
++ parentNode->addRectangleNode(rect, m_selectionColor, recycleBin);
++ }
++
++ // Add decorations for each node to the tree.
++ for (int i = 0; i < m_lines.size(); ++i) {
++ const TextDecoration &textDecoration = m_lines.at(i);
++
++ QColor color = textDecoration.selectionState == Selected
++ ? m_selectedTextColor
++ : textDecoration.color;
++
++ parentNode->addDecorationNode(textDecoration.rect,
++ color,
++ resolveUnderlineStyle(textDecoration.underlineStyle),
++ recycleBin);
++ }
++
++ // Finally add the selected text on top of everything
++ for (int i = 0; i < nodes.size(); ++i) {
++ const BinaryTreeNode *node = nodes.at(i);
++ QQuickDefaultClipNode *clipNode = node->clipNode;
++ if (clipNode != nullptr && clipNode->parent() == nullptr)
++ parentNode->appendChildNode(clipNode);
++
++ if (node->selectionState == Selected) {
++ QColor color = m_selectedTextColor;
++ int previousNodeIndex = i - 1;
++ int nextNodeIndex = i + 1;
++ const BinaryTreeNode *previousNode = previousNodeIndex < 0 ? 0 : nodes.at(previousNodeIndex);
++ while (previousNode != nullptr && qFuzzyCompare(previousNode->boundingRect.left(), node->boundingRect.left()))
++ previousNode = --previousNodeIndex < 0 ? 0 : nodes.at(previousNodeIndex);
++
++ const BinaryTreeNode *nextNode = nextNodeIndex == nodes.size() ? 0 : nodes.at(nextNodeIndex);
++
++ if (previousNode != nullptr && previousNode->selectionState == Unselected)
++ parentNode->addGlyphs(previousNode->position, previousNode->glyphRun, color, recycleBin, style, styleColor, clipNode);
++
++ if (nextNode != nullptr && nextNode->selectionState == Unselected)
++ parentNode->addGlyphs(nextNode->position, nextNode->glyphRun, color, recycleBin, style, styleColor, clipNode);
++
++ // If the previous or next node completely overlaps this one, then we have already drawn the glyphs of
++ // this node
++ bool drawCurrent = false;
++ if (previousNode != nullptr || nextNode != nullptr) {
++ // Return |rangeLength| minus the overlap between |range| and the
++ // ranges of |otherNode|. Only considers nodes from the same text position;
++ // nodes from different positions (e.g. list bullets from a separate
++ // QTextLayout) have coincidental range overlaps that don't represent
++ // actual glyph coverage.
++ auto subtractOverlap = [](int rangeLength, const std::pair<int, int> &range,
++ const BinaryTreeNode *otherNode, const QPointF &nodePos) {
++ if (otherNode->position != nodePos)
++ return rangeLength;
++ for (int j = 0; j < otherNode->ranges.size(); ++j) {
++ const std::pair<int, int> &otherRange = otherNode->ranges.at(j);
++ if (range.first < otherRange.second && range.second > otherRange.first) {
++ int start = qMax(range.first, otherRange.first);
++ int end = qMin(range.second, otherRange.second);
++ rangeLength -= end - start + 1;
++ if (rangeLength == 0)
++ break;
++ }
++ }
++ return rangeLength;
++ };
++
++ for (int i = 0; i < node->ranges.size(); ++i) {
++ const std::pair<int, int> &range = node->ranges.at(i);
++
++ int rangeLength = range.second - range.first + 1;
++ if (previousNode != nullptr)
++ rangeLength = subtractOverlap(rangeLength, range, previousNode, node->position);
++ if (nextNode != nullptr && rangeLength > 0)
++ rangeLength = subtractOverlap(rangeLength, range, nextNode, node->position);
++
++ if (rangeLength > 0) {
++ drawCurrent = true;
++ break;
++ }
++ }
++ } else {
++ drawCurrent = true;
++ }
++
++ if (drawCurrent)
++ parentNode->addGlyphs(node->position, node->glyphRun, color, recycleBin, style, styleColor, clipNode);
++ }
++ }
++
++ for (int i = 0; i < imageNodes.size(); ++i) {
++ const BinaryTreeNode *node = imageNodes.at(i);
++ if (node->selectionState == Selected) {
++ parentNode->addImage(node->boundingRect, node->image, recycleBin);
++ if (node->selectionState == Selected) {
++ QColor color = m_selectionColor;
++ color.setAlpha(128);
++ parentNode->addRectangleNode(node->boundingRect, color, recycleBin);
++ }
++ }
++ }
++ }
++
++ void QQuickTextNodeEngine::mergeFormats(QTextLayout *textLayout, QVarLengthArray<QTextLayout::FormatRange> *mergedFormats)
++ {
++ Q_ASSERT(mergedFormats != nullptr);
++ if (textLayout == nullptr)
++ return;
++
++ const QList<QTextLayout::FormatRange> additionalFormats = textLayout->formats();
++ for (QTextLayout::FormatRange additionalFormat : additionalFormats) {
++ if (additionalFormat.format.hasProperty(QTextFormat::ForegroundBrush)
++ || additionalFormat.format.hasProperty(QTextFormat::BackgroundBrush)
++ || additionalFormat.format.hasProperty(QTextFormat::TextUnderlineStyle)
++ || additionalFormat.format.hasProperty(QTextFormat::TextUnderlineColor)
++ || additionalFormat.format.isAnchor()) {
++ // Merge overlapping formats
++ if (!mergedFormats->isEmpty()) {
++ QTextLayout::FormatRange *lastFormat = mergedFormats->data() + mergedFormats->size() - 1;
++
++ if (additionalFormat.start < lastFormat->start + lastFormat->length) {
++ QTextLayout::FormatRange *mergedRange = nullptr;
++
++ int length = additionalFormat.length;
++ if (additionalFormat.start > lastFormat->start) {
++ lastFormat->length = additionalFormat.start - lastFormat->start;
++ length -= lastFormat->length;
++
++ mergedFormats->append(QTextLayout::FormatRange());
++ mergedRange = mergedFormats->data() + mergedFormats->size() - 1;
++ lastFormat = mergedFormats->data() + mergedFormats->size() - 2;
++ } else {
++ mergedRange = lastFormat;
++ }
++
++ mergedRange->format = lastFormat->format;
++ mergedRange->format.merge(additionalFormat.format);
++ mergedRange->start = additionalFormat.start;
++
++ int end = qMin(additionalFormat.start + additionalFormat.length,
++ lastFormat->start + lastFormat->length);
++
++ mergedRange->length = end - mergedRange->start;
++ length -= mergedRange->length;
++
++ additionalFormat.start = end;
++ additionalFormat.length = length;
++ }
++ }
++
++ if (additionalFormat.length > 0)
++ mergedFormats->append(additionalFormat);
++ }
++ }
++ }
++
++ /*!
++ \internal
++ Adds the \a block from the \a textDocument at \a position if its
++ \l {QAbstractTextDocumentLayout::blockBoundingRect()}{bounding rect}
++ intersects the \a viewport, or if \c viewport is not valid
++ (i.e. use a default-constructed QRectF to skip the viewport check).
++
++ \sa QQuickItem::clipRect()
++ */
++ void QQuickTextNodeEngine::addTextBlock(QTextDocument *textDocument, const QTextBlock &block, const QPointF &position,
++ const QColor &textColor, const QColor &anchorColor, int selectionStart, int selectionEnd, const QRectF &viewport)
++ {
++ Q_ASSERT(textDocument);
++ #if QT_CONFIG(im)
++ int preeditLength = block.isValid() ? block.layout()->preeditAreaText().size() : 0;
++ int preeditPosition = block.isValid() ? block.layout()->preeditAreaPosition() : -1;
++ #endif
++
++ setCurrentTextDirection(block.textDirection());
++
++ QVarLengthArray<QTextLayout::FormatRange> colorChanges;
++ mergeFormats(block.layout(), &colorChanges);
++
++ const QTextCharFormat charFormat = block.charFormat();
++ const QRectF blockBoundingRect = textDocument->documentLayout()->blockBoundingRect(block).translated(position);
++ if (viewport.isValid()) {
++ if (!blockBoundingRect.intersects(viewport))
++ return;
++ qCDebug(lcSgText) << "adding block with length" << block.length() << ':' << blockBoundingRect << "in viewport" << viewport;
++ }
++
++ if (charFormat.background().style() != Qt::NoBrush)
++ m_backgrounds.append(std::make_pair(blockBoundingRect, charFormat.background().color()));
++
++ if (QTextList *textList = block.textList()) {
++ QPointF pos = blockBoundingRect.topLeft();
++ QTextLayout *layout = block.layout();
++ if (layout->lineCount() > 0) {
++ QTextLine firstLine = layout->lineAt(0);
++ Q_ASSERT(firstLine.isValid());
++
++ setCurrentLine(firstLine);
++
++ QRectF textRect = firstLine.naturalTextRect();
++ pos += textRect.topLeft();
++ if (block.textDirection() == Qt::RightToLeft)
++ pos.rx() += textRect.width();
++
++ QFont font(charFormat.font());
++ QFontMetricsF fontMetrics(font);
++ QTextListFormat listFormat = textList->format();
++
++ QString listItemBullet;
++ switch (listFormat.style()) {
++ case QTextListFormat::ListCircle:
++ listItemBullet = QChar(0x25E6); // White bullet
++ break;
++ case QTextListFormat::ListSquare:
++ listItemBullet = QChar(0x25AA); // Black small square
++ break;
++ case QTextListFormat::ListDecimal:
++ case QTextListFormat::ListLowerAlpha:
++ case QTextListFormat::ListUpperAlpha:
++ case QTextListFormat::ListLowerRoman:
++ case QTextListFormat::ListUpperRoman:
++ listItemBullet = textList->itemText(block);
++ break;
++ default:
++ listItemBullet = QChar(0x2022); // Black bullet
++ break;
++ };
++
++ switch (block.blockFormat().marker()) {
++ case QTextBlockFormat::MarkerType::Checked:
++ listItemBullet = QChar(0x2612); // Checked checkbox
++ break;
++ case QTextBlockFormat::MarkerType::Unchecked:
++ listItemBullet = QChar(0x2610); // Unchecked checkbox
++ break;
++ case QTextBlockFormat::MarkerType::NoMarker:
++ break;
++ }
++
++ QSizeF size(fontMetrics.horizontalAdvance(listItemBullet), fontMetrics.height());
++ qreal xoff = fontMetrics.horizontalAdvance(QLatin1Char(' '));
++ if (block.textDirection() == Qt::LeftToRight)
++ xoff = -xoff - size.width();
++
++ QTextLayout layout;
++ layout.setFont(font);
++ layout.setText(listItemBullet); // Bullet
++ layout.beginLayout();
++ QTextLine line = layout.createLine();
++ line.setPosition(QPointF(0, 0));
++ layout.endLayout();
++
++ // Align the marker to the first line's baseline, so it does not float
++ // above taller content such as an inline image or a larger font.
++ const qreal baselineOffset = firstLine.ascent() - line.ascent();
++ setPosition(pos + QPointF(xoff, baselineOffset));
++
++ // set the color for the bullets, instead of using the previous QTextBlock's color.
++ if (charFormat.foreground().style() == Qt::NoBrush)
++ setTextColor(textColor);
++ else
++ setTextColor(charFormat.foreground().color());
++
++ const QList<QGlyphRun> glyphRuns = layout.glyphRuns();
++ for (const auto &e : glyphRuns)
++ addUnselectedGlyphs(e);
++ }
++ }
++
++ int textPos = block.position();
++ QTextBlock::iterator blockIterator = block.begin();
++
++ while (!blockIterator.atEnd()) {
++ QTextFragment fragment = blockIterator.fragment();
++ QString text = fragment.text();
++ if (text.isEmpty())
++ continue;
++
++ QTextCharFormat charFormat = fragment.charFormat();
++ QFont font(charFormat.font());
++ QFontMetricsF fontMetrics(font);
++
++ int fontHeight = fontMetrics.descent() + fontMetrics.ascent();
++ int valign = charFormat.verticalAlignment();
++ if (valign == QTextCharFormat::AlignSuperScript)
++ setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() - fontHeight / 2));
++ else if (valign == QTextCharFormat::AlignSubScript)
++ setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() + fontHeight / 6));
++ else
++ setPosition(blockBoundingRect.topLeft());
++
++ if (text.contains(QChar::ObjectReplacementCharacter) && charFormat.objectType() != QTextFormat::NoObject) {
++ QTextFrame *frame = qobject_cast<QTextFrame *>(textDocument->objectForFormat(charFormat));
++ if (!frame || frame->frameFormat().position() == QTextFrameFormat::InFlow) {
++ int blockRelativePosition = textPos - block.position();
++ QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition);
++ if (!currentLine().isValid()
++ || line.lineNumber() != currentLine().lineNumber()) {
++ setCurrentLine(line);
++ }
++
++ QQuickTextNodeEngine::SelectionState selectionState =
++ (selectionStart < textPos + text.size()
++ && selectionEnd >= textPos)
++ ? QQuickTextNodeEngine::Selected
++ : QQuickTextNodeEngine::Unselected;
++
++ addTextObject(block, QPointF(), charFormat, selectionState, textDocument, textPos);
++ }
++ textPos += text.size();
++ } else {
++ if (charFormat.foreground().style() != Qt::NoBrush)
++ setTextColor(charFormat.foreground().color());
++ else if (charFormat.isAnchor())
++ setTextColor(anchorColor);
++ else
++ setTextColor(textColor);
++
++ int fragmentEnd = textPos + fragment.length();
++ #if QT_CONFIG(im)
++ if (preeditPosition >= 0
++ && (preeditPosition + block.position()) >= textPos
++ && (preeditPosition + block.position()) <= fragmentEnd) {
++ fragmentEnd += preeditLength;
++ }
++ #endif
++ if (charFormat.background().style() != Qt::NoBrush
++ || charFormat.hasProperty(QTextFormat::TextUnderlineColor)
++ || charFormat.hasProperty(QTextFormat::TextUnderlineStyle)) {
++ QTextLayout::FormatRange additionalFormat;
++ additionalFormat.start = textPos - block.position();
++ additionalFormat.length = fragmentEnd - textPos;
++ additionalFormat.format = charFormat;
++ colorChanges << additionalFormat;
++ }
++
++ textPos = addText(block, charFormat, textColor, colorChanges, textPos, fragmentEnd,
++ selectionStart, selectionEnd);
++ }
++
++ ++blockIterator;
++ }
++
++ #if QT_CONFIG(im)
++ if (preeditLength >= 0 && textPos <= block.position() + preeditPosition) {
++ setPosition(blockBoundingRect.topLeft());
++ textPos = block.position() + preeditPosition;
++ QTextLine line = block.layout()->lineForTextPosition(preeditPosition);
++ if (!currentLine().isValid()
++ || line.lineNumber() != currentLine().lineNumber()) {
++ setCurrentLine(line);
++ }
++ textPos = addText(block, block.charFormat(), textColor, colorChanges,
++ textPos, textPos + preeditLength,
++ selectionStart, selectionEnd);
++ }
++ #endif
++
++ // Add block decorations (so far only horizontal rules)
++ if (block.blockFormat().hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
++ auto ruleLength = qvariant_cast<QTextLength>(block.blockFormat().property(QTextFormat::BlockTrailingHorizontalRulerWidth));
++ QRectF ruleRect(0, 0, ruleLength.value(blockBoundingRect.width()), 1);
++ ruleRect.moveCenter(blockBoundingRect.center());
++ const QColor ruleColor = block.blockFormat().hasProperty(QTextFormat::BackgroundBrush)
++ ? qvariant_cast<QBrush>(block.blockFormat().property(QTextFormat::BackgroundBrush)).color()
++ : m_textColor;
++ m_lines.append(TextDecoration(QQuickTextNodeEngine::Unselected, ruleRect, ruleColor));
++ }
++
++ setCurrentLine(QTextLine()); // Reset current line because the text layout changed
++ m_hasContents = true;
++ }
++
++
++ QT_END_NAMESPACE
++