From f71ed855e5fe5cf6755bee0f5bddf2e2dc7d33fe Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:45:35 +0100 Subject: [PATCH] waffles: taskview: show windows --- .../quickshell/ii/assets/icons/fluent/.svg | 35 +++++ .../assets/icons/fluent/checkmark-filled.svg | 1 + .../ii/assets/icons/fluent/checkmark.svg | 1 + .../ii/assets/icons/fluent/empty.svg | 35 +++++ .../ii/modules/common/models/IndexModel.qml | 6 + .../ii/modules/waffle/looks/Looks.qml | 2 +- .../ii/modules/waffle/looks/WListView.qml | 2 + .../waffle/taskView/TaskViewContent.qml | 109 ++++++++++++++- .../waffle/taskView/TaskViewWindow.qml | 127 ++++++++++++++++++ .../waffle/taskView/TaskViewWorkspace.qml | 78 +++++++---- .../modules/waffle/taskView/window-layout.js | 36 +++++ .../quickshell/ii/services/HyprlandData.qml | 25 ++++ 12 files changed, 423 insertions(+), 34 deletions(-) create mode 100644 dots/.config/quickshell/ii/assets/icons/fluent/.svg create mode 100644 dots/.config/quickshell/ii/assets/icons/fluent/checkmark-filled.svg create mode 100644 dots/.config/quickshell/ii/assets/icons/fluent/checkmark.svg create mode 100644 dots/.config/quickshell/ii/assets/icons/fluent/empty.svg create mode 100644 dots/.config/quickshell/ii/modules/common/models/IndexModel.qml create mode 100644 dots/.config/quickshell/ii/modules/waffle/taskView/TaskViewWindow.qml create mode 100644 dots/.config/quickshell/ii/modules/waffle/taskView/window-layout.js 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; }