Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquicktextnodeengine.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qpoint.h>
8#include <QtGui/qabstracttextdocumentlayout.h>
9#include <QtGui/qrawfont.h>
10#include <QtGui/qtextdocument.h>
11#include <QtGui/qtextlayout.h>
12#include <QtGui/qtextobject.h>
13#include <QtGui/qtexttable.h>
14#include <QtGui/qtextlist.h>
15
16#include <private/qquicktext_p.h>
17#include <private/qtextdocumentlayout_p.h>
18#include <private/qtextimagehandler_p.h>
19#include <private/qrawfont_p.h>
20#include <private/qglyphrun_p.h>
21#include <private/qquickitem_p.h>
22#include <private/qsgdistancefieldglyphnode_p.h>
23
25
27 : fontEngine(QRawFontPrivate::get(node->glyphRun.rawFont())->fontEngine)
28 , clipNode(node->clipNode)
29 , color(node->color.rgba())
30 , selectionState(node->selectionState)
31{
32}
33
35 SelectionState selState,
36 const QRectF &brect,
37 const Decorations &decs,
38 const QColor &c,
39 const QColor &bc, const QColor &dc,
40 const QPointF &pos, qreal a)
41 : glyphRun(g)
44 , clipNode(nullptr)
46 , color(c)
49 , position(pos)
50 , ascent(a)
51 , leftChildIndex(-1)
52 , rightChildIndex(-1)
53{
54 QGlyphRunPrivate *d = QGlyphRunPrivate::get(g);
55 ranges.append(std::make_pair(d->textRangeStart, d->textRangeEnd));
56}
57
58
59void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray<BinaryTreeNode, 16> *binaryTree, const QGlyphRun &glyphRun, SelectionState selectionState,
60 Decorations decorations, const QColor &textColor,
61 const QColor &backgroundColor, const QColor &decorationColor, const QPointF &position)
62{
63 QRectF searchRect = glyphRun.boundingRect();
64 searchRect.translate(position);
65
66 if (qFuzzyIsNull(searchRect.width()) || qFuzzyIsNull(searchRect.height()))
67 return;
68
69 decorations |= (glyphRun.underline() ? Decoration::Underline : Decoration::NoDecoration);
70 decorations |= (glyphRun.overline() ? Decoration::Overline : Decoration::NoDecoration);
71 decorations |= (glyphRun.strikeOut() ? Decoration::StrikeOut : Decoration::NoDecoration);
72 decorations |= (backgroundColor.isValid() ? Decoration::Background : Decoration::NoDecoration);
73
74 qreal ascent = glyphRun.rawFont().ascent();
75 insert(binaryTree, BinaryTreeNode(glyphRun,
76 selectionState,
77 searchRect,
78 decorations,
79 textColor,
80 backgroundColor,
81 decorationColor,
82 position,
83 ascent));
84}
85
86void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray<BinaryTreeNode, 16> *binaryTree, const BinaryTreeNode &binaryTreeNode)
87{
88 int newIndex = binaryTree->size();
89 binaryTree->append(binaryTreeNode);
90 if (newIndex == 0)
91 return;
92
93 int searchIndex = 0;
94 forever {
95 BinaryTreeNode *node = binaryTree->data() + searchIndex;
96 if (binaryTreeNode.boundingRect.left() < node->boundingRect.left()) {
97 if (node->leftChildIndex < 0) {
98 node->leftChildIndex = newIndex;
99 break;
100 } else {
101 searchIndex = node->leftChildIndex;
102 }
103 } else {
104 if (node->rightChildIndex < 0) {
105 node->rightChildIndex = newIndex;
106 break;
107 } else {
108 searchIndex = node->rightChildIndex;
109 }
110 }
111 }
112}
113
114void QQuickTextNodeEngine::BinaryTreeNode::inOrder(const QVarLengthArray<BinaryTreeNode, 16> &binaryTree,
115 QVarLengthArray<int> *sortedIndexes, int currentIndex)
116{
117 Q_ASSERT(currentIndex < binaryTree.size());
118
119 const BinaryTreeNode *node = binaryTree.data() + currentIndex;
120 if (node->leftChildIndex >= 0)
121 inOrder(binaryTree, sortedIndexes, node->leftChildIndex);
122
123 sortedIndexes->append(currentIndex);
124
125 if (node->rightChildIndex >= 0)
126 inOrder(binaryTree, sortedIndexes, node->rightChildIndex);
127}
128
129
130int QQuickTextNodeEngine::addText(const QTextBlock &block,
131 const QTextCharFormat &charFormat,
132 const QColor &textColor,
133 const QVarLengthArray<QTextLayout::FormatRange> &colorChanges,
134 int textPos, int fragmentEnd,
135 int selectionStart, int selectionEnd)
136{
137 if (charFormat.foreground().style() != Qt::NoBrush)
138 setTextColor(charFormat.foreground().color());
139 else
140 setTextColor(textColor);
141
142 while (textPos < fragmentEnd) {
143 int blockRelativePosition = textPos - block.position();
144 QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition);
145 if (!line.isValid())
146 break;
147 if (!currentLine().isValid()
148 || line.lineNumber() != currentLine().lineNumber()) {
150 }
151
152 Q_ASSERT(line.textLength() > 0);
153 int lineEnd = line.textStart() + block.position() + line.textLength();
154
155 int len = qMin(lineEnd - textPos, fragmentEnd - textPos);
156 Q_ASSERT(len > 0);
157
158 int currentStepEnd = textPos + len;
159
160 addGlyphsForRanges(colorChanges,
161 textPos - block.position(),
162 currentStepEnd - block.position(),
163 selectionStart - block.position(),
164 selectionEnd - block.position());
165
166 textPos = currentStepEnd;
167 }
168 return textPos;
169}
170
171void QQuickTextNodeEngine::addTextDecorations(const QVarLengthArray<TextDecoration> &textDecorations,
172 qreal offset, qreal thickness)
173{
174 for (int i=0; i<textDecorations.size(); ++i) {
175 TextDecoration textDecoration = textDecorations.at(i);
176
177 {
178 QRectF &rect = textDecoration.rect;
179 rect.setY(qRound(rect.y() + m_currentLine.ascent() + offset));
180 rect.setHeight(thickness);
181 }
182
183 m_lines.append(textDecoration);
184 }
185}
186
187void QQuickTextNodeEngine::processCurrentLine()
188{
189 // No glyphs, do nothing
190 if (m_currentLineTree.isEmpty())
191 return;
192
193 // 1. Go through current line and get correct decoration position for each node based on
194 // neighbouring decorations. Add decoration to global list
195 // 2. Create clip nodes for all selected text. Try to merge as many as possible within
196 // the line.
197 // 3. Add QRects to a list of selection rects.
198 // 4. Add all nodes to a global processed list
199 QVarLengthArray<int> sortedIndexes; // Indexes in tree sorted by x position
200 BinaryTreeNode::inOrder(m_currentLineTree, &sortedIndexes);
201
202 Q_ASSERT(sortedIndexes.size() == m_currentLineTree.size());
203
204 SelectionState currentSelectionState = Unselected;
205 QRectF currentRect;
206
207 Decorations currentDecorations = Decoration::NoDecoration;
208 qreal underlineOffset = 0.0;
209 qreal underlineThickness = 0.0;
210
211 qreal overlineOffset = 0.0;
212 qreal overlineThickness = 0.0;
213
214 qreal strikeOutOffset = 0.0;
215 qreal strikeOutThickness = 0.0;
216
217 QRectF decorationRect = currentRect;
218
219 QColor lastColor;
220 QColor lastBackgroundColor;
221 QColor lastDecorationColor;
222
223 QVarLengthArray<TextDecoration> pendingUnderlines;
224 QVarLengthArray<TextDecoration> pendingOverlines;
225 QVarLengthArray<TextDecoration> pendingStrikeOuts;
226 if (!sortedIndexes.isEmpty()) {
227 QQuickDefaultClipNode *currentClipNode = m_hasSelection ? new QQuickDefaultClipNode(QRectF()) : nullptr;
228 bool currentClipNodeUsed = false;
229 for (int i=0; i<=sortedIndexes.size(); ++i) {
230 BinaryTreeNode *node = nullptr;
231 if (i < sortedIndexes.size()) {
232 int sortedIndex = sortedIndexes.at(i);
233 Q_ASSERT(sortedIndex < m_currentLineTree.size());
234
235 node = m_currentLineTree.data() + sortedIndex;
236 if (i == 0)
237 currentSelectionState = node->selectionState;
238 }
239
240 // Update decorations
241 if (currentDecorations != Decoration::NoDecoration) {
242 decorationRect.setY(m_position.y() + m_currentLine.y());
243 decorationRect.setHeight(m_currentLine.height());
244
245 if (node != nullptr)
246 decorationRect.setRight(node->boundingRect.left());
247
248 TextDecoration textDecoration(currentSelectionState, decorationRect, lastColor);
249 if (lastDecorationColor.isValid() &&
250 (currentDecorations.testFlag(Decoration::Underline) ||
251 currentDecorations.testFlag(Decoration::Overline) ||
252 currentDecorations.testFlag(Decoration::StrikeOut)))
253 textDecoration.color = lastDecorationColor;
254
255 if (currentDecorations & Decoration::Underline)
256 pendingUnderlines.append(textDecoration);
257
258 if (currentDecorations & Decoration::Overline)
259 pendingOverlines.append(textDecoration);
260
261 if (currentDecorations & Decoration::StrikeOut)
262 pendingStrikeOuts.append(textDecoration);
263
264 if (currentDecorations & Decoration::Background)
265 m_backgrounds.append(std::make_pair(decorationRect, lastBackgroundColor));
266 }
267
268 // If we've reached an unselected node from a selected node, we add the
269 // selection rect to the graph, and we add decoration every time the
270 // selection state changes, because that means the text color changes
271 if (node == nullptr || node->selectionState != currentSelectionState) {
272 currentRect.setY(m_position.y() + m_currentLine.y());
273 currentRect.setHeight(m_currentLine.height());
274
275 if (currentSelectionState == Selected)
276 m_selectionRects.append(currentRect);
277
278 if (currentClipNode != nullptr) {
279 if (!currentClipNodeUsed) {
280 delete currentClipNode;
281 } else {
282 currentClipNode->setIsRectangular(true);
283 currentClipNode->setRect(currentRect);
284 currentClipNode->update();
285 }
286 }
287
288 if (node != nullptr && m_hasSelection)
289 currentClipNode = new QQuickDefaultClipNode(QRectF());
290 else
291 currentClipNode = nullptr;
292 currentClipNodeUsed = false;
293
294 if (node != nullptr) {
295 currentSelectionState = node->selectionState;
296 currentRect = node->boundingRect;
297
298 // Make sure currentRect is valid, otherwise the unite won't work
299 if (currentRect.isNull())
300 currentRect.setSize(QSizeF(1, 1));
301 }
302 } else {
303 if (currentRect.isNull())
304 currentRect = node->boundingRect;
305 else
306 currentRect = currentRect.united(node->boundingRect);
307 }
308
309 if (node != nullptr) {
310 if (node->selectionState == Selected) {
311 node->clipNode = currentClipNode;
312 currentClipNodeUsed = true;
313 }
314
315 decorationRect = node->boundingRect;
316
317 // If previous item(s) had underline and current does not, then we add the
318 // pending lines to the lists and likewise for overlines and strikeouts
319 if (!pendingUnderlines.isEmpty()
320 && !(node->decorations & Decoration::Underline)) {
321 addTextDecorations(pendingUnderlines, underlineOffset, underlineThickness);
322
323 pendingUnderlines.clear();
324
325 underlineOffset = 0.0;
326 underlineThickness = 0.0;
327 }
328
329 // ### Add pending when overlineOffset/thickness changes to minimize number of
330 // nodes
331 if (!pendingOverlines.isEmpty()) {
332 addTextDecorations(pendingOverlines, overlineOffset, overlineThickness);
333
334 pendingOverlines.clear();
335
336 overlineOffset = 0.0;
337 overlineThickness = 0.0;
338 }
339
340 // ### Add pending when overlineOffset/thickness changes to minimize number of
341 // nodes
342 if (!pendingStrikeOuts.isEmpty()) {
343 addTextDecorations(pendingStrikeOuts, strikeOutOffset, strikeOutThickness);
344
345 pendingStrikeOuts.clear();
346
347 strikeOutOffset = 0.0;
348 strikeOutThickness = 0.0;
349 }
350
351 // Merge current values with previous. Prefer greatest thickness
352 QRawFont rawFont = node->glyphRun.rawFont();
353 if (node->decorations & Decoration::Underline) {
354 if (rawFont.lineThickness() > underlineThickness) {
355 underlineThickness = rawFont.lineThickness();
356 underlineOffset = rawFont.underlinePosition();
357 }
358 }
359
360 if (node->decorations & Decoration::Overline) {
361 overlineOffset = -rawFont.ascent();
362 overlineThickness = rawFont.lineThickness();
363 }
364
365 if (node->decorations & Decoration::StrikeOut) {
366 strikeOutThickness = rawFont.lineThickness();
367 strikeOutOffset = rawFont.ascent() / -3.0;
368 }
369
370 currentDecorations = node->decorations;
371 lastColor = node->color;
372 lastBackgroundColor = node->backgroundColor;
373 lastDecorationColor = node->decorationColor;
374 m_processedNodes.append(*node);
375 }
376 }
377
378 if (!pendingUnderlines.isEmpty())
379 addTextDecorations(pendingUnderlines, underlineOffset, underlineThickness);
380
381 if (!pendingOverlines.isEmpty())
382 addTextDecorations(pendingOverlines, overlineOffset, overlineThickness);
383
384 if (!pendingStrikeOuts.isEmpty())
385 addTextDecorations(pendingStrikeOuts, strikeOutOffset, strikeOutThickness);
386 }
387
388 m_currentLineTree.clear();
389 m_currentLine = QTextLine();
390 m_hasSelection = false;
391}
392
393void QQuickTextNodeEngine::addImage(const QRectF &rect, const QImage &image, qreal ascent,
394 SelectionState selectionState,
395 QTextFrameFormat::Position layoutPosition)
396{
397 QRectF searchRect = rect;
398 if (layoutPosition == QTextFrameFormat::InFlow) {
399 if (m_currentLineTree.isEmpty()) {
400 qreal y = m_currentLine.ascent() - ascent;
401 if (m_currentTextDirection == Qt::RightToLeft)
402 searchRect.moveTopRight(m_position + m_currentLine.rect().topRight() + QPointF(0, y));
403 else
404 searchRect.moveTopLeft(m_position + m_currentLine.position() + QPointF(0, y));
405 } else {
406 const BinaryTreeNode *lastNode = m_currentLineTree.data() + m_currentLineTree.size() - 1;
407 if (lastNode->glyphRun.isRightToLeft()) {
408 QPointF lastPos = lastNode->boundingRect.topLeft();
409 searchRect.moveTopRight(lastPos - QPointF(0, ascent - lastNode->ascent));
410 } else {
411 QPointF lastPos = lastNode->boundingRect.topRight();
412 searchRect.moveTopLeft(lastPos - QPointF(0, ascent - lastNode->ascent));
413 }
414 }
415 }
416
417 BinaryTreeNode::insert(&m_currentLineTree, searchRect, image, ascent, selectionState);
418 m_hasContents = true;
419}
420
421void QQuickTextNodeEngine::addTextObject(const QTextBlock &block, const QPointF &position, const QTextCharFormat &format,
422 SelectionState selectionState,
423 QTextDocument *textDocument, int pos,
424 QTextFrameFormat::Position layoutPosition)
425{
426 QTextObjectInterface *handler = textDocument->documentLayout()->handlerForObject(format.objectType());
427 if (handler != nullptr) {
428 QImage image;
429 QSizeF size = handler->intrinsicSize(textDocument, pos, format);
430
431 if (format.objectType() == QTextFormat::ImageObject) {
432 QTextImageFormat imageFormat = format.toImageFormat();
433 QTextImageHandler *imageHandler = static_cast<QTextImageHandler *>(handler);
434 image = imageHandler->image(textDocument, imageFormat);
435 }
436
437 if (image.isNull()) {
438 image = QImage((size * m_devicePixelRatio).toSize(), QImage::Format_ARGB32_Premultiplied);
439 image.setDevicePixelRatio(m_devicePixelRatio);
440 image.fill(Qt::transparent);
441 {
442 QPainter painter(&image);
443 handler->drawObject(&painter, QRectF({}, size), textDocument, pos, format);
444 }
445 }
446
447 // Use https://developer.mozilla.org/de/docs/Web/CSS/vertical-align as a reference
448 // The top/bottom positions are supposed to be higher/lower than the text and reference
449 // the line height, not the text height (using QFontMetrics)
450 qreal ascent;
451 QTextLine line = block.layout()->lineForTextPosition(pos - block.position());
452 switch (format.verticalAlignment())
453 {
454 case QTextCharFormat::AlignTop:
455 ascent = line.ascent();
456 break;
457 case QTextCharFormat::AlignMiddle:
458 // Middlepoint of line (height - descent) + Half object height
459 ascent = (line.ascent() + line.descent()) / 2 - line.descent() + size.height() / 2;
460 break;
461 case QTextCharFormat::AlignBottom:
462 ascent = size.height() - line.descent();
463 break;
464 case QTextCharFormat::AlignBaseline:
465 default:
466 ascent = size.height();
467 }
468
469 addImage(QRectF(position, size), image, ascent, selectionState, layoutPosition);
470 }
471}
472
473void QQuickTextNodeEngine::addUnselectedGlyphs(const QGlyphRun &glyphRun)
474{
475 BinaryTreeNode::insert(&m_currentLineTree,
476 glyphRun,
477 Unselected,
478 Decoration::NoDecoration,
479 m_textColor,
480 m_backgroundColor,
481 m_decorationColor,
482 m_position);
483}
484
485void QQuickTextNodeEngine::addSelectedGlyphs(const QGlyphRun &glyphRun)
486{
487 int currentSize = m_currentLineTree.size();
488 BinaryTreeNode::insert(&m_currentLineTree,
489 glyphRun,
490 Selected,
491 Decoration::NoDecoration,
492 m_textColor,
493 m_backgroundColor,
494 m_decorationColor,
495 m_position);
496 m_hasSelection = m_hasSelection || m_currentLineTree.size() > currentSize;
497}
498
499void QQuickTextNodeEngine::addGlyphsForRanges(const QVarLengthArray<QTextLayout::FormatRange> &ranges,
500 int start, int end,
501 int selectionStart, int selectionEnd)
502{
503 int currentPosition = start;
504 int remainingLength = end - start;
505 for (int j=0; j<ranges.size(); ++j) {
506 const QTextLayout::FormatRange &range = ranges.at(j);
507 if (range.start + range.length > currentPosition
508 && range.start < currentPosition + remainingLength) {
509
510 if (range.start > currentPosition) {
511 addGlyphsInRange(currentPosition, range.start - currentPosition,
512 QColor(), QColor(), QColor(), selectionStart, selectionEnd);
513 }
514 int rangeEnd = qMin(range.start + range.length, currentPosition + remainingLength);
515 QColor rangeColor;
516 if (range.format.hasProperty(QTextFormat::ForegroundBrush))
517 rangeColor = range.format.foreground().color();
518 else if (range.format.isAnchor())
519 rangeColor = m_anchorColor;
520 QColor rangeBackgroundColor = range.format.hasProperty(QTextFormat::BackgroundBrush)
521 ? range.format.background().color()
522 : QColor();
523
524 QColor rangeDecorationColor = range.format.hasProperty(QTextFormat::TextUnderlineColor)
525 ? range.format.underlineColor()
526 : QColor();
527
528 addGlyphsInRange(range.start, rangeEnd - range.start,
529 rangeColor, rangeBackgroundColor, rangeDecorationColor,
530 selectionStart, selectionEnd);
531
532 currentPosition = range.start + range.length;
533 remainingLength = end - currentPosition;
534
535 } else if (range.start > currentPosition + remainingLength || remainingLength <= 0) {
536 break;
537 }
538 }
539
540 if (remainingLength > 0) {
541 addGlyphsInRange(currentPosition, remainingLength, QColor(), QColor(), QColor(),
542 selectionStart, selectionEnd);
543 }
544
545}
546
547void QQuickTextNodeEngine::addGlyphsInRange(int rangeStart, int rangeLength,
548 const QColor &color, const QColor &backgroundColor, const QColor &decorationColor,
549 int selectionStart, int selectionEnd)
550{
551 QColor oldColor;
552 if (color.isValid()) {
553 oldColor = m_textColor;
554 m_textColor = color;
555 }
556
557 QColor oldBackgroundColor = m_backgroundColor;
558 if (backgroundColor.isValid()) {
559 oldBackgroundColor = m_backgroundColor;
560 m_backgroundColor = backgroundColor;
561 }
562
563 QColor oldDecorationColor = m_decorationColor;
564 if (decorationColor.isValid()) {
565 oldDecorationColor = m_decorationColor;
566 m_decorationColor = decorationColor;
567 }
568
569 bool hasSelection = selectionEnd >= 0
570 && selectionStart <= selectionEnd;
571
572 QTextLine &line = m_currentLine;
573 int rangeEnd = rangeStart + rangeLength;
574 if (!hasSelection || (selectionStart > rangeEnd || selectionEnd < rangeStart)) {
575 QList<QGlyphRun> glyphRuns = line.glyphRuns(rangeStart, rangeLength);
576 for (int j=0; j<glyphRuns.size(); ++j) {
577 const QGlyphRun &glyphRun = glyphRuns.at(j);
578 addUnselectedGlyphs(glyphRun);
579 }
580 } else {
581 if (rangeStart < selectionStart) {
582 int length = qMin(selectionStart - rangeStart, rangeLength);
583 QList<QGlyphRun> glyphRuns = line.glyphRuns(rangeStart, length);
584 for (int j=0; j<glyphRuns.size(); ++j) {
585 const QGlyphRun &glyphRun = glyphRuns.at(j);
586 addUnselectedGlyphs(glyphRun);
587 }
588 }
589
590 if (rangeEnd > selectionStart) {
591 int start = qMax(selectionStart, rangeStart);
592 int length = qMin(selectionEnd - start + 1, rangeEnd - start);
593 QList<QGlyphRun> glyphRuns = line.glyphRuns(start, length);
594
595 for (int j=0; j<glyphRuns.size(); ++j) {
596 const QGlyphRun &glyphRun = glyphRuns.at(j);
597 addSelectedGlyphs(glyphRun);
598 }
599 }
600
601 if (selectionEnd >= rangeStart && selectionEnd < rangeEnd) {
602 int start = selectionEnd + 1;
603 int length = rangeEnd - selectionEnd - 1;
604 QList<QGlyphRun> glyphRuns = line.glyphRuns(start, length);
605 for (int j=0; j<glyphRuns.size(); ++j) {
606 const QGlyphRun &glyphRun = glyphRuns.at(j);
607 addUnselectedGlyphs(glyphRun);
608 }
609 }
610 }
611
612 if (decorationColor.isValid())
613 m_decorationColor = oldDecorationColor;
614
615 if (backgroundColor.isValid())
616 m_backgroundColor = oldBackgroundColor;
617
618 if (oldColor.isValid())
619 m_textColor = oldColor;
620}
621
622void QQuickTextNodeEngine::addBorder(const QRectF &rect, qreal border,
623 QTextFrameFormat::BorderStyle borderStyle,
624 const QBrush &borderBrush)
625{
626 const QColor &color = borderBrush.color();
627
628 // Currently we don't support other styles than solid
629 Q_UNUSED(borderStyle);
630
631 m_backgrounds.append(std::make_pair(QRectF(rect.left(), rect.top(), border, rect.height() + border), color));
632 m_backgrounds.append(std::make_pair(QRectF(rect.left() + border, rect.top(), rect.width(), border), color));
633 m_backgrounds.append(std::make_pair(QRectF(rect.right(), rect.top() + border, border, rect.height() - border), color));
634 m_backgrounds.append(std::make_pair(QRectF(rect.left() + border, rect.bottom(), rect.width(), border), color));
635}
636
637void QQuickTextNodeEngine::addFrameDecorations(QTextDocument *document, QTextFrame *frame)
638{
639 QTextDocumentLayout *documentLayout = qobject_cast<QTextDocumentLayout *>(document->documentLayout());
640 if (Q_UNLIKELY(!documentLayout))
641 return;
642
643 QTextFrameFormat frameFormat = frame->format().toFrameFormat();
644 QTextTable *table = qobject_cast<QTextTable *>(frame);
645
646 QRectF boundingRect = table == nullptr
647 ? documentLayout->frameBoundingRect(frame)
648 : documentLayout->tableBoundingRect(table);
649
650 QBrush bg = frame->frameFormat().background();
651 if (bg.style() != Qt::NoBrush)
652 m_backgrounds.append(std::make_pair(boundingRect, bg.color()));
653
654 if (!frameFormat.hasProperty(QTextFormat::FrameBorder))
655 return;
656
657 qreal borderWidth = frameFormat.border();
658 if (qFuzzyIsNull(borderWidth))
659 return;
660
661 QBrush borderBrush = frameFormat.borderBrush();
662 QTextFrameFormat::BorderStyle borderStyle = frameFormat.borderStyle();
663 if (borderStyle == QTextFrameFormat::BorderStyle_None)
664 return;
665
666 const auto collapsed = table->format().borderCollapse();
667
668 if (!collapsed) {
669 addBorder(boundingRect.adjusted(frameFormat.leftMargin(), frameFormat.topMargin(),
670 -frameFormat.rightMargin() - borderWidth,
671 -frameFormat.bottomMargin() - borderWidth),
672 borderWidth, borderStyle, borderBrush);
673 }
674 if (table != nullptr) {
675 int rows = table->rows();
676 int columns = table->columns();
677
678 for (int row=0; row<rows; ++row) {
679 for (int column=0; column<columns; ++column) {
680 QTextTableCell cell = table->cellAt(row, column);
681
682 QRectF cellRect = documentLayout->tableCellBoundingRect(table, cell);
683 addBorder(cellRect.adjusted(-borderWidth, -borderWidth, collapsed ? -borderWidth : 0, collapsed ? -borderWidth : 0), borderWidth,
684 borderStyle, borderBrush);
685 }
686 }
687 }
688}
689
691{
692 return qHashMulti(seed, key.fontEngine, key.clipNode, key.color, key.selectionState);
693}
694
695void QQuickTextNodeEngine::mergeProcessedNodes(QList<BinaryTreeNode *> *regularNodes,
696 QList<BinaryTreeNode *> *imageNodes)
697{
698 QHash<BinaryTreeNodeKey, QList<BinaryTreeNode *> > map;
699
700 for (int i = 0; i < m_processedNodes.size(); ++i) {
701 BinaryTreeNode *node = m_processedNodes.data() + i;
702
703 if (node->image.isNull()) {
704 if (node->glyphRun.isEmpty())
705 continue;
706
707 BinaryTreeNodeKey key(node);
708
709 QList<BinaryTreeNode *> &nodes = map[key];
710 if (nodes.isEmpty())
711 regularNodes->append(node);
712
713 nodes.append(node);
714 } else {
715 imageNodes->append(node);
716 }
717 }
718
719 for (int i = 0; i < regularNodes->size(); ++i) {
720 BinaryTreeNode *primaryNode = regularNodes->at(i);
721 BinaryTreeNodeKey key(primaryNode);
722
723 const QList<BinaryTreeNode *> &nodes = map.value(key);
724 Q_ASSERT(nodes.first() == primaryNode);
725
726 int count = 0;
727 for (int j = 0; j < nodes.size(); ++j)
728 count += nodes.at(j)->glyphRun.glyphIndexes().size();
729
730 if (count != primaryNode->glyphRun.glyphIndexes().size()) {
731 QGlyphRun &glyphRun = primaryNode->glyphRun;
732 QVector<quint32> glyphIndexes = glyphRun.glyphIndexes();
733 glyphIndexes.reserve(count);
734
735 QVector<QPointF> glyphPositions = glyphRun.positions();
736 glyphPositions.reserve(count);
737
738 QRectF glyphBoundingRect = glyphRun.boundingRect();
739
740 for (int j = 1; j < nodes.size(); ++j) {
741 BinaryTreeNode *otherNode = nodes.at(j);
742 glyphIndexes += otherNode->glyphRun.glyphIndexes();
743 primaryNode->ranges += otherNode->ranges;
744 glyphBoundingRect = glyphBoundingRect.united(otherNode->boundingRect);
745
746 QVector<QPointF> otherPositions = otherNode->glyphRun.positions();
747 for (int k = 0; k < otherPositions.size(); ++k)
748 glyphPositions += otherPositions.at(k) + (otherNode->position - primaryNode->position);
749 }
750
751 Q_ASSERT(glyphPositions.size() == count);
752 Q_ASSERT(glyphIndexes.size() == count);
753
754 glyphRun.setGlyphIndexes(glyphIndexes);
755 glyphRun.setPositions(glyphPositions);
756 glyphRun.setBoundingRect(glyphBoundingRect);
757 }
758 }
759}
760
761void QQuickTextNodeEngine::addToSceneGraph(QSGInternalTextNode *parentNode,
762 QQuickText::TextStyle style,
763 const QColor &styleColor)
764{
765 if (m_currentLine.isValid())
766 processCurrentLine();
767
768 QList<BinaryTreeNode *> nodes;
769 QList<BinaryTreeNode *> imageNodes;
770 mergeProcessedNodes(&nodes, &imageNodes);
771
772 for (int i = 0; i < m_backgrounds.size(); ++i) {
773 const QRectF &rect = m_backgrounds.at(i).first;
774 const QColor &color = m_backgrounds.at(i).second;
775 if (color.alpha() != 0)
776 parentNode->addRectangleNode(rect, color);
777 }
778
779 // Add all text with unselected color first
780 for (int i = 0; i < nodes.size(); ++i) {
781 const BinaryTreeNode *node = nodes.at(i);
782 parentNode->addGlyphs(node->position, node->glyphRun, node->color, style, styleColor, nullptr);
783 }
784
785 for (int i = 0; i < imageNodes.size(); ++i) {
786 const BinaryTreeNode *node = imageNodes.at(i);
787 if (node->selectionState == Unselected)
788 parentNode->addImage(node->boundingRect, node->image);
789 }
790
791 // Then, prepend all selection rectangles to the tree
792 for (int i = 0; i < m_selectionRects.size(); ++i) {
793 const QRectF &rect = m_selectionRects.at(i);
794 if (m_selectionColor.alpha() != 0)
795 parentNode->addRectangleNode(rect, m_selectionColor);
796 }
797
798 // Add decorations for each node to the tree.
799 for (int i = 0; i < m_lines.size(); ++i) {
800 const TextDecoration &textDecoration = m_lines.at(i);
801
802 QColor color = textDecoration.selectionState == Selected
803 ? m_selectedTextColor
804 : textDecoration.color;
805
806 parentNode->addDecorationNode(textDecoration.rect, color);
807 }
808
809 // Finally add the selected text on top of everything
810 for (int i = 0; i < nodes.size(); ++i) {
811 const BinaryTreeNode *node = nodes.at(i);
812 QQuickDefaultClipNode *clipNode = node->clipNode;
813 if (clipNode != nullptr && clipNode->parent() == nullptr)
814 parentNode->appendChildNode(clipNode);
815
816 if (node->selectionState == Selected) {
817 QColor color = m_selectedTextColor;
818 int previousNodeIndex = i - 1;
819 int nextNodeIndex = i + 1;
820 const BinaryTreeNode *previousNode = previousNodeIndex < 0 ? 0 : nodes.at(previousNodeIndex);
821 while (previousNode != nullptr && qFuzzyCompare(previousNode->boundingRect.left(), node->boundingRect.left()))
822 previousNode = --previousNodeIndex < 0 ? 0 : nodes.at(previousNodeIndex);
823
824 const BinaryTreeNode *nextNode = nextNodeIndex == nodes.size() ? 0 : nodes.at(nextNodeIndex);
825
826 if (previousNode != nullptr && previousNode->selectionState == Unselected)
827 parentNode->addGlyphs(previousNode->position, previousNode->glyphRun, color, style, styleColor, clipNode);
828
829 if (nextNode != nullptr && nextNode->selectionState == Unselected)
830 parentNode->addGlyphs(nextNode->position, nextNode->glyphRun, color, style, styleColor, clipNode);
831
832 // If the previous or next node completely overlaps this one, then we have already drawn the glyphs of
833 // this node
834 bool drawCurrent = false;
835 if (previousNode != nullptr || nextNode != nullptr) {
836 for (int i = 0; i < node->ranges.size(); ++i) {
837 const std::pair<int, int> &range = node->ranges.at(i);
838
839 int rangeLength = range.second - range.first + 1;
840 if (previousNode != nullptr) {
841 for (int j = 0; j < previousNode->ranges.size(); ++j) {
842 const std::pair<int, int> &otherRange = previousNode->ranges.at(j);
843
844 if (range.first < otherRange.second && range.second > otherRange.first) {
845 int start = qMax(range.first, otherRange.first);
846 int end = qMin(range.second, otherRange.second);
847 rangeLength -= end - start + 1;
848 if (rangeLength == 0)
849 break;
850 }
851 }
852 }
853
854 if (nextNode != nullptr && rangeLength > 0) {
855 for (int j = 0; j < nextNode->ranges.size(); ++j) {
856 const std::pair<int, int> &otherRange = nextNode->ranges.at(j);
857
858 if (range.first < otherRange.second && range.second > otherRange.first) {
859 int start = qMax(range.first, otherRange.first);
860 int end = qMin(range.second, otherRange.second);
861 rangeLength -= end - start + 1;
862 if (rangeLength == 0)
863 break;
864 }
865 }
866 }
867
868 if (rangeLength > 0) {
869 drawCurrent = true;
870 break;
871 }
872 }
873 } else {
874 drawCurrent = true;
875 }
876
877 if (drawCurrent)
878 parentNode->addGlyphs(node->position, node->glyphRun, color, style, styleColor, clipNode);
879 }
880 }
881
882 for (int i = 0; i < imageNodes.size(); ++i) {
883 const BinaryTreeNode *node = imageNodes.at(i);
884 if (node->selectionState == Selected) {
885 parentNode->addImage(node->boundingRect, node->image);
886 if (node->selectionState == Selected) {
887 QColor color = m_selectionColor;
888 color.setAlpha(128);
889 parentNode->addRectangleNode(node->boundingRect, color);
890 }
891 }
892 }
893}
894
895void QQuickTextNodeEngine::mergeFormats(QTextLayout *textLayout, QVarLengthArray<QTextLayout::FormatRange> *mergedFormats)
896{
897 Q_ASSERT(mergedFormats != nullptr);
898 if (textLayout == nullptr)
899 return;
900
901 QVector<QTextLayout::FormatRange> additionalFormats = textLayout->formats();
902 for (int i=0; i<additionalFormats.size(); ++i) {
903 QTextLayout::FormatRange additionalFormat = additionalFormats.at(i);
904 if (additionalFormat.format.hasProperty(QTextFormat::ForegroundBrush)
905 || additionalFormat.format.hasProperty(QTextFormat::BackgroundBrush)
906 || additionalFormat.format.isAnchor()) {
907 // Merge overlapping formats
908 if (!mergedFormats->isEmpty()) {
909 QTextLayout::FormatRange *lastFormat = mergedFormats->data() + mergedFormats->size() - 1;
910
911 if (additionalFormat.start < lastFormat->start + lastFormat->length) {
912 QTextLayout::FormatRange *mergedRange = nullptr;
913
914 int length = additionalFormat.length;
915 if (additionalFormat.start > lastFormat->start) {
916 lastFormat->length = additionalFormat.start - lastFormat->start;
917 length -= lastFormat->length;
918
919 mergedFormats->append(QTextLayout::FormatRange());
920 mergedRange = mergedFormats->data() + mergedFormats->size() - 1;
921 lastFormat = mergedFormats->data() + mergedFormats->size() - 2;
922 } else {
923 mergedRange = lastFormat;
924 }
925
926 mergedRange->format = lastFormat->format;
927 mergedRange->format.merge(additionalFormat.format);
928 mergedRange->start = additionalFormat.start;
929
930 int end = qMin(additionalFormat.start + additionalFormat.length,
931 lastFormat->start + lastFormat->length);
932
933 mergedRange->length = end - mergedRange->start;
934 length -= mergedRange->length;
935
936 additionalFormat.start = end;
937 additionalFormat.length = length;
938 }
939 }
940
941 if (additionalFormat.length > 0)
942 mergedFormats->append(additionalFormat);
943 }
944 }
945
946}
947
948/*!
949 \internal
950 Adds the \a block from the \a textDocument at \a position if its
951 \l {QAbstractTextDocumentLayout::blockBoundingRect()}{bounding rect}
952 intersects the \a viewport, or if \c viewport is not valid
953 (i.e. use a default-constructed QRectF to skip the viewport check).
954
955 \sa QQuickItem::clipRect()
956 */
957void QQuickTextNodeEngine::addTextBlock(QTextDocument *textDocument, const QTextBlock &block, const QPointF &position,
958 const QColor &textColor, const QColor &anchorColor, int selectionStart, int selectionEnd, const QRectF &viewport)
959{
960 Q_ASSERT(textDocument);
961#if QT_CONFIG(im)
962 int preeditLength = block.isValid() ? block.layout()->preeditAreaText().size() : 0;
963 int preeditPosition = block.isValid() ? block.layout()->preeditAreaPosition() : -1;
964#endif
965
966 setCurrentTextDirection(block.textDirection());
967
968 QVarLengthArray<QTextLayout::FormatRange> colorChanges;
969 mergeFormats(block.layout(), &colorChanges);
970
971 const QTextCharFormat charFormat = block.charFormat();
972 const QRectF blockBoundingRect = textDocument->documentLayout()->blockBoundingRect(block).translated(position);
973 if (viewport.isValid()) {
974 if (!blockBoundingRect.intersects(viewport))
975 return;
976 qCDebug(lcSgText) << "adding block with length" << block.length() << ':' << blockBoundingRect << "in viewport" << viewport;
977 }
978
979 if (charFormat.background().style() != Qt::NoBrush)
980 m_backgrounds.append(std::make_pair(blockBoundingRect, charFormat.background().color()));
981
982 if (QTextList *textList = block.textList()) {
983 QPointF pos = blockBoundingRect.topLeft();
984 QTextLayout *layout = block.layout();
985 if (layout->lineCount() > 0) {
986 QTextLine firstLine = layout->lineAt(0);
987 Q_ASSERT(firstLine.isValid());
988
989 setCurrentLine(firstLine);
990
991 QRectF textRect = firstLine.naturalTextRect();
992 pos += textRect.topLeft();
993 if (block.textDirection() == Qt::RightToLeft)
994 pos.rx() += textRect.width();
995
996 QFont font(charFormat.font());
997 QFontMetricsF fontMetrics(font);
998 QTextListFormat listFormat = textList->format();
999
1000 QString listItemBullet;
1001 switch (listFormat.style()) {
1002 case QTextListFormat::ListCircle:
1003 listItemBullet = QChar(0x25E6); // White bullet
1004 break;
1005 case QTextListFormat::ListSquare:
1006 listItemBullet = QChar(0x25AA); // Black small square
1007 break;
1008 case QTextListFormat::ListDecimal:
1009 case QTextListFormat::ListLowerAlpha:
1010 case QTextListFormat::ListUpperAlpha:
1011 case QTextListFormat::ListLowerRoman:
1012 case QTextListFormat::ListUpperRoman:
1013 listItemBullet = textList->itemText(block);
1014 break;
1015 default:
1016 listItemBullet = QChar(0x2022); // Black bullet
1017 break;
1018 };
1019
1020 switch (block.blockFormat().marker()) {
1021 case QTextBlockFormat::MarkerType::Checked:
1022 listItemBullet = QChar(0x2612); // Checked checkbox
1023 break;
1024 case QTextBlockFormat::MarkerType::Unchecked:
1025 listItemBullet = QChar(0x2610); // Unchecked checkbox
1026 break;
1027 case QTextBlockFormat::MarkerType::NoMarker:
1028 break;
1029 }
1030
1031 QSizeF size(fontMetrics.horizontalAdvance(listItemBullet), fontMetrics.height());
1032 qreal xoff = fontMetrics.horizontalAdvance(QLatin1Char(' '));
1033 if (block.textDirection() == Qt::LeftToRight)
1034 xoff = -xoff - size.width();
1035 setPosition(pos + QPointF(xoff, 0));
1036
1037 QTextLayout layout;
1038 layout.setFont(font);
1039 layout.setText(listItemBullet); // Bullet
1040 layout.beginLayout();
1041 QTextLine line = layout.createLine();
1042 line.setPosition(QPointF(0, 0));
1043 layout.endLayout();
1044
1045 // set the color for the bullets, instead of using the previous QTextBlock's color.
1046 if (charFormat.foreground().style() == Qt::NoBrush)
1047 setTextColor(textColor);
1048 else
1049 setTextColor(charFormat.foreground().color());
1050
1051 QList<QGlyphRun> glyphRuns = layout.glyphRuns();
1052 for (int i=0; i<glyphRuns.size(); ++i)
1053 addUnselectedGlyphs(glyphRuns.at(i));
1054 }
1055 }
1056
1057 int textPos = block.position();
1058 QTextBlock::iterator blockIterator = block.begin();
1059
1060 while (!blockIterator.atEnd()) {
1061 QTextFragment fragment = blockIterator.fragment();
1062 QString text = fragment.text();
1063 if (text.isEmpty())
1064 continue;
1065
1066 QTextCharFormat charFormat = fragment.charFormat();
1067 QFont font(charFormat.font());
1068 QFontMetricsF fontMetrics(font);
1069
1070 int fontHeight = fontMetrics.descent() + fontMetrics.ascent();
1071 int valign = charFormat.verticalAlignment();
1072 if (valign == QTextCharFormat::AlignSuperScript)
1073 setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() - fontHeight / 2));
1074 else if (valign == QTextCharFormat::AlignSubScript)
1075 setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() + fontHeight / 6));
1076 else
1077 setPosition(blockBoundingRect.topLeft());
1078
1079 if (text.contains(QChar::ObjectReplacementCharacter) && charFormat.objectType() != QTextFormat::NoObject) {
1080 QTextFrame *frame = qobject_cast<QTextFrame *>(textDocument->objectForFormat(charFormat));
1081 if (!frame || frame->frameFormat().position() == QTextFrameFormat::InFlow) {
1082 int blockRelativePosition = textPos - block.position();
1083 QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition);
1084 if (!currentLine().isValid()
1085 || line.lineNumber() != currentLine().lineNumber()) {
1086 setCurrentLine(line);
1087 }
1088
1089 QQuickTextNodeEngine::SelectionState selectionState =
1090 (selectionStart < textPos + text.size()
1091 && selectionEnd >= textPos)
1092 ? QQuickTextNodeEngine::Selected
1093 : QQuickTextNodeEngine::Unselected;
1094
1095 addTextObject(block, QPointF(), charFormat, selectionState, textDocument, textPos);
1096 }
1097 textPos += text.size();
1098 } else {
1099 if (charFormat.foreground().style() != Qt::NoBrush)
1100 setTextColor(charFormat.foreground().color());
1101 else if (charFormat.isAnchor())
1102 setTextColor(anchorColor);
1103 else
1104 setTextColor(textColor);
1105
1106 int fragmentEnd = textPos + fragment.length();
1107#if QT_CONFIG(im)
1108 if (preeditPosition >= 0
1109 && (preeditPosition + block.position()) >= textPos
1110 && (preeditPosition + block.position()) <= fragmentEnd) {
1111 fragmentEnd += preeditLength;
1112 }
1113#endif
1114 if (charFormat.background().style() != Qt::NoBrush || charFormat.hasProperty(QTextFormat::TextUnderlineColor)) {
1115 QTextLayout::FormatRange additionalFormat;
1116 additionalFormat.start = textPos - block.position();
1117 additionalFormat.length = fragmentEnd - textPos;
1118 additionalFormat.format = charFormat;
1119 colorChanges << additionalFormat;
1120 }
1121
1122 textPos = addText(block, charFormat, textColor, colorChanges, textPos, fragmentEnd,
1123 selectionStart, selectionEnd);
1124 }
1125
1126 ++blockIterator;
1127 }
1128
1129#if QT_CONFIG(im)
1130 if (preeditLength >= 0 && textPos <= block.position() + preeditPosition) {
1131 setPosition(blockBoundingRect.topLeft());
1132 textPos = block.position() + preeditPosition;
1133 QTextLine line = block.layout()->lineForTextPosition(preeditPosition);
1134 if (!currentLine().isValid()
1135 || line.lineNumber() != currentLine().lineNumber()) {
1136 setCurrentLine(line);
1137 }
1138 textPos = addText(block, block.charFormat(), textColor, colorChanges,
1139 textPos, textPos + preeditLength,
1140 selectionStart, selectionEnd);
1141 }
1142#endif
1143
1144 // Add block decorations (so far only horizontal rules)
1145 if (block.blockFormat().hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
1146 auto ruleLength = qvariant_cast<QTextLength>(block.blockFormat().property(QTextFormat::BlockTrailingHorizontalRulerWidth));
1147 QRectF ruleRect(0, 0, ruleLength.value(blockBoundingRect.width()), 1);
1148 ruleRect.moveCenter(blockBoundingRect.center());
1149 const QColor ruleColor = block.blockFormat().hasProperty(QTextFormat::BackgroundBrush)
1150 ? qvariant_cast<QBrush>(block.blockFormat().property(QTextFormat::BackgroundBrush)).color()
1151 : m_textColor;
1152 m_lines.append(TextDecoration(QQuickTextNodeEngine::Unselected, ruleRect, ruleColor));
1153 }
1154
1155 setCurrentLine(QTextLine()); // Reset current line because the text layout changed
1156 m_hasContents = true;
1157}
1158
1159
1160QT_END_NAMESPACE
void addSelectedGlyphs(const QGlyphRun &glyphRun)
void addUnselectedGlyphs(const QGlyphRun &glyphRun)
void addTextObject(const QTextBlock &block, const QPointF &position, const QTextCharFormat &format, SelectionState selectionState, QTextDocument *textDocument, int pos, QTextFrameFormat::Position layoutPosition=QTextFrameFormat::InFlow)
void mergeProcessedNodes(QList< BinaryTreeNode * > *regularNodes, QList< BinaryTreeNode * > *imageNodes)
void addTextBlock(QTextDocument *, const QTextBlock &, const QPointF &position, const QColor &textColor, const QColor &anchorColor, int selectionStart, int selectionEnd, const QRectF &viewport=QRectF())
void addGlyphsInRange(int rangeStart, int rangeEnd, const QColor &color, const QColor &backgroundColor, const QColor &underlineColor, int selectionStart, int selectionEnd)
void setTextColor(const QColor &textColor)
void setCurrentLine(const QTextLine &currentLine)
int addText(const QTextBlock &block, const QTextCharFormat &charFormat, const QColor &textColor, const QVarLengthArray< QTextLayout::FormatRange > &colorChanges, int textPos, int fragmentEnd, int selectionStart, int selectionEnd)
void addImage(const QRectF &rect, const QImage &image, qreal ascent, SelectionState selectionState, QTextFrameFormat::Position layoutPosition)
void addBorder(const QRectF &rect, qreal border, QTextFrameFormat::BorderStyle borderStyle, const QBrush &borderBrush)
void addFrameDecorations(QTextDocument *document, QTextFrame *frame)
void addGlyphsForRanges(const QVarLengthArray< QTextLayout::FormatRange > &ranges, int start, int end, int selectionStart, int selectionEnd)
size_t qHash(const QQuickTextNodeEngine::BinaryTreeNodeKey &key, size_t seed=0)
BinaryTreeNode(const QGlyphRun &g, SelectionState selState, const QRectF &brect, const Decorations &decs, const QColor &c, const QColor &bc, const QColor &dc, const QPointF &pos, qreal a)