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:
@@ -0,0 +1,217 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.HyprChrome.Theme
|
||||
import qs.HyprChrome.Widgets.Bar.Debug
|
||||
import qs.HyprChrome.Widgets.Bar.Host
|
||||
import qs.HyprChrome.Widgets.Bar.Vitals
|
||||
import qs.HyprChrome.Widgets.Bar.Workspaces
|
||||
import qs.HyprChrome.Widgets.Bar.Tray
|
||||
|
||||
// The dense status rail itself: one layer surface holding the panel row.
|
||||
//
|
||||
// It owns nothing shared — the screen, the density and its layer all arrive
|
||||
// from HyprChromeShell, which is also what keeps this surface and the backdrop
|
||||
// one layer apart. What it does own is its own measurement: `contentHeight` is
|
||||
// the settled height of the current density, which the shell hands to the
|
||||
// backdrop and which sizes the exclusive zone.
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
// Density for every panel on the rail; driven by the shell.
|
||||
property bool expanded: false
|
||||
|
||||
// Which layer to sit on. An int rather than a private decision: it is half
|
||||
// of a pair with the backdrop's, so the shell derives both. See
|
||||
// HyprChromeShell.
|
||||
property int wlrLayer: WlrLayer.Overlay
|
||||
|
||||
// Own namespace so a layerrule can exempt the rail from Hyprland's layer
|
||||
// animation without also catching the launchers, which share the default
|
||||
// "quickshell" namespace and do want their fade.
|
||||
WlrLayershell.namespace: "hyprchrome-bar"
|
||||
WlrLayershell.layer: window.wlrLayer
|
||||
|
||||
property int margin: 12
|
||||
|
||||
// The surface never resizes: it is always tall enough for the expanded
|
||||
// rail, and only the exclusive zone tracks the current state. Resizing a
|
||||
// layer surface makes Hyprland animate the change, which showed up as the
|
||||
// panels twitching a pixel or two the moment the collapse finished.
|
||||
//
|
||||
// The zone still follows the target height, so tiled windows reflow once
|
||||
// per toggle, at the start, and slide while the panels animate.
|
||||
readonly property real expandedContent: Math.max(hostPanel.expandedHeight, workspacesPanel.expandedHeight, vitalsPanel.expandedHeight, testPanel.expandedHeight, trayPanel.expandedHeight) + window.margin * 2
|
||||
readonly property real contentHeight: Math.max(hostPanel.targetHeight, workspacesPanel.targetHeight, vitalsPanel.targetHeight, testPanel.targetHeight, trayPanel.targetHeight) + window.margin * 2
|
||||
|
||||
implicitHeight: Math.round(window.expandedContent)
|
||||
|
||||
exclusionMode: ExclusionMode.Normal
|
||||
exclusiveZone: Math.round(window.contentHeight)
|
||||
|
||||
// Only the panels take input. Without this the surface would keep eating
|
||||
// clicks across its full height while the rail is collapsed.
|
||||
mask: Region {
|
||||
item: panelRow
|
||||
}
|
||||
|
||||
anchors { top: true; left: true; right: true; }
|
||||
|
||||
color: "transparent"
|
||||
|
||||
RowLayout {
|
||||
id: panelRow
|
||||
x: window.margin
|
||||
y: window.margin
|
||||
width: window.width - window.margin * 2
|
||||
spacing: 0
|
||||
|
||||
HostPanel {
|
||||
id: hostPanel
|
||||
|
||||
expanded: window.expanded
|
||||
// Workspaces butt against its right edge; the left end of the row is free.
|
||||
rightChamfer: false
|
||||
|
||||
toggleOnClick: false
|
||||
Layout.preferredWidth: 400
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
WorkspacesPanel {
|
||||
id: workspacesPanel
|
||||
|
||||
expanded: window.expanded
|
||||
// Mid-rail: a panel on either side, so neither corner is cut.
|
||||
leftChamfer: false
|
||||
rightChamfer: false
|
||||
|
||||
toggleOnClick: false
|
||||
// No preferred width: the panel sizes itself to however many outputs
|
||||
// the session has, the same way the tray sizes itself to its items.
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
VitalsPanel {
|
||||
id: vitalsPanel
|
||||
|
||||
expanded: window.expanded
|
||||
// Workspaces on the left, the spacer on the right — and a spacer is not
|
||||
// a panel, so that edge keeps its cut.
|
||||
leftChamfer: false
|
||||
|
||||
toggleOnClick: false
|
||||
Layout.preferredWidth: 500
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// Slack on both sides of the staging panel, so it floats between the left
|
||||
// group and the tray rather than butting against either. Two spacers is
|
||||
// also what puts a gap on both of its sides, which is what gives it a cap
|
||||
// and a trace at each end.
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
TestPanel {
|
||||
id: testPanel
|
||||
|
||||
expanded: window.expanded
|
||||
toggleOnClick: false
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
TrayPanel {
|
||||
id: trayPanel
|
||||
|
||||
expanded: window.expanded
|
||||
toggleOnClick: false
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
// Traces between the panels' caps. They live here rather than in BarPanel
|
||||
// because the run they draw is the gap BETWEEN two panels, which is the one
|
||||
// piece of this geometry no panel can see. Laid over the row, in the row's
|
||||
// own coordinates, so a panel's x is directly usable.
|
||||
Item {
|
||||
id: traces
|
||||
|
||||
x: panelRow.x
|
||||
y: panelRow.y
|
||||
width: panelRow.width
|
||||
height: panelRow.height
|
||||
|
||||
// Consecutive PANELS, with the spacers dropped — a spacer has no caps, so
|
||||
// it is not something a trace can start or end at, and skipping it is
|
||||
// exactly what makes the trace span it. `rightChamfer` is the tell: an
|
||||
// Item put in the row for slack has no such property.
|
||||
readonly property var pairs: {
|
||||
const panels = [];
|
||||
for (let i = 0; i < panelRow.children.length; i++) {
|
||||
const child = panelRow.children[i];
|
||||
if (child.rightChamfer !== undefined)
|
||||
panels.push(child);
|
||||
}
|
||||
|
||||
// A cap only exists on an open chamfer, so a pair that has both is
|
||||
// exactly a pair with something to join.
|
||||
const found = [];
|
||||
for (let i = 0; i + 1 < panels.length; i++) {
|
||||
if (panels[i].rightChamfer && panels[i + 1].leftChamfer)
|
||||
found.push({ from: panels[i], to: panels[i + 1] });
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: traces.pairs
|
||||
|
||||
CapTrace {
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One run: out of a panel's top-right cap, along the top, one 45° step down
|
||||
// at the midpoint of the gap, then along the bottom into the next panel's
|
||||
// bottom-left cap. 45° means the step is as wide as it is tall, so the run
|
||||
// IS the drop — clamped if the gap is too narrow to fit it, which is the
|
||||
// only case where the angle gives.
|
||||
component CapTrace: Item {
|
||||
id: trace
|
||||
|
||||
required property var modelData
|
||||
|
||||
property int lineWidth: 3
|
||||
|
||||
readonly property real fromX: trace.modelData.from.x + trace.modelData.from.rightCapX
|
||||
readonly property real fromY: trace.modelData.from.y + trace.modelData.from.rightCapY
|
||||
readonly property real toX: trace.modelData.to.x + trace.modelData.to.leftCapX
|
||||
readonly property real toY: trace.modelData.to.y + trace.modelData.to.leftCapY
|
||||
|
||||
readonly property real drop: trace.toY - trace.fromY
|
||||
readonly property real gap: trace.toX - trace.fromX
|
||||
readonly property real step: Math.max(0, Math.min(Math.abs(trace.drop), trace.gap))
|
||||
readonly property real mid: (trace.fromX + trace.toX) / 2
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Theme.accent
|
||||
// Nothing to draw while the panels overlap, which they do for a frame
|
||||
// or two while the row is still laying itself out.
|
||||
strokeWidth: trace.gap > 0 ? trace.lineWidth : 0
|
||||
startX: trace.fromX; startY: trace.fromY
|
||||
PathLine { x: trace.mid - trace.step / 2; y: trace.fromY }
|
||||
PathLine { x: trace.mid + trace.step / 2; y: trace.toY }
|
||||
PathLine { x: trace.toX; y: trace.toY }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user