// Workspace indicator, in the shape GNOME's workspace switcher had: a row of // dots, the current one drawn as a wide accent pill. Hyprland workspaces are // dynamic here, so the row grows and shrinks as workspaces come and go. // // Scrolling anywhere over the row switches workspace — a habit carried over // from the GNOME top panel and explicitly worth keeping. import Quickshell import Quickshell.Hyprland import QtQuick import qs.config Item { id: root // The QsScreen this bar instance lives on. When set, only that monitor's // workspaces are shown — otherwise a second monitor would duplicate them. property var screen: null readonly property HyprlandMonitor monitor: root.screen ? Hyprland.monitorFor(root.screen) : null // Special workspaces (negative ids) are scratchpads, not places you switch // to by index — GNOME had no equivalent, so they stay out of the row. readonly property var workspaces: { const all = Hyprland.workspaces ? Hyprland.workspaces.values : []; return all.filter(ws => ws && ws.id > 0 && (!root.monitor || !ws.monitor || ws.monitor === root.monitor)).sort((a, b) => a.id - b.id); } // The delegate currently focused. The accent pill tracks its geometry, which // is what makes the pill appear to slide from one workspace to the next. property Item focusedItem: null readonly property int dotSize: 8 readonly property int slotWidth: 16 readonly property int focusedSlotWidth: 28 implicitWidth: row.implicitWidth + 12 // Full bar height makes (0, 0) part of the wheel target. The dots stay // optically centered by the row and active-pill geometry below. implicitHeight: Theme.barHeight // Scroll over the padding either side of the row, not just over a dot. MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: event => root.cycle(event.angleDelta.y) } Rectangle { id: activePill // Behind the dots: the focused delegate hides its own dot, so nothing // is ever drawn on top of the pill. z: -1 visible: root.focusedItem !== null height: root.dotSize radius: Theme.pillRadius border.width: 0 // QTBUG-137166: rounded rects can lose their corners // The prism, as a fill rather than a hairline. This is the one element // in the bar that is always colored, so it is where the pair belongs. gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0 color: Theme.accent } GradientStop { position: 1.0 color: Theme.accentSecondary } } y: (root.height - height) / 2 x: row.x + (root.focusedItem ? root.focusedItem.x : 0) width: root.focusedItem ? root.focusedItem.width : 0 Behavior on x { NumberAnimation { duration: Theme.durNormal easing.type: Easing.OutCubic } } Behavior on width { NumberAnimation { duration: Theme.durNormal easing.type: Easing.OutCubic } } } Row { id: row anchors.centerIn: parent spacing: 4 Repeater { model: root.workspaces delegate: Item { id: slot required property var modelData readonly property bool isFocused: slot.modelData.focused readonly property bool isOccupied: slot.modelData.toplevels ? slot.modelData.toplevels.values.length > 0 : false implicitWidth: slot.isFocused ? root.focusedSlotWidth : root.slotWidth implicitHeight: root.height Behavior on implicitWidth { NumberAnimation { duration: Theme.durNormal easing.type: Easing.OutCubic } } onIsFocusedChanged: if (slot.isFocused) root.focusedItem = slot Component.onCompleted: if (slot.isFocused) root.focusedItem = slot Rectangle { anchors.centerIn: parent width: root.dotSize height: root.dotSize radius: width / 2 border.width: 0 visible: !slot.isFocused color: { if (slot.modelData.urgent) return Theme.urgent; return slot.isOccupied ? Theme.fg : Theme.alpha(Theme.fg, 0.3); } Behavior on color { ColorAnimation { duration: Theme.durFast } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: slot.modelData.activate() onWheel: event => root.cycle(event.angleDelta.y) } } } } // Scroll up goes to the previous workspace, down to the next — the same // direction GNOME used. Relative focus rather than by index so it keeps // working with dynamic workspaces. function cycle(angleDeltaY: int): void { Hyprland.dispatch(angleDeltaY > 0 ? 'hl.dsp.focus({ workspace = "-1" })' : 'hl.dsp.focus({ workspace = "+1" })'); } }