diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/.svg b/dots/.config/quickshell/ii/assets/icons/fluent/.svg
new file mode 100644
index 000000000..c047f0c03
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/.svg
@@ -0,0 +1,35 @@
+
+
diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg
new file mode 100644
index 000000000..966863066
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg
new file mode 100644
index 000000000..07380988b
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg b/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg
new file mode 100644
index 000000000..c047f0c03
--- /dev/null
+++ b/dots/.config/quickshell/ii/assets/icons/fluent/empty.svg
@@ -0,0 +1,35 @@
+
+
diff --git a/dots/.config/quickshell/ii/modules/common/models/IndexModel.qml b/dots/.config/quickshell/ii/modules/common/models/IndexModel.qml
new file mode 100644
index 000000000..0cf30cdea
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/common/models/IndexModel.qml
@@ -0,0 +1,6 @@
+import Quickshell
+
+ScriptModel {
+ required property int count
+ values: Array(count).map((_, i) => i)
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml
index 85728c754..8120aa85e 100644
--- a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml
@@ -177,7 +177,7 @@ Singleton {
property Component color: Component {
ColorAnimation {
- duration: 70
+ duration: 80
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml
index bc567762a..e2ea3cda3 100644
--- a/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/looks/WListView.qml
@@ -6,5 +6,7 @@ import QtQuick.Controls
ListView {
id: root
+ boundsBehavior: Flickable.DragOverBounds
+
ScrollBar.vertical: WScrollBar {}
}
diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml
index 79f08fc79..f767ee435 100644
--- a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewContent.qml
@@ -1,11 +1,16 @@
import QtQuick
+import QtQuick.Layouts
import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
+import qs.modules.common.models
import qs.modules.common.widgets
import qs.modules.waffle.looks
+import "window-layout.js" as WindowLayout
Rectangle {
id: root
@@ -36,6 +41,90 @@ Rectangle {
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn
}
+ // Windows
+ property real maxWindowHeight: 290
+ property real maxWindowWidth: 738
+ property real padding: 52
+ property real spacing: 25
+ readonly property list toplevels: ToplevelManager.toplevels.values.filter(t => {
+ const client = HyprlandData.clientForToplevel(t);
+ return client && client.workspace.id === HyprlandData.activeWorkspace?.id;
+ })
+ readonly property list arrangedToplevels: {
+ const maxRowWidth = width - padding * 2;
+ const count = toplevels.length;
+ const resultLayout = [];
+
+ var i = 0;
+ while (i < count) {
+ var row = [];
+ var rowWidth = 0;
+ var j = i;
+
+ while (j < count) {
+ const toplevel = toplevels[j];
+ const client = HyprlandData.clientForToplevel(toplevel);
+ const scaledSize = WindowLayout.scaleWindow(client, maxWindowWidth, maxWindowHeight);
+
+ if (rowWidth + scaledSize.width <= maxRowWidth || row.length === 0) {
+ row.push(toplevel);
+ rowWidth += scaledSize.width;
+ j++;
+ } else {
+ break;
+ }
+ }
+
+ resultLayout.push(row);
+ i = j;
+ }
+ return resultLayout;
+ }
+
+ // Windows
+ WListView {
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ topMargin: (root.height - (wsBorder.height + 16) - height) / 2
+ }
+ spacing: root.spacing
+ topMargin: root.padding
+ bottomMargin: root.padding
+ leftMargin: root.padding
+ rightMargin: root.padding
+ height: Math.min(contentHeight + topMargin + bottomMargin, root.height - (wsBorder.height + 16))
+
+ interactive: height < contentHeight
+
+ clip: true
+
+ model: IndexModel {
+ count: arrangedToplevels.length
+ }
+ delegate: RowLayout {
+ id: clientRow
+ required property int index
+ spacing: root.spacing
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Repeater {
+ model: IndexModel {
+ count: root.arrangedToplevels[clientRow.index].length
+ }
+ delegate: TaskViewWindow {
+ id: client
+ required property int index
+ Layout.alignment: Qt.AlignTop
+ maxHeight: root.maxWindowHeight
+ maxWidth: root.maxWindowWidth
+ toplevel: root.arrangedToplevels[clientRow.index][index]
+ }
+ }
+ }
+ }
+
// Workspaces
Rectangle {
id: wsBorder
@@ -65,7 +154,7 @@ Rectangle {
implicitHeight: 174
- ListView {
+ WListView {
id: workspaceListView
anchors {
top: parent.top
@@ -83,15 +172,27 @@ Rectangle {
clip: true
spacing: 4
- model: ScriptModel {
- values: {
+ function reposition() {
+ positionViewAtIndex(HyprlandData.activeWorkspace.id - 1, ListView.Contain);
+ }
+
+ Connections {
+ target: HyprlandData
+ function onActiveWorkspaceChanged() {
+ workspaceListView.reposition();
+ }
+ }
+ model: IndexModel {
+ id: workspaceIndexModel
+ count: {
const maxWorkspaceId = Math.max.apply(null, HyprlandData.workspaces.map(ws => ws.id));
- return Array(Math.max(maxWorkspaceId, 1));
+ return Math.max(maxWorkspaceId, 1) + 1;
}
}
delegate: TaskViewWorkspace {
required property int index
workspace: index + 1
+ newWorkspace: index == workspaceIndexModel.count - 1
}
}
}
diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml
new file mode 100644
index 000000000..e738ac6d5
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml
@@ -0,0 +1,127 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import qs
+import qs.services
+import qs.modules.common
+import qs.modules.common.functions
+import qs.modules.common.widgets
+import qs.modules.waffle.looks
+import "window-layout.js" as WindowLayout
+
+WMouseAreaButton {
+ id: root
+
+ required property var toplevel
+ required property int maxHeight
+ required property int maxWidth
+
+ property var hyprlandClient: HyprlandData.clientForToplevel(root.toplevel)
+
+ property string iconName: AppSearch.guessIcon(hyprlandClient?.class)
+
+ color: containsMouse ? Looks.colors.bg1Base : Looks.colors.bgPanelFooterBase
+ borderColor: Looks.colors.bg2Border
+ radius: Looks.radius.xLarge
+
+ property size size: WindowLayout.scaleWindow(hyprlandClient, maxWidth, maxHeight)
+ implicitWidth: Math.max(Math.round(contentItem.implicitWidth), 138)
+ implicitHeight: Math.round(contentItem.implicitHeight)
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: root.background
+ }
+ scale: (root.pressedButtons & Qt.LeftButton) ? 0.95 : 1
+ Behavior on scale {
+ NumberAnimation {
+ id: scaleAnim
+ duration: 300
+ easing.type: Easing.OutExpo
+ }
+ }
+
+ function closeWindow() {
+ Hyprland.dispatch(`closewindow address:${root.hyprlandClient?.address}`)
+ }
+
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
+ onClicked: (event) => {
+ if (event.button === Qt.LeftButton) {
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`focuswindow address:${root.hyprlandClient?.address}`)
+ GlobalStates.overviewOpen = false;
+ } else if (event.button === Qt.MiddleButton) {
+ root.closeWindow();
+ event.accepted = true;
+ } else if (event.button === Qt.RightButton) {
+ if (!windowMenu.visible) windowMenu.popup();
+ else windowMenu.close();
+ }
+ }
+
+ ColumnLayout {
+ id: contentItem
+ z: 2
+ anchors.fill: parent
+ anchors.margins: 1
+ spacing: 0
+
+ RowLayout {
+ spacing: 8
+ WAppIcon {
+ Layout.leftMargin: 10
+ Layout.alignment: Qt.AlignVCenter
+ iconName: root.iconName
+ implicitSize: 16
+ tryCustomIcon: false
+ }
+ WText {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+ elide: Text.ElideRight
+ text: root.hyprlandClient?.title ?? ""
+ }
+ CloseButton {
+ implicitWidth: 38
+ implicitHeight: 38
+ padding: 8
+ onClicked: root.closeWindow()
+ }
+ }
+
+ ScreencopyView {
+ Layout.fillHeight: true
+ Layout.alignment: Qt.AlignHCenter
+ implicitWidth: Math.round(root.size.width)
+ implicitHeight: Math.round(root.size.height)
+
+ captureSource: root.toplevel ?? null
+ live: true
+ }
+ }
+
+ WMenu {
+ id: windowMenu
+ downDirection: true
+
+ Action {
+ enabled: root.hyprlandClient?.floating
+ property bool isPinned: root.hyprlandClient?.pinned
+ icon.name: isPinned ? "checkmark" : "empty"
+ text: Translation.tr("Show this window on all desktops")
+ onTriggered: {
+ Hyprland.dispatch(`pin address:${root.hyprlandClient?.address}`)
+ }
+ }
+ Action {
+ icon.name: "empty"
+ text: Translation.tr("Close")
+ onTriggered: root.closeWindow()
+ }
+ }
+}
diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml
index b285d91e0..cb3976997 100644
--- a/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml
+++ b/dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWorkspace.qml
@@ -15,13 +15,15 @@ WMouseAreaButton {
id: root
required property int workspace
+ property bool newWorkspace: false
readonly property bool isActiveWorkspace: HyprlandData.activeWorkspace?.id === root.workspace
readonly property real screenWidth: QsWindow.window.width
readonly property real screenHeight: QsWindow.window.height
readonly property real screenAspectRatio: screenWidth / screenHeight
- readonly property real screenScale: QsWindow.window.devicePixelRatio
- readonly property real scale: 0.1148148148
+ readonly property real windowScale: wallpaperHeight / screenHeight
+
+ property real wallpaperHeight: 124
height: ListView.view.height
implicitWidth: 244 // for now
@@ -36,8 +38,18 @@ WMouseAreaButton {
animation: Looks.transition.color.createObject(this)
}
+ scale: root.containsPress ? 0.95 : 1
+ Behavior on scale {
+ NumberAnimation {
+ id: scaleAnim
+ duration: 300
+ easing.type: Easing.OutExpo
+ }
+ }
+
// Content
ColumnLayout {
+ id: contentItem
anchors {
fill: parent
leftMargin: 12
@@ -52,15 +64,15 @@ WMouseAreaButton {
Layout.fillHeight: false
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
- text: Translation.tr("Desktop %1").arg(root.workspace)
+ text: root.newWorkspace ? Translation.tr("New desktop") : Translation.tr("Desktop %1").arg(root.workspace)
}
Rectangle {
id: wsBg
- height: 124
+ height: root.wallpaperHeight
Layout.fillHeight: true
Layout.fillWidth: true
- color: Looks.colors.bg1Base
+ color: Looks.colors.bg1
layer.enabled: true
layer.effect: OpacityMask {
@@ -71,34 +83,42 @@ WMouseAreaButton {
}
}
- StyledImage {
+ // Workspace content
+ Loader {
anchors.fill: parent
- cache: true
- sourceSize: Qt.size(root.screenAspectRatio * 124, 124)
- source: Config.options.background.wallpaperPath
- fillMode: Image.PreserveAspectCrop
+ active: !root.newWorkspace
+ sourceComponent: StyledImage {
+ cache: true
+ sourceSize: Qt.size(root.screenAspectRatio * root.wallpaperHeight, root.wallpaperHeight)
+ source: Config.options.background.wallpaperPath
+ fillMode: Image.PreserveAspectCrop
- Repeater {
- model: ScriptModel {
- values: ToplevelManager.toplevels.values.filter(toplevel => {
- const address = `0x${toplevel.HyprlandToplevel?.address}`;
- var win = HyprlandData.windowByAddress[address];
- const inWorkspace = win?.workspace?.id === root.workspace;
- return inWorkspace;
- })
- }
- delegate: ScreencopyView {
- required property var modelData
- readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`]
- captureSource: modelData
- live: true
- width: hyprlandWindowData?.size[0] * root.scale
- height: hyprlandWindowData?.size[1] * root.scale
- x: hyprlandWindowData?.at[0] * root.scale
- y: hyprlandWindowData?.at[1] * root.scale
+ Repeater {
+ model: ScriptModel {
+ values: HyprlandData.toplevelsForWorkspace(root.workspace)
+ }
+ delegate: ScreencopyView {
+ required property var modelData
+ readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`]
+ captureSource: modelData
+ live: true
+ width: hyprlandWindowData?.size[0] * root.windowScale
+ height: hyprlandWindowData?.size[1] * root.windowScale
+ x: hyprlandWindowData?.at[0] * root.windowScale
+ y: hyprlandWindowData?.at[1] * root.windowScale
+ }
}
}
}
+
+ // New plus icon
+ Loader {
+ anchors.centerIn: parent
+ active: root.newWorkspace
+ sourceComponent: FluentIcon {
+ icon: "add"
+ }
+ }
}
}
@@ -109,7 +129,7 @@ WMouseAreaButton {
bottom: parent.bottom
}
shown: root.isActiveWorkspace
-
+
sourceComponent: Rectangle {
id: activeIndicator
implicitWidth: 32
diff --git a/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js b/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js
new file mode 100644
index 000000000..d5548bdb5
--- /dev/null
+++ b/dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js
@@ -0,0 +1,36 @@
+function scaleWindow(hyprlandClient, maxWindowWidth, maxWindowHeight) {
+ const [width, height] = hyprlandClient.size;
+ const [xScale, yScale] = [maxWindowWidth / width, maxWindowHeight / height];
+ const scale = Math.min(xScale, yScale);
+ return Qt.size(width * scale, height * scale)
+}
+
+function arrangedClients(hyprlandClients, maxRowWidth, maxWindowWidth, maxWindowHeight) {
+ const count = hyprlandClients.length;
+ const resultLayout = [];
+
+ var i = 0;
+ while (i < count) {
+ var row = [];
+ var rowWidth = 0;
+ var j = i;
+
+ while (j < count) {
+ const client = hyprlandClients[j];
+ const scaledSize = scaleWindow(client, maxWindowWidth, maxWindowHeight);
+
+ if (rowWidth + scaledSize.width <= maxRowWidth || row.length === 0) {
+ row.push(client);
+ rowWidth += scaledSize.width;
+ j++;
+ } else {
+ break;
+ }
+ }
+
+ resultLayout.push(row);
+ i = j;
+ }
+
+ return resultLayout;
+}
diff --git a/dots/.config/quickshell/ii/services/HyprlandData.qml b/dots/.config/quickshell/ii/services/HyprlandData.qml
index abbaaf577..7bb437c84 100644
--- a/dots/.config/quickshell/ii/services/HyprlandData.qml
+++ b/dots/.config/quickshell/ii/services/HyprlandData.qml
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
+import Quickshell.Wayland
import Quickshell.Hyprland
/**
@@ -21,6 +22,30 @@ Singleton {
property var monitors: []
property var layers: ({})
+ // Convenient stuff
+
+ function toplevelsForWorkspace(workspace) {
+ return ToplevelManager.toplevels.values.filter(toplevel => {
+ const address = `0x${toplevel.HyprlandToplevel?.address}`;
+ var win = HyprlandData.windowByAddress[address];
+ return win?.workspace?.id === workspace;
+ })
+ }
+
+ function hyprlandClientsForWorkspace(workspace) {
+ return root.windowList.filter(win => win.workspace.id === workspace);
+ }
+
+ function clientForToplevel(toplevel) {
+ if (!toplevel || !toplevel.HyprlandToplevel) {
+ return null;
+ }
+ const address = `0x${toplevel?.HyprlandToplevel?.address}`;
+ return root.windowByAddress[address];
+ }
+
+ // Internals
+
function updateWindowList() {
getClients.running = true;
}