Files
dotfiles/quickshell/overlays/PowerMenu.qml
T
2026-08-11 01:53:26 +02:00

275 lines
8.8 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// Full-screen power menu. Crimson stripes sweep in behind a stack of leaning
// buttons; Escape or a click on the backdrop dismisses it.
PanelWindow {
id: root
required property var modelData
screen: modelData
visible: Actions.powerMenuOpen || closeAnim.running
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-power"
WlrLayershell.keyboardFocus: Actions.powerMenuOpen
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
readonly property bool open: Actions.powerMenuOpen
onOpenChanged: {
if (open) {
openAnim.restart();
} else {
closeAnim.restart();
}
}
// ------------------------------------------------------------- backdrop
Rectangle {
id: backdrop
anchors.fill: parent
color: Theme.alpha(Theme.base, 0.82)
opacity: 0
TapHandler {
onTapped: Actions.closePowerMenu()
}
}
// Stripes and wedge, shared with the lock screen. The wedge used to lean at
// 0.32 here, which was its own angle and nothing else's; P5Backdrop leans
// it at Theme.skew like every other edge in the shell.
P5Backdrop {
id: ground
anchors.fill: parent
reveal: 0
stripeOpacity: 0.10
wedgeOpacity: 0.35
wedgeSpan: 0.34
// The live desktop is still visible behind this, so a dot wash would be
// laid over other people's windows rather than over a ground of our
// own. The lock owns the halftone; here it just muddies things.
halftone: false
}
// ---------------------------------------------------------------- title
Column {
id: title
anchors.left: parent.left
anchors.leftMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -60
spacing: -14
opacity: 0
SplitText {
text: "TAKE"
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
}
SplitText {
text: "YOUR"
pixelSize: Theme.fsMega
color: Theme.accent
split: 4
splitOpacity: 0.5
}
SplitText {
text: "TIME."
pixelSize: Theme.fsMega
split: 4
splitOpacity: 0.9
}
Text {
text: "— TAE TAKEMI"
color: Theme.primary
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.letterSpacing: 5
topPadding: 24
renderType: Text.NativeRendering
}
}
// -------------------------------------------------------------- actions
Column {
id: actions
anchors.right: parent.right
anchors.rightMargin: root.width * 0.12
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padM
// The widest slab in the stack. Fixed rather than derived so the ragged
// x offsets below cannot feed back into the Column's own width.
readonly property int stackWidth: 312
width: stackWidth
Repeater {
id: buttonRepeater
model: [
{ label: "Lock", icon: "󰌾", danger: false, act: "lock" },
{ label: "Suspend", icon: "󰤄", danger: false, act: "suspend" },
{ label: "Hibernate", icon: "󰒲", danger: false, act: "hibernate" },
{ label: "Log Out", icon: "󰗽", danger: true, act: "logout" },
{ label: "Reboot", icon: "󰜉", danger: true, act: "reboot" },
{ label: "Shut Down", icon: "󰐥", danger: true, act: "shutdown" }
]
// A carrier item owns the staggered slide so the button keeps its
// own hover transform.
Item {
id: entry
required property var modelData
required property int index
readonly property int slabWidth: actions.stackWidth
width: entry.slabWidth
height: 52
opacity: 0
transform: [
Translate { id: slide; x: 90 },
Rotation {
id: tilt
angle: -3
origin.x: entry.slabWidth / 2
origin.y: 26
}
]
P5Button {
anchors.fill: parent
text: entry.modelData.label
icon: entry.modelData.icon
destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary
lean: Theme.skew
lunge: -10
onClicked: root.invoke(entry.modelData.act)
}
// Staggered entry, one button after another.
Connections {
target: root
function onOpenChanged() {
if (root.open) entryAnim.restart();
else entry.opacity = 0;
}
}
SequentialAnimation {
id: entryAnim
PauseAnimation { duration: 40 + entry.index * 55 }
ParallelAnimation {
NumberAnimation {
target: entry; property: "opacity"
to: 1; duration: Theme.durBase
}
NumberAnimation {
target: slide; property: "x"
from: 90; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
}
// Slabs land out of true and snap square. Sliding alone
// reads like a slide deck; the tilt is what makes it
// land like a card being thrown down. Same direction
// every time — the motion is the flourish, the geometry
// stays disciplined.
NumberAnimation {
target: tilt; property: "angle"
from: -3; to: 0
duration: Theme.durSlow
easing.type: Easing.OutBack
easing.overshoot: 2.2
}
}
}
}
}
}
// Hint line.
Text {
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
text: "ESC TO CANCEL"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 4
opacity: backdrop.opacity
renderType: Text.NativeRendering
}
function invoke(what: string) {
Actions.closePowerMenu();
switch (what) {
case "lock": Actions.lock(); break;
case "suspend": Actions.suspend(); break;
case "hibernate": Actions.hibernate(); break;
case "logout": Actions.logout(); break;
case "reboot": Actions.reboot(); break;
case "shutdown": Actions.shutdown(); break;
}
}
// Escape closes. The layer takes exclusive focus while open.
Item {
anchors.fill: parent
focus: root.open
Keys.onEscapePressed: Actions.closePowerMenu()
}
ParallelAnimation {
id: openAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 1; duration: Theme.durBase; easing.type: Easing.OutQuad
}
// One number now drives the slide, the stripe opacity and the wedge.
NumberAnimation {
target: ground; property: "reveal"
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
}
NumberAnimation {
target: title; property: "opacity"
to: 1; duration: Theme.durSlow
}
}
ParallelAnimation {
id: closeAnim
NumberAnimation {
target: backdrop; property: "opacity"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: ground; property: "reveal"
to: 0; duration: Theme.durFast
}
NumberAnimation {
target: title; property: "opacity"
to: 0; duration: Theme.durFast
}
}
}