aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2021-08-19 22:40:56 +0200
committerShawn Rutledge <shawn.rutledge@qt.io>2021-11-29 23:12:45 +0100
commit9b62f4c27ac3fb3dc563c7f4657094c14d752bac (patch)
tree4b950ad460a556526ab46f0cea02ee3f23cf5324
parentf7af78fced16e349b861a412ba467fd33d549377 (diff)
Allow any Item to act as a viewport for any of its children
The children that have the ItemObservesViewport flag set will have transformChanged() called whenever any parent moves, and can use clipRect() to avoid creating SG nodes that fall outside the viewport. So now the viewport can be narrower than the whole QQuickWindow; but the fallback is the window. It was always unclear why we had both boundingRect() and clipRect(), returning the same rectangular areas. Now clipRect() can actually be used for a rough pre-clipping, to limit the amount of vertex data going into the scene graph, which can save a lot of memory in some cases. setClip() sets the ItemIsViewport flag, for the sake of making it easy to do viewporting without writing C++; but it's well known that scissoring has a performance impact, so we recommend that users who are writing C++ anyway can set the flags in C++ rather than in QML. In case there are nested items with both flags ItemIsViewport and ItemObservesViewport, calling clipRect() on something inside the innermost viewport is recursive: we intersect all the viewports, going up the hierarchy to the window. So it's possible for the innermost item to be clipped down to a small "iris" where all the viewports are letting "light" pass through. [ChangeLog][QtQuick][QQuickItem] QQuickItem::clipRect() now provides the region visible in the viewport, and can be used to limit SG node vertices as an optimization in custom items, at the cost of having updatePaintNode() called more often. See docs about the new ItemObservesViewport and ItemIsViewport flags. Fixes: QTBUG-37364 Task-number: QTBUG-60491 Task-number: QTBUG-65741 Task-number: QTBUG-77521 Task-number: QTBUG-90734 Change-Id: I71a26c2dab4e991d7fb0f6679f1aa0c34e7a14ee Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
-rw-r--r--src/quick/doc/src/concepts/visualcanvas/scenegraph.qdoc20
-rw-r--r--src/quick/items/qquickflickable.cpp1
-rw-r--r--src/quick/items/qquickflipable.cpp6
-rw-r--r--src/quick/items/qquickitem.cpp132
-rw-r--r--src/quick/items/qquickitem.h5
-rw-r--r--src/quick/items/qquickitem_p.h11
-rw-r--r--src/quick/items/qquicktext.cpp29
-rw-r--r--src/quick/items/qquicktext_p_p.h2
-rw-r--r--src/quick/items/qquicktextnode.cpp20
-rw-r--r--src/quick/items/qquicktextnode_p.h4
-rw-r--r--src/quick/items/qquickview.cpp1
-rw-r--r--src/quick/items/qquickwindow.cpp7
-rw-r--r--tests/auto/quick/qquickitem2/data/viewports.qml54
-rw-r--r--tests/auto/quick/qquickitem2/tst_qquickitem.cpp165
-rw-r--r--tests/auto/quick/qquicktext/data/viewport.qml19
-rw-r--r--tests/auto/quick/qquicktext/tst_qquicktext.cpp63
16 files changed, 500 insertions, 39 deletions
diff --git a/src/quick/doc/src/concepts/visualcanvas/scenegraph.qdoc b/src/quick/doc/src/concepts/visualcanvas/scenegraph.qdoc
index faec8df7e4..735efdf8e0 100644
--- a/src/quick/doc/src/concepts/visualcanvas/scenegraph.qdoc
+++ b/src/quick/doc/src/concepts/visualcanvas/scenegraph.qdoc
@@ -692,6 +692,26 @@ with multiple windows.
like a TextArea, this is fine. One should, however, use clip on
smaller items with caution as it prevents batching. This includes
button label, text field or list delegate and table cells.
+ Clipping a Flickable (or item view) can often be avoided by arranging
+ the UI so that opaque items cover areas around the Flickable, and
+ otherwise relying on the window edges to clip everything else.
+
+ Setting Item::clip to \c true also sets the \l QQuickItem::ItemIsViewport
+ flag; child items with the \l QQuickItem::ItemObservesViewport flag may
+ use the viewport for a rough pre-clipping step: e.g. \l Text omits
+ lines of text that are completely outside the viewport. Omitting scene
+ graph nodes or limiting the \l {QSGGeometry::vertexCount()}{vertices}
+ is an optimization, which can be achieved by setting the
+ \l {QQuickItem::flags()}{flags} in C++ rather than setting
+ \l Item::clip in QML.
+
+ When implementing QQuickItem::updatePaintNode() in a custom item,
+ if it can render a lot of details over a large geometric area,
+ you should think about whether it's efficient to limit the graphics
+ to the viewport; if so, you can set the \l ItemObservesViewport flag
+ and read the currently exposed area from QQuickItem::clipRect().
+ One consequence is that updatePaintNode() will be called more often
+ (typically once per frame whenever content is moving in the viewport).
\section2 Vertex Buffers
diff --git a/src/quick/items/qquickflickable.cpp b/src/quick/items/qquickflickable.cpp
index b865270973..9024e2ba93 100644
--- a/src/quick/items/qquickflickable.cpp
+++ b/src/quick/items/qquickflickable.cpp
@@ -289,6 +289,7 @@ void QQuickFlickablePrivate::init()
q->setAcceptedMouseButtons(Qt::LeftButton);
q->setAcceptTouchEvents(true);
q->setFiltersChildMouseEvents(true);
+ q->setFlag(QQuickItem::ItemIsViewport);
QQuickItemPrivate *viewportPrivate = QQuickItemPrivate::get(contentItem);
viewportPrivate->addItemChangeListener(this, QQuickItemPrivate::Geometry);
}
diff --git a/src/quick/items/qquickflipable.cpp b/src/quick/items/qquickflipable.cpp
index 27fb02987c..c8c96d925e 100644
--- a/src/quick/items/qquickflipable.cpp
+++ b/src/quick/items/qquickflipable.cpp
@@ -70,7 +70,7 @@ class QQuickFlipablePrivate : public QQuickItemPrivate
public:
QQuickFlipablePrivate() : current(QQuickFlipable::Front), front(nullptr), back(nullptr), sideDirty(false) {}
- void transformChanged(QQuickItem *transformedItem) override;
+ bool transformChanged(QQuickItem *transformedItem) override;
void updateSide();
void setBackTransform();
@@ -219,7 +219,7 @@ QQuickFlipable::Side QQuickFlipable::side() const
return d->current;
}
-void QQuickFlipablePrivate::transformChanged(QQuickItem *transformedItem)
+bool QQuickFlipablePrivate::transformChanged(QQuickItem *transformedItem)
{
Q_Q(QQuickFlipable);
@@ -228,7 +228,7 @@ void QQuickFlipablePrivate::transformChanged(QQuickItem *transformedItem)
q->polish();
}
- QQuickItemPrivate::transformChanged(transformedItem);
+ return QQuickItemPrivate::transformChanged(transformedItem);
}
void QQuickFlipable::updatePolish()
diff --git a/src/quick/items/qquickitem.cpp b/src/quick/items/qquickitem.cpp
index a5b93297b0..fffee1afa0 100644
--- a/src/quick/items/qquickitem.cpp
+++ b/src/quick/items/qquickitem.cpp
@@ -94,6 +94,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcHoverTrace)
Q_DECLARE_LOGGING_CATEGORY(lcPtr)
Q_DECLARE_LOGGING_CATEGORY(lcTransient)
Q_LOGGING_CATEGORY(lcHandlerParent, "qt.quick.handler.parent")
+Q_LOGGING_CATEGORY(lcVP, "qt.quick.viewport")
void debugFocusTree(QQuickItem *item, QQuickItem *scope = nullptr, int depth = 1)
{
@@ -2060,6 +2061,9 @@ void QQuickItemPrivate::updateSubFocusItem(QQuickItem *scope, bool focus)
\value ItemHasContents Indicates the item has visual content and should be
rendered by the scene graph.
\value ItemAcceptsDrops Indicates the item accepts drag and drop events.
+ \value ItemIsViewport Indicates that the item defines a viewport for its children.
+ \value ItemObservesViewport Indicates that the item wishes to know the
+ viewport bounds when any ancestor has the ItemIsViewport flag set.
\sa setFlag(), setFlags(), flags()
*/
@@ -3213,6 +3217,7 @@ QQuickItemPrivate::QQuickItemPrivate()
, touchEnabled(false)
, hasCursorHandler(false)
, maybeHasSubsceneDeliveryAgent(true)
+ , subtreeTransformChangedEnabled(true)
, dirtyAttributes(0)
, nextDirtyItem(nullptr)
, prevDirtyItem(nullptr)
@@ -3748,6 +3753,12 @@ QList<QQuickItem *> QQuickItem::childItems() const
\note Clipping can affect rendering performance. See \l {Clipping} for more
information.
+
+ \note For the sake of QML, setting clip to \c true also sets the
+ \l ItemIsViewport flag, which sometimes acts as an optimization: child items
+ that have the \l ItemObservesViewport flag may forego creating scene graph nodes
+ that fall outside the viewport. But the \c ItemIsViewport flag can also be set
+ independently.
*/
bool QQuickItem::clip() const
{
@@ -3760,6 +3771,10 @@ void QQuickItem::setClip(bool c)
return;
setFlag(ItemClipsChildrenToShape, c);
+ if (c)
+ setFlag(ItemIsViewport);
+ else if (!(inherits("QQuickFlickable") || inherits("QQuickRootItem")))
+ setFlag(ItemIsViewport, false);
emit clipChanged(c);
}
@@ -5275,22 +5290,38 @@ QPointF QQuickItemPrivate::computeTransformOrigin() const
all its children that its transform has changed, with \a transformedItem always
being the parent item that caused the change. Override to react, e.g. to
call update() if the item needs to re-generate SG nodes based on visible extents.
+ If you override in a subclass, you must also call this (superclass) function
+ and return the value from it.
+
+ This function recursively visits all children as long as
+ subtreeTransformChangedEnabled is true, returns \c true if any of those
+ children still has the ItemObservesViewport flag set, but otherwise
+ turns subtreeTransformChangedEnabled off, if no children are observing.
*/
-void QQuickItemPrivate::transformChanged(QQuickItem *transformedItem)
+bool QQuickItemPrivate::transformChanged(QQuickItem *transformedItem)
{
Q_Q(QQuickItem);
- if (q != transformedItem)
- return;
- // Inform the children in paint order: by the time we visit leaf items,
- // they can see any consequences in their parents
- for (auto child : paintOrderChildItems())
- QQuickItemPrivate::get(child)->transformChanged(transformedItem);
+ bool childWantsIt = false;
+ if (subtreeTransformChangedEnabled) {
+ // Inform the children in paint order: by the time we visit leaf items,
+ // they can see any consequences in their parents
+ for (auto child : paintOrderChildItems())
+ childWantsIt |= QQuickItemPrivate::get(child)->transformChanged(transformedItem);
+ }
#if QT_CONFIG(quick_shadereffect)
- if (extra.isAllocated() && extra->layer)
- extra->layer->updateMatrix();
+ if (q == transformedItem) {
+ if (extra.isAllocated() && extra->layer)
+ extra->layer->updateMatrix();
+ }
#endif
+ const bool ret = childWantsIt || q->flags().testFlag(QQuickItem::ItemObservesViewport);
+ if (!ret && componentComplete && subtreeTransformChangedEnabled) {
+ qCDebug(lcVP) << "turned off subtree transformChanged notification after checking all children of" << q;
+ subtreeTransformChangedEnabled = false;
+ }
+ return ret;
}
QPointF QQuickItemPrivate::adjustedPosForTransform(const QPointF &centroidParentPos,
@@ -5570,19 +5601,75 @@ void QQuickItem::updateInputMethod(Qt::InputMethodQueries queries)
}
#endif // im
-// XXX todo - do we want/need this anymore?
-/*! \internal */
+/*!
+ Returns the extents of the item in its own coordinate system:
+ a rectangle from \c{0, 0} to \l width() and \l height().
+*/
QRectF QQuickItem::boundingRect() const
{
Q_D(const QQuickItem);
return QRectF(0, 0, d->width, d->height);
}
-/*! \internal */
+/*!
+ Returns the rectangular area within this item that is currently visible in
+ \l viewportItem(), if there is a viewport; otherwise, the extents of this
+ item in its own coordinate system: a rectangle from \c{0, 0} to \l width()
+ and \l height(). This is the region intended to remain visible if \a clip()
+ is \c true. It can also be used in updatePaintNode() to limit the graphics
+ added to the scene graph.
+
+ For example, a large drawing or a large text document might be shown in a
+ Flickable that occupies only part of the application's Window: in that
+ case, Flickable is the viewport item, and a custom content-rendering item
+ may choose to omit scene graph nodes that fall outside the area that is
+ currently visible. If the \l ItemObservesViewport flag is set, this area
+ will change each time the user scrolls the content in the Flickable.
+
+ In case of nested viewport items, clipRect() is the intersection of the
+ \c {boundingRect}s of all ancestors that have the \l ItemIsViewport flag set,
+ mapped to the coordinate system of \em this item.
+
+ \sa boundingRect()
+*/
QRectF QQuickItem::clipRect() const
{
Q_D(const QQuickItem);
- return QRectF(0, 0, d->width, d->height);
+ QRectF ret(0, 0, d->width, d->height);
+ if (QQuickItem *viewport = viewportItem()) {
+ // if the viewport is already "this", there's nothing to intersect;
+ // and don't call clipRect() again, to avoid infinite recursion
+ if (viewport == this)
+ return ret;
+ const auto mappedViewportRect = mapRectFromItem(viewport, viewport->clipRect());
+ qCDebug(lcVP) << this << "intersecting" << viewport << mappedViewportRect << ret << "->" << mappedViewportRect.intersected(ret);
+ return mappedViewportRect.intersected(ret);
+ }
+ return ret;
+}
+
+/*!
+ If the \l ItemObservesViewport flag is set,
+ returns the nearest parent with the \l ItemIsViewport flag.
+ Returns the window's contentItem if the flag is not set,
+ or if no other viewport item is found.
+
+ Returns \nullptr only if there is no viewport item and this item is not
+ shown in a window.
+
+ \sa clipRect()
+*/
+QQuickItem *QQuickItem::viewportItem() const
+{
+ if (flags().testFlag(ItemObservesViewport)) {
+ QQuickItem *par = parentItem();
+ while (par) {
+ if (par->flags().testFlag(QQuickItem::ItemIsViewport))
+ return par;
+ par = par->parentItem();
+ }
+ }
+ return (window() ? window()->contentItem() : nullptr);
}
/*!
@@ -6528,6 +6615,8 @@ void QQuickItemPrivate::itemChange(QQuickItem::ItemChange change, const QQuickIt
switch (change) {
case QQuickItem::ItemChildAddedChange: {
q->itemChange(change, data);
+ if (!subtreeTransformChangedEnabled)
+ subtreeTransformChangedEnabled = true;
if (!changeListeners.isEmpty()) {
const auto listeners = changeListeners; // NOTE: intentional copy (QTBUG-54732)
for (const QQuickItemPrivate::ChangeListener &change : listeners) {
@@ -6803,6 +6892,17 @@ void QQuickItem::setFlag(Flag flag, bool enabled)
setFlags((Flags)(d->flags | (quint32)flag));
else
setFlags((Flags)(d->flags & ~(quint32)flag));
+
+ if (enabled && flag == ItemObservesViewport) {
+ QQuickItem *par = parentItem();
+ while (par) {
+ auto parPriv = QQuickItemPrivate::get(par);
+ if (!parPriv->subtreeTransformChangedEnabled)
+ qCDebug(lcVP) << "turned on transformChanged notification for subtree of" << par;
+ parPriv->subtreeTransformChangedEnabled = true;
+ par = par->parentItem();
+ }
+ }
}
/*!
@@ -8710,6 +8810,10 @@ QDebug operator<<(QDebug debug,
QtDebugUtils::formatQRect(debug, rect);
if (const qreal z = item->z())
debug << ", z=" << z;
+ if (item->flags().testFlag(QQuickItem::ItemIsViewport))
+ debug << " \U0001f5bc"; // frame with picture
+ if (item->flags().testFlag(QQuickItem::ItemObservesViewport))
+ debug << " \u23ff"; // observer eye
debug << ')';
return debug;
}
@@ -9467,7 +9571,7 @@ void QQuickItemLayer::updateGeometry()
{
QQuickItem *l = m_effect ? (QQuickItem *) m_effect : (QQuickItem *) m_effectSource;
Q_ASSERT(l);
- QRectF bounds = m_item->clipRect();
+ QRectF bounds = m_item->boundingRect();
l->setSize(bounds.size());
l->setPosition(bounds.topLeft() + m_item->position());
}
diff --git a/src/quick/items/qquickitem.h b/src/quick/items/qquickitem.h
index 9bb1cd113a..b9f57f6c63 100644
--- a/src/quick/items/qquickitem.h
+++ b/src/quick/items/qquickitem.h
@@ -166,7 +166,9 @@ public:
#endif
ItemIsFocusScope = 0x04,
ItemHasContents = 0x08,
- ItemAcceptsDrops = 0x10
+ ItemAcceptsDrops = 0x10,
+ ItemIsViewport = 0x20,
+ ItemObservesViewport = 0x40,
// Remember to increment the size of QQuickItemPrivate::flags
};
Q_DECLARE_FLAGS(Flags, Flag)
@@ -292,6 +294,7 @@ public:
virtual QRectF boundingRect() const;
virtual QRectF clipRect() const;
+ QQuickItem *viewportItem() const;
bool hasActiveFocus() const;
bool hasFocus() const;
diff --git a/src/quick/items/qquickitem_p.h b/src/quick/items/qquickitem_p.h
index 0d35cfd48c..51ef850d5f 100644
--- a/src/quick/items/qquickitem_p.h
+++ b/src/quick/items/qquickitem_p.h
@@ -441,7 +441,7 @@ public:
inline QQuickItem::TransformOrigin origin() const;
// Bit 0
- quint32 flags:5;
+ quint32 flags:7;
bool widthValidFlag:1;
bool heightValidFlag:1;
bool componentComplete:1;
@@ -451,9 +451,9 @@ public:
bool smooth:1;
bool antialiasing:1;
bool focus:1;
+ // Bit 16
bool activeFocus:1;
bool notifiedFocus:1;
- // Bit 16
bool notifiedActiveFocus:1;
bool filtersChildMouseEvents:1;
bool explicitVisible:1;
@@ -468,9 +468,9 @@ public:
bool inheritMirrorFromItem:1;
bool isAccessible:1;
bool culled:1;
+ // Bit 32
bool hasCursor:1;
bool subtreeCursorEnabled:1;
- // Bit 32
bool subtreeHoverEnabled:1;
bool activeFocusOnTab:1;
bool implicitAntialiasing:1;
@@ -486,6 +486,9 @@ public:
bool hasCursorHandler:1;
// set true when this item does not expect events via a subscene delivery agent; false otherwise
bool maybeHasSubsceneDeliveryAgent:1;
+ // set true if this item or any child wants QQuickItemPrivate::transformChanged() to visit all children
+ // (e.g. when parent has ItemIsViewport and child has ItemObservesViewport)
+ bool subtreeTransformChangedEnabled:1;
enum DirtyType {
TransformOrigin = 0x00000001,
@@ -617,7 +620,7 @@ public:
}
QPointF computeTransformOrigin() const;
- virtual void transformChanged(QQuickItem *transformedItem);
+ virtual bool transformChanged(QQuickItem *transformedItem);
QPointF adjustedPosForTransform(const QPointF &centroid,
const QPointF &startPos, const QVector2D &activeTranslatation,
diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp
index ee516c3c6e..f1da7ad1e6 100644
--- a/src/quick/items/qquicktext.cpp
+++ b/src/quick/items/qquicktext.cpp
@@ -130,6 +130,7 @@ void QQuickTextPrivate::init()
Q_Q(QQuickText);
q->setAcceptedMouseButtons(Qt::LeftButton);
q->setFlag(QQuickItem::ItemHasContents);
+ q->setFlag(QQuickItem::ItemObservesViewport); // default until size is known
}
QQuickTextPrivate::~QQuickTextPrivate()
@@ -723,6 +724,7 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const baseline)
}
if (lineCount) {
lineCount = 0;
+ q->setFlag(QQuickItem::ItemObservesViewport, false);
emit q->lineCountChanged();
}
@@ -1751,6 +1753,7 @@ void QQuickText::setText(const QString &n)
qDeleteAll(d->extra->imgTags);
d->extra->imgTags.clear();
}
+ setFlag(QQuickItem::ItemObservesViewport, n.size() > QQuickTextPrivate::largeTextSizeThreshold);
d->updateLayout();
setAcceptHoverEvents(d->richText || d->styledText);
emit textChanged(d->text);
@@ -2346,7 +2349,13 @@ void QQuickText::resetBaseUrl()
setBaseUrl(QUrl());
}
-/*! \internal */
+/*!
+ Returns the extents of the text after layout.
+ If the \l style() is not \c Text.Normal, a margin is added to ensure
+ that the rendering effect will fit within this rectangle.
+
+ \sa contentWidth(), contentHeight(), clilpRect()
+*/
QRectF QQuickText::boundingRect() const
{
Q_D(const QQuickText);
@@ -2362,6 +2371,17 @@ QRectF QQuickText::boundingRect() const
return rect;
}
+/*!
+ Returns a rectangular area slightly larger than what is currently visible
+ in \l viewportItem(); otherwise, the rectangle \c (0, 0, width, height).
+ The text will be clipped to fit if \l clip is \c true.
+
+ \note If the \l style is not \c Text.Normal, the clip rectangle is adjusted
+ to be slightly larger, to limit clipping of the outline effect at the edges.
+ But it still looks better to set \l clip to \c false in that case.
+
+ \sa contentWidth(), contentHeight(), boundingRect()
+*/
QRectF QQuickText::clipRect() const
{
Q_D(const QQuickText);
@@ -2979,16 +2999,15 @@ void QQuickText::hoverLeaveEvent(QHoverEvent *event)
d->processHoverEvent(event);
}
-void QQuickTextPrivate::transformChanged(QQuickItem *transformedItem)
+bool QQuickTextPrivate::transformChanged(QQuickItem *transformedItem)
{
- QQuickItemPrivate::transformChanged(transformedItem);
-
// If there's a lot of text, we may need QQuickText::updatePaintNode() to call
// QQuickTextNode::addTextLayout() again to populate a different range of lines
- if (text.size() > largeTextSizeThreshold) {
+ if (flags & QQuickItem::ItemObservesViewport) {
updateType = UpdatePaintNode;
dirty(QQuickItemPrivate::Content);
}
+ return QQuickImplicitSizeItemPrivate::transformChanged(transformedItem);
}
/*!
diff --git a/src/quick/items/qquicktext_p_p.h b/src/quick/items/qquicktext_p_p.h
index c34eca7824..fdd4bb7817 100644
--- a/src/quick/items/qquicktext_p_p.h
+++ b/src/quick/items/qquicktext_p_p.h
@@ -89,7 +89,7 @@ public:
void clearFormats();
void processHoverEvent(QHoverEvent *event);
- void transformChanged(QQuickItem *transformedItem) override;
+ bool transformChanged(QQuickItem *transformedItem) override;
QRectF layedOutTextRect;
QSizeF advance;
diff --git a/src/quick/items/qquicktextnode.cpp b/src/quick/items/qquicktextnode.cpp
index 87cae92565..387a97d451 100644
--- a/src/quick/items/qquicktextnode.cpp
+++ b/src/quick/items/qquicktextnode.cpp
@@ -75,7 +75,7 @@ namespace {
}
-Q_DECLARE_LOGGING_CATEGORY(lcSgText)
+Q_DECLARE_LOGGING_CATEGORY(lcVP)
/*!
Creates an empty QQuickTextNode
@@ -264,12 +264,11 @@ void QQuickTextNode::addTextLayout(const QPointF &position, QTextLayout *textLay
QVarLengthArray<QTextLayout::FormatRange> colorChanges;
engine.mergeFormats(textLayout, &colorChanges);
- // If there's a lot of text, transform the window's bounds into the Text item's space
- // and then insert only the range of lines that can possibly be visible within that viewport.
+ // If there's a lot of text, insert only the range of lines that can possibly be visible within the viewport.
QRectF viewport;
- if (textLayout->text().size() > QQuickTextPrivate::largeTextSizeThreshold && m_ownerElement->window()) {
- viewport = m_ownerElement->mapRectFromScene(m_ownerElement->window()->contentItem()->boundingRect());
- qCDebug(lcSgText) << "text viewport" << viewport;
+ if (m_ownerElement->flags().testFlag(QQuickItem::ItemObservesViewport)) {
+ viewport = m_ownerElement->clipRect();
+ qCDebug(lcVP) << "text viewport" << viewport;
}
lineCount = lineCount >= 0
? qMin(lineStart + lineCount, textLayout->lineCount())
@@ -291,14 +290,17 @@ void QQuickTextNode::addTextLayout(const QPointF &position, QTextLayout *textLay
}
#endif
if (viewport.isNull() || (line.y() + line.height() > viewport.top() && line.y() < viewport.bottom())) {
- if (!inViewport && !viewport.isNull())
- qCDebug(lcSgText) << "first line in viewport" << i << "@" << line.y();
+ if (!inViewport && !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(!viewport.isNull());
- qCDebug(lcSgText) << "first omitted line past bottom of viewport" << i << "@" << line.y();
+ 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
}
}
diff --git a/src/quick/items/qquicktextnode_p.h b/src/quick/items/qquicktextnode_p.h
index c5b8d74099..8cec136a79 100644
--- a/src/quick/items/qquicktextnode_p.h
+++ b/src/quick/items/qquicktextnode_p.h
@@ -111,12 +111,16 @@ public:
void setRenderTypeQuality(int renderTypeQuality) { m_renderTypeQuality = renderTypeQuality; }
int renderTypeQuality() const { return m_renderTypeQuality; }
+ QPair<int, int> renderedLineRange() const { return { m_firstLineInViewport, m_firstLinePastViewport }; }
+
private:
QSGInternalRectangleNode *m_cursorNode;
QList<QSGTexture *> m_textures;
QQuickItem *m_ownerElement;
bool m_useNativeRenderer;
int m_renderTypeQuality;
+ int m_firstLineInViewport = -1;
+ int m_firstLinePastViewport = -1;
friend class QQuickTextEdit;
friend class QQuickTextEditPrivate;
diff --git a/src/quick/items/qquickview.cpp b/src/quick/items/qquickview.cpp
index b3a5270e9b..78efd8277b 100644
--- a/src/quick/items/qquickview.cpp
+++ b/src/quick/items/qquickview.cpp
@@ -528,6 +528,7 @@ bool QQuickViewPrivate::setRootObject(QObject *obj)
if (QQuickItem *sgItem = qobject_cast<QQuickItem *>(obj)) {
root = sgItem;
+ root->setFlag(QQuickItem::ItemIsViewport);
sgItem->setParentItem(q->QQuickWindow::contentItem());
QQml_setParent_noEvent(sgItem, q->QQuickWindow::contentItem());
initialSize = rootObjectSize();
diff --git a/src/quick/items/qquickwindow.cpp b/src/quick/items/qquickwindow.cpp
index b6d665c357..5f95534394 100644
--- a/src/quick/items/qquickwindow.cpp
+++ b/src/quick/items/qquickwindow.cpp
@@ -201,6 +201,9 @@ have a scope focused item), and the other items will have their focus cleared.
QQuickRootItem::QQuickRootItem()
{
+ // child items with ItemObservesViewport can treat the window's content item
+ // as the ultimate viewport: avoid populating SG nodes that fall outside
+ setFlag(ItemIsViewport);
}
/*! \reimp */
@@ -1967,7 +1970,7 @@ void QQuickWindowPrivate::updateDirtyNode(QQuickItem *item)
if (item->clip()) {
Q_ASSERT(itemPriv->clipNode() == nullptr);
- QQuickDefaultClipNode *clip = new QQuickDefaultClipNode(item->clipRect());
+ QQuickDefaultClipNode *clip = new QQuickDefaultClipNode(item->boundingRect());
itemPriv->extra.value().clipNode = clip;
clip->update();
@@ -2091,7 +2094,7 @@ void QQuickWindowPrivate::updateDirtyNode(QQuickItem *item)
}
if ((dirty & QQuickItemPrivate::Size) && itemPriv->clipNode()) {
- itemPriv->clipNode()->setRect(item->clipRect());
+ itemPriv->clipNode()->setRect(item->boundingRect());
itemPriv->clipNode()->update();
}
diff --git a/tests/auto/quick/qquickitem2/data/viewports.qml b/tests/auto/quick/qquickitem2/data/viewports.qml
new file mode 100644
index 0000000000..18258676b4
--- /dev/null
+++ b/tests/auto/quick/qquickitem2/data/viewports.qml
@@ -0,0 +1,54 @@
+import QtQuick
+import Test 1.0
+
+Item {
+ width: 300
+ height: 300
+
+ Rectangle {
+ objectName: "outerViewport"
+
+ x: 40
+ y: 40
+ width: 220
+ height: 220
+ border.color: "green"
+ color: "transparent"
+
+ Rectangle {
+ objectName: "innerViewport"
+ width: parent.width
+ height: parent.height
+ x: 20
+ y: 20
+ border.color: "cyan"
+ color: "transparent"
+
+ Rectangle {
+ objectName: "innerRect"
+ color: "wheat"
+ opacity: 0.5
+ x: -55
+ y: -55
+ width: 290
+ height: 290
+ ViewportTestItem {
+ anchors.fill: parent
+ Rectangle {
+ id: viewportFillingRect
+ color: "transparent"
+ border.color: "red"
+ border.width: 3
+ x: parent.viewport.x
+ y: parent.viewport.y
+ width: parent.viewport.width
+ height: parent.viewport.height
+ }
+ }
+ }
+ }
+ }
+ Text {
+ text: "viewport " + viewportFillingRect.width + " x " + viewportFillingRect.height
+ }
+}
diff --git a/tests/auto/quick/qquickitem2/tst_qquickitem.cpp b/tests/auto/quick/qquickitem2/tst_qquickitem.cpp
index 72404bd161..fd294e5e71 100644
--- a/tests/auto/quick/qquickitem2/tst_qquickitem.cpp
+++ b/tests/auto/quick/qquickitem2/tst_qquickitem.cpp
@@ -143,6 +143,9 @@ private slots:
void undefinedIsInvalidForWidthAndHeight();
+ void viewport_data();
+ void viewport();
+
private:
QQmlEngine engine;
bool qt_tab_all_widgets() {
@@ -320,6 +323,22 @@ private:
QML_DECLARE_TYPE(HollowTestItem);
+class ViewportTestItem : public QQuickItem
+{
+ Q_OBJECT
+ Q_PROPERTY(QRectF viewport READ viewport NOTIFY viewportChanged)
+
+public:
+ ViewportTestItem(QQuickItem *parent = nullptr) : QQuickItem(parent) { }
+
+ QRectF viewport() const { return clipRect(); }
+
+signals:
+ void viewportChanged();
+};
+
+QML_DECLARE_TYPE(ViewportTestItem);
+
class TabFenceItem : public QQuickItem
{
Q_OBJECT
@@ -361,6 +380,7 @@ void tst_QQuickItem::initTestCase()
QQmlDataTest::initTestCase();
qmlRegisterType<KeyTestItem>("Test",1,0,"KeyTestItem");
qmlRegisterType<HollowTestItem>("Test", 1, 0, "HollowTestItem");
+ qmlRegisterType<ViewportTestItem>("Test", 1, 0, "ViewportTestItem");
qmlRegisterType<TabFenceItem>("Test", 1, 0, "TabFence");
qmlRegisterType<TabFenceItem2>("Test", 1, 0, "TabFence2");
}
@@ -3834,6 +3854,151 @@ void tst_QQuickItem::undefinedIsInvalidForWidthAndHeight()
QVERIFY(!priv->heightValid());
}
+void tst_QQuickItem::viewport_data()
+{
+ QTest::addColumn<bool>("contentObservesViewport");
+
+ QTest::addColumn<bool>("innerClip");
+ QTest::addColumn<bool>("innerViewport");
+ QTest::addColumn<bool>("innerObservesViewport");
+
+ QTest::addColumn<bool>("outerClip");
+ QTest::addColumn<bool>("outerViewport");
+ QTest::addColumn<bool>("outerObservesViewport");
+
+ QTest::addColumn<QPoint>("innerViewportOffset");
+ QTest::addColumn<QPoint>("outerViewportOffset");
+
+ QTest::addColumn<QRect>("expectedViewportTestRect");
+ QTest::addColumn<QRect>("expectedContentClipRect");
+
+ QTest::newRow("default") << false
+ << false << false << false
+ << false << false << false
+ << QPoint() << QPoint() << QRect(0, 0, 290, 290) << QRect(0, 0, 290, 290);
+ QTest::newRow("inner and outer: vp, clipping and observing") << true
+ << true << true << true
+ << true << true << true
+ << QPoint() << QPoint() << QRect(55, 55, 200, 200) << QRect(0, 0, 290, 290);
+ QTest::newRow("inner and outer: vp, clipping and observing; content not observing") << false
+ << true << true << true
+ << true << true << true
+ << QPoint() << QPoint() << QRect(0, 0, 290, 290) << QRect(0, 0, 290, 290);
+ QTest::newRow("inner and outer: vp and observing") << true
+ << false << true << true
+ << false << true << true
+ << QPoint() << QPoint() << QRect(55, 55, 200, 200) << QRect(0, 0, 290, 290);
+ QTest::newRow("inner and outer: vp and observing, inner pos offset") << true
+ << false << true << true
+ << false << true << true
+ << QPoint(120, 120) << QPoint() << QRect(55, 55, 80, 80) << QRect(0, 0, 175, 175);
+ QTest::newRow("inner and outer: vp and observing, inner neg offset") << true
+ << false << true << true
+ << false << true << true
+ << QPoint(-70, -50) << QPoint() << QRect(105, 85, 170, 190) << QRect(65, 45, 225, 245);
+ QTest::newRow("inner and outer: vp and observing, outer pos offset") << true
+ << false << true << true
+ << false << true << true
+ << QPoint() << QPoint(220, 220) << QRect(55, 55, 20, 20) << QRect(0, 0, 75, 75);
+ QTest::newRow("inner and outer: vp and observing, outer neg offset") << true
+ << false << true << true
+ << false << true << true
+ << QPoint() << QPoint(-70, -50) << QRect(65, 55, 190, 200) << QRect(65, 45, 225, 245);
+ QTest::newRow("inner and outer: vp and observing, pos and neg offset") << true
+ << false << true << true
+ << false << true << true
+ << QPoint(150, 150) << QPoint(-170, -150) << QRect(55, 55, 50, 50) << QRect(15, 0, 275, 290);
+ QTest::newRow("inner and outer: vp and observing, neg and pos offset") << true
+ << false << true << true
+ << false << true << true
+ << QPoint(-180, -210) << QPoint(100, 115) << QRect(215, 245, 60, 30) << QRect(75, 90, 215, 200);
+ QTest::newRow("inner and outer: vp not observing") << true
+ << false << true << false
+ << false << true << false
+ << QPoint() << QPoint() << QRect(55, 55, 220, 220) << QRect(0, 0, 290, 290);
+ QTest::newRow("inner and outer: vp not observing, inner pos offset") << true
+ << false << true << false
+ << false << true << false
+ << QPoint(120, 120) << QPoint() << QRect(55, 55, 120, 120) << QRect(0, 0, 175, 175);
+ QTest::newRow("inner and outer: vp not observing, inner neg offset") << true
+ << false << true << false
+ << false << true << false
+ << QPoint(-70, -50) << QPoint() << QRect(65, 55, 210, 220) << QRect(65, 45, 225, 245);
+ QTest::newRow("inner and outer: vp not observing, outer pos offset") << true
+ << false << true << false
+ << false << true << false
+ << QPoint() << QPoint(220, 220) << QRect(55, 55, 20, 20) << QRect(0, 0, 75, 75);
+ QTest::newRow("inner and outer: vp not observing, outer neg offset") << true
+ << false << true << false
+ << false << true << false
+ << QPoint() << QPoint(-70, -50) << QRect(65, 55, 210, 220) << QRect(65, 45, 225, 245);
+ QTest::newRow("inner clipping and observing") << true
+ << true << true << true
+ << false << false << false
+ << QPoint() << QPoint() << QRect(55, 55, 220, 220) << QRect(0, 0, 290, 290);
+ QTest::newRow("inner clipping and observing only outer") << true
+ << true << true << true
+ << false << true << false
+ << QPoint() << QPoint() << QRect(55, 55, 200, 200) << QRect(0, 0, 290, 290);
+}
+
+void tst_QQuickItem::viewport()
+{
+ QFETCH(bool, contentObservesViewport);
+ QFETCH(bool, innerClip);
+ QFETCH(bool, innerViewport);
+ QFETCH(bool, innerObservesViewport);
+ QFETCH(bool, outerClip);
+ QFETCH(bool, outerViewport);
+ QFETCH(bool, outerObservesViewport);
+ QFETCH(QPoint, innerViewportOffset);
+ QFETCH(QPoint, outerViewportOffset);
+ QFETCH(QRect, expectedViewportTestRect);
+ QFETCH(QRect, expectedContentClipRect);
+
+ QQuickView window;
+ QVERIFY(QQuickTest::showView(window, testFileUrl("viewports.qml")));
+
+ QQuickItem *root = qobject_cast<QQuickItem *>(window.rootObject());
+ QQuickItem *outer = root->findChild<QQuickItem *>("outerViewport");
+ QVERIFY(outer);
+ QQuickItem *inner = root->findChild<QQuickItem *>("innerViewport");
+ QVERIFY(inner);
+ QQuickItem *contentItem = root->findChild<QQuickItem *>("innerRect");
+ QVERIFY(contentItem);
+ ViewportTestItem *viewportTestItem = root->findChild<ViewportTestItem *>();
+ QVERIFY(viewportTestItem);
+
+ inner->setPosition(inner->position() + innerViewportOffset);
+ outer->setPosition(outer->position() + outerViewportOffset);
+ outer->setClip(outerClip);
+ QCOMPARE(outer->flags().testFlag(QQuickItem::ItemIsViewport), outerClip);
+ outer->setFlag(QQuickItem::ItemIsViewport, outerViewport);
+ outer->setFlag(QQuickItem::ItemObservesViewport, outerObservesViewport);
+ inner->setClip(innerClip);
+ QCOMPARE(inner->flags().testFlag(QQuickItem::ItemIsViewport), innerClip);
+ inner->setFlag(QQuickItem::ItemIsViewport, innerViewport);
+ inner->setFlag(QQuickItem::ItemObservesViewport, innerObservesViewport);
+ viewportTestItem->setFlag(QQuickItem::ItemObservesViewport, contentObservesViewport);
+ emit viewportTestItem->viewportChanged();
+
+ if (lcTests().isDebugEnabled())
+ QTest::qWait(1000);
+ if (contentObservesViewport) {
+ if (innerViewport)
+ QCOMPARE(viewportTestItem->viewportItem(), inner);
+ else if (outerViewport)
+ QCOMPARE(viewportTestItem->viewportItem(), outer);
+ else
+ QCOMPARE(viewportTestItem->viewportItem(), root->parentItem()); // QQuickRootItem
+ } else {
+ QCOMPARE(viewportTestItem->viewportItem(), root->parentItem()); // QQuickRootItem
+ }
+
+ QCOMPARE(contentItem->clipRect().toRect(), expectedContentClipRect);
+ QCOMPARE(viewportTestItem->clipRect().toRect(), expectedViewportTestRect);
+}
+
QTEST_MAIN(tst_QQuickItem)
#include "tst_qquickitem.moc"
diff --git a/tests/auto/quick/qquicktext/data/viewport.qml b/tests/auto/quick/qquicktext/data/viewport.qml
new file mode 100644
index 0000000000..e5c8805670
--- /dev/null
+++ b/tests/auto/quick/qquicktext/data/viewport.qml
@@ -0,0 +1,19 @@
+import QtQuick
+
+Item {
+ width: 480; height: 480
+
+ Rectangle {
+ id: viewport
+ anchors.fill: parent
+ anchors.margins: 100
+ border.color: "red"
+
+ Text {
+ Component.onCompleted: {
+ for (let i = 0; i < 20; ++i)
+ text += "Line " + i + "\n";
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicktext/tst_qquicktext.cpp b/tests/auto/quick/qquicktext/tst_qquicktext.cpp
index 512cef5fc6..282cb64ce6 100644
--- a/tests/auto/quick/qquicktext/tst_qquicktext.cpp
+++ b/tests/auto/quick/qquicktext/tst_qquicktext.cpp
@@ -35,6 +35,7 @@
#include <QtQuick/private/qquickmousearea_p.h>
#include <QtQuickTest/QtQuickTest>
#include <private/qquicktext_p_p.h>
+#include <private/qquicktextnode_p.h>
#include <private/qquicktextdocument_p.h>
#include <private/qquickvaluetypes_p.h>
#include <QFontMetrics>
@@ -56,6 +57,8 @@ QT_BEGIN_NAMESPACE
extern void qt_setQtEnableTestFont(bool value);
QT_END_NAMESPACE
+Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests")
+
class tst_qquicktext : public QQmlDataTest
{
Q_OBJECT
@@ -119,6 +122,7 @@ private slots:
void boundingRect_data();
void boundingRect();
void clipRect();
+ void largeTextObservesViewport();
void lineLaidOut();
void lineLaidOutRelayout();
void lineLaidOutHAlign();
@@ -2940,6 +2944,65 @@ void tst_qquicktext::clipRect()
QCOMPARE(text->clipRect().height(), text->height() + 2);
}
+void tst_qquicktext::largeTextObservesViewport()
+{
+ QQuickView window;
+ QByteArray errorMessage;
+ QVERIFY2(QQuickTest::initView(window, testFileUrl("viewport.qml"), true, &errorMessage), errorMessage.constData());
+ window.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&window));
+ QQuickText *textItem = window.rootObject()->findChild<QQuickText*>();
+ QVERIFY(textItem);
+ QQuickItem *viewportItem = textItem->parentItem();
+ QQuickTextPrivate *textPriv = QQuickTextPrivate::get(textItem);
+ QQuickTextNode *node = static_cast<QQuickTextNode *>(textPriv->paintNode);
+ QFontMetricsF fm(textItem->font());
+ const qreal expectedTextHeight = window.height() - viewportItem->y();
+ const qreal lineSpacing = qCeil(fm.height());
+ int expectedLastLine = int(expectedTextHeight / lineSpacing);
+
+ QStringList lines;
+ // "line 100" is 8 characters; many lines are longer, some are shorter
+ // so we populate 1250 lines, 11389 characters
+ const int lineCount = QQuickTextPrivate::largeTextSizeThreshold / 8;
+ lines.reserve(lineCount);
+ for (int i = 0; i < lineCount; ++i)
+ lines << QLatin1String("line ") + QString::number(i);
+ textItem->setText(lines.join('\n'));
+ Q_ASSERT(textItem->text().size() > QQuickTextPrivate::largeTextSizeThreshold);
+ qCDebug(lcTests) << "text size" << textItem->text().size() << "lines" << lineCount;
+
+ // by default, the root item acts as the viewport:
+ // QQuickTextNode doesn't populate lines of text beyond the bottom of the window
+ QVERIFY(textItem->flags().testFlag(QQuickItem::ItemObservesViewport));
+ QVERIFY(window.rootObject()->flags().testFlag(QQuickItem::ItemIsViewport));
+ QTRY_COMPARE(node->renderedLineRange().first, 0);
+ auto renderedLineRange = node->renderedLineRange();
+ qCDebug(lcTests) << "lines rendered" << renderedLineRange.second
+ << "expected last line" << expectedLastLine
+ << "based on available height" << expectedTextHeight
+ << "and line height" << lineSpacing;
+ QVERIFY(qAbs(renderedLineRange.second - (expectedLastLine + 1)) < 2);
+
+ // make the rectangle into a viewport item, and move the text upwards to force re-rendering:
+ // QQuickTextNode doesn't populate lines of text beyond the bottom of the viewport rectangle
+ viewportItem->setFlag(QQuickItem::ItemIsViewport);
+ int expectedFirstLine = 10;
+ expectedLastLine = expectedFirstLine + int(viewportItem->height() / lineSpacing);
+ textItem->setY(lineSpacing * -10);
+ QTRY_VERIFY(node->renderedLineRange().second != renderedLineRange.second); // wait for re-rendering
+ renderedLineRange = node->renderedLineRange();
+ // now the render thread will call QQuickTextPrivate::transformChanged()
+ qCDebug(lcTests) << "first line rendered" << renderedLineRange.first
+ << "expected" << expectedFirstLine
+ << "first line past viewport" << renderedLineRange.second
+ << "expected last line" << expectedLastLine
+ << "based on available height" << viewportItem->height()
+ << "and line height" << lineSpacing;
+ QCOMPARE(renderedLineRange.first, expectedFirstLine);
+ QVERIFY(qAbs(renderedLineRange.second - (expectedLastLine + 1)) < 2);
+}
+
void tst_qquicktext::lineLaidOut()
{
QScopedPointer<QQuickView> window(createView(testFile("lineLayout.qml")));