From ead9141aca2e8f634dd6518c6fbe111ab961bd9b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 12 May 2025 13:53:07 -0700 Subject: [PATCH] widgets/wrapper: add distinct top/bottom/left/right margins --- src/widgets/ClippingWrapperRectangle.qml | 31 +++++-- src/widgets/WrapperItem.qml | 27 +++++- src/widgets/WrapperRectangle.qml | 38 +++++++-- src/widgets/marginwrapper.cpp | 84 +++++++++++++------ src/widgets/marginwrapper.hpp | 101 +++++++++++++++++++++-- 5 files changed, 230 insertions(+), 51 deletions(-) diff --git a/src/widgets/ClippingWrapperRectangle.qml b/src/widgets/ClippingWrapperRectangle.qml index 26014a68..368c3c43 100644 --- a/src/widgets/ClippingWrapperRectangle.qml +++ b/src/widgets/ClippingWrapperRectangle.qml @@ -5,7 +5,7 @@ import QtQuick /// a child item. If you don't need clipping, use @@WrapperRectangle. /// /// > [!NOTE] ClippingWrapperRectangle is a @@MarginWrapperManager based component. -/// > You should read its documentation as well. +/// > See its documentation for information on how margins and sizes are calculated. /// /// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, /// > @@Item.height or @@Item.anchors on the child item, as they are used @@ -14,11 +14,32 @@ import QtQuick ClippingRectangle { id: root - /// The minimum margin between the child item and the ClippingWrapperRectangle's - /// edges. Defaults to 0. + /// The default for @@topMargin, @@bottomMargin, @@leftMargin and @@rightMargin. + /// Defaults to 0. property /*real*/alias margin: manager.margin - /// If the child item should be resized larger than its implicit size if - /// the WrapperRectangle is resized larger than its implicit size. Defaults to false. + /// An extra margin applied in addition to @@topMargin, @@bottomMargin, + /// @@leftMargin, and @@rightMargin. + /// If @@contentInsideBorder is true, the rectangle's border width will be added + /// to this property. Defaults to 0. + property /*real*/alias extraMargin: manager.extraMargin + /// The requested top margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias topMargin: manager.topMargin + /// The requested bottom margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias bottomMargin: manager.bottomMargin + /// The requested left margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias leftMargin: manager.leftMargin + /// The requested right margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias rightMargin: manager.rightMargin + /// Determines if child item should be resized larger than its implicit size if + /// the parent is resized larger than its implicit size. Defaults to false. property /*bool*/alias resizeChild: manager.resizeChild /// See @@WrapperManager.child for details. property alias child: manager.child diff --git a/src/widgets/WrapperItem.qml b/src/widgets/WrapperItem.qml index e1701cac..90c62351 100644 --- a/src/widgets/WrapperItem.qml +++ b/src/widgets/WrapperItem.qml @@ -6,7 +6,7 @@ import QtQuick /// for positioning multiple items. /// /// > [!NOTE] WrapperItem is a @@MarginWrapperManager based component. -/// > You should read its documentation as well. +/// > See its documentation for information on how margins and sizes are calculated. /// /// ### Example: Adding a margin to an item /// The snippet below adds a 10px margin to all sides of the @@QtQuick.Text item. @@ -30,11 +30,30 @@ import QtQuick /// /// [QtQuick.Layouts]: https://doc.qt.io/qt-6/qtquicklayouts-index.html Item { - /// The minimum margin between the child item and the WrapperItem's edges. + /// The default for @@topMargin, @@bottomMargin, @@leftMargin and @@rightMargin. /// Defaults to 0. property /*real*/alias margin: manager.margin - /// If the child item should be resized larger than its implicit size if - /// the WrapperItem is resized larger than its implicit size. Defaults to false. + /// An extra margin applied in addition to @@topMargin, @@bottomMargin, + /// @@leftMargin, and @@rightMargin. Defaults to 0. + property /*real*/alias extraMargin: manager.extraMargin + /// The requested top margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias topMargin: manager.topMargin + /// The requested bottom margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias bottomMargin: manager.bottomMargin + /// The requested left margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias leftMargin: manager.leftMargin + /// The requested right margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias rightMargin: manager.rightMargin + /// Determines if child item should be resized larger than its implicit size if + /// the parent is resized larger than its implicit size. Defaults to false. property /*bool*/alias resizeChild: manager.resizeChild /// See @@WrapperManager.child for details. property /*Item*/alias child: manager.child diff --git a/src/widgets/WrapperRectangle.qml b/src/widgets/WrapperRectangle.qml index e1c2c833..588e5e04 100644 --- a/src/widgets/WrapperRectangle.qml +++ b/src/widgets/WrapperRectangle.qml @@ -6,7 +6,7 @@ import QtQuick /// border, see @@ClippingWrapperRectangle. /// /// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component. -/// > You should read its documentation as well. +/// > See its documentation for information on how margins and sizes are calculated. /// /// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, /// > @@Item.height or @@Item.anchors on the child item, as they are used @@ -16,20 +16,40 @@ Rectangle { id: root /// If true (default), the rectangle's border width will be added - /// to the margin. + /// to @@extraMargin. property bool contentInsideBorder: true - /// The minimum margin between the child item and the WrapperRectangle's - /// edges. If @@contentInsideBorder is true, this excludes the border, - /// otherwise it includes it. Defaults to 0. - property real margin: 0 - /// If the child item should be resized larger than its implicit size if - /// the WrapperRectangle is resized larger than its implicit size. Defaults to false. + /// The default for @@topMargin, @@bottomMargin, @@leftMargin and @@rightMargin. + /// Defaults to 0. + property /*real*/alias margin: manager.margin + /// An extra margin applied in addition to @@topMargin, @@bottomMargin, + /// @@leftMargin, and @@rightMargin. + /// If @@contentInsideBorder is true, the rectangle's border width will be added + /// to this property. Defaults to 0. + property real extraMargin: 0 + /// The requested top margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias topMargin: manager.topMargin + /// The requested bottom margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias bottomMargin: manager.bottomMargin + /// The requested left margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias leftMargin: manager.leftMargin + /// The requested right margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + property /*real*/alias rightMargin: manager.rightMargin + /// Determines if child item should be resized larger than its implicit size if + /// the parent is resized larger than its implicit size. Defaults to false. property /*bool*/alias resizeChild: manager.resizeChild /// See @@WrapperManager.child for details. property alias child: manager.child MarginWrapperManager { id: manager - margin: (root.contentInsideBorder ? root.border.width : 0) + root.margin + extraMargin: (root.contentInsideBorder ? root.border.width : 0) + root.extraMargin } } diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp index 5dc907ee..0a5fd37f 100644 --- a/src/widgets/marginwrapper.cpp +++ b/src/widgets/marginwrapper.cpp @@ -1,5 +1,4 @@ #include "marginwrapper.hpp" -#include #include #include @@ -17,6 +16,35 @@ MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(pare this, &MarginWrapperManager::onChildChanged ); + + this->bTopMargin.setBinding([this] { + return this->bExtraMargin + + (this->bTopMarginSet.value() ? this->bTopMarginValue : this->bMargin); + }); + + this->bBottomMargin.setBinding([this] { + return this->bExtraMargin + + (this->bBottomMarginSet.value() ? this->bBottomMarginValue : this->bMargin); + }); + + this->bLeftMargin.setBinding([this] { + return this->bExtraMargin + + (this->bLeftMarginSet.value() ? this->bLeftMarginValue : this->bMargin); + }); + + this->bRightMargin.setBinding([this] { + return this->bExtraMargin + + (this->bRightMarginSet.value() ? this->bRightMarginValue : this->bMargin); + }); + + // Coalesces updates via binding infrastructure + this->bUpdateWatcher.setBinding([this] { + this->bTopMargin.value(); + this->bBottomMargin.value(); + this->bLeftMargin.value(); + this->bRightMargin.value(); + return 0; + }); } void MarginWrapperManager::componentComplete() { @@ -41,15 +69,6 @@ void MarginWrapperManager::componentComplete() { if (!this->mChild) this->updateGeometry(); } -qreal MarginWrapperManager::margin() const { return this->mMargin; } - -void MarginWrapperManager::setMargin(qreal margin) { - if (margin == this->mMargin) return; - this->mMargin = margin; - this->updateGeometry(); - emit this->marginChanged(); -} - bool MarginWrapperManager::resizeChild() const { return this->mResizeChild; } void MarginWrapperManager::setResizeChild(bool resizeChild) { @@ -82,33 +101,36 @@ void MarginWrapperManager::onChildChanged() { } qreal MarginWrapperManager::targetChildWidth() const { - auto max = this->mWrapper->width() - this->mMargin * 2; + auto max = this->mWrapper->width() - (this->bLeftMargin + this->bRightMargin); if (this->mResizeChild) return max; - else return std::min(this->mChild->implicitWidth(), max); + else return this->mChild->implicitWidth(); } qreal MarginWrapperManager::targetChildHeight() const { - auto max = this->mWrapper->height() - this->mMargin * 2; + auto max = this->mWrapper->height() - (this->bTopMargin + this->bBottomMargin); if (this->mResizeChild) return max; - else return std::min(this->mChild->implicitHeight(), max); + else return this->mChild->implicitHeight(); } qreal MarginWrapperManager::targetChildX() const { - if (this->mResizeChild) return this->mMargin; + if (this->mResizeChild) return this->bLeftMargin; else { - return std::max(this->mMargin, this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2); + auto total = this->bLeftMargin + this->bRightMargin; + auto mul = total == 0 ? 0.5 : this->bLeftMargin / total; + auto margin = this->mWrapper->width() - this->mChild->implicitWidth(); + return margin * mul; } } qreal MarginWrapperManager::targetChildY() const { - if (this->mResizeChild) return this->mMargin; + if (this->mResizeChild) return this->bTopMargin; else { - return std::max( - this->mMargin, - this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2 - ); + auto total = this->bTopMargin + this->bBottomMargin; + auto mul = total == 0 ? 0.5 : this->bTopMargin / total; + auto margin = this->mWrapper->height() - this->mChild->implicitHeight(); + return margin * mul; } } @@ -126,7 +148,9 @@ void MarginWrapperManager::updateChildY() { void MarginWrapperManager::onChildImplicitWidthChanged() { if (!this->mChild || !this->mWrapper) return; - this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); + this->mWrapper->setImplicitWidth( + this->mChild->implicitWidth() + this->bLeftMargin + this->bRightMargin + ); // If the implicit width change does not result in an actual width change, // this will not be called anywhere else. @@ -135,7 +159,9 @@ void MarginWrapperManager::onChildImplicitWidthChanged() { void MarginWrapperManager::onChildImplicitHeightChanged() { if (!this->mChild || !this->mWrapper) return; - this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); + this->mWrapper->setImplicitHeight( + this->mChild->implicitHeight() + this->bTopMargin + this->bBottomMargin + ); // If the implicit height change does not result in an actual height change, // this will not be called anywhere else. @@ -146,15 +172,19 @@ void MarginWrapperManager::updateGeometry() { if (!this->mWrapper) return; if (this->mChild) { - this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); - this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); + this->mWrapper->setImplicitWidth( + this->mChild->implicitWidth() + this->bLeftMargin + this->bRightMargin + ); + this->mWrapper->setImplicitHeight( + this->mChild->implicitHeight() + this->bTopMargin + this->bBottomMargin + ); this->mChild->setX(this->targetChildX()); this->mChild->setY(this->targetChildY()); this->mChild->setWidth(this->targetChildWidth()); this->mChild->setHeight(this->targetChildHeight()); } else { - this->mWrapper->setImplicitWidth(this->mMargin * 2); - this->mWrapper->setImplicitHeight(this->mMargin * 2); + this->mWrapper->setImplicitWidth(this->bLeftMargin + this->bRightMargin); + this->mWrapper->setImplicitHeight(this->bTopMargin + this->bBottomMargin); } } diff --git a/src/widgets/marginwrapper.hpp b/src/widgets/marginwrapper.hpp index 63b3eae2..8af6255e 100644 --- a/src/widgets/marginwrapper.hpp +++ b/src/widgets/marginwrapper.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include #include +#include "../core/util.hpp" #include "wrapper.hpp" namespace qs::widgets { @@ -26,13 +28,46 @@ namespace qs::widgets { /// ### Implementing a margin wrapper type /// Follow the directions in @@WrapperManager$'s documentation, and or /// alias the @@margin property if you wish to expose it. +/// +/// ## Margin calculation +/// The margin of the content item is calculated based on @@topMargin, @@bottomMargin, +/// @@leftMargin, @@rightMargin, @@extraMargin and @@resizeChild. +/// +/// If @@resizeChild is `true`, each side's margin will be the value of `Margin` +/// plus @@extraMargin, and the content item will be stretched to match the given margin +/// if the wrapper is not at its implicit size. +/// +/// If @@resizeChild is `false`, the `Margin` properties will be interpreted as a +/// ratio and the content item will not be stretched if the wrapper is not at its implicit side. +/// +/// The implicit size of the wrapper is the implicit size of the content item +/// plus all margins. class MarginWrapperManager: public WrapperManager { Q_OBJECT; // clang-format off - /// The minimum margin between the child item and the parent item's edges. + /// The default for @@topMargin, @@bottomMargin, @@leftMargin and @@rightMargin. /// Defaults to 0. - Q_PROPERTY(qreal margin READ margin WRITE setMargin NOTIFY marginChanged FINAL); - /// If the child item should be resized larger than its implicit size if + Q_PROPERTY(qreal margin READ default WRITE default BINDABLE bindableMargin NOTIFY marginChanged FINAL); + /// An extra margin applied in addition to @@topMargin, @@bottomMargin, + /// @@leftMargin, and @@rightMargin. Defaults to 0. + Q_PROPERTY(qreal extraMargin READ default WRITE default BINDABLE bindableExtraMargin NOTIFY baseMarginChanged FINAL); + /// The requested top margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + Q_PROPERTY(qreal topMargin READ topMargin WRITE setTopMargin RESET resetTopMargin NOTIFY topMarginChanged FINAL); + /// The requested bottom margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + Q_PROPERTY(qreal bottomMargin READ bottomMargin WRITE setBottomMargin RESET resetBottomMargin NOTIFY bottomMarginChanged FINAL); + /// The requested left margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + Q_PROPERTY(qreal leftMargin READ leftMargin WRITE setLeftMargin RESET resetLeftMargin NOTIFY leftMarginChanged FINAL); + /// The requested right margin of the content item, not counting @@extraMargin. + /// + /// Defaults to @@margin, and may be reset by assigning `undefined`. + Q_PROPERTY(qreal rightMargin READ rightMargin WRITE setRightMargin RESET resetRightMargin NOTIFY rightMarginChanged FINAL); + /// Determines if child item should be resized larger than its implicit size if /// the parent is resized larger than its implicit size. Defaults to false. Q_PROPERTY(bool resizeChild READ resizeChild WRITE setResizeChild NOTIFY resizeChildChanged FINAL); // clang-format on @@ -43,14 +78,47 @@ public: void componentComplete() override; - [[nodiscard]] qreal margin() const; - void setMargin(qreal margin); + [[nodiscard]] QBindable bindableMargin() { return &this->bMargin; } + [[nodiscard]] QBindable bindableExtraMargin() { return &this->bExtraMargin; } + + [[nodiscard]] qreal topMargin() const { return this->bTopMargin.value(); } + void resetTopMargin() { this->bTopMarginSet = false; } + void setTopMargin(qreal topMargin) { + this->bTopMarginValue = topMargin; + this->bTopMarginSet = true; + } + + [[nodiscard]] qreal bottomMargin() const { return this->bBottomMargin.value(); } + void resetBottomMargin() { this->bBottomMarginSet = false; } + void setBottomMargin(qreal bottomMargin) { + this->bBottomMarginValue = bottomMargin; + this->bBottomMarginSet = true; + } + + [[nodiscard]] qreal leftMargin() const { return this->bLeftMargin.value(); } + void resetLeftMargin() { this->bLeftMarginSet = false; } + void setLeftMargin(qreal leftMargin) { + this->bLeftMarginValue = leftMargin; + this->bLeftMarginSet = true; + } + + [[nodiscard]] qreal rightMargin() const { return this->bRightMargin.value(); } + void resetRightMargin() { this->bRightMarginSet = false; } + void setRightMargin(qreal rightMargin) { + this->bRightMarginValue = rightMargin; + this->bRightMarginSet = true; + } [[nodiscard]] bool resizeChild() const; void setResizeChild(bool resizeChild); signals: void marginChanged(); + void baseMarginChanged(); + void topMarginChanged(); + void bottomMarginChanged(); + void leftMarginChanged(); + void rightMarginChanged(); void resizeChildChanged(); private slots: @@ -68,8 +136,29 @@ private: [[nodiscard]] qreal targetChildWidth() const; [[nodiscard]] qreal targetChildHeight() const; - qreal mMargin = 0; bool mResizeChild = false; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bMargin, &MarginWrapperManager::marginChanged); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bExtraMargin, &MarginWrapperManager::baseMarginChanged); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bTopMarginValue); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bBottomMarginValue); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bLeftMarginValue); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bRightMarginValue); + + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, bool, bTopMarginSet); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, bool, bBottomMarginSet); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, bool, bLeftMarginSet); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, bool, bRightMarginSet); + + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bTopMargin, &MarginWrapperManager::topMarginChanged); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bBottomMargin, &MarginWrapperManager::bottomMarginChanged); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bLeftMargin, &MarginWrapperManager::leftMarginChanged); + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, qreal, bRightMargin, &MarginWrapperManager::rightMarginChanged); + + Q_OBJECT_BINDABLE_PROPERTY(MarginWrapperManager, int, bUpdateWatcher); + QS_BINDING_SUBSCRIBE_METHOD(MarginWrapperManager, bUpdateWatcher, updateGeometry, subscribe); + // clang-format on }; } // namespace qs::widgets