Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qsginternaltextnode_p.h"
++
++ #include "qquicktextnodeengine_p.h"
++
++ #include <private/qsgadaptationlayer_p.h>
++ #include <private/qsgdistancefieldglyphnode_p.h>
++ #include <private/qquickclipnode_p.h>
++ #include <private/qquickitem_p.h>
++ #include <private/qquicktextdocument_p.h>
++ #include <private/qsgrendernode_p.h>
++
++ #include <QtCore/qpoint.h>
++ #include <qtextdocument.h>
++ #include <qtextlayout.h>
++ #include <qabstracttextdocumentlayout.h>
++ #include <private/qquickstyledtext_p.h>
++ #include <private/qquicktext_p_p.h>
++ #include <private/qfont_p.h>
++ #include <private/qfontengine_p.h>
++
++ #include <private/qtextdocumentlayout_p.h>
++ #include <qhash.h>
++
++ // #define QSGINTERNALTEXTNODE_NO_RECYCLE
++
++ Q_STATIC_LOGGING_CATEGORY(lcTextRecycle, "qt.quick.text.noderecycling")
++
++ QT_BEGIN_NAMESPACE
++
++ /*!
++ \class QSGInternalTextNode
++ \inmodule QtQuick
++ \internal
++
++ Creates an empty QSGInternalTextNode
++ */
++ QSGInternalTextNode::QSGInternalTextNode(QSGRenderContext *renderContext)
++ : m_renderContext(renderContext)
++ {
++ #ifdef QSG_RUNTIME_DESCRIPTION
++ qsgnode_set_description(this, QLatin1String("text"));
++ #endif
++
++ static_assert(int(QSGTextNode::Normal) == int(QQuickText::Normal));
++ static_assert(int(QSGTextNode::Outline) == int(QQuickText::Outline));
++ static_assert(int(QSGTextNode::Raised) == int(QQuickText::Raised));
++ static_assert(int(QSGTextNode::Sunken) == int(QQuickText::Sunken));
++
++ static_assert(int(QSGTextNode::QtRendering) == int(QQuickText::QtRendering));
++ static_assert(int(QSGTextNode::NativeRendering) == int(QQuickText::NativeRendering));
++ static_assert(int(QSGTextNode::CurveRendering) == int(QQuickText::CurveRendering));
++ }
++
++ QSGInternalTextNode::~QSGInternalTextNode()
++ {
++ qDeleteAll(m_textures);
++ }
++
++ QSGGlyphNode *QSGInternalTextNode::addGlyphs(const QPointF &position,
++ const QGlyphRun &glyphs,
++ const QColor &color,
++ RecycleBin *recycleBin,
++ QQuickText::TextStyle style,
++ const QColor &styleColor,
++ QSGNode *parentNode)
++ {
++ QRawFont font = glyphs.rawFont();
++
++ QSGTextNode::RenderType preferredRenderType = m_renderType;
++ if (m_renderType != NativeRendering) {
++ if (const QFontEngine *fe = QRawFontPrivate::get(font)->fontEngine)
++ if (fe->hasUnreliableGlyphOutline() || !fe->isSmoothlyScalable)
++ preferredRenderType = QSGTextNode::NativeRendering;
++ }
++
++ if (preferredRenderType == NativeRendering)
++ m_containsUnscalableGlyphs = true;
++
++ if (parentNode == nullptr)
++ parentNode = this;
++
++ QSGGlyphNode *node = findOrCreateGlyphNode(preferredRenderType, recycleBin, parentNode);
++ node->setRenderTypeQuality(m_renderTypeQuality);
++ node->setGlyphs(position + QPointF(0, glyphs.rawFont().ascent()), glyphs);
++ node->setStyle(style);
++ node->setStyleColor(styleColor);
++ node->setColor(color);
++ node->update();
++
++ /* We flag the geometry as static, but we never call markVertexDataDirty
++ or markIndexDataDirty on them. This is because all text nodes are
++ discarded when a change occurs. If we start appending/removing from
++ existing geometry, then we also need to start marking the geometry as
++ dirty.
++ */
++ node->geometry()->setIndexDataPattern(QSGGeometry::StaticPattern);
++ node->geometry()->setVertexDataPattern(QSGGeometry::StaticPattern);
++
++ if (node->parent() == nullptr)
++ parentNode->appendChildNode(node);
++
++ if (style == QQuickText::Outline && color.alpha() > 0 && styleColor != color) {
++ QSGGlyphNode *fillNode = findOrCreateGlyphNode(preferredRenderType, recycleBin, parentNode);
++ fillNode->setRenderTypeQuality(m_renderTypeQuality);
++ fillNode->setGlyphs(position + QPointF(0, glyphs.rawFont().ascent()), glyphs);
++ fillNode->setStyle(QQuickText::Normal);
++ fillNode->setPreferredAntialiasingMode(QSGGlyphNode::GrayAntialiasing);
++ fillNode->setColor(color);
++ fillNode->update();
++
++ fillNode->geometry()->setIndexDataPattern(QSGGeometry::StaticPattern);
++ fillNode->geometry()->setVertexDataPattern(QSGGeometry::StaticPattern);
++
++ if (fillNode->parent() == nullptr)
++ parentNode->appendChildNode(fillNode);
++ fillNode->setRenderOrder(node->renderOrder() + 1);
++ }
++
++ return node;
++ }
++
++ void QSGInternalTextNode::setCursor(const QRectF &rect, const QColor &color, RecycleBin *recycleBin)
++ {
++ if (m_cursorNode == nullptr)
++ m_cursorNode = findOrCreateRectangleNode(recycleBin);
++ m_cursorNode->setRect(rect);
++ m_cursorNode->setColor(color);
++ m_cursorNode->update();
++ if (m_cursorNode->parent() == nullptr)
++ appendChildNode(m_cursorNode);
++ }
++
++ void QSGInternalTextNode::clearCursor()
++ {
++ if (m_cursorNode)
++ removeChildNode(m_cursorNode);
++ delete m_cursorNode;
++ m_cursorNode = nullptr;
++ }
++
++ void QSGInternalTextNode::addDecorationNode(const QRectF &rect,
++ const QColor &color,
++ QTextCharFormat::UnderlineStyle style,
++ RecycleBin *recycleBin)
++ {
++ Q_UNUSED(style); // Software renderer does not support styled underlines
++ addRectangleNode(rect, color, recycleBin);
++ }
++
++ void QSGInternalTextNode::addRectangleNode(const QRectF &rect,
++ const QColor &color,
++ RecycleBin *recycleBin)
++ {
++ QSGInternalRectangleNode *node = findOrCreateRectangleNode(recycleBin);
++ node->setRect(rect);
++ node->setColor(color);
++ node->update();
++
++ if (node->parent() == nullptr)
++ appendChildNode(node);
++ }
++
++ void QSGInternalTextNode::addImage(const QRectF &rect, const QImage &image, RecycleBin *recycleBin)
++ {
++ QSGInternalImageNode *node = findOrCreateImageNode(recycleBin);
++ QSGTexture *texture = m_renderContext->createTexture(image);
++ texture->setFiltering(m_filtering);
++ m_textures.append(texture);
++ node->setTargetRect(rect);
++ node->setInnerTargetRect(rect);
++ node->setTexture(texture);
++ node->setFiltering(m_filtering);
++ if (node->parent() == nullptr)
++ appendChildNode(node);
++ node->update();
++ }
++
++ void QSGInternalTextNode::doAddTextDocument(QPointF position,
++ QTextDocument *textDocument,
++ int selectionStart,
++ int selectionEnd,
++ RecycleBin *recycleBin)
++ {
++ QQuickTextNodeEngine engine;
++ engine.setTextColor(m_color);
++ engine.setSelectedTextColor(m_selectionTextColor);
++ engine.setSelectionColor(m_selectionColor);
++ engine.setAnchorColor(m_linkColor);
++ engine.setPosition(position);
++ engine.setDevicePixelRatio(m_devicePixelRatio);
++
++ QList<QTextFrame *> frames;
++ frames.append(textDocument->rootFrame());
++ while (!frames.isEmpty()) {
++ QTextFrame *textFrame = frames.takeFirst();
++ frames.append(textFrame->childFrames());
++
++ engine.addFrameDecorations(textDocument, textFrame);
++
++ if (textFrame->firstPosition() > textFrame->lastPosition()
++ && textFrame->frameFormat().position() != QTextFrameFormat::InFlow) {
++ const int pos = textFrame->firstPosition() - 1;
++ auto *a = static_cast<QtPrivate::ProtectedLayoutAccessor *>(textDocument->documentLayout());
++ QTextCharFormat format = a->formatAccessor(pos);
++ QRectF rect = a->frameBoundingRect(textFrame);
++
++ QTextBlock block = textFrame->firstCursorPosition().block();
++ engine.setCurrentLine(block.layout()->lineForTextPosition(pos - block.position()));
++ engine.addTextObject(block, rect.topLeft(), format, QQuickTextNodeEngine::Unselected, textDocument,
++ pos, textFrame->frameFormat().position());
++ } else {
++ QTextFrame::iterator it = textFrame->begin();
++
++ while (!it.atEnd()) {
++ Q_ASSERT(!engine.currentLine().isValid());
++
++ QTextBlock block = it.currentBlock();
++ engine.addTextBlock(textDocument, block, position, m_color, m_linkColor, selectionStart, selectionEnd,
++ (textDocument->characterCount() > QQuickTextPrivate::largeTextSizeThreshold ?
++ m_viewport : QRectF()));
++ ++it;
++ }
++ }
++ }
++
++ engine.addToSceneGraph(this, recycleBin, QQuickText::TextStyle(m_textStyle), m_styleColor);
++ }
++
++ void QSGInternalTextNode::doAddTextLayout(QPointF position,
++ QTextLayout *textLayout,
++ int selectionStart,
++ int selectionEnd,
++ int lineStart,
++ int lineCount,
++ RecycleBin *recycleBin)
++ {
++ QQuickTextNodeEngine engine;
++ engine.setTextColor(m_color);
++ engine.setSelectedTextColor(m_selectionTextColor);
++ engine.setSelectionColor(m_selectionColor);
++ engine.setAnchorColor(m_linkColor);
++ engine.setPosition(position);
++ engine.setDevicePixelRatio(m_devicePixelRatio);
++
++ #if QT_CONFIG(im)
++ int preeditLength = textLayout->preeditAreaText().size();
++ int preeditPosition = textLayout->preeditAreaPosition();
++ #endif
++
++ QVarLengthArray<QTextLayout::FormatRange> colorChanges;
++ engine.mergeFormats(textLayout, &colorChanges);
++
++ lineCount = lineCount >= 0
++ ? qMin(lineStart + lineCount, textLayout->lineCount())
++ : textLayout->lineCount();
++
++ bool inViewport = false;
++ for (int i=lineStart; i<lineCount; ++i) {
++ QTextLine line = textLayout->lineAt(i);
++
++ int start = line.textStart();
++ int length = line.textLength();
++ int end = start + length;
++
++ #if QT_CONFIG(im)
++ if (preeditPosition >= 0
++ && preeditPosition >= start
++ && preeditPosition < end) {
++ end += preeditLength;
++ }
++ #endif
++ // If there's a lot of text, insert only the range of lines that can possibly be visible within the viewport.
++ if (m_viewport.isNull() || (line.y() + line.height() > m_viewport.top() && line.y() < m_viewport.bottom())) {
++ if (!inViewport && !m_viewport.isNull()) {
++ m_firstLineInViewport = i;
++ qCDebug(lcVP) << "first line in viewport" << i << "@" << line.y();
++ }
++ inViewport = true;
++ engine.setCurrentLine(line);
++ engine.addGlyphsForRanges(colorChanges, start, end, selectionStart, selectionEnd);
++ } else if (inViewport) {
++ Q_ASSERT(!m_viewport.isNull());
++ m_firstLinePastViewport = i;
++ qCDebug(lcVP) << "first omitted line past bottom of viewport" << i << "@" << line.y();
++ break; // went past the bottom of the viewport, so we're done
++ }
++ }
++
++ engine.addToSceneGraph(this, recycleBin, QQuickText::TextStyle(m_textStyle), m_styleColor);
++ }
++
++ namespace {
++
++ using RecycleBin = QSGInternalTextNode::RecycleBin;
++
++ class ChildNodeCollector : public QSGNodeVisitorEx
++ {
++ public:
++ ChildNodeCollector(RecycleBin &recycleBin,
++ std::vector<QSGNode *> &untrackedNodes)
++ : m_recycleBin(recycleBin)
++ , m_untrackedNodes(untrackedNodes)
++ {
++ }
++
++ bool visit(QSGTransformNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGTransformNode *) override
++ {
++ }
++
++ bool visit(QSGClipNode *node) override
++ {
++ // Clip nodes are used for selections and are currently not recycled. Clip nodes
++ // will anyway be separate batch roots, so this text does not trigger reuploads.
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGClipNode *) override
++ {
++ }
++
++ bool visit(QSGGeometryNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGGeometryNode *) override
++ {
++ }
++
++ bool visit(QSGOpacityNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGOpacityNode *) override
++ {
++ }
++
++ bool visit(QSGInternalImageNode *node) override
++ {
++ m_recycleBin.unusedNodes.append({ node, RecycleBin::ImageNode });
++ return false;
++ }
++
++ void endVisit(QSGInternalImageNode *) override
++ {
++ }
++
++ bool visit(QSGPainterNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGPainterNode *) override
++ {
++ }
++
++ bool visit(QSGInternalRectangleNode *node) override
++ {
++ m_recycleBin.unusedNodes.append({ node, RecycleBin::RectangleNode });
++ return false;
++ }
++
++ void endVisit(QSGInternalRectangleNode *) override
++ {
++ }
++
++ bool visit(QSGGlyphNode *node) override
++ {
++ // Some glyph nodes with a lot of content have content split into child nodes.
++ // For now, we do not recycle these nodes as it is an uncommon case and adds
++ // complexity. This also affects curve-rendered glyphs, which will have children
++ // and thus be excluded from recycling.
++ if (node->childCount() > 0)
++ m_untrackedNodes.push_back(node);
++ else
++ m_recycleBin.unusedNodes.append({ node, RecycleBin::GlyphNode });
++
++ return false;
++ }
++
++ void endVisit(QSGGlyphNode *) override
++ {
++ }
++
++ bool visit(QSGRootNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGRootNode *) override
++ {
++ }
++
++ #if QT_CONFIG(quick_sprite)
++ bool visit(QSGSpriteNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGSpriteNode *) override
++ {
++ }
++ #endif
++ bool visit(QSGRenderNode *node) override
++ {
++ Q_UNREACHABLE();
++ m_untrackedNodes.push_back(node);
++ return false;
++ }
++
++ void endVisit(QSGRenderNode *) override
++ {
++ }
++
++ private:
++ RecycleBin &m_recycleBin;
++ std::vector<QSGNode *> &m_untrackedNodes;
++ };
++ }
++
++ QSGGlyphNode *QSGInternalTextNode::RecycleBin::takeNextReusableGlyphNode(
++ QSGTextNode::RenderType renderType)
++ {
++ const UnusedNode *next = peekNextReusableNode(GlyphNode);
++ if (next == nullptr)
++ return nullptr;
++
++ QSGGlyphNode *glyphNode = static_cast<QSGGlyphNode *>(next->node);
++ if (glyphNode->renderType() != renderType) {
++ qCDebug(lcTextRecycle) << " Unsuitable node found:" << glyphNode->renderType();
++ stopReusing();
++ return nullptr;
++ }
++
++ ++reusedNodes;
++ return glyphNode;
++ }
++
++ QSGInternalRectangleNode *QSGInternalTextNode::findOrCreateRectangleNode(RecycleBin *recycleBin)
++ {
++ qCDebug(lcTextRecycle) << "Searching for rectangle node";
++
++ if (recycleBin != nullptr) {
++ if (QSGVisitableNode *node = recycleBin->takeNextReusableNode(RecycleBin::RectangleNode)) {
++ qCDebug(lcTextRecycle) << " Found node, recycling";
++ return static_cast<QSGInternalRectangleNode *>(node);
++ }
++ }
++
++ qCDebug(lcTextRecycle) << " Creating new rectangle node, no suitable node found";
++ return m_renderContext->sceneGraphContext()->createInternalRectangleNode();
++ }
++
++
++ QSGInternalImageNode *QSGInternalTextNode::findOrCreateImageNode(RecycleBin *recycleBin)
++ {
++ qCDebug(lcTextRecycle) << "Searching for image node";
++
++ if (recycleBin != nullptr) {
++ if (QSGVisitableNode *node = recycleBin->takeNextReusableNode(RecycleBin::ImageNode)) {
++ qCDebug(lcTextRecycle) << " Found node, recycling";
++ return static_cast<QSGInternalImageNode *>(node);
++ }
++ }
++
++ qCDebug(lcTextRecycle) << " Creating new image node, no suitable node found";
++ return m_renderContext->sceneGraphContext()->createInternalImageNode(m_renderContext);
++ }
++
++ QSGGlyphNode *QSGInternalTextNode::findOrCreateGlyphNode(RenderType renderType,
++ RecycleBin *recycleBin,
++ QSGNode *parentNode)
++ {
++ qCDebug(lcTextRecycle) << "Searching for glyph node with renderType" << renderType;
++ renderType = m_renderContext->sceneGraphContext()->processTextRenderType(renderType);
++
++ if (recycleBin != nullptr) {
++ if (parentNode != this) {
++ recycleBin->stopReusing();
++ } else if (QSGGlyphNode *node = recycleBin->takeNextReusableGlyphNode(renderType)) {
++ qCDebug(lcTextRecycle) << " Found node, recycling";
++ return node;
++ }
++ }
++
++ qCDebug(lcTextRecycle) << " Creating new glyph node, no suitable node found";
++ return m_renderContext->sceneGraphContext()->createGlyphNode(m_renderContext,
++ renderType);
++ }
++
++ void QSGInternalTextNode::clear()
++ {
++ while (firstChild() != nullptr)
++ delete firstChild();
++ m_cursorNode = nullptr;
++ qDeleteAll(m_textures);
++ m_textures.clear();
++ }
++
++ void QSGInternalTextNode::recycle(RecycleBin *recycleBin)
++ {
++ Q_ASSERT(recycleBin != nullptr);
++ Q_ASSERT(recycleBin->unusedNodes.isEmpty());
++
++ recycleBin->unusedNodes.reserve(childCount());
++
++ std::vector<QSGNode *> untrackedNodes;
++ ChildNodeCollector collector(*recycleBin, untrackedNodes);
++
++ // We retain nodes for recycling and then discard the unused ones after adding a new layout.
++ // This is because removing and adding nodes to the root batch can cause expensive reuploads
++ // of all data. So we visit all immediate children of the text node and keep them around
++ // until they are either used or discardUnusedNodes() is called later. For rapid updates,
++ // you will typically just have a single glyph node and rapidly replace its text, so this
++ // is what we are optimzing for.
++ QSGNode *node = firstChild();
++ while (node != nullptr) {
++ if (node->type() == QSGNode::GeometryNodeType
++ && node->flags().testFlag(QSGNode::IsVisitableNode)) {
++ QSGVisitableNode *visitableNode = static_cast<QSGVisitableNode *>(node);
++ visitableNode->accept(&collector);
++ } else {
++ untrackedNodes.push_back(node);
++ }
++
++ node = node->nextSibling();
++ }
++
++ for (const RecycleBin::UnusedNode &unusedNode : recycleBin->unusedNodes) {
++ if (unusedNode.type == RecycleBin::GlyphNode)
++ static_cast<QSGGlyphNode *>(unusedNode.node)->recycle();
++ }
++
++ qDeleteAll(untrackedNodes);
++
++ #if defined(QSGINTERNALTEXTNODE_NO_RECYCLE)
++ for (const RecycleBin::UnusedNode &unusedNode : recycleBin->unusedNodes)
++ delete unusedNode.node;
++ recycleBin->unusedNodes = {};
++
++ qDeleteAll(m_textures);
++ #else
++ recycleBin->unusedTextures = m_textures;
++ #endif
++
++ qCDebug(lcTextRecycle) << "Recycle: "
++ << recycleBin->unusedNodes.size() << "nodes"
++ << recycleBin->unusedTextures.size() << "textures";
++
++ m_textures.clear();
++ m_cursorNode = nullptr;
++ recycleBin->unusedNodes.squeeze();
++ }
++
++ void QSGInternalTextNode::discardUnusedNodes(RecycleBin *recycleBin)
++ {
++ qCDebug(lcTextRecycle) << "Discard: "
++ << recycleBin->unusedNodes.size() - recycleBin->reusedNodes << "nodes"
++ << recycleBin->unusedTextures.size() << "textures";
++
++ for (qsizetype i = recycleBin->reusedNodes; i < recycleBin->unusedNodes.size(); ++i)
++ delete recycleBin->unusedNodes.at(i).node;
++ recycleBin->unusedNodes = {};
++ recycleBin->reusedNodes = 0;
++ recycleBin->reuseStopped = false;
++
++ qDeleteAll(recycleBin->unusedTextures);
++ recycleBin->unusedTextures = {};
++ }
++
++ QT_END_NAMESPACE