import qs.modules.bar.popouts as BarPopouts import qs.config import qs.custom import qs.services import qs.util import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import Quickshell.Widgets import QtQuick import QtQuick.Controls import QtQuick.Effects import QtQuick.Layouts Item { id: root required property PersistentProperties uiState required property BarPopouts.Wrapper popouts width: Config.dashboard.workspaceWidth + 32 height: 750 property HyprlandWorkspace dragSourceWorkspace: null property HyprlandWorkspace dragTargetWorkspace: null // Controls scrolling at edge of panel while dragging property real dragScrollVelocity: 0 CustomListView { id: list anchors.fill: parent anchors.margins: 12 anchors.rightMargin: 0 spacing: 12 clip: true CustomScrollBar.vertical: CustomScrollBar { flickable: list } model: Hypr.monitors delegate: Item { id: monitor required property int index required property HyprlandMonitor modelData readonly property ListModel workspaces: States.screens.get(modelData).workspaces width: list.width - 20 height: childrenRect.height Item { id: monitorHeader anchors.left: parent.left anchors.right: parent.right height: monIcon.height MaterialIcon { id: monIcon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left text: "desktop_windows" color: Config.colors.tertiary } CustomText { id: monText anchors.verticalCenter: parent.verticalCenter anchors.left: monIcon.right anchors.leftMargin: 4 text: monitor.modelData.name color: Config.colors.tertiary font.family: Config.font.family.mono font.pointSize: Config.font.size.smaller } CustomRect { anchors.verticalCenter: parent.verticalCenter anchors.left: monText.right anchors.right: parent.right anchors.leftMargin: 8 anchors.rightMargin: 18 height: 1 color: Config.colors.inactive } } ListView { id: workspaceList anchors.top: monitorHeader.bottom anchors.topMargin: 8 width: list.width - 20 spacing: 22 height: contentHeight acceptedButtons: Qt.NoButton boundsBehavior: Flickable.StopAtBounds model: monitor.workspaces delegate: Column { id: entry required property int index required property HyprlandWorkspace workspace width: list.width - 20 spacing: 5 property bool hoveredWhileDragging: false z: root.dragSourceWorkspace === workspace ? 10 : 0 Item { id: header anchors.left: parent.left anchors.right: parent.right implicitHeight: labelState.height property color nonAnimColor: entry.hoveredWhileDragging ? Config.colors.workspaceMove : entry.workspace?.active ? Config.colors.workspaces : Config.colors.primary property color color: nonAnimColor Behavior on color { CAnim {} } StateLayer { id: labelState anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left implicitWidth: label.width + 20 implicitHeight: label.height + 8 color: Config.colors.secondary function onClicked(): void { Hypr.dispatch(`workspace ${entry.workspace.id}`) } Row { id: label anchors.centerIn: parent spacing: 7 MaterialIcon { text: Icons.getWorkspaceIcon(entry.workspace) color: header.color font.pointSize: Config.font.size.larger animate: true animateDuration: Config.anim.durations.small } CustomText { anchors.top: parent.top anchors.topMargin: 1 text: qsTr("Workspace %1").arg(index + 1) font.family: Config.font.family.mono font.pointSize: Config.font.size.normal color: header.color } } } StateLayer { anchors.verticalCenter: parent.verticalCenter anchors.right: downArrow.left anchors.rightMargin: 2 implicitWidth: implicitHeight implicitHeight: upIcon.height + 6 disabled: entry.index === 0 && monitor.index === 0 function onClicked(): void { if (entry.index !== 0) { monitor.workspaces.move(entry.index, entry.index - 1, 1) } else { const workspace = entry.workspace; monitor.workspaces.remove(entry.index, 1); const otherWorkspaces = list.itemAtIndex(monitor.index - 1).workspaces; otherWorkspaces.insert(0, {"workspace": workspace}); } } MaterialIcon { id: upIcon anchors.centerIn: parent text: "keyboard_arrow_up" color: parent.disabled ? Config.colors.inactive : header.nonAnimColor font.pointSize: Config.font.size.larger Behavior on color { CAnim {} } } } StateLayer { id: downArrow anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right implicitWidth: implicitHeight implicitHeight: downIcon.height + 6 disabled: entry.index === monitor.workspaces.count - 1 && monitor.index === list.count - 1 function onClicked(): void { if (entry.index !== monitor.workspaces.count - 1) { root.uiState.workspaces.move(entry.index, entry.index + 1, 1) } else { const workspace = entry.workspace; monitor.workspaces.remove(entry.index, 1); const otherWorkspaces = list.itemAtIndex(monitor.index + 1).workspaces; otherWorkspaces.append({"workspace": workspace}); } } MaterialIcon { id: downIcon anchors.centerIn: parent text: "keyboard_arrow_down" color: parent.disabled ? Config.colors.inactive : header.nonAnimColor font.pointSize: Config.font.size.larger Behavior on color { CAnim {} } } } } CustomRect { id: preview anchors.left: parent.left anchors.right: parent.right readonly property HyprlandMonitor mon: entry.workspace.monitor // Exclude UI border and apply monitor scale readonly property real monX: Config.border.thickness readonly property real monY: Config.bar.height readonly property real monWidth: (mon.width / mon.scale) - 2 * Config.border.thickness readonly property real monHeight: (mon.height / mon.scale) - Config.bar.height - Config.border.thickness readonly property real aspectRatio: monHeight / monWidth readonly property real sizeRatio: Config.dashboard.workspaceWidth / monWidth width: Config.dashboard.workspaceWidth height: aspectRatio * width radius: 7 color: entry.hoveredWhileDragging ? Config.colors.workspaceMove : entry.workspace?.active ? Color.mute(Config.colors.workspaces, 1.5, 1.1) : Config.colors.container Behavior on color { CAnim {} } DropArea { anchors.fill: parent onEntered: { root.dragTargetWorkspace = entry.workspace; entry.hoveredWhileDragging = true; } onExited: { if (root.draggingTargetWorkspace === entry.workspace) root.draggingTargetWorkspace = null; entry.hoveredWhileDragging = false; } } Repeater { anchors.fill: parent model: entry.workspace?.toplevels delegate: Item { id: window required property HyprlandToplevel modelData property var ipc: modelData.lastIpcObject opacity: ipc && ipc.at && !ipc.hidden ? 1 : 0 property real nonAnimX: ipc?.at ? (ipc.at[0] - preview.monX) * preview.sizeRatio : 0 property real nonAnimY: ipc?.at ? (ipc.at[1] - preview.monY) * preview.sizeRatio : 0 property real nonAnimWidth: ipc?.size ? ipc.size[0] * preview.sizeRatio : 0 property real nonAnimHeight: ipc?.size ? ipc.size[1] * preview.sizeRatio : 0 x: nonAnimX y: nonAnimY width: nonAnimWidth height: nonAnimHeight z: nonAnimX !== x || nonAnimY !== y ? 10 : 0 Behavior on x { enabled: window.x !== 0 Anim {} } Behavior on y { enabled: window.y !== 0 Anim {} } Behavior on width { enabled: window.width !== 0 Anim {} } Behavior on height { enabled: window.height !== 0 Anim {} } Behavior on opacity { Anim {} } ScreencopyView { id: view visible: false anchors.left: parent.left anchors.top: parent.top layer.enabled: true // NOTE: Simulates cropping fill (which ScreencopyView does not natively support) readonly property real aspectRatio: sourceSize.height / sourceSize.width width: Math.max(window.nonAnimWidth, window.nonAnimHeight / aspectRatio) height: Math.max(window.nonAnimHeight, window.nonAnimWidth * aspectRatio) captureSource: window.modelData.wayland live: true CustomRect { anchors.fill: parent color: mouse.drag.active ? Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaceMove, 0.2)) : mouse.pressed ? Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaces, 0.2)) : mouse.containsMouse ? Qt.tint(Qt.alpha(Config.colors.overlay, 0.5), Qt.alpha(Config.colors.workspaces, 0.1)) : Qt.alpha(Config.colors.overlay, 0.5) Behavior on color { CAnim { duration: Config.anim.durations.small } } } } Item { id: mask visible: false anchors.fill: view layer.enabled: true Rectangle { width: window.width height: window.height radius: 8 } } MultiEffect { anchors.fill: view source: view maskEnabled: true maskSource: mask } IconImage { id: icon anchors.centerIn: parent implicitSize: Math.min(48, window.width * 0.5, window.height * 0.5) source: Icons.getAppIcon(window.ipc?.class ?? "", "") } // Interactions Drag.active: mouse.drag.active Drag.source: window Drag.hotSpot.x: nonAnimWidth / 2 Drag.hotSpot.y: nonAnimHeight / 2 MouseArea { id: mouse anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.AllButtons drag.target: parent onPressed: event => { root.dragSourceWorkspace = entry.workspace; window.Drag.hotSpot.x = event.x; window.Drag.hotSpot.y = event.y; } onReleased: event => { const targetWorkspace = root.dragTargetWorkspace; root.dragSourceWorkspace = null; root.dragScrollVelocity = 0; if (targetWorkspace !== null && targetWorkspace !== entry.workspace) { Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace.id}, address:0x${window.modelData.address}`) } else { window.x = Qt.binding(() => window.nonAnimX); window.y = Qt.binding(() => window.nonAnimY); } } onClicked: event => { if (event.button === Qt.LeftButton) { root.uiState.dashboard = false; Hypr.dispatch(`focuswindow address:0x${window.modelData.address}`); } else if (event.button === Qt.MiddleButton) { Hypr.dispatch(`closewindow address:0x${window.modelData.address}`); } else if (event.button === Qt.RightButton) { root.uiState.dashboard = false; popouts.currentName = "activewindow"; popouts.currentCenter = QsWindow.window.width / 2; popouts.window = window.modelData; popouts.hasCurrent = true; } } onPositionChanged: event => { if (!drag.active) return; const y = root.mapFromItem(this, 0, event.y).y; if (y < 100) { root.dragScrollVelocity = (y - 100) / 25; } else if (y > root.height - 100) { root.dragScrollVelocity = (y - root.height + 100) / 25; } else { root.dragScrollVelocity = 0; } } } } } } } add: Transition { Anim { property: "opacity" from: 0 to: 1 } } remove: Transition { Anim { property: "opacity" from: 1 to: 0 } } move: Transition { Anim { property: "y" } Anim { properties: "opacity" to: 1 } } displaced: Transition { Anim { property: "y" } Anim { properties: "opacity" to: 1 } } } } } Timer { running: root.dragScrollVelocity !== 0 repeat: true interval: 10 onTriggered: { list.contentY += root.dragScrollVelocity; list.returnToBounds(); } } }