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/qtextdocumentlayout_p.h>
17#include <private/qtextimagehandler_p.h>
18#include <private/qrawfont_p.h>
19#include <private/qglyphrun_p.h>
20#include <private/qquickitem_p.h>
21
22QT_BEGIN_NAMESPACE
23
24Q_DECLARE_LOGGING_CATEGORY(lcSgText)
25
26QQuickTextNodeEngine::BinaryTreeNodeKey::BinaryTreeNodeKey(BinaryTreeNode *node)
27 : fontEngine(QRawFontPrivate::get(font: node->glyphRun.rawFont())->fontEngine)
28 , clipNode(node->clipNode)
29 , color(node->color.rgba())
30 , selectionState(node->selectionState)
31{
32}
33
34QQuickTextNodeEngine::BinaryTreeNode::BinaryTreeNode(const QGlyphRun &g,
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)
42 , boundingRect(brect)
43 , selectionState(selState)
44 , clipNode(nullptr)
45 , decorations(decs)
46 , color(c)
47 , backgroundColor(bc)
48 , decorationColor(dc)
49 , position(pos)
50 , ascent(a)
51 , leftChildIndex(-1)
52 , rightChildIndex(-1)
53{
54 QGlyphRunPrivate *d = QGlyphRunPrivate::get(glyphRun: g);
55 ranges.append(t: qMakePair(value1&: d->textRangeStart, value2&: 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(p: position);
65
66 if (qFuzzyIsNull(d: searchRect.width()) || qFuzzyIsNull(d: 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: 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(t: 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, currentIndex: node->leftChildIndex);
122
123 sortedIndexes->append(t: currentIndex);
124
125 if (node->rightChildIndex >= 0)
126 inOrder(binaryTree, sortedIndexes, currentIndex: 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(pos: blockRelativePosition);
145 if (!line.isValid())
146 break;
147 if (!currentLine().isValid()
148 || line.lineNumber() != currentLine().lineNumber()) {
149 setCurrentLine(line);
150 }
151
152 Q_ASSERT(line.textLength() > 0);
153 int lineEnd = line.textStart() + block.position() + line.textLength();
154
155 int len = qMin(a: lineEnd - textPos, b: fragmentEnd - textPos);
156 Q_ASSERT(len > 0);
157
158 int currentStepEnd = textPos + len;
159
160 addGlyphsForRanges(ranges: colorChanges,
161 start: textPos - block.position(),
162 end: currentStepEnd - block.position(),
163 selectionStart: selectionStart - block.position(),
164 selectionEnd: 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(idx: i);
176
177 {
178 QRectF &rect = textDecoration.rect;
179 rect.setY(qRound(d: rect.y() + m_currentLine.ascent() + offset));
180 rect.setHeight(thickness);
181 }
182
183 m_lines.append(t: 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(binaryTree: m_currentLineTree, sortedIndexes: &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(idx: 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(flag: Decoration::Underline) ||
251 currentDecorations.testFlag(flag: Decoration::Overline) ||
252 currentDecorations.testFlag(flag: Decoration::StrikeOut)))
253 textDecoration.color = lastDecorationColor;
254
255 if (currentDecorations & Decoration::Underline)
256 pendingUnderlines.append(t: textDecoration);
257
258 if (currentDecorations & Decoration::Overline)
259 pendingOverlines.append(t: textDecoration);
260
261 if (currentDecorations & Decoration::StrikeOut)
262 pendingStrikeOuts.append(t: textDecoration);
263
264 if (currentDecorations & Decoration::Background)
265 m_backgrounds.append(t: qMakePair(value1&: decorationRect, value2&: 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(t: 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(r: 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(textDecorations: pendingUnderlines, offset: underlineOffset, thickness: 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(textDecorations: pendingOverlines, offset: overlineOffset, thickness: 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(textDecorations: pendingStrikeOuts, offset: strikeOutOffset, thickness: 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(t: *node);
375 }
376 }
377
378 if (!pendingUnderlines.isEmpty())
379 addTextDecorations(textDecorations: pendingUnderlines, offset: underlineOffset, thickness: underlineThickness);
380
381 if (!pendingOverlines.isEmpty())
382 addTextDecorations(textDecorations: pendingOverlines, offset: overlineOffset, thickness: overlineThickness);
383
384 if (!pendingStrikeOuts.isEmpty())
385 addTextDecorations(textDecorations: pendingStrikeOuts, offset: strikeOutOffset, thickness: 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(p: m_position + m_currentLine.rect().topRight() + QPointF(0, y));
403 else
404 searchRect.moveTopLeft(p: 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(p: lastPos - QPointF(0, ascent - lastNode->ascent));
410 } else {
411 QPointF lastPos = lastNode->boundingRect.topRight();
412 searchRect.moveTopLeft(p: lastPos - QPointF(0, ascent - lastNode->ascent));
413 }
414 }
415 }
416
417 BinaryTreeNode::insert(binaryTree: &m_currentLineTree, rect: 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(objectType: format.objectType());
427 if (handler != nullptr) {
428 QImage image;
429 QSizeF size = handler->intrinsicSize(doc: textDocument, posInDocument: 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(doc: textDocument, imageFormat);
435 }
436
437 if (image.isNull()) {
438 image = QImage(size.toSize(), QImage::Format_ARGB32_Premultiplied);
439 image.fill(color: Qt::transparent);
440 {
441 QPainter painter(&image);
442 handler->drawObject(painter: &painter, rect: image.rect(), doc: textDocument, posInDocument: pos, format);
443 }
444 }
445
446 // Use https://developer.mozilla.org/de/docs/Web/CSS/vertical-align as a reference
447 // The top/bottom positions are supposed to be higher/lower than the text and reference
448 // the line height, not the text height (using QFontMetrics)
449 qreal ascent;
450 QTextLine line = block.layout()->lineForTextPosition(pos: pos - block.position());
451 switch (format.verticalAlignment())
452 {
453 case QTextCharFormat::AlignTop:
454 ascent = line.ascent();
455 break;
456 case QTextCharFormat::AlignMiddle:
457 // Middlepoint of line (height - descent) + Half object height
458 ascent = (line.ascent() + line.descent()) / 2 - line.descent() + size.height() / 2;
459 break;
460 case QTextCharFormat::AlignBottom:
461 ascent = size.height() - line.descent();
462 break;
463 case QTextCharFormat::AlignBaseline:
464 default:
465 ascent = size.height();
466 }
467
468 addImage(rect: QRectF(position, size), image, ascent, selectionState, layoutPosition);
469 }
470}
471
472void QQuickTextNodeEngine::addUnselectedGlyphs(const QGlyphRun &glyphRun)
473{
474 BinaryTreeNode::insert(binaryTree: &m_currentLineTree,
475 glyphRun,
476 selectionState: Unselected,
477 decorations: Decoration::NoDecoration,
478 textColor: m_textColor,
479 backgroundColor: m_backgroundColor,
480 decorationColor: m_decorationColor,
481 position: m_position);
482}
483
484void QQuickTextNodeEngine::addSelectedGlyphs(const QGlyphRun &glyphRun)
485{
486 int currentSize = m_currentLineTree.size();
487 BinaryTreeNode::insert(binaryTree: &m_currentLineTree,
488 glyphRun,
489 selectionState: Selected,
490 decorations: Decoration::NoDecoration,
491 textColor: m_textColor,
492 backgroundColor: m_backgroundColor,
493 decorationColor: m_decorationColor,
494 position: m_position);
495 m_hasSelection = m_hasSelection || m_currentLineTree.size() > currentSize;
496}
497
498void QQuickTextNodeEngine::addGlyphsForRanges(const QVarLengthArray<QTextLayout::FormatRange> &ranges,
499 int start, int end,
500 int selectionStart, int selectionEnd)
501{
502 int currentPosition = start;
503 int remainingLength = end - start;
504 for (int j=0; j<ranges.size(); ++j) {
505 const QTextLayout::FormatRange &range = ranges.at(idx: j);
506 if (range.start + range.length > currentPosition
507 && range.start < currentPosition + remainingLength) {
508
509 if (range.start > currentPosition) {
510 addGlyphsInRange(rangeStart: currentPosition, rangeEnd: range.start - currentPosition,
511 color: QColor(), backgroundColor: QColor(), underlineColor: QColor(), selectionStart, selectionEnd);
512 }
513 int rangeEnd = qMin(a: range.start + range.length, b: currentPosition + remainingLength);
514 QColor rangeColor;
515 if (range.format.hasProperty(propertyId: QTextFormat::ForegroundBrush))
516 rangeColor = range.format.foreground().color();
517 else if (range.format.isAnchor())
518 rangeColor = m_anchorColor;
519 QColor rangeBackgroundColor = range.format.hasProperty(propertyId: QTextFormat::BackgroundBrush)
520 ? range.format.background().color()
521 : QColor();
522
523 QColor rangeDecorationColor = range.format.hasProperty(propertyId: QTextFormat::TextUnderlineColor)
524 ? range.format.underlineColor()
525 : QColor();
526
527 addGlyphsInRange(rangeStart: range.start, rangeEnd: rangeEnd - range.start,
528 color: rangeColor, backgroundColor: rangeBackgroundColor, underlineColor: rangeDecorationColor,
529 selectionStart, selectionEnd);
530
531 currentPosition = range.start + range.length;
532 remainingLength = end - currentPosition;
533
534 } else if (range.start > currentPosition + remainingLength || remainingLength <= 0) {
535 break;
536 }
537 }
538
539 if (remainingLength > 0) {
540 addGlyphsInRange(rangeStart: currentPosition, rangeEnd: remainingLength, color: QColor(), backgroundColor: QColor(), underlineColor: QColor(),
541 selectionStart, selectionEnd);
542 }
543
544}
545
546void QQuickTextNodeEngine::addGlyphsInRange(int rangeStart, int rangeLength,
547 const QColor &color, const QColor &backgroundColor, const QColor &decorationColor,
548 int selectionStart, int selectionEnd)
549{
550 QColor oldColor;
551 if (color.isValid()) {
552 oldColor = m_textColor;
553 m_textColor = color;
554 }
555
556 QColor oldBackgroundColor = m_backgroundColor;
557 if (backgroundColor.isValid()) {
558 oldBackgroundColor = m_backgroundColor;
559 m_backgroundColor = backgroundColor;
560 }
561
562 QColor oldDecorationColor = m_decorationColor;
563 if (decorationColor.isValid()) {
564 oldDecorationColor = m_decorationColor;
565 m_decorationColor = decorationColor;
566 }
567
568 bool hasSelection = selectionEnd >= 0
569 && selectionStart <= selectionEnd;
570
571 QTextLine &line = m_currentLine;
572 int rangeEnd = rangeStart + rangeLength;
573 if (!hasSelection || (selectionStart > rangeEnd || selectionEnd < rangeStart)) {
574 QList<QGlyphRun> glyphRuns = line.glyphRuns(from: rangeStart, length: rangeLength);
575 for (int j=0; j<glyphRuns.size(); ++j) {
576 const QGlyphRun &glyphRun = glyphRuns.at(i: j);
577 addUnselectedGlyphs(glyphRun);
578 }
579 } else {
580 if (rangeStart < selectionStart) {
581 int length = qMin(a: selectionStart - rangeStart, b: rangeLength);
582 QList<QGlyphRun> glyphRuns = line.glyphRuns(from: rangeStart, length);
583 for (int j=0; j<glyphRuns.size(); ++j) {
584 const QGlyphRun &glyphRun = glyphRuns.at(i: j);
585 addUnselectedGlyphs(glyphRun);
586 }
587 }
588
589 if (rangeEnd > selectionStart) {
590 int start = qMax(a: selectionStart, b: rangeStart);
591 int length = qMin(a: selectionEnd - start + 1, b: rangeEnd - start);
592 QList<QGlyphRun> glyphRuns = line.glyphRuns(from: start, length);
593
594 for (int j=0; j<glyphRuns.size(); ++j) {
595 const QGlyphRun &glyphRun = glyphRuns.at(i: j);
596 addSelectedGlyphs(glyphRun);
597 }
598 }
599
600 if (selectionEnd >= rangeStart && selectionEnd < rangeEnd) {
601 int start = selectionEnd + 1;
602 int length = rangeEnd - selectionEnd - 1;
603 QList<QGlyphRun> glyphRuns = line.glyphRuns(from: start, length);
604 for (int j=0; j<glyphRuns.size(); ++j) {
605 const QGlyphRun &glyphRun = glyphRuns.at(i: j);
606 addUnselectedGlyphs(glyphRun);
607 }
608 }
609 }
610
611 if (decorationColor.isValid())
612 m_decorationColor = oldDecorationColor;
613
614 if (backgroundColor.isValid())
615 m_backgroundColor = oldBackgroundColor;
616
617 if (oldColor.isValid())
618 m_textColor = oldColor;
619}
620
621void QQuickTextNodeEngine::addBorder(const QRectF &rect, qreal border,
622 QTextFrameFormat::BorderStyle borderStyle,
623 const QBrush &borderBrush)
624{
625 const QColor &color = borderBrush.color();
626
627 // Currently we don't support other styles than solid
628 Q_UNUSED(borderStyle);
629
630 m_backgrounds.append(t: qMakePair(value1: QRectF(rect.left(), rect.top(), border, rect.height() + border), value2: color));
631 m_backgrounds.append(t: qMakePair(value1: QRectF(rect.left() + border, rect.top(), rect.width(), border), value2: color));
632 m_backgrounds.append(t: qMakePair(value1: QRectF(rect.right(), rect.top() + border, border, rect.height() - border), value2: color));
633 m_backgrounds.append(t: qMakePair(value1: QRectF(rect.left() + border, rect.bottom(), rect.width(), border), value2: color));
634}
635
636void QQuickTextNodeEngine::addFrameDecorations(QTextDocument *document, QTextFrame *frame)
637{
638 QTextDocumentLayout *documentLayout = qobject_cast<QTextDocumentLayout *>(object: document->documentLayout());
639 if (Q_UNLIKELY(!documentLayout))
640 return;
641
642 QTextFrameFormat frameFormat = frame->format().toFrameFormat();
643 QTextTable *table = qobject_cast<QTextTable *>(object: frame);
644
645 QRectF boundingRect = table == nullptr
646 ? documentLayout->frameBoundingRect(frame)
647 : documentLayout->tableBoundingRect(table);
648
649 QBrush bg = frame->frameFormat().background();
650 if (bg.style() != Qt::NoBrush)
651 m_backgrounds.append(t: qMakePair(value1&: boundingRect, value2: bg.color()));
652
653 if (!frameFormat.hasProperty(propertyId: QTextFormat::FrameBorder))
654 return;
655
656 qreal borderWidth = frameFormat.border();
657 if (qFuzzyIsNull(d: borderWidth))
658 return;
659
660 QBrush borderBrush = frameFormat.borderBrush();
661 QTextFrameFormat::BorderStyle borderStyle = frameFormat.borderStyle();
662 if (borderStyle == QTextFrameFormat::BorderStyle_None)
663 return;
664
665 const auto collapsed = table->format().borderCollapse();
666
667 if (!collapsed) {
668 addBorder(rect: boundingRect.adjusted(xp1: frameFormat.leftMargin(), yp1: frameFormat.topMargin(),
669 xp2: -frameFormat.rightMargin() - borderWidth,
670 yp2: -frameFormat.bottomMargin() - borderWidth),
671 border: borderWidth, borderStyle, borderBrush);
672 }
673 if (table != nullptr) {
674 int rows = table->rows();
675 int columns = table->columns();
676
677 for (int row=0; row<rows; ++row) {
678 for (int column=0; column<columns; ++column) {
679 QTextTableCell cell = table->cellAt(row, col: column);
680
681 QRectF cellRect = documentLayout->tableCellBoundingRect(table, cell);
682 addBorder(rect: cellRect.adjusted(xp1: -borderWidth, yp1: -borderWidth, xp2: collapsed ? -borderWidth : 0, yp2: collapsed ? -borderWidth : 0), border: borderWidth,
683 borderStyle, borderBrush);
684 }
685 }
686 }
687}
688
689size_t qHash(const QQuickTextNodeEngine::BinaryTreeNodeKey &key, size_t seed = 0)
690{
691 return qHashMulti(seed, args: key.fontEngine, args: key.clipNode, args: key.color, args: key.selectionState);
692}
693
694void QQuickTextNodeEngine::mergeProcessedNodes(QList<BinaryTreeNode *> *regularNodes,
695 QList<BinaryTreeNode *> *imageNodes)
696{
697 QHash<BinaryTreeNodeKey, QList<BinaryTreeNode *> > map;
698
699 for (int i = 0; i < m_processedNodes.size(); ++i) {
700 BinaryTreeNode *node = m_processedNodes.data() + i;
701
702 if (node->image.isNull()) {
703 if (node->glyphRun.isEmpty())
704 continue;
705
706 BinaryTreeNodeKey key(node);
707
708 QList<BinaryTreeNode *> &nodes = map[key];
709 if (nodes.isEmpty())
710 regularNodes->append(t: node);
711
712 nodes.append(t: node);
713 } else {
714 imageNodes->append(t: node);
715 }
716 }
717
718 for (int i = 0; i < regularNodes->size(); ++i) {
719 BinaryTreeNode *primaryNode = regularNodes->at(i);
720 BinaryTreeNodeKey key(primaryNode);
721
722 const QList<BinaryTreeNode *> &nodes = map.value(key);
723 Q_ASSERT(nodes.first() == primaryNode);
724
725 int count = 0;
726 for (int j = 0; j < nodes.size(); ++j)
727 count += nodes.at(i: j)->glyphRun.glyphIndexes().size();
728
729 if (count != primaryNode->glyphRun.glyphIndexes().size()) {
730 QGlyphRun &glyphRun = primaryNode->glyphRun;
731 QVector<quint32> glyphIndexes = glyphRun.glyphIndexes();
732 glyphIndexes.reserve(asize: count);
733
734 QVector<QPointF> glyphPositions = glyphRun.positions();
735 glyphPositions.reserve(asize: count);
736
737 QRectF glyphBoundingRect = glyphRun.boundingRect();
738
739 for (int j = 1; j < nodes.size(); ++j) {
740 BinaryTreeNode *otherNode = nodes.at(i: j);
741 glyphIndexes += otherNode->glyphRun.glyphIndexes();
742 primaryNode->ranges += otherNode->ranges;
743 glyphBoundingRect = glyphBoundingRect.united(r: otherNode->boundingRect);
744
745 QVector<QPointF> otherPositions = otherNode->glyphRun.positions();
746 for (int k = 0; k < otherPositions.size(); ++k)
747 glyphPositions += otherPositions.at(i: k) + (otherNode->position - primaryNode->position);
748 }
749
750 Q_ASSERT(glyphPositions.size() == count);
751 Q_ASSERT(glyphIndexes.size() == count);
752
753 glyphRun.setGlyphIndexes(glyphIndexes);
754 glyphRun.setPositions(glyphPositions);
755 glyphRun.setBoundingRect(glyphBoundingRect);
756 }
757 }
758}
759
760void QQuickTextNodeEngine::addToSceneGraph(QSGInternalTextNode *parentNode,
761 QQuickText::TextStyle style,
762 const QColor &styleColor)
763{
764 if (m_currentLine.isValid())
765 processCurrentLine();
766
767 QList<BinaryTreeNode *> nodes;
768 QList<BinaryTreeNode *> imageNodes;
769 mergeProcessedNodes(regularNodes: &nodes, imageNodes: &imageNodes);
770
771 for (int i = 0; i < m_backgrounds.size(); ++i) {
772 const QRectF &rect = m_backgrounds.at(i).first;
773 const QColor &color = m_backgrounds.at(i).second;
774 if (color.alpha() != 0)
775 parentNode->addRectangleNode(rect, color);
776 }
777
778 // Add all text with unselected color first
779 for (int i = 0; i < nodes.size(); ++i) {
780 const BinaryTreeNode *node = nodes.at(i);
781 parentNode->addGlyphs(position: node->position, glyphs: node->glyphRun, color: node->color, style, styleColor, parentNode: nullptr);
782 }
783
784 for (int i = 0; i < imageNodes.size(); ++i) {
785 const BinaryTreeNode *node = imageNodes.at(i);
786 if (node->selectionState == Unselected)
787 parentNode->addImage(rect: node->boundingRect, image: node->image);
788 }
789
790 // Then, prepend all selection rectangles to the tree
791 for (int i = 0; i < m_selectionRects.size(); ++i) {
792 const QRectF &rect = m_selectionRects.at(i);
793 if (m_selectionColor.alpha() != 0)
794 parentNode->addRectangleNode(rect, color: m_selectionColor);
795 }
796
797 // Add decorations for each node to the tree.
798 for (int i = 0; i < m_lines.size(); ++i) {
799 const TextDecoration &textDecoration = m_lines.at(i);
800
801 QColor color = textDecoration.selectionState == Selected
802 ? m_selectedTextColor
803 : textDecoration.color;
804
805 parentNode->addDecorationNode(rect: textDecoration.rect, color);
806 }
807
808 // Finally add the selected text on top of everything
809 for (int i = 0; i < nodes.size(); ++i) {
810 const BinaryTreeNode *node = nodes.at(i);
811 QQuickDefaultClipNode *clipNode = node->clipNode;
812 if (clipNode != nullptr && clipNode->parent() == nullptr)
813 parentNode->appendChildNode(node: clipNode);
814
815 if (node->selectionState == Selected) {
816 QColor color = m_selectedTextColor;
817 int previousNodeIndex = i - 1;
818 int nextNodeIndex = i + 1;
819 const BinaryTreeNode *previousNode = previousNodeIndex < 0 ? 0 : nodes.at(i: previousNodeIndex);
820 while (previousNode != nullptr && qFuzzyCompare(p1: previousNode->boundingRect.left(), p2: node->boundingRect.left()))
821 previousNode = --previousNodeIndex < 0 ? 0 : nodes.at(i: previousNodeIndex);
822
823 const BinaryTreeNode *nextNode = nextNodeIndex == nodes.size() ? 0 : nodes.at(i: nextNodeIndex);
824
825 if (previousNode != nullptr && previousNode->selectionState == Unselected)
826 parentNode->addGlyphs(position: previousNode->position, glyphs: previousNode->glyphRun, color, style, styleColor, parentNode: clipNode);
827
828 if (nextNode != nullptr && nextNode->selectionState == Unselected)
829 parentNode->addGlyphs(position: nextNode->position, glyphs: nextNode->glyphRun, color, style, styleColor, parentNode: clipNode);
830
831 // If the previous or next node completely overlaps this one, then we have already drawn the glyphs of
832 // this node
833 bool drawCurrent = false;
834 if (previousNode != nullptr || nextNode != nullptr) {
835 for (int i = 0; i < node->ranges.size(); ++i) {
836 const QPair<int, int> &range = node->ranges.at(i);
837
838 int rangeLength = range.second - range.first + 1;
839 if (previousNode != nullptr) {
840 for (int j = 0; j < previousNode->ranges.size(); ++j) {
841 const QPair<int, int> &otherRange = previousNode->ranges.at(i: j);
842
843 if (range.first < otherRange.second && range.second > otherRange.first) {
844 int start = qMax(a: range.first, b: otherRange.first);
845 int end = qMin(a: range.second, b: otherRange.second);
846 rangeLength -= end - start + 1;
847 if (rangeLength == 0)
848 break;
849 }
850 }
851 }
852
853 if (nextNode != nullptr && rangeLength > 0) {
854 for (int j = 0; j < nextNode->ranges.size(); ++j) {
855 const QPair<int, int> &otherRange = nextNode->ranges.at(i: j);
856
857 if (range.first < otherRange.second && range.second > otherRange.first) {
858 int start = qMax(a: range.first, b: otherRange.first);
859 int end = qMin(a: range.second, b: otherRange.second);
860 rangeLength -= end - start + 1;
861 if (rangeLength == 0)
862 break;
863 }
864 }
865 }
866
867 if (rangeLength > 0) {
868 drawCurrent = true;
869 break;
870 }
871 }
872 } else {
873 drawCurrent = true;
874 }
875
876 if (drawCurrent)
877 parentNode->addGlyphs(position: node->position, glyphs: node->glyphRun, color, style, styleColor, parentNode: clipNode);
878 }
879 }
880
881 for (int i = 0; i < imageNodes.size(); ++i) {
882 const BinaryTreeNode *node = imageNodes.at(i);
883 if (node->selectionState == Selected) {
884 parentNode->addImage(rect: node->boundingRect, image: node->image);
885 if (node->selectionState == Selected) {
886 QColor color = m_selectionColor;
887 color.setAlpha(128);
888 parentNode->addRectangleNode(rect: node->boundingRect, color);
889 }
890 }
891 }
892}
893
894void QQuickTextNodeEngine::mergeFormats(QTextLayout *textLayout, QVarLengthArray<QTextLayout::FormatRange> *mergedFormats)
895{
896 Q_ASSERT(mergedFormats != nullptr);
897 if (textLayout == nullptr)
898 return;
899
900 QVector<QTextLayout::FormatRange> additionalFormats = textLayout->formats();
901 for (int i=0; i<additionalFormats.size(); ++i) {
902 QTextLayout::FormatRange additionalFormat = additionalFormats.at(i);
903 if (additionalFormat.format.hasProperty(propertyId: QTextFormat::ForegroundBrush)
904 || additionalFormat.format.hasProperty(propertyId: QTextFormat::BackgroundBrush)
905 || additionalFormat.format.isAnchor()) {
906 // Merge overlapping formats
907 if (!mergedFormats->isEmpty()) {
908 QTextLayout::FormatRange *lastFormat = mergedFormats->data() + mergedFormats->size() - 1;
909
910 if (additionalFormat.start < lastFormat->start + lastFormat->length) {
911 QTextLayout::FormatRange *mergedRange = nullptr;
912
913 int length = additionalFormat.length;
914 if (additionalFormat.start > lastFormat->start) {
915 lastFormat->length = additionalFormat.start - lastFormat->start;
916 length -= lastFormat->length;
917
918 mergedFormats->append(t: QTextLayout::FormatRange());
919 mergedRange = mergedFormats->data() + mergedFormats->size() - 1;
920 lastFormat = mergedFormats->data() + mergedFormats->size() - 2;
921 } else {
922 mergedRange = lastFormat;
923 }
924
925 mergedRange->format = lastFormat->format;
926 mergedRange->format.merge(other: additionalFormat.format);
927 mergedRange->start = additionalFormat.start;
928
929 int end = qMin(a: additionalFormat.start + additionalFormat.length,
930 b: lastFormat->start + lastFormat->length);
931
932 mergedRange->length = end - mergedRange->start;
933 length -= mergedRange->length;
934
935 additionalFormat.start = end;
936 additionalFormat.length = length;
937 }
938 }
939
940 if (additionalFormat.length > 0)
941 mergedFormats->append(t: additionalFormat);
942 }
943 }
944
945}
946
947/*!
948 \internal
949 Adds the \a block from the \a textDocument at \a position if its
950 \l {QAbstractTextDocumentLayout::blockBoundingRect()}{bounding rect}
951 intersects the \a viewport, or if \c viewport is not valid
952 (i.e. use a default-constructed QRectF to skip the viewport check).
953
954 \sa QQuickItem::clipRect()
955 */
956void QQuickTextNodeEngine::addTextBlock(QTextDocument *textDocument, const QTextBlock &block, const QPointF &position,
957 const QColor &textColor, const QColor &anchorColor, int selectionStart, int selectionEnd, const QRectF &viewport)
958{
959 Q_ASSERT(textDocument);
960#if QT_CONFIG(im)
961 int preeditLength = block.isValid() ? block.layout()->preeditAreaText().size() : 0;
962 int preeditPosition = block.isValid() ? block.layout()->preeditAreaPosition() : -1;
963#endif
964
965 setCurrentTextDirection(block.textDirection());
966
967 QVarLengthArray<QTextLayout::FormatRange> colorChanges;
968 mergeFormats(textLayout: block.layout(), mergedFormats: &colorChanges);
969
970 const QTextCharFormat charFormat = block.charFormat();
971 const QRectF blockBoundingRect = textDocument->documentLayout()->blockBoundingRect(block).translated(p: position);
972 if (viewport.isValid()) {
973 if (!blockBoundingRect.intersects(r: viewport))
974 return;
975 qCDebug(lcSgText) << "adding block with length" << block.length() << ':' << blockBoundingRect << "in viewport" << viewport;
976 }
977
978 if (charFormat.background().style() != Qt::NoBrush)
979 m_backgrounds.append(t: qMakePair(value1: blockBoundingRect, value2: charFormat.background().color()));
980
981 if (QTextList *textList = block.textList()) {
982 QPointF pos = blockBoundingRect.topLeft();
983 QTextLayout *layout = block.layout();
984 if (layout->lineCount() > 0) {
985 QTextLine firstLine = layout->lineAt(i: 0);
986 Q_ASSERT(firstLine.isValid());
987
988 setCurrentLine(firstLine);
989
990 QRectF textRect = firstLine.naturalTextRect();
991 pos += textRect.topLeft();
992 if (block.textDirection() == Qt::RightToLeft)
993 pos.rx() += textRect.width();
994
995 QFont font(charFormat.font());
996 QFontMetricsF fontMetrics(font);
997 QTextListFormat listFormat = textList->format();
998
999 QString listItemBullet;
1000 switch (listFormat.style()) {
1001 case QTextListFormat::ListCircle:
1002 listItemBullet = QChar(0x25E6); // White bullet
1003 break;
1004 case QTextListFormat::ListSquare:
1005 listItemBullet = QChar(0x25AA); // Black small square
1006 break;
1007 case QTextListFormat::ListDecimal:
1008 case QTextListFormat::ListLowerAlpha:
1009 case QTextListFormat::ListUpperAlpha:
1010 case QTextListFormat::ListLowerRoman:
1011 case QTextListFormat::ListUpperRoman:
1012 listItemBullet = textList->itemText(block);
1013 break;
1014 default:
1015 listItemBullet = QChar(0x2022); // Black bullet
1016 break;
1017 };
1018
1019 switch (block.blockFormat().marker()) {
1020 case QTextBlockFormat::MarkerType::Checked:
1021 listItemBullet = QChar(0x2612); // Checked checkbox
1022 break;
1023 case QTextBlockFormat::MarkerType::Unchecked:
1024 listItemBullet = QChar(0x2610); // Unchecked checkbox
1025 break;
1026 case QTextBlockFormat::MarkerType::NoMarker:
1027 break;
1028 }
1029
1030 QSizeF size(fontMetrics.horizontalAdvance(string: listItemBullet), fontMetrics.height());
1031 qreal xoff = fontMetrics.horizontalAdvance(QLatin1Char(' '));
1032 if (block.textDirection() == Qt::LeftToRight)
1033 xoff = -xoff - size.width();
1034 setPosition(pos + QPointF(xoff, 0));
1035
1036 QTextLayout layout;
1037 layout.setFont(font);
1038 layout.setText(listItemBullet); // Bullet
1039 layout.beginLayout();
1040 QTextLine line = layout.createLine();
1041 line.setPosition(QPointF(0, 0));
1042 layout.endLayout();
1043
1044 // set the color for the bullets, instead of using the previous QTextBlock's color.
1045 if (charFormat.foreground().style() == Qt::NoBrush)
1046 setTextColor(textColor);
1047 else
1048 setTextColor(charFormat.foreground().color());
1049
1050 QList<QGlyphRun> glyphRuns = layout.glyphRuns();
1051 for (int i=0; i<glyphRuns.size(); ++i)
1052 addUnselectedGlyphs(glyphRun: glyphRuns.at(i));
1053 }
1054 }
1055
1056 int textPos = block.position();
1057 QTextBlock::iterator blockIterator = block.begin();
1058
1059 while (!blockIterator.atEnd()) {
1060 QTextFragment fragment = blockIterator.fragment();
1061 QString text = fragment.text();
1062 if (text.isEmpty())
1063 continue;
1064
1065 QTextCharFormat charFormat = fragment.charFormat();
1066 QFont font(charFormat.font());
1067 QFontMetricsF fontMetrics(font);
1068
1069 int fontHeight = fontMetrics.descent() + fontMetrics.ascent();
1070 int valign = charFormat.verticalAlignment();
1071 if (valign == QTextCharFormat::AlignSuperScript)
1072 setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() - fontHeight / 2));
1073 else if (valign == QTextCharFormat::AlignSubScript)
1074 setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() + fontHeight / 6));
1075 else
1076 setPosition(blockBoundingRect.topLeft());
1077
1078 if (text.contains(c: QChar::ObjectReplacementCharacter) && charFormat.objectType() != QTextFormat::NoObject) {
1079 QTextFrame *frame = qobject_cast<QTextFrame *>(object: textDocument->objectForFormat(charFormat));
1080 if (!frame || frame->frameFormat().position() == QTextFrameFormat::InFlow) {
1081 int blockRelativePosition = textPos - block.position();
1082 QTextLine line = block.layout()->lineForTextPosition(pos: blockRelativePosition);
1083 if (!currentLine().isValid()
1084 || line.lineNumber() != currentLine().lineNumber()) {
1085 setCurrentLine(line);
1086 }
1087
1088 QQuickTextNodeEngine::SelectionState selectionState =
1089 (selectionStart < textPos + text.size()
1090 && selectionEnd >= textPos)
1091 ? QQuickTextNodeEngine::Selected
1092 : QQuickTextNodeEngine::Unselected;
1093
1094 addTextObject(block, position: QPointF(), format: charFormat, selectionState, textDocument, pos: textPos);
1095 }
1096 textPos += text.size();
1097 } else {
1098 if (charFormat.foreground().style() != Qt::NoBrush)
1099 setTextColor(charFormat.foreground().color());
1100 else if (charFormat.isAnchor())
1101 setTextColor(anchorColor);
1102 else
1103 setTextColor(textColor);
1104
1105 int fragmentEnd = textPos + fragment.length();
1106#if QT_CONFIG(im)
1107 if (preeditPosition >= 0
1108 && (preeditPosition + block.position()) >= textPos
1109 && (preeditPosition + block.position()) <= fragmentEnd) {
1110 fragmentEnd += preeditLength;
1111 }
1112#endif
1113 if (charFormat.background().style() != Qt::NoBrush || charFormat.hasProperty(propertyId: QTextFormat::TextUnderlineColor)) {
1114 QTextLayout::FormatRange additionalFormat;
1115 additionalFormat.start = textPos - block.position();
1116 additionalFormat.length = fragmentEnd - textPos;
1117 additionalFormat.format = charFormat;
1118 colorChanges << additionalFormat;
1119 }
1120
1121 textPos = addText(block, charFormat, textColor, colorChanges, textPos, fragmentEnd,
1122 selectionStart, selectionEnd);
1123 }
1124
1125 ++blockIterator;
1126 }
1127
1128#if QT_CONFIG(im)
1129 if (preeditLength >= 0 && textPos <= block.position() + preeditPosition) {
1130 setPosition(blockBoundingRect.topLeft());
1131 textPos = block.position() + preeditPosition;
1132 QTextLine line = block.layout()->lineForTextPosition(pos: preeditPosition);
1133 if (!currentLine().isValid()
1134 || line.lineNumber() != currentLine().lineNumber()) {
1135 setCurrentLine(line);
1136 }
1137 textPos = addText(block, charFormat: block.charFormat(), textColor, colorChanges,
1138 textPos, fragmentEnd: textPos + preeditLength,
1139 selectionStart, selectionEnd);
1140 }
1141#endif
1142
1143 // Add block decorations (so far only horizontal rules)
1144 if (block.blockFormat().hasProperty(propertyId: QTextFormat::BlockTrailingHorizontalRulerWidth)) {
1145 auto ruleLength = qvariant_cast<QTextLength>(v: block.blockFormat().property(propertyId: QTextFormat::BlockTrailingHorizontalRulerWidth));
1146 QRectF ruleRect(0, 0, ruleLength.value(maximumLength: blockBoundingRect.width()), 1);
1147 ruleRect.moveCenter(p: blockBoundingRect.center());
1148 const QColor ruleColor = block.blockFormat().hasProperty(propertyId: QTextFormat::BackgroundBrush)
1149 ? qvariant_cast<QBrush>(v: block.blockFormat().property(propertyId: QTextFormat::BackgroundBrush)).color()
1150 : m_textColor;
1151 m_lines.append(t: TextDecoration(QQuickTextNodeEngine::Unselected, ruleRect, ruleColor));
1152 }
1153
1154 setCurrentLine(QTextLine()); // Reset current line because the text layout changed
1155 m_hasContents = true;
1156}
1157
1158
1159QT_END_NAMESPACE
1160
1161

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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