aboutsummaryrefslogtreecommitdiffstats
path: root/src/labs/stylekit/impl/qqstylekitlayout.cpp
diff options
context:
space:
mode:
authorRichard Moe Gustavsen <richard.gustavsen@qt.io>2025-11-19 15:49:47 +0100
committerRichard Moe Gustavsen <richard.gustavsen@qt.io>2025-11-27 12:22:49 +0100
commitc056aaa055187af7290d363d5ef02a4948cd8813 (patch)
treed5df750ffefe1d9dbb366177587a9b151d113c78 /src/labs/stylekit/impl/qqstylekitlayout.cpp
parent8890b4a2d56ebdd51bf550d307f48d60ccf9a632 (diff)
StyleKit: add StyleKit to labs
This patch introduces a new styling API for Controls called StyleKit. StyleKit provides a higher-level, key–value–based approach to styling applications, serving as an alternative to working directly with the lower-level Templates API. The primary goal of StyleKit is to offer a unified API for styling both Controls and Widgets. The current Templates-based approach relies heavily on JavaScript, which makes it unsuitable for use with Widgets. This initial version supports Controls only; support for Widgets will be added in a future update. StyleKit is designed to make it easier for designers and UI developers to: - Focus on visual styling rather than Template logic (such as geometry, delegate positioning, and handle placement). - Allow style properties to propagate, enabling you to factor out common styling into shared control types and override only what differs in the more specific control types. - Style controls independently in each of their states, without needing nested ternary expressions to check state. - Define and apply multiple themes with minimal effort. - Provide different style variations depending on context. For example, styling a Switch differently when it appears inside a ToolBar. [ChangeLog][Qt labs] Introduced new QML module 'StyleKit'. StyleKit provides a flexible styling framework for Qt Quick Controls, enabling developers to define reusable styles and themes using a simple key-value property format. Change-Id: Iae25324486aea7a7b9b2ce52135327ec7e9b6f59 Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Diffstat (limited to 'src/labs/stylekit/impl/qqstylekitlayout.cpp')
-rw-r--r--src/labs/stylekit/impl/qqstylekitlayout.cpp538
1 files changed, 538 insertions, 0 deletions
diff --git a/src/labs/stylekit/impl/qqstylekitlayout.cpp b/src/labs/stylekit/impl/qqstylekitlayout.cpp
new file mode 100644
index 0000000000..f29bcc4c57
--- /dev/null
+++ b/src/labs/stylekit/impl/qqstylekitlayout.cpp
@@ -0,0 +1,538 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qqstylekitlayout_p.h"
+
+QT_BEGIN_NAMESPACE
+
+static qreal width(QQStyleKitLayoutItem *li, qreal availableWidth = .0)
+{
+ Q_ASSERT(li);
+ Q_ASSERT(li->item());
+ QQuickItem *item = li->item();
+ qreal w = item->implicitWidth();
+ if (li->fillWidth())
+ w = qMax(.0, availableWidth - li->margins().left() - li->margins().right());
+ return qMax(.0, w);
+}
+
+static qreal height(QQStyleKitLayoutItem *li, qreal availableHeight = .0)
+{
+ Q_ASSERT(li);
+ Q_ASSERT(li->item());
+ QQuickItem *item = li->item();
+ qreal h = item->implicitHeight();
+ if (li->fillHeight())
+ h = qMax(.0, availableHeight - li->margins().top() - li->margins().bottom());
+ return qMax(.0, h);
+}
+
+static qreal totalWidth(const QVector<QQStyleKitLayoutItem *> &items, qreal spacing)
+{
+ qreal total = .0;
+ for (QQStyleKitLayoutItem *li : items) {
+ if (li->item() && li->item()->isVisible())
+ total += width(li) + li->margins().left() + li->margins().right() + spacing;
+ }
+ return total;
+}
+
+static qreal totalHeight(const QVector<QQStyleKitLayoutItem *> &items)
+{
+ qreal maxHeight = .0;
+ for (QQStyleKitLayoutItem *li : items) {
+ if (li->item() && li->item()->isVisible()) {
+ const qreal h = height(li) + li->margins().top() + li->margins().bottom();
+ if (h > maxHeight)
+ maxHeight = h;
+ }
+ }
+ return maxHeight;
+}
+
+static qreal vAlignY(QQStyleKitLayoutItem *li, qreal containerY, qreal containerHeight)
+{
+ Q_ASSERT(li);
+ Q_ASSERT(li->item());
+
+ const auto itemHeight = height(li, containerHeight);
+ const auto vAlign = li->alignment() & Qt::AlignVertical_Mask;
+ const auto margins = li->margins();
+ switch (vAlign) {
+ case Qt::AlignTop:
+ return containerY + margins.top();
+ case Qt::AlignBottom:
+ return containerY + containerHeight - itemHeight - margins.bottom();
+ default: // AlignVCenter
+ return containerY + margins.top()
+ + (containerHeight - margins.top() - margins.bottom() - itemHeight) / 2.0;
+ }
+}
+
+QQStyleKitLayoutItem::QQStyleKitLayoutItem(QObject *parent)
+ : QObject(parent)
+{
+}
+
+QQuickItem *QQStyleKitLayoutItem::item() const
+{
+ return m_item;
+}
+
+void QQStyleKitLayoutItem::setItem(QQuickItem *item)
+{
+ if (m_item == item)
+ return;
+
+ if (m_item)
+ disconnect(m_item, nullptr, this, nullptr);
+
+ m_item = item;
+ if (m_item) {
+ connect(m_item, &QQuickItem::implicitWidthChanged, this, [this]() { emit itemChanged(); });
+ connect(m_item, &QQuickItem::implicitHeightChanged, this, [this]() { emit itemChanged(); });
+ connect(m_item, &QQuickItem::visibleChanged, this, [this]() { emit itemChanged(); });
+ }
+ // TODO: parentchanged
+ emit itemChanged();
+}
+
+qreal QQStyleKitLayoutItem::x() const
+{
+ return m_x;
+}
+
+void QQStyleKitLayoutItem::setX(qreal x)
+{
+ if (qFuzzyCompare(m_x, x))
+ return;
+
+ m_x = x;
+ emit xChanged();
+}
+
+qreal QQStyleKitLayoutItem::y() const
+{
+ return m_y;
+}
+
+void QQStyleKitLayoutItem::setY(qreal y)
+{
+ if (qFuzzyCompare(m_y, y))
+ return;
+
+ m_y = y;
+ emit yChanged();
+}
+
+qreal QQStyleKitLayoutItem::width() const
+{
+ return m_width;
+}
+
+void QQStyleKitLayoutItem::setWidth(qreal width)
+{
+ if (qFuzzyCompare(m_width, width))
+ return;
+
+ m_width = width;
+ emit widthChanged();
+}
+
+qreal QQStyleKitLayoutItem::height() const
+{
+ return m_height;
+}
+
+void QQStyleKitLayoutItem::setHeight(qreal height)
+{
+ if (qFuzzyCompare(m_height, height))
+ return;
+
+ m_height = height;
+ emit heightChanged();
+}
+
+Qt::Alignment QQStyleKitLayoutItem::alignment() const
+{
+ return m_alignment;
+}
+
+void QQStyleKitLayoutItem::setAlignment(Qt::Alignment alignment)
+{
+ if (m_alignment == alignment)
+ return;
+
+ m_alignment = alignment;
+ emit alignmentChanged();
+}
+
+QMarginsF QQStyleKitLayoutItem::margins() const
+{
+ return m_margins;
+}
+
+void QQStyleKitLayoutItem::setMargins(const QMarginsF &margins)
+{
+ if (m_margins == margins)
+ return;
+
+ m_margins = margins;
+ emit marginsChanged();
+}
+
+bool QQStyleKitLayoutItem::fillWidth() const
+{
+ return m_fillWidth;
+}
+
+void QQStyleKitLayoutItem::setFillWidth(bool fill)
+{
+ if (m_fillWidth == fill)
+ return;
+
+ m_fillWidth = fill;
+ emit fillWidthChanged();
+}
+
+bool QQStyleKitLayoutItem::fillHeight() const
+{
+ return m_fillHeight;
+}
+
+void QQStyleKitLayoutItem::setFillHeight(bool fill)
+{
+ if (m_fillHeight == fill)
+ return;
+
+ m_fillHeight = fill;
+ emit fillHeightChanged();
+}
+
+QQStyleKitLayout::QQStyleKitLayout(QObject *parent)
+ : QObject(parent)
+ , m_mirrored(false)
+ , m_enabled(true)
+ , m_updatingLayout(false)
+{
+ m_updateTimer.setSingleShot(true);
+ connect(&m_updateTimer, &QTimer::timeout, this, &QQStyleKitLayout::updateLayout);
+}
+
+QQuickItem *QQStyleKitLayout::container() const
+{
+ return m_container;
+}
+
+void QQStyleKitLayout::setContainer(QQuickItem *container)
+{
+ if (m_container == container)
+ return;
+
+ m_container = container;
+ emit containerChanged();
+ connect(m_container, &QQuickItem::widthChanged, this, &QQStyleKitLayout::scheduleUpdate);
+ connect(m_container, &QQuickItem::heightChanged, this, &QQStyleKitLayout::scheduleUpdate);
+
+ scheduleUpdate();
+}
+
+QQmlListProperty<QQStyleKitLayoutItem> QQStyleKitLayout::layoutItems()
+{
+ return QQmlListProperty<QQStyleKitLayoutItem>(const_cast<QQStyleKitLayout *>(this),
+ nullptr,
+ &QQStyleKitLayout::layoutItem_append,
+ &QQStyleKitLayout::layoutItem_count,
+ &QQStyleKitLayout::layoutItem_at,
+ &QQStyleKitLayout::layoutItem_clear);
+}
+
+void QQStyleKitLayout::layoutItem_append(QQmlListProperty<QQStyleKitLayoutItem> *list, QQStyleKitLayoutItem *item)
+{
+ QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object);
+ if (layout && item) {
+ layout->m_layoutItems.append(item);
+ connect(item, &QQStyleKitLayoutItem::itemChanged, layout, &QQStyleKitLayout::scheduleUpdate);
+ connect(item, &QQStyleKitLayoutItem::alignmentChanged, layout, &QQStyleKitLayout::scheduleUpdate);
+ connect(item, &QQStyleKitLayoutItem::marginsChanged, layout, &QQStyleKitLayout::scheduleUpdate);
+ connect(item, &QQStyleKitLayoutItem::fillWidthChanged, layout, &QQStyleKitLayout::scheduleUpdate);
+ connect(item, &QQStyleKitLayoutItem::fillHeightChanged, layout, &QQStyleKitLayout::scheduleUpdate);
+ emit layout->layoutItemsChanged();
+ layout->scheduleUpdate();
+ }
+}
+
+qsizetype QQStyleKitLayout::layoutItem_count(QQmlListProperty<QQStyleKitLayoutItem> *list)
+{
+ QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object);
+ if (layout)
+ return layout->m_layoutItems.size();
+ return 0;
+}
+
+QQStyleKitLayoutItem *QQStyleKitLayout::layoutItem_at(QQmlListProperty<QQStyleKitLayoutItem> *list, qsizetype index)
+{
+ QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object);
+ if (layout)
+ return layout->m_layoutItems.value(index);
+ return nullptr;
+}
+
+void QQStyleKitLayout::layoutItem_clear(QQmlListProperty<QQStyleKitLayoutItem> *list)
+{
+ QQStyleKitLayout *layout = qobject_cast<QQStyleKitLayout *>(list->object);
+ if (layout) {
+ layout->m_layoutItems.clear();
+ emit layout->layoutItemsChanged();
+ layout->scheduleUpdate();
+ }
+}
+
+QMarginsF QQStyleKitLayout::padding() const
+{
+ return m_padding;
+}
+
+QMarginsF QQStyleKitLayout::contentMargins() const
+{
+ return m_contentMargins;
+}
+
+void QQStyleKitLayout::setContentMargins(const QMarginsF &margins)
+{
+ if (m_contentMargins == margins)
+ return;
+
+ m_contentMargins = margins;
+ emit contentMarginsChanged();
+ scheduleUpdate();
+}
+
+qreal QQStyleKitLayout::spacing() const
+{
+ return m_spacing;
+}
+
+void QQStyleKitLayout::setSpacing(qreal spacing)
+{
+ if (qFuzzyCompare(m_spacing, spacing))
+ return;
+
+ m_spacing = spacing;
+ emit spacingChanged();
+ scheduleUpdate();
+}
+
+bool QQStyleKitLayout::isMirrored() const
+{
+ return m_mirrored;
+}
+
+void QQStyleKitLayout::setMirrored(bool mirrored)
+{
+ if (m_mirrored == mirrored)
+ return;
+
+ m_mirrored = mirrored;
+ emit mirroredChanged();
+ scheduleUpdate();
+}
+
+qreal QQStyleKitLayout::implicitWidth() const
+{
+ return m_implicitWidth;
+}
+
+qreal QQStyleKitLayout::implicitHeight() const
+{
+ return m_implicitHeight;
+}
+
+void QQStyleKitLayout::setImplicitWidth(qreal width)
+{
+ if (qFuzzyCompare(m_implicitWidth, width))
+ return;
+
+ m_implicitWidth = width;
+ emit implicitWidthChanged();
+ scheduleUpdate();
+}
+
+void QQStyleKitLayout::setImplicitHeight(qreal height)
+{
+ if (qFuzzyCompare(m_implicitHeight, height))
+ return;
+
+ m_implicitHeight = height;
+ emit implicitHeightChanged();
+ scheduleUpdate();
+}
+
+bool QQStyleKitLayout::isEnabled() const
+{
+ return m_enabled;
+}
+
+void QQStyleKitLayout::setEnabled(bool enabled)
+{
+ if (m_enabled == enabled)
+ return;
+
+ m_enabled = enabled;
+ emit enabledChanged();
+
+ if (m_enabled)
+ scheduleUpdate();
+}
+
+void QQStyleKitLayout::updateLayout()
+{
+ if (!m_enabled)
+ return;
+
+ if (!m_container || m_container->width() <= 0 || m_container->height() <= 0)
+ return;
+
+ if (m_updatingLayout)
+ return;
+ m_updatingLayout = true;
+
+ QVector<QQStyleKitLayoutItem *> left;
+ QVector<QQStyleKitLayoutItem *> right;
+ QVector<QQStyleKitLayoutItem *> center;
+
+ for (QQStyleKitLayoutItem *li : m_layoutItems) {
+ if (!li->item() || !li->item()->isVisible())
+ continue;
+ const auto hAlign = li->alignment() & Qt::AlignHorizontal_Mask;
+ const bool isMirrored = m_mirrored && !(hAlign & Qt::AlignAbsolute);
+ switch (hAlign) {
+ case Qt::AlignLeft:
+ if (isMirrored)
+ right.append(li);
+ else
+ left.append(li);
+ break;
+ case Qt::AlignRight:
+ if (isMirrored)
+ left.append(li);
+ else
+ right.append(li);
+ break;
+ default:
+ center.append(li);
+ break;
+ }
+ }
+
+ const qreal containerWidth = m_container->width() ? m_container->width() : m_container->implicitWidth();
+ const qreal containerHeight = m_container->height() ? m_container->height() : m_container->implicitHeight();
+ const qreal paddedX = m_contentMargins.left();
+ const qreal paddedY = m_contentMargins.top();
+ const qreal paddedWidth = qMax(containerWidth - m_contentMargins.left() - m_contentMargins.right(), .0);
+ const qreal paddedHeight = qMax(containerHeight - m_contentMargins.top() - m_contentMargins.bottom(), .0);
+
+ // Position left-aligned items
+ {
+ qreal x = paddedX;
+ for (QQStyleKitLayoutItem *li : left) {
+ QQuickItem *item = li->item();
+ if (!item || !item->isVisible())
+ continue;
+
+ const QMarginsF margins = li->margins();
+ const qreal itemWidth = width(li, paddedWidth);
+ const qreal itemHeight = height(li, paddedHeight);
+ auto y = vAlignY(li, paddedY, paddedHeight);
+ li->setX(x + margins.left());
+ li->setY(y);
+ li->setWidth(itemWidth);
+ li->setHeight(itemHeight);
+ x += itemWidth + margins.left() + margins.right() + m_spacing;
+ }
+ }
+
+ // Position right-aligned items
+ {
+ qreal x = paddedX + paddedWidth;
+ for (QQStyleKitLayoutItem *li : right) {
+ QQuickItem *item = li->item();
+ if (!item || !item->isVisible())
+ continue;
+
+ const QMarginsF margins = li->margins();
+ const qreal itemWidth = width(li, paddedWidth);
+ const qreal itemHeight = height(li, paddedHeight);
+ x -= itemWidth + margins.right() + margins.left();
+ auto y = vAlignY(li, paddedY, paddedHeight);
+ li->setX(x + margins.left());
+ li->setY(y);
+ li->setWidth(itemWidth);
+ li->setHeight(itemHeight);
+ x -= m_spacing;
+ }
+ }
+
+ // Position center-aligned items
+ {
+ qreal x = paddedX + (paddedWidth - totalWidth(center, m_spacing)) / 2;
+ for (QQStyleKitLayoutItem *li : center) {
+ QQuickItem *item = li->item();
+ if (!item || !item->isVisible())
+ continue;
+
+ const QMarginsF margins = li->margins();
+ const qreal itemWidth = width(li, paddedWidth);
+ const qreal itemHeight = height(li, paddedHeight);
+ auto y = vAlignY(li, paddedY, paddedHeight);
+ li->setX(x + margins.left());
+ li->setY(y);
+ li->setWidth(itemWidth);
+ li->setHeight(itemHeight);
+ x += itemWidth + margins.left() + margins.right() + m_spacing;
+ }
+ }
+
+ const auto leftWidth = totalWidth(left, m_spacing);
+ const auto leftHeight = totalHeight(left);
+ const auto rightWidth = totalWidth(right, m_spacing);
+ const auto rightHeight = totalHeight(right);
+ const auto centerWidth = totalWidth(center, m_spacing);
+ const auto centerHeight = totalHeight(center);
+
+ const auto implicitWidth = leftWidth + rightWidth + centerWidth
+ - m_spacing * (left.isEmpty() ? 0 : 1)
+ - m_spacing * (right.isEmpty() ? 0 : 1)
+ - m_spacing * (center.isEmpty() ? 0 : 1)
+ + m_contentMargins.left() + m_contentMargins.right();
+ setImplicitWidth(implicitWidth);
+ const auto implicitHeight = qMax(qMax(leftHeight, rightHeight), centerHeight)
+ + m_contentMargins.top() + m_contentMargins.bottom();
+ setImplicitHeight(implicitHeight);
+
+ // HACK for control's contentItem
+ // QQuickControl determines the contentItem geometry based on the control size and padding
+ // So we include the layout's left/right widths into the padding calculation
+ auto leftPadding = m_contentMargins.left() + leftWidth;
+ auto topPadding = m_contentMargins.top(); // TODO: support vertical layout items
+ auto rightPadding = m_contentMargins.right() + rightWidth;
+ auto bottomPadding = m_contentMargins.bottom(); // TODO: support vertical layout items
+ if (isMirrored())
+ std::swap(leftPadding, rightPadding);
+ QMarginsF newPadding = QMarginsF(leftPadding, topPadding, rightPadding, bottomPadding);
+ if (m_padding != newPadding) {
+ m_padding = newPadding;
+ emit paddingChanged();
+ }
+ m_updatingLayout = false;
+}
+
+void QQStyleKitLayout::scheduleUpdate()
+{
+ if (!m_updateTimer.isActive())
+ m_updateTimer.start(0);
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qqstylekitlayout_p.cpp"