Files
homelab/dotfiles/quickshell/HyprChrome/Widgets/Bar/Workspaces/WorkspacesPanel.qml
T
darmanandClaude Opus 5 2bf71494f4 feat(quickshell): cap the rail's open chamfers and trace between them
A chamfer with no neighbour behind it now carries the corner it removed,
put back OUTSIDE the panel as a detached accent triangle. capGap is the
perpendicular distance from the cut, hence the per-axis shift of capGap
over root 2: the cap moves along the cut's normal, not along an axis.

The trace joining two caps belongs to the RAIL, not the panel — the run it
draws is the gap BETWEEN two panels, which no panel can see. HyprChromeBar
filters the row down to panels (a slack Item has no `rightChamfer`, so
spacers drop out, and dropping them is exactly what makes a trace span
them), then joins each right cap to the next left cap: straight, one 45°
step at the midpoint of the gap, straight. It meets the middle of each
cap's outward face rather than its tip.

TestPanel is a staging slot between two spacers. It counts seconds since
load, which is the cheapest proof the panel is live, and standing alone it
exercises a cap and a trace at both ends — something a rail of butted
panels never does.

Restructures the module in the same commit, since the moves and the edits
above land in the same files: folders are PascalCase, the bar and its
widgets moved under Widgets/Bar, and DebugWindow takes the HyprChrome Theme
instead of the legacy one. The two singletons are identical today, so that
is not a visual fix — it is a palette edit reaching the debug stage in
future. Rename detection needs -M40% to follow HyprChromeBar, which grew
past the default similarity threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv
2026-08-31 00:30:58 +02:00

238 lines
8.0 KiB
QML

pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import QtQuick
import qs.HyprChrome.Theme
import qs.HyprChrome.Widgets.Bar.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
}
}
}