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
This commit is contained in:
2026-08-31 00:30:58 +02:00
co-authored by Claude Opus 5
parent 3aecaf9de5
commit 2bf71494f4
18 changed files with 366 additions and 141 deletions
@@ -0,0 +1,96 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.HyprChrome.Widgets.Bar
import qs.HyprChrome.Widgets
// The hyprchrome shell: owns everything the rail's surfaces have to agree on,
// and instantiates them.
//
// State lives here rather than in any one surface because more than one of them
// reads it, and a second reader is what turns a local property into shared
// state. Three things qualify so far:
//
// * which monitor the shell lives on — every surface has to pick the same one
// * the density — the whole rail expands and collapses as one, so the toggle
// and the shortcut that drives it belong to the shell, not to the bar
// * the layer PAIR — the backdrop must sit exactly one layer below the bar in
// both densities. Two surfaces on the same layer stack by creation order,
// which is not something to rely on; one layer apart is a guarantee. Split
// across two files those two assignments drifted apart and the scrim ended
// up over the bar, so they are derived together here and passed down.
//
// A future widget joins by taking `targetScreen` and `expanded` the same way.
Scope {
id: shell
// Monitor the rail lives on. Falls back to the FIRST connected screen when
// the name matches nothing, so the bar still appears on a single-monitor
// session or after a cable swap (DebugWindow falls back to the last one
// instead — it wants the secondary).
property string screenName: "DP-2"
// Quickshell.screens is a QML list, not a JS array — no .find() on it.
readonly property var targetScreen: {
const screens = Quickshell.screens;
if (screens.length === 0)
return null;
for (let i = 0; i < screens.length; i++) {
if (screens[i].name === shell.screenName)
return screens[i];
}
return screens[0];
}
// Density is a property of the SHELL: every panel follows it, so the whole
// rail expands and collapses as one. Panels keep their own animation; only
// the decision is centralised here.
property bool expanded: false
function toggle() {
shell.expanded = !shell.expanded;
}
// Expanded the rail is over everything; collapsed it drops below ordinary
// windows. BOTTOM rather than BACKGROUND for the collapsed bar: it is the
// lowest level that still leaves a layer underneath for the backdrop, and
// it keeps the rail off the wallpaper's own level.
readonly property int barLayer: shell.expanded ? WlrLayer.Overlay : WlrLayer.Bottom
readonly property int backdropLayer: shell.expanded ? WlrLayer.Top : WlrLayer.Background
// SUPER A — see hosts/terra/home/hyprland.nix.
GlobalShortcut {
name: "chrome"
description: "Expand or collapse the hyprchrome bar"
onPressed: shell.toggle()
}
// Backdrop first: it is a layer below the bar, so stacking does not depend
// on creation order, but keeping the declaration order the same as the
// visual order costs nothing.
ChromeBackdrop {
screen: shell.targetScreen
active: shell.expanded
wlrLayer: shell.backdropLayer
// Collapsed, the scrim only backs the rail, so it needs the band the
// rail occupies. contentHeight is the SETTLED height for the current
// state — it jumps once per toggle rather than tracking the panels
// frame by frame, so the backdrop animates the change itself instead of
// chasing a value that is already being animated.
barHeight: bar.contentHeight
}
HyprChromeBar {
id: bar
screen: shell.targetScreen
// Set here, not from the window's own `screen`: reading that inside
// `visible` is circular — a hidden window has no screen to report.
visible: shell.targetScreen !== null
expanded: shell.expanded
wlrLayer: shell.barLayer
}
}