updated style

This commit is contained in:
2026-08-11 00:22:59 +02:00
parent 47ff5233bd
commit 0a439d951e
16 changed files with 1077 additions and 208 deletions
+63 -25
View File
@@ -18,13 +18,38 @@ Hyprland starts it from `~/.config/hypr/modules/autostart.conf`.
## Layout ## Layout
The shell is spread across three surfaces rather than crammed into one bar,
because on a 1080p panel horizontal space is cheap and vertical space is not: a
48px rail costs 2.5% of the width, where the same 48px along the bottom would
have cost 4.4% of the height on top of what the top bar already takes.
``` ```
shell.qml entrypoint — one bar, power menu, toasts and OSD per screen ┌────────────────────────────────────────────┐
config/ palette, type scale, geometry, motion curves │ ◆ TAKEMI [1][2][3] window title 00:09 │ bar — 48px exclusive
├────────────────────────────────────────────┤
│ ▐█▌│ rail — 48px exclusive
│ windows ▐ ▌│
│ ◣ ▐█▌│
♪ TRACK ╲ ▁▃▅ 42% │ dock — no exclusive zone
└────────────────────────────────────────────┘
```
- **bar** — identity, workspaces, focused window, clock.
- **rail** — network, bluetooth, audio, mic, backlight, battery, tray,
notifications, power. Popouts open leftward, into the screen.
- **dock** — now playing and the CPU/MEM/GPU readout. Reserves nothing: windows
tile the full height underneath, and the input mask is clipped to the cluster
so clicks elsewhere in the corner fall through. Idles near-invisible and wakes
on hover or on playback; the crimson corner wedge is the always-on handle that
marks where it lives.
```
shell.qml entrypoint — bar, rail, dock, power menu, toasts, OSD per screen
config/ palette, type scale, geometry, motion curves, shape variance
components/ the P5 primitives: panels, gauges, sliders, split-ink text components/ the P5 primitives: panels, gauges, sliders, split-ink text
services/ data sources, all singletons services/ data sources, all singletons
bar/ the bar and its modules bar/ the three surfaces and their modules
popouts/ hover panels hanging off bar modules popouts/ hover panels hanging off rail and bar modules
overlays/ power menu, notification toasts, volume/brightness OSD overlays/ power menu, notification toasts, volume/brightness OSD
lock/ session lock lock/ session lock
assets/ halftone and stripe tiles, generated with ImageMagick assets/ halftone and stripe tiles, generated with ImageMagick
@@ -34,24 +59,24 @@ assets/ halftone and stripe tiles, generated with ImageMagick
callers set implicit sizes from their own layout (`row.implicitWidth + callers set implicit sizes from their own layout (`row.implicitWidth +
contentPad * 2`), which keeps the shape maths out of binding loops. contentPad * 2`), which keeps the shape maths out of binding loops.
## Bar ## Modules
| Module | Left click | Right click | Middle | Scroll | Hover | | Module | Where | Left click | Right click | Middle | Scroll | Hover |
|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| Sigil | app menu | reload shell | | | | | Sigil | bar | app menu | reload shell | | | |
| Workspaces | switch | | | | | | Workspaces | bar | switch | | | | |
| CPU/MEM/GPU | btop | | | | resource dashboard | | Clock | bar | pin calendar | | | | calendar |
| Media | play/pause | next | raise player | next/prev | full player | | Network | rail | pin panel | toggle Wi-Fi | nm-connection-editor | | networks |
| Clock | pin calendar | | | | calendar | | Bluetooth | rail | pin panel | toggle adapter | | | devices |
| Tray | activate | app menu | secondary | | | | Volume | rail | mute | pin panel | pavucontrol | volume | audio mixer |
| Network | pin panel | toggle Wi-Fi | nm-connection-editor | | networks | | Mic | rail | mute | pin panel | | mic volume | audio mixer |
| Bluetooth | pin panel | toggle adapter | | | devices | | Brightness | rail | | | | brightness | |
| Volume | mute | pin panel | pavucontrol | volume | audio mixer | | Battery | rail | pin panel | | | | power detail |
| Mic | mute | pin panel | | mic volume | audio mixer | | Tray | rail | activate | app menu | secondary | | |
| Brightness | | | | brightness | | | Bell | rail | history + mark read | do not disturb | | | history |
| Battery | pin panel | | | | power detail | | Power | rail | power menu | | | | |
| Bell | history + mark read | do not disturb | | | history | | Media | dock | play/pause | next | raise player | next/prev | full player |
| Power | power menu | | | | | | CPU/MEM/GPU | dock | btop | | | | resource dashboard |
Popouts open on a short dwell and stay while the pointer is over the trigger or Popouts open on a short dwell and stay while the pointer is over the trigger or
the panel. Clicking pins one open. the panel. Clicking pins one open.
@@ -112,7 +137,20 @@ means restarting Hyprland from a TTY.
`power-profiles-daemon` is not installed (TLP is), so the power-profile `power-profiles-daemon` is not installed (TLP is), so the power-profile
switching in `services/Battery.qml` stays dormant. Nothing else depends on it. switching in `services/Battery.qml` stays dormant. Nothing else depends on it.
The display face is Adwaita Sans Black Italic, which is what was already on the The display face is Archivo Black, set italic almost everywhere. Change
box. A heavy condensed grotesque — Anton, Archivo Black, Oswald — would land `Theme.fontDisplay` and everything follows.
closer to the real Persona 5 type. Change `Theme.fontDisplay` and everything
follows. ## Palette discipline
Two hues, deliberately: teal carries every resting state, crimson means alarm
and nothing else. The old blue, green and amber remain in `Theme.qml` only as
aliases onto those two families so existing call sites keep working — don't
introduce new uses. `Theme.heat()` is a continuous teal→crimson ramp rather than
a hue cycle, so a number that has gone red means the same thing as a battery
that has gone red. Tray icons are force-flattened to a single colour for the
same reason: app brand colours wreck the palette faster than anything else.
Geometry variance lives in the same file. `Theme.leanFor(i)`, `chopFor(i)` and
`cornersFor(i)` hand each module a different lean and chamfer by index, so
modules stop being congruent parallelograms in an even rhythm — pass a module's
index rather than accepting the defaults.
+21 -57
View File
@@ -7,10 +7,11 @@ import "root:/services"
// One bar per monitor. // One bar per monitor.
// //
// Layout is three clusters: identity and system state on the left, the clock // Identity, workspaces and the focused window on the left; the clock on the
// and player dead centre, controls on the right. The centre is genuinely // right. Status moved to the rail and the player and metrics moved to the dock,
// centred, so the left cluster's window title is capped at whatever space is // which is what buys the room for this to breathe — nothing here is fighting
// actually left over — otherwise the clusters overlap on a 1080p panel. // for horizontal space any more, so the title gets a generous cap and the type
// can run large instead of being squeezed to fit between two clusters.
PanelWindow { PanelWindow {
id: root id: root
@@ -36,10 +37,6 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent" color: "transparent"
// Half the space not taken by the centre cluster, minus breathing room.
readonly property real sideBudget:
(width - centreCluster.width) / 2 - Theme.barGap * 2
// ------------------------------------------------------------------ left // ------------------------------------------------------------------ left
Row { Row {
id: leftCluster id: leftCluster
@@ -55,15 +52,18 @@ PanelWindow {
BarPill { BarPill {
id: spacesPill id: spacesPill
accent: Theme.primary accent: Theme.primary
padding: Theme.padS padding: Theme.padM
interactive: false interactive: false
lean: Theme.leanFor(1)
chop: Theme.chopFor(1)
chopCorners: Theme.cornersFor(1)
implicitWidth: spacesRow.implicitWidth + contentPad * 2 implicitWidth: spacesRow.implicitWidth + contentPad * 2
Row { Row {
id: spacesRow id: spacesRow
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
spacing: Theme.padM spacing: Theme.padL
Workspaces { Workspaces {
id: spaces id: spaces
@@ -82,63 +82,27 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData screenRef: root.modelData
// Everything else on this side is fixed width, so the title // The only thing competing for this row is the clock, so the
// gets whatever remains. // title can have most of the bar.
maxWidth: Math.max(70, root.sideBudget maxWidth: Math.max(120, root.width
- sigil.implicitWidth - sigil.implicitWidth
- resources.implicitWidth - clock.implicitWidth
- spaces.implicitWidth - spaces.implicitWidth
- Theme.barGap * 2 - Theme.barGap * 3
- Theme.padM * 3 - Theme.padL * 2
- spacesPill.leanInset) - Theme.padXL
- spacesPill.contentPad * 2)
} }
} }
} }
Resources {
id: resources
screenRef: root.modelData
}
}
// ---------------------------------------------------------------- centre
Row {
id: centreCluster
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.barGap
MediaPill {
screenRef: root.modelData
}
Clock {
screenRef: root.modelData
}
} }
// ----------------------------------------------------------------- right // ----------------------------------------------------------------- right
Row { Clock {
id: rightCluster id: clock
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.barGap screenRef: root.modelData
Tray {
screenRef: root.modelData
}
Status {
screenRef: root.modelData
}
NotifButton {
screenRef: root.modelData
}
PowerButton {
screenRef: root.modelData
}
} }
// A hairline of crimson under the whole bar — the Persona 5 underscore. // A hairline of crimson under the whole bar — the Persona 5 underscore.
+9
View File
@@ -11,10 +11,16 @@ Item {
property bool active: false property bool active: false
property bool slash: false property bool slash: false
property real lean: Theme.skew property real lean: Theme.skew
property bool leanLeft: false
property real padding: Theme.padM property real padding: Theme.padM
property bool interactive: true property bool interactive: true
property bool halftone: false property bool halftone: false
// Chamfer, forwarded so modules can differ from one another instead of all
// sharing the panel default.
property real chop: Theme.cut
property var chopCorners: [1, 3]
readonly property bool hovered: hover.hovered readonly property bool hovered: hover.hovered
readonly property real leanInset: panel.leanInset readonly property real leanInset: panel.leanInset
@@ -35,6 +41,9 @@ Item {
id: panel id: panel
anchors.fill: parent anchors.fill: parent
lean: root.lean lean: root.lean
leanLeft: root.leanLeft
chop: root.chop
chopCorners: root.chopCorners
padding: root.padding padding: root.padding
halftone: root.halftone halftone: root.halftone
halftoneColor: root.accent halftoneColor: root.accent
+117
View File
@@ -0,0 +1,117 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
// The floating bottom-left cluster: now playing, plus the CPU/MEM/GPU readout.
//
// This one reserves no space at all. Windows tile the full height of the screen
// underneath it, and the input mask is clipped to the cluster itself so clicks
// anywhere else in the corner fall straight through to whatever is below.
//
// At rest it sits at a quarter opacity — present enough to read at a glance,
// faint enough to ignore — and snaps to full weight when the pointer arrives.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-dock"
anchors {
bottom: true
left: true
}
margins {
bottom: Theme.dockMargin
left: Theme.dockMargin
}
implicitWidth: cluster.implicitWidth
implicitHeight: cluster.implicitHeight
exclusiveZone: 0
exclusionMode: ExclusionMode.Ignore
color: "transparent"
// Only the cluster takes pointer input; the rest of the surface is
// transparent to clicks.
mask: Region { item: cluster }
readonly property bool awake: clusterHover.hovered || Media.playing
// A crimson wedge pinned to the corner. This is the one part of the dock
// that never fades — it marks where the cluster lives so a nearly
// invisible idle state is still something you can aim at, and it doubles
// as the corner furniture the layout was missing.
Shape {
anchors.left: parent.left
anchors.bottom: parent.bottom
width: 26
height: 26
z: -1
preferredRendererType: Shape.CurveRenderer
opacity: root.awake ? 0.0 : 0.9
Behavior on opacity {
NumberAnimation { duration: Theme.durBase }
}
ShapePath {
fillColor: Theme.accent
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(0, 26),
Qt.point(0, 6),
Qt.point(26, 26),
Qt.point(0, 26)
]
}
}
}
Row {
id: cluster
spacing: Theme.barGap
opacity: root.awake ? 1.0 : Theme.dockIdleOpacity
Behavior on opacity {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutExpo
}
}
// Lift off the corner as it wakes.
transform: Translate {
y: root.awake ? 0 : 4
Behavior on y {
NumberAnimation {
duration: Theme.durSlow
easing.type: Easing.OutBack
}
}
}
HoverHandler {
id: clusterHover
}
MediaPill {
screenRef: root.modelData
}
Resources {
screenRef: root.modelData
}
}
}
+273
View File
@@ -0,0 +1,273 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// The right-hand status rail.
//
// Everything that used to be crammed into the right third of the top bar lives
// here instead: connectivity, audio, power, tray, notifications. On a 1080p
// panel this is the cheap edge — 48px of width is 2.5% of the screen, where the
// same 48px of height would have been 4.4% on top of what the top bar already
// takes.
//
// Slabs are grouped by what they do rather than spaced evenly, and each pulls a
// different lean and chamfer from the shape tables, so the column reads as a
// stack of distinct cards instead of a ladder.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "takemi-rail"
anchors {
top: true
bottom: true
right: true
}
margins {
top: Theme.barHeight + Theme.barMargin * 2
bottom: Theme.barMargin
right: Theme.barMargin
}
implicitWidth: Theme.railWidth
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent"
// Corner furniture. The dock pins a crimson wedge to the bottom-left; this
// is its opposite number, so the desktop is bracketed on the diagonal
// rather than merely edged. Decorative only — it sits under the slabs and
// takes no input.
Shape {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 30
height: 30
z: -1
preferredRendererType: Shape.CurveRenderer
opacity: 0.9
ShapePath {
fillColor: Theme.accent
strokeColor: "transparent"
PathPolyline {
path: [
Qt.point(30, 30),
Qt.point(30, 4),
Qt.point(2, 30),
Qt.point(30, 30)
]
}
}
}
// ------------------------------------------------------------------- top
Column {
id: upper
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap
// Connectivity.
RailGroup {
variant: 0
accent: Sys.netUp ? Theme.primary : Theme.muted
implicitHeight: linkCol.implicitHeight + contentPad * 2
Column {
id: linkCol
anchors.centerIn: parent
spacing: Theme.padS
RailIcon {
id: netIcon
icon: Net.icon
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
RailIcon {
id: btIcon
icon: Bt.icon
tint: Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
}
}
// Audio and backlight.
RailGroup {
variant: 1
accent: Audio.muted || Audio.micMuted ? Theme.accent : Theme.primary
implicitHeight: audioCol.implicitHeight + contentPad * 2
Column {
id: audioCol
anchors.centerIn: parent
spacing: Theme.padS
RailIcon {
id: volIcon
icon: Audio.icon
readout: Audio.muted ? "" : Audio.volumePercent + "%"
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
RailIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
RailIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%"
tint: Theme.primary
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
}
}
// Battery.
RailGroup {
variant: 2
visible: Battery.available
accent: Battery.critical ? Theme.accent : Theme.primary
implicitHeight: battIcon.implicitHeight + contentPad * 2
RailIcon {
id: battIcon
anchors.centerIn: parent
icon: Battery.icon
readout: Math.round(Battery.percent) + "%"
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
}
// ---------------------------------------------------------------- bottom
Column {
id: lower
anchors.bottom: parent.bottom
// Clear of the corner wedge, which would otherwise sit entirely behind
// the bottom slab and never be seen.
anchors.bottomMargin: 26
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap
// Tray.
RailGroup {
variant: 3
visible: tray.visible
accent: Theme.primary
implicitHeight: tray.implicitHeight + contentPad * 2
Tray {
id: tray
anchors.centerIn: parent
}
}
// Notifications and power — the two that act on the session rather than
// report on it, so they sit apart at the foot of the rail.
RailGroup {
variant: 4
accent: Notifs.dnd ? Theme.muted : Theme.primary
implicitHeight: actionCol.implicitHeight + contentPad * 2
Column {
id: actionCol
anchors.centerIn: parent
spacing: Theme.padS
RailIcon {
id: bellIcon
icon: Notifs.icon
readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: ""
tint: Notifs.hasUnread ? Theme.accent : Theme.primary
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
onClicked: {
notifPopout.pinned = !notifPopout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
}
RailIcon {
id: powerIcon
icon: "󰐥"
tint: Actions.powerMenuOpen ? Theme.accent : Theme.subtext
onClicked: Actions.togglePowerMenu()
}
}
}
}
// ------------------------------------------------------------- popouts
// All of these hang off the left edge of the rail, opening into the screen.
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
fromEdge: Edges.Left
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
fromEdge: Edges.Left
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
fromEdge: Edges.Left
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
fromEdge: Edges.Left
visible: Battery.available && shown
}
NotifPopout {
id: notifPopout
anchorItem: bellIcon
triggerHovered: bellIcon.hovered
fromEdge: Edges.Left
}
}
+92
View File
@@ -0,0 +1,92 @@
import QtQuick
import "root:/config"
import "root:/components"
// A slab on the right-hand rail. The vertical counterpart to BarPill: it sizes
// itself to a stacked column rather than a row, and pulls its lean and chamfer
// from the shape-variance tables by `variant`, so no two slabs on the rail are
// congruent.
Item {
id: root
property color accent: Theme.primary
property int variant: 0
property bool active: false
property real padding: Theme.padS
property bool interactive: false
property bool halftone: false
readonly property bool hovered: hover.hovered
readonly property real contentPad: panel.contentPad
readonly property alias panelItem: panel
default property alias content: holder.data
signal clicked()
implicitWidth: Theme.railWidth
P5Panel {
id: panel
anchors.fill: parent
vertical: true
// Scaled down from the horizontal tables: a rail slab leans by a
// fraction of its 48px width, so the raw values would be far too steep.
lean: Theme.leanFor(root.variant) * 0.6
leanLeft: Theme.leanLeftFor(root.variant)
chop: Math.min(Theme.chopFor(root.variant), 10)
chopCorners: Theme.cornersFor(root.variant)
padding: root.padding
halftone: root.halftone
halftoneColor: root.accent
sheen: true
fill: root.active ? Theme.surfaceAlt : Theme.surface
fillOpacity: root.hovered ? 0.98 : 0.88
border: root.hovered || root.active
? Theme.alpha(root.accent, 0.9)
: Theme.alpha(Theme.outline, 0.7)
borderWidth: root.hovered || root.active ? Theme.stroke : 1
slash: root.active
slashColor: root.accent
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
}
Behavior on border {
ColorAnimation { duration: Theme.durBase }
}
Item {
id: holder
anchors.fill: parent
}
}
// Slabs lift toward the pointer, away from the screen edge.
transform: Translate {
x: root.hovered && root.interactive ? -3 : 0
Behavior on x {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
}
}
}
HoverHandler {
id: hover
enabled: root.interactive
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: root.interactive
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
}
+111
View File
@@ -0,0 +1,111 @@
import QtQuick
import "root:/config"
import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits
// under the glyph rather than beside it, and anything longer than a percentage
// belongs in the popout instead.
Item {
id: root
property string icon: ""
property string readout: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: Theme.railWidth
implicitHeight: root.readout !== "" ? 42 : 32
Column {
id: layout
anchors.centerIn: parent
spacing: 1
Icon {
id: glyph
anchors.horizontalCenter: parent.horizontalCenter
text: root.icon
pulsing: root.pulsing
font.pixelSize: Theme.fsLarge
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
visible: root.readout !== ""
text: root.readout
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
// Leading-edge wipe. On the rail this runs vertically down the left side,
// mirroring the slash P5Panel puts on horizontal modules.
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 2
height: root.hovered ? layout.implicitHeight : 0
color: root.alert ? Theme.accent : root.tint
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.08 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+24 -26
View File
@@ -13,7 +13,14 @@ BarPill {
required property var screenRef required property var screenRef
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent)) accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
padding: Theme.padS
// Roomier than it was on the bar. In the dock there is nothing to compete
// with, so the figures get real padding instead of being pushed up against
// the slanted edges.
padding: Theme.padM
lean: Theme.leanFor(2)
chop: Theme.chopFor(2)
chopCorners: Theme.cornersFor(2)
implicitWidth: layout.implicitWidth + contentPad * 2 implicitWidth: layout.implicitWidth + contentPad * 2
onClicked: Actions.openSystemMonitor() onClicked: Actions.openSystemMonitor()
@@ -59,6 +66,10 @@ BarPill {
} }
} }
// The CPU/MEM/GPU labels are gone: three characters of chrome per metric,
// repeated three times, in the most cramped part of the shell. The number
// over its meter is legible on its own, and dropping them buys the room to
// set the figure in display italic instead of squeezing it into mono.
component Metric: Column { component Metric: Column {
id: metric id: metric
@@ -67,33 +78,20 @@ BarPill {
property string readout: "" property string readout: ""
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 1 spacing: 2
Row { Text {
spacing: 4 text: metric.readout
color: Theme.heat(metric.value)
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay
font.italic: true
font.letterSpacing: 0.5
renderType: Text.NativeRendering
Text { Behavior on color {
anchors.verticalCenter: parent.verticalCenter ColorAnimation { duration: Theme.durBase }
text: metric.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: metric.readout
color: Theme.heat(metric.value)
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
} }
} }
+71 -61
View File
@@ -1,92 +1,102 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import "root:/config" import "root:/config"
import "root:/components" import "root:/components"
// StatusNotifier tray. Left click activates, right click opens the app's own // StatusNotifier tray, as a vertical column for the right-hand rail. Left click
// menu, middle click is the secondary action. // activates, right click opens the app's own menu, middle click is the
BarPill { // secondary action. The surrounding slab is the caller's business — this is
// just the stack of icons.
Column {
id: root id: root
required property var screenRef spacing: Theme.padS + 2
accent: Theme.blue
padding: Theme.padS
interactive: false
visible: SystemTray.items.values.length > 0 visible: SystemTray.items.values.length > 0
implicitWidth: visible ? layout.implicitWidth + contentPad * 2 : 0
Row { Repeater {
id: layout model: SystemTray.items
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
Repeater { Item {
model: SystemTray.items id: entry
Item { required property SystemTrayItem modelData
id: entry
required property SystemTrayItem modelData width: 18
height: 18
anchors.horizontalCenter: parent.horizontalCenter
width: 18 // App icons arrive in their own brand colours, which wrecks the
height: 18 // two-hue palette faster than anything else in the shell. Flatten
anchors.verticalCenter: parent.verticalCenter // every one of them to a single-colour silhouette: white at rest,
// crimson under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look.
IconImage {
id: img
anchors.fill: parent
source: entry.modelData.icon
asynchronous: true
IconImage { layer.enabled: true
id: img layer.effect: MultiEffect {
anchors.fill: parent colorization: 1.0
source: entry.modelData.icon colorizationColor: itemHover.hovered
asynchronous: true ? Theme.accent
opacity: itemHover.hovered ? 1.0 : 0.82 : Theme.text
Behavior on opacity { Behavior on colorizationColor {
NumberAnimation { duration: Theme.durFast } ColorAnimation { duration: Theme.durFast }
} }
} }
scale: itemHover.hovered ? 1.18 : 1.0 opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler { Behavior on opacity {
id: itemHover NumberAnimation { duration: Theme.durFast }
cursorShape: Qt.PointingHandCursor
} }
}
TapHandler { scale: itemHover.hovered ? 1.18 : 1.0
acceptedButtons: Qt.LeftButton Behavior on scale {
onTapped: { NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
if (entry.modelData.onlyMenu) { }
menuAnchor.open();
} else { HoverHandler {
entry.modelData.activate(); id: itemHover
} cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
if (entry.modelData.onlyMenu) {
menuAnchor.open();
} else {
entry.modelData.activate();
} }
} }
}
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
onTapped: menuAnchor.open() onTapped: menuAnchor.open()
} }
TapHandler { TapHandler {
acceptedButtons: Qt.MiddleButton acceptedButtons: Qt.MiddleButton
onTapped: entry.modelData.secondaryActivate() onTapped: entry.modelData.secondaryActivate()
} }
QsMenuAnchor { // Menus open to the left, into the screen rather than off its edge.
id: menuAnchor QsMenuAnchor {
menu: entry.modelData.menu id: menuAnchor
anchor.item: entry menu: entry.modelData.menu
anchor.edges: Edges.Bottom anchor.item: entry
anchor.gravity: Edges.Bottom anchor.edges: Edges.Left
anchor.margins.top: Theme.popoutGap anchor.gravity: Edges.Left
} anchor.margins.right: Theme.popoutGap
} }
} }
} }
+45 -6
View File
@@ -40,12 +40,15 @@ Item {
} }
implicitWidth: row.implicitWidth implicitWidth: row.implicitWidth
implicitHeight: 20 implicitHeight: 24
Row { Row {
id: row id: row
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 6
// Uneven gaps. An evenly spaced row of identical ticks reads as a
// progress bar; the whole point here is that it should not.
spacing: 5
Repeater { Repeater {
model: root.slots model: root.slots
@@ -54,6 +57,7 @@ Item {
id: pip id: pip
required property var modelData required property var modelData
required property int index
readonly property int wsId: modelData readonly property int wsId: modelData
readonly property var info: root.occupancy[wsId] readonly property var info: root.occupancy[wsId]
readonly property bool occupied: info !== undefined && info.count > 0 readonly property bool occupied: info !== undefined && info.count > 0
@@ -61,8 +65,19 @@ Item {
readonly property bool focused: root.focusedId === wsId readonly property bool focused: root.focusedId === wsId
readonly property bool hovered: pipHover.hovered readonly property bool hovered: pipHover.hovered
width: focused ? 34 : (occupied ? 16 : 10) // Each tick pulls its own shear and its own height off the
height: 18 // variance tables, and sits a little off the baseline, so the
// row is a ragged run of marks rather than a repeating comb.
readonly property real shear: Theme.leanFor(pip.index)
readonly property int baseHeight: [18, 14, 20, 16][pip.index % 4]
readonly property int drift: [0, 2, -1, 1][pip.index % 4]
width: focused ? 36 : (occupied ? 16 : 9)
// The focused tick grows past its neighbours and past the row's
// own height, so it breaks the line instead of sitting in it.
height: focused ? 24 : pip.baseHeight
y: focused ? -2 : pip.drift
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
@@ -71,6 +86,30 @@ Item {
easing.overshoot: 1.6 easing.overshoot: 1.6
} }
} }
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
Behavior on y {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
// Black offset copy behind the focused tick — the same
// screen-print stack the panels use, at tick scale.
Rectangle {
visible: pip.focused
anchors.fill: parent
x: 3
y: 3
color: Theme.ink
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -pip.shear, 0, pip.height * pip.shear,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
}
}
Rectangle { Rectangle {
id: tick id: tick
@@ -85,7 +124,7 @@ Item {
transform: Matrix4x4 { transform: Matrix4x4 {
matrix: Qt.matrix4x4( matrix: Qt.matrix4x4(
1, -Theme.skew, 0, pip.height * Theme.skew, 1, -pip.shear, 0, pip.height * pip.shear,
0, 1, 0, 0, 0, 1, 0, 0,
0, 0, 1, 0, 0, 0, 1, 0,
0, 0, 0, 1) 0, 0, 0, 1)
@@ -104,7 +143,7 @@ Item {
text: pip.wsId text: pip.wsId
color: Theme.base color: Theme.base
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay font.weight: Theme.weightDisplay
font.italic: true font.italic: true
renderType: Text.NativeRendering renderType: Text.NativeRendering
+23
View File
@@ -31,6 +31,29 @@ Singleton {
]; ];
} }
// Vertical counterpart: the *horizontal* edges slant instead, by a fraction
// of width. A tall rail slab has to lean this way — leaning its vertical
// edges by a fraction of its height would shear it clean off the screen.
function parallelogramV(w: real, h: real, lean: real): var {
const off = w * lean;
return [
Qt.point(0, off),
Qt.point(w, 0),
Qt.point(w, h - off),
Qt.point(0, h)
];
}
function parallelogramVL(w: real, h: real, lean: real): var {
const off = w * lean;
return [
Qt.point(0, 0),
Qt.point(w, off),
Qt.point(w, h),
Qt.point(0, h - off)
];
}
function rect(w: real, h: real): var { function rect(w: real, h: real): var {
return [Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)]; return [Qt.point(0, 0), Qt.point(w, 0), Qt.point(w, h), Qt.point(0, h)];
} }
+87 -13
View File
@@ -18,6 +18,11 @@ Item {
property real lean: Theme.skew property real lean: Theme.skew
property bool leanLeft: false property bool leanLeft: false
// Tall panels (the right-hand rail) slant their horizontal edges instead,
// by a fraction of width, and inset their content top/bottom rather than
// left/right.
property bool vertical: false
// Corner chop size, and which corners get chopped (indices into the // Corner chop size, and which corners get chopped (indices into the
// polygon, clockwise from the top-left). Default chops the two corners that // polygon, clockwise from the top-left). Default chops the two corners that
// read as "cut" against the lean. // read as "cut" against the lean.
@@ -37,10 +42,24 @@ Item {
// Inner glow along the top edge, for panels that should feel backlit. // Inner glow along the top edge, for panels that should feel backlit.
property bool sheen: false property bool sheen: false
readonly property real leanInset: height * Math.abs(lean) // The screen-print stack. A pure-black copy of the silhouette sits behind
readonly property var polygon: leanLeft // the fill, offset down-and-right, and the fill carries a black keyline
? Geom.parallelogramL(width, height, lean) // outside its coloured border. Together these are what stop the panel from
: Geom.parallelogram(width, height, lean) // reading as a flat clip-path.
property bool drop: true
property real dropOffset: Theme.dropOffset
property color dropColor: Theme.ink
property bool keyline: true
property real keylineWidth: Theme.keylineWidth
readonly property real leanInset:
(vertical ? width : height) * Math.abs(lean)
readonly property var polygon: vertical
? (leanLeft ? Geom.parallelogramVL(width, height, lean)
: Geom.parallelogramV(width, height, lean))
: (leanLeft ? Geom.parallelogramL(width, height, lean)
: Geom.parallelogram(width, height, lean))
// Child content lives inside `contentItem`, inset on both sides far enough // Child content lives inside `contentItem`, inset on both sides far enough
// to clear the slanted edges. Panels do not auto-size: callers set implicit // to clear the slanted edges. Panels do not auto-size: callers set implicit
@@ -52,14 +71,57 @@ Item {
readonly property real contentPad: padding + leanInset readonly property real contentPad: padding + leanInset
// Offset black copy of the silhouette, behind everything.
Loader {
anchors.fill: parent
active: root.drop && root.dropOffset > 0
sourceComponent: Shape {
preferredRendererType: Shape.CurveRenderer
asynchronous: false
transform: Translate {
x: root.dropOffset * (root.leanLeft ? -1 : 1)
y: root.dropOffset
}
ShapePath {
fillColor: root.dropColor
strokeColor: "transparent"
joinStyle: ShapePath.MiterJoin
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
}
}
Shape { Shape {
id: body id: body
anchors.fill: parent anchors.fill: parent
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
asynchronous: false asynchronous: false
// Fill, carrying the black keyline. The stroke is centred on the path,
// so half of it sits outside the silhouette — which is exactly the
// heavy outer contour the style wants.
ShapePath { ShapePath {
fillColor: Theme.alpha(root.fill, root.fillOpacity) fillColor: Theme.alpha(root.fill, root.fillOpacity)
strokeColor: root.keyline ? root.dropColor : "transparent"
strokeWidth: root.keyline
? root.borderWidth + root.keylineWidth * 2
: 0
joinStyle: ShapePath.MiterJoin
capStyle: ShapePath.FlatCap
PathPolyline {
path: Geom.chamfer(root.polygon, root.chop, root.chopCorners)
}
}
// Coloured border, drawn on top of the keyline so it reads as an inlay.
ShapePath {
fillColor: "transparent"
strokeColor: root.borderWidth > 0 ? root.border : "transparent" strokeColor: root.borderWidth > 0 ? root.border : "transparent"
strokeWidth: root.borderWidth strokeWidth: root.borderWidth
joinStyle: ShapePath.MiterJoin joinStyle: ShapePath.MiterJoin
@@ -141,13 +203,25 @@ Item {
strokeColor: "transparent" strokeColor: "transparent"
PathPolyline { PathPolyline {
path: { path: {
const t = root.slashWidth;
// On a rail slab the slash runs along the slanted top
// edge instead of down a side.
if (root.vertical) {
const voff = root.width * root.lean;
return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(root.width, voff),
Qt.point(root.width, voff + t), Qt.point(0, t)])
: Geom.closed([Qt.point(0, voff), Qt.point(root.width, 0),
Qt.point(root.width, t), Qt.point(0, voff + t)]);
}
const off = root.height * root.lean; const off = root.height * root.lean;
const w = root.slashWidth;
return root.leanLeft return root.leanLeft
? Geom.closed([Qt.point(0, 0), Qt.point(w, 0), ? Geom.closed([Qt.point(0, 0), Qt.point(t, 0),
Qt.point(off + w, root.height), Qt.point(off, root.height)]) Qt.point(off + t, root.height), Qt.point(off, root.height)])
: Geom.closed([Qt.point(off, 0), Qt.point(off + w, 0), : Geom.closed([Qt.point(off, 0), Qt.point(off + t, 0),
Qt.point(w, root.height), Qt.point(0, root.height)]); Qt.point(t, root.height), Qt.point(0, root.height)]);
} }
} }
} }
@@ -157,9 +231,9 @@ Item {
Item { Item {
id: contentHolder id: contentHolder
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: root.contentPad anchors.leftMargin: root.vertical ? root.padding : root.contentPad
anchors.rightMargin: root.contentPad anchors.rightMargin: root.vertical ? root.padding : root.contentPad
anchors.topMargin: root.padding anchors.topMargin: root.vertical ? root.contentPad : root.padding
anchors.bottomMargin: root.padding anchors.bottomMargin: root.vertical ? root.contentPad : root.padding
} }
} }
+28 -6
View File
@@ -27,12 +27,25 @@ PopupWindow {
signal dismissedFully() signal dismissedFully()
// Panels hanging off the vertical rail open sideways, so the entry slide has
// to follow whichever axis the popout actually travels along, and start on
// the side nearest its trigger.
readonly property bool horizontal:
fromEdge === Edges.Left || fromEdge === Edges.Right
readonly property string slideProperty: horizontal ? "x" : "y"
readonly property real slideOrigin: bleed + (fromEdge === Edges.Left
|| fromEdge === Edges.Top ? 14 : -14)
anchor.item: anchorItem anchor.item: anchorItem
anchor.edges: fromEdge anchor.edges: fromEdge
anchor.gravity: fromEdge anchor.gravity: fromEdge
anchor.adjustment: PopupAdjustment.SlideX anchor.adjustment: horizontal
? PopupAdjustment.SlideY
: PopupAdjustment.SlideX
anchor.margins.top: Theme.popoutGap anchor.margins.top: Theme.popoutGap
anchor.margins.bottom: Theme.popoutGap anchor.margins.bottom: Theme.popoutGap
anchor.margins.left: Theme.popoutGap
anchor.margins.right: Theme.popoutGap
implicitWidth: contentWidth + bleed * 2 implicitWidth: contentWidth + bleed * 2
implicitHeight: contentHeight + bleed * 2 implicitHeight: contentHeight + bleed * 2
@@ -83,7 +96,14 @@ PopupWindow {
height: root.contentHeight height: root.contentHeight
opacity: 0 opacity: 0
transformOrigin: Item.Top transformOrigin: {
switch (root.fromEdge) {
case Edges.Left: return Item.Right;
case Edges.Right: return Item.Left;
case Edges.Top: return Item.Bottom;
default: return Item.Top;
}
}
scale: 0.94 scale: 0.94
// The handler has to live on the content item, not the window: a // The handler has to live on the content item, not the window: a
@@ -105,8 +125,8 @@ PopupWindow {
to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack to: 1.0; duration: Theme.durSlow; easing.type: Easing.OutBack
} }
NumberAnimation { NumberAnimation {
target: contentHolder; property: "y" target: contentHolder; property: root.slideProperty
from: root.bleed - 14; to: root.bleed from: root.slideOrigin; to: root.bleed
duration: Theme.durSlow; easing.type: Easing.OutBack duration: Theme.durSlow; easing.type: Easing.OutBack
} }
} }
@@ -122,12 +142,14 @@ PopupWindow {
to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad to: 0.96; duration: Theme.durFast; easing.type: Easing.InQuad
} }
NumberAnimation { NumberAnimation {
target: contentHolder; property: "y" target: contentHolder; property: root.slideProperty
to: root.bleed - 8; duration: Theme.durFast; easing.type: Easing.InQuad to: root.bleed + (root.slideOrigin - root.bleed) * 0.6
duration: Theme.durFast; easing.type: Easing.InQuad
} }
onFinished: { onFinished: {
root.shown = false; root.shown = false;
root.pinned = false; root.pinned = false;
contentHolder.x = root.bleed;
contentHolder.y = root.bleed; contentHolder.y = root.bleed;
root.dismissedFully(); root.dismissedFully();
} }
+64 -11
View File
@@ -3,12 +3,19 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
// Takemi palette: cold teal/cyan over near-black, with Persona 5 crimson as the // Takemi palette. Two hues, no exceptions: a cold teal that carries every
// alert/accent voice. Derived from ~/.wallpapers/takemi_2.jpg. // resting state, and Persona 5 crimson that means alarm and nothing else.
// Anything that used to be blue/green/amber now resolves into one of those two
// families, so a splash of red in the bar always reads as "something is wrong".
// Derived from ~/.wallpapers/takemi_2.jpg.
Singleton { Singleton {
id: root id: root
// ---------------------------------------------------------------- colors // ---------------------------------------------------------------- colors
// True black, for keylines and drop shapes. `base` is the near-black the
// surfaces sit on; `ink` is never tinted and never blended.
readonly property color ink: "#000000"
readonly property color base: "#04070a" readonly property color base: "#04070a"
readonly property color mantle: "#080e14" readonly property color mantle: "#080e14"
readonly property color surface: "#0d1922" readonly property color surface: "#0d1922"
@@ -23,13 +30,17 @@ Singleton {
readonly property color primary: "#34d3e6" // electric teal — the house colour readonly property color primary: "#34d3e6" // electric teal — the house colour
readonly property color primaryDim: "#17879b" readonly property color primaryDim: "#17879b"
readonly property color glow: "#8ef2ff" readonly property color glow: "#8ef2ff"
readonly property color blue: "#3b8fd6"
readonly property color deep: "#0d4b5e" readonly property color deep: "#0d4b5e"
readonly property color accent: "#ff2d40" // P5 crimson readonly property color accent: "#ff2d40" // P5 crimson — alarm, only ever alarm
readonly property color accentDim: "#a8121f" readonly property color accentDim: "#a8121f"
readonly property color warn: "#ffb03a" readonly property color accentSoft: "#ff6b78" // the "getting there" step below accent
readonly property color ok: "#35e08f"
// Retired hues, kept as aliases so call sites keep working while resolving
// into the two-hue system. Do not introduce new uses.
readonly property color blue: root.primaryDim
readonly property color ok: root.primary
readonly property color warn: root.accentSoft
// Chromatic aberration pair, straight off the wallpaper's RGB split. // Chromatic aberration pair, straight off the wallpaper's RGB split.
readonly property color splitRed: "#ff2b4d" readonly property color splitRed: "#ff2b4d"
@@ -39,12 +50,21 @@ Singleton {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
} }
// Heat ramp for load/usage readouts. // Linear blend, t = 0 gives a, t = 1 gives b.
function mix(a: color, b: color, t: real): color {
const k = Math.max(0, Math.min(1, t));
return Qt.rgba(a.r + (b.r - a.r) * k,
a.g + (b.g - a.g) * k,
a.b + (b.b - a.b) * k,
a.a + (b.a - a.a) * k);
}
// Heat ramp for load/usage readouts. Deliberately *not* a hue cycle: it sits
// flat teal until load actually matters, then bleeds continuously into
// crimson. A number that has gone red means the same thing everywhere.
function heat(pct: real): color { function heat(pct: real): color {
if (pct >= 90) return root.accent; if (pct <= 55) return root.primary;
if (pct >= 75) return root.warn; return root.mix(root.primary, root.accent, (pct - 55) / 45);
if (pct >= 45) return root.glow;
return root.primary;
} }
// ----------------------------------------------------------------- fonts // ----------------------------------------------------------------- fonts
@@ -68,11 +88,26 @@ Singleton {
readonly property int barMargin: 8 readonly property int barMargin: 8
readonly property int barGap: 8 readonly property int barGap: 8
// Right-hand status rail and the floating bottom-left dock.
readonly property int railWidth: 48
readonly property int railGap: 10
readonly property int dockMargin: 14
// Low enough that the cluster stops competing with whatever window is
// underneath it — at 0.25 it just muddies text it happens to overlap. The
// corner wedge stays fully opaque as the thing you aim at.
readonly property real dockIdleOpacity: 0.13
readonly property int dockRevealZone: 140 // px of screen bottom that wakes it
// Horizontal offset per unit of height. tan(14deg) — every panel leans. // Horizontal offset per unit of height. tan(14deg) — every panel leans.
readonly property real skew: 0.249 readonly property real skew: 0.249
readonly property int cut: 9 // corner chamfer, in px readonly property int cut: 9 // corner chamfer, in px
readonly property int stroke: 2 readonly property int stroke: 2
// Pure-black keyline copy sitting behind every panel. This is what makes a
// shape read as screen-printed rather than as a clipped rectangle.
readonly property real dropOffset: 4
readonly property real keylineWidth: 2
readonly property int padS: 6 readonly property int padS: 6
readonly property int padM: 12 readonly property int padM: 12
readonly property int padL: 20 readonly property int padL: 20
@@ -81,6 +116,24 @@ Singleton {
readonly property int popoutWidth: 380 readonly property int popoutWidth: 380
readonly property int popoutGap: 10 readonly property int popoutGap: 10
// ------------------------------------------------------- shape variance
// Persona geometry is never a repeating pattern, so modules pull their lean
// and chamfer from these tables by index rather than sharing one default.
// Deterministic, so the bar looks the same every launch.
readonly property var leanTable: [0.32, 0.18, 0.26, 0.0, 0.29, 0.14, 0.35, 0.21]
readonly property var chopTable: [12, 5, 18, 8, 14, 4, 10, 16]
readonly property var cornerTable: [
[1, 3], [0, 2], [1], [0, 2, 3], [3], [1, 3], [2], [0, 1, 3]
]
function leanFor(i: int): real { return root.leanTable[i % root.leanTable.length]; }
function chopFor(i: int): real { return root.chopTable[i % root.chopTable.length]; }
function cornersFor(i: int): var { return root.cornerTable[i % root.cornerTable.length]; }
// Every other module leans against its neighbour, so the row reads as
// opposing diagonals instead of a comb.
function leanLeftFor(i: int): bool { return i % 3 === 1; }
// ------------------------------------------------------------ animation // ------------------------------------------------------------ animation
// P5 motion is snappy and slightly overshooting — never soft. // P5 motion is snappy and slightly overshooting — never soft.
readonly property int durFast: 110 readonly property int durFast: 110
+37 -3
View File
@@ -141,6 +141,11 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padM 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 { Repeater {
id: buttonRepeater id: buttonRepeater
@@ -160,11 +165,29 @@ PanelWindow {
required property var modelData required property var modelData
required property int index required property int index
width: 280 // 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 height: 52
x: actions.stackWidth - entry.slabWidth - entry.nudge
opacity: 0 opacity: 0
transform: Translate { id: slide; x: 90 } transform: [
Translate { id: slide; x: 90 },
Rotation {
id: tilt
angle: -3
origin.x: entry.slabWidth / 2
origin.y: 26
}
]
P5Button { P5Button {
anchors.fill: parent anchors.fill: parent
@@ -172,7 +195,8 @@ PanelWindow {
icon: entry.modelData.icon icon: entry.modelData.icon
destructive: entry.modelData.danger destructive: entry.modelData.danger
accent: entry.modelData.danger ? Theme.accent : Theme.primary accent: entry.modelData.danger ? Theme.accent : Theme.primary
lunge: -10 lean: Theme.leanFor(entry.index)
lunge: -10 - entry.index * 2
onClicked: root.invoke(entry.modelData.act) onClicked: root.invoke(entry.modelData.act)
} }
@@ -199,6 +223,16 @@ PanelWindow {
duration: Theme.durSlow duration: Theme.durSlow
easing.type: Easing.OutBack 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
}
} }
} }
} }
+12
View File
@@ -36,6 +36,18 @@ ShellRoot {
Bar {} Bar {}
} }
Variants {
model: Quickshell.screens
Rail {}
}
Variants {
model: Quickshell.screens
Dock {}
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens