waffles: taskview: show windows

This commit is contained in:
end-4
2025-12-13 00:45:35 +01:00
parent 39a3a0c484
commit f71ed855e5
12 changed files with 423 additions and 34 deletions
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname=".svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="7.75"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1173"
inkscape:window-height="790"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
</svg>

After

Width:  |  Height:  |  Size: 1007 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m8.5 16.586-3.793-3.793a1 1 0 0 0-1.414 1.414l4.5 4.5a1 1 0 0 0 1.414 0l11-11a1 1 0 0 0-1.414-1.414L8.5 16.586Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 241 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname=".svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="7.75"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1173"
inkscape:window-height="790"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
</svg>

After

Width:  |  Height:  |  Size: 1007 B

@@ -0,0 +1,6 @@
import Quickshell
ScriptModel {
required property int count
values: Array(count).map((_, i) => i)
}
@@ -177,7 +177,7 @@ Singleton {
property Component color: Component { property Component color: Component {
ColorAnimation { ColorAnimation {
duration: 70 duration: 80
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn easing.bezierCurve: transition.easing.bezierCurve.easeIn
} }
@@ -6,5 +6,7 @@ import QtQuick.Controls
ListView { ListView {
id: root id: root
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: WScrollBar {} ScrollBar.vertical: WScrollBar {}
} }
@@ -1,11 +1,16 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.functions import qs.modules.common.functions
import qs.modules.common.models
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.waffle.looks import qs.modules.waffle.looks
import "window-layout.js" as WindowLayout
Rectangle { Rectangle {
id: root id: root
@@ -36,6 +41,90 @@ Rectangle {
easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn 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<var> toplevels: ToplevelManager.toplevels.values.filter(t => {
const client = HyprlandData.clientForToplevel(t);
return client && client.workspace.id === HyprlandData.activeWorkspace?.id;
})
readonly property list<var> 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 // Workspaces
Rectangle { Rectangle {
id: wsBorder id: wsBorder
@@ -65,7 +154,7 @@ Rectangle {
implicitHeight: 174 implicitHeight: 174
ListView { WListView {
id: workspaceListView id: workspaceListView
anchors { anchors {
top: parent.top top: parent.top
@@ -83,15 +172,27 @@ Rectangle {
clip: true clip: true
spacing: 4 spacing: 4
model: ScriptModel { function reposition() {
values: { 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)); 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 { delegate: TaskViewWorkspace {
required property int index required property int index
workspace: index + 1 workspace: index + 1
newWorkspace: index == workspaceIndexModel.count - 1
} }
} }
} }
@@ -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()
}
}
}
@@ -15,13 +15,15 @@ WMouseAreaButton {
id: root id: root
required property int workspace required property int workspace
property bool newWorkspace: false
readonly property bool isActiveWorkspace: HyprlandData.activeWorkspace?.id === root.workspace readonly property bool isActiveWorkspace: HyprlandData.activeWorkspace?.id === root.workspace
readonly property real screenWidth: QsWindow.window.width readonly property real screenWidth: QsWindow.window.width
readonly property real screenHeight: QsWindow.window.height readonly property real screenHeight: QsWindow.window.height
readonly property real screenAspectRatio: screenWidth / screenHeight readonly property real screenAspectRatio: screenWidth / screenHeight
readonly property real screenScale: QsWindow.window.devicePixelRatio readonly property real windowScale: wallpaperHeight / screenHeight
readonly property real scale: 0.1148148148
property real wallpaperHeight: 124
height: ListView.view.height height: ListView.view.height
implicitWidth: 244 // for now implicitWidth: 244 // for now
@@ -36,8 +38,18 @@ WMouseAreaButton {
animation: Looks.transition.color.createObject(this) 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 // Content
ColumnLayout { ColumnLayout {
id: contentItem
anchors { anchors {
fill: parent fill: parent
leftMargin: 12 leftMargin: 12
@@ -52,15 +64,15 @@ WMouseAreaButton {
Layout.fillHeight: false Layout.fillHeight: false
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight 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 { Rectangle {
id: wsBg id: wsBg
height: 124 height: root.wallpaperHeight
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
color: Looks.colors.bg1Base color: Looks.colors.bg1
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
@@ -71,34 +83,42 @@ WMouseAreaButton {
} }
} }
StyledImage { // Workspace content
Loader {
anchors.fill: parent anchors.fill: parent
cache: true active: !root.newWorkspace
sourceSize: Qt.size(root.screenAspectRatio * 124, 124) sourceComponent: StyledImage {
source: Config.options.background.wallpaperPath cache: true
fillMode: Image.PreserveAspectCrop sourceSize: Qt.size(root.screenAspectRatio * root.wallpaperHeight, root.wallpaperHeight)
source: Config.options.background.wallpaperPath
fillMode: Image.PreserveAspectCrop
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: ToplevelManager.toplevels.values.filter(toplevel => { values: HyprlandData.toplevelsForWorkspace(root.workspace)
const address = `0x${toplevel.HyprlandToplevel?.address}`; }
var win = HyprlandData.windowByAddress[address]; delegate: ScreencopyView {
const inWorkspace = win?.workspace?.id === root.workspace; required property var modelData
return inWorkspace; readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`]
}) captureSource: modelData
} live: true
delegate: ScreencopyView { width: hyprlandWindowData?.size[0] * root.windowScale
required property var modelData height: hyprlandWindowData?.size[1] * root.windowScale
readonly property var hyprlandWindowData: HyprlandData.windowByAddress[`0x${modelData.HyprlandToplevel?.address}`] x: hyprlandWindowData?.at[0] * root.windowScale
captureSource: modelData y: hyprlandWindowData?.at[1] * root.windowScale
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
} }
} }
} }
// New plus icon
Loader {
anchors.centerIn: parent
active: root.newWorkspace
sourceComponent: FluentIcon {
icon: "add"
}
}
} }
} }
@@ -109,7 +129,7 @@ WMouseAreaButton {
bottom: parent.bottom bottom: parent.bottom
} }
shown: root.isActiveWorkspace shown: root.isActiveWorkspace
sourceComponent: Rectangle { sourceComponent: Rectangle {
id: activeIndicator id: activeIndicator
implicitWidth: 32 implicitWidth: 32
@@ -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;
}
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
/** /**
@@ -21,6 +22,30 @@ Singleton {
property var monitors: [] property var monitors: []
property var layers: ({}) 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() { function updateWindowList() {
getClients.running = true; getClients.running = true;
} }