160 lines
6.1 KiB
QML
160 lines
6.1 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import "root:/config"
|
|
import "root:/components"
|
|
|
|
// Workspace indicator: leaning ticks that stretch open when focused. Occupied
|
|
// workspaces fill in, urgent ones snap to the alarm colour.
|
|
Item {
|
|
id: root
|
|
|
|
required property var screenRef
|
|
|
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screenRef)
|
|
readonly property int focusedId: monitor?.activeWorkspace?.id ?? -1
|
|
|
|
// Live workspaces on this monitor, keyed by id, ignoring special workspaces.
|
|
readonly property var occupancy: {
|
|
const map = {};
|
|
for (const w of (Hyprland.workspaces?.values ?? [])) {
|
|
if (w.id < 1) continue;
|
|
if (root.monitor && w.monitor && w.monitor.id !== root.monitor.id) continue;
|
|
map[w.id] = {
|
|
count: w.toplevels?.values?.length ?? 0,
|
|
urgent: w.urgent,
|
|
fullscreen: w.hasFullscreen
|
|
};
|
|
}
|
|
return map;
|
|
}
|
|
|
|
// Always show 1..5, plus anything beyond that which is actually in use.
|
|
readonly property var slots: {
|
|
const ids = [1, 2, 3, 4, 5];
|
|
for (const key in root.occupancy) {
|
|
const id = parseInt(key);
|
|
if (ids.indexOf(id) === -1) ids.push(id);
|
|
}
|
|
return ids.sort((a, b) => a - b);
|
|
}
|
|
|
|
implicitWidth: row.implicitWidth
|
|
implicitHeight: 24
|
|
|
|
Row {
|
|
id: row
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
spacing: 6
|
|
|
|
Repeater {
|
|
model: root.slots
|
|
|
|
Item {
|
|
id: pip
|
|
|
|
required property var modelData
|
|
required property int index
|
|
readonly property int wsId: modelData
|
|
readonly property var info: root.occupancy[wsId]
|
|
readonly property bool occupied: info !== undefined && info.count > 0
|
|
readonly property bool urgent: info !== undefined && info.urgent
|
|
readonly property bool focused: root.focusedId === wsId
|
|
readonly property bool hovered: pipHover.hovered
|
|
|
|
// Every tick is the same height and the same shear. Only the
|
|
// width changes, and only to say which workspace is focused.
|
|
//
|
|
// Kept well short of the bar's 20px divider rules. At 18px an
|
|
// empty outline tick was the same height, lean and weight as a
|
|
// divider, and the workspace strip read as a row of rules.
|
|
width: focused ? 30 : (occupied ? 16 : 8)
|
|
height: 14
|
|
|
|
Behavior on width {
|
|
NumberAnimation {
|
|
duration: Theme.durSlow
|
|
easing.type: Easing.OutBack
|
|
easing.overshoot: 1.6
|
|
}
|
|
}
|
|
|
|
Skew {
|
|
id: tick
|
|
anchors.fill: parent
|
|
|
|
// Focused is a solid accent block. Urgent is a solid alarm
|
|
// block — it used to be the accent too, which made an
|
|
// urgent workspace indistinguishable from the focused one.
|
|
// Occupied is a subtext block. Empty is a flat recessed
|
|
// slot, filled rather than outlined so it cannot pass for a
|
|
// divider.
|
|
color: {
|
|
if (pip.focused) return Theme.accent;
|
|
if (pip.urgent) return Theme.alarm;
|
|
if (pip.hovered) return Theme.text;
|
|
if (pip.occupied) return Theme.subtext;
|
|
return Theme.surfaceAlt;
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durBase }
|
|
}
|
|
|
|
// Urgent is the one pip that has to be seen peripherally.
|
|
SequentialAnimation on opacity {
|
|
running: pip.urgent && !pip.focused
|
|
loops: Animation.Infinite
|
|
alwaysRunToEnd: true
|
|
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
|
|
NumberAnimation { to: 1.0; duration: 520; easing.type: Easing.InOutQuad }
|
|
}
|
|
}
|
|
|
|
// The focused workspace wears its number.
|
|
Text {
|
|
anchors.centerIn: parent
|
|
opacity: pip.focused ? 1 : 0
|
|
visible: opacity > 0.01
|
|
text: pip.wsId
|
|
color: Theme.ink
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsSmall
|
|
font.weight: Theme.weightDisplay
|
|
font.italic: true
|
|
renderType: Text.NativeRendering
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Theme.durBase }
|
|
}
|
|
}
|
|
|
|
// Fullscreen marker.
|
|
Rectangle {
|
|
visible: pip.info !== undefined && pip.info.fullscreen && !pip.focused
|
|
width: 4; height: 4
|
|
color: Theme.accent
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.top: parent.bottom
|
|
anchors.topMargin: 1
|
|
}
|
|
|
|
HoverHandler {
|
|
id: pipHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
// Hyprland evaluates IPC `dispatch` payloads as Lua now that the
|
|
// config is Lua (hyprland.lua rather than hyprland.conf), so the
|
|
// old `dispatch workspace 3` string is a Lua syntax error and the
|
|
// click silently does nothing. Dispatchers have to be called the
|
|
// same way lua/binds/workspaces.lua calls them.
|
|
TapHandler {
|
|
onTapped: Hyprland.dispatch("hl.dsp.focus({workspace = " + pip.wsId + "})")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|