From 788c01c2428fbd2d75f58c1cae5ba9fa20771f34 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:50:21 +0100 Subject: [PATCH] overlay: make widgets resizable --- .../ii/modules/common/Persistent.qml | 14 ++- .../modules/common/widgets/StyledSlider.qml | 2 +- .../widgets/widgetCanvas/AbstractWidget.qml | 4 + .../modules/overlay/StyledOverlayWidget.qml | 114 ++++++++++++++++-- .../modules/overlay/crosshair/Crosshair.qml | 1 + .../modules/overlay/fpsLimiter/FpsLimiter.qml | 2 + .../ii/modules/overlay/recorder/Recorder.qml | 15 +-- .../modules/overlay/resources/Resources.qml | 7 +- .../overlay/volumeMixer/VolumeMixer.qml | 7 +- 9 files changed, 134 insertions(+), 32 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 5848b1683..171f150e6 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -84,20 +84,26 @@ Singleton { property JsonObject crosshair: JsonObject { property bool pinned: false property bool clickthrough: true - property real x: 835 - property real y: 483 + property real x: 827 + property real y: 441 + property real width: 250 + property real height: 100 } property JsonObject recorder: JsonObject { property bool pinned: false property bool clickthrough: false property real x: 80 property real y: 80 + property real width: 350 + property real height: 130 } property JsonObject resources: JsonObject { property bool pinned: false property bool clickthrough: true property real x: 1500 property real y: 770 + property real width: 350 + property real height: 200 property int tabIndex: 0 } property JsonObject volumeMixer: JsonObject { @@ -105,6 +111,8 @@ Singleton { property bool clickthrough: false property real x: 80 property real y: 280 + property real width: 350 + property real height: 600 property int tabIndex: 0 } property JsonObject fpsLimiter: JsonObject { @@ -112,6 +120,8 @@ Singleton { property bool clickthrough: false property real x: 1576 property real y: 630 + property real width: 280 + property real height: 80 } } diff --git a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml index 38a6e9394..98b8eebf3 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml @@ -12,7 +12,7 @@ import Quickshell.Widgets * It doesn't exactly match the spec because it does not make sense to have stuff on a computer that fucking huge. * Should be at 3/4 scale... */ - + Slider { id: root diff --git a/dots/.config/quickshell/ii/modules/common/widgets/widgetCanvas/AbstractWidget.qml b/dots/.config/quickshell/ii/modules/common/widgets/widgetCanvas/AbstractWidget.qml index 20b0c27c1..ee97c7168 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/widgetCanvas/AbstractWidget.qml +++ b/dots/.config/quickshell/ii/modules/common/widgets/widgetCanvas/AbstractWidget.qml @@ -8,6 +8,8 @@ import qs.modules.common MouseArea { id: root + property alias animateXPos: xBehavior.enabled + property alias animateYPos: yBehavior.enabled property bool draggable: true drag.target: draggable ? root : undefined cursorShape: (draggable && containsPress) ? Qt.ClosedHandCursor : draggable ? Qt.OpenHandCursor : Qt.ArrowCursor @@ -18,9 +20,11 @@ MouseArea { } Behavior on x { + id: xBehavior animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Behavior on y { + id: yBehavior animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } } diff --git a/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml b/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml index f6c21199b..fbd94f5f3 100644 --- a/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml +++ b/dots/.config/quickshell/ii/modules/overlay/StyledOverlayWidget.qml @@ -13,29 +13,67 @@ import qs.modules.common.widgets.widgetCanvas * To make an overlay widget: * 1. Create a modules/overlay//.qml, using this as the base class and declare your widget content as contentItem * 2. Add an entry to OverlayContext.availableWidgets with identifier= - * 3. Add an entry in Persistent.states.overlay. with x, y, pinned, clickthrough properties set to reasonable defaults + * 3. Add an entry in Persistent.states.overlay. with x, y, width, height, pinned, clickthrough properties set to reasonable defaults * 4. Add an entry in OverlayWidgetDelegateChooser with roleValue= and Declare your widget in there * Use existing entries as reference. */ AbstractOverlayWidget { id: root + // To be defined by subclasses required property Item contentItem property bool fancyBorders: true property bool showCenterButton: false property bool showClickabilityButton: true + // Defaults n stuff required property var modelData readonly property string identifier: modelData.identifier readonly property string materialSymbol: modelData.materialSymbol ?? "widgets" property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); }) property var persistentStateEntry: Persistent.states.overlay[identifier] property real radius: Appearance.rounding.windowRounding - property real minWidth: 250 + property real minimumWidth: 250 + property real minimumHeight: 100 + property real resizeMargin: 8 property real padding: 6 property real contentRadius: radius - padding + // Resizing + function getXResizeDirection(x) { + return (x < root.resizeMargin) ? -1 : (x > root.width - root.resizeMargin) ? 1 : 0 + } + function getYResizeDirection(y) { + return (y < root.resizeMargin) ? -1 : (y > root.height - root.resizeMargin) ? 1 : 0 + } + hoverEnabled: true + property bool resizable: true + property bool resizing: false + property int resizeXDirection: getXResizeDirection(mouseX) + property int resizeYDirection: getYResizeDirection(mouseY) draggable: GlobalStates.overlayOpen + drag.target: undefined + animateXPos: !dragHandler.active + animateYPos: !dragHandler.active + z: dragHandler.active ? 2 : 1 + cursorShape: { + if (dragHandler.active) return root.resizing ? cursorShape : Qt.ArrowCursor; + if (resizeMargin < mouseX && mouseX < width - resizeMargin && + resizeMargin < mouseY && mouseY < height - resizeMargin) { + return Qt.ArrowCursor; + } else { + if (!root.resizable) return Qt.ArrowCursor; + const dragIsLeft = mouseX < width / 2 + const dragIsTop = mouseY < height / 2 + if ((dragIsLeft && dragIsTop) || (!dragIsLeft && !dragIsTop)) { + return Qt.SizeFDiagCursor + } else { + return Qt.SizeBDiagCursor + } + } + } + + // Positioning & sizing x: Math.round(persistentStateEntry.x) // Round or it'll be blurry y: Math.round(persistentStateEntry.y) // Round or it'll be blurry pinned: persistentStateEntry.pinned @@ -68,7 +106,52 @@ AbstractOverlayWidget { } // Hooks - onReleased: savePosition(); + onPressed: (event) => { + // We're only interested in handling resize here + // Early returns + if (!root.resizable) return; + if (root.resizeMargin < event.x && event.x < root.width - root.resizeMargin && + root.resizeMargin < event.y && event.y < root.height - root.resizeMargin) { + return; + } + // Resizing setup + root.resizing = true; + root.resizeXDirection = getXResizeDirection(event.x); + root.resizeYDirection = getYResizeDirection(event.y); + if (root.resizeYDirection !== 0 && root.resizeXDirection === 0) { + root.resizeXDirection = event.x < root.width / 2 ? -1 : 1; + } else if (root.resizeXDirection !== 0 && root.resizeYDirection === 0) { + root.resizeYDirection = event.y < root.height / 2 ? -1 : 1; + } + } + onPositionChanged: (event) => { + if (!resizing) return; + contentContainer.implicitWidth = Math.max(root.persistentStateEntry.width + dragHandler.xAxis.activeValue * root.resizeXDirection, root.minimumWidth); + contentContainer.implicitHeight = Math.max(root.persistentStateEntry.height + dragHandler.yAxis.activeValue * root.resizeYDirection, root.minimumHeight); + const negativeXDrag = root.resizeXDirection === -1; + const negativeYDrag = root.resizeYDirection === -1; + const wantedX = root.persistentStateEntry.x + (negativeXDrag ? dragHandler.xAxis.activeValue : 0) + const wantedY = root.persistentStateEntry.y + (negativeYDrag ? dragHandler.yAxis.activeValue : 0) + const negativeXDragLimit = root.persistentStateEntry.x + root.persistentStateEntry.width - contentContainer.implicitWidth; + const negativeYDragLimit = root.persistentStateEntry.y + root.persistentStateEntry.height - contentContainer.implicitHeight; + root.x = negativeXDrag ? Math.min(wantedX, negativeXDragLimit) : wantedX; + root.y = negativeYDrag ? Math.min(wantedY, negativeYDragLimit) : wantedY; + } + DragHandler { + id: dragHandler + acceptedButtons: Qt.LeftButton | Qt.RightButton + target: (root.draggable && !root.resizing) ? root : null + onActiveChanged: { // Handle drag release + if (!active) { + root.resizing = false; + root.savePosition(); + } + } + xAxis.minimum: 0 + xAxis.maximum: root.parent?.width - root.width + yAxis.minimum: 0 + yAxis.maximum: root.parent?.height - root.height + } function close() { Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier); @@ -82,26 +165,31 @@ AbstractOverlayWidget { persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough; } - function savePosition(xPos = root.x, yPos = root.y) { - persistentStateEntry.x = xPos; - persistentStateEntry.y = yPos; + function savePosition(xPos = root.x, yPos = root.y, width = contentContainer.implicitWidth, height = contentContainer.implicitHeight) { + persistentStateEntry.x = Math.round(xPos); + persistentStateEntry.y = Math.round(yPos); + persistentStateEntry.width = Math.round(width); + persistentStateEntry.height = Math.round(height); } function center() { - const targetX = (root.parent.width - contentColumn.width) / 2 - const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight + border.border.width + const targetX = (root.parent.width - contentColumn.width) / 2 - root.resizeMargin + const targetY = (root.parent.height - contentContainer.height) / 2 - titleBar.implicitHeight + border.border.width - root.resizeMargin root.x = targetX root.y = targetY root.savePosition(targetX, targetY) } visible: GlobalStates.overlayOpen || actuallyPinned - implicitWidth: Math.max(contentColumn.implicitWidth, minWidth) - implicitHeight: contentColumn.implicitHeight + implicitWidth: contentColumn.implicitWidth + resizeMargin * 2 + implicitHeight: contentColumn.implicitHeight + resizeMargin * 2 Rectangle { id: border - anchors.fill: parent + anchors { + fill: parent + margins: root.resizeMargin + } color: (root.fancyBorders && GlobalStates.overlayOpen) ? Appearance.colors.colLayer1 : "transparent" radius: root.radius border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1) @@ -205,8 +293,8 @@ AbstractOverlayWidget { Layout.margins: root.fancyBorders ? root.padding : 0 Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - implicitWidth: root.contentItem.implicitWidth - implicitHeight: root.contentItem.implicitHeight + implicitWidth: Math.max(root.persistentStateEntry.width, root.minimumWidth) + implicitHeight: Math.max(root.persistentStateEntry.height, root.minimumHeight) children: [root.contentItem] } } diff --git a/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml b/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml index 4a92aa43e..ea2f5a1ad 100644 --- a/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml +++ b/dots/.config/quickshell/ii/modules/overlay/crosshair/Crosshair.qml @@ -11,6 +11,7 @@ StyledOverlayWidget { opacity: 1 // The crosshair itself already has transparency if configured showClickabilityButton: false clickthrough: true + resizable: false contentItem: CrosshairContent { anchors.centerIn: parent diff --git a/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml index 7628d58be..65711c6d2 100644 --- a/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml +++ b/dots/.config/quickshell/ii/modules/overlay/fpsLimiter/FpsLimiter.qml @@ -6,6 +6,8 @@ import qs.modules.overlay StyledOverlayWidget { id: root title: "MangoHud FPS" + minimumWidth: 275 + minimumHeight: 100 contentItem: FpsLimiterContent { radius: root.contentRadius } diff --git a/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml b/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml index 07e7c8d9f..05c0fa680 100644 --- a/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml +++ b/dots/.config/quickshell/ii/modules/overlay/recorder/Recorder.qml @@ -9,25 +9,22 @@ import qs.modules.overlay StyledOverlayWidget { id: root + minimumWidth: 310 + minimumHeight: 130 contentItem: Rectangle { id: contentItem - anchors.centerIn: parent + anchors.fill: parent radius: root.contentRadius color: Appearance.m3colors.m3surfaceContainer property real padding: 8 - implicitHeight: contentColumn.implicitHeight + padding * 2 - implicitWidth: 350 ColumnLayout { id: contentColumn - anchors { - fill: parent - margins: parent.padding - } + anchors.centerIn: parent spacing: 10 Row { - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter spacing: 10 BigRecorderButton { @@ -68,7 +65,7 @@ StyledOverlayWidget { } RippleButton { - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.fillWidth: false buttonRadius: height / 2 colBackground: Appearance.colors.colLayer3 diff --git a/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml b/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml index fe32d2a91..0dcd6d0f6 100644 --- a/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml +++ b/dots/.config/quickshell/ii/modules/overlay/resources/Resources.qml @@ -14,6 +14,8 @@ import qs.modules.overlay StyledOverlayWidget { id: root + minimumWidth: 300 + minimumHeight: 200 property list resources: [ { "icon": "planner_review", @@ -37,13 +39,10 @@ StyledOverlayWidget { contentItem: Rectangle { id: contentItem - anchors.centerIn: parent + anchors.fill: parent color: Appearance.m3colors.m3surfaceContainer radius: root.contentRadius property real padding: 4 - implicitWidth: 350 - implicitHeight: 200 - // implicitHeight: contentColumn.implicitHeight + padding * 2 ColumnLayout { id: contentColumn anchors { diff --git a/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml b/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml index d08b22a8d..228ea575d 100644 --- a/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml +++ b/dots/.config/quickshell/ii/modules/overlay/volumeMixer/VolumeMixer.qml @@ -10,13 +10,14 @@ import qs.modules.sidebarRight.volumeMixer StyledOverlayWidget { id: root + minimumWidth: 300 + minimumHeight: 380 + contentItem: Rectangle { - anchors.centerIn: parent + anchors.fill: parent color: Appearance.m3colors.m3surfaceContainer radius: root.contentRadius property real padding: 6 - implicitHeight: 600 - implicitWidth: 350 ColumnLayout { id: contentColumn