theme: calm YoRHa lock screen and power menu; nvim and btop follow
The lock screen and the power menu lose the last Persona motion: no split display type, no grid sweeping in behind a dark column, no tilted slabs, no skewed password blocks or hard shake. Both are now one small group of type in the dark — the porthole mark, a line of light display type, a status line, and either a rounded password field that fills with dots or a NieR menu whose selected row is a pill of light (rust for the session-ending three). The lock keeps the wallpaper almost whole; a wrong password nudges the field once and turns it to rust. P5Backdrop and the grid texture had no users left and go. Neovim moves to a local yorha colorscheme (nvim submodule) and btop gets a yorha theme with hairline boxes and grey-to-light gradients. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a8c19961e2
commit
63a910bf14
11 files changed
+558
-542
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
|
||||
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
|
||||
color_theme = "/home/narl/.config/btop/themes/catppuccin_mocha.theme"
|
||||
color_theme = "/home/narl/.config/btop/themes/yorha.theme"
|
||||
|
||||
#* If the theme set background should be shown, set to False if you want terminal background transparency.
|
||||
theme_background = false
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# YoRHa dark — matched to ~/.config/quickshell/config/Theme.qml and
|
||||
# ~/.config/alacritty/yorha.toml.
|
||||
#
|
||||
# The void ground and sage text of the space-1 wallpaper. Boxes are hairline
|
||||
# grey, not four competing hues; the light marks what is selected. Every
|
||||
# gradient climbs from the suit's grey up to the light, and only temperature
|
||||
# and load run on into the hull's rust, the shell's one colour for "wrong".
|
||||
|
||||
theme[main_bg]="#0f110e"
|
||||
theme[main_fg]="#ccd2cb"
|
||||
theme[title]="#eef1ea"
|
||||
theme[hi_fg]="#eef1ea"
|
||||
theme[selected_bg]="#20241f"
|
||||
theme[selected_fg]="#eef1ea"
|
||||
theme[inactive_fg]="#545c52"
|
||||
theme[graph_text]="#9fa7a2"
|
||||
theme[meter_bg]="#20241f"
|
||||
theme[proc_misc]="#9fa7a2"
|
||||
|
||||
theme[cpu_box]="#383e35"
|
||||
theme[mem_box]="#383e35"
|
||||
theme[net_box]="#383e35"
|
||||
theme[proc_box]="#383e35"
|
||||
theme[div_line]="#2a2f28"
|
||||
|
||||
theme[temp_start]="#8e978d"
|
||||
theme[temp_mid]="#c9b27c"
|
||||
theme[temp_end]="#c8684f"
|
||||
|
||||
theme[cpu_start]="#79847a"
|
||||
theme[cpu_mid]="#bcc1bf"
|
||||
theme[cpu_end]="#c8684f"
|
||||
|
||||
theme[free_start]="#545c52"
|
||||
theme[free_mid]="#79847a"
|
||||
theme[free_end]="#9fa7a2"
|
||||
|
||||
theme[cached_start]="#545c52"
|
||||
theme[cached_mid]="#7f9aa8"
|
||||
theme[cached_end]="#96afbc"
|
||||
|
||||
theme[available_start]="#545c52"
|
||||
theme[available_mid]="#86a8a0"
|
||||
theme[available_end]="#9cbdb4"
|
||||
|
||||
theme[used_start]="#79847a"
|
||||
theme[used_mid]="#bcc1bf"
|
||||
theme[used_end]="#eef1ea"
|
||||
|
||||
theme[download_start]="#545c52"
|
||||
theme[download_mid]="#8fa37e"
|
||||
theme[download_end]="#a5b893"
|
||||
|
||||
theme[upload_start]="#545c52"
|
||||
theme[upload_mid]="#c9b27c"
|
||||
theme[upload_end]="#d8c491"
|
||||
|
||||
theme[process_start]="#79847a"
|
||||
theme[process_mid]="#9fa7a2"
|
||||
theme[process_end]="#eef1ea"
|
||||
+1
-1
Submodule nvim updated: 4736228253...19e0db60a1.
+21
-9
@@ -52,9 +52,10 @@ its own panel.
|
||||
The shell uses three deliberate surface tiers. **Chrome** (the hull and
|
||||
`ChromeSurface`) is the calm, texture-free bar, rail, and tooltip layer. **Transient**
|
||||
(`PopoutSurface`) is reserved for expressive menus and detail panels, with one
|
||||
clear composition and stronger separation from the desktop. **Cinematic**
|
||||
(`P5Backdrop`) belongs only to scene-scale experiences such as power and lock;
|
||||
it is never used as decoration inside ordinary chrome or popouts.
|
||||
clear composition and stronger separation from the desktop. **Scene**
|
||||
surfaces — the lock screen and the power menu — have no panel at all: the
|
||||
desktop or the wallpaper dims, and one small group of type (the mark, a line of
|
||||
thin display type, a short log, a menu or a field) stands in the dark.
|
||||
|
||||
```
|
||||
shell.qml entrypoint — hull, bar, rail, dashboard, power menu, toasts, OSD per screen
|
||||
@@ -217,6 +218,12 @@ Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
|
||||
styled in an ordinary window (`qs -p lockpreview.qml`) without locking the
|
||||
session.
|
||||
|
||||
It is the wallpaper, barely softened, with the face set in the dark of the
|
||||
porthole: the mark, the time in light display type, the date, a status log
|
||||
(battery, network, what is playing, unread notices), and a rounded field that
|
||||
fills with dots as you type. The dots breathe while PAM checks; a wrong
|
||||
password turns them and the field's rim to rust and nudges the field once.
|
||||
|
||||
If the shell ever dies while the session is locked, the compositor keeps the
|
||||
session secured by protocol and there is no unlocker left to talk to — recovery
|
||||
means restarting Hyprland from a TTY.
|
||||
@@ -270,9 +277,9 @@ Borders are structure and stay neutral.
|
||||
`Theme.tracking`, on a one-pixel soft drop. The two ghosts either side of it
|
||||
appear only while `glitch()` runs — NieR's brief data-corruption flicker.
|
||||
|
||||
**Texture is tiered.** Chrome (bar, rail, tooltips) and transient panels
|
||||
(popouts, OSD, toasts) are flat. The hairline grid and the dot wash live only in
|
||||
`P5Backdrop`, behind the lock screen and the power menu.
|
||||
**Texture is rare.** Chrome (bar, rail, tooltips), transient panels (popouts,
|
||||
OSD, toasts) and the two scene surfaces are flat. Depth comes from the hull's
|
||||
dark, the rim hairline and the wallpaper, not from patterns.
|
||||
|
||||
**Type has a floor.** Nothing renders below `Theme.fsMicro` (10px).
|
||||
|
||||
@@ -284,6 +291,8 @@ appear only while `glitch()` runs — NieR's brief data-corruption flicker.
|
||||
| `hypr/lua/palette.lua` | window borders, groups |
|
||||
| `alacritty/yorha.toml` | the terminal |
|
||||
| `fish/conf.d/yorha_theme.fish` | the prompt and pager |
|
||||
| `nvim/lua/yorha.lua` | the editor and its statusline |
|
||||
| `btop/themes/yorha.theme` | the resource monitor |
|
||||
|
||||
Everything else derives. `glow` *is* `accent`, and `primary` is plain `text`.
|
||||
`Theme.heat()` is a continuous text → light → red ramp, so a CPU figure that
|
||||
@@ -308,9 +317,12 @@ network and bluetooth, but keep running as the secret and pairing agents.
|
||||
Alacritty only live-reloads when `alacritty.toml` itself changes, so touch
|
||||
it after editing the palette.
|
||||
The Catppuccin and `takemi.toml` files are left in place, unreferenced.
|
||||
- **Neovim** — still the `catppuccin` plugin, flavour mocha, configured in
|
||||
`nvim/lua/plugins/ui.lua`. Not yet moved to the YoRHa palette.
|
||||
- **GTK and Qt** — still `catppuccin-mocha-blue`. Not yet moved either.
|
||||
- **Neovim** — `nvim/lua/yorha.lua` (loaded by `colors/yorha.lua`, with a
|
||||
matching lualine theme). Syntax in the terminal's earth hues, floats as hull
|
||||
surfaces with rounded rims, menus selecting with the light slab.
|
||||
- **btop** — `btop/themes/yorha.theme`. Hairline boxes; gradients climb from
|
||||
grey to the light, and only temperature and CPU load run on into rust.
|
||||
- **GTK and Qt** — still `catppuccin-mocha-blue`. Not yet moved.
|
||||
- **Fish** — `fish/conf.d/yorha_theme.fish`.
|
||||
- **The launcher** — no external theming to keep in step, because it is part of
|
||||
this shell (`launcher/`). Fuzzel is gone; `takemi-dmenu` stands in for
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 337 B |
@@ -1,91 +0,0 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// The shared ground for the two full-screen surfaces, in the NieR menu
|
||||
// pattern: a hairline grid that sweeps in from the left, a darker column down
|
||||
// the left-hand side for the menu or the clock to stand in, and a dot wash
|
||||
// over the top.
|
||||
//
|
||||
// The lock screen and the power menu both stand on this one component, which
|
||||
// is what makes them read as the same object seen twice rather than as two
|
||||
// guesses at the same brief.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// 0 = withdrawn, 1 = fully present. Drives the grid slide, the grid
|
||||
// opacity and the column together, so a caller animates one number instead
|
||||
// of keeping three in sync. The power menu runs it both ways on open and
|
||||
// close; the lock runs it once on entry.
|
||||
property real reveal: 1
|
||||
|
||||
property color tone: Theme.accent
|
||||
|
||||
// The column: a band of the void, edged by a hairline of the light.
|
||||
// `wedge*` names are kept from the Persona layout so callers did not have
|
||||
// to move; there is nothing wedge-shaped left.
|
||||
property bool wedge: true
|
||||
property real wedgeOpacity: 0.35
|
||||
property real wedgeLeft: 0
|
||||
property real wedgeSpan: 0.34
|
||||
|
||||
property bool stripes: true
|
||||
property real stripeOpacity: 0.05
|
||||
|
||||
property bool halftone: true
|
||||
property real halftoneOpacity: 0.05
|
||||
property color halftoneColor: Theme.primary
|
||||
|
||||
// Hairline grid, sliding in from off-screen left.
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: root.stripes && root.reveal > 0
|
||||
source: Theme.texGrid
|
||||
fillMode: Image.Tile
|
||||
opacity: root.stripeOpacity * root.reveal
|
||||
smooth: false
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: TintEffect {
|
||||
tintColor: root.tone
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
x: -root.width * (1 - root.reveal)
|
||||
}
|
||||
}
|
||||
|
||||
// The column. Drawn after the grid so it reads as a panel laid over it.
|
||||
Item {
|
||||
x: root.wedgeLeft * root.width
|
||||
width: root.wedgeSpan * root.width
|
||||
height: root.height
|
||||
visible: root.wedge && root.reveal > 0
|
||||
opacity: root.reveal
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(Theme.ink, root.wedgeOpacity)
|
||||
}
|
||||
|
||||
// Its edge, drawn top-down as the column arrives.
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
width: 1
|
||||
height: parent.height * root.reveal
|
||||
color: Theme.alpha(root.tone, 0.35)
|
||||
}
|
||||
}
|
||||
|
||||
// Dot wash over the whole thing.
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
visible: root.halftone
|
||||
source: Theme.texDots
|
||||
fillMode: Image.Tile
|
||||
opacity: root.halftoneOpacity
|
||||
smooth: false
|
||||
layer.enabled: true
|
||||
layer.effect: TintEffect { tintColor: root.halftoneColor }
|
||||
}
|
||||
}
|
||||
@@ -240,10 +240,8 @@ Singleton {
|
||||
readonly property var easeIn: [0.55, 0.0, 0.9, 0.35, 1, 1]
|
||||
|
||||
// -------------------------------------------------------------- textures
|
||||
// A 1px dot every 8px, and a 32px hairline grid — the two backgrounds the
|
||||
// NieR menus are printed on.
|
||||
// A 1px dot every 8px, the ground NieR's menus are printed on.
|
||||
readonly property url texDots: "root:/assets/dots.png"
|
||||
readonly property url texGrid: "root:/assets/grid.png"
|
||||
readonly property url texScanline: "root:/assets/scanline.png"
|
||||
|
||||
// Last resort only. The live wallpaper comes from the Wallpaper singleton,
|
||||
|
||||
+269
-242
@@ -10,6 +10,13 @@ import "root:/lock"
|
||||
// The lock screen's whole face, kept separate from WlSessionLockSurface so it
|
||||
// can be instantiated in an ordinary window for development without actually
|
||||
// locking the session.
|
||||
//
|
||||
// Quiet on purpose. The wallpaper stays almost whole — the porthole, the
|
||||
// planet, the girl looking out — and the face is one small group of type set
|
||||
// in the dark of the porthole: the mark, the time, the date, a short status
|
||||
// log, and a rounded field to type into. Nothing sweeps, splits or shakes;
|
||||
// the group resolves in, and a wrong password nudges it once and turns its
|
||||
// dots to rust.
|
||||
Item {
|
||||
id: surface
|
||||
|
||||
@@ -20,12 +27,12 @@ Item {
|
||||
property string status: ""
|
||||
property bool failed: false
|
||||
property bool busy: pam.active
|
||||
readonly property real uiScale: Math.max(0.72, Math.min(1.15,
|
||||
readonly property real uiScale: Math.max(0.72, Math.min(1.3,
|
||||
Math.min(width / 1920, height / 1080)))
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.base
|
||||
color: Theme.ink
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- background
|
||||
@@ -43,240 +50,254 @@ Item {
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: false
|
||||
cache: true
|
||||
opacity: 0
|
||||
|
||||
// Barely softened: enough that the scene steps back behind the type,
|
||||
// not so much that it stops being a place.
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
blurEnabled: true
|
||||
blur: 0.55
|
||||
blurMax: 48
|
||||
saturation: -0.15
|
||||
brightness: -0.35
|
||||
blur: 0.2
|
||||
blurMax: 32
|
||||
saturation: -0.1
|
||||
brightness: -0.18
|
||||
}
|
||||
}
|
||||
|
||||
// Wash pulling the photo down toward the shell's ground. Was described as
|
||||
// a teal wash back when the UI was teal; the colours it actually uses are
|
||||
// the neutral surfaces, which is what it should have been all along.
|
||||
// Shade deepening toward the left, where the type sits in the porthole's
|
||||
// dark, and a breath of it along the foot.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Theme.alpha(Theme.deep, 0.55) }
|
||||
GradientStop { position: 0.6; color: Theme.alpha(Theme.base, 0.75) }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.base, 0.92) }
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.72) }
|
||||
GradientStop { position: 0.45; color: Theme.alpha(Theme.ink, 0.28) }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.18) }
|
||||
}
|
||||
}
|
||||
|
||||
// The same wedge-and-stripes ground the power menu stands on. The wedge
|
||||
// sits under the clock, which is where the power menu puts it under the
|
||||
// title — so the two screens land the accent mass in the same place.
|
||||
P5Backdrop {
|
||||
id: backdrop
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
reveal: 0
|
||||
wedgeSpan: 0.30
|
||||
wedgeOpacity: 0.30
|
||||
stripeOpacity: 0.04
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.6; color: "transparent" }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.5) }
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ clock
|
||||
// ------------------------------------------------------------- face
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
// The house mark, the same porthole the bar carries far left. Small, above
|
||||
// the clock — the lock's hero is the time, not the branding.
|
||||
Column {
|
||||
id: face
|
||||
x: parent.width * 0.14
|
||||
y: parent.height * 0.2
|
||||
width: 380
|
||||
scale: surface.uiScale
|
||||
transformOrigin: Item.TopLeft
|
||||
opacity: 0
|
||||
spacing: 0
|
||||
|
||||
transform: Translate { id: drift }
|
||||
|
||||
// The house mark, the same porthole the bar carries far left.
|
||||
Porthole {
|
||||
id: mark
|
||||
anchors.left: timeBlock.left
|
||||
anchors.bottom: timeBlock.top
|
||||
anchors.bottomMargin: 10
|
||||
size: 28
|
||||
opacity: timeBlock.opacity
|
||||
}
|
||||
|
||||
Column {
|
||||
id: timeBlock
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: parent.width * 0.1
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: parent.height * 0.18
|
||||
spacing: -18
|
||||
|
||||
SplitText {
|
||||
text: Qt.formatDateTime(clock.date, "HH")
|
||||
pixelSize: Theme.fsMega * 1.7
|
||||
split: 5
|
||||
}
|
||||
|
||||
SplitText {
|
||||
text: Qt.formatDateTime(clock.date, "mm")
|
||||
pixelSize: Theme.fsMega * 1.7
|
||||
color: Theme.primary
|
||||
split: 5
|
||||
size: 26
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(clock.date, "dddd, dd MMMM").toUpperCase()
|
||||
topPadding: Theme.padM
|
||||
text: Qt.formatDateTime(clock.date, "HH:mm")
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsMega * 1.35
|
||||
font.weight: Font.Light
|
||||
font.letterSpacing: 2
|
||||
// Scaled with the face, so distance-field glyphs: native hinting
|
||||
// at this size under a scale transform comes out stepped.
|
||||
renderType: Text.QtRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
leftPadding: 4
|
||||
text: Qt.formatDateTime(clock.date, "dddd · dd MMMM").toUpperCase()
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsBody
|
||||
font.letterSpacing: 6
|
||||
topPadding: 34
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ status rail
|
||||
Column {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: parent.width * 0.1
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: parent.height * 0.2
|
||||
spacing: Theme.padM
|
||||
width: Math.min(300, parent.width * 0.28)
|
||||
scale: surface.uiScale
|
||||
transformOrigin: Item.TopRight
|
||||
|
||||
RailRow {
|
||||
width: parent.width
|
||||
visible: Battery.available
|
||||
glyph: Battery.icon
|
||||
label: Math.round(Battery.percent) + "%"
|
||||
detail: Battery.timeLabel
|
||||
tone: Battery.tint
|
||||
}
|
||||
|
||||
RailRow {
|
||||
width: parent.width
|
||||
glyph: Net.icon
|
||||
label: Net.label
|
||||
detail: Sys.netUp ? Sys.netIp : "no link"
|
||||
tone: Sys.netUp ? Theme.primary : Theme.muted
|
||||
}
|
||||
|
||||
RailRow {
|
||||
width: parent.width
|
||||
visible: Media.hasPlayer
|
||||
glyph: Media.playing ? "" : ""
|
||||
label: Media.title
|
||||
detail: Media.artist
|
||||
tone: Theme.primary
|
||||
}
|
||||
|
||||
RailRow {
|
||||
width: parent.width
|
||||
visible: Notifs.count > 0
|
||||
glyph: ""
|
||||
label: Notifs.count + " notification" + (Notifs.count === 1 ? "" : "s")
|
||||
detail: Notifs.history.length > 0 ? Notifs.history[0].summary : ""
|
||||
tone: Theme.accent
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- auth panel
|
||||
Item {
|
||||
id: authBlock
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: parent.height * 0.14
|
||||
width: Math.min(460, parent.width - Theme.padXL * 2)
|
||||
height: 150
|
||||
scale: surface.uiScale
|
||||
transformOrigin: Item.Bottom
|
||||
|
||||
transform: Translate { id: shakeShift }
|
||||
|
||||
SplitText {
|
||||
id: prompt
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
text: surface.failed
|
||||
? (surface.status || "WRONG").toUpperCase()
|
||||
: (surface.busy ? "CHECKING…" : "WHO GOES THERE")
|
||||
color: surface.failed ? Theme.alarm : Theme.text
|
||||
pixelSize: Theme.fsTitle
|
||||
letterSpacing: 4
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
split: surface.failed ? 4 : 1.6
|
||||
}
|
||||
|
||||
// Password field: one chopped block per character.
|
||||
P5Panel {
|
||||
id: field
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: prompt.bottom
|
||||
anchors.topMargin: Theme.padL
|
||||
width: Math.min(380, authBlock.width - Theme.padL * 2)
|
||||
height: 56
|
||||
fill: Theme.mantle
|
||||
fillOpacity: 0.94
|
||||
border: surface.failed
|
||||
? Theme.accent
|
||||
: (input.activeFocus ? Theme.primary : Theme.outline)
|
||||
borderWidth: 2
|
||||
slash: true
|
||||
slashColor: surface.failed ? Theme.alarm : Theme.primary
|
||||
slashWidth: 6
|
||||
halftone: true
|
||||
halftoneColor: surface.failed ? Theme.alarm : Theme.primary
|
||||
halftoneOpacity: 0.08
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
visible: surface.entry.length > 0
|
||||
|
||||
Repeater {
|
||||
model: Math.min(surface.entry.length, 22)
|
||||
|
||||
Skew {
|
||||
required property int index
|
||||
width: 10
|
||||
height: 20
|
||||
color: surface.failed ? Theme.alarm : Theme.primary
|
||||
|
||||
// Each new block snaps in.
|
||||
Component.onCompleted: pop.start()
|
||||
NumberAnimation {
|
||||
id: pop
|
||||
target: parent
|
||||
property: "scale"
|
||||
from: 0.2; to: 1
|
||||
duration: Theme.durBase
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: surface.entry.length === 0 && !surface.busy
|
||||
text: "PASSWORD"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightDisplay
|
||||
font.letterSpacing: 5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
// Busy sweep.
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 6
|
||||
width: 60
|
||||
height: 3
|
||||
color: Theme.primary
|
||||
visible: surface.busy
|
||||
Item { width: 1; height: Theme.padXL }
|
||||
|
||||
XAnimator on x {
|
||||
// ------------------------------------------------------ status log
|
||||
Column {
|
||||
width: parent.width
|
||||
leftPadding: 4
|
||||
|
||||
StatusRow {
|
||||
width: parent.width - 4
|
||||
visible: Battery.available
|
||||
label: "Battery"
|
||||
detail: Math.round(Battery.percent) + "%"
|
||||
+ (Battery.timeLabel ? " " + Battery.timeLabel : "")
|
||||
tone: Battery.tint
|
||||
}
|
||||
|
||||
StatusRow {
|
||||
width: parent.width - 4
|
||||
label: "Network"
|
||||
detail: Sys.netUp ? Net.label : "no link"
|
||||
tone: Sys.netUp ? Theme.accentSoft : Theme.overlay
|
||||
}
|
||||
|
||||
StatusRow {
|
||||
width: parent.width - 4
|
||||
visible: Media.hasPlayer
|
||||
label: Media.playing ? "Playing" : "Paused"
|
||||
detail: Media.artist ? Media.title + " — " + Media.artist : Media.title
|
||||
tone: Media.playing ? Theme.accent : Theme.primaryDim
|
||||
}
|
||||
|
||||
StatusRow {
|
||||
width: parent.width - 4
|
||||
visible: Notifs.count > 0
|
||||
label: "Notices"
|
||||
detail: Notifs.count + (Notifs.count === 1 ? " message" : " messages")
|
||||
tone: Theme.accentSoft
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.padXL }
|
||||
|
||||
// ------------------------------------------------------ the field
|
||||
Item {
|
||||
id: authBlock
|
||||
width: parent.width
|
||||
height: field.height + hint.height + Theme.padS
|
||||
|
||||
transform: Translate { id: nudge }
|
||||
|
||||
Rectangle {
|
||||
id: field
|
||||
width: parent.width
|
||||
height: 48
|
||||
radius: height / 2
|
||||
color: Theme.alpha(Theme.hull, 0.86)
|
||||
border.width: 1
|
||||
border.color: surface.failed
|
||||
? Theme.alarm
|
||||
: (input.activeFocus && surface.entry.length > 0 ? Theme.outline : Theme.rim)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
|
||||
Icon {
|
||||
id: lockGlyph
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ""
|
||||
color: surface.failed ? Theme.alarm : Theme.muted
|
||||
font.pixelSize: Theme.fsLarge
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: lockGlyph.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: surface.entry.length === 0
|
||||
text: "PASSWORD"
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.letterSpacing: Theme.tracking * 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
// One dot per character, each resolving in as it is typed.
|
||||
// They breathe while PAM is checking.
|
||||
Row {
|
||||
id: dots
|
||||
anchors.left: lockGlyph.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.right: enter.left
|
||||
anchors.rightMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 7
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
model: Math.min(surface.entry.length, 28)
|
||||
|
||||
Rectangle {
|
||||
id: pip
|
||||
required property int index
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 7
|
||||
height: 7
|
||||
radius: width / 2
|
||||
color: surface.failed ? Theme.alarm : Theme.accent
|
||||
opacity: 0
|
||||
scale: 0.4
|
||||
|
||||
Component.onCompleted: arrive.start()
|
||||
|
||||
ParallelAnimation {
|
||||
id: arrive
|
||||
NumberAnimation {
|
||||
target: pip; property: "opacity"
|
||||
to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: pip; property: "scale"
|
||||
to: 1; duration: Theme.durBase; easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: surface.busy
|
||||
loops: Animation.Infinite
|
||||
from: 20
|
||||
to: field.width - 80
|
||||
duration: 700
|
||||
easing.type: Easing.InOutQuad
|
||||
alwaysRunToEnd: true
|
||||
NumberAnimation { to: 0.35; duration: 520; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { to: 1; duration: 520; easing.type: Easing.InOutSine }
|
||||
}
|
||||
}
|
||||
|
||||
// Submit: a disc at the field's end that lights once there is
|
||||
// something to send.
|
||||
Rectangle {
|
||||
id: enter
|
||||
readonly property bool ready: surface.entry.length > 0 && !surface.busy
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
height: 32
|
||||
radius: width / 2
|
||||
color: enter.ready ? Theme.accent : "transparent"
|
||||
border.width: enter.ready ? 0 : 1
|
||||
border.color: Theme.rim
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase }
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: enter.ready ? Theme.ink : Theme.overlay
|
||||
font.pixelSize: Theme.fsLarge
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: surface.submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +313,7 @@ Item {
|
||||
|
||||
onTextChanged: {
|
||||
surface.entry = text;
|
||||
if (surface.failed && text.length === 0) surface.failed = false;
|
||||
if (surface.failed && text.length > 0) surface.failed = false;
|
||||
}
|
||||
|
||||
onAccepted: surface.submit()
|
||||
@@ -304,30 +325,38 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Under the field: who is locked, what is happening, or what went
|
||||
// wrong — one line, never more.
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
id: hint
|
||||
anchors.top: field.bottom
|
||||
anchors.topMargin: Theme.padM
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padL
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ""
|
||||
color: Theme.muted
|
||||
font.pixelSize: Theme.fsBody
|
||||
width: 4
|
||||
height: 4
|
||||
radius: 2
|
||||
color: surface.failed ? Theme.alarm : Theme.overlay
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Quickshell.env("USER") || "USER"
|
||||
color: Theme.muted
|
||||
text: surface.failed
|
||||
? (surface.status || "Access denied").toUpperCase()
|
||||
: (surface.busy ? "VERIFYING" : (Quickshell.env("USER") || "user"))
|
||||
color: surface.failed ? Theme.alarm : Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.letterSpacing: 3
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep focus on the field no matter what.
|
||||
Timer {
|
||||
@@ -362,62 +391,60 @@ Item {
|
||||
surface.entry = "";
|
||||
surface.failed = false;
|
||||
} else {
|
||||
surface.failed = true;
|
||||
if (surface.status === "") surface.status = "Access denied";
|
||||
input.text = "";
|
||||
surface.entry = "";
|
||||
shake.restart();
|
||||
surface.failed = true;
|
||||
if (surface.status === "") surface.status = "Access denied";
|
||||
refuse.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onError: err => {
|
||||
surface.failed = true;
|
||||
surface.status = "PAM error";
|
||||
input.text = "";
|
||||
surface.entry = "";
|
||||
shake.restart();
|
||||
surface.failed = true;
|
||||
surface.status = "PAM error";
|
||||
refuse.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// A wrong password: the field leans away once and comes back, a head
|
||||
// shaken slowly rather than a buzzer.
|
||||
SequentialAnimation {
|
||||
id: shake
|
||||
loops: 2
|
||||
id: refuse
|
||||
NumberAnimation {
|
||||
target: shakeShift; property: "x"
|
||||
to: -16; duration: 55; easing.type: Easing.OutQuad
|
||||
target: nudge; property: "x"
|
||||
to: -7; duration: 90; easing.type: Easing.OutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
target: shakeShift; property: "x"
|
||||
to: 16; duration: 55; easing.type: Easing.OutQuad
|
||||
target: nudge; property: "x"
|
||||
to: 5; duration: 140; easing.type: Easing.InOutSine
|
||||
}
|
||||
NumberAnimation {
|
||||
target: shakeShift; property: "x"
|
||||
to: 0; duration: 90; easing.type: Easing.OutCubic
|
||||
target: nudge; property: "x"
|
||||
to: 0; duration: 220; easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Entry flourish when the lock comes up.
|
||||
// The lock comes up the way the scene would if you opened your eyes: the
|
||||
// picture first, then the type settling a few pixels into place.
|
||||
Component.onCompleted: intro.start()
|
||||
|
||||
ParallelAnimation {
|
||||
SequentialAnimation {
|
||||
id: intro
|
||||
// The stripes sweep in under everything, the same move the power menu
|
||||
// opens with.
|
||||
NumberAnimation {
|
||||
target: backdrop; property: "reveal"
|
||||
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
|
||||
target: wall; property: "opacity"
|
||||
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
|
||||
}
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: face; property: "opacity"
|
||||
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: timeBlock; property: "opacity"
|
||||
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
|
||||
target: drift; property: "y"
|
||||
from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint
|
||||
}
|
||||
NumberAnimation {
|
||||
target: authBlock; property: "opacity"
|
||||
from: 0; to: 1; duration: Theme.durLazy; easing.type: Easing.OutExpo
|
||||
}
|
||||
NumberAnimation {
|
||||
target: shakeShift; property: "y"
|
||||
from: 40; to: 0; duration: Theme.durLazy; easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// One line of the lock screen's status rail.
|
||||
Item {
|
||||
id: rail
|
||||
|
||||
property string glyph: ""
|
||||
property string label: ""
|
||||
property string detail: ""
|
||||
property color tone: Theme.primary
|
||||
|
||||
implicitHeight: visible ? 38 : 0
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 3
|
||||
height: 26
|
||||
color: rail.tone
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.padS
|
||||
|
||||
Icon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: rail.glyph
|
||||
color: rail.tone
|
||||
font.pixelSize: Theme.fsLarge
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: -2
|
||||
|
||||
Text {
|
||||
text: rail.label
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Font.DemiBold
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: rail.detail !== ""
|
||||
text: rail.detail
|
||||
color: Theme.muted
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import QtQuick
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
|
||||
// One line of the lock screen's status log, laid out like the dashboard's
|
||||
// facts: a dot in the line's tone, the label tracked in caps, a hairline
|
||||
// running out to the detail at the far end.
|
||||
Item {
|
||||
id: row
|
||||
|
||||
property string label: ""
|
||||
property string detail: ""
|
||||
property color tone: Theme.primaryDim
|
||||
|
||||
implicitHeight: visible ? 26 : 0
|
||||
|
||||
Rectangle {
|
||||
id: dot
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.marker
|
||||
height: Theme.marker
|
||||
radius: width / 2
|
||||
color: row.tone
|
||||
}
|
||||
|
||||
Text {
|
||||
id: name
|
||||
anchors.left: dot.right
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, row.width * 0.55)
|
||||
text: row.label.toUpperCase()
|
||||
color: Theme.text
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightDisplay
|
||||
font.letterSpacing: Theme.tracking
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: name.right
|
||||
anchors.leftMargin: Theme.padS
|
||||
anchors.right: value.left
|
||||
anchors.rightMargin: Theme.padS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 1
|
||||
color: Theme.alpha(Theme.outline, 0.4)
|
||||
}
|
||||
|
||||
Text {
|
||||
id: value
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, row.width * 0.4)
|
||||
text: row.detail
|
||||
color: Theme.muted
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
+139
-132
@@ -1,12 +1,16 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Full-screen power menu. An accent wedge and stripes sweep in behind a stack of leaning
|
||||
// buttons; Escape or a click on the backdrop dismisses it.
|
||||
// Full-screen power menu, built like the lock screen: the desktop dims to the
|
||||
// void, and one small group of type stands in it — the mark, the motto, a
|
||||
// line about the session, and a NieR menu of six rows. The selected row is a
|
||||
// pill of light with the text punched out of it; the three that end the
|
||||
// session light in rust instead. Escape or a click on the dark dismisses it.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
@@ -32,16 +36,9 @@ PanelWindow {
|
||||
|
||||
property int selectedAction: 0
|
||||
|
||||
function focusSelectedAction(): void {
|
||||
const entry = buttonRepeater.itemAt(root.selectedAction);
|
||||
if (entry && entry.children.length > 0)
|
||||
entry.children[0].forceActiveFocus(Qt.TabFocusReason);
|
||||
}
|
||||
|
||||
function moveSelection(delta: int): void {
|
||||
root.selectedAction = (root.selectedAction + delta + root.actionModel.length)
|
||||
% root.actionModel.length;
|
||||
root.focusSelectedAction();
|
||||
}
|
||||
|
||||
readonly property var actionModel: [
|
||||
@@ -53,6 +50,11 @@ PanelWindow {
|
||||
{ label: "Shut Down", icon: "", danger: true, act: "shutdown" }
|
||||
]
|
||||
|
||||
FileView {
|
||||
id: hostFile
|
||||
path: "/etc/hostname"
|
||||
}
|
||||
|
||||
onOpenChanged: {
|
||||
if (open) {
|
||||
PopupCoordinator.dismissAll();
|
||||
@@ -64,128 +66,155 @@ PanelWindow {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- backdrop
|
||||
// The desktop stays faintly there behind the dark, deepest on the left
|
||||
// where the menu stands.
|
||||
Rectangle {
|
||||
id: backdrop
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(Theme.base, 0.9)
|
||||
opacity: 0
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: Theme.alpha(Theme.ink, 0.97) }
|
||||
GradientStop { position: 0.5; color: Theme.alpha(Theme.ink, 0.93) }
|
||||
GradientStop { position: 1.0; color: Theme.alpha(Theme.ink, 0.86) }
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: Actions.closePowerMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Grid and column, shared with the lock screen.
|
||||
P5Backdrop {
|
||||
id: ground
|
||||
anchors.fill: parent
|
||||
reveal: 0
|
||||
stripeOpacity: 0.05
|
||||
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
|
||||
// ---------------------------------------------------------------- group
|
||||
Column {
|
||||
id: title
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.width * 0.12
|
||||
id: group
|
||||
x: root.width * 0.14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -60
|
||||
spacing: -14
|
||||
width: 340
|
||||
opacity: 0
|
||||
|
||||
SplitText {
|
||||
text: "GLORY"
|
||||
pixelSize: Theme.fsMega
|
||||
split: 4
|
||||
}
|
||||
transform: Translate { id: drift }
|
||||
|
||||
SplitText {
|
||||
text: "TO"
|
||||
pixelSize: Theme.fsMega
|
||||
color: Theme.accent
|
||||
split: 4
|
||||
}
|
||||
|
||||
SplitText {
|
||||
text: "MANKIND."
|
||||
pixelSize: Theme.fsMega
|
||||
split: 4
|
||||
Porthole {
|
||||
size: 26
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "— YORHA"
|
||||
color: Theme.primary
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.letterSpacing: 5
|
||||
topPadding: 24
|
||||
topPadding: Theme.padM
|
||||
text: "GLORY TO MANKIND."
|
||||
color: Theme.accent
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsHuge
|
||||
font.weight: Font.Light
|
||||
font.letterSpacing: 6
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
topPadding: Theme.padS
|
||||
leftPadding: 2
|
||||
text: ["YORHA", (Quickshell.env("USER") || "user") + "@" + hostFile.text().trim(),
|
||||
Sys.uptime ? "UP " + Sys.uptime : ""].filter(t => t !== "").join(" · ")
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
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
|
||||
Item { width: 1; height: Theme.padXL }
|
||||
|
||||
// ------------------------------------------------------------ menu
|
||||
Repeater {
|
||||
id: buttonRepeater
|
||||
|
||||
model: root.actionModel
|
||||
|
||||
// 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
|
||||
readonly property bool selected: root.selectedAction === entry.index
|
||||
readonly property color tone: entry.modelData.danger ? Theme.alarm : Theme.accent
|
||||
|
||||
width: group.width
|
||||
height: 44
|
||||
opacity: 0
|
||||
transform: [
|
||||
Translate { id: slide; x: 90 },
|
||||
Rotation {
|
||||
id: tilt
|
||||
angle: -3
|
||||
origin.x: entry.slabWidth / 2
|
||||
origin.y: 26
|
||||
}
|
||||
]
|
||||
|
||||
P5Button {
|
||||
id: actionButton
|
||||
transform: Translate { id: rise; y: 6 }
|
||||
|
||||
// The divider after the third row: what pauses the session
|
||||
// above, what ends it below.
|
||||
Rectangle {
|
||||
visible: entry.index === 3
|
||||
anchors.top: parent.top
|
||||
x: Theme.padL
|
||||
width: parent.width - Theme.padL * 2
|
||||
height: 1
|
||||
color: Theme.alpha(Theme.outline, 0.35)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: slab
|
||||
anchors.fill: parent
|
||||
text: entry.modelData.label
|
||||
icon: entry.modelData.icon
|
||||
destructive: entry.modelData.danger
|
||||
selected: root.selectedAction === entry.index
|
||||
accent: entry.modelData.danger ? Theme.alarm : Theme.primary
|
||||
lean: Theme.skew
|
||||
onClicked: root.invoke(entry.modelData.act)
|
||||
onHoveredChanged: if (hovered) root.selectedAction = entry.index
|
||||
onActiveFocusChanged: if (actionButton.activeFocus) root.selectedAction = entry.index
|
||||
Keys.forwardTo: [keyController]
|
||||
anchors.topMargin: 3
|
||||
anchors.bottomMargin: 3
|
||||
radius: height / 2
|
||||
color: entry.selected ? entry.tone : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
// Staggered entry, one button after another.
|
||||
Rectangle {
|
||||
id: marker
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.padL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.marker
|
||||
height: Theme.marker
|
||||
radius: width / 2
|
||||
color: entry.selected ? Theme.ink
|
||||
: (entry.modelData.danger ? Theme.alpha(Theme.alarm, 0.6) : Theme.overlay)
|
||||
}
|
||||
|
||||
Icon {
|
||||
id: glyph
|
||||
anchors.left: marker.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Fixed width so the labels line up whatever the glyph.
|
||||
width: 20
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: entry.modelData.icon
|
||||
color: entry.selected ? Theme.ink : Theme.muted
|
||||
font.pixelSize: Theme.fsLarge
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: glyph.right
|
||||
anchors.leftMargin: Theme.padM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: entry.modelData.label.toUpperCase()
|
||||
color: entry.selected ? Theme.ink : Theme.text
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsBody
|
||||
font.weight: Theme.weightDisplay
|
||||
font.letterSpacing: Theme.tracking * 1.5
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onHoveredChanged: if (hovered) root.selectedAction = entry.index
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: root.invoke(entry.modelData.act)
|
||||
}
|
||||
|
||||
// Staggered entry: each row resolves a beat after the last.
|
||||
Connections {
|
||||
target: root
|
||||
function onOpenChanged() {
|
||||
@@ -196,49 +225,32 @@ PanelWindow {
|
||||
|
||||
SequentialAnimation {
|
||||
id: entryAnim
|
||||
PauseAnimation { duration: 40 + entry.index * 55 }
|
||||
PauseAnimation { duration: 120 + entry.index * 40 }
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: entry; property: "opacity"
|
||||
to: 1; duration: Theme.durBase
|
||||
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: slide; property: "x"
|
||||
from: 90; to: 0
|
||||
duration: Theme.durSlow
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
// 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.OutCubic
|
||||
easing.overshoot: 2.2
|
||||
}
|
||||
target: rise; property: "y"
|
||||
from: 6; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hint line.
|
||||
Text {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 40
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
topPadding: Theme.padXL
|
||||
leftPadding: Theme.padL
|
||||
text: "↑ ↓ SELECT ENTER CONFIRM ESC CANCEL"
|
||||
color: Theme.muted
|
||||
color: Theme.overlay
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 4
|
||||
opacity: backdrop.opacity
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
function invoke(what: string) {
|
||||
Actions.closePowerMenu();
|
||||
@@ -280,16 +292,15 @@ PanelWindow {
|
||||
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
|
||||
to: 1; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: title; property: "opacity"
|
||||
to: 1; duration: Theme.durSlow
|
||||
target: group; property: "opacity"
|
||||
to: 1; duration: Theme.durLazy; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: drift; property: "y"
|
||||
from: 14; to: 0; duration: Theme.durLazy; easing.type: Easing.OutQuint
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,14 +308,10 @@ PanelWindow {
|
||||
id: closeAnim
|
||||
NumberAnimation {
|
||||
target: backdrop; property: "opacity"
|
||||
to: 0; duration: Theme.durFast
|
||||
to: 0; duration: Theme.durBase; easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: ground; property: "reveal"
|
||||
to: 0; duration: Theme.durFast
|
||||
}
|
||||
NumberAnimation {
|
||||
target: title; property: "opacity"
|
||||
target: group; property: "opacity"
|
||||
to: 0; duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user