From 0723153b30f2760ef4d008f7836febfe80f35b69 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:02:18 +1000 Subject: bar/workspaces: add special ws overlay --- .../components/workspaces/SpecialWorkspaces.qml | 364 +++++++++++++++++++++ modules/bar/components/workspaces/Workspaces.qml | 129 ++++++-- 2 files changed, 465 insertions(+), 28 deletions(-) create mode 100644 modules/bar/components/workspaces/SpecialWorkspaces.qml (limited to 'modules/bar/components/workspaces') diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml new file mode 100644 index 0000000..7ed4515 --- /dev/null +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -0,0 +1,364 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.effects +import qs.services +import qs.utils +import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property ShellScreen screen + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(screen) + readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hyprland.focusedMonitor)?.lastIpcObject.specialWorkspace.name ?? "" + + layer.enabled: true + layer.effect: ShaderEffect { + required property Item source + readonly property Item maskSource: mask + + fragmentShader: `file://${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb` + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.full + + gradient: Gradient { + orientation: Gradient.Vertical + + GradientStop { + position: 0 + color: Qt.rgba(0, 0, 0, 0) + } + GradientStop { + position: 0.3 + color: Qt.rgba(0, 0, 0, 1) + } + GradientStop { + position: 0.7 + color: Qt.rgba(0, 0, 0, 1) + } + GradientStop { + position: 1 + color: Qt.rgba(0, 0, 0, 0) + } + } + } + + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + + radius: Appearance.rounding.full + implicitHeight: parent.height / 2 + opacity: view.contentY > 0 ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + radius: Appearance.rounding.full + implicitHeight: parent.height / 2 + opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1 + + Behavior on opacity { + Anim {} + } + } + } + + ListView { + id: view + + anchors.fill: parent + spacing: Appearance.spacing.normal + interactive: false + + currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) + onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial)) + + model: ScriptModel { + values: Hyprland.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) + } + + preferredHighlightBegin: 0 + preferredHighlightEnd: height + highlightRangeMode: ListView.StrictlyEnforceRange + + highlightFollowsCurrentItem: false + highlight: Item { + y: view.currentItem?.y ?? 0 + implicitHeight: view.currentItem?.size ?? 0 + + Behavior on y { + Anim {} + } + } + + delegate: ColumnLayout { + id: ws + + required property HyprlandWorkspace modelData + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) + property string icon + property bool hasWindows + + anchors.left: view.contentItem.left + anchors.right: view.contentItem.right + + spacing: 0 + + Component.onCompleted: { + icon = Icons.getSpecialWsIcon(modelData.name); + hasWindows = Config.bar.workspaces.showWindows && modelData.lastIpcObject.windows > 0; + } + + // Hacky thing cause modelData gets destroyed before the remove anim finishes + Connections { + target: ws.modelData + + function onNameChanged(): void { + if (ws.modelData) + ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); + } + + function onLastIpcObjectChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindows && ws.modelData.lastIpcObject.windows > 0; + } + } + + Connections { + target: Config.bar.workspaces + + function onShowWindowsChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindows && ws.modelData.lastIpcObject.windows > 0; + } + } + + Loader { + id: label + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + + asynchronous: true + sourceComponent: ws.icon.length === 1 ? letterComp : iconComp + + Component { + id: iconComp + + MaterialIcon { + fill: 1 + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + + Component { + id: letterComp + + StyledText { + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + } + + Loader { + id: windows + + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.preferredHeight: implicitHeight + + visible: active + active: ws.hasWindows + asynchronous: true + + sourceComponent: Column { + spacing: 0 + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + + move: Transition { + Anim { + properties: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + } + } + + Repeater { + model: ScriptModel { + values: Hyprland.toplevels.values.filter(c => c.workspace?.id === ws.modelData.id) + } + + MaterialIcon { + required property var modelData + + grade: 0 + text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") + color: Colours.palette.m3onSurfaceVariant + } + } + } + + Behavior on Layout.preferredHeight { + Anim {} + } + } + } + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + + remove: Transition { + Anim { + property: "scale" + to: 0.5 + duration: Appearance.anim.durations.small + } + Anim { + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + } + } + + move: Transition { + Anim { + properties: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + } + } + + displaced: Transition { + Anim { + properties: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + } + } + } + + Loader { + active: Config.bar.workspaces.activeIndicator + asynchronous: true + anchors.fill: parent + + sourceComponent: Item { + StyledClippingRect { + id: indicator + + anchors.left: parent.left + anchors.right: parent.right + + y: (view.currentItem?.y ?? 0) - view.contentY + implicitHeight: view.currentItem?.size ?? 0 + + color: Colours.palette.m3tertiary + radius: Appearance.rounding.full + + Colouriser { + source: view + sourceColor: Colours.palette.m3onSurface + colorizationColor: Colours.palette.m3onTertiary + + anchors.horizontalCenter: parent.horizontalCenter + + x: 0 + y: -indicator.y + implicitWidth: view.width + implicitHeight: view.height + } + + Behavior on y { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + Behavior on implicitHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + } + } + + MouseArea { + property real startY + + anchors.fill: view + + drag.target: view.contentItem + drag.axis: Drag.YAxis + drag.maximumY: 0 + drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) + + onPressed: event => startY = event.y + + onClicked: event => { + if (Math.abs(event.y - startY) > drag.threshold) + return; + + const ws = view.itemAt(event.x, event.y); + if (ws?.modelData) + Hyprland.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); + else + Hyprland.dispatch("togglespecialworkspace special"); + } + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index 1acc111..7b92ffa 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -6,12 +6,14 @@ import qs.components import Quickshell import QtQuick import QtQuick.Layouts +import QtQuick.Effects -StyledRect { +StyledClippingRect { id: root required property ShellScreen screen + readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hyprland.monitorFor(screen) : Hyprland.focusedMonitor)?.lastIpcObject.specialWorkspace.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hyprland.monitorFor(screen).activeWorkspace?.id ?? 1) : Hyprland.activeWsId readonly property var occupied: Hyprland.workspaces.values.reduce((acc, curr) => { @@ -20,54 +22,125 @@ StyledRect { }, {}) readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown - implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 + property real blur: onSpecial ? 1 : 0 + implicitWidth: Config.bar.sizes.innerWidth + implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.full - Loader { - active: Config.bar.workspaces.occupiedBg - asynchronous: true - + Item { anchors.fill: parent - anchors.margins: Appearance.padding.small + scale: root.onSpecial ? 0.8 : 1 + opacity: root.onSpecial ? 0.5 : 1 + + layer.enabled: root.blur > 0 + layer.effect: MultiEffect { + blurEnabled: true + blur: root.blur + blurMax: 32 + } + + Loader { + active: Config.bar.workspaces.occupiedBg + asynchronous: true - sourceComponent: OccupiedBg { - workspaces: workspaces - occupied: root.occupied - groupOffset: root.groupOffset + anchors.fill: parent + anchors.margins: Appearance.padding.small + + sourceComponent: OccupiedBg { + workspaces: workspaces + occupied: root.occupied + groupOffset: root.groupOffset + } } - } - ColumnLayout { - id: layout + ColumnLayout { + id: layout + + anchors.centerIn: parent + spacing: Math.floor(Appearance.spacing.small / 2) + + Repeater { + id: workspaces - anchors.centerIn: parent - spacing: Math.floor(Appearance.spacing.small / 2) + model: Config.bar.workspaces.shown - Repeater { - id: workspaces + Workspace { + activeWsId: root.activeWsId + occupied: root.occupied + groupOffset: root.groupOffset + } + } + } - model: Config.bar.workspaces.shown + Loader { + anchors.horizontalCenter: parent.horizontalCenter + active: Config.bar.workspaces.activeIndicator + asynchronous: true - Workspace { + sourceComponent: ActiveIndicator { activeWsId: root.activeWsId - occupied: root.occupied - groupOffset: root.groupOffset + workspaces: workspaces + mask: layout } } + + MouseArea { + anchors.fill: layout + onClicked: event => { + const ws = layout.childAt(event.x, event.y).index + root.groupOffset + 1; + if (Hyprland.activeWsId !== ws) + Hyprland.dispatch(`workspace ${ws}`); + else + Hyprland.dispatch("togglespecialworkspace special"); + } + } + + Behavior on scale { + Anim {} + } + + Behavior on opacity { + Anim {} + } } Loader { - anchors.horizontalCenter: parent.horizontalCenter - active: Config.bar.workspaces.activeIndicator + id: specialWs + + anchors.fill: parent + anchors.margins: Appearance.padding.small + + active: opacity > 0 asynchronous: true - sourceComponent: ActiveIndicator { - activeWsId: root.activeWsId - workspaces: workspaces - mask: layout + scale: root.onSpecial ? 1 : 0.5 + opacity: root.onSpecial ? 1 : 0 + + sourceComponent: SpecialWorkspaces { + screen: root.screen + } + + Behavior on scale { + Anim {} + } + + Behavior on opacity { + Anim {} } } + + Behavior on blur { + Anim { + duration: Appearance.anim.durations.small + } + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } } -- cgit v1.2.3-freya