summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/bar/components/workspaces/SpecialWorkspaces.qml364
-rw-r--r--modules/bar/components/workspaces/Workspaces.qml129
2 files changed, 465 insertions, 28 deletions
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
- 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
+ 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
- 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
- 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
+ }
}