#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; // Which way the children are traversed, so the leading and trailing margin // of a child are the sides the arrangement measures its gap from. const int sgn = horiz && effectiveLayoutDirection() == Qt::RightToLeft ? -1 : 1; // 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)); // Every margin side is rounded on its own, exactly as the arrangement // rounds it when it places the child. Rounding the two sides of a child // together can leave the hint a device pixel short of what is written - // two one-pixel margins are three device pixels at 1.5x when summed but // four when rounded separately - and a Flickable that takes this hint as // its content height would then have bounds that cut into the content. const qreal mainLead = horiz ? (sgn > 0 ? margins.left() : margins.right()) : margins.top(); const qreal mainTrail = horiz ? (sgn > 0 ? margins.right() : margins.left()) : margins.bottom(); const qint64 mainMarginDev = std::llround(mainLead * dpr) + std::llround(mainTrail * dpr); const qreal crossLead = horiz ? margins.top() : (sgn > 0 ? margins.left() : margins.right()); const qreal crossTrail = horiz ? margins.bottom() : (sgn > 0 ? margins.right() : margins.left()); const qint64 crossDevForChild = qint64(std::ceil(crossHint * dpr - 1e-9)) + std::llround(crossLead * dpr) + std::llround(crossTrail * dpr); 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); // 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)); // 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. 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)); }; struct Row { QQuickItem* child = nullptr; qreal mainExact = 0.0; // engine start, device pixels qreal mainSizeExact = 0.0; // engine main size, device pixels qint64 mainFarExact = 0; // engine end, device pixels qint64 crossExact = 0; // engine cross start, device pixels qint64 engineWidth = 0; qint64 engineHeight = 0; qint64 mainOwnDev = -1; qint64 crossOwnDev = -1; qint64 marginLeadDev = 0; // Layout margin before the child, device pixels qint64 marginTrailDev = 0; // Layout margin after the child, device pixels bool fillsMain = false; qint64 pos = 0; // leading edge in traversal device pixels qint64 size = 0; // main size to write qint64 cross = 0; // cross size to write }; // Traversal coordinates run along the main axis in child order: left to right // unless the layout is mirrored. A child's `pos` is its leading edge in that // direction, so it spans [pos, pos + size] whichever way the layout runs, and // the chain and the anchoring below are direction-free. const int sgn = horiz && effectiveLayoutDirection() == Qt::RightToLeft ? -1 : 1; // Spacing is normalized to whole device pixels, and each child's Layout // margins are rounded on their own rather than together with the spacing: // rounding the sum makes the same spacing read as 7 or 8 device pixels // depending on which two children it sits between. const qint64 spacingDev = std::llround(spacing() * dpr); QVector rows; rows.reserve(d->engine.itemCount()); 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; Row row; row.child = child; row.mainExact = mainExact; row.mainSizeExact = mainSizeExact; row.mainFarExact = std::llround(mainExact + mainSizeExact); row.crossExact = std::llround((horiz ? cell.y() : cell.x()) * dpr); const qint64 mainSizeDev = std::llround(mainSizeExact); const qint64 crossSizeDev = std::llround((horiz ? cell.height() : cell.width()) * dpr); row.engineWidth = horiz ? mainSizeDev : crossSizeDev; row.engineHeight = horiz ? crossSizeDev : mainSizeDev; QQuickLayoutAttached* info = attachedLayoutObject(child, false); const qreal mainOwn = horiz ? child->implicitWidth() : child->implicitHeight(); const qreal crossOwn = horiz ? child->implicitHeight() : child->implicitWidth(); row.fillsMain = info && (horiz ? info->fillWidth() : info->fillHeight()); const bool fillsCross = info && (horiz ? info->fillHeight() : info->fillWidth()); row.mainOwnDev = snappedOwn( row.fillsMain, horiz ? cell.width() : cell.height(), mainOwn); row.crossOwnDev = snappedOwn( fillsCross, horiz ? cell.height() : cell.width(), crossOwn); const QMarginsF margins = info ? info->effectiveQMargins() : QMarginsF(0, 0, 0, 0); const qreal marginLead = horiz ? (sgn > 0 ? margins.left() : margins.right()) : margins.top(); const qreal marginTrail = horiz ? (sgn > 0 ? margins.right() : margins.left()) : margins.bottom(); row.marginLeadDev = std::llround(marginLead * dpr); row.marginTrailDev = std::llround(marginTrail * dpr); rows.append(row); } if (rows.isEmpty()) return; // The layout's own edges in traversal coordinates. The first child is placed // against the start edge and the last one back from the end edge, each by its // own rounded margin - the same rounding the gaps and `sizeHint` use, so the // layout's implicit size is the size its children actually occupy. Rounding // the engine's cell edge instead measures the margin from the far end, which // can land a device pixel away from that. const qint64 layoutStartDev = sgn > 0 ? 0 : -layoutMainExtentDev; const qint64 layoutEndDev = sgn > 0 ? layoutMainExtentDev : 0; // The engine's trailing edge in traversal coordinates: only used to keep an // over-constrained layout overflowing, as Qt does, instead of pulling its // last child back inside the box. const auto engineTrailing = [&](const Row& row) { return sgn > 0 ? row.mainFarExact : -std::llround(row.mainExact); }; const auto gapDev = [&](int prev, int next) { return spacingDev + rows.at(prev).marginTrailDev + rows.at(next).marginLeadDev; }; // Sizes are decided before positions. A gap is measured from the size that // actually gets written, so spacing stays equal however a child was rounded, // remembered, or filled. for (Row& row : rows) { qint64 main = row.mainOwnDev >= 0 ? row.mainOwnDev : (std::max)(std::llround(row.mainSizeExact), qint64(0)); qint64 cross = row.crossOwnDev >= 0 ? row.crossOwnDev : (std::max)(horiz ? row.engineHeight : row.engineWidth, qint64(0)); // 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(row.child); if (stored != d->childSizes.constEnd() && stored.value().engineWidth == row.engineWidth && stored.value().engineHeight == row.engineHeight && std::llround(row.child->width() * dpr) == stored.value().width && std::llround(row.child->height() * dpr) == stored.value().height) { main = horiz ? stored.value().width : stored.value().height; cross = horiz ? stored.value().height : stored.value().width; } row.size = (std::max)(main, qint64(0)); row.cross = (std::max)(cross, qint64(0)); } // The first child that fills the main axis absorbs every change in front of // it, and every child behind it is measured back from the layout's own end // edge. A chain that runs through a fill would otherwise carry a leading // child's rounding - and its size change - into children that are supposed // to sit still against the far edge. int fillIdx = int(rows.size()); for (int i = 0; i < int(rows.size()); ++i) { if (rows.at(i).fillsMain) { fillIdx = i; break; } } qint64 prevPos = 0; qint64 prevSize = 0; const int leadEnd = (std::min)(fillIdx, int(rows.size()) - 1); for (int i = 0; i <= leadEnd; ++i) { Row& row = rows[i]; row.pos = i == 0 ? layoutStartDev + row.marginLeadDev : prevPos + prevSize + gapDev(i - 1, i); prevPos = row.pos; prevSize = row.size; } if (fillIdx + 1 < int(rows.size())) { // Measured back from the layout's end edge, by the last child's own // rounded trailing margin. Only when the engine says the children need // more room than the layout has - an over-constrained layout, which // overflows in Qt too - is the engine's own edge used instead. const qint64 engineEnd = engineTrailing(rows.back()); qint64 far = engineEnd > layoutEndDev ? engineEnd : layoutEndDev - rows.back().marginTrailDev; for (int i = int(rows.size()) - 1; i > fillIdx; --i) { Row& row = rows[i]; row.pos = far - row.size; far = row.pos - gapDev(i - 1, i); } // Whatever the leading block left over belongs to the filling child. Row& fill = rows[fillIdx]; const qint64 remainder = far - fill.pos; if (remainder >= 0) fill.size = remainder; } else if (fillIdx < int(rows.size())) { // A fill that ends the layout takes the whole space up to its end edge. Row& fill = rows[fillIdx]; const qint64 remainder = layoutEndDev - fill.pos; if (remainder >= 0) fill.size = remainder; } for (Row& row : rows) { QQuickItem* child = row.child; // Nothing may be written past the layout's own end edge: a child sized // from a rounded-up implicit size can reach past it while the layout is // still being sized. Corrections are made in steps of two device pixels, // so an item that centers its own content (Layout.HCenter / // Layout.VCenter, anchors.centerIn) stays on whole device pixels. The // layout's edge is the only ceiling - the engine's cell edge is not, // because sizes here are rounded up and a child that asks for half a // device pixel more than its cell is not shrunk: shrinking it would push // the spacing behind it off the normalized value. const qint64 mainLimit = (std::max)(layoutEndDev - row.pos, qint64(0)); qint64 mainSize = row.size; if (mainSize > mainLimit) { mainSize = mainLimit; // Keep the parity of the size the child asks for. if (int(mainSize % 2) != int(row.size % 2)) --mainSize; } mainSize = (std::max)(mainSize, qint64(0)); const qint64 near = sgn > 0 ? row.pos : -row.pos - mainSize; const qint64 width = horiz ? mainSize : row.cross; const qint64 height = horiz ? row.cross : mainSize; // What is remembered is what the child asked for, not what was written: // a size shortened by the ceiling above must be tried again once the // layout has room, or the content stays shorter than the layout's own // implicit size forever - a Flickable that takes that implicit size as // its content height then has bounds that do not match its content. d->childSizes[child] = SnapLayoutPrivate::ChildSize{ row.engineWidth, row.engineHeight, horiz ? row.size : row.cross, horiz ? row.cross : row.size}; const qint64 nearX = originXDev + (horiz ? near : row.crossExact); const qint64 nearY = originYDev + (horiz ? row.crossExact : near); 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