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
238 lines
8.0 KiB
QML
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
|
|
}
|
|
}
|
|
}
|