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 <noreply@anthropic.com>
This commit is contained in:
rope 2026-05-26 15:42:07 +01:00
parent 0aba95779a
commit f50e2d875f

View file

@ -717,7 +717,7 @@ in
if (contextMenu.justDismissed) return; if (contextMenu.justDismissed) return;
if (event.button === Qt.RightButton && modelData.hasMenu) { if (event.button === Qt.RightButton && modelData.hasMenu) {
let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0); let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0);
contextMenu.menuX = pos.x; contextMenu.dropdownX = pos.x;
contextMenu.trayItem = modelData; contextMenu.trayItem = modelData;
menuOpener.menu = modelData.menu; menuOpener.menu = modelData.menu;
contextMenu.visible = true; contextMenu.visible = true;
@ -731,18 +731,18 @@ in
} }
} }
// Custom-rendered context menu // Reusable dropdown component
PopupWindow { component BarDropdown: PopupWindow {
id: contextMenu id: dropdown
property var trayItem: null
property bool open: false property bool open: false
property bool justDismissed: false property bool justDismissed: false
property real menuX: 0 property real dropdownX: 0
property real fullWidth: menuItems.width + 16 property real fullWidth: 200
property real fullHeight: menuItems.height + 12 property real fullHeight: 200
default property alias content: dropdownContent.data
anchor.window: bar anchor.window: bar
anchor.rect.x: menuX - (fullWidth + 16) / 2 anchor.rect.x: dropdownX - (fullWidth + 16) / 2
anchor.rect.y: bar.height anchor.rect.y: bar.height
anchor.edges: Edges.Top | Edges.Left anchor.edges: Edges.Top | Edges.Left
anchor.gravity: Edges.Bottom | Edges.Right anchor.gravity: Edges.Bottom | Edges.Right
@ -759,29 +759,23 @@ in
} else { } else {
open = false; open = false;
justDismissed = true; justDismissed = true;
menuDismissGuard.start(); _dismissGuard.start();
menuOpener.menu = null;
} }
} }
Timer { Timer {
id: menuDismissGuard id: _dismissGuard
interval: 100 interval: 100
onTriggered: contextMenu.justDismissed = false onTriggered: dropdown.justDismissed = false
} }
QsMenuOpener {
id: menuOpener
}
// Concave corner left
Item { Item {
anchors.right: menuContent.left anchors.right: _dropdownRect.left
anchors.top: parent.top anchors.top: parent.top
width: 8 width: 8
height: Math.min(8, menuContent.height) height: Math.min(8, _dropdownRect.height)
clip: true clip: true
visible: menuContent.height > 0 visible: _dropdownRect.height > 0
Canvas { Canvas {
anchors.top: parent.top anchors.top: parent.top
width: 8; height: 8 width: 8; height: 8
@ -797,14 +791,13 @@ in
} }
} }
// Concave corner right
Item { Item {
anchors.left: menuContent.right anchors.left: _dropdownRect.right
anchors.top: parent.top anchors.top: parent.top
width: 8 width: 8
height: Math.min(8, menuContent.height) height: Math.min(8, _dropdownRect.height)
clip: true clip: true
visible: menuContent.height > 0 visible: _dropdownRect.height > 0
Canvas { Canvas {
anchors.top: parent.top anchors.top: parent.top
width: 8; height: 8 width: 8; height: 8
@ -821,11 +814,11 @@ in
} }
Rectangle { Rectangle {
id: menuContent id: _dropdownRect
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
width: contextMenu.fullWidth width: dropdown.fullWidth
height: contextMenu.open ? contextMenu.fullHeight : 0 height: dropdown.open ? dropdown.fullHeight : 0
color: "#D1${c.base00}" color: "#D1${c.base00}"
radius: 8 radius: 8
topLeftRadius: 0 topLeftRadius: 0
@ -836,6 +829,28 @@ in
NumberAnimation { duration: 220; easing.type: Easing.OutCubic } NumberAnimation { duration: 220; easing.type: Easing.OutCubic }
} }
Item {
id: dropdownContent
anchors.fill: parent
}
}
}
// 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 { Column {
id: menuItems id: menuItems
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@ -901,117 +916,13 @@ in
} }
} }
} }
}
// Calendar popup // Calendar popup
PopupWindow { BarDropdown {
id: calPopup id: calPopup
anchor.window: bar dropdownX: bar.width / 2
anchor.rect.x: bar.width / 2 - (fullWidth + 16) / 2 fullWidth: calCol.width + 32
anchor.rect.y: bar.height fullHeight: calCol.height + 24
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 }
}
Column { Column {
id: calCol id: calCol