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 (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