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 | |
22 | QT_BEGIN_NAMESPACE |
23 | |
24 | Q_DECLARE_LOGGING_CATEGORY(lcSgText) |
25 | |
26 | QQuickTextNodeEngine::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 | |
34 | QQuickTextNodeEngine::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 | |
59 | void 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 | |
86 | void 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 | |
114 | void 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 | |
130 | int 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 | |
171 | void 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 | |
187 | void 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 | |
393 | void 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 | |
421 | void 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 | |
472 | void 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 | |
484 | void 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 | |
498 | void 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 | |
546 | void 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 | |
621 | void 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 | |
636 | void 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 | |
689 | size_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 | |
694 | void 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 | |
760 | void 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 | |
894 | void 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 | */ |
956 | void 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 | |
1159 | QT_END_NAMESPACE |
1160 | |
1161 | |