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