aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2023-12-21 17:45:19 -0700
committerShawn Rutledge <shawn.rutledge@qt.io>2024-04-08 08:31:14 -0700
commitdb112ff02952437544dad9503b22785b3ff24e99 (patch)
tree28599134307d34f2d649daecee99cb562935229f
parent0d1462b3ae8b7cf7975828afc6009d6934c20608 (diff)
TextEdit: don't skip nested frames or their blocks in updatePaintNode
I don't fully understand the previous logic behind keeping the same firstDirtyPos while we iterate all the frames. In case a document contains a mix of tables and text for example, we may iterate to a block in the root frame that is just beyond the first table, and set firstDirtyPos accordingly (which I think is meant to say that everything up to that character position has been rendered, but it's not true at that moment in time); and then when we look at the frame corresponding to the first table, rule out rendering the whole frame or any of its blocks based on the fact that it comes _before_ firstDirtyPos. The fact that we got past its position in the root frame should not preclude rendering the nested frame. So now it looks safer to reset firstDirtyPos before iterating each nested frame. Anyway the logic from f513e88403b66c4a5efe4c62c160dfce151efb33 assumed that we wanted to populate the whole document into the scene graph, and should just avoid redoing work while keeping the nodes up-to-date; whereas since 9db23e0e04906cf9ea33e23fa41f34955e5e6fe0 we're trying to avoid populating nodes too far outside the viewport. That's probably the bigger optimization: if we are avoiding most of the blocks in large documents most of the time, redoing work to render the ones that are in the viewport is not such a big deal. And it seems dangerous to preemptively skip frames or blocks before we've checked whether they are in the viewport. So now we visit all frames regardless of firstDirtyPos, but still skip blocks within the frame that are beyond the bottom of the viewport. And whenever we do render the first block that is beyond the viewport, we also enlarge our local copy of the viewport rectangle to encompass that block's bounds, to ensure that when we visit nested frames, we will also render other blocks within the enlarged bounds. Effectively this means if we render the next block in the root frame immediately past a table, we will also render all cells in the lower part of that table (between the top of the viewport and the bottom of the table). That might result in more SG nodes than we strictly need (outside the viewport bounds); but at the end, the stored QQuickTextEditPrivate::renderedRegion must include that one block that was past the table, so we want all intervening blocks too. QQuickTextEditPrivate::transformChanged() will not call update() if renderedRegion tells it that the coverage is complete within the current viewport, and nothing more needs to be rendered. That was the root of this bug. The autotest just counts the number of blocks rendered at different scroll positions in a Flickable. Perhaps it will turn out to be sensitive to font sizes on various platforms; but the number of blocks rendered was way too small in case of the bug, so we can afford a tolerance in the comparison, if needed. Fixes: QTBUG-118636 Pick-to: 6.5 6.6 6.7 Change-Id: Id1f2b393f26e2dd5b198e9d5251e8c33459c3c07 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io>
-rw-r--r--src/quick/items/qquicktextedit.cpp19
-rw-r--r--src/quick/items/qquicktextedit_p_p.h1
-rw-r--r--tests/auto/quick/qquicktextedit/data/inFlickable.qml7
-rw-r--r--tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp105
4 files changed, 127 insertions, 5 deletions
diff --git a/src/quick/items/qquicktextedit.cpp b/src/quick/items/qquicktextedit.cpp
index 66615d9354..22bbd6e05b 100644
--- a/src/quick/items/qquicktextedit.cpp
+++ b/src/quick/items/qquicktextedit.cpp
@@ -2523,14 +2523,17 @@ QSGNode *QQuickTextEdit::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
d->firstBlockInViewport = -1;
d->firstBlockPastViewport = -1;
+ int frameCount = -1;
while (!frames.isEmpty()) {
QTextFrame *textFrame = frames.takeFirst();
+ ++frameCount;
+ if (frameCount > 0)
+ firstDirtyPos = 0;
+ qCDebug(lcVP) << "frame" << frameCount << textFrame
+ << "from" << positionToRectangle(textFrame->firstPosition()).topLeft()
+ << "to" << positionToRectangle(textFrame->lastPosition()).bottomRight();
frames.append(textFrame->childFrames());
frameDecorationsEngine.addFrameDecorations(d->document, textFrame);
-
- if (textFrame->lastPosition() < firstDirtyPos
- || textFrame->firstPosition() >= firstCleanNode.startPos())
- continue;
resetEngine(&engine, d->color, d->selectedTextColor, d->selectionColor);
if (textFrame->firstPosition() > textFrame->lastPosition()
@@ -2613,8 +2616,13 @@ QSGNode *QQuickTextEdit::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
}
break; // skip rest of blocks in this frame
}
- if (inView && !block.text().isEmpty() && coveredRegion.isValid())
+ if (inView && !block.text().isEmpty() && coveredRegion.isValid()) {
d->renderedRegion = d->renderedRegion.united(coveredRegion);
+ // In case we're going to visit more (nested) frames after this, ensure that we
+ // don't omit any blocks that fit within the region that we claim as fully rendered.
+ if (!frames.isEmpty())
+ viewport = viewport.united(d->renderedRegion);
+ }
}
if (inView && d->firstBlockInViewport < 0)
d->firstBlockInViewport = block.blockNumber();
@@ -3289,6 +3297,7 @@ void QQuickTextEditPrivate::addCurrentTextNodeToRoot(QQuickTextNodeEngine *engin
it = textNodeMap.insert(it, TextNode(startPos, node));
++it;
root->appendChildNode(node);
+ ++renderedBlockCount;
}
QSGInternalTextNode *QQuickTextEditPrivate::createTextNode()
diff --git a/src/quick/items/qquicktextedit_p_p.h b/src/quick/items/qquicktextedit_p_p.h
index c5f0137ea0..da01be47c2 100644
--- a/src/quick/items/qquicktextedit_p_p.h
+++ b/src/quick/items/qquicktextedit_p_p.h
@@ -179,6 +179,7 @@ public:
int lineCount = 0;
int firstBlockInViewport = -1; // can be wrong after scrolling sometimes
int firstBlockPastViewport = -1; // only for the autotest
+ int renderedBlockCount = -1; // only for the autotest
QRectF renderedRegion;
enum UpdateType {
diff --git a/tests/auto/quick/qquicktextedit/data/inFlickable.qml b/tests/auto/quick/qquicktextedit/data/inFlickable.qml
index 7a896db29b..183ddd6701 100644
--- a/tests/auto/quick/qquicktextedit/data/inFlickable.qml
+++ b/tests/auto/quick/qquicktextedit/data/inFlickable.qml
@@ -1,6 +1,7 @@
import QtQuick 2.0
Flickable {
+ id: flick
width: 320; height: 120; contentHeight: text.height
TextEdit {
id: text
@@ -8,4 +9,10 @@ Flickable {
font.pixelSize: 20
text: "several\nlines\nof\ntext\n-\ntry\nto\nflick"
}
+ Text {
+ color: "red"
+ parent: flick // stay on top
+ anchors.right: parent.right
+ text: flick.contentY.toFixed(1)
+ }
}
diff --git a/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp b/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp
index 3a4231f953..cc994fe783 100644
--- a/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp
+++ b/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp
@@ -8,6 +8,8 @@
#include <QtQuick/QQuickTextDocument>
#include <QtQuickTest/QtQuickTest>
#include <QTextDocument>
+#include <QtGui/qtextobject.h>
+#include <QtGui/QTextTable>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcontext.h>
#include <QtQml/qqmlexpression.h>
@@ -58,6 +60,8 @@ Q_DECLARE_METATYPE(QKeySequence::StandardKey)
Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests")
+// #define DEBUG_WRITE_INPUT
+
static bool isPlatformWayland()
{
return !QGuiApplication::platformName().compare(QLatin1String("wayland"), Qt::CaseInsensitive);
@@ -158,6 +162,8 @@ private slots:
void largeTextObservesViewport();
void largeTextSelection();
void renderingAroundSelection();
+ void largeTextTables_data();
+ void largeTextTables();
void signal_editingfinished();
@@ -3886,6 +3892,105 @@ void tst_qquicktextedit::renderingAroundSelection()
QTRY_COMPARE(textItem->sortedLinePositions, sortedLinePositions);
}
+struct OffsetAndExpectedBlocks {
+ int tableIndex; // which nested frame
+ qreal tableOffset; // fraction of that frame's height to scroll to
+ int minExpectedBlockCount;
+
+ OffsetAndExpectedBlocks(int i, qreal o, int c)
+ : tableIndex(i), tableOffset(o), minExpectedBlockCount(c) {}
+};
+
+typedef QList<OffsetAndExpectedBlocks> OffsetAndExpectedBlocksList;
+
+void tst_qquicktextedit::largeTextTables_data()
+{
+ QTest::addColumn<int>("tables");
+ QTest::addColumn<int>("tableCols");
+ QTest::addColumn<int>("tableRows");
+ QTest::addColumn<OffsetAndExpectedBlocksList>("steps");
+
+ QTest::newRow("one big table") << 1 << 3 << 70
+ << OffsetAndExpectedBlocksList{
+ OffsetAndExpectedBlocks(1, 0.75, 150),
+ OffsetAndExpectedBlocks(1, 0.5, 150)};
+ QTest::newRow("short tables") << 5 << 3 << 10
+ << OffsetAndExpectedBlocksList{
+ OffsetAndExpectedBlocks(4, 0.75, 35),
+ OffsetAndExpectedBlocks(3, 0.25, 50),
+ OffsetAndExpectedBlocks(2, 0.75, 50)};
+}
+
+void tst_qquicktextedit::largeTextTables() // QTBUG-118636
+{
+ QFETCH(int, tables);
+ QFETCH(int, tableCols);
+ QFETCH(int, tableRows);
+ QFETCH(OffsetAndExpectedBlocksList, steps);
+
+ QStringList lines;
+
+ lines << QLatin1String("<h1>") + QTest::currentDataTag() + "</h1>";
+ for (int t = 0; t < tables; ++t) {
+ if (t > 0)
+ lines << QString("<p>table %1</p>").arg(t);
+ lines << "<table border='1'>";
+ for (int r = 0; r < tableRows; ++r) {
+ lines << " <tr>";
+ for (int c = 0; c < tableCols; ++c)
+ lines << QString(" <td>table %1 cell %2, %3</td>").arg(t).arg(c).arg(r);
+ lines << " </tr>";
+ }
+ lines << "</table>";
+ }
+ lines << "<p>here endeth the tables</p>";
+ QString html = lines.join('\n');
+
+#ifdef DEBUG_WRITE_INPUT
+ QFile f(QLatin1String("/tmp/") + QTest::currentDataTag() + ".html");
+ f.open(QFile::WriteOnly);
+ f.write(html.toUtf8());
+ f.close();
+#endif
+
+ QQuickView window;
+ QVERIFY(QQuickTest::showView(window, testFileUrl("inFlickable.qml")));
+ QQuickFlickable *flick = qmlobject_cast<QQuickFlickable *>(window.rootObject());
+ QVERIFY(flick);
+ QQuickTextEdit *te = window.rootObject()->findChild<QQuickTextEdit *>();
+ QVERIFY(te);
+ auto *tePriv = QQuickTextEditPrivate::get(te);
+ auto font = te->font();
+ font.setPixelSize(10);
+ te->setFont(font);
+
+ te->setTextFormat(QQuickTextEdit::RichText);
+ te->setText(html);
+ te->setFlag(QQuickItem::ItemObservesViewport); // this isn't "large text", but test viewporting anyway
+
+ QTextDocument *doc = te->textDocument()->textDocument();
+ QList<QTextFrame *> frames = doc->rootFrame()->childFrames();
+ frames.prepend(doc->rootFrame());
+ qCDebug(lcTests) << "blocks" << doc->blockCount() << "chars" << doc->characterCount() << "frames" << frames;
+
+ for (const OffsetAndExpectedBlocks &oeb : steps) {
+ QCOMPARE_GT(frames.size(), oeb.tableIndex);
+ const QTextFrame *textFrame = frames.at(oeb.tableIndex);
+ const QTextCursor top = textFrame->firstCursorPosition();
+ const qreal yTop = te->positionToRectangle(top.position()).top();
+ const QTextCursor bottom = textFrame->lastCursorPosition();
+ const qreal yBottom = te->positionToRectangle(bottom.position()).bottom();
+ const qreal y = yTop + (yBottom - yTop) * oeb.tableOffset;
+ qCDebug(lcTests) << "frame" << textFrame << "goes from pos" << top.position() << "y" << yTop
+ << "to pos" << bottom.position() << "y" << yBottom << "; scrolling to" << y
+ << "which is at" << oeb.tableOffset << "of table height" << (yBottom - yTop);
+ flick->setContentY(y);
+ qCDebug(lcTests) << tePriv->renderedRegion << "rendered blocks" << tePriv->renderedBlockCount << ":"
+ << tePriv->firstBlockInViewport << "to" << tePriv->firstBlockPastViewport;
+ QTRY_COMPARE_GE(tePriv->renderedBlockCount, oeb.minExpectedBlockCount);
+ }
+}
+
void tst_qquicktextedit::signal_editingfinished()
{
QQuickView window;