bar/workspaces: add special ws overlay

This commit is contained in:
2 * r + 2 * t 2025-08-16 16:02:18 +10:00
parent d8f7bac960
commit 0723153b30
3 changed files with 467 additions and 30 deletions

View file

@ -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
}
}

View file

@ -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
sourceComponent: OccupiedBg {
workspaces: workspaces
occupied: root.occupied
groupOffset: root.groupOffset
layer.enabled: root.blur > 0
layer.effect: MultiEffect {
blurEnabled: true
blur: root.blur
blurMax: 32
}
}
ColumnLayout {
id: layout
Loader {
active: Config.bar.workspaces.occupiedBg
asynchronous: true
anchors.centerIn: parent
spacing: Math.floor(Appearance.spacing.small / 2)
anchors.fill: parent
anchors.margins: Appearance.padding.small
Repeater {
id: workspaces
model: Config.bar.workspaces.shown
Workspace {
activeWsId: root.activeWsId
sourceComponent: OccupiedBg {
workspaces: workspaces
occupied: root.occupied
groupOffset: root.groupOffset
}
}
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Math.floor(Appearance.spacing.small / 2)
Repeater {
id: workspaces
model: Config.bar.workspaces.shown
Workspace {
activeWsId: root.activeWsId
occupied: root.occupied
groupOffset: root.groupOffset
}
}
}
Loader {
anchors.horizontalCenter: parent.horizontalCenter
active: Config.bar.workspaces.activeIndicator
asynchronous: true
sourceComponent: ActiveIndicator {
activeWsId: root.activeWsId
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
}
}

View file

@ -191,7 +191,7 @@ Singleton {
if (name === "communication")
return "forum";
if (name === "music")
return "music_note";
return "music_cast";
if (name === "todo")
return "checklist";
if (name === "sysmon")