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,72 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
import QtQuick.Shapes
import qs.HyprChrome.Theme
// One tray item as a chamfered cell. Declares `modelData` required so it can be
// a Repeater delegate directly, without an Item wrapper in between.
//
// Left click activates, right click opens the item's own menu — anchored under
// the cell rather than at the window edge, since this rail sits along the top.
MouseArea {
id: cell
required property SystemTrayItem modelData
property int iconSize: 18
property int chamfer: 4
implicitWidth: cell.iconSize + 8
implicitHeight: cell.iconSize + 8
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: event => {
if (event.button === Qt.LeftButton) {
cell.modelData.activate();
} else if (cell.modelData.hasMenu) {
const window = cell.QsWindow?.window;
if (window) {
const anchor = cell.mapToItem(null, 0, cell.height);
cell.modelData.display(window, anchor.x, anchor.y);
}
}
event.accepted = true;
}
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 1
strokeColor: cell.containsMouse ? Theme.accent : Theme.textAlpha(0.18)
fillColor: cell.containsMouse ? Theme.selection : Theme.textAlpha(0.06)
startX: cell.chamfer
startY: 0
PathLine { x: cell.width; y: 0 }
PathLine { x: cell.width; y: cell.height - cell.chamfer }
PathLine { x: cell.width - cell.chamfer; y: cell.height }
PathLine { x: 0; y: cell.height }
PathLine { x: 0; y: cell.chamfer }
PathLine { x: cell.chamfer; y: 0 }
Behavior on strokeColor { ColorAnimation { duration: 150 } }
}
}
Image {
anchors.centerIn: parent
source: cell.modelData.icon
width: cell.iconSize
height: cell.iconSize
fillMode: Image.PreserveAspectFit
smooth: true
}
}
@@ -0,0 +1,80 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
import qs.HyprChrome.Theme
import qs.HyprChrome.Widgets.Bar.Panels
// System tray in the hyprchrome panel chrome, in both densities: the same
// items, drawn large enough to hit when expanded and shrunk onto the header
// line when collapsed.
//
// Unlike the other panels this one sizes itself horizontally — the item count
// is whatever the session happens to be running — so a layout can just give it
// `Layout.fillHeight` and let its implicit width stand.
BarPanel {
id: panel
panelId: "TRY"
title: "SYSTEM TRAY"
meta: panel.itemCount + (panel.itemCount === 1 ? " ITEM" : " ITEMS")
readonly property int itemCount: SystemTray.items.values.length
implicitWidth: Math.max(panel.headerMinWidth,
panel.briefLeft + brief.implicitWidth + panel.padding,
panel.padding * 2 + icons.implicitWidth)
// Collapsed: the same icons, small, on the header line.
summary: Row {
id: brief
spacing: 5
Repeater {
model: SystemTray.items
TrayIcon {
iconSize: 13
}
}
Text {
visible: panel.itemCount === 0
text: "NO ITEMS"
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 9
font.letterSpacing: 1.2
height: 21
verticalAlignment: Text.AlignVCenter
}
}
// Expanded: full-size cells.
Row {
id: icons
spacing: 8
Repeater {
model: SystemTray.items
TrayIcon {
iconSize: 18
}
}
Text {
visible: panel.itemCount === 0
text: "NO ITEMS"
color: Theme.muted
font.family: Theme.microFont
font.pixelSize: 9
font.letterSpacing: 1.2
height: 26
verticalAlignment: Text.AlignVCenter
}
}
}