diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 5fbcc5b5..3f8de41f 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,5 +1,7 @@ qt_add_library(quickshell-widgets STATIC cliprect.cpp + wrapper.cpp + marginwrapper.cpp ) qt_add_qml_module(quickshell-widgets @@ -8,6 +10,8 @@ qt_add_qml_module(quickshell-widgets QML_FILES IconImage.qml ClippingRectangle.qml + WrapperItem.qml + WrapperRectangle.qml ) qt6_add_shaders(quickshell-widgets "widgets-cliprect" diff --git a/src/widgets/WrapperItem.qml b/src/widgets/WrapperItem.qml new file mode 100644 index 00000000..dfa7c0fd --- /dev/null +++ b/src/widgets/WrapperItem.qml @@ -0,0 +1,44 @@ +import QtQuick +import Quickshell.Widgets + +///! Item that handles sizes and positioning for a single visual child. +/// This component is useful when you need to wrap a single component in +/// an item, or give a single component a margin. See [QtQuick.Layouts] +/// for positioning multiple items. +/// +/// > [!NOTE] WrapperItem is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// ### Example: Adding a margin to an item +/// The snippet below adds a 10px margin to all sides of the @@QtQuick.Text item. +/// +/// ```qml +/// WrapperItem { +/// margin: 10 +/// +/// @@QtQuick.Text { text: "Hello!" } +/// } +/// ``` +/// +/// > [!NOTE] The child item can be specified by writing it inline in the wrapper, +/// > as in the example above, or by using the @@child property. See +/// > @@WrapperManager.child for details. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +/// +/// [QtQuick.Layouts]: https://doc.qt.io/qt-6/qtquicklayouts-index.html +Item { + /// The minimum margin between the child item and the WrapperItem's edges. + /// 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. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property /*Item*/alias child: manager.child + + MarginWrapperManager { id: manager } +} diff --git a/src/widgets/WrapperRectangle.qml b/src/widgets/WrapperRectangle.qml new file mode 100644 index 00000000..c198c47b --- /dev/null +++ b/src/widgets/WrapperRectangle.qml @@ -0,0 +1,35 @@ +import QtQuick +import Quickshell.Widgets + +///! Rectangle that handles sizes and positioning for a single visual child. +/// This component is useful for adding a border or background rectangle to +/// a child item. +/// +/// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +Rectangle { + id: root + + /// If true (default), the rectangle's border width will be added + /// to the margin. + 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. + 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 + } +} diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp new file mode 100644 index 00000000..b960a9f7 --- /dev/null +++ b/src/widgets/marginwrapper.cpp @@ -0,0 +1,146 @@ +#include "marginwrapper.hpp" +#include + +#include +#include +#include +#include + +#include "wrapper.hpp" + +namespace qs::widgets { + +MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(parent) { + QObject::connect( + this, + &WrapperManager::initializedChildChanged, + this, + &MarginWrapperManager::onChildChanged + ); +} + +void MarginWrapperManager::componentComplete() { + if (this->mWrapper) { + QObject::connect( + this->mWrapper, + &QQuickItem::widthChanged, + this, + &MarginWrapperManager::onWrapperWidthChanged + ); + + QObject::connect( + this->mWrapper, + &QQuickItem::heightChanged, + this, + &MarginWrapperManager::onWrapperHeightChanged + ); + } + + this->WrapperManager::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) { + if (resizeChild == this->mResizeChild) return; + this->mResizeChild = resizeChild; + this->updateGeometry(); + emit this->resizeChildChanged(); +} + +void MarginWrapperManager::onChildChanged() { + // QObject::disconnect in MarginWrapper handles disconnecting old item + + if (this->mChild) { + QObject::connect( + this->mChild, + &QQuickItem::implicitWidthChanged, + this, + &MarginWrapperManager::onChildImplicitWidthChanged + ); + + QObject::connect( + this->mChild, + &QQuickItem::implicitHeightChanged, + this, + &MarginWrapperManager::onChildImplicitHeightChanged + ); + } + + this->updateGeometry(); +} + +qreal MarginWrapperManager::targetChildWidth() const { + auto max = this->mWrapper->width() - this->mMargin * 2; + + if (this->mResizeChild) return max; + else return std::min(this->mChild->implicitWidth(), max); +} + +qreal MarginWrapperManager::targetChildHeight() const { + auto max = this->mWrapper->height() - this->mMargin * 2; + + if (this->mResizeChild) return max; + else return std::min(this->mChild->implicitHeight(), max); +} + +qreal MarginWrapperManager::targetChildX() const { + if (this->mResizeChild) return this->mMargin; + else return this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2; +} + +qreal MarginWrapperManager::targetChildY() const { + if (this->mResizeChild) return this->mMargin; + else return this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2; +} + +void MarginWrapperManager::onWrapperWidthChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mChild->setX(this->targetChildX()); + this->mChild->setWidth(this->targetChildWidth()); +} + +void MarginWrapperManager::onWrapperHeightChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mChild->setY(this->targetChildY()); + this->mChild->setHeight(this->targetChildHeight()); +} + +void MarginWrapperManager::onChildImplicitWidthChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); +} + +void MarginWrapperManager::onChildImplicitHeightChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); +} + +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->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); + } +} + +} // namespace qs::widgets diff --git a/src/widgets/marginwrapper.hpp b/src/widgets/marginwrapper.hpp new file mode 100644 index 00000000..7946951a --- /dev/null +++ b/src/widgets/marginwrapper.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include + +#include "wrapper.hpp" + +namespace qs::widgets { + +///! Helper object for applying sizes and margins to a single child item. +/// > [!NOTE] MarginWrapperManager is an extension of @@WrapperManager. +/// > You should read its documentation to understand wrapper types. +/// +/// MarginWrapperManager can be used to apply margins to a child item, +/// in addition to handling the size / implicit size relationship +/// between the parent and the child. @@WrapperItem and @@WrapperRectangle +/// exist for Item and Rectangle implementations respectively. +/// +/// > [!WARNING] MarginWrapperManager based types set the child item's +/// > @@QtQuick.Item.x, @@QtQuick.Item.y, @@QtQuick.Item.width, @@QtQuick.Item.height +/// > or @@QtQuick.Item.anchors properties. Do not set them yourself, +/// > instead set @@Item.implicitWidth and @@Item.implicitHeight. +/// +/// ### Implementing a margin wrapper type +/// Follow the directions in @@WrapperManager$'s documentation, and or +/// alias the @@margin property if you wish to expose it. +class MarginWrapperManager: public WrapperManager { + Q_OBJECT; + // clang-format off + /// The minimum margin between the child item and the parent item's edges. + /// 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 + /// 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 + QML_ELEMENT; + +public: + explicit MarginWrapperManager(QObject* parent = nullptr); + + void componentComplete() override; + + [[nodiscard]] qreal margin() const; + void setMargin(qreal margin); + + [[nodiscard]] bool resizeChild() const; + void setResizeChild(bool resizeChild); + +signals: + void marginChanged(); + void resizeChildChanged(); + +private slots: + void onChildChanged(); + void onWrapperWidthChanged(); + void onWrapperHeightChanged(); + void onChildImplicitWidthChanged(); + void onChildImplicitHeightChanged(); + +private: + void updateGeometry(); + + [[nodiscard]] qreal targetChildX() const; + [[nodiscard]] qreal targetChildY() const; + [[nodiscard]] qreal targetChildWidth() const; + [[nodiscard]] qreal targetChildHeight() const; + + qreal mMargin = 0; + bool mResizeChild = false; +}; + +} // namespace qs::widgets diff --git a/src/widgets/module.md b/src/widgets/module.md index c24bb876..77d4a3a5 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -1,7 +1,15 @@ name = "Quickshell.Widgets" description = "Bundled widgets" + +headers = [ + "wrapper.hpp", + "marginwrapper.hpp", +] + qml_files = [ "IconImage.qml", "ClippingRectangle.qml", + "WrapperItem.qml", + "WrapperRectangle.qml", ] ----- diff --git a/src/widgets/wrapper.cpp b/src/widgets/wrapper.cpp new file mode 100644 index 00000000..4e502cee --- /dev/null +++ b/src/widgets/wrapper.cpp @@ -0,0 +1,127 @@ +#include "wrapper.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace qs::widgets { + +void WrapperManager::componentComplete() { + this->mWrapper = qobject_cast(this->parent()); + + if (!this->mWrapper) { + QString pstr; + QDebug(&pstr) << this->parent(); + + qmlWarning(this) << "Parent of WrapperManager is not a QQuickItem. Parent: " << pstr; + return; + } + + QQuickItem* child = this->mChild; + this->mChild = nullptr; // avoids checks for the old item in setChild. + + const auto& childItems = this->mWrapper->childItems(); + + if (childItems.length() == 1) { + this->mDefaultChild = childItems.first(); + } else if (childItems.length() != 0) { + this->flags.setFlag(WrapperManager::HasMultipleChildren); + + if (!child && !this->flags.testFlags(WrapperManager::NullChild)) { + this->printChildCountWarning(); + } + } + + for (auto* item: childItems) { + if (item != child) item->setParentItem(nullptr); + } + + if (child && !this->flags.testFlag(WrapperManager::NullChild)) { + this->setChild(child); + } +} + +QQuickItem* WrapperManager::child() const { return this->mChild; } + +void WrapperManager::setChild(QQuickItem* child) { + if (child && child == this->mChild) return; + + if (this->mChild != nullptr) { + QObject::disconnect(this->mChild, nullptr, this, nullptr); + + if (this->mChild->parentItem() == this->mWrapper) { + this->mChild->setParentItem(nullptr); + } + } + + this->mChild = child; + this->flags.setFlag(WrapperManager::NullChild, child == nullptr); + + if (child) { + QObject::connect( + child, + &QObject::destroyed, + this, + &WrapperManager::onChildDestroyed, + Qt::UniqueConnection + ); + + if (auto* wrapper = this->mWrapper) { + child->setParentItem(wrapper); + } + } + + emit this->initializedChildChanged(); + emit this->childChanged(); +} + +void WrapperManager::setProspectiveChild(QQuickItem* child) { + if (child && child == this->mChild) return; + + if (!this->mWrapper) { + if (this->mChild) { + QObject::disconnect(this->mChild, nullptr, this, nullptr); + } + + this->mChild = child; + this->flags.setFlag(WrapperManager::NullChild, child == nullptr); + + if (child) { + QObject::connect(child, &QObject::destroyed, this, &WrapperManager::onChildDestroyed); + } + } else { + this->setChild(child); + } +} + +void WrapperManager::unsetChild() { + if (!this->mWrapper) { + this->setProspectiveChild(nullptr); + } else { + this->setChild(this->mDefaultChild); + + if (!this->mDefaultChild && this->flags.testFlag(WrapperManager::HasMultipleChildren)) { + this->printChildCountWarning(); + } + } + + this->flags.setFlag(WrapperManager::NullChild, false); +} + +void WrapperManager::onChildDestroyed() { + this->mChild = nullptr; + this->unsetChild(); + emit this->childChanged(); +} + +void WrapperManager::printChildCountWarning() const { + qmlWarning(this->mWrapper) << "Wrapper component cannot have more than one visual child."; + qmlWarning(this->mWrapper) << "Remove all additional children, or pick a specific component " + "to wrap using the child property."; +} + +} // namespace qs::widgets diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp new file mode 100644 index 00000000..95b3adea --- /dev/null +++ b/src/widgets/wrapper.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" + +namespace qs::widgets { + +///! Helper object for creating components with a single visual child. +/// WrapperManager determines which child of an Item should be its visual +/// child, and exposes it for further operations. See @@MarginWrapperManager +/// for a subclass that implements automatic sizing and margins. +/// +/// ### Using wrapper types +/// WrapperManager based types have a single visual child item. +/// You can specify the child item using the default property, or by +/// setting the @@child property. You must use the @@child property if +/// the widget has more than one @@QtQuick.Item based child. +/// +/// #### Example using the default property +/// ```qml +/// WrapperWidget { // a widget that uses WrapperManager +/// // Putting the item inline uses the default property of WrapperWidget. +/// @@QtQuick.Text { text: "Hello" } +/// +/// // Scope does not extend Item, so it can be placed in the +/// // default property without issue. +/// @@Quickshell.Scope {} +/// } +/// ``` +/// +/// #### Example using the child property +/// ```qml +/// WrapperWidget { +/// @@QtQuick.Text { +/// id: text +/// text: "Hello" +/// } +/// +/// @@QtQuick.Text { +/// id: otherText +/// text: "Other Text" +/// } +/// +/// // Both text and otherText extend Item, so one must be specified. +/// child: text +/// } +/// ``` +/// +/// See @@child for more details on how the child property can be used. +/// +/// ### Implementing wrapper types +/// In addition to the bundled wrapper types, you can make your own using +/// WrapperManager. To implement a wrapper, create a WrapperManager inside +/// your wrapper component 's default property, then alias a new property +/// to the WrapperManager's @@child property. +/// +/// #### Example +/// ```qml +/// Item { // your wrapper component +/// WrapperManager { id: wrapperManager } +/// +/// // Allows consumers of your wrapper component to use the child property. +/// property alias child: wrapperManager.child +/// +/// // The rest of your component logic. You can use +/// // `wrapperManager.child` or `this.child` to refer to the selected child. +/// } +/// ``` +/// +/// ### See also +/// - @@WrapperItem - A @@MarginWrapperManager based component that sizes itself +/// to its child. +/// - @@WrapperRectangle - A @@MarginWrapperManager based component that sizes +/// itself to its child, and provides an option to use its border as an inset. +class WrapperManager + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + // clang-format off + /// The wrapper component's selected child. + /// + /// Setting this property override's WrapperManager's default selection, + /// and resolve ambiguity when more than one visual child is present. + /// The property can additionally be defined inline or reference a component + /// that is not already a child of the wrapper, in which case it will be + /// reparented to the wrapper. Setting child to `null` will select no child, + /// and `undefined` will restore the default child. + /// + /// When read, `child` will always return the (potentially null) selected child, + /// and not `undefined`. + Q_PROPERTY(QQuickItem* child READ child WRITE setProspectiveChild RESET unsetChild NOTIFY childChanged FINAL); + // clang-format on + QML_ELEMENT; + +public: + explicit WrapperManager(QObject* parent = nullptr): QObject(parent) {} + + void classBegin() override {} + void componentComplete() override; + + [[nodiscard]] QQuickItem* child() const; + void setChild(QQuickItem* child); + void setProspectiveChild(QQuickItem* child); + void unsetChild(); + +signals: + void childChanged(); + QSDOC_HIDE void initializedChildChanged(); + +private slots: + void onChildDestroyed(); + +protected: + enum Flag : quint8 { + NoFlags = 0x0, + NullChild = 0x1, + HasMultipleChildren = 0x2, + }; + Q_DECLARE_FLAGS(Flags, Flag); + + void printChildCountWarning() const; + void updateGeometry(); + + QQuickItem* mWrapper = nullptr; + QPointer mDefaultChild; + QQuickItem* mChild = nullptr; + Flags flags; +}; + +} // namespace qs::widgets