improvements

This commit is contained in:
2026-08-11 17:35:26 +02:00
parent 39f925cb58
commit a868f50ab7
24 changed files with 924 additions and 239 deletions
+74 -13
View File
@@ -4,8 +4,8 @@ import Quickshell.Hyprland
import "root:/config"
import "root:/components"
// Focused window title, glitching on every change the way a Persona 5 caption
// snaps into place.
// Focused window title. Rapid application title churn is settled before it is
// painted, then the stable caption lands like a misregistered print layer.
Item {
id: root
@@ -19,7 +19,8 @@ Item {
&& (root.monitor === null || toplevel.monitor === null
|| toplevel.monitor.id === root.monitor.id)
readonly property string title: mine && toplevel.title ? toplevel.title : "—"
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
property string displayedTitle: "—"
// The bar shrinks this when the three clusters would otherwise collide.
property real maxWidth: 240
@@ -31,18 +32,60 @@ Item {
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
}
Marquee {
id: label
Item {
id: titleStage
anchors.fill: parent
text: root.title
color: root.mine ? Theme.subtext : Theme.muted
pixelSize: Theme.fsSmall
family: Theme.fontDisplay
italic: true
weight: Font.DemiBold
clip: true
Text {
id: label
anchors.verticalCenter: parent.verticalCenter
width: parent.width
text: root.displayedTitle
color: root.mine ? Theme.subtext : Theme.muted
elide: Text.ElideRight
font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay
font.italic: true
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
// Crimson duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap
// without leaving permanent motion in the bar.
Text {
id: ghost
anchors.verticalCenter: parent.verticalCenter
width: parent.width
text: root.displayedTitle
color: Theme.accent
opacity: 0
elide: Text.ElideRight
font: label.font
renderType: Text.NativeRendering
}
}
onTitleChanged: flash.restart()
function commitTitle(): void {
if (root.displayedTitle === root.incomingTitle)
return;
root.displayedTitle = root.incomingTitle;
titleLanding.restart();
}
// Browser tabs, terminals and editors can update titles several times in a
// frame. Keep the old caption steady until the stream settles instead of
// restarting an animation for every intermediate value.
Timer {
id: titleSettle
interval: 120
repeat: false
onTriggered: root.commitTitle()
}
onIncomingTitleChanged: titleSettle.restart()
Component.onCompleted: displayedTitle = incomingTitle
// A quick crimson wipe under the title on focus change.
Rectangle {
@@ -55,7 +98,7 @@ Item {
}
SequentialAnimation {
id: flash
id: titleLanding
ParallelAnimation {
NumberAnimation {
target: wipe; property: "width"
@@ -66,6 +109,24 @@ Item {
target: wipe; property: "opacity"
from: 0; to: 1; duration: Theme.durFast
}
NumberAnimation {
target: label; property: "x"
from: -10; to: 0
duration: Theme.durBase; easing.type: Easing.OutBack
}
NumberAnimation {
target: label; property: "opacity"
from: 0.35; to: 1; duration: Theme.durFast
}
NumberAnimation {
target: ghost; property: "x"
from: 9; to: -3
duration: Theme.durBase; easing.type: Easing.OutExpo
}
NumberAnimation {
target: ghost; property: "opacity"
from: 0.8; to: 0; duration: Theme.durBase
}
}
NumberAnimation {
target: wipe; property: "opacity"
+3 -16
View File
@@ -37,29 +37,16 @@ PanelWindow {
exclusiveZone: Theme.barHeight + Theme.barMargin
color: "transparent"
// The slab. One panel, full width, flat fill, black keyline.
P5Panel {
// Calm permanent chrome: one opaque slab with no texture or offset copy.
ChromeSurface {
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
active: true
slashWidth: 5
// No offset copy on the big slabs. The 3px black silhouette behind the
// fill is a print-misregistration trick, and at this scale it stops
// reading as one: on a 1900px bar you only ever see it at the chopped
// corners, where it turns the single clean cut into a staggered double
// edge that looks like a rendering fault. The black keyline still
// carries the heavy outer contour, which is what the corner needs.
drop: false
}
readonly property real edgeInset: Theme.barHeight * Theme.skew + Theme.padM
+1 -11
View File
@@ -17,7 +17,7 @@ BarPill {
SystemClock {
id: clock
precision: SystemClock.Seconds
precision: SystemClock.Minutes
}
onClicked: calendar.pinned = !calendar.pinned
@@ -65,16 +65,6 @@ BarPill {
split: 1.4
}
// Seconds, kept quiet so they read as a ticking detail rather than noise.
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 4
text: Qt.formatDateTime(clock.date, "ss")
color: Theme.alpha(Theme.primary, 0.8)
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
// Glitch the time on the hour, because it should feel like something happened.
+15 -12
View File
@@ -17,7 +17,7 @@ BarPill {
visible: Media.hasPlayer || width > 1
// Track position, drawn by the pill in its own rule band.
progress: Media.canSeek ? Media.progress : -1
progress: Media.hasTimeline ? Media.progress : -1
implicitWidth: Media.hasPlayer
? layout.implicitWidth + contentPad * 2
@@ -83,8 +83,6 @@ BarPill {
text: Media.sourceIcon
font.pixelSize: Theme.fsLarge
color: Theme.primary
pulsing: Media.playing
pulseScale: 1.12
}
}
@@ -101,28 +99,33 @@ BarPill {
readonly property real cap: 150
Marquee {
Text {
width: Math.min(meta.cap, implicitWidth)
height: 14
text: Media.title || Media.identity
color: Theme.text
pixelSize: Theme.fsSmall
family: Theme.fontDisplay
weight: Font.DemiBold
italic: true
elide: Text.ElideRight
font.pixelSize: Theme.fsSmall
font.family: Theme.fontDisplay
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
Marquee {
Text {
width: Math.min(meta.cap, implicitWidth)
height: 12
text: Media.artist || Media.album || Media.identity
color: Theme.muted
pixelSize: Theme.fsMicro
family: Theme.fontMono
elide: Text.ElideRight
font.pixelSize: Theme.fsMicro
font.family: Theme.fontMono
renderType: Text.NativeRendering
}
}
// Play state, as a two-bar equaliser that freezes when paused.
// Compact play-state equaliser. It moves only while audio is playing
// and freezes into three quiet bars when paused.
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
+36 -26
View File
@@ -40,8 +40,8 @@ PanelWindow {
exclusiveZone: Theme.railWidth + Theme.barMargin
color: "transparent"
// The slab.
P5Panel {
// The same calm permanent-chrome preset used by the top bar.
ChromeSurface {
id: slab
anchors.fill: parent
vertical: true
@@ -49,18 +49,8 @@ PanelWindow {
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
active: true
slashWidth: 5
// Same as the bar — see the note there. The doubled corner is worse on
// the rail, because its chopped corners sit against the wallpaper's
// high-contrast stripes rather than against other chrome.
drop: false
}
readonly property real edgeInset: Theme.railWidth * Theme.skew + Theme.padM
@@ -76,18 +66,21 @@ PanelWindow {
RailIcon {
id: netIcon
icon: Net.icon
label: "Network"
statusText: Net.label
suppressTooltip: netPopout.detailActive
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
onClicked: netPopout.togglePinned()
}
RailIcon {
id: btIcon
icon: Bt.icon
label: "Bluetooth"
statusText: Bt.label
suppressTooltip: btPopout.detailActive
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
onClicked: btPopout.togglePinned()
}
Divider {}
@@ -96,19 +89,24 @@ PanelWindow {
id: volIcon
icon: Audio.icon
readout: Audio.muted ? "" : Audio.volumePercent + "%"
label: "Volume"
statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
RailIcon {
id: micIcon
icon: Audio.micIcon
label: "Microphone"
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
@@ -117,6 +115,9 @@ PanelWindow {
visible: Brightness.available
icon: Brightness.icon
readout: Math.round(Brightness.percent) + "%"
label: "Brightness"
statusText: Math.round(Brightness.percent) + "%"
hint: "Scroll to adjust"
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
@@ -127,9 +128,12 @@ PanelWindow {
visible: Battery.available
icon: Battery.icon
readout: Math.round(Battery.percent) + "%"
label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
onClicked: battPopout.togglePinned()
}
}
@@ -151,21 +155,27 @@ PanelWindow {
RailIcon {
id: bellIcon
icon: Notifs.icon
label: "Notifications"
statusText: Notifs.dnd
? "Do not disturb"
: (Notifs.unread > 0 ? Notifs.unread + " unread" : "All caught up")
readout: Notifs.unread > 0
? (Notifs.unread > 9 ? "9+" : String(Notifs.unread))
: ""
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive
onClicked: {
notifPopout.pinned = !notifPopout.pinned;
notifPopout.togglePinned();
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
}
RailIcon {
id: powerIcon
icon: "󰐥"
label: "Power"
statusText: "Session actions"
alert: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu()
}
+13 -10
View File
@@ -12,6 +12,10 @@ Item {
property string icon: ""
property string readout: ""
property string label: ""
property string statusText: ""
property string hint: ""
property bool suppressTooltip: false
property color tint: Theme.primary
property bool alert: false
property bool dim: false
@@ -24,8 +28,6 @@ Item {
readonly property real wipeInset: 5
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: Theme.railWidth
@@ -110,16 +112,17 @@ Item {
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)
}
P5Tooltip {
anchorItem: root
triggerHovered: root.hovered
title: root.label
status: root.statusText
hint: root.hint
suppressed: root.suppressTooltip
}
}
+29 -15
View File
@@ -56,10 +56,8 @@ BarPill {
color: Theme.alpha(Theme.outline, 0.8)
}
// 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.
// A tiny fixed label makes every number self-describing without turning the
// compact meter cluster back into three separate cards.
component Metric: Column {
id: metric
@@ -70,18 +68,34 @@ BarPill {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
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
Row {
spacing: 4
Behavior on color {
ColorAnimation { duration: Theme.durBase }
Text {
anchors.baseline: readoutText.baseline
text: metric.label
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
font.letterSpacing: 0.5
renderType: Text.NativeRendering
}
Text {
id: readoutText
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
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}