Skip to content

macos: Keep a zoomed window's normal frame from before the zoom - #1434

Merged
madcodelife merged 1 commit into
mainfrom
macos-zoomed-restore-bounds
Nov 9, 2026
Merged

madcodelife merged 1 commit into
mainfrom
macos-zoomed-restore-bounds

Conversation

@madcodelife

Copy link
Copy Markdown
Member

Description

On macOS, a zoomed (maximized) window's placement carries the wrong normal bounds: a frame close to the zoomed one instead of the one the window had before the zoom. With restoreBounds, that is what gets saved, so after quitting while zoomed and launching again, leaving the zoom brings the window back to nearly the zoomed size instead of where the user had it.

The cause: AppKit animates zoom:, and the window posts NSWindowDidResize / NSWindowDidMove for the frames on the way. The main window's state is read on each of them, and since the window is not zoomed yet, read_window kept each animated frame as the windowed frame (Watch::windowed). Once the window is zoomed, the last frame of the animation is what remained. The same happens when the runner opens a kept maximized window (it sets the kept frame, then zooms).

The fix: every zoom: (the Window menu, the zoom button, a double click on the title bar, WindowRef.setMode / setPlacement, the runner's launch) asks the window's delegate windowShouldZoom:toFrame: first. The standalone main window's delegate and the delegate of the app's own windows now answer it with window_state::will_zoom, which keeps the frame the window has then as its windowed frame and holds it until a read sees the window zoomed (or in full screen, or minimized). Leaving the zoom is unchanged: the moves on the way are normal-mode reads, and the last one is the frame AppKit restores.

A window of a host app that embeds an AppView has the host's delegate, so it keeps the old behavior.

How to Test

Manual, on macOS 27.0.1 (one display, 1800×1169), with examples/tasks-rs changed locally (not in this PR) to open with restoreBounds:

platform::Root::of::<tasks_rs::App>()
    .app_id("com.example.tasks.rust")
    .main_window_json(r#"{"restoreBounds":true}"#)

HOME pointed at an empty directory, the window moved and resized through System Events, zoomed through Window > Zoom, quit with kill, and the kept placement read from the app's storage.json (nsh.window:main):

Step Window frame (x, y, w, h) Kept placement, before Kept placement, after
Move and resize 200, 150, 700, 500 normal 200, 150, 700×500 normal 200, 150, 700×500
Zoom 74, 39, 1726, 1130 maximized 76, 41, 1707×1118 maximized 200, 150, 700×500
Quit, launch again 74, 39, 1726, 1130 (zoomed) maximized 75, 40, 1715×1123 maximized 200, 150, 700×500
Leave the zoom before: 76, 41, 1707, 1118 · after: 200, 150, 700, 500 normal 76, 41, 1707×1118 normal 200, 150, 700×500

Also checked after the fix, in one run: zoom and leave it, then move and resize to 300, 200, 640×480, zoom again (kept: maximized 300, 200, 640×480) and leave it (back to 300, 200, 640×480); full screen in and out through AXFullScreen (kept: fullScreen with 300, 200, 640×480, then normal at the same frame), as before.

No automated test: the behavior needs a real NSWindow, its zoom animation and the runner's launch on the main thread; the crate's window tests don't drive a standalone app's launch.

Checks:

  • cargo +nightly fmt --all
  • cargo clippy -p nsh-platform-macos -p nsh-macos-host -p nsh-macos-app --all-targets: no warnings
  • cargo test -p nsh-platform-macos: 115 unit tests and the window tests pass

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (if any) is accurate.
  • Passed cargo test --workspace, cargo +nightly fmt --all and clippy (including the Catalyst and Android runs for platform code). — fmt and the macOS crates' clippy and tests; the change is in nsh-platform-macos only.
  • Changes to what apps use keep what released versions shipped and come with a case in tests/compat/ (cargo xtask compat). — no change to what apps use.
  • Ran the related examples/gallery demos on iOS and Android (if the change is visible or platform-specific). — macOS only; run on macOS as above.

AppKit animates zoom:, and the window posts moves and resizes for the
frames on the way. The main window's state is read on each of them,
and as the window is not zoomed yet, every animated frame was kept as
its windowed frame. A zoomed window's placement then carried a frame
close to the zoomed one: restoreBounds saved it, and after a relaunch
leaving the zoom brought the window back to nearly the zoomed size.

Every zoom: asks the window's delegate windowShouldZoom:toFrame:
first. The standalone main window's delegate and the delegate of the
app's own windows now keep the frame there and hold it until the window
is zoomed. A host app's window keeps its own delegate and the old
behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@madcodelife
madcodelife merged commit 5485503 into main Nov 9, 2026
6 checks passed
@madcodelife
madcodelife deleted the macos-zoomed-restore-bounds branch November 9, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant