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,237 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user