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

source code of qtdeclarative/src/quick/items/qquicktextnodeengine.cpp