320 lines
10 KiB
QML
320 lines
10 KiB
QML
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
|
|
}
|
|
}
|
|
}
|