diff --git a/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml b/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml index 54d41f6b1..593262670 100644 --- a/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml +++ b/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Layouts import Quickshell import Quickshell.Hyprland import qs.Common @@ -80,7 +79,6 @@ Item { readonly property int displayWorkspaceCount: displayedWorkspaceIds.length readonly property int effectiveColumns: SettingsData.overviewColumns - readonly property int effectiveRows: Math.max(SettingsData.overviewRows, Math.ceil(displayWorkspaceCount / effectiveColumns)) function getWorkspaceMonitorName(workspaceId) { if (!allWorkspaces || !workspaceId) @@ -107,21 +105,53 @@ Item { } } - function getWorkspaceViewportBounds(workspaceId) { + function monitorIpcForWorkspace(workspaceId) { const workspace = allWorkspaces?.find(ws => ws?.id === workspaceId); - const mon = workspace?.monitor?.lastIpcObject || monitor?.lastIpcObject || {}; - const reserved = mon.reserved || [0, 0, 0, 0]; + return workspace?.monitor?.lastIpcObject ?? monitor?.lastIpcObject ?? null; + } - const x = (mon.x ?? 0) + (reserved[0] ?? 0); - const y = (mon.y ?? 0) + (reserved[1] ?? 0); - const width = Math.max((mon.width ?? monitorPhysicalWidth) - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1); - const height = Math.max((mon.height ?? monitorPhysicalHeight) - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1); - const scale = Math.min(root.workspaceImplicitWidth / width, root.workspaceImplicitHeight / height); + function monitorLogicalSize(ipc) { + if (!ipc || !ipc.width || !ipc.height) + return { + "width": monitorPhysicalWidth, + "height": monitorPhysicalHeight + }; + + const monScale = ipc.scale > 0 ? ipc.scale : 1; + const rotated = ((ipc.transform ?? 0) % 2) === 1; + return { + "width": (rotated ? ipc.height : ipc.width) / monScale, + "height": (rotated ? ipc.width : ipc.height) / monScale + }; + } + + function cellForWorkspace(workspaceId) { + const cell = gridLayout.cells.find(c => c.id === workspaceId); + if (cell) + return cell; + return { + "id": workspaceId, + "x": 0, + "y": 0, + "width": workspaceImplicitWidth, + "height": workspaceImplicitHeight + }; + } + + function getWorkspaceViewportBounds(workspaceId, cellWidth, cellHeight) { + const ipc = monitorIpcForWorkspace(workspaceId) ?? {}; + const logical = monitorLogicalSize(ipc); + const reserved = ipc.reserved || [0, 0, 0, 0]; + + const x = (ipc.x ?? 0) + (reserved[0] ?? 0); + const y = (ipc.y ?? 0) + (reserved[1] ?? 0); + const width = Math.max(logical.width - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1); + const height = Math.max(logical.height - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1); return { "x": x, "y": y, - "scale": scale + "scale": Math.min(cellWidth / width, cellHeight / height) }; } @@ -143,6 +173,56 @@ Item { property int draggingFromWorkspace: -1 property int draggingTargetWorkspace: -1 + readonly property var gridLayout: { + const ids = displayedWorkspaceIds; + const columns = effectiveColumns; + if (!ids || ids.length === 0 || columns < 1) + return { + "cells": [], + "width": 0, + "height": 0 + }; + + const rows = []; + for (let i = 0; i < ids.length; i += columns) { + rows.push(ids.slice(i, i + columns).map(id => { + const logical = monitorLogicalSize(monitorIpcForWorkspace(id)); + return { + "id": id, + "width": logical.width * scale, + "height": logical.height * scale + }; + })); + } + + const rowWidth = row => row.reduce((acc, cell) => acc + cell.width, 0) + workspaceSpacing * (row.length - 1); + const totalWidth = rows.reduce((acc, row) => Math.max(acc, rowWidth(row)), 0); + + const cells = []; + let cursorY = 0; + for (const row of rows) { + const rowHeight = row.reduce((acc, cell) => Math.max(acc, cell.height), 0); + let cursorX = (totalWidth - rowWidth(row)) / 2; + for (const cell of row) { + cells.push({ + "id": cell.id, + "x": cursorX, + "y": cursorY + (rowHeight - cell.height) / 2, + "width": cell.width, + "height": cell.height + }); + cursorX += cell.width + workspaceSpacing; + } + cursorY += rowHeight + workspaceSpacing; + } + + return { + "cells": cells, + "width": totalWidth, + "height": cursorY - workspaceSpacing + }; + } + implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2 implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2 @@ -166,8 +246,8 @@ Item { anchors.fill: parent anchors.margins: Theme.spacingL - implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2 - implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2 + implicitWidth: workspaceGrid.implicitWidth + padding * 2 + implicitHeight: workspaceGrid.implicitHeight + padding * 2 radius: Theme.cornerRadius color: Theme.surfaceContainer @@ -182,84 +262,78 @@ Item { shadowEnabled: Theme.elevationEnabled } - ColumnLayout { - id: workspaceColumnLayout + Item { + id: workspaceGrid z: root.workspaceZ anchors.centerIn: parent - spacing: workspaceSpacing + implicitWidth: root.gridLayout.width + implicitHeight: root.gridLayout.height Repeater { - model: root.effectiveRows - delegate: RowLayout { - id: row - property int rowIndex: index - spacing: workspaceSpacing + model: root.gridLayout.cells.length - Repeater { - model: root.effectiveColumns - Rectangle { - id: workspace - property int colIndex: index - property int workspaceIndex: rowIndex * root.effectiveColumns + colIndex - property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1 - property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false - property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null - property bool isActive: workspaceObj?.active ?? false - property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true - property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false - property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : "" - property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3) - property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1) - property color hoveredBorderColor: Theme.surfaceVariant - property bool hoveredWhileDragging: false - property bool shouldShowActiveIndicator: isActive && isOnThisMonitor && hasWindows + Rectangle { + id: workspace + required property int index + readonly property var cell: root.gridLayout.cells[index] ?? null + property int workspaceValue: cell?.id ?? -1 + property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false + property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null + property bool isActive: workspaceObj?.active ?? false + property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true + property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false + property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3) + property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1) + property color hoveredBorderColor: Theme.surfaceVariant + property bool hoveredWhileDragging: false + property bool shouldShowActiveIndicator: isActive && isOnThisMonitor && hasWindows - visible: workspaceValue !== -1 + visible: workspaceValue !== -1 - implicitWidth: root.workspaceImplicitWidth - implicitHeight: root.workspaceImplicitHeight - color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor - radius: Theme.cornerRadius - border.width: 2 - border.color: hoveredWhileDragging ? hoveredBorderColor : (shouldShowActiveIndicator ? root.activeBorderColor : Theme.withAlpha(root.activeBorderColor, 0)) + x: cell?.x ?? 0 + y: cell?.y ?? 0 + width: cell?.width ?? 0 + height: cell?.height ?? 0 + color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor + radius: Theme.cornerRadius + border.width: 2 + border.color: hoveredWhileDragging ? hoveredBorderColor : (shouldShowActiveIndicator ? root.activeBorderColor : Theme.withAlpha(root.activeBorderColor, 0)) - StyledText { - anchors.centerIn: parent - text: workspaceValue - font.pixelSize: Theme.fontSizeXLarge * 6 - font.weight: Font.DemiBold - color: Theme.withAlpha(Theme.surfaceText, workspaceExists ? 0.2 : 0.1) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + StyledText { + anchors.centerIn: parent + text: workspace.workspaceValue + font.pixelSize: Theme.fontSizeXLarge * 6 + font.weight: Font.DemiBold + color: Theme.withAlpha(Theme.surfaceText, workspace.workspaceExists ? 0.2 : 0.1) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + id: workspaceArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + if (root.draggingTargetWorkspace === -1) { + root.overviewOpen = false; + HyprlandService.focusWorkspace(workspace.workspaceValue); } + } + } - MouseArea { - id: workspaceArea - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - if (root.draggingTargetWorkspace === -1) { - root.overviewOpen = false; - HyprlandService.focusWorkspace(workspaceValue); - } - } - } - - DropArea { - anchors.fill: parent - onEntered: { - root.draggingTargetWorkspace = workspaceValue; - if (root.draggingFromWorkspace == root.draggingTargetWorkspace) - return; - hoveredWhileDragging = true; - } - onExited: { - hoveredWhileDragging = false; - if (root.draggingTargetWorkspace == workspaceValue) - root.draggingTargetWorkspace = -1; - } - } + DropArea { + anchors.fill: parent + onEntered: { + root.draggingTargetWorkspace = workspace.workspaceValue; + if (root.draggingFromWorkspace == root.draggingTargetWorkspace) + return; + workspace.hoveredWhileDragging = true; + } + onExited: { + workspace.hoveredWhileDragging = false; + if (root.draggingTargetWorkspace == workspace.workspaceValue) + root.draggingTargetWorkspace = -1; } } } @@ -269,8 +343,8 @@ Item { Item { id: windowSpace anchors.centerIn: parent - implicitWidth: workspaceColumnLayout.implicitWidth - implicitHeight: workspaceColumnLayout.implicitHeight + implicitWidth: workspaceGrid.implicitWidth + implicitHeight: workspaceGrid.implicitHeight Repeater { model: ScriptModel { @@ -306,41 +380,21 @@ Item { overviewOpen: root.overviewOpen readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1 - - function getWorkspaceIndex() { - if (!root.displayedWorkspaceIds || root.displayedWorkspaceIds.length === 0) - return 0; - if (!windowWorkspaceId || windowWorkspaceId < 0) - return 0; - try { - for (let i = 0; i < root.displayedWorkspaceIds.length; i++) { - if (root.displayedWorkspaceIds[i] === windowWorkspaceId) { - return i; - } - } - return 0; - } catch (e) { - return 0; - } - } - - readonly property int workspaceIndex: getWorkspaceIndex() - readonly property int workspaceColIndex: workspaceIndex % root.effectiveColumns - readonly property int workspaceRowIndex: Math.floor(workspaceIndex / root.effectiveColumns) - readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId) + readonly property var workspaceCell: root.cellForWorkspace(windowWorkspaceId) + readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId, workspaceCell.width, workspaceCell.height) toplevel: modelData scale: root.scale monitorDpr: root.dpr - availableWorkspaceWidth: root.workspaceImplicitWidth - availableWorkspaceHeight: root.workspaceImplicitHeight + availableWorkspaceWidth: workspaceCell.width + availableWorkspaceHeight: workspaceCell.height contentOriginX: workspaceBounds.x contentOriginY: workspaceBounds.y contentScale: workspaceBounds.scale widgetMonitorId: root.monitor.id - xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex - yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex + xOffset: workspaceCell.x + yOffset: workspaceCell.y z: atInitPosition ? root.windowZ : root.windowDraggingZ property bool atInitPosition: (initX == x && initY == y) @@ -409,54 +463,44 @@ Item { Item { id: monitorLabelSpace anchors.centerIn: parent - implicitWidth: workspaceColumnLayout.implicitWidth - implicitHeight: workspaceColumnLayout.implicitHeight + implicitWidth: workspaceGrid.implicitWidth + implicitHeight: workspaceGrid.implicitHeight z: root.monitorLabelZ Repeater { - model: root.effectiveRows + model: root.gridLayout.cells.length delegate: Item { - id: labelRow - property int rowIndex: index - y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex - width: parent.width - height: root.workspaceImplicitHeight + id: labelItem + required property int index + readonly property var cell: root.gridLayout.cells[index] ?? null + property int workspaceValue: cell?.id ?? -1 + property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false + property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : "" - Repeater { - model: root.effectiveColumns - delegate: Item { - id: labelItem - property int colIndex: index - property int workspaceIndex: labelRow.rowIndex * root.effectiveColumns + colIndex - property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1 - property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false - property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : "" + x: cell?.x ?? 0 + y: cell?.y ?? 0 + width: cell?.width ?? 0 + height: cell?.height ?? 0 - x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex - width: root.workspaceImplicitWidth - height: root.workspaceImplicitHeight + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingS + width: monitorNameText.contentWidth + Theme.spacingS * 2 + height: monitorNameText.contentHeight + Theme.spacingXS * 2 + radius: Theme.cornerRadius + color: Theme.surface + visible: labelItem.workspaceExists && labelItem.workspaceMonitorName !== "" - Rectangle { - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacingS - width: monitorNameText.contentWidth + Theme.spacingS * 2 - height: monitorNameText.contentHeight + Theme.spacingXS * 2 - radius: Theme.cornerRadius - color: Theme.surface - visible: labelItem.workspaceExists && labelItem.workspaceMonitorName !== "" - - StyledText { - id: monitorNameText - anchors.centerIn: parent - text: labelItem.workspaceMonitorName - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } + StyledText { + id: monitorNameText + anchors.centerIn: parent + text: labelItem.workspaceMonitorName + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter } } } diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index a82034bc2..9b9238dc5 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -942,15 +942,16 @@ Singleton { } } - // Primary detection asks the OS which process owns $WAYLAND_DISPLAY — the - // compositor quickshell is actually connected to. Env vars like + // Primary detection asks the kernel which process owns the $WAYLAND_DISPLAY + // socket — the compositor quickshell is actually connected to. Env vars like // HYPRLAND_INSTANCE_SIGNATURE / MANGO_INSTANCE_SIGNATURE can leak into the - // systemd user environment from previous sessions and lie. + // systemd user environment from previous sessions and lie. Unset + // WAYLAND_DISPLAY falls back to "wayland-0", mirroring wl_display_connect. + // /proc/net/unix: field 6 is state (01 = listening), 7 inode, 8 bound path. function detectCompositor() { - const script = 'sock="$WAYLAND_DISPLAY"; case "$sock" in /*) ;; *) sock="$XDG_RUNTIME_DIR/$sock" ;; esac; ss -xlp 2>/dev/null | grep -F "$sock " | head -n1'; + const script = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"'; Proc.runCommand("waylandSocketOwner", ["sh", "-c", script], (output, exitCode) => { - const match = output ? output.match(/users:\(\("([^"]+)"/) : null; - const comm = match ? match[1].toLowerCase() : ""; + const comm = (exitCode === 0 && output) ? output.trim().toLowerCase() : ""; const name = _compositorNameFromComm(comm); if (name) { _applyCompositor(name);