fix: font macro expansion + snaplayout jitter with fillWidth: true

This commit is contained in:
2026-11-07 14:42:22 +01:00
parent 4ac55b0c2e
commit a7767d2a80
6 changed files with 296 additions and 172 deletions
+237 -104
View File
@@ -172,6 +172,10 @@ QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
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())`
@@ -239,10 +243,29 @@ QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
// 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));
// 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)) +
qint64(std::ceil(crossMargin * dpr - 1e-9));
std::llround(crossLead * dpr) + std::llround(crossTrail * dpr);
mainExtentDev += mainDev + mainMarginDev;
maxMainDev = (std::max)(maxMainDev, mainDev + mainMarginDev);
@@ -598,11 +621,6 @@ void SnapLayout::arrangeChildren() {
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.
@@ -610,6 +628,60 @@ void SnapLayout::arrangeChildren() {
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<Row> rows;
rows.reserve(d->engine.itemCount());
for (int i = 0; i < d->engine.itemCount(); ++i) {
SnapGridLayoutItem* gridItem =
static_cast<SnapGridLayoutItem*>(d->engine.itemAt(i));
@@ -620,129 +692,190 @@ void SnapLayout::arrangeChildren() {
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;
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);
const qint64 engineWidth = horiz ? mainSizeDev : crossSizeDev;
const qint64 engineHeight = horiz ? crossSizeDev : mainSizeDev;
row.engineWidth = horiz ? mainSizeDev : crossSizeDev;
row.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());
row.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(
row.mainOwnDev = snappedOwn(
row.fillsMain, horiz ? cell.width() : cell.height(), mainOwn);
row.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;
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);
}
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 (rows.isEmpty()) return;
if (crossOwnDev >= 0) {
if (horiz)
height = crossOwnDev;
else
width = crossOwnDev;
}
// 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(child);
const auto stored = d->childSizes.constFind(row.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;
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;
}
// 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));
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{
engineWidth, engineHeight, width, height};
row.engineWidth,
row.engineHeight,
horiz ? row.size : row.cross,
horiz ? row.cross : row.size};
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 qint64 nearX = originXDev + (horiz ? near : row.crossExact);
const qint64 nearY = originYDev + (horiz ? row.crossExact : near);
const QRectF snapped(
qreal(nearX) / dpr - origin.x(),