feat(quickshell): add HyprChromeShell, workspaces panel, persistent backdrop
HyprChromeShell owns what the rail's surfaces have to agree on: the target screen, the density and the shortcut that toggles it, and the bar/backdrop layer PAIR. That pair is why the wrapper exists — the backdrop has to sit exactly one layer below the bar in both densities, and split across two files the two assignments drifted apart and put the scrim over the bar. HyprChromeBar is now just its own surface; ChromeBackdrop takes its layer. The backdrop renders in both densities instead of fading out: full-screen while expanded, scoped to the band the rail occupies while collapsed, with a proportional tail fading off the bottom of that band. Both gradients end on their solid color when there is no fade — with a zero-length ramp the start and end stops coincide, Qt sorts stops unstably, and the transparent one winning turned "no fade" into a ramp across the whole scrim. WorkspacesPanel, between host and vitals, shows the active workspace per monitor: one accent box per output collapsed, the full strip with the shown one filled expanded, lined up in a column behind a fixed-width name cell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import qs.hyprchrome.theme
|
||||
import qs.hyprchrome.widgets.panels
|
||||
|
||||
// Which workspace each monitor is currently showing, in the hyprchrome panel
|
||||
// chrome, in both of BarPanel's densities.
|
||||
//
|
||||
// One indicator in both: a box carrying the workspace's name, filled accent
|
||||
// while its monitor is showing it and outlined otherwise. Collapsed, each
|
||||
// output gets exactly one — the workspace it is on. Expanded, it gets the whole
|
||||
// strip it cycles through, at a larger cell, with the shown one filled.
|
||||
//
|
||||
// Expanded, the strips line up in a column: the output name sits in a
|
||||
// fixed-width cell, so the boxes start at the same x on every line regardless
|
||||
// of how long a connector name is.
|
||||
//
|
||||
// Both densities read the same two models, so they cannot disagree. Hyprland's
|
||||
// own distinction is kept: `active` is the workspace its monitor is showing
|
||||
// (one per output), `focused` is the single one taking input — so the fill
|
||||
// marks the shown workspace and the accent marker marks where the keyboard is.
|
||||
//
|
||||
// Display only: workspaces expose activate(), but BarPanel's density toggle
|
||||
// covers the whole panel, so a chip could not receive the click anyway.
|
||||
BarPanel {
|
||||
id: panel
|
||||
|
||||
panelId: "WKS"
|
||||
title: "WORKSPACES"
|
||||
meta: panel.monitorCount + (panel.monitorCount === 1 ? " OUTPUT" : " OUTPUTS")
|
||||
|
||||
readonly property int monitorCount: Hyprland.monitors.values.length
|
||||
|
||||
// Sizes itself horizontally, like the tray: how many outputs a session has
|
||||
// is not something the bar can hardcode.
|
||||
implicitWidth: Math.max(panel.headerMinWidth,
|
||||
panel.briefLeft + brief.implicitWidth + panel.padding,
|
||||
panel.padding * 2 + outputs.implicitWidth)
|
||||
|
||||
// Height of one expanded output line; every cell on it centres against this.
|
||||
readonly property int lineHeight: 26
|
||||
|
||||
// Width of the output-name cell, which is what makes the strips align.
|
||||
// Fixed rather than measured: a connector name is "DP-2" or "HDMI-A-1", and
|
||||
// the alternative is probing every name's rendered width to take a maximum,
|
||||
// which costs a hidden Text per output to save nothing. Anything longer
|
||||
// elides.
|
||||
readonly property int nameWidth: 64
|
||||
|
||||
// What to call a workspace. Hyprland numbers them, but a named workspace
|
||||
// carries its name instead and a scratchpad arrives as "special:<name>" —
|
||||
// the prefix is noise once it is sitting next to a monitor's name.
|
||||
function label(ws): string {
|
||||
if (!ws)
|
||||
return "--";
|
||||
const name = ws.name ?? "";
|
||||
if (name.startsWith("special:"))
|
||||
return name.slice(8).toUpperCase();
|
||||
return (name.length > 0 ? name : String(ws.id)).toUpperCase();
|
||||
}
|
||||
|
||||
// The ordinary workspaces on one output, lowest id first. Specials share
|
||||
// the same list under negative ids: they show up as the active workspace
|
||||
// when one is open, but never as a slot in the strip, which is meant to be
|
||||
// the fixed set the output cycles through.
|
||||
function slots(monitor): var {
|
||||
return Hyprland.workspaces.values
|
||||
.filter(ws => ws.monitor === monitor && ws.id > 0)
|
||||
.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
// Collapsed: output name and the one box it is showing.
|
||||
summary: Row {
|
||||
id: brief
|
||||
|
||||
spacing: 14
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.monitors
|
||||
|
||||
Row {
|
||||
id: briefOutput
|
||||
|
||||
required property HyprlandMonitor modelData
|
||||
|
||||
spacing: 8
|
||||
height: 18
|
||||
|
||||
// A Row aligns its children by their tops only, so the label
|
||||
// takes the box's height and centres its text in it.
|
||||
Text {
|
||||
text: briefOutput.modelData.name
|
||||
color: briefOutput.modelData.focused ? Theme.accent : Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 11
|
||||
font.letterSpacing: 1.2
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Chip {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
modelData: briefOutput.modelData.activeWorkspace
|
||||
// Filled by construction: this box IS the output's active
|
||||
// workspace, so it does not wait on the flag that says so.
|
||||
shown: true
|
||||
cell: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: panel.monitorCount === 0
|
||||
text: "NO OUTPUTS"
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 9
|
||||
font.letterSpacing: 1.2
|
||||
height: 18
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded: one line per output, strips aligned.
|
||||
Column {
|
||||
id: outputs
|
||||
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.monitors
|
||||
|
||||
Output {}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: panel.monitorCount === 0
|
||||
text: "NO OUTPUTS"
|
||||
color: Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 9
|
||||
font.letterSpacing: 1.2
|
||||
height: panel.lineHeight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// One output: focus marker, name, then its workspace strip. Every cell is
|
||||
// lineHeight tall and centres its own content, so the pieces sit on one
|
||||
// line and the same cell widths repeat down the column.
|
||||
component Output: Row {
|
||||
id: output
|
||||
|
||||
required property HyprlandMonitor modelData
|
||||
|
||||
spacing: 10
|
||||
|
||||
// Marker rather than a colored name: the focused output has to be
|
||||
// findable without reading anything.
|
||||
Rectangle {
|
||||
width: 3
|
||||
height: panel.lineHeight
|
||||
color: output.modelData.focused ? Theme.accent : Theme.hair
|
||||
}
|
||||
|
||||
Item {
|
||||
width: panel.nameWidth
|
||||
height: panel.lineHeight
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
text: output.modelData.name
|
||||
color: output.modelData.focused ? Theme.accent : Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.4
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: strip.implicitWidth
|
||||
height: panel.lineHeight
|
||||
|
||||
Row {
|
||||
id: strip
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: panel.slots(output.modelData)
|
||||
|
||||
Chip {
|
||||
cell: 22
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The indicator, at whatever size the density asks for: filled accent while
|
||||
// its output is showing that workspace, outlined otherwise.
|
||||
component Chip: Rectangle {
|
||||
id: chip
|
||||
|
||||
required property HyprlandWorkspace modelData
|
||||
|
||||
// Box height; the width grows with the label and the type scales with
|
||||
// the box, so one component covers both densities.
|
||||
property int cell: 16
|
||||
property bool shown: chip.modelData?.active ?? false
|
||||
|
||||
readonly property bool urgent: chip.modelData?.urgent ?? false
|
||||
|
||||
width: Math.max(chip.cell, chipText.implicitWidth + chip.cell / 2)
|
||||
height: chip.cell
|
||||
color: chip.shown ? Theme.accent : "transparent"
|
||||
border.width: 1
|
||||
border.color: chip.urgent ? Theme.hot : chip.shown ? Theme.accent : Theme.hair
|
||||
|
||||
Text {
|
||||
id: chipText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: panel.label(chip.modelData)
|
||||
color: chip.shown ? Theme.surface : chip.urgent ? Theme.hot : Theme.muted
|
||||
font.family: Theme.microFont
|
||||
font.pixelSize: 11
|
||||
font.bold: chip.shown
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user