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 {
ColorAnimation {
duration: 70
duration: 80
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeIn
}
@@ -6,5 +6,7 @@ import QtQuick.Controls
ListView {
id: root
boundsBehavior: Flickable.DragOverBounds
ScrollBar.vertical: WScrollBar {}
}
@@ -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<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
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
}
}
}
@@ -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
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
@@ -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 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;
}