import QtQuick import QtQuick.Shapes 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() } } // Diagonal stripe field sweeping in from the left. Image { id: stripes anchors.fill: parent source: Theme.texStripes fillMode: Image.Tile opacity: 0 smooth: false layer.enabled: true layer.effect: TintEffect { tintColor: Theme.accent } transform: Translate { id: stripeShift x: -root.width } } // A crimson wedge behind the buttons. Shape { id: wedge anchors.fill: parent opacity: 0 preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: Theme.alpha(Theme.accentDim, 0.35) strokeColor: "transparent" PathPolyline { path: [ Qt.point(root.width * 0.18, 0), Qt.point(root.width * 0.52, 0), Qt.point(root.width * 0.34, root.height), Qt.point(0, root.height), Qt.point(root.width * 0.18, 0) ] } } } // ---------------------------------------------------------------- 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 // Six identical slabs stacked at a constant width is the most // obviously machine-made thing in the shell. Vary the width and // push each row a different distance off the right margin, so // the stack has a ragged edge on both sides. readonly property int slabWidth: [286, 252, 300, 268, 312, 244][entry.index] readonly property int nudge: [0, 22, 8, 34, 14, 26][entry.index] // Column owns y, so the ragged edge is an explicit x offset // measured against the widest slab in the stack. width: entry.slabWidth height: 52 x: actions.stackWidth - entry.slabWidth - entry.nudge 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.leanFor(entry.index) lunge: -10 - entry.index * 2 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. NumberAnimation { target: tilt; property: "angle" from: entry.index % 2 === 0 ? -4 : 4; 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 } NumberAnimation { target: stripes; property: "opacity" to: 0.1; duration: Theme.durSlow } NumberAnimation { target: stripeShift; property: "x" from: -root.width; to: 0 duration: Theme.durLazy; easing.type: Easing.OutExpo } NumberAnimation { target: wedge; property: "opacity" to: 1; duration: Theme.durSlow; 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: stripes; property: "opacity" to: 0; duration: Theme.durFast } NumberAnimation { target: wedge; property: "opacity" to: 0; duration: Theme.durFast } NumberAnimation { target: title; property: "opacity" to: 0; duration: Theme.durFast } } }