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