From f50e2d875f93498c79ee38193b3cff5b55274057 Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 26 May 2026 15:42:07 +0100 Subject: [PATCH] quickshell: extract BarDropdown reusable component Inline QML component encapsulating PopupWindow + concave corners + morphing animation + dismiss guard. Calendar and tray context menu both use it now. Co-Authored-By: Claude Opus 4.6 --- settings/hyprland.nix | 285 +++++++++++++++--------------------------- 1 file changed, 98 insertions(+), 187 deletions(-) diff --git a/settings/hyprland.nix b/settings/hyprland.nix index 4296bf9..30a420d 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -717,7 +717,7 @@ in if (contextMenu.justDismissed) return; if (event.button === Qt.RightButton && modelData.hasMenu) { let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0); - contextMenu.menuX = pos.x; + contextMenu.dropdownX = pos.x; contextMenu.trayItem = modelData; menuOpener.menu = modelData.menu; contextMenu.visible = true; @@ -731,18 +731,18 @@ in } } - // Custom-rendered context menu - PopupWindow { - id: contextMenu - property var trayItem: null + // Reusable dropdown component + component BarDropdown: PopupWindow { + id: dropdown property bool open: false property bool justDismissed: false - property real menuX: 0 - property real fullWidth: menuItems.width + 16 - property real fullHeight: menuItems.height + 12 + property real dropdownX: 0 + property real fullWidth: 200 + property real fullHeight: 200 + default property alias content: dropdownContent.data anchor.window: bar - anchor.rect.x: menuX - (fullWidth + 16) / 2 + anchor.rect.x: dropdownX - (fullWidth + 16) / 2 anchor.rect.y: bar.height anchor.edges: Edges.Top | Edges.Left anchor.gravity: Edges.Bottom | Edges.Right @@ -759,29 +759,23 @@ in } else { open = false; justDismissed = true; - menuDismissGuard.start(); - menuOpener.menu = null; + _dismissGuard.start(); } } Timer { - id: menuDismissGuard + id: _dismissGuard interval: 100 - onTriggered: contextMenu.justDismissed = false + onTriggered: dropdown.justDismissed = false } - QsMenuOpener { - id: menuOpener - } - - // Concave corner — left Item { - anchors.right: menuContent.left + anchors.right: _dropdownRect.left anchors.top: parent.top width: 8 - height: Math.min(8, menuContent.height) + height: Math.min(8, _dropdownRect.height) clip: true - visible: menuContent.height > 0 + visible: _dropdownRect.height > 0 Canvas { anchors.top: parent.top width: 8; height: 8 @@ -797,14 +791,13 @@ in } } - // Concave corner — right Item { - anchors.left: menuContent.right + anchors.left: _dropdownRect.right anchors.top: parent.top width: 8 - height: Math.min(8, menuContent.height) + height: Math.min(8, _dropdownRect.height) clip: true - visible: menuContent.height > 0 + visible: _dropdownRect.height > 0 Canvas { anchors.top: parent.top width: 8; height: 8 @@ -821,11 +814,11 @@ in } Rectangle { - id: menuContent + id: _dropdownRect anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - width: contextMenu.fullWidth - height: contextMenu.open ? contextMenu.fullHeight : 0 + width: dropdown.fullWidth + height: dropdown.open ? dropdown.fullHeight : 0 color: "#D1${c.base00}" radius: 8 topLeftRadius: 0 @@ -836,66 +829,87 @@ in NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } - Column { - id: menuItems - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 6 - width: 200 + Item { + id: dropdownContent + anchors.fill: parent + } + } + } - Repeater { - model: menuOpener.children + // Context menu + BarDropdown { + id: contextMenu + property var trayItem: null + fullWidth: menuItems.width + 16 + fullHeight: menuItems.height + 12 + + onVisibleChanged: { + if (!visible) menuOpener.menu = null; + } + + QsMenuOpener { + id: menuOpener + } + + Column { + id: menuItems + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 6 + width: 200 + + Repeater { + model: menuOpener.children + + Rectangle { + required property var modelData + width: 200 + height: modelData.isSeparator ? 9 : 28 + color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled + ? "#${c.base02}" : "transparent" + radius: modelData.isSeparator ? 0 : 4 Rectangle { - required property var modelData - width: 200 - height: modelData.isSeparator ? 9 : 28 - color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled - ? "#${c.base02}" : "transparent" - radius: modelData.isSeparator ? 0 : 4 + visible: modelData.isSeparator + anchors.centerIn: parent + width: parent.width - 20 + height: 1 + color: "#${c.base03}" + } - Rectangle { - visible: modelData.isSeparator - anchors.centerIn: parent - width: parent.width - 20 - height: 1 - color: "#${c.base03}" + RowLayout { + visible: !modelData.isSeparator + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + spacing: 8 + + Text { + Layout.fillWidth: true + text: modelData.text ?? "" + color: modelData.enabled ? "#${c.base05}" : "#${c.base03}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 12 + elide: Text.ElideRight } - RowLayout { - visible: !modelData.isSeparator - anchors.fill: parent - anchors.leftMargin: 10 - anchors.rightMargin: 10 - spacing: 8 - - Text { - Layout.fillWidth: true - text: modelData.text ?? "" - color: modelData.enabled ? "#${c.base05}" : "#${c.base03}" - font.family: "FiraMono Nerd Font" - font.pixelSize: 12 - elide: Text.ElideRight - } - - Text { - visible: modelData.buttonType !== QsMenuButtonType.None - text: modelData.checkState === Qt.Checked ? "\u2713" : "" - color: "#${c.base0D}" - font.family: "FiraMono Nerd Font" - font.pixelSize: 12 - } + Text { + visible: modelData.buttonType !== QsMenuButtonType.None + text: modelData.checkState === Qt.Checked ? "\u2713" : "" + color: "#${c.base0D}" + font.family: "FiraMono Nerd Font" + font.pixelSize: 12 } + } - MouseArea { - id: itemMouse - anchors.fill: parent - hoverEnabled: true - enabled: !modelData.isSeparator && modelData.enabled - onClicked: { - modelData.triggered(); - contextMenu.visible = false; - } + MouseArea { + id: itemMouse + anchors.fill: parent + hoverEnabled: true + enabled: !modelData.isSeparator && modelData.enabled + onClicked: { + modelData.triggered(); + contextMenu.visible = false; } } } @@ -904,114 +918,11 @@ in } // Calendar popup - PopupWindow { + BarDropdown { id: calPopup - anchor.window: bar - anchor.rect.x: bar.width / 2 - (fullWidth + 16) / 2 - anchor.rect.y: bar.height - anchor.edges: Edges.Top | Edges.Left - anchor.gravity: Edges.Bottom | Edges.Right - anchor.adjustment: PopupAdjustment.Slide - grabFocus: true - visible: false - color: "transparent" - - property bool open: false - property bool justDismissed: false - property real fullWidth: calCol.width + 32 - property real fullHeight: calCol.height + 24 - - implicitWidth: fullWidth + 16 - implicitHeight: fullHeight + 4 - - onVisibleChanged: { - if (visible) { - open = true; - } else { - open = false; - justDismissed = true; - dismissGuardTimer.start(); - } - } - - Timer { - id: dismissGuardTimer - interval: 100 - onTriggered: calPopup.justDismissed = false - } - - // Concave corner — left - Item { - anchors.right: calContent.left - anchors.top: parent.top - width: 8 - height: Math.min(8, calContent.height) - clip: true - visible: calContent.height > 0 - Canvas { - anchors.top: parent.top - width: 8 - height: 8 - onPaint: { - var ctx = getContext("2d"); - var r = 8; - ctx.clearRect(0, 0, r, r); - ctx.fillStyle = "#D1${c.base00}"; - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(r, 0); - ctx.lineTo(r, r); - ctx.arc(0, r, r, 0, -Math.PI / 2, true); - ctx.closePath(); - ctx.fill(); - } - } - } - - // Concave corner — right - Item { - anchors.left: calContent.right - anchors.top: parent.top - width: 8 - height: Math.min(8, calContent.height) - clip: true - visible: calContent.height > 0 - Canvas { - anchors.top: parent.top - width: 8 - height: 8 - onPaint: { - var ctx = getContext("2d"); - var r = 8; - ctx.clearRect(0, 0, r, r); - ctx.fillStyle = "#D1${c.base00}"; - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(r, 0); - ctx.arc(r, r, r, -Math.PI / 2, Math.PI, true); - ctx.closePath(); - ctx.fill(); - } - } - } - - Rectangle { - id: calContent - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 0 - width: calPopup.open ? calPopup.fullWidth : calPopup.fullWidth - height: calPopup.open ? calPopup.fullHeight : 0 - color: "#D1${c.base00}" - border.width: 0 - radius: 8 - topLeftRadius: 0 - topRightRadius: 0 - clip: true - - Behavior on height { - NumberAnimation { duration: 220; easing.type: Easing.OutCubic } - } + dropdownX: bar.width / 2 + fullWidth: calCol.width + 32 + fullHeight: calCol.height + 24 Column { id: calCol