Files
z-bar-qt/Plugins/ZShell/Components/snaplayout.cpp
T
zach 11e7aac195
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
feat: better notification storage and image caching + blobs
2026-11-02 19:00:43 +01:00

807 lines
27 KiB
C++

#include "snaplayout.hpp"
#include <QLoggingCategory>
#include <QQuickWindow>
#include <QScopeGuard>
#include <QtGui/private/qfont_p.h>
#include <algorithm>
#include <cmath>
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<double>(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<QQuickLayout*>(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<const SnapGridLayoutItem*>(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<QQuickLayout*>(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<QQuickGridLayoutItem*>(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<QQuickItemPrivate::ChangeType> mask =
QQuickItemPrivate::Geometry;
QVector<QQuickItem*> current;
for (QQuickItem* it = parentItem(); it; it = it->parentItem()) {
if (qobject_cast<SnapLayout*>(it)) break;
current.append(it);
}
for (const QPointer<QQuickItem>& 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<QQuickItem>& 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<SnapGridLayoutItem*>(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<QQuickLayout*>(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<SnapGridLayoutItem*>(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