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:" — // 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 } } }