added tray menu + fixed antialising

This commit is contained in:
2026-08-11 11:08:44 +02:00
parent 58e0ba2f99
commit ccb54f01d9
22 changed files with 581 additions and 167 deletions
+150
View File
@@ -0,0 +1,150 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/config"
import "root:/components"
// The tray's context menu, on the shell's own slab.
//
// Quickshell can hand a StatusNotifier menu straight to Qt's platform menu
// implementation, but that draws a stock widget popup — square corners, system
// palette, no lean — hanging off the corner of a shell that has none of those
// things. This walks the menu with QsMenuOpener instead and draws it as a
// P5Panel, so a right click on a tray icon produces the same object language as
// everything else on the rail.
//
// Dismissal is a Hyprland focus grab: click anywhere outside and the compositor
// tells us to close.
PopupWindow {
id: root
required property Item anchorItem
property var menuHandle: null
// Room around the slab for the entry overshoot and the offset black copy.
readonly property int bleed: 20
readonly property int minWidth: 160
readonly property int maxWidth: 320
// The slab's own lean and padding, which the entries sit inside. Sizing the
// window to the entry widths alone left every label short by that inset and
// elided perfectly ordinary items.
readonly property real slabLean: 0.06
readonly property real slabPad: Theme.padS
readonly property real contentHeight: list.implicitHeight + slabPad * 2
readonly property real contentWidth:
Math.max(root.minWidth, Math.min(root.maxWidth, list.menuWidth))
+ (slabPad + contentHeight * slabLean) * 2
anchor.item: anchorItem
anchor.edges: Edges.Left
anchor.gravity: Edges.Left
anchor.adjustment: PopupAdjustment.SlideY
anchor.margins.right: Theme.popoutGap
implicitWidth: contentWidth + bleed * 2
implicitHeight: contentHeight + bleed * 2
color: "transparent"
visible: shown
property bool shown: false
function open() {
if (!menuHandle) return;
closeAnim.stop();
shown = true;
openAnim.restart();
}
function close() {
if (!shown) return;
openAnim.stop();
closeAnim.restart();
}
function toggle() {
if (shown) close(); else open();
}
// The grab can only take hold once the popup is actually mapped. Asked for
// in the same tick as `shown`, Hyprland has no surface to hand it and
// clears it immediately — which closed the menu the instant it opened.
Timer {
id: grabDelay
interval: 60
running: root.shown
}
HyprlandFocusGrab {
active: root.shown && !grabDelay.running
windows: [root]
onCleared: root.close()
}
Item {
id: holder
x: root.bleed
y: root.bleed
width: root.contentWidth
height: root.contentHeight
opacity: 0
scale: 0.94
transformOrigin: Item.Right
PopoutSurface {
anchors.fill: parent
tone: Theme.accent
padding: root.slabPad
chop: 12
lean: root.slabLean
TrayMenuList {
id: list
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
// Only walk the menu while it is on screen. Bound unconditionally,
// every tray app would get a GetLayout the moment the shell
// starts, for a menu nobody has asked for yet.
menuHandle: root.shown ? root.menuHandle : null
onActivated: root.close()
}
}
}
ParallelAnimation {
id: openAnim
NumberAnimation {
target: holder; property: "opacity"
to: 1.0; duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: holder; property: "scale"
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
}
NumberAnimation {
target: holder; property: "x"
from: root.bleed + 14; to: root.bleed
duration: Theme.durSlow; easing.type: Easing.OutBack
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: holder; property: "opacity"
to: 0.0; duration: Theme.durFast; easing.type: Easing.InQuad
}
NumberAnimation {
target: holder; property: "scale"
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
}
onFinished: {
root.shown = false;
holder.x = root.bleed;
}
}
}