updated theme

This commit is contained in:
2026-08-11 01:53:26 +02:00
parent 0a439d951e
commit 105cf6b3a8
31 changed files with 1218 additions and 976 deletions
+78 -66
View File
@@ -5,13 +5,13 @@ import "root:/config"
import "root:/components"
import "root:/services"
// One bar per monitor.
// One bar per monitor, and one surface.
//
// Identity, workspaces and the focused window on the left; the clock on the
// right. Status moved to the rail and the player and metrics moved to the dock,
// which is what buys the room for this to breathe — nothing here is fighting
// 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.
// Identity and workspaces on the left, the player, metrics and clock on the
// right, all inside a single continuous slab and separated by hairline rules
// rather than by gaps. Nothing floats and nothing has its own card. Status
// lives on the rail, which is likewise one slab — two surfaces, not a dozen
// islands.
PanelWindow {
id: root
@@ -37,87 +37,99 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent"
// The slab. One panel, full width, flat fill, black keyline.
P5Panel {
id: slab
anchors.fill: parent
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
fill: Theme.surface
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
// ------------------------------------------------------------------ left
Row {
id: leftCluster
id: leftGroup
anchors.left: parent.left
anchors.leftMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.barGap
spacing: 0
Sigil {
id: sigil
screenRef: root.modelData
}
BarPill {
id: spacesPill
accent: Theme.primary
padding: Theme.padM
interactive: false
lean: Theme.leanFor(1)
chop: Theme.chopFor(1)
chopCorners: Theme.cornersFor(1)
implicitWidth: spacesRow.implicitWidth + contentPad * 2
Divider {}
Row {
id: spacesRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padL
Workspaces {
id: spaces
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
}
Workspaces {
id: spaces
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
}
Divider {}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 18
color: Theme.alpha(Theme.outline, 0.8)
}
ActiveWindow {
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
// The only thing competing for this row is the clock, so the
// title can have most of the bar.
maxWidth: Math.max(120, root.width
- sigil.implicitWidth
- clock.implicitWidth
- spaces.implicitWidth
- Theme.barGap * 3
- Theme.padL * 2
- Theme.padXL
- spacesPill.contentPad * 2)
}
}
ActiveWindow {
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
maxWidth: Math.max(120, root.width
- leftGroup.x - rightGroup.width - sigil.implicitWidth
- spaces.implicitWidth - root.edgeInset * 2 - Theme.padXL)
}
}
// ----------------------------------------------------------------- right
Clock {
id: clock
Row {
id: rightGroup
anchors.right: parent.right
anchors.rightMargin: root.edgeInset
anchors.verticalCenter: parent.verticalCenter
screenRef: root.modelData
spacing: 0
MediaPill {
screenRef: root.modelData
}
Divider {}
Resources {
screenRef: root.modelData
}
Divider {}
Clock {
screenRef: root.modelData
}
}
// A hairline of crimson under the whole bar — the Persona 5 underscore.
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.18; color: Theme.alpha(Theme.accent, 0.55) }
GradientStop { position: 0.5; color: Theme.alpha(Theme.primary, 0.35) }
GradientStop { position: 0.82; color: Theme.alpha(Theme.accent, 0.55) }
GradientStop { position: 1.0; color: "transparent" }
// A hairline rule between modules, sheared to the house angle. This is what
// divides the bar now — gaps between cards are what made it look scattered.
component Divider: Item {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: Theme.padM * 2
implicitHeight: Theme.barHeight
Rectangle {
anchors.centerIn: parent
width: 1
height: 20
color: Theme.alpha(Theme.outline, 0.9)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 20 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
}
+76 -44
View File
@@ -2,12 +2,19 @@ import QtQuick
import "root:/config"
import "root:/components"
// Shared chrome for every bar module: a leaning slab that lifts and lights up
// under the pointer, and forwards clicks and scrolls to the widget.
// Shared chrome for every bar module.
//
// The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson
// underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands.
//
// `standalone: true` brings back the individual leaning slab, for the few
// places that genuinely are separate surfaces.
Item {
id: root
property color accent: Theme.primary
property color accent: Theme.accent
property bool active: false
property bool slash: false
property real lean: Theme.skew
@@ -15,18 +22,29 @@ Item {
property real padding: Theme.padM
property bool interactive: true
property bool halftone: false
property bool standalone: 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 real leanInset: panel.leanInset
readonly property real leanInset: standalone ? panelLoader.item?.leanInset ?? 0 : 0
// Total horizontal inset a pill must add on top of its content width.
readonly property real contentPad: panel.contentPad
readonly property alias panelItem: panel
// The hover rule's own strip, reserved at the bottom of the module.
//
// The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the
// module instead of across it.
readonly property real ruleHeight: 2
readonly property real ruleInset: 5
readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset
// Total horizontal inset a module must add on top of its content width.
readonly property real contentPad: standalone
? (panelLoader.item?.contentPad ?? root.padding)
: root.padding
default property alias content: holder.data
@@ -37,49 +55,63 @@ Item {
implicitHeight: Theme.barHeight
P5Panel {
id: panel
Loader {
id: panelLoader
anchors.fill: parent
lean: root.lean
leanLeft: root.leanLeft
chop: root.chop
chopCorners: root.chopCorners
padding: root.padding
halftone: root.halftone
halftoneColor: root.accent
sheen: true
active: root.standalone
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
sourceComponent: P5Panel {
lean: root.lean
leanLeft: root.leanLeft
chop: root.chop
chopCorners: root.chopCorners
padding: root.padding
halftone: root.halftone
halftoneColor: root.accent
slash: root.slash || root.active
slashColor: root.accent
fill: root.active ? Theme.surfaceAlt : Theme.surface
fillOpacity: 1.0
border: root.hovered || root.active
? root.accent
: Theme.alpha(Theme.outline, 0.9)
borderWidth: root.hovered || root.active ? Theme.stroke : 1
Behavior on fillOpacity {
NumberAnimation { duration: Theme.durFast }
}
Behavior on border {
ColorAnimation { duration: Theme.durBase }
}
Item {
id: holder
anchors.fill: parent
slash: root.slash || root.active
slashColor: root.accent
}
}
// Lift toward the pointer. Subtle, but it makes the bar feel alive.
transform: Translate {
y: root.hovered && root.interactive ? -2 : 0
Item {
id: holder
anchors.fill: parent
anchors.leftMargin: root.contentPad
anchors.rightMargin: root.contentPad
anchors.bottomMargin: root.ruleBand
}
Behavior on y {
NumberAnimation {
duration: Theme.durBase
easing.type: Easing.OutBack
// Hover mark for in-bar modules: a crimson rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses.
Item {
visible: !root.standalone && root.interactive
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.ruleInset
height: root.ruleHeight
clip: true
Rectangle {
height: parent.height
width: root.hovered || root.active ? parent.width : 0
color: root.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 2 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
}
-117
View File
@@ -1,117 +0,0 @@
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
}
}
}
+122 -169
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Wayland
import "root:/config"
@@ -7,17 +6,15 @@ import "root:/components"
import "root:/services"
import "root:/popouts"
// The right-hand status rail.
// The right-hand status rail — one slab, not a stack of cards.
//
// 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.
// Connectivity, audio, power, tray and notifications all sit inside a single
// continuous column, divided by hairline rules the same way the bar divides its
// modules. Same lean, same chamfer, same keyline as the bar and the dock, so
// the three surfaces read as one system.
//
// 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.
// 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 be 4.4% on top of what the bar takes.
PanelWindow {
id: root
@@ -43,131 +40,91 @@ PanelWindow {
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)
]
}
}
// The slab.
P5Panel {
id: slab
anchors.fill: parent
vertical: true
lean: Theme.skew
chop: Theme.cut
chopCorners: [1, 3]
padding: 0
fill: Theme.surface
fillOpacity: 1.0
border: Theme.alpha(Theme.outline, 0.9)
borderWidth: 1
slash: true
slashColor: Theme.accent
slashWidth: 5
}
readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
// ------------------------------------------------------------------- top
Column {
id: upper
anchors.top: parent.top
anchors.topMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap
spacing: 0
// 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()
}
}
RailIcon {
id: netIcon
icon: Net.icon
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
// 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)
}
}
RailIcon {
id: btIcon
icon: Bt.icon
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
// Battery.
RailGroup {
variant: 2
Divider {}
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) + "%"
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
Divider { visible: Battery.available }
RailIcon {
id: battIcon
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
}
icon: Battery.icon
readout: Math.round(Battery.percent) + "%"
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
@@ -175,60 +132,56 @@ PanelWindow {
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.bottomMargin: root.edgeInset
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.railGap
spacing: 0
// Tray.
RailGroup {
variant: 3
visible: tray.visible
accent: Theme.primary
implicitHeight: tray.implicitHeight + contentPad * 2
Tray {
id: tray
anchors.centerIn: parent
}
Tray {
id: tray
anchors.horizontalCenter: parent.horizontalCenter
}
// 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
Divider { visible: tray.visible }
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))
: ""
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
onClicked: {
notifPopout.pinned = !notifPopout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
}
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: "󰐥"
alert: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu()
}
}
RailIcon {
id: powerIcon
icon: "󰐥"
tint: Actions.powerMenuOpen ? Theme.accent : Theme.subtext
onClicked: Actions.togglePowerMenu()
}
// The rail's counterpart to the bar's divider: a horizontal hairline,
// sheared to the same angle the rail's own edges lean at.
component Divider: Item {
implicitWidth: Theme.railWidth
implicitHeight: Theme.padM + 4
Rectangle {
anchors.centerIn: parent
width: 22
height: 1
color: Theme.alpha(Theme.outline, 0.9)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, 0, 0, 0,
-Theme.skew, 1, 0, 22 * Theme.skew / 2,
0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
-92
View File
@@ -1,92 +0,0 @@
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()
}
}
+15 -1
View File
@@ -19,6 +19,10 @@ Item {
readonly property bool hovered: hover.hovered
// Clearance between the wipe and the rail's left edge, chosen so the bar
// stays inside the slab once `scale` has expanded the icon about its centre.
readonly property real wipeInset: 5
signal clicked()
signal rightClicked()
signal middleClicked()
@@ -68,12 +72,22 @@ Item {
// Leading-edge wipe. On the rail this runs vertically down the left side,
// mirroring the slash P5Panel puts on horizontal modules.
//
// Held off the edge by `wipeInset`: the icon spans the rail's full width, so
// at x = 0 this landed exactly on the slab's own border, and the 1.08 hover
// scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2.
//
// It is crimson rather than `tint` because it only ever appears on hover,
// and hover is crimson everywhere else in the shell (Theme.glow). Painting
// it white put a white bar on the rail's edge for no stated reason.
Rectangle {
anchors.left: parent.left
anchors.leftMargin: root.wipeInset
anchors.verticalCenter: parent.verticalCenter
width: 2
height: root.hovered ? layout.implicitHeight : 0
color: root.alert ? Theme.accent : root.tint
color: Theme.glow
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
-6
View File
@@ -14,13 +14,7 @@ BarPill {
accent: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
// 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
onClicked: Actions.openSystemMonitor()
+13 -14
View File
@@ -20,26 +20,25 @@ BarPill {
onClicked: Actions.launch("fuzzel")
onRightClicked: Actions.reloadShell()
Icon {
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a
// pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black
// offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar.
SplitText {
id: mark
anchors.centerIn: parent
text: "󰋠"
font.pixelSize: Theme.fsTitle
text: "P5"
pixelSize: Theme.fsLarge
letterSpacing: 0
color: root.hovered ? Theme.accent : Theme.primary
}
// Spin the mark on click, then settle.
// Kick the ghosts out and let them snap back. The skull could spin a full
// turn and still read as a skull; a wordmark upside down just reads as
// broken, so the click gets the glitch instead.
Connections {
target: root
function onClicked() { spin.restart(); }
}
SequentialAnimation {
id: spin
NumberAnimation {
target: mark; property: "rotation"
from: 0; to: 360; duration: Theme.durSlow
easing.type: Easing.OutExpo
}
function onClicked() { mark.glitch(); }
}
}
+3 -1
View File
@@ -71,7 +71,9 @@ BarPill {
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
tint: Theme.warn
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
+19 -47
View File
@@ -46,9 +46,7 @@ Item {
id: row
anchors.verticalCenter: parent.verticalCenter
// 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
spacing: 6
Repeater {
model: root.slots
@@ -65,19 +63,10 @@ Item {
readonly property bool focused: root.focusedId === wsId
readonly property bool hovered: pipHover.hovered
// Each tick pulls its own shear and its own height off the
// 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
// Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9)
height: 18
Behavior on width {
NumberAnimation {
@@ -86,45 +75,28 @@ Item {
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 {
id: tick
anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white
// block at half weight. Empty is an outline. Three states,
// two colours, no ambiguity.
color: {
if (pip.urgent) return Theme.accent;
if (pip.focused) return Theme.primary;
if (pip.hovered) return Theme.glow;
if (pip.occupied) return Theme.alpha(Theme.primary, 0.55);
return Theme.alpha(Theme.outline, 0.75);
if (pip.focused || pip.urgent) return Theme.accent;
if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
return "transparent";
}
border.color: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
border.width: 1
transform: Matrix4x4 {
matrix: Qt.matrix4x4(
1, -pip.shear, 0, pip.height * pip.shear,
1, -Theme.skew, 0, pip.height * Theme.skew,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
@@ -141,7 +113,7 @@ Item {
opacity: pip.focused ? 1 : 0
visible: opacity > 0.01
text: pip.wsId
color: Theme.base
color: Theme.ink
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.weight: Theme.weightDisplay