tauri-apps / tauri-apps/plugins-workspace
[bug][window-state] macOS: restored window doubles in size and position when scale_factor() is still 1.0 during on_window_ready
- Dominant language
- Rust
- Stars
- 1.8k
- Forks
- 602
- Avg merge
- 4d 14h
- Merged PRs (30d)
- 9
Description
### Describe the bug
On macOS the restored window can come back at **twice** the saved size and offset, and because the doubled geometry is what gets saved on exit, it grows again on each subsequent launch until it hits the screen edge.
The cause is that `restore_state` runs from `on_window_ready`, and at that point the window has not been associated with a screen yet, so `window.scale_factor()` returns `1.0` while the monitor it is about to appear on really is `2.0`.
Since the saved state is stored in **physical pixels**, `restore_state` applies it with:
```rust
self.set_position(PhysicalPosition { x: ..., y: ... })?;
self.set_size(PhysicalSize { width: state.width, height: state.height })?;
```
Converting those physical values to points divides by the scale factor — which is the stale `1.0` — so the saved physical numbers get applied as if they were logical points, and the window ends up 2× on a Retina display.
This is intermittent: it depends on whether the window already has its screen assigned by the time `on_window_ready` fires. On my multi-monitor, mixed-DPI setup (2× built-in + 2× 5K + 1× 1080p) it happens regularly.
### Evidence
Seeding `.window-state.json` with `1600x1000` at `(200,300)` (physical), then logging inside the app right after startup:
```
[diag immediate] scale=Ok(1.0) inner=Ok(PhysicalSize { width: 3200, height: 1644 }) outer_pos=Ok(PhysicalPosition { x: 400, y: 52 }) monitor=Ok(Some((Some("Monitor #41053"), 2.0, PhysicalSize { width: 2624, height: 1696 })))
[diag settled] scale=Ok(2.0) inner=Ok(PhysicalSize { width: 3200, height: 1644 }) outer_pos=Ok(PhysicalPosition { x: 400, y: 52 })
```
Note `scale=1.0` while the monitor reports `2.0`. Width `1600` → `3200` and `x` `200` → `400`, both exactly doubled. (Height is `1644` rather than `2000` only because it is clamped to the screen.) Three seconds later the window reports `scale=2.0`, but the geometry has already been applied.
On the next clean exit those doubled values are written back to `.window-state.json`, so the growth compounds launch over launch.
### Reproduction
1. On a macOS machine with a Retina (2×) display, use the plugin with default flags.
2. Note the values in `.window-state.json`.
3. Quit and relaunch the app a few times, checking the file each time.
4. When the bug triggers, `width`/`height`/`x`/`y` are double what they were.
### Expected behavior
The window should be restored at the size and position it was saved at, and repeated quit/relaunch cycles should leave `.window-state.json` unchanged.
### Notes on possible fixes
A few directions, roughly in order of how self-contained they are:
- Wait until the window reports a scale factor matching `current_monitor()` before restoring. I tried deferring the restore this way in my own app and it did **not** work — by the time the deferred restore ran, the plugin's own `Moved`/`Resized` handlers had already overwritten the in-memory cache with the startup geometry, so `restore_state` became a no-op. A fix along these lines would need to suppress those handlers until the restore has happened.
- Persist and restore **logical points** instead of physical pixels. This is what I ended up doing in my app, and it sidesteps the problem entirely: restoring logical values needs no scale factor at all, so the stale `scale_factor()` can't corrupt it. It is also arguably the more correct unit on macOS, where all displays share one point coordinate space, whereas physical coordinates are only meaningful relative to a particular display's scale — which is also the root of the mixed-DPI weirdness in #1097 and #1988.
- Re-apply the geometry on the first `ScaleFactorChanged`.
Happy to open a PR if a maintainer indicates which direction is preferred.
### Possibly related
- #3289 — random resize to about half the default width on macOS. Opposite direction, but a scale-factor mismatch would explain that too.
- #1988, #1097 — other mixed-DPI/multi-monitor restore problems that a logical-unit representation would also address.
### Full `tauri info` output
```text
[✔] Environment
- OS: Mac OS 26.5.2 arm64 (X64)
✔ Xcode Command Line Tools: installed
✔ Xcode: 26.5
✔ rustc: 1.96.0 (ac68faa20 2026-05-25)
✔ cargo: 1.96.0 (30a34c682 2026-05-25)
✔ rustup: 1.29.0 (28d1352db 2026-03-05)
✔ Rust toolchain: stable-aarch64-apple-darwin (default)
- node: 26.5.0
- pnpm: 11.7.0
- npm: 11.17.0
[-] Packages
- tauri 🦀: 2.11.3, (outdated, latest: 2.11.5)
- tauri-build 🦀: 2.6.3
- wry 🦀: 0.55.1, (outdated, latest: 0.56.0)
- tao 🦀: 0.35.3, (outdated, latest: 0.36.0)
- @tauri-apps/cli ⱼₛ: 2.11.4
[-] Plugins
- tauri-plugin-window-state 🦀: 2.4.1
- tauri-plugin-opener 🦀: 2.5.4
- tauri-plugin-shell 🦀: 2.3.5
- tauri-plugin-dialog 🦀: 2.7.1
```
Contributor guide
Research direction
Start in the window-state plugin's restore_state and on_window_ready flow, then trace how physical geometry and scale_factor() are used before the monitor is assigned. Compare the startup handlers with the saved-state updates and choose a restoration path that avoids stale scaling. Done means repeated macOS quit and relaunch cycles preserve the saved size and position, including mixed-DPI displays.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, rust
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100