feat: better notification storage and image caching + blobs
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
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
This commit is contained in:
@@ -17,6 +17,20 @@ qint64 originResidual(qreal logical, qreal 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;
|
||||
@@ -102,9 +116,174 @@ void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return d->engine.sizeHint(whichSizeHint, QSizeF(), d->styleInfo);
|
||||
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 {
|
||||
@@ -325,24 +504,33 @@ void SnapLayout::itemVisibilityChanged(QQuickItem* item) {
|
||||
|
||||
void SnapLayout::itemGeometryChanged(
|
||||
QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) {
|
||||
Q_UNUSED(item);
|
||||
Q_UNUSED(oldGeometry);
|
||||
if (!isReady() || !change.positionChange()) return;
|
||||
Q_UNUSED(item)
|
||||
Q_UNUSED(change)
|
||||
Q_UNUSED(oldGeometry)
|
||||
if (!isReady()) return;
|
||||
|
||||
Q_D(SnapLayout);
|
||||
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;
|
||||
polish();
|
||||
|
||||
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;
|
||||
@@ -351,15 +539,34 @@ void SnapLayout::updateAncestorListeners() {
|
||||
|
||||
for (const QPointer<QQuickItem>& ancestor :
|
||||
std::as_const(d->ancestorListeners)) {
|
||||
if (ancestor && !current.contains(ancestor.data()))
|
||||
if (ancestor && !current.contains(ancestor.data())) {
|
||||
QQuickItemPrivate::get(ancestor.data())
|
||||
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
|
||||
->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)) {
|
||||
if (!d->ancestorListeners.contains(ancestor))
|
||||
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, QQuickItemPrivate::Geometry);
|
||||
this, missing);
|
||||
}
|
||||
|
||||
d->ancestorMasks[ancestor] = mask;
|
||||
}
|
||||
|
||||
d->ancestorListeners.clear();
|
||||
d->ancestorListeners.reserve(current.size());
|
||||
for (QQuickItem* ancestor : std::as_const(current))
|
||||
@@ -370,10 +577,13 @@ void SnapLayout::clearAncestorListeners() {
|
||||
Q_D(SnapLayout);
|
||||
for (const QPointer<QQuickItem>& ancestor :
|
||||
std::as_const(d->ancestorListeners)) {
|
||||
if (ancestor)
|
||||
if (ancestor) {
|
||||
QQuickItemPrivate::get(ancestor.data())
|
||||
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
|
||||
->removeItemChangeListener(
|
||||
this, d->ancestorMasks.value(ancestor.data()));
|
||||
}
|
||||
}
|
||||
d->ancestorMasks.clear();
|
||||
d->ancestorListeners.clear();
|
||||
}
|
||||
|
||||
@@ -393,6 +603,13 @@ void SnapLayout::arrangeChildren() {
|
||||
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));
|
||||
@@ -419,16 +636,83 @@ void SnapLayout::arrangeChildren() {
|
||||
std::llround(mainExact - prevEndExact);
|
||||
}
|
||||
|
||||
const qint64 mainFarDev = std::llround(mainExact + mainSizeExact);
|
||||
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;
|
||||
|
||||
const auto stored = d->childSizes.constFind(child);
|
||||
qint64 width = engineWidth;
|
||||
qint64 height = engineHeight;
|
||||
const qint64 mainSizeWritten =
|
||||
mainOwnDev >= 0 ? mainOwnDev
|
||||
: (std::max)(mainFarExact - mainNear, qint64(0));
|
||||
if (horiz)
|
||||
width = std::max(mainFarDev - mainNear, qint64(0));
|
||||
width = mainSizeWritten;
|
||||
else
|
||||
height = std::max(mainFarDev - mainNear, qint64(0));
|
||||
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 &&
|
||||
@@ -436,11 +720,22 @@ void SnapLayout::arrangeChildren() {
|
||||
std::llround(child->height() * dpr) == stored.value().height) {
|
||||
width = stored.value().width;
|
||||
height = stored.value().height;
|
||||
} else {
|
||||
d->childSizes[child] = SnapLayoutPrivate::ChildSize{
|
||||
engineWidth, engineHeight, width, 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;
|
||||
|
||||
Reference in New Issue
Block a user