From c3866507bebf336b6426a49badf30fec55ad3d68 Mon Sep 17 00:00:00 2001 From: robin <242058117+kanikamaxxing@users.noreply.github.com> Date: Sat, 23 May 2026 22:05:42 +0200 Subject: [PATCH] overview: render windows correctly on non-origin monitors The mapped monitor object in AxctlService discards mon.metadata.x/y/transform so monitorData.x is undefined for every monitor. OverviewWindow's position math falls back to 0, which works for the monitor at the origin but renders windows on any other monitor outside the thumbnail bounds (the global window x is used without the monitor offset subtracted). Also drops the per-monitor window filter in Overview so each overview shows all windows across monitors, and computes per-window scale from the window's own monitor width so cross-monitor windows fit the cell properly instead of being sized for the overview's monitor. OverviewWindow now also pins the target workspace to the drop monitor after movetoworkspacesilent, so drops on cells in monitor A's overview actually land on monitor A regardless of where the workspace currently lives. And imports qs.modules.bar.workspaces so the existing CompositorData reference on drag finalize stops throwing ReferenceError. Repro for the primary bug: dual-monitor setup with monitors at non-zero offsets. The overview on the second monitor renders all window thumbnails outside their cell because monitorData.x is undefined and global x positions are used as-is. --- modules/services/AxctlService.qml | 3 +++ modules/widgets/overview/Overview.qml | 20 ++++++++++++----- modules/widgets/overview/OverviewWindow.qml | 24 +++++++++++++++------ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/modules/services/AxctlService.qml b/modules/services/AxctlService.qml index bade7b38..79683d67 100644 --- a/modules/services/AxctlService.qml +++ b/modules/services/AxctlService.qml @@ -141,6 +141,9 @@ Singleton { height: mon.height, refreshRate: mon.refresh_rate, scale: mon.scale, + x: parseInt(mon.metadata ? mon.metadata.x : 0) || 0, + y: parseInt(mon.metadata ? mon.metadata.y : 0) || 0, + transform: parseInt(mon.metadata ? mon.metadata.transform : 0) || 0, activeWorkspace: { id: parseInt(mon.metadata ? mon.metadata.active_workspace : 0) || 0, name: mon.metadata ? mon.metadata.active_workspace : "" } })); root.monitors.values = mappedMonitors; diff --git a/modules/widgets/overview/Overview.qml b/modules/widgets/overview/Overview.qml index 6dad0e66..ded832b4 100644 --- a/modules/widgets/overview/Overview.qml +++ b/modules/widgets/overview/Overview.qml @@ -303,16 +303,19 @@ Item { implicitWidth: workspaceColumnLayout.implicitWidth implicitHeight: workspaceColumnLayout.implicitHeight - // Pre-filter windows for this monitor and workspace group + // Pre-filter windows for the visible workspace group. + // Show windows from ALL monitors in every overview instance — the user can + // then drag windows from any monitor's overview to any workspace cell. + // Each window uses its OWN monitor's metadata for position math (looked up + // per-delegate below) so cross-monitor windows render at correct local coords. readonly property var filteredWindowData: { const minWs = overviewRoot.workspaceGroup * overviewRoot.workspacesShown; const maxWs = (overviewRoot.workspaceGroup + 1) * overviewRoot.workspacesShown; - const monId = overviewRoot.monitorId; const toplevels = ToplevelManager.toplevels.values; return overviewRoot.windowList.filter(win => { const wsId = win?.workspace?.id; - return wsId > minWs && wsId <= maxWs && win.monitor === monId; + return wsId > minWs && wsId <= maxWs; }).map(win => ({ windowData: win, toplevel: (() => { @@ -331,12 +334,19 @@ Item { delegate: OverviewWindow { id: window required property var modelData + // Resolve window's own monitor (may differ from overview's monitor when + // showing cross-monitor windows). Used both for position offset AND scale + // so windows always fit the cell regardless of source monitor size. + readonly property var winMonitor: overviewRoot.monitors.find(m => m.id === modelData.windowData.monitor) ?? overviewRoot.monitorData windowData: modelData.windowData toplevel: modelData.toplevel - scale: overviewRoot.scale + // Per-window scale = cell_width / window_monitor_width — keeps each window + // sized as a thumbnail of its OWN monitor, so a DP-2 window in a DP-1 overview + // fills the cell properly instead of rendering at "0.15 * 1920 in a 2560-sized cell". + scale: (winMonitor && winMonitor.width > 0) ? (overviewRoot.workspaceImplicitWidth / winMonitor.width) : overviewRoot.scale availableWorkspaceWidth: overviewRoot.workspaceImplicitWidth availableWorkspaceHeight: overviewRoot.workspaceImplicitHeight - monitorData: overviewRoot.monitorData + monitorData: winMonitor barPosition: overviewRoot.barPosition barReserved: overviewRoot.barReserved diff --git a/modules/widgets/overview/OverviewWindow.qml b/modules/widgets/overview/OverviewWindow.qml index 8943a6ee..4fb266d3 100644 --- a/modules/widgets/overview/OverviewWindow.qml +++ b/modules/widgets/overview/OverviewWindow.qml @@ -6,6 +6,7 @@ import Quickshell.Wayland import qs.modules.globals import qs.modules.theme import qs.modules.services +import qs.modules.bar.workspaces // For CompositorData import qs.modules.components import qs.config @@ -299,6 +300,11 @@ Item { // Check if moving to different workspace if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { + // The monitor whose overview received the drop — pin the target workspace there + // so windows land on the monitor the user dragged within, not on whatever + // monitor the workspace happened to be bound to previously. + const dropMonitorName = overviewRoot.monitor ? overviewRoot.monitor.name : ""; + // Moving to different workspace if (windowData?.floating && (root.x !== root.initX || root.y !== root.initY)) { // Calculate position in the target workspace @@ -307,29 +313,35 @@ Item { const targetRowIndex = Math.floor((targetWorkspace - 1) % overviewRoot.workspacesShown / overviewRoot.columns); const targetXOffset = Math.round((overviewRoot.workspaceImplicitWidth + overviewRoot.workspacePadding + overviewRoot.workspaceSpacing) * targetColIndex + overviewRoot.workspacePadding / 2); const targetYOffset = Math.round((overviewRoot.workspaceImplicitHeight + overviewRoot.workspacePadding + overviewRoot.workspaceSpacing) * targetRowIndex + overviewRoot.workspacePadding / 2); - + // Calculate relative position in target workspace const relativeX = root.x - targetXOffset; const relativeY = root.y - targetYOffset; - + // Convert to percentage const percentageX = Math.round((relativeX / root.availableWorkspaceWidth) * 100); const percentageY = Math.round((relativeY / root.availableWorkspaceHeight) * 100); - + // Move to workspace and set position AxctlService.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${windowData?.address}`); + if (dropMonitorName) { + AxctlService.dispatch(`moveworkspacetomonitor ${targetWorkspace} ${dropMonitorName}`); + } AxctlService.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${windowData?.address}`); - + // Force immediate window data update CompositorData.updateWindowList(); } else { // Just move workspace without repositioning AxctlService.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${windowData?.address}`); - + if (dropMonitorName) { + AxctlService.dispatch(`moveworkspacetomonitor ${targetWorkspace} ${dropMonitorName}`); + } + // Force immediate window data update CompositorData.updateWindowList(); } - + // Reset position in overview root.x = root.initX; root.y = root.initY;