#include "snaplayout.hpp" #include #include #include #include #include #include Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts") namespace ZShell::components { namespace { qint64 originResidual(qreal logical, qreal dpr) { const qreal device = logical * dpr; return std::llround((device - std::floor(device)) * 1024.0) % 1024; } QPointF absoluteOrigin(const QQuickItem* item) { qreal x = 0.0; qreal y = 0.0; for (const QQuickItem* it = item; it; it = it->parentItem()) { x += it->x(); y += it->y(); } return QPointF(x, y); } } // namespace qreal SnapLayoutStyleInfo::spacing(Qt::Orientation /*orientation*/) const { #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_QNX) qreal spacing = 8.0; #else qreal spacing = 5.0; #endif #if !defined(Q_OS_MACOS) && !defined(Q_OS_HARMONY) spacing = qRound(spacing * (qreal(qt_defaultDpiX()) / 96.0)); #endif return spacing; } qreal SnapLayoutStyleInfo::windowMargin(Qt::Orientation /*orientation*/) const { return 0; } bool SnapLayoutStyleInfo::hasChangedCore() const { return false; } SnapGridLayoutItem::SnapGridLayoutItem( QQuickItem* item, int row, int column, int rowSpan, int columnSpan, Qt::Alignment alignment) : QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) {} void SnapGridLayoutItem::setGeometry(const QRectF& rect) { QQuickLayoutAttached* info = attachedLayoutObject(m_item, false); engineRect = info ? rect.marginsRemoved(info->effectiveQMargins()) : rect; placed = true; } void SnapLayoutPrivate::mirrorChange() { Q_Q(SnapLayout); q->invalidate(); } SnapLayout::SnapLayout(QQuickItem* parent) : QQuickLayout(*new SnapLayoutPrivate, parent) { d_func()->styleInfo = new SnapLayoutStyleInfo; } SnapLayout::~SnapLayout() { Q_D(SnapLayout); clearAncestorListeners(); deactivateRecur(); delete d->styleInfo; } void SnapLayout::componentComplete() { QQuickLayout::componentComplete(); applySpacing(); invalidate(); ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints); if (qobject_cast(parentItem())) return; rearrange(QSizeF(width(), height())); } void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) { QQuickLayout::itemChange(change, value); if (change == ItemChildRemovedChange && value.item) d_func()->childSizes.remove(value.item); if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) { applySpacing(); invalidate(); } } QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const { Q_D(const SnapLayout); return d->engine.sizeHint(whichSizeHint, QSizeF(), d->styleInfo); } Qt::Orientation SnapLayout::orientation() const { return d_func()->orientation; } void SnapLayout::setOrientation(Qt::Orientation orientation) { Q_D(SnapLayout); if (d->orientation == orientation) return; d->orientation = orientation; invalidate(); emit orientationChanged(); } Qt::LayoutDirection SnapLayout::layoutDirection() const { return d_func()->layoutDirection; } void SnapLayout::setLayoutDirection(Qt::LayoutDirection direction) { Q_D(SnapLayout); if (d->layoutDirection == direction) return; d->layoutDirection = direction; invalidate(); emit layoutDirectionChanged(); } Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const { return !isMirrored() == (layoutDirection() == Qt::LeftToRight) ? Qt::LeftToRight : Qt::RightToLeft; } qreal SnapLayout::spacing() const { Q_D(const SnapLayout); if (!std::isnan(d->spacingRequest)) return d->spacingRequest; return d->styleInfo->spacing(d->orientation); } void SnapLayout::applySpacing() { Q_D(SnapLayout); if (!isReady()) return; const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0; qreal space = d->spacingRequest; if (std::isnan(space)) space = d->styleInfo->spacing(d->orientation); if (dpr > 0.0) space = qreal(std::llround(space * dpr)) / dpr; d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical); } void SnapLayout::setSpacing(qreal space) { Q_D(SnapLayout); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" if (std::isnan(space) || d->spacingRequest == space) return; #pragma GCC diagnostic pop d->spacingRequest = space; applySpacing(); invalidate(); emit spacingChanged(); } bool SnapLayout::uniformCellSizes() const { Q_D(const SnapLayout); return d->engine.uniformCellWidths(); } void SnapLayout::setUniformCellSizes(bool uniformCellSizes) { Q_D(SnapLayout); if (d->engine.uniformCellHeights() == uniformCellSizes) return; d->engine.setUniformCellWidths(uniformCellSizes); d->engine.setUniformCellHeights(uniformCellSizes); invalidate(); emit uniformCellSizesChanged(); } void SnapLayout::setAlignment(QQuickItem* item, Qt::Alignment align) { Q_D(SnapLayout); if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(item)) { layoutItem->setAlignment(align); d->engine.invalidate(); } maybeSubscribeToBaseLineOffsetChanges(item); } void SnapLayout::setStretchFactor( QQuickItem* item, int stretchFactor, Qt::Orientation orient) { Q_D(SnapLayout); if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(item)) { layoutItem->setStretchFactor(stretchFactor, orient); d->engine.invalidate(); } } void SnapLayout::invalidate(QQuickItem* childItem) { Q_D(SnapLayout); if (!isReady()) return; if (childItem) { if (d->rearranging) { if (!d->invalidateAfterRearrange.contains(childItem)) d->invalidateAfterRearrange << childItem; return; } if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(childItem)) { layoutItem->invalidate(); } } d->engine.invalidate(); QQuickLayout::invalidate(); if (auto* parentLayout = qobject_cast(parentItem())) parentLayout->invalidate(this); } void SnapLayout::updateLayoutItems() { Q_D(SnapLayout); if (!isReady()) return; const auto items = childItems(); for (auto entry = d->childSizes.begin(); entry != d->childSizes.end();) { if (items.contains(entry.key())) ++entry; else entry = d->childSizes.erase(entry); } d->engine.deleteItems(); insertLayoutItems(); } QQuickItem* SnapLayout::itemAt(int index) const { Q_D(const SnapLayout); return static_cast(d->engine.itemAt(index)) ->layoutItem(); } int SnapLayout::itemCount() const { Q_D(const SnapLayout); return d->engine.itemCount(); } void SnapLayout::insertLayoutItems() { Q_D(SnapLayout); const auto items = childItems(); for (QQuickItem* child : items) { Q_ASSERT(child); checkAnchors(child); if (shouldIgnoreItem(child)) continue; QQuickLayoutAttached* info = attachedLayoutObject(child, false); Qt::Alignment alignment; int hStretch = -1; int vStretch = -1; bool fillWidth = false; bool fillHeight = false; if (info) { alignment = info->alignment(); hStretch = info->horizontalStretchFactor(); vStretch = info->verticalStretchFactor(); fillWidth = info->fillWidth(); fillHeight = info->fillHeight(); } const int index = d->engine.rowCount(d->orientation); d->engine.insertRow(index, d->orientation); int gridRow = 0; int gridColumn = index; if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn); QGridLayoutItem* layoutItem = new SnapGridLayoutItem(child, gridRow, gridColumn, 1, 1, alignment); if (hStretch >= 0) { if (!fillWidth) qWarning() << "horizontalStretchFactor requires fillWidth to " "also be set to true"; layoutItem->setStretchFactor(hStretch, Qt::Horizontal); } if (vStretch >= 0) { if (!fillHeight) qWarning() << "verticalStretchFactor requires fillHeight to " "also be set to true"; layoutItem->setStretchFactor(vStretch, Qt::Vertical); } d->engine.insertItem(layoutItem, index); } } void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) { Q_D(SnapLayout); const int index = gridItem->firstRow(d->orientation); d->engine.removeItem(gridItem); d->engine.removeRows(index, 1, d->orientation); } void SnapLayout::itemDestroyed(QQuickItem* item) { if (!isReady()) return; Q_D(SnapLayout); d->childSizes.remove(item); if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) { removeGridItem(gridItem); delete gridItem; invalidate(); } } void SnapLayout::itemVisibilityChanged(QQuickItem* item) { if (!isReady()) return; invalidate(item); } void SnapLayout::itemGeometryChanged( QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) { Q_UNUSED(item); Q_UNUSED(oldGeometry); if (!isReady() || !change.positionChange()) return; Q_D(SnapLayout); const QQuickWindow* win = window(); const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0; if (dpr <= 0.0) return; const QPointF origin = absoluteOrigin(this); if (originResidual(origin.x(), dpr) == d->originResidualX && originResidual(origin.y(), dpr) == d->originResidualY) return; polish(); } void SnapLayout::updateAncestorListeners() { Q_D(SnapLayout); QVector current; for (QQuickItem* it = parentItem(); it; it = it->parentItem()) { if (qobject_cast(it)) break; current.append(it); } for (const QPointer& ancestor : std::as_const(d->ancestorListeners)) { if (ancestor && !current.contains(ancestor.data())) QQuickItemPrivate::get(ancestor.data()) ->removeItemChangeListener(this, QQuickItemPrivate::Geometry); } for (QQuickItem* ancestor : std::as_const(current)) { if (!d->ancestorListeners.contains(ancestor)) QQuickItemPrivate::get(ancestor)->addItemChangeListener( this, QQuickItemPrivate::Geometry); } d->ancestorListeners.clear(); d->ancestorListeners.reserve(current.size()); for (QQuickItem* ancestor : std::as_const(current)) d->ancestorListeners.append(ancestor); } void SnapLayout::clearAncestorListeners() { Q_D(SnapLayout); for (const QPointer& ancestor : std::as_const(d->ancestorListeners)) { if (ancestor) QQuickItemPrivate::get(ancestor.data()) ->removeItemChangeListener(this, QQuickItemPrivate::Geometry); } 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; for (int i = 0; i < d->engine.itemCount(); ++i) { SnapGridLayoutItem* gridItem = static_cast(d->engine.itemAt(i)); if (!gridItem->placed) continue; QQuickItem* child = gridItem->layoutItem(); const QRectF& cell = gridItem->engineRect; const qreal mainExact = (horiz ? cell.x() : cell.y()) * dpr; const qreal mainSizeExact = (horiz ? cell.width() : cell.height()) * dpr; 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 mainFarDev = std::llround(mainExact + mainSizeExact); const auto stored = d->childSizes.constFind(child); qint64 width = engineWidth; qint64 height = engineHeight; if (horiz) width = std::max(mainFarDev - mainNear, qint64(0)); else height = std::max(mainFarDev - mainNear, qint64(0)); 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; } else { 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(child)) { if (lay->invalidatedArrangement()) lay->rearrange(newSize); } } else { child->setSize(newSize); } } } void SnapLayout::rearrange(const QSizeF& size) { Q_D(SnapLayout); if (!isReady() || !size.isValid()) return; const auto refCounter = qScopeGuard([&d] { --(d->recurRearrangeCounter); }); if (d->recurRearrangeCounter++ == 2) { qWarning() << "Qt Quick Layouts: Detected recursive rearrange. " "Aborting after two iterations."; return; } ensureLayoutItemsUpdated( QQuickLayout::ApplySizeHints | QQuickLayout::Recursive); d->rearranging = true; d->engine.setVisualDirection(effectiveLayoutDirection()); updateAncestorListeners(); if (const QQuickWindow* win = window()) { if (const qreal dpr = win->effectiveDevicePixelRatio(); dpr > 0.0) { const QPointF origin = absoluteOrigin(this); d->originResidualX = originResidual(origin.x(), dpr); d->originResidualY = originResidual(origin.y(), dpr); } } QQuickLayout::rearrange(size); for (int i = 0; i < d->engine.itemCount(); ++i) static_cast(d->engine.itemAt(i))->placed = false; d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo); arrangeChildren(); d->rearranging = false; for (auto childItem : std::as_const(d->invalidateAfterRearrange)) invalidate(childItem); d->invalidateAfterRearrange.clear(); } } // namespace ZShell::components