940 lines
32 KiB
C++
940 lines
32 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;
|
|
// 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<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));
|
|
// 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<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);
|
|
|
|
// 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<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));
|
|
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<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
|