// One workspace in the overview: a proportional card holding live thumbnails // of its windows, labeled underneath. The focused workspace is outlined in the // accent color, as GNOME outlines the current workspace. // // A card with a null `workspace` is the trailing "new workspace" affordance // that dynamic-workspace setups (GNOME's, and Hyprland's) always show. import Quickshell import QtQuick import qs.config Item { id: root // HyprlandWorkspace, or null for the trailing "new workspace" card. required property var workspace // Shown on the empty trailing card. property int newWorkspaceId: 0 property int refreshToken: 0 // `emphasized` is the overview's current spatial selection. It normally // follows compositor focus, but can point at a requested focus workspace // before the user commits to switching there. property bool emphasized: focused property bool focusBound: false signal activated signal windowActivated(var toplevel) signal windowCloseRequested(var toplevel) signal windowDropped(var toplevel, string workspaceName) readonly property bool isNew: !workspace readonly property var windows: workspace ? workspace.toplevels.values : [] readonly property bool focused: workspace ? workspace.focused : false readonly property string dropWorkspaceName: workspace ? String(workspace.id) : (newWorkspaceId > 0 ? String(newWorkspaceId) : "") readonly property string label: { if (isNew) return qsTr("New"); // Named workspaces keep their name; numbered ones read better as // "Workspace N" only when there is room, so just show the name. return workspace.name ? workspace.name : String(workspace.id); } // Card body — the label sits under it, so reserve that space. readonly property int labelHeight: 22 Rectangle { id: card anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom anchors.bottomMargin: root.labelHeight radius: Theme.cardRadius color: Theme.alpha(Theme.bgDark, 0.55) border.width: 2 border.color: { if (dropArea.containsDrag) return Theme.ok; if (root.emphasized) return Theme.accent; if (root.focusBound) return Theme.alpha(Theme.accentSecondary, 0.72); return Theme.alpha(Theme.fg, 0.08); } Behavior on border.color { ColorAnimation { duration: Theme.durFast } } // Windows are laid out on a near-square grid. Hyprland does not expose // live per-window geometry, so an honest grid beats a wrong guess at // the real tiling. readonly property int columns: Math.max(1, Math.ceil(Math.sqrt(root.windows.length))) readonly property int rows: Math.max(1, Math.ceil(root.windows.length / columns)) readonly property int cellW: Math.floor((width - Theme.itemSpacing * (columns + 1)) / columns) readonly property int cellH: Math.floor((height - Theme.itemSpacing * (rows + 1)) / rows) Grid { anchors.centerIn: parent columns: card.columns spacing: Theme.itemSpacing visible: root.windows.length > 0 Repeater { model: root.windows WindowThumbnail { required property var modelData toplevel: modelData refreshToken: root.refreshToken width: card.cellW height: card.cellH onActivated: root.windowActivated(modelData) onCloseRequested: root.windowCloseRequested(modelData) } } } DropArea { id: dropArea anchors.fill: parent z: 3 enabled: root.dropWorkspaceName !== "" onDropped: drop => { if (!drop.source?.toplevel) return; root.windowDropped(drop.source.toplevel, root.dropWorkspaceName); drop.acceptProposedAction(); } } // Empty workspace: a big soft "+" so the card still reads as clickable. Text { anchors.centerIn: parent visible: root.windows.length === 0 text: "+" color: Theme.alpha(Theme.fg, 0.25) font.family: Theme.fontFamily font.pixelSize: Math.max(Theme.fontSizeTitle, Math.round(card.height * 0.28)) } Rectangle { anchors.top: parent.top anchors.right: parent.right anchors.margins: 8 visible: root.focusBound width: focusBadge.implicitWidth + 16 height: 24 radius: Theme.pillRadius border.width: 0 color: Theme.alpha(Theme.bgDark, 0.82) Row { id: focusBadge anchors.centerIn: parent spacing: 5 Text { anchors.verticalCenter: parent.verticalCenter text: "\u{F051F}" color: Theme.accent font.family: Theme.fontMono font.pixelSize: 12 } Text { anchors.verticalCenter: parent.verticalCenter text: "Focus" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium } } } // Sits below the thumbnails so clicking a window still focuses it. MouseArea { anchors.fill: parent z: -1 onClicked: root.activated() } } Text { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter height: root.labelHeight verticalAlignment: Text.AlignVCenter text: root.label color: root.emphasized ? Theme.accent : Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.bold: root.emphasized } }