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
+15 -5
View File
@@ -9,12 +9,19 @@ qml_module(ZShell
toaster.hpp toaster.cpp
qalculator.hpp qalculator.cpp
zutils.hpp zutils.cpp
alignaudit.hpp alignaudit.cpp
alignauditor.hpp alignauditor.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
Qt::DBus
Qt6::Gui
Qt6::Quick
Qt6::QuickPrivate
Qt6::QuickLayouts
Qt6::QuickLayoutsPrivate
Qt6::QmlPrivate
Qt6::CorePrivate
Qt6::Concurrent
Qt6::Sql
Qt6::DBus
PkgConfig::Qalculate
zshell-util
)
@@ -24,6 +31,9 @@ target_compile_definitions(ZShell PRIVATE
GIT_REVISION="${GIT_REVISION}"
)
target_compile_definitions(ZShell PRIVATE ALIGN_AUDIT_USE_QML_PRIVATE)
# Node macros expand to Q_OBJECT, automoc can't see it otherwise
list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
CONFIG_NODE
+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
+6 -6
View File
@@ -137,12 +137,12 @@ class FontTokens : public QObject {
Q_OBJECT
QML_ANONYMOUS
Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL)
Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL)
Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL)
Q_PROPERTY(FontStyle* label READ label CONSTANT FINAL)
Q_PROPERTY(FontStyle* mono READ mono CONSTANT FINAL)
Q_PROPERTY(IconFontStyle* icon READ icon CONSTANT FINAL)
Q_PROPERTY(ZShell::config::FontStyle* headline READ headline CONSTANT FINAL)
Q_PROPERTY(ZShell::config::FontStyle* title READ title CONSTANT FINAL)
Q_PROPERTY(ZShell::config::FontStyle* body READ body CONSTANT FINAL)
Q_PROPERTY(ZShell::config::FontStyle* label READ label CONSTANT FINAL)
Q_PROPERTY(ZShell::config::FontStyle* mono READ mono CONSTANT FINAL)
Q_PROPERTY(ZShell::config::IconFontStyle* icon READ icon CONSTANT FINAL)
Q_PROPERTY(
ZShell::config::FontBuilder clock READ clock NOTIFY clockChanged FINAL)
Q_PROPERTY(QString workspaces READ workspaces NOTIFY workspacesChanged FINAL)
-10
View File
@@ -21,15 +21,6 @@ class LauncherActionEntry : public settings::ObjectNode {
CONFIG_LIST_TYPE(LauncherActionEntry, LauncherActionList)
class LauncherSizes : public settings::ObjectNode {
CONFIG_NODE(LauncherSizes, settings::ObjectNode)
CONFIG_PROPERTY(int, itemHeight, 50)
CONFIG_PROPERTY(int, itemWidth, 600)
CONFIG_PROPERTY(int, wallpaperHeight, 200)
CONFIG_PROPERTY(int, wallpaperWidth, 280)
};
class UseFuzzy : public settings::ObjectNode {
CONFIG_NODE(UseFuzzy, settings::ObjectNode)
@@ -127,7 +118,6 @@ class Launcher : public settings::ObjectNode {
CONFIG_PROPERTY(QStringList, hiddenApps, {})
CONFIG_PROPERTY(int, maxAppsShown, 18)
CONFIG_PROPERTY(int, maxWallpapers, 7)
CONFIG_SUBOBJECT(LauncherSizes, sizes)
CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
CONFIG_SUBOBJECT(UseFuzzy, useFuzzy)
CONFIG_PROPERTY(bool, uwsm, true)
+10
View File
@@ -164,11 +164,21 @@ class DashboardTokens : public settings::ObjectNode {
CONFIG_PROPERTY(int, weatherWidth, 250)
};
class LauncherTokens : public settings::ObjectNode {
CONFIG_NODE(LauncherTokens, settings::ObjectNode)
CONFIG_PROPERTY(int, itemHeight, 50)
CONFIG_PROPERTY(int, itemWidth, 600)
CONFIG_PROPERTY(int, wallpaperHeight, 200)
CONFIG_PROPERTY(int, wallpaperWidth, 280)
};
class SizeTokens : public settings::ObjectNode {
CONFIG_NODE(SizeTokens, settings::ObjectNode)
CONFIG_SUBOBJECT(DashboardTokens, dashboard)
CONFIG_SUBOBJECT(BarTokens, bar)
CONFIG_SUBOBJECT(LauncherTokens, launcher)
};
} // namespace ZShell::config
+206
View File
@@ -0,0 +1,206 @@
#include "alignaudit.hpp"
#include <QDateTime>
#include <QFile>
#include <QGuiApplication>
#include <QQmlContext>
#include <QRegularExpression>
#include <QScreen>
#include <QTextStream>
#include <QTimer>
#include <QQuickItem>
#include <QQuickWindow>
#include <cmath>
#include <functional>
#include <qobject.h>
#ifdef ALIGN_AUDIT_USE_QML_PRIVATE
#include <private/qqmldata_p.h>
#endif
namespace ZShell {
namespace {
bool isOff(double v, double tol) {
return std::abs(v - std::round(v)) > tol;
}
QString num(double v, bool off) {
return QString::number(v, 'f', 2) + (off ? QStringLiteral("*") : QString());
}
QString typeName(const QQuickItem* item) {
static const QRegularExpression suffix(QStringLiteral("_QML(TYPE)?_\\d+$"));
QString name = QString::fromUtf8(item->metaObject()->className());
name.remove(suffix);
if (name.startsWith(QLatin1String("QQuick"))) {
name.remove(0, 6);
}
return name;
}
QString label(const QQuickItem* item) {
QString s = typeName(item);
if (!item->objectName().isEmpty()) {
s += QLatin1Char('#') + item->objectName();
}
return s;
}
QString ancestry(const QQuickItem* item, int maxDepth = 8) {
QStringList parts;
for (const QQuickItem* i = item; i && parts.size() < maxDepth;
i = i->parentItem()) {
parts.prepend(label(i));
}
return parts.join(QLatin1String(" > "));
}
struct Source {
QString file;
int line = 0;
int column = 0;
};
Source sourceOf(QQuickItem* item) {
Source s;
if (const QQmlContext* ctx = qmlContext(item)) {
const QUrl url = ctx->baseUrl();
s.file = url.isLocalFile() ? url.toLocalFile() : url.toString();
}
#ifdef ALIGN_AUDIT_USE_QML_PRIVATE
if (const QQmlData* d = QQmlData::get(item, false)) {
s.line = int(d->lineNumber);
s.column = int(d->columnNumber);
}
#endif
return s;
}
void visit(QQuickItem* item, const std::function<void(QQuickItem*)>& fn) {
if (!item->isVisible() || item->opacity() <= 0.0) {
return; // whole subtree is invisible
}
fn(item);
for (QQuickItem* child : item->childItems()) {
visit(child, fn);
}
}
int auditWindow(QQuickWindow* window, const Options& o, QTextStream& out) {
const double dpr = window->effectiveDevicePixelRatio();
const double tol = o.tolerance;
int count = 0;
visit(window->contentItem(), [&](QQuickItem* item) {
if (item->width() <= 0 || item->height() <= 0) {
return;
}
if (o.paintedOnly &&
!item->flags().testFlag(QQuickItem::ItemHasContents) &&
!item->clip()) {
return;
}
const bool isText = item->inherits("QQuickText");
const QRectF r =
item->mapRectToScene(QRectF(0, 0, item->width(), item->height()));
const double l = r.left() * dpr, t = r.top() * dpr;
const double rt = r.right() * dpr, b = r.bottom() * dpr;
const bool offL = isOff(l, tol), offT = isOff(t, tol);
const bool offR = !isText && isOff(rt, tol);
const bool offB = !isText && isOff(b, tol);
if (!(offL || offT || offR || offB)) {
return;
}
const bool own = isOff(item->x() * dpr, tol) ||
isOff(item->y() * dpr, tol) ||
(!isText && (isOff(item->width() * dpr, tol) ||
isOff(item->height() * dpr, tol)));
if (!own && !o.includeInherited) {
return;
}
const Source src = sourceOf(item);
out << src.file;
if (src.line > 0) {
out << ':' << src.line << ':' << src.column;
}
out << ": " << label(item) << " unaligned ("
<< (own ? "own" : "inherited") << ")\n"
<< " device px: left=" << num(l, offL) << " top=" << num(t, offT)
<< " right=" << num(rt, offR) << " bottom=" << num(b, offB) << "\n"
<< " local: x=" << item->x() << " y=" << item->y()
<< " w=" << item->width() << " h=" << item->height() << "\n"
<< " path: " << ancestry(item) << "\n";
++count;
});
return count;
}
} // namespace
struct ScreenReport {
double dpr = 1.0;
int windows = 0;
int count = 0;
QString body;
};
int run(const Options& options) {
QMap<QString, ScreenReport> reports;
for (QWindow* w : QGuiApplication::allWindows()) {
auto* qw = qobject_cast<QQuickWindow*>(w);
if (!qw || !qw->isVisible() || !qw->contentItem()) {
continue;
}
const QString name = qw->screen() ? qw->screen()->name()
: QStringLiteral("?");
ScreenReport& report = reports[name];
report.dpr = qw->effectiveDevicePixelRatio();
++report.windows;
QString windowBody;
QTextStream stream(&windowBody);
report.count += auditWindow(qw, options, stream);
stream.flush();
report.body += windowBody;
}
QFile file(options.outputPath);
if (!file.open(
QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
qWarning() << "AlignAudit: cannot open" << options.outputPath;
return -1;
}
QTextStream out(&file);
int total = 0;
for (const ScreenReport& r : std::as_const(reports)) {
total += r.count;
}
out << "# align audit "
<< QDateTime::currentDateTime().toString(Qt::ISODate)
<< " ('*' marks the off edges, values are device pixels)\n"
<< "# total: " << total << "\n\n";
for (auto it = reports.cbegin(); it != reports.cend(); ++it) {
out << "# screen " << it.key() << " dpr=" << it.value().dpr
<< " windows=" << it.value().windows
<< " offenders=" << it.value().count << "\n"
<< it.value().body << "\n";
}
return total;
}
void runLater(int delayMs, const Options& options) {
QTimer::singleShot(delayMs, qApp, [options] { run(options); });
}
} // namespace ZShell
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QString>
#include <qtmetamacros.h>
namespace ZShell {
struct Options {
QString outputPath = QStringLiteral("/tmp/align-audit.txt");
bool paintedOnly = true; // skip items that draw nothing
bool includeInherited =
false; // also report items only off because an ancestor is
double tolerance = 1e-3; // in device pixels
};
Q_INVOKABLE void runLater(int delayMs, const Options& options = {});
int run(const Options& options = {});
} // namespace ZShell
+26
View File
@@ -0,0 +1,26 @@
#include "alignauditor.hpp"
#include <QTimer>
namespace ZShell {
Options AlignAuditor::options() const {
Options o;
o.outputPath = m_outputPath;
o.paintedOnly = m_paintedOnly;
o.includeInherited = m_includeInherited;
o.tolerance = m_tolerance;
return o;
}
int AlignAuditor::run() {
m_lastCount = ZShell::run(options());
emit finished(m_lastCount);
return m_lastCount;
}
void AlignAuditor::runLater() {
QTimer::singleShot(m_delayMs, this, [this] { run(); });
}
} // namespace ZShell
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <QObject>
#include <QString>
#include <QtQml/qqmlregistration.h>
#include "alignaudit.hpp"
namespace ZShell {
class AlignAuditor : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString outputPath MEMBER m_outputPath NOTIFY outputPathChanged)
Q_PROPERTY(int delayMs MEMBER m_delayMs NOTIFY delayMsChanged)
Q_PROPERTY(bool paintedOnly MEMBER m_paintedOnly NOTIFY paintedOnlyChanged)
Q_PROPERTY(
bool includeInherited MEMBER m_includeInherited NOTIFY
includeInheritedChanged)
Q_PROPERTY(double tolerance MEMBER m_tolerance NOTIFY toleranceChanged)
Q_PROPERTY(int lastCount READ lastCount NOTIFY finished)
public:
explicit AlignAuditor(QObject* parent = nullptr) : QObject(parent) {}
Q_INVOKABLE int run();
Q_INVOKABLE void runLater();
int lastCount() const { return m_lastCount; }
signals:
void outputPathChanged();
void delayMsChanged();
void paintedOnlyChanged();
void includeInheritedChanged();
void toleranceChanged();
void finished(int count);
private:
Options options() const;
QString m_outputPath = QStringLiteral("/tmp/align-audit.txt");
int m_delayMs = 5000;
bool m_paintedOnly = true;
bool m_includeInherited = false;
double m_tolerance = 1e-3;
int m_lastCount = 0;
};
} // namespace ZShell
+424
View File
@@ -1,8 +1,14 @@
#include "zutils.hpp"
#include <QtConcurrent/qtconcurrentrun.h>
#include <QtQuick/private/qquickanchors_p_p.h>
#include <QtQuickLayouts/private/qquicklayout_p.h>
#include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h>
#include <QQmlContext>
#include <QQmlExpression>
#include <QDebug>
#include <QUrl>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qdiriterator.h>
@@ -337,4 +343,422 @@ QStringList ZUtils::listFiles(const QString& dir, const QString& suffix) {
return out;
}
qreal ZUtils::dprFor(QQuickItem* item) {
if (item) {
if (const QQuickWindow* window = item->window()) {
return window->effectiveDevicePixelRatio();
}
}
if (const QScreen* screen = QGuiApplication::primaryScreen()) {
return screen->devicePixelRatio();
}
return 1.0;
}
namespace {
// Per-axis centering detected from the item's own state. Anchor lines are
// Q_GADGET values with no properties, so centering is read from the anchors'
// used-anchor bitmask instead of by evaluating `anchors.verticalCenter`.
// Layout alignment comes from the attached Layout object; an item with no
// attached object uses Qt's default (AlignLeft | AlignVCenter).
struct Centering {
bool horizontal = false;
bool vertical = false;
// anchors.alignWhenCentered: false makes Qt round half-sizes in logical px
// (y = qRound(parentSize/2) - qRound(size/2)) instead of dividing exactly.
bool alignWhenCentered = true;
QString source;
};
Centering detectCentering(QQuickItem* item) {
Centering c;
QQuickAnchors* anchors = QQuickItemPrivate::get(item)->_anchors;
if (anchors) {
const QQuickAnchorsPrivate* ad = QQuickAnchorsPrivate::get(anchors);
c.alignWhenCentered = ad->centerAligned;
if (ad->usedAnchors & QQuickAnchors::HCenterAnchor) {
c.horizontal = true;
c.source = QStringLiteral("hCenter");
}
if (ad->usedAnchors & QQuickAnchors::VCenterAnchor) {
c.vertical = true;
c.source += (c.source.isEmpty() ? "" : " ") + QStringLiteral("vCenter");
}
}
QQuickItem* parent = item->parentItem();
if (parent && parent->inherits("QQuickLayout")) {
QQuickLayoutAttached* info = attachedLayoutObject(item, false);
const Qt::Alignment flags = info ? info->alignment() : Qt::Alignment();
// Filling an axis means the layout stretches that axis, so no centering.
if (info && !info->fillWidth() && (flags & Qt::AlignHCenter)) {
c.horizontal = true;
c.source += (c.source.isEmpty() ? "" : " ") +
QStringLiteral("Layout.HCenter");
}
const bool defaultVCenter = flags == 0 || (flags & Qt::AlignVCenter);
if ((!info || !info->fillHeight()) && defaultVCenter &&
!(flags & (Qt::AlignTop | Qt::AlignBottom))) {
c.vertical = true;
c.source += (c.source.isEmpty() ? "" : " ") +
QStringLiteral("Layout.VCenter");
}
}
return c;
}
// Parity (mod 2) of an item's whole-device-pixel size; no item = even.
int devParity(const QQuickItem* it, qreal dpr, bool horizontal) {
if (!it)
return 0;
const qint64 dev = std::llround((horizontal ? it->width() : it->height()) * dpr);
return static_cast<int>(dev % 2);
}
// Snap a size to a whole device-pixel count whose parity matches `parity`
// (the parent's device-size parity). An item centered by *exact* division
// (Layout alignment, or anchors with alignWhenCentered: true) lands on whole
// device pixels exactly when its device size shares the parent's parity,
// since edges = parentLeft + (parentSize - size) / 2.
qreal snapToParity(qreal value, qreal dpr, int parity, int mode) {
const double exact = value * dpr;
qint64 n = mode > 0 ? std::llround(std::ceil(exact - 1e-6))
: mode < 0 ? std::llround(std::floor(exact + 1e-6))
: std::llround(exact);
if (static_cast<int>(n % 2) != parity) {
if (mode > 0) {
++n;
} else if (mode < 0) {
--n;
} else {
// nearest: prefer the side closer to exact; exact ties go up
n = (exact >= static_cast<double>(n)) ? n + 1 : n - 1;
}
}
if (n < 0)
n = 0;
return static_cast<qreal>(n) / dpr;
}
// qRound() semantics: round half away from zero.
qint64 roundAway(double value) {
return value >= 0.0 ? qint64(value + 0.5) : qint64(value - 0.5);
}
// Snap a size for an item centered by anchors with `alignWhenCentered: false`.
// Qt resolves that as `y = qRound(parentSize / 2) + parentPos - size / 2`
// (qquickanchors.cpp: the *anchor target* rounds its half, the anchored item
// divides exactly), so with the parent's half at a whole device offset the
// child's device size must be even; if the parent's half sits on a half device
// pixel, the child's device size must be odd instead.
qreal snapCenteredSize(qreal value, qreal dpr, qint64 parentHalfRounded, int mode) {
const double exact = value * dpr;
const double anchorDev = double(parentHalfRounded) * dpr;
const bool anchorWhole =
std::fabs(anchorDev - std::round(anchorDev)) < 1e-6;
const qint64 parity = anchorWhole ? 0 : 1;
qint64 base = mode > 0 ? std::llround(std::ceil(exact - 1e-6))
: mode < 0 ? std::llround(std::floor(exact + 1e-6))
: std::llround(exact);
for (qint64 step = 0; step < 8; ++step) {
if (base + step >= 0 && (base + step) % 2 == parity)
return qreal(base + step) / dpr;
if (step > 0 && base - step >= 0 && (base - step) % 2 == parity)
return qreal(base - step) / dpr;
}
return qreal(std::max<qint64>(0, base)) / dpr;
}
// Snap a size along `horizontal` for an item whose placement on that axis is
// centered. Anchor-centered items follow Qt's rounding (`alignWhenCentered`);
// anything else only needs the parent's device-size parity.
qreal snapCenteredAxis(
qreal value,
QQuickItem* item,
qreal dpr,
bool horizontal,
const Centering& centering,
int mode) {
QQuickItem* parent = item->parentItem();
if (!centering.alignWhenCentered && parent) {
const qreal parentSize = horizontal ? parent->width() : parent->height();
return snapCenteredSize(value, dpr, roundAway(parentSize / 2.0), mode);
}
return snapToParity(value, dpr, devParity(parent, dpr, horizontal), mode);
}
} // namespace
qreal ZUtils::snap(qreal value, QQuickItem* item) {
const qreal dpr = dprFor(item);
if (item) {
const Centering c = detectCentering(item);
if (c.horizontal)
return snapCenteredAxis(value, item, dpr, true, c, 0);
if (c.vertical)
return snapCenteredAxis(value, item, dpr, false, c, 0);
}
return std::round(value * dpr) / dpr;
}
qreal ZUtils::snapCeil(qreal value, QQuickItem* item) {
const qreal dpr = dprFor(item);
if (item) {
const Centering c = detectCentering(item);
if (c.horizontal)
return snapCenteredAxis(value, item, dpr, true, c, 1);
if (c.vertical)
return snapCenteredAxis(value, item, dpr, false, c, 1);
}
return std::ceil(value * dpr - 1e-6) / dpr;
}
qreal ZUtils::snapFloor(qreal value, QQuickItem* item) {
const qreal dpr = dprFor(item);
if (item) {
const Centering c = detectCentering(item);
if (c.horizontal)
return snapCenteredAxis(value, item, dpr, true, c, -1);
if (c.vertical)
return snapCenteredAxis(value, item, dpr, false, c, -1);
}
return std::floor(value * dpr + 1e-6) / dpr;
}
bool ZUtils::logPlacement(QQuickItem* item, const QString& tag) {
if (!item) {
qWarning() << "logPlacement: null item";
return false;
}
constexpr double tol = 1e-3;
const auto isOff = [](double v) {
return std::abs(v - std::round(v)) > tol;
};
const auto fmt = [&](double v) {
return QString::number(v, 'f', 2) +
(isOff(v) ? QStringLiteral("*") : QString());
};
// Centering is detected from the item: anchors.centerIn, the individual
// center anchor lines, and Layout alignment.
const Centering c = detectCentering(item);
const bool centeredH = c.horizontal;
const bool centeredV = c.vertical;
const QString& centerSrc = c.source;
const double dpr = dprFor(item);
const QRectF r =
item->mapRectToScene(QRectF(0, 0, item->width(), item->height()));
const double l = r.left() * dpr;
const double t = r.top() * dpr;
const double rt = r.right() * dpr;
const double b = r.bottom() * dpr;
// Same rule as the audit: a Text's fractional implicit size doesn't blur anything.
const bool isText = item->inherits("QQuickText");
// Centered rule per axis: whole device size, and the center on the grid
// with parity matching that size (even size -> integer center, odd ->
// pixel center at .5). That is what makes both edges land on whole device
// pixels when the center is divided exactly (`alignWhenCentered: true`).
// With `alignWhenCentered: false` Qt rounds the anchor target's half
// instead of dividing, so the edges are what matter.
const auto centeredAxisOk = [&](double lo, double hi) {
const double size = hi - lo;
if (isOff(size))
return false;
if (!c.alignWhenCentered)
return !isOff(lo) && !isOff(hi);
const double center = (lo + hi) / 2.0;
const double parity = (std::llround(size) % 2 == 0) ? 0.0 : 0.5;
double err = std::fmod(center - parity, 1.0);
if (err < 0)
err += 1.0;
return std::min(err, 1.0 - err) <= tol;
};
bool aligned;
if (centeredH && centeredV) {
aligned = centeredAxisOk(l, rt) && (isText || centeredAxisOk(t, b));
} else if (centeredH) {
aligned = centeredAxisOk(l, rt) && !isOff(t) && (isText || !isOff(b));
} else if (centeredV) {
aligned = !isOff(l) && centeredAxisOk(t, b) && (isText || !isOff(rt));
} else {
aligned = !isOff(l) && !isOff(t) &&
(isText || (!isOff(rt) && !isOff(b)));
}
QString file;
if (const QQmlContext* ctx = qmlContext(item)) {
const QUrl url = ctx->baseUrl();
file = url.isLocalFile() ? url.toLocalFile() : url.toString();
}
qInfo().noquote()
<< (aligned ? "[ALIGNED] " : "[UNALIGNED]")
<< (tag.isEmpty() ? QString::fromUtf8(item->metaObject()->className())
: tag)
<< "\n device px: left=" << fmt(l) << " top=" << fmt(t)
<< " right=" << fmt(rt) << " bottom=" << fmt(b)
<< (isText ? " (right/bottom ignored: Text)" : "")
<< (!centerSrc.isEmpty()
? " (centered: " + centerSrc + ")"
: QString())
<< (centeredH || centeredV
? QString("\n center: x=%1 y=%2 size=%3 x %4")
.arg(QString::number((l + rt) / 2, 'f', 2))
.arg(QString::number((t + b) / 2, 'f', 2))
.arg(QString::number(rt - l, 'f', 2))
.arg(QString::number(b - t, 'f', 2))
: QString())
<< "\n size: " << fmt(item->width() * dpr) << " x "
<< fmt(item->height() * dpr) << "\n local: x=" << item->x()
<< " y=" << item->y() << " w=" << item->width()
<< " h=" << item->height() << "\n dpr=" << dpr
<< (item->window() ? "" : " (NO WINDOW, using fallback dpr)")
<< (file.isEmpty() ? QString() : "\n file: " + file);
return aligned;
}
bool ZUtils::logCurvePlacement(
QQuickItem* item,
const QList<qreal>& samples,
qreal lineWidth,
const QString& tag) {
if (!item) {
qWarning() << "logCurvePlacement: null item";
return false;
}
if (samples.size() < 4 || samples.size() % 2 != 0) {
qWarning() << "logCurvePlacement: need flat x,y pairs (>= 2 points)";
return false;
}
constexpr double tol = 1e-3;
const double dpr = dprFor(item);
const int devW = static_cast<int>(
std::max<qint64>(1, std::llround(lineWidth * dpr)));
const double half = devW / 2.0;
qsizetype offCount = 0;
qsizetype sloped = 0;
double worst = 0.0;
double worstX = 0.0;
double worstY = 0.0;
double worstEdge = 0.0;
int worstSide = 0; // -1 top edge, +1 bottom edge
for (qsizetype i = 0; i < samples.size(); i += 2) {
const double x = samples[i];
const double y = samples[i + 1];
// A sloped stroke's edge is never horizontal in device space, so
// only locally-flat samples can (and must) sit on pixel boundaries
const double yPrev = i >= 2 ? samples[i - 1] : y;
const double yNext = i + 3 < samples.size() ? samples[i + 3] : y;
if (std::abs(yNext - yPrev) > tol) {
++sloped;
continue;
}
const double yc = y * dpr;
const double top = yc - half;
const double bottom = yc + half;
for (int side = -1; side <= 1; side += 2) {
const double edge = side < 0 ? top : bottom;
const double dev = std::abs(edge - std::round(edge));
if (dev > tol) {
++offCount;
if (dev > worst) {
worst = dev;
worstX = x;
worstY = y;
worstEdge = edge;
worstSide = side;
}
}
}
}
const qsizetype total = samples.size() / 2;
const qsizetype flat = total - sloped;
const bool aligned = offCount == 0;
qInfo().noquote()
<< (aligned ? "[CURVE ALIGNED] " : "[CURVE UNALIGNED]")
<< (tag.isEmpty() ? QString::fromUtf8(item->metaObject()->className())
: tag)
<< "\n samples: " << total << " points (" << flat << " flat, "
<< sloped << " sloped ignored), dpr=" << dpr
<< ", stroke width=" << lineWidth << " logical = " << devW
<< " device px"
<< "\n edges: " << offCount << " of " << flat * 2
<< " flat stroke edges off the device grid";
if (!aligned) {
qInfo().noquote()
<< " worst: deviation=" << QString::number(worst, 'f', 3)
<< " device px at logical x=" << QString::number(worstX, 'f', 2)
<< " y=" << QString::number(worstY, 'f', 2)
<< " (" << (worstSide < 0 ? "top" : "bottom") << " edge device="
<< QString::number(worstEdge, 'f', 3) << ")";
}
return aligned;
}
QList<qreal> ZUtils::distribute(
qreal total, const QList<qreal>& preferred, QQuickItem* item) {
const qsizetype n = preferred.size();
QList<qreal> out(n, 0.0);
if (n == 0) {
return out;
}
const qreal dpr = dprFor(item);
const qint64 totalPx = std::max<qint64>(0, std::llround(total * dpr));
QList<qint64> px;
px.reserve(n);
qint64 sum = 0;
for (const qreal p : preferred) {
const qint64 v =
std::max<qint64>(0, std::llround(std::ceil(p * dpr - 1e-6)));
px.append(v);
sum += v;
}
const qint64 left = totalPx - sum;
if (left >= 0) {
// Extra space: split evenly, remainder pixels to the first items.
const qint64 base = left / n;
const qint64 rem = left % n;
for (qsizetype i = 0; i < n; ++i) {
px[i] += base + (i < rem ? 1 : 0);
}
} else if (sum > 0) {
// Not enough space: shrink proportionally, then hand out the rounding leftovers.
qint64 assigned = 0;
for (qsizetype i = 0; i < n; ++i) {
px[i] = static_cast<qint64>(std::floor(
static_cast<double>(px[i]) * static_cast<double>(totalPx) /
static_cast<double>(sum)));
assigned += px[i];
}
for (qsizetype i = 0; assigned < totalPx; i = (i + 1) % n, ++assigned) {
++px[i];
}
}
for (qsizetype i = 0; i < n; ++i) {
out[i] = static_cast<qreal>(px[i]) / dpr;
}
return out;
}
} // namespace ZShell
+38
View File
@@ -53,6 +53,44 @@ class ZUtils : public QObject {
Q_INVOKABLE static QList<QQuickItem*> findChildrenMatching(
QQuickItem* root, const QString& pattern);
// Device pixel ratio of the window the item lives in (falls back to the primary screen, then 1.0)
Q_INVOKABLE static qreal dprFor(QQuickItem* item);
// Snap a logical value to the device pixel grid of the item's window.
// Centering is detected from the item: anchors.centerIn,
// anchors.horizontalCenter / verticalCenter, and Layout alignment. A size
// along a centered axis is snapped so the centered edges land on whole
// device pixels — using Qt's own rounding rule for that anchor style
// (exact halves when `alignWhenCentered` is true, the rounded half of the
// anchor target when it is false).
Q_INVOKABLE static qreal snap(qreal value, QQuickItem* item);
Q_INVOKABLE static qreal snapCeil(qreal value, QQuickItem* item); // text-derived sizes
Q_INVOKABLE static qreal snapFloor(qreal value, QQuickItem* item);
// Logs where an item sits in device pixels and whether it is aligned.
// Returns true if aligned. `tag` is an optional label for the log line.
// Items centered on a target are judged by the centering rule above
// (whole device size, plus the offset the anchor produces), which is
// detected from the item's anchors and Layout alignment.
Q_INVOKABLE static bool logPlacement(
QQuickItem* item,
const QString& tag = QString());
// Walks a stroked curve path (flat x,y pairs in the item's logical coords,
// every point along the curve, not just its defined points) and checks
// whether both stroke edges land on device pixel boundaries at `lineWidth`.
// Returns true if every sample is aligned.
Q_INVOKABLE static bool logCurvePlacement(
QQuickItem* item,
const QList<qreal>& samples,
qreal lineWidth,
const QString& tag = QString());
// Distributes `total` (logical px) across items with the given preferred widths.
// Every result is a whole number of device pixels and the results sum exactly to
// the snapped total. If the preferred widths don't fit, they shrink proportionally.
Q_INVOKABLE static QList<qreal> distribute(
qreal total, const QList<qreal>& preferred, QQuickItem* item);
[[nodiscard]] QString version() const;
[[nodiscard]] QString qtVersion() const;
};