#include "snaplayout.hpp" #include #include #include #include #include #include Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts") namespace ZShell::components { namespace { qint64 originResidual(qreal logical, qreal dpr) { const qreal device = logical * dpr; return std::llround((device - std::floor(device)) * 1024.0) % 1024; } // Snap one size hint to the device grid. `mode` is >0 to round up, <0 to round // down, 0 to round to nearest. Infinities and NaN are left alone: they mean // "unbounded". qreal snapSizeHint(qreal value, qreal dpr, int mode) { if (qIsNaN(value) || std::isinf(value)) return value; const double exact = value * dpr; const double snapped = mode > 0 ? std::ceil(exact - 1e-9) : mode < 0 ? std::floor(exact + 1e-9) : static_cast(std::llround(exact)); return snapped / dpr; } QPointF absoluteOrigin(const QQuickItem* item) { qreal x = 0.0; qreal y = 0.0; for (const QQuickItem* it = item; it; it = it->parentItem()) { x += it->x(); y += it->y(); } return QPointF(x, y); } } // namespace qreal SnapLayoutStyleInfo::spacing(Qt::Orientation /*orientation*/) const { #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_QNX) qreal spacing = 8.0; #else qreal spacing = 5.0; #endif #if !defined(Q_OS_MACOS) && !defined(Q_OS_HARMONY) spacing = qRound(spacing * (qreal(qt_defaultDpiX()) / 96.0)); #endif return spacing; } qreal SnapLayoutStyleInfo::windowMargin(Qt::Orientation /*orientation*/) const { return 0; } bool SnapLayoutStyleInfo::hasChangedCore() const { return false; } SnapGridLayoutItem::SnapGridLayoutItem( QQuickItem* item, int row, int column, int rowSpan, int columnSpan, Qt::Alignment alignment) : QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) {} void SnapGridLayoutItem::setGeometry(const QRectF& rect) { QQuickLayoutAttached* info = attachedLayoutObject(m_item, false); engineRect = info ? rect.marginsRemoved(info->effectiveQMargins()) : rect; placed = true; } void SnapLayoutPrivate::mirrorChange() { Q_Q(SnapLayout); q->invalidate(); } SnapLayout::SnapLayout(QQuickItem* parent) : QQuickLayout(*new SnapLayoutPrivate, parent) { d_func()->styleInfo = new SnapLayoutStyleInfo; } SnapLayout::~SnapLayout() { Q_D(SnapLayout); clearAncestorListeners(); deactivateRecur(); delete d->styleInfo; } void SnapLayout::componentComplete() { QQuickLayout::componentComplete(); applySpacing(); invalidate(); ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints); if (qobject_cast(parentItem())) return; rearrange(QSizeF(width(), height())); } void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) { QQuickLayout::itemChange(change, value); if (change == ItemChildRemovedChange && value.item) d_func()->childSizes.remove(value.item); if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) { applySpacing(); invalidate(); } } void SnapLayout::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry) { Q_D(SnapLayout); QQuickLayout::geometryChange(newGeometry, oldGeometry); if (!isReady()) return; const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0; if (dpr <= 0.0) return; // Any change of our own geometry can move us: our size changed and an anchor // on a bottom or right edge moved us with it, or an ancestor resized and the // same anchors moved us. Children were placed relative to the origin the // arrangement saw: their local positions carry the compensation for that // origin's fraction of a device pixel, so after a move they land on a // fraction - which is why a centered child is blurred by a different amount // every time the panel opens. Re-arrange whenever the origin's residual // changes; it is remembered on every rearrange, so this fires only when a // re-arrange is really needed, and it converges as soon as the animation // stops moving the layout. const QPointF origin = absoluteOrigin(this); if (originResidual(origin.x(), dpr) == d->originResidualX && originResidual(origin.y(), dpr) == d->originResidualY) return; rearrangeForOriginMove(); } // Children are placed against the origin the arrangement saw, so a move that // changes the origin's fraction of a device pixel needs a new arrangement. // polish() defers that to the next frame, and a polish request does nothing // while one is already pending - if no frame follows, or the pending polish is // never delivered, the children stay placed against the old origin forever. // Rearrange immediately instead; polish() is only the fallback for the cases // where rearranging now is not possible. void SnapLayout::rearrangeForOriginMove() { Q_D(SnapLayout); const QSizeF size(width(), height()); if (d->rearranging || size.width() <= 0.0 || size.height() <= 0.0) { polish(); return; } rearrange(size); } QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const { Q_D(const SnapLayout); const QSizeF engineHint = d->engine.sizeHint(whichSizeHint, QSizeF(), d->styleInfo); const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; if (dpr <= 0.0) return engineHint; const bool horiz = d->orientation == Qt::Horizontal; // Qt builds implicit sizes out of whole logical pixels: // QQuickLayout::effectiveSizeHints_helper takes `qCeil(item->implicitHeight())` // for every child, so the sum can be an odd number of logical pixels, which is // a half device pixel at a fractional ratio - 37 logical pixels at 1.5x is 55.5 // device pixels. A layout that sizes itself from its implicit size then has an // edge on a fraction of a device pixel, and anchored to a bottom edge its top // edge - and every child measured down from that top - lands off the grid. // // The preferred size is therefore summed here in device pixels: each child's // own hint rounded up to the device grid, plus its Layout margins and the // device-normalized spacing. That is exactly what the arrangement writes, so // the layout is neither larger than its children (dead space, and a Flickable // that reports more content height than the content occupies) nor smaller // (the last child cut off). qint64 mainExtentDev = 0; qint64 crossDev = 0; qint64 maxMainDev = 0; int count = 0; for (int i = 0; i < d->engine.itemCount(); ++i) { const SnapGridLayoutItem* gridItem = static_cast(d->engine.itemAt(i)); QQuickItem* child = gridItem->layoutItem(); if (!child || shouldIgnoreItem(child)) continue; QQuickLayoutAttached* info = attachedLayoutObject(child, false); const QMarginsF margins = info ? info->effectiveQMargins() : QMarginsF(0, 0, 0, 0); // The engine's per-child hints are authoritative: they follow // Layout.preferredWidth/Height when those are set, otherwise the child's // implicit size, otherwise the first width and height it had, and they // include the child's Layout margins. They are also cached, so a nested // layout contributes its own preferred size even before it is polished. const QSizeF* hints = gridItem->effectiveSizeHints(); const qreal mainMargin = horiz ? margins.left() + margins.right() : margins.top() + margins.bottom(); const qreal crossMargin = horiz ? margins.top() + margins.bottom() : margins.left() + margins.right(); qreal mainHint = (horiz ? hints[Qt::PreferredSize].width() : hints[Qt::PreferredSize].height()) - mainMargin; qreal crossHint = (horiz ? hints[Qt::PreferredSize].height() : hints[Qt::PreferredSize].width()) - crossMargin; // The engine hint is `qCeil` of the implicit size, so it can sit up to a // whole logical pixel above it - a device pixel per child at 1.5x, which // adds up to dead space in a long column. When the child's own implicit // size is known and the hint is just that ceiling, measure the implicit // size and let the device rounding be the only rounding. const qreal ownMain = horiz ? child->implicitWidth() : child->implicitHeight(); if (ownMain > 0.0 && mainHint <= ownMain + 1.0) mainHint = ownMain; const qreal ownCross = horiz ? child->implicitHeight() : child->implicitWidth(); if (ownCross > 0.0 && crossHint <= ownCross + 1.0) crossHint = ownCross; if (!std::isfinite(mainHint) || mainHint < 0.0) mainHint = 0.0; if (!std::isfinite(crossHint) || crossHint < 0.0) crossHint = 0.0; // Rounding up keeps the hint from ever under-sizing its contents. const qint64 mainDev = qint64(std::ceil(mainHint * dpr - 1e-9)); const qint64 mainMarginDev = qint64(std::ceil(mainMargin * dpr - 1e-9)); const qint64 crossDevForChild = qint64(std::ceil(crossHint * dpr - 1e-9)) + qint64(std::ceil(crossMargin * dpr - 1e-9)); mainExtentDev += mainDev + mainMarginDev; maxMainDev = (std::max)(maxMainDev, mainDev + mainMarginDev); crossDev = (std::max)(crossDev, crossDevForChild); ++count; } qreal space = d->spacingRequest; if (qIsNaN(space)) space = d->styleInfo->spacing(d->orientation); const qint64 gapDev = dpr > 0.0 ? qint64(std::llround(space * dpr)) : qint64(space); if (uniformCellSizes() && count > 0) mainExtentDev = maxMainDev * qint64(count); if (count > 1) mainExtentDev += gapDev * qint64(count - 1); const QSizeF preferred( horiz ? qreal(mainExtentDev) / dpr : qreal(crossDev) / dpr, horiz ? qreal(crossDev) / dpr : qreal(mainExtentDev) / dpr); if (whichSizeHint == Qt::PreferredSize) return preferred; if (whichSizeHint == Qt::MinimumSize) { // Minimums round up, but never past the preferred size, or cells would // grow past what the arrangement writes. const QSizeF minimum( snapSizeHint(engineHint.width(), dpr, 1), snapSizeHint(engineHint.height(), dpr, 1)); return QSizeF( (std::min)(minimum.width(), preferred.width()), (std::min)(minimum.height(), preferred.height())); } // Maximums round down, but never below the preferred size: a layout is always // allowed to be as large as its contents need. const QSizeF maximum( snapSizeHint(engineHint.width(), dpr, -1), snapSizeHint(engineHint.height(), dpr, -1)); return QSizeF( (std::max)(maximum.width(), preferred.width()), (std::max)(maximum.height(), preferred.height())); } Qt::Orientation SnapLayout::orientation() const { return d_func()->orientation; } void SnapLayout::setOrientation(Qt::Orientation orientation) { Q_D(SnapLayout); if (d->orientation == orientation) return; d->orientation = orientation; invalidate(); emit orientationChanged(); } Qt::LayoutDirection SnapLayout::layoutDirection() const { return d_func()->layoutDirection; } void SnapLayout::setLayoutDirection(Qt::LayoutDirection direction) { Q_D(SnapLayout); if (d->layoutDirection == direction) return; d->layoutDirection = direction; invalidate(); emit layoutDirectionChanged(); } Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const { return !isMirrored() == (layoutDirection() == Qt::LeftToRight) ? Qt::LeftToRight : Qt::RightToLeft; } qreal SnapLayout::spacing() const { Q_D(const SnapLayout); if (!std::isnan(d->spacingRequest)) return d->spacingRequest; return d->styleInfo->spacing(d->orientation); } void SnapLayout::applySpacing() { Q_D(SnapLayout); if (!isReady()) return; const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; qreal space = d->spacingRequest; if (std::isnan(space)) space = d->styleInfo->spacing(d->orientation); if (dpr > 0.0) space = qreal(std::llround(space * dpr)) / dpr; d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical); } void SnapLayout::setSpacing(qreal space) { Q_D(SnapLayout); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" if (std::isnan(space) || d->spacingRequest == space) return; #pragma GCC diagnostic pop d->spacingRequest = space; applySpacing(); invalidate(); emit spacingChanged(); } bool SnapLayout::uniformCellSizes() const { Q_D(const SnapLayout); return d->engine.uniformCellWidths(); } void SnapLayout::setUniformCellSizes(bool uniformCellSizes) { Q_D(SnapLayout); if (d->engine.uniformCellHeights() == uniformCellSizes) return; d->engine.setUniformCellWidths(uniformCellSizes); d->engine.setUniformCellHeights(uniformCellSizes); invalidate(); emit uniformCellSizesChanged(); } void SnapLayout::setAlignment(QQuickItem* item, Qt::Alignment align) { Q_D(SnapLayout); if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(item)) { layoutItem->setAlignment(align); d->engine.invalidate(); } maybeSubscribeToBaseLineOffsetChanges(item); } void SnapLayout::setStretchFactor( QQuickItem* item, int stretchFactor, Qt::Orientation orient) { Q_D(SnapLayout); if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(item)) { layoutItem->setStretchFactor(stretchFactor, orient); d->engine.invalidate(); } } void SnapLayout::invalidate(QQuickItem* childItem) { Q_D(SnapLayout); if (!isReady()) return; if (childItem) { if (d->rearranging) { if (!d->invalidateAfterRearrange.contains(childItem)) d->invalidateAfterRearrange << childItem; return; } if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(childItem)) { layoutItem->invalidate(); } } d->engine.invalidate(); QQuickLayout::invalidate(); if (auto* parentLayout = qobject_cast(parentItem())) parentLayout->invalidate(this); } void SnapLayout::updateLayoutItems() { Q_D(SnapLayout); if (!isReady()) return; const auto items = childItems(); for (auto entry = d->childSizes.begin(); entry != d->childSizes.end();) { if (items.contains(entry.key())) ++entry; else entry = d->childSizes.erase(entry); } d->engine.deleteItems(); insertLayoutItems(); } QQuickItem* SnapLayout::itemAt(int index) const { Q_D(const SnapLayout); return static_cast(d->engine.itemAt(index)) ->layoutItem(); } int SnapLayout::itemCount() const { Q_D(const SnapLayout); return d->engine.itemCount(); } void SnapLayout::insertLayoutItems() { Q_D(SnapLayout); const auto items = childItems(); for (QQuickItem* child : items) { Q_ASSERT(child); checkAnchors(child); if (shouldIgnoreItem(child)) continue; QQuickLayoutAttached* info = attachedLayoutObject(child, false); Qt::Alignment alignment; int hStretch = -1; int vStretch = -1; bool fillWidth = false; bool fillHeight = false; if (info) { alignment = info->alignment(); hStretch = info->horizontalStretchFactor(); vStretch = info->verticalStretchFactor(); fillWidth = info->fillWidth(); fillHeight = info->fillHeight(); } const int index = d->engine.rowCount(d->orientation); d->engine.insertRow(index, d->orientation); int gridRow = 0; int gridColumn = index; if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn); QGridLayoutItem* layoutItem = new SnapGridLayoutItem(child, gridRow, gridColumn, 1, 1, alignment); if (hStretch >= 0) { if (!fillWidth) qWarning() << "horizontalStretchFactor requires fillWidth to " "also be set to true"; layoutItem->setStretchFactor(hStretch, Qt::Horizontal); } if (vStretch >= 0) { if (!fillHeight) qWarning() << "verticalStretchFactor requires fillHeight to " "also be set to true"; layoutItem->setStretchFactor(vStretch, Qt::Vertical); } d->engine.insertItem(layoutItem, index); } } void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) { Q_D(SnapLayout); const int index = gridItem->firstRow(d->orientation); d->engine.removeItem(gridItem); d->engine.removeRows(index, 1, d->orientation); } void SnapLayout::itemDestroyed(QQuickItem* item) { if (!isReady()) return; Q_D(SnapLayout); d->childSizes.remove(item); if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) { removeGridItem(gridItem); delete gridItem; invalidate(); } } void SnapLayout::itemVisibilityChanged(QQuickItem* item) { if (!isReady()) return; invalidate(item); } void SnapLayout::itemGeometryChanged( QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) { Q_UNUSED(item) Q_UNUSED(change) Q_UNUSED(oldGeometry) if (!isReady()) return; const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0; if (dpr <= 0.0) return; // An ancestor changing size can move us too - a layout anchored to a bottom // edge slides when its parent grows - so only the origin's fraction of a // device pixel matters, not which kind of change reached us. Q_D(SnapLayout); const QPointF origin = absoluteOrigin(this); if (originResidual(origin.x(), dpr) == d->originResidualX && originResidual(origin.y(), dpr) == d->originResidualY) return; rearrangeForOriginMove(); } void SnapLayout::updateAncestorListeners() { Q_D(SnapLayout); const QFlags mask = QQuickItemPrivate::Geometry; QVector current; for (QQuickItem* it = parentItem(); it; it = it->parentItem()) { if (qobject_cast(it)) break; current.append(it); } for (const QPointer& ancestor : std::as_const(d->ancestorListeners)) { if (ancestor && !current.contains(ancestor.data())) { QQuickItemPrivate::get(ancestor.data()) ->removeItemChangeListener( this, d->ancestorMasks.value(ancestor.data())); d->ancestorMasks.remove(ancestor.data()); } } // Qt appends one registration per addItemChangeListener call, so only the // bits that are not registered yet may be added; re-adding the same mask on // every rearrange would duplicate listeners without bound. for (QQuickItem* ancestor : std::as_const(current)) { const auto have = d->ancestorMasks.value(ancestor); const auto missing = mask & ~have; const auto extra = have & ~mask; if (extra != 0) { QQuickItemPrivate::get(ancestor)->removeItemChangeListener( this, extra); } if (missing != 0) { QQuickItemPrivate::get(ancestor)->addItemChangeListener( this, missing); } d->ancestorMasks[ancestor] = mask; } d->ancestorListeners.clear(); d->ancestorListeners.reserve(current.size()); for (QQuickItem* ancestor : std::as_const(current)) d->ancestorListeners.append(ancestor); } void SnapLayout::clearAncestorListeners() { Q_D(SnapLayout); for (const QPointer& ancestor : std::as_const(d->ancestorListeners)) { if (ancestor) { QQuickItemPrivate::get(ancestor.data()) ->removeItemChangeListener( this, d->ancestorMasks.value(ancestor.data())); } } d->ancestorMasks.clear(); d->ancestorListeners.clear(); } void SnapLayout::arrangeChildren() { Q_D(SnapLayout); const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; if (dpr <= 0.0) return; const bool horiz = d->orientation == Qt::Horizontal; const QPointF origin = absoluteOrigin(this); const qint64 originXDev = std::llround(origin.x() * dpr); const qint64 originYDev = std::llround(origin.y() * dpr); qreal prevEndExact = 0.0; qint64 prevNearDev = 0; qint64 prevSizeDev = 0; bool started = false; // The layout's own main extent, rounded down: no child is placed past it, so // a Flickable that takes this layout's height as its content height can still // reach the last child. const QSizeF self = size(); const qint64 layoutMainExtentDev = qint64(std::floor((horiz ? self.width() : self.height()) * dpr + 1e-6)); for (int i = 0; i < d->engine.itemCount(); ++i) { SnapGridLayoutItem* gridItem = static_cast(d->engine.itemAt(i)); if (!gridItem->placed) continue; QQuickItem* child = gridItem->layoutItem(); const QRectF& cell = gridItem->engineRect; const qreal mainExact = (horiz ? cell.x() : cell.y()) * dpr; const qreal mainSizeExact = (horiz ? cell.width() : cell.height()) * dpr; const qreal crossExact = (horiz ? cell.y() : cell.x()) * dpr; const qint64 mainSizeDev = std::llround(mainSizeExact); const qint64 crossSizeDev = std::llround((horiz ? cell.height() : cell.width()) * dpr); const qint64 engineWidth = horiz ? mainSizeDev : crossSizeDev; const qint64 engineHeight = horiz ? crossSizeDev : mainSizeDev; qint64 mainNear = 0; if (!started) { mainNear = std::llround(mainExact); started = true; } else { mainNear = prevNearDev + prevSizeDev + std::llround(mainExact - prevEndExact); } const qint64 mainFarExact = std::llround(mainExact + mainSizeExact); // An aligned child whose cell is its own implicit size is sized from that // implicit size. The engine recomputes cells from animated geometry, so a // cell that is mathematically 41 can arrive as 40.9999999999; when the // device pixel ratio puts 41 exactly on half a device pixel, that epsilon // flips the rounding from pass to pass and the child visibly resizes while // it only moves. Children that fill their axis, or whose cell has grown // past their implicit size, keep the engine-derived size so that animated // fills still step through device pixels. QQuickLayoutAttached* info = attachedLayoutObject(child, false); const qreal mainOwn = horiz ? child->implicitWidth() : child->implicitHeight(); const qreal crossOwn = horiz ? child->implicitHeight() : child->implicitWidth(); const bool fillsMain = info && (horiz ? info->fillWidth() : info->fillHeight()); const bool fillsCross = info && (horiz ? info->fillHeight() : info->fillWidth()); const auto snappedOwn = [&](bool fills, qreal cellSize, qreal ownSize) { if (fills || ownSize <= 0.0 || qAbs(cellSize - ownSize) >= 1.0) return qint64(-1); // Round up, like Qt's `qCeil` of implicit sizes: a child is never // smaller than the content it holds, and this is the same rounding // sizeHint() sums to compute the layout's own preferred size, so the // children fill the layout without slack and without overflow. return (std::max)(qint64(std::ceil(ownSize * dpr - 1e-9)), qint64(0)); }; const qint64 mainOwnDev = snappedOwn(fillsMain, horiz ? cell.width() : cell.height(), mainOwn); const qint64 crossOwnDev = snappedOwn( fillsCross, horiz ? cell.height() : cell.width(), crossOwn); // The size along the main axis. An implicit-sized child keeps its own // implicit size, which carries the device-pixel parity ZUtils chose for // it. Rounding is not neutral: an odd logical size at 1.5x is exactly half // a device pixel and always rounds up, so left unchecked every child adds // half a pixel and a long list ends outside the layout - a Flickable reads // the layout's height as the end of the content and the last rows become // unreachable. Corrections are made in steps of two device pixels: an item // that centers its own content (Layout.HCenter / Layout.VCenter, // anchors.centerIn) lands on whole device pixels only while its device // size keeps that parity, so a one-pixel correction moves its centered // children onto half a pixel and they antialias. Drift from the engine's // edge is bounded at two device pixels, and no child is placed past the // layout's own edge - a layout with less room than its children needs // overflows in Qt too, so the engine's own edge is never pulled back. const qint64 mainFarCeil = (std::max)(mainFarExact, layoutMainExtentDev); const qint64 mainLimit = (std::min)(mainFarExact + 2, mainFarCeil) - mainNear; qint64 width = engineWidth; qint64 height = engineHeight; const qint64 mainSizeWritten = mainOwnDev >= 0 ? mainOwnDev : (std::max)(mainFarExact - mainNear, qint64(0)); if (horiz) width = mainSizeWritten; else height = mainSizeWritten; if (crossOwnDev >= 0) { if (horiz) height = crossOwnDev; else width = crossOwnDev; } // Keep the size already written unless the engine asks for a different // one, or something else resized the child. An aligned size stays // aligned wherever the layout moves, and resizing again costs the child // a relayout: an Image reloads its texture, a nested layout rearranges. const auto stored = d->childSizes.constFind(child); if (stored != d->childSizes.constEnd() && stored.value().engineWidth == engineWidth && stored.value().engineHeight == engineHeight && std::llround(child->width() * dpr) == stored.value().width && std::llround(child->height() * dpr) == stored.value().height) { width = stored.value().width; height = stored.value().height; } // A remembered size can predate the layout being sized, so the limit is // applied to whatever ends up being written, keeping the parity of the // size this child is asking for. qint64& mainAxisSize = horiz ? width : height; const int mainParity = int(mainAxisSize % 2); if (mainAxisSize > mainLimit) { mainAxisSize = mainLimit; if (int(mainAxisSize % 2) != mainParity) --mainAxisSize; } mainAxisSize = (std::max)(mainAxisSize, qint64(0)); d->childSizes[child] = SnapLayoutPrivate::ChildSize{ engineWidth, engineHeight, width, height}; prevEndExact = mainExact + mainSizeExact; prevNearDev = mainNear; prevSizeDev = horiz ? width : height; const qint64 crossNear = std::llround(crossExact); const qint64 nearX = originXDev + (horiz ? mainNear : crossNear); const qint64 nearY = originYDev + (horiz ? crossNear : mainNear); const QRectF snapped( qreal(nearX) / dpr - origin.x(), qreal(nearY) / dpr - origin.y(), qreal(width) / dpr, qreal(height) / dpr); const QSizeF oldSize(child->width(), child->height()); const QSizeF newSize = snapped.size(); child->setPosition(snapped.topLeft()); if (newSize == oldSize) { if (QQuickLayout* lay = qobject_cast(child)) { if (lay->invalidatedArrangement()) lay->rearrange(newSize); } } else { child->setSize(newSize); } } } void SnapLayout::rearrange(const QSizeF& size) { Q_D(SnapLayout); if (!isReady() || !size.isValid()) return; const auto refCounter = qScopeGuard([&d] { --(d->recurRearrangeCounter); }); if (d->recurRearrangeCounter++ == 2) { qWarning() << "Qt Quick Layouts: Detected recursive rearrange. " "Aborting after two iterations."; return; } ensureLayoutItemsUpdated( QQuickLayout::ApplySizeHints | QQuickLayout::Recursive); d->rearranging = true; d->engine.setVisualDirection(effectiveLayoutDirection()); updateAncestorListeners(); if (const QQuickWindow* win = window()) { if (const qreal dpr = win->effectiveDevicePixelRatio(); dpr > 0.0) { const QPointF origin = absoluteOrigin(this); d->originResidualX = originResidual(origin.x(), dpr); d->originResidualY = originResidual(origin.y(), dpr); } } QQuickLayout::rearrange(size); for (int i = 0; i < d->engine.itemCount(); ++i) static_cast(d->engine.itemAt(i))->placed = false; d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo); arrangeChildren(); d->rearranging = false; for (auto childItem : std::as_const(d->invalidateAfterRearrange)) invalidate(childItem); d->invalidateAfterRearrange.clear(); } } // namespace ZShell::components