Skip to content

Floating window position outside screen bounds when moving between workspaces#1964

Merged
mobile-ar merged 1 commit intonikitabobko:mainfrom
mobile-ar:fix/floating-window-position
Mar 2, 2026
Merged

Floating window position outside screen bounds when moving between workspaces#1964
mobile-ar merged 1 commit intonikitabobko:mainfrom
mobile-ar:fix/floating-window-position

Conversation

@mobile-ar
Copy link
Collaborator

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: #1875 (with my fix the screen no longer disappear, but it gets re positioned in the screen)
Possible related issue: #1519

PR checklist

  • Explain your changes in the relevant commit messages rather than in the PR description. The PR description must not contain more information than the commit messages (except for images and other media).
  • Each commit must explain what/why/how and motivation in its description. https://cbea.ms/git-commit/
  • Don't forget to link the appropriate issues/discussions in commit messages (if applicable).
  • Each commit must be an atomic change (a PR may contain several commits). Don't introduce new functional changes together with refactorings in the same commit.
  • ./run-tests.sh exits with non-zero exit code.
  • Avoid merge commits, always rebase and force push.

Failure to follow the checklist with no apparent reasons will result in silent PR rejection.

@mobile-ar
Copy link
Collaborator Author

Based on this discussion 1875, this fix improves the usability, so the floating windows will no longer disappear but they will still change position. The issue I can see is that at some point in MacWindow.hideInCorner when calculating prevUnhiddenProportionalPositionInsideWorkspaceRect it might get called twice, returning an incorrect position (off screen now).
This 2 prints happened sequentially one after the other.

hideInCorner. for: 4545. windowRect: Rect(topLeftX: 2212.0, topLeftY: 146.0, _width: 980.0, _height: 600.0) nodeMonitor.visibleRect: Rect(topLeftX: 1800.0, topLeftY: -241.0, _width: 2560.0, _height: 1410.0)
hideInCorner. for: 4545. windowRect: Rect(topLeftX: 4359.0, topLeftY: 1117.0, _width: 980.0, _height: 600.0) nodeMonitor.visibleRect: Rect(topLeftX: 1800.0, topLeftY: -241.0, _width: 2560.0, _height: 1410.0)

I tested adding @MainActor to isHiddenInCorner and prevUnhiddenProportionalPositionInsideWorkspaceRect, this makes it a little more stable, but after some stress testing it start failing again less constant, it looks like some sort of race condition.

Copy link
Owner

@nikitabobko nikitabobko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! The patch makes sense. Feel free to merge it.

One major comment: please change the commit message so that the title (first line of the message) is one-line concise summarization of the change. Right now, the title of the commit is "Issue:" which def looks odd.

And sorry for the delay. I didn't have a lot of time to work on the project lately, but I am going to have much more time to work on the project starting from April.

Comment on lines +72 to +74
guard let windowRect = try await getAxRect() else { return }
let currentMonitor = windowRect.center.monitorApproximation
if workspace != currentMonitor.activeWorkspace {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
guard let windowRect = try await getAxRect() else { return }
let currentMonitor = windowRect.center.monitorApproximation
if workspace != currentMonitor.activeWorkspace {
let windowRect = try await getAxRect() // Probably not idempotent
let currentMonitor = windowRect?.center.monitorApproximation
if let currentMonitor, let windowRect, workspace != currentMonitor.activeWorkspace {

Otherwise, you will skip layoutFullscreen below

setAxFrame(workspaceRect.topLeftCorner + pointInsideWorkspace, nil)
var newX = workspaceRect.topLeftX + workspaceRect.width * prevUnhiddenProportionalPositionInsideWorkspaceRect.x
var newY = workspaceRect.topLeftY + workspaceRect.height * prevUnhiddenProportionalPositionInsideWorkspaceRect.y
let windowWidth = lastFloatingSize?.width ?? 0
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let windowWidth = lastFloatingSize?.width ?? 0
// todo we probably should replace lastFloatingSize with proper floating window sizing
// https://github.com/nikitabobko/AeroSpace/issues/1519
let windowWidth = lastFloatingSize?.width ?? 0

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about the commit message, I added the full explanation in the commit message inside Sublime Merge, it displays the full message:
image

I will fix this commit to have a more descriptive first line and I will add your suggestions too.

…ow and unhideFromCorner calculates the coordinates for the top-left corner of the window.

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: nikitabobko#1875 (with my fix the screen no longer disappear, but it gets re positioned in the screen)
Possible related issue: nikitabobko#1519
@mobile-ar mobile-ar force-pushed the fix/floating-window-position branch from 7bc7205 to bc80b3c Compare March 2, 2026 20:47
@mobile-ar mobile-ar merged commit 8134ad0 into nikitabobko:main Mar 2, 2026
5 checks passed
@mobile-ar mobile-ar deleted the fix/floating-window-position branch March 2, 2026 21:14
@nikitabobko nikitabobko added the pr-merged Pull Request is merged label Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-merged Pull Request is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants