fix(window-state): stop macOS overlay windows changing size on every restore (#3416)

* fix(window-state): convert macOS coordinates to Logical to fix overlay shrink and multi-monitor DPI scaling drift

* chore: add changeset for logical coordinates and macOS overlay fix

* chore(window-state): fix change file package keys

* fix(window-state): save the macOS content rect size instead of guessing the title bar

The 20-40px outer/inner heuristic also matched ordinary decorated windows, saving their outer size so they grew by the title bar height on every restore. Derive the title bar height from the content rect instead, in one helper shared by update_state, the Resized handler and the initial metadata.

* fix(window-state): keep the saved state in physical pixels

State files written by earlier versions hold physical pixels, and logical coordinates are not one global space across monitors with different scale factors, so a position converted with one monitor's scale was restored on another. Converting with the window's scale at Moved time could also pick up a stale scale mid-drag, and an unavailable scale silently stored physical values as logical.

* fix(window-state): check the restored position against the monitors

For a maximized window the monitor check tested x and y, which are the monitor's corner, while the window was then placed at prev_x and prev_y. A window maximized after leaving a now-disconnected monitor passed the check and was restored off-screen.

* test(window-state): cover repeated size and position restores in e2e

Restores each window three times, also with an overlay title bar, and checks the inner and outer size (or position) still match the first save, so drift between saving and restoring accumulates and fails the test.

* chore(window-state): describe the restore fixes in the change file

* chore(deps): update @crabnebula/test-runner-backend to 0.3.1

0.3.0 returns a new-session response that WebdriverIO 10 rejects as not W3C, so every macOS e2e spec failed to create a session.

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Grzegorz KućmierzandLucas Nogueira authored and GitHub committed 2026-10-17 22:13:18 -03:00
1 parent f36ba7ba48
commit 11211c30e7
6 files changed
+230 -128

No files matched your search

+36 -15
View File
@@ -204,24 +204,20 @@ impl<R: Runtime> WindowExt for Window<R> {
}
if flags.contains(StateFlags::POSITION) {
let position = (state.x, state.y).into();
// a maximized window's x and y are the monitor's corner, so it is
// placed where it was before it was maximized
let position = if state.maximized {
PhysicalPosition::new(state.prev_x, state.prev_y)
} else {
PhysicalPosition::new(state.x, state.y)
};
let size = (state.width, state.height).into();
// restore position to saved value if saved monitor exists
// otherwise, let the OS decide where to place the window
for m in self.available_monitors()? {
if m.intersects(position, size) {
self.set_position(PhysicalPosition {
x: if state.maximized {
state.prev_x
} else {
state.x
},
y: if state.maximized {
state.prev_y
} else {
state.y
},
})?;
self.set_position(position)?;
break;
}
}
}
@@ -246,7 +242,7 @@ impl<R: Runtime> WindowExt for Window<R> {
let mut metadata = WindowState::default();
if flags.contains(StateFlags::SIZE) {
let size = self.inner_size()?;
let size = restorable_size(self)?;
metadata.width = size.width;
metadata.height = size.height;
}
@@ -285,6 +281,27 @@ impl<R: Runtime> WindowExt for Window<R> {
}
}
/// The size to save for `window`, so that restoring it with `set_size` recreates the window.
///
/// `set_size` sets the inner size, which on macOS is the window's content rect: the frame minus
/// the title bar. `inner_size` reports the webview's bounds instead, which include the title bar
/// when it is drawn over the content (`TitleBarStyle::Overlay`), so saving it would grow the
/// window by the title bar height on every restore. `inner_position` is the content rect's
/// origin, so its offset from `outer_position` is the title bar height.
fn restorable_size<R: Runtime>(window: &Window<R>) -> tauri::Result<PhysicalSize<u32>> {
#[cfg(target_os = "macos")]
{
let outer = window.outer_size()?;
let title_bar = window.inner_position()?.y - window.outer_position()?.y;
Ok(PhysicalSize::new(
outer.width,
outer.height.saturating_sub(title_bar.max(0) as u32),
))
}
#[cfg(not(target_os = "macos"))]
window.inner_size()
}
trait WindowExtInternal {
fn update_state(&self, state: &mut WindowState, flags: StateFlags) -> tauri::Result<()>;
}
@@ -320,7 +337,7 @@ impl<R: Runtime> WindowExtInternal for Window<R> {
}
if flags.contains(StateFlags::SIZE) && !is_maximized && !is_minimized {
let size = self.inner_size()?;
let size = restorable_size(self)?;
// It doesn't make sense to save a window with 0 height or width
if size.width > 0 && size.height > 0 {
state.width = size.width;
@@ -517,6 +534,10 @@ impl Builder {
if !window_clone.is_minimized().unwrap_or_default() && !is_maximized {
let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) {
// the event carries the inner size, which is not what
// macOS restores (see `restorable_size`)
#[cfg(target_os = "macos")]
let size = &restorable_size(&window_clone).unwrap_or(*size);
state.width = size.width;
state.height = size.height;
}