From bc80b3c77e82070c21f9dd652168247ec4f468d6 Mon Sep 17 00:00:00 2001 From: Fernando Romiti Date: Tue, 3 Mar 2026 09:46:56 +1300 Subject: [PATCH] Accommodate for the target workspace's bounds when layoutFloatingWindow and unhideFromCorner calculates the coordinates for the top-left corner of the window. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: When moving a floating window to another workspace (potentially on a different monitor with different resolution), the window could end up placed almost completely outside the bounds of the target workspace. This happens because the proportional repositioning logic only remaps the window's top-left corner without considering the window's size, so a window near the edge of a larger monitor gets its top-left mapped to the edge of a smaller monitor, and the window extends almost completely off the screen. Root Cause: When layoutFloatingWindow and unhideFromCorner calculates the coordinates for the top-left corner it does not accommodate for the target workspace's bounds. Fix: layoutFloatingWindow: - Replaced two separate AX calls of `getCenter()` and `getAxTopLeftCorner()` with a single call to `getAxRect()` to get both position and size of the window. - After computing the proportional position on the target workspace, restrict the position using `coerceIn` so the window stays within `workspace.workspaceMonitor.visibleRect`, accounting for the window's width and height unhideFromCorner: - After computing the restored proportional position on the workspace, clamp it within `workspaceRect` using `lastFloatingSize` to account for window dimensions. The restrict logic uses `max(rect.minX, rect.maxX - windowWidth)` as the upper bound, which handles the edge case where the window is larger than the target workspace — in that case, the range collapses to `minX...minX` and the window is pinned to the top-left corner of the workspace (which is the most reasonable behavior). Related discussion: https://github.com/nikitabobko/AeroSpace/discussions/1875 (with my fix the screen no longer disappear, but it gets re positioned in the screen) Possible related issue: https://github.com/nikitabobko/AeroSpace/issues/1519 --- .../AppBundle/layout/layoutRecursive.swift | 21 ++++++++++++------- Sources/AppBundle/tree/MacWindow.swift | 15 ++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Sources/AppBundle/layout/layoutRecursive.swift b/Sources/AppBundle/layout/layoutRecursive.swift index face0a784..1f3b685a5 100644 --- a/Sources/AppBundle/layout/layoutRecursive.swift +++ b/Sources/AppBundle/layout/layoutRecursive.swift @@ -69,16 +69,23 @@ extension Window { @MainActor fileprivate func layoutFloatingWindow(_ context: LayoutContext) async throws { let workspace = context.workspace - let currentMonitor = try await getCenter()?.monitorApproximation // Probably not idempotent - if let currentMonitor, let windowTopLeftCorner = try await getAxTopLeftCorner(), workspace != currentMonitor.activeWorkspace { + let windowRect = try await getAxRect() // Probably not idempotent + let currentMonitor = windowRect?.center.monitorApproximation + if let currentMonitor, let windowRect, workspace != currentMonitor.activeWorkspace { + let windowTopLeftCorner = windowRect.topLeftCorner let xProportion = (windowTopLeftCorner.x - currentMonitor.visibleRect.topLeftX) / currentMonitor.visibleRect.width let yProportion = (windowTopLeftCorner.y - currentMonitor.visibleRect.topLeftY) / currentMonitor.visibleRect.height - let moveTo = workspace.workspaceMonitor - setAxFrame(CGPoint( - x: moveTo.visibleRect.topLeftX + xProportion * moveTo.visibleRect.width, - y: moveTo.visibleRect.topLeftY + yProportion * moveTo.visibleRect.height, - ), nil) + let workspaceRect = workspace.workspaceMonitor.visibleRect + var newX = workspaceRect.topLeftX + xProportion * workspaceRect.width + var newY = workspaceRect.topLeftY + yProportion * workspaceRect.height + + let windowWidth = windowRect.width + let windowHeight = windowRect.height + newX = newX.coerceIn(workspaceRect.minX ... max(workspaceRect.minX, workspaceRect.maxX - windowWidth)) + newY = newY.coerceIn(workspaceRect.minY ... max(workspaceRect.minY, workspaceRect.maxY - windowHeight)) + + setAxFrame(CGPoint(x: newX, y: newY), nil) } if isFullscreen { layoutFullscreen(context) diff --git a/Sources/AppBundle/tree/MacWindow.swift b/Sources/AppBundle/tree/MacWindow.swift index bd702c0e6..a8d70fe92 100644 --- a/Sources/AppBundle/tree/MacWindow.swift +++ b/Sources/AppBundle/tree/MacWindow.swift @@ -163,11 +163,16 @@ final class MacWindow: Window { // Tiling windows should be unhidden with layoutRecursive anyway case .floatingWindow: let workspaceRect = nodeWorkspace.workspaceMonitor.rect - let pointInsideWorkspace = CGPoint( - x: workspaceRect.width * prevUnhiddenProportionalPositionInsideWorkspaceRect.x, - y: workspaceRect.height * prevUnhiddenProportionalPositionInsideWorkspaceRect.y, - ) - setAxFrame(workspaceRect.topLeftCorner + pointInsideWorkspace, nil) + var newX = workspaceRect.topLeftX + workspaceRect.width * prevUnhiddenProportionalPositionInsideWorkspaceRect.x + var newY = workspaceRect.topLeftY + workspaceRect.height * prevUnhiddenProportionalPositionInsideWorkspaceRect.y + // todo we probably should replace lastFloatingSize with proper floating window sizing + // https://github.com/nikitabobko/AeroSpace/issues/1519 + let windowWidth = lastFloatingSize?.width ?? 0 + let windowHeight = lastFloatingSize?.height ?? 0 + newX = newX.coerceIn(workspaceRect.minX ... max(workspaceRect.minX, workspaceRect.maxX - windowWidth)) + newY = newY.coerceIn(workspaceRect.minY ... max(workspaceRect.minY, workspaceRect.maxY - windowHeight)) + + setAxFrame(CGPoint(x: newX, y: newY), nil) case .macosNativeFullscreenWindow, .macosNativeHiddenAppWindow, .macosNativeMinimizedWindow, .macosPopupWindow, .tiling, .rootTilingContainer, .shimContainerRelation: break }