feat: initial commit for pixel-snapping components and helpers

This commit is contained in:
2026-10-29 17:34:57 +01:00
parent 9d8bde1413
commit 9cbb90f9e1
163 changed files with 2671 additions and 951 deletions
+5
View File
@@ -1,5 +1,6 @@
qml_module(ZShell-components
URI ZShell.Components
DEPENDENCIES QtQuick.Layouts
SOURCES
lazylistview.hpp lazylistview.cpp
chatlistview.hpp chatlistview.cpp
@@ -12,10 +13,14 @@ qml_module(ZShell-components
sparklinesceneitem.hpp sparklinesceneitem.cpp
roundedmask.hpp roundedmask.cpp
roundedclip.hpp roundedclip.cpp
snaplayout.hpp snaplayout.cpp
wheelinverter.hpp
LIBRARIES
Qt::Quick
Qt::QuickPrivate
Qt::QuickLayouts
Qt::QuickLayoutsPrivate
Qt::GuiPrivate
Qt::CanvasPainter
ZShell-internal
)
+419
View File
@@ -0,0 +1,419 @@
#include "snaplayout.hpp"
#include <QLoggingCategory>
#include <QQuickWindow>
#include <QScopeGuard>
#include <QtGui/private/qfont_p.h>
#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;
}
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,
SnapLayout* layout)
: QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment)
, m_layout(layout) {}
void SnapGridLayoutItem::setGeometry(const QRectF& rect) {
QQuickLayoutAttached* info = attachedLayoutObject(m_item, false);
QRectF placed = info ? rect.marginsRemoved(info->effectiveQMargins())
: rect;
const QQuickWindow* win = m_item->window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
if (dpr > 0.0) {
const QPointF origin = absoluteOrigin(m_layout);
const qreal left = (origin.x() + placed.x()) * dpr;
const qreal top = (origin.y() + placed.y()) * dpr;
const qreal right = (origin.x() + placed.right()) * dpr;
const qreal bottom = (origin.y() + placed.bottom()) * dpr;
const qint64 nearX = std::llround(left);
const qint64 nearY = std::llround(top);
const qint64 farX = std::llround(right);
const qint64 farY = std::llround(bottom);
placed = QRectF(
qreal(nearX) / dpr - origin.x(),
qreal(nearY) / dpr - origin.y(),
qreal(farX - nearX) / dpr,
qreal(farY - nearY) / dpr);
}
const QSizeF oldSize(m_item->width(), m_item->height());
const QSizeF newSize = placed.size();
m_item->setPosition(placed.topLeft());
if (newSize == oldSize) {
if (QQuickLayout* lay = qobject_cast<QQuickLayout*>(m_item)) {
if (lay->invalidatedArrangement()) lay->rearrange(newSize);
}
} else {
m_item->setSize(newSize);
}
}
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();
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 == ItemDevicePixelRatioHasChanged || change == ItemSceneChange)
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);
return d->engine.spacing(d->orientation, d->styleInfo);
}
void SnapLayout::setSpacing(qreal space) {
Q_D(SnapLayout);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal"
if (std::isnan(space) || spacing() == space) return;
#pragma GCC diagnostic pop
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical);
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;
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, this);
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);
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<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, 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<QQuickItem>& ancestor :
std::as_const(d->ancestorListeners)) {
if (ancestor)
QQuickItemPrivate::get(ancestor.data())
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
}
d->ancestorListeners.clear();
}
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);
d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo);
d->rearranging = false;
for (auto childItem : std::as_const(d->invalidateAfterRearrange))
invalidate(childItem);
d->invalidateAfterRearrange.clear();
}
} // namespace ZShell::components
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <QPointer>
#include <QtGui/private/qabstractlayoutstyleinfo_p.h>
#include <QtGui/private/qgridlayoutengine_p.h>
#include <QtQuickLayouts/private/qquickgridlayoutengine_p.h>
#include <QtQuickLayouts/private/qquicklayout_p.h>
#include <QtQuick/private/qquickitem_p.h>
#include <qqmlintegration.h>
namespace ZShell::components {
class SnapLayout;
class SnapLayoutPrivate;
class SnapLayoutStyleInfo : public QAbstractLayoutStyleInfo {
public:
qreal spacing(Qt::Orientation orientation) const override;
qreal windowMargin(Qt::Orientation orientation) const override;
bool hasChangedCore() const override;
};
class SnapGridLayoutItem : public QQuickGridLayoutItem {
public:
SnapGridLayoutItem(
QQuickItem* item,
int row,
int column,
int rowSpan,
int columnSpan,
Qt::Alignment alignment,
SnapLayout* layout);
void setGeometry(const QRectF& rect) override;
private:
SnapLayout* m_layout;
};
class SnapLayout : public QQuickLayout {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY
orientationChanged)
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
Q_PROPERTY(
Qt::LayoutDirection layoutDirection READ layoutDirection WRITE
setLayoutDirection NOTIFY layoutDirectionChanged)
Q_PROPERTY(
bool uniformCellSizes READ uniformCellSizes WRITE setUniformCellSizes
NOTIFY uniformCellSizesChanged)
public:
explicit SnapLayout(QQuickItem* parent = nullptr);
~SnapLayout() override;
void componentComplete() override;
QSizeF sizeHint(Qt::SizeHint whichSizeHint) const override;
void setAlignment(QQuickItem* item, Qt::Alignment align) override;
void setStretchFactor(
QQuickItem* item, int stretchFactor, Qt::Orientation orient) override;
void invalidate(QQuickItem* childItem = nullptr) override;
void updateLayoutItems() override;
QQuickItem* itemAt(int index) const override;
int itemCount() const override;
void rearrange(const QSizeF& size) override;
void itemChange(ItemChange change, const ItemChangeData& value) override;
void itemDestroyed(QQuickItem* item) override;
void itemVisibilityChanged(QQuickItem* item) override;
void itemGeometryChanged(
QQuickItem* item,
QQuickGeometryChange change,
const QRectF& oldGeometry) override;
Qt::Orientation orientation() const;
void setOrientation(Qt::Orientation orientation);
qreal spacing() const;
void setSpacing(qreal spacing);
Qt::LayoutDirection layoutDirection() const;
void setLayoutDirection(Qt::LayoutDirection direction);
Qt::LayoutDirection effectiveLayoutDirection() const;
bool uniformCellSizes() const;
void setUniformCellSizes(bool uniformCellSizes);
signals:
void orientationChanged();
void spacingChanged();
void layoutDirectionChanged();
void uniformCellSizesChanged();
private:
Q_DECLARE_PRIVATE(SnapLayout)
void insertLayoutItems();
void removeGridItem(QGridLayoutItem* gridItem);
void updateAncestorListeners();
void clearAncestorListeners();
};
class SnapLayoutPrivate : public QQuickLayoutPrivate {
Q_DECLARE_PUBLIC(SnapLayout)
public:
SnapLayoutPrivate() = default;
void mirrorChange() override;
QQuickGridLayoutEngine engine;
SnapLayoutStyleInfo* styleInfo = nullptr;
Qt::Orientation orientation = Qt::Horizontal;
Qt::LayoutDirection layoutDirection = Qt::LeftToRight;
bool rearranging = false;
int recurRearrangeCounter = 0;
QList<QQuickItem*> invalidateAfterRearrange;
QVector<QPointer<QQuickItem>> ancestorListeners;
qint64 originResidualX = -1;
qint64 originResidualY = -1;
};
} // namespace ZShell::components