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

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