From f623cb1e91d66e0f3b079e0194ac53a5ac397808 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Tue, 3 Nov 2026 23:07:54 -0800 Subject: [PATCH 01/11] fix(window): restore position before size and never fall back to a sub-minimum window Restore now moves the main window before resizing it, as tauri-plugin-window-state does, so a scale change on the move (Windows DPI change) cannot undo the saved physical size. When the saved monitor is gone, a physical size saved at another scale can fall below minWidth/minHeight at the fallback monitor's scale. The window then opened clamped and re-saved the smaller size on every launch. Use the configured default size in that case. Co-authored-by: Claude Opus 5.5 Signed-off-by: Cheng Peng Co-authored-by: Cursor --- src-tauri/src/window_state.rs | 138 +++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index cc55ee3cd1..fc2a8a20b5 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -1,6 +1,7 @@ use serde::Deserialize; use std::collections::HashMap; -use tauri::{Manager, PhysicalPosition, PhysicalRect, PhysicalSize, Runtime, Window}; +use tauri::utils::config::WindowConfig; +use tauri::{LogicalSize, Manager, PhysicalPosition, PhysicalRect, PhysicalSize, Runtime, Window}; use tauri_plugin_window_state::{AppHandleExt, Builder, StateFlags, WindowExt}; pub(crate) fn builder() -> Builder { @@ -138,8 +139,8 @@ fn restore_geometry_from( .collect(); let fallback = window .primary_monitor()? - .map(|monitor| *monitor.work_area()) - .or_else(|| areas.first().copied()); + .or_else(|| monitors.first().cloned()) + .map(|monitor| (*monitor.work_area(), monitor.scale_factor())); if let Some(fallback) = fallback { // Only the client-to-frame origin offset is needed. Linux's initial // outer_size is not reliable until its first configure event. @@ -151,12 +152,17 @@ fn restore_geometry_from( ); // AppShell owns a 48 logical-pixel header on all desktop platforms. let header_height = (48.0 * window.scale_factor()?).ceil() as u32; - let (position, size) = restore_request(&saved, offset, header_height, &areas, fallback); + let default = WindowConfig::default(); + let config = window.config().app.windows.first().unwrap_or(&default); + let (position, size) = + restore_request(&saved, offset, header_height, &areas, fallback, config); // Validate the requested geometry BEFORE sending it. Native setters // may apply asynchronously (Linux/macOS); getters can still report the - // startup frame. - window.set_size(size)?; + // startup frame. Move first: crossing to a monitor with another scale + // can rescale the window (Windows DPI change), which would undo an + // earlier physical resize. window.set_position(position)?; + window.set_size(size)?; } Ok(()) } @@ -166,7 +172,8 @@ fn restore_request( client_offset: PhysicalPosition, header_height: u32, areas: &[PhysicalRect], - fallback: PhysicalRect, + fallback: (PhysicalRect, f64), + config: &WindowConfig, ) -> (PhysicalPosition, PhysicalSize) { let position = if saved.maximized { PhysicalPosition::new(saved.prev_x, saved.prev_y) @@ -180,7 +187,7 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - let target = reachable_client(client, header_height, areas, fallback).unwrap_or(client); + let target = reachable_client(client, header_height, areas, fallback, config).unwrap_or(client); ( PhysicalPosition::new( target.position.x.saturating_sub(client_offset.x), @@ -194,7 +201,8 @@ fn reachable_client( client: PhysicalRect, header_height: u32, areas: &[PhysicalRect], - fallback: PhysicalRect, + (fallback, fallback_scale): (PhysicalRect, f64), + config: &WindowConfig, ) -> Option> { let x = i64::from(client.position.x); let y = i64::from(client.position.y); @@ -217,9 +225,20 @@ fn reachable_client( } // Unlike any-corner intersection, this keeps the drag strip and both sets // of platform controls on-screen, including after removing an upper monitor. + // The saved size is physical at the missing monitor's scale. If it is below + // the minimum at the fallback's scale, the window would open clamped and + // re-save that smaller size, so use the configured default instead. + let saved = client.size.to_logical::(fallback_scale); + let wanted = if saved.width < config.min_width.unwrap_or(0.0) + || saved.height < config.min_height.unwrap_or(0.0) + { + LogicalSize::new(config.width, config.height).to_physical(fallback_scale) + } else { + client.size + }; let size = PhysicalSize::new( - client.size.width.min(fallback.size.width), - client.size.height.min(fallback.size.height), + wanted.width.min(fallback.size.width), + wanted.height.min(fallback.size.height), ); Some(PhysicalRect { position: PhysicalPosition::new( @@ -260,17 +279,64 @@ mod tests { ); } + // Mirrors the main window in tauri.conf.json. + fn config() -> tauri::utils::config::WindowConfig { + tauri::utils::config::WindowConfig { + width: 1200.0, + height: 800.0, + min_width: Some(480.0), + min_height: Some(400.0), + ..Default::default() + } + } + + #[test] + fn saved_size_on_a_present_monitor_is_kept_whatever_the_fallback_scale() { + let external = rect(1512, 0, 1920, 1080); + let laptop = rect(0, 50, 3024, 1840); + let saved = decode(&saved_state(1600, 100, 602, 470)); + let offset = tauri::PhysicalPosition::new(0, 0); + let areas = [laptop, external]; + assert_eq!( + super::restore_request(&saved, offset, 96, &areas, (laptop, 2.0), &config()), + ( + tauri::PhysicalPosition::new(1600, 100), + tauri::PhysicalSize::new(602, 470) + ), + ); + } + + #[test] + fn fallback_uses_default_size_when_saved_size_is_below_minimum_at_its_scale() { + let laptop = rect(0, 50, 3024, 1840); + let fallback = + |client, area| super::reachable_client(client, 96, &[area], (area, 2.0), &config()); + // Saved on a 1x monitor that is gone: 301x235 points on a 2x laptop. + assert_frame( + fallback(rect(5530, 100, 602, 470), laptop), + Some(rect(312, 50, 2400, 1600)), + ); + // At or above the minimum at the fallback's scale, the saved size stays. + assert_frame( + fallback(rect(5530, 100, 960, 800), laptop), + Some(rect(1032, 50, 960, 800)), + ); + // The default still fits inside a small fallback work area. + let small = rect(0, 25, 1440, 875); + assert_frame(fallback(rect(5530, 100, 602, 470), small), Some(small)); + } + #[test] fn partial_overlap_after_removing_upper_monitor_recovers_entire_header() { let laptop = rect(0, 25, 1440, 875); let saved = rect(100, -450, 960, 640); assert_frame( - super::reachable_client(saved, 48, &[laptop], laptop), + super::reachable_client(saved, 48, &[laptop], (laptop, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); let upper = rect(0, -900, 1440, 900); assert_frame( - super::reachable_client(saved, 48, &[laptop, upper], laptop), + super::reachable_client(saved, 48, &[laptop, upper], (laptop, 1.0), &config()), None, ); } @@ -281,7 +347,7 @@ mod tests { let left = rect(-1920, 0, 1920, 1080); for saved in [rect(100, 120, 960, 640), rect(-1800, 80, 960, 640)] { assert_frame( - super::reachable_client(saved, 48, &[primary, left], primary), + super::reachable_client(saved, 48, &[primary, left], (primary, 1.0), &config()), None, ); } @@ -297,7 +363,7 @@ mod tests { rect(100, 860, 960, 640), ] { assert_frame( - super::reachable_client(saved, 48, &[area], area), + super::reachable_client(saved, 48, &[area], (area, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); } @@ -307,15 +373,27 @@ mod tests { fn oversized_window_fits_remaining_work_area_and_scaled_header_is_checked() { let area = rect(0, 50, 1440, 850); assert_frame( - super::reachable_client(rect(0, 50, 2800, 1800), 96, &[area], area), + super::reachable_client(rect(0, 50, 2800, 1800), 96, &[area], (area, 1.0), &config()), Some(area), ); assert_frame( - super::reachable_client(rect(100, 820, 960, 640), 48, &[area], area), + super::reachable_client( + rect(100, 820, 960, 640), + 48, + &[area], + (area, 1.0), + &config(), + ), None, ); assert_frame( - super::reachable_client(rect(100, 820, 960, 640), 96, &[area], area), + super::reachable_client( + rect(100, 820, 960, 640), + 96, + &[area], + (area, 1.0), + &config(), + ), Some(rect(240, 50, 960, 640)), ); } @@ -330,7 +408,8 @@ mod tests { tauri::PhysicalPosition::new(7, 0), 48, &[area], - area + (area, 1.0), + &config() ), (tauri::PhysicalPosition::new(-7, 0), area.size), ); @@ -342,12 +421,12 @@ mod tests { let right = rect(0, 0, 1920, 1080); let spanning = rect(-480, 100, 960, 640); assert_frame( - super::reachable_client(spanning, 48, &[left, right], right), + super::reachable_client(spanning, 48, &[left, right], (right, 1.0), &config()), None, ); let below = rect(0, 300, 1920, 1080); assert_frame( - super::reachable_client(spanning, 48, &[left, below], left), + super::reachable_client(spanning, 48, &[left, below], (left, 1.0), &config()), Some(rect(-1440, 0, 960, 640)), ); } @@ -371,7 +450,10 @@ mod tests { let completed = rect(100, -450, 960, 640); let saved = decode(&saved_state(100, -450, 960, 640)); // The old post-set guard approves this stale, reachable startup frame. - assert_frame(super::reachable_client(startup, 48, &[area], area), None); + assert_frame( + super::reachable_client(startup, 48, &[area], (area, 1.0), &config()), + None, + ); // The planner accepts no current bounds: they can be either startup or // completed bounds. Only the frame-to-client offset (zero for this // undecorated window) is used. The unsafe origin is never sent to GTK. @@ -380,19 +462,19 @@ mod tests { tauri::PhysicalSize::new(960, 640), ); assert_frame( - super::reachable_client(completed, 48, &[area], area), + super::reachable_client(completed, 48, &[area], (area, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); let offset = tauri::PhysicalPosition::new(0, 0); assert_eq!( - super::restore_request(&saved, offset, 48, &[area], area), + super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), expected ); // Resize is asynchronous too: validating only startup width misses // the right-hand controls of a wider saved client. let saved = decode(&saved_state(100, 120, 1800, 800)); assert_eq!( - super::restore_request(&saved, offset, 48, &[area], area), + super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), (area.position, tauri::PhysicalSize::new(1440, 800)), ); } @@ -403,7 +485,7 @@ mod tests { let offset = tauri::PhysicalPosition::new(7, 0); let saved = decode(&saved_state(-7, 0, 1920, 1000)); assert_eq!( - super::restore_request(&saved, offset, 48, &[area], area), + super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(-7, 0), tauri::PhysicalSize::new(1920, 1000) @@ -414,7 +496,7 @@ mod tests { state["main"]["prev_x"] = json!(100); state["main"]["prev_y"] = json!(-450); assert_eq!( - super::restore_request(&decode(&state), offset, 48, &[area], area), + super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(473, 0), tauri::PhysicalSize::new(960, 640) @@ -424,7 +506,7 @@ mod tests { state["main"]["maximized"] = json!(false); state["main"]["fullscreen"] = json!(true); assert_eq!( - super::restore_request(&decode(&state), offset, 48, &[area], area), + super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(0, 0), tauri::PhysicalSize::new(960, 640) From 3037146622e6baeb82a66ca37da5d7c975c3bd08 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Tue, 3 Nov 2026 23:53:31 -0800 Subject: [PATCH 02/11] fix(window): recover a sub-minimum size on a lone monitor and send relocated geometry in its logical units A rect saved on a 1x external to the right of a 2x laptop can still fall inside the laptop's physical rect after the external is unplugged, so it counted as reachable and skipped the minimum check. With a single monitor its scale is known, so apply the check there too. macOS and GTK setters convert physical values with the window's current scale, so a relocated window planned at the fallback monitor's scale is now sent in that monitor's logical units. Windows keeps physical values. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 85 +++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index fc2a8a20b5..9ff5cca54f 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -154,13 +154,21 @@ fn restore_geometry_from( let header_height = (48.0 * window.scale_factor()?).ceil() as u32; let default = WindowConfig::default(); let config = window.config().app.windows.first().unwrap_or(&default); - let (position, size) = + let (position, size, relocated) = restore_request(&saved, offset, header_height, &areas, fallback, config); // Validate the requested geometry BEFORE sending it. Native setters // may apply asynchronously (Linux/macOS); getters can still report the - // startup frame. Move first: crossing to a monitor with another scale - // can rescale the window (Windows DPI change), which would undo an - // earlier physical resize. + // startup frame. macOS and GTK convert physical values at the startup + // scale, so a relocated window is sent in its monitor's logical units. + let (position, size): (tauri::Position, tauri::Size) = + match relocated.filter(|_| cfg!(not(windows))) { + Some(scale) => ( + position.to_logical::(scale).into(), + size.to_logical::(scale).into(), + ), + None => (position.into(), size.into()), + }; + // Move first: a Windows DPI change rescales the window. window.set_position(position)?; window.set_size(size)?; } @@ -174,7 +182,7 @@ fn restore_request( areas: &[PhysicalRect], fallback: (PhysicalRect, f64), config: &WindowConfig, -) -> (PhysicalPosition, PhysicalSize) { +) -> (PhysicalPosition, PhysicalSize, Option) { let position = if saved.maximized { PhysicalPosition::new(saved.prev_x, saved.prev_y) } else { @@ -187,13 +195,15 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - let target = reachable_client(client, header_height, areas, fallback, config).unwrap_or(client); + let relocated = reachable_client(client, header_height, areas, fallback, config); + let target = relocated.unwrap_or(client); ( PhysicalPosition::new( target.position.x.saturating_sub(client_offset.x), target.position.y.saturating_sub(client_offset.y), ), target.size, + relocated.map(|_| fallback.1), ) } @@ -220,18 +230,18 @@ fn reachable_client( && y < i64::from(area.position.y) + i64::from(area.size.height) }) }); - if reachable { + // A saved size below the minimum at the fallback's scale was saved at a + // lower scale. It would open clamped and re-save that smaller size. With a + // single monitor its scale is known even when the rect is still reachable. + let saved = client.size.to_logical::(fallback_scale); + let too_small = saved.width < config.min_width.unwrap_or(0.0) + || saved.height < config.min_height.unwrap_or(0.0); + if reachable && !(too_small && areas.len() == 1) { return None; } // Unlike any-corner intersection, this keeps the drag strip and both sets // of platform controls on-screen, including after removing an upper monitor. - // The saved size is physical at the missing monitor's scale. If it is below - // the minimum at the fallback's scale, the window would open clamped and - // re-save that smaller size, so use the configured default instead. - let saved = client.size.to_logical::(fallback_scale); - let wanted = if saved.width < config.min_width.unwrap_or(0.0) - || saved.height < config.min_height.unwrap_or(0.0) - { + let wanted = if too_small { LogicalSize::new(config.width, config.height).to_physical(fallback_scale) } else { client.size @@ -291,17 +301,33 @@ mod tests { } #[test] - fn saved_size_on_a_present_monitor_is_kept_whatever_the_fallback_scale() { + fn saved_size_is_kept_with_monitors_attached_and_recovered_on_a_lone_monitor() { let external = rect(1512, 0, 1920, 1080); let laptop = rect(0, 50, 3024, 1840); let saved = decode(&saved_state(1600, 100, 602, 470)); let offset = tauri::PhysicalPosition::new(0, 0); - let areas = [laptop, external]; + let request = |areas: &[tauri::PhysicalRect]| { + super::restore_request(&saved, offset, 96, areas, (laptop, 2.0), &config()) + }; + // The 1x external overlaps the 2x laptop's physical rect, so the scale + // this rect was saved at is ambiguous. Keep it as saved. assert_eq!( - super::restore_request(&saved, offset, 96, &areas, (laptop, 2.0), &config()), + request(&[laptop, external]), ( tauri::PhysicalPosition::new(1600, 100), - tauri::PhysicalSize::new(602, 470) + tauri::PhysicalSize::new(602, 470), + None + ), + ); + // Unplugged: the laptop's physical rect still contains the saved rect, + // but 301x235 points is below the minimum at its known scale. Send the + // default in the laptop's logical units. + assert_eq!( + request(&[laptop]), + ( + tauri::PhysicalPosition::new(312, 50), + tauri::PhysicalSize::new(2400, 1600), + Some(2.0) ), ); } @@ -316,6 +342,11 @@ mod tests { fallback(rect(5530, 100, 602, 470), laptop), Some(rect(312, 50, 2400, 1600)), ); + // One axis below the minimum is enough. + assert_frame( + fallback(rect(5530, 100, 960, 799), laptop), + Some(rect(312, 50, 2400, 1600)), + ); // At or above the minimum at the fallback's scale, the saved size stays. assert_frame( fallback(rect(5530, 100, 960, 800), laptop), @@ -411,7 +442,7 @@ mod tests { (area, 1.0), &config() ), - (tauri::PhysicalPosition::new(-7, 0), area.size), + (tauri::PhysicalPosition::new(-7, 0), area.size, None), ); } @@ -460,6 +491,7 @@ mod tests { let expected = ( tauri::PhysicalPosition::new(240, 25), tauri::PhysicalSize::new(960, 640), + Some(1.0), ); assert_frame( super::reachable_client(completed, 48, &[area], (area, 1.0), &config()), @@ -475,7 +507,11 @@ mod tests { let saved = decode(&saved_state(100, 120, 1800, 800)); assert_eq!( super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), - (area.position, tauri::PhysicalSize::new(1440, 800)), + ( + area.position, + tauri::PhysicalSize::new(1440, 800), + Some(1.0) + ), ); } @@ -488,7 +524,8 @@ mod tests { super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(-7, 0), - tauri::PhysicalSize::new(1920, 1000) + tauri::PhysicalSize::new(1920, 1000), + None ), ); let mut state = saved_state(0, 0, 960, 640); @@ -499,7 +536,8 @@ mod tests { super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(473, 0), - tauri::PhysicalSize::new(960, 640) + tauri::PhysicalSize::new(960, 640), + Some(1.0) ), ); // Fullscreen uses x/y, not prev_x/prev_y, matching the plugin format. @@ -509,7 +547,8 @@ mod tests { super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(0, 0), - tauri::PhysicalSize::new(960, 640) + tauri::PhysicalSize::new(960, 640), + None ), ); } From 7de4895d195555b443cf4dec8816b62813bac55e Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 00:03:29 -0800 Subject: [PATCH 03/11] fix(window): count a monitor only if the saved size fits its scale Replace the single-monitor special case with one rule: a monitor only holds the saved rect if the saved size meets the minimum at that monitor's scale. A smaller size was saved at a lower scale, so with two 2x monitors left after unplugging a 1x external the window is now relocated with the default size instead of opening clamped. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 148 ++++++++++++++++++++++------------ 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index 9ff5cca54f..a0cb9bf5d2 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -135,12 +135,12 @@ fn restore_geometry_from( let monitors = window.available_monitors()?; let areas: Vec<_> = monitors .iter() - .map(|monitor| *monitor.work_area()) + .map(|monitor| (*monitor.work_area(), monitor.scale_factor())) .collect(); let fallback = window .primary_monitor()? - .or_else(|| monitors.first().cloned()) - .map(|monitor| (*monitor.work_area(), monitor.scale_factor())); + .map(|monitor| (*monitor.work_area(), monitor.scale_factor())) + .or_else(|| areas.first().copied()); if let Some(fallback) = fallback { // Only the client-to-frame origin offset is needed. Linux's initial // outer_size is not reliable until its first configure event. @@ -179,7 +179,7 @@ fn restore_request( saved: &SavedGeometry, client_offset: PhysicalPosition, header_height: u32, - areas: &[PhysicalRect], + areas: &[(PhysicalRect, f64)], fallback: (PhysicalRect, f64), config: &WindowConfig, ) -> (PhysicalPosition, PhysicalSize, Option) { @@ -210,7 +210,7 @@ fn restore_request( fn reachable_client( client: PhysicalRect, header_height: u32, - areas: &[PhysicalRect], + areas: &[(PhysicalRect, f64)], (fallback, fallback_scale): (PhysicalRect, f64), config: &WindowConfig, ) -> Option> { @@ -218,33 +218,35 @@ fn reachable_client( let y = i64::from(client.position.y); let right = x + i64::from(client.size.width.saturating_sub(1)); let bottom = y + i64::from(header_height.saturating_sub(1)); + // A size below the minimum at a monitor's scale was saved at a lower scale + // and would open clamped there, then re-save that smaller size. + let fits = |scale: f64| { + let size = client.size.to_logical::(scale); + size.width >= config.min_width.unwrap_or(0.0) + && size.height >= config.min_height.unwrap_or(0.0) + }; // A header may span adjacent monitors. Both control regions must remain // reachable, but need not belong to the same monitor. let reachable = [(x, y), (right, y), (x, bottom), (right, bottom)] .into_iter() .all(|(x, y)| { - areas.iter().any(|area| { - x >= i64::from(area.position.x) + areas.iter().any(|(area, scale)| { + fits(*scale) + && x >= i64::from(area.position.x) && y >= i64::from(area.position.y) && x < i64::from(area.position.x) + i64::from(area.size.width) && y < i64::from(area.position.y) + i64::from(area.size.height) }) }); - // A saved size below the minimum at the fallback's scale was saved at a - // lower scale. It would open clamped and re-save that smaller size. With a - // single monitor its scale is known even when the rect is still reachable. - let saved = client.size.to_logical::(fallback_scale); - let too_small = saved.width < config.min_width.unwrap_or(0.0) - || saved.height < config.min_height.unwrap_or(0.0); - if reachable && !(too_small && areas.len() == 1) { + if reachable { return None; } // Unlike any-corner intersection, this keeps the drag strip and both sets // of platform controls on-screen, including after removing an upper monitor. - let wanted = if too_small { - LogicalSize::new(config.width, config.height).to_physical(fallback_scale) - } else { + let wanted = if fits(fallback_scale) { client.size + } else { + LogicalSize::new(config.width, config.height).to_physical(fallback_scale) }; let size = PhysicalSize::new( wanted.width.min(fallback.size.width), @@ -301,18 +303,18 @@ mod tests { } #[test] - fn saved_size_is_kept_with_monitors_attached_and_recovered_on_a_lone_monitor() { + fn saved_size_is_kept_only_on_a_monitor_whose_scale_fits_it() { let external = rect(1512, 0, 1920, 1080); let laptop = rect(0, 50, 3024, 1840); let saved = decode(&saved_state(1600, 100, 602, 470)); let offset = tauri::PhysicalPosition::new(0, 0); - let request = |areas: &[tauri::PhysicalRect]| { + let request = |areas: &[(tauri::PhysicalRect, f64)]| { super::restore_request(&saved, offset, 96, areas, (laptop, 2.0), &config()) }; - // The 1x external overlaps the 2x laptop's physical rect, so the scale - // this rect was saved at is ambiguous. Keep it as saved. + // The 1x external overlaps the 2x laptop's physical rect. The saved size + // only fits the external's scale, which still holds the rect: keep it. assert_eq!( - request(&[laptop, external]), + request(&[(laptop, 2.0), (external, 1.0)]), ( tauri::PhysicalPosition::new(1600, 100), tauri::PhysicalSize::new(602, 470), @@ -320,23 +322,25 @@ mod tests { ), ); // Unplugged: the laptop's physical rect still contains the saved rect, - // but 301x235 points is below the minimum at its known scale. Send the - // default in the laptop's logical units. - assert_eq!( - request(&[laptop]), - ( - tauri::PhysicalPosition::new(312, 50), - tauri::PhysicalSize::new(2400, 1600), - Some(2.0) - ), + // but 301x235 points is below the minimum at its scale. Plan the + // default on the laptop and report its scale for the setters. + let relocated = ( + tauri::PhysicalPosition::new(312, 50), + tauri::PhysicalSize::new(2400, 1600), + Some(2.0), ); + assert_eq!(request(&[(laptop, 2.0)]), relocated); + // Another 2x monitor does not change that. + let left = rect(-3024, 0, 3024, 1964); + assert_eq!(request(&[(laptop, 2.0), (left, 2.0)]), relocated); } #[test] fn fallback_uses_default_size_when_saved_size_is_below_minimum_at_its_scale() { let laptop = rect(0, 50, 3024, 1840); - let fallback = - |client, area| super::reachable_client(client, 96, &[area], (area, 2.0), &config()); + let fallback = |client, area| { + super::reachable_client(client, 96, &[(area, 2.0)], (area, 2.0), &config()) + }; // Saved on a 1x monitor that is gone: 301x235 points on a 2x laptop. assert_frame( fallback(rect(5530, 100, 602, 470), laptop), @@ -362,12 +366,18 @@ mod tests { let laptop = rect(0, 25, 1440, 875); let saved = rect(100, -450, 960, 640); assert_frame( - super::reachable_client(saved, 48, &[laptop], (laptop, 1.0), &config()), + super::reachable_client(saved, 48, &[(laptop, 1.0)], (laptop, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); let upper = rect(0, -900, 1440, 900); assert_frame( - super::reachable_client(saved, 48, &[laptop, upper], (laptop, 1.0), &config()), + super::reachable_client( + saved, + 48, + &[(laptop, 1.0), (upper, 1.0)], + (laptop, 1.0), + &config(), + ), None, ); } @@ -378,7 +388,13 @@ mod tests { let left = rect(-1920, 0, 1920, 1080); for saved in [rect(100, 120, 960, 640), rect(-1800, 80, 960, 640)] { assert_frame( - super::reachable_client(saved, 48, &[primary, left], (primary, 1.0), &config()), + super::reachable_client( + saved, + 48, + &[(primary, 1.0), (left, 1.0)], + (primary, 1.0), + &config(), + ), None, ); } @@ -394,7 +410,7 @@ mod tests { rect(100, 860, 960, 640), ] { assert_frame( - super::reachable_client(saved, 48, &[area], (area, 1.0), &config()), + super::reachable_client(saved, 48, &[(area, 1.0)], (area, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); } @@ -404,14 +420,20 @@ mod tests { fn oversized_window_fits_remaining_work_area_and_scaled_header_is_checked() { let area = rect(0, 50, 1440, 850); assert_frame( - super::reachable_client(rect(0, 50, 2800, 1800), 96, &[area], (area, 1.0), &config()), + super::reachable_client( + rect(0, 50, 2800, 1800), + 96, + &[(area, 1.0)], + (area, 1.0), + &config(), + ), Some(area), ); assert_frame( super::reachable_client( rect(100, 820, 960, 640), 48, - &[area], + &[(area, 1.0)], (area, 1.0), &config(), ), @@ -421,7 +443,7 @@ mod tests { super::reachable_client( rect(100, 820, 960, 640), 96, - &[area], + &[(area, 1.0)], (area, 1.0), &config(), ), @@ -438,7 +460,7 @@ mod tests { &saved, tauri::PhysicalPosition::new(7, 0), 48, - &[area], + &[(area, 1.0)], (area, 1.0), &config() ), @@ -452,12 +474,24 @@ mod tests { let right = rect(0, 0, 1920, 1080); let spanning = rect(-480, 100, 960, 640); assert_frame( - super::reachable_client(spanning, 48, &[left, right], (right, 1.0), &config()), + super::reachable_client( + spanning, + 48, + &[(left, 1.0), (right, 1.0)], + (right, 1.0), + &config(), + ), None, ); let below = rect(0, 300, 1920, 1080); assert_frame( - super::reachable_client(spanning, 48, &[left, below], (left, 1.0), &config()), + super::reachable_client( + spanning, + 48, + &[(left, 1.0), (below, 1.0)], + (left, 1.0), + &config(), + ), Some(rect(-1440, 0, 960, 640)), ); } @@ -482,7 +516,7 @@ mod tests { let saved = decode(&saved_state(100, -450, 960, 640)); // The old post-set guard approves this stale, reachable startup frame. assert_frame( - super::reachable_client(startup, 48, &[area], (area, 1.0), &config()), + super::reachable_client(startup, 48, &[(area, 1.0)], (area, 1.0), &config()), None, ); // The planner accepts no current bounds: they can be either startup or @@ -494,19 +528,19 @@ mod tests { Some(1.0), ); assert_frame( - super::reachable_client(completed, 48, &[area], (area, 1.0), &config()), + super::reachable_client(completed, 48, &[(area, 1.0)], (area, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); let offset = tauri::PhysicalPosition::new(0, 0); assert_eq!( - super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), + super::restore_request(&saved, offset, 48, &[(area, 1.0)], (area, 1.0), &config()), expected ); // Resize is asynchronous too: validating only startup width misses // the right-hand controls of a wider saved client. let saved = decode(&saved_state(100, 120, 1800, 800)); assert_eq!( - super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), + super::restore_request(&saved, offset, 48, &[(area, 1.0)], (area, 1.0), &config()), ( area.position, tauri::PhysicalSize::new(1440, 800), @@ -521,7 +555,7 @@ mod tests { let offset = tauri::PhysicalPosition::new(7, 0); let saved = decode(&saved_state(-7, 0, 1920, 1000)); assert_eq!( - super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), + super::restore_request(&saved, offset, 48, &[(area, 1.0)], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(-7, 0), tauri::PhysicalSize::new(1920, 1000), @@ -533,7 +567,14 @@ mod tests { state["main"]["prev_x"] = json!(100); state["main"]["prev_y"] = json!(-450); assert_eq!( - super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), + super::restore_request( + &decode(&state), + offset, + 48, + &[(area, 1.0)], + (area, 1.0), + &config() + ), ( tauri::PhysicalPosition::new(473, 0), tauri::PhysicalSize::new(960, 640), @@ -544,7 +585,14 @@ mod tests { state["main"]["maximized"] = json!(false); state["main"]["fullscreen"] = json!(true); assert_eq!( - super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), + super::restore_request( + &decode(&state), + offset, + 48, + &[(area, 1.0)], + (area, 1.0), + &config() + ), ( tauri::PhysicalPosition::new(0, 0), tauri::PhysicalSize::new(960, 640), From 7e2393e72cba77d0ef8320cdeb681c064dfc5bbf Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 00:15:36 -0800 Subject: [PATCH 04/11] fix(window): keep mixed-scale spanning headers and match tao's minimum rounding Keep header visibility geometric again and check the minimum separately: at least one monitor under the header must have a scale at which the saved size fits. A 1x window spanning onto a 2x laptop stays where it was. Compare against the minimum converted to pixels with tao's rounding, so a minimum-size window under a custom Windows scale is not reset. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 67 +++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index a0cb9bf5d2..a4a8e436a5 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -218,27 +218,33 @@ fn reachable_client( let y = i64::from(client.position.y); let right = x + i64::from(client.size.width.saturating_sub(1)); let bottom = y + i64::from(header_height.saturating_sub(1)); - // A size below the minimum at a monitor's scale was saved at a lower scale - // and would open clamped there, then re-save that smaller size. - let fits = |scale: f64| { - let size = client.size.to_logical::(scale); - size.width >= config.min_width.unwrap_or(0.0) - && size.height >= config.min_height.unwrap_or(0.0) + let corners = [(x, y), (right, y), (x, bottom), (right, bottom)]; + let inside = |area: &PhysicalRect, (x, y): (i64, i64)| { + x >= i64::from(area.position.x) + && y >= i64::from(area.position.y) + && x < i64::from(area.position.x) + i64::from(area.size.width) + && y < i64::from(area.position.y) + i64::from(area.size.height) }; // A header may span adjacent monitors. Both control regions must remain // reachable, but need not belong to the same monitor. - let reachable = [(x, y), (right, y), (x, bottom), (right, bottom)] - .into_iter() - .all(|(x, y)| { - areas.iter().any(|(area, scale)| { - fits(*scale) - && x >= i64::from(area.position.x) - && y >= i64::from(area.position.y) - && x < i64::from(area.position.x) + i64::from(area.size.width) - && y < i64::from(area.position.y) + i64::from(area.size.height) - }) - }); - if reachable { + let reachable = corners + .iter() + .all(|&corner| areas.iter().any(|(area, _)| inside(area, corner))); + // A size below the minimum (rounded as tao rounds it) at the scale of every + // monitor under the header was saved at a lower scale. It would open + // clamped and re-save that smaller size. + let min = LogicalSize::new( + config.min_width.unwrap_or(0.0), + config.min_height.unwrap_or(0.0), + ); + let fits = |scale: f64| { + let min = min.to_physical::(scale); + client.size.width >= min.width && client.size.height >= min.height + }; + let fitted = areas + .iter() + .any(|(area, scale)| fits(*scale) && corners.iter().any(|&corner| inside(area, corner))); + if reachable && fitted { return None; } // Unlike any-corner intersection, this keeps the drag strip and both sets @@ -494,6 +500,31 @@ mod tests { ), Some(rect(-1440, 0, 960, 640)), ); + // A 1x window may span onto a 2x laptop where its size is below the + // minimum. The 1x monitor under the header holds it. + let laptop = rect(0, 50, 3024, 1840); + assert_frame( + super::reachable_client( + rect(-500, 100, 602, 470), + 48, + &[(left, 1.0), (laptop, 2.0)], + (laptop, 2.0), + &config(), + ), + None, + ); + } + + #[test] + fn minimum_size_is_compared_with_the_rounding_tao_applies() { + // At 110 DPI the 480x400 minimum is 550x458.33, and tao enforces 550x458. + let scale = 110.0 / 96.0; + let area = rect(0, 0, 2200, 1200); + let keep = |size: tauri::PhysicalRect| { + super::reachable_client(size, 55, &[(area, scale)], (area, scale), &config()) + }; + assert_frame(keep(rect(100, 100, 550, 458)), None); + assert!(keep(rect(100, 100, 550, 457)).is_some()); } fn saved_state(x: i32, y: i32, width: u32, height: u32) -> Value { From 3dbdecb825512abad8ada9c4f81239ff1f1023b8 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 08:42:42 -0800 Subject: [PATCH 05/11] Revert "fix(window): keep mixed-scale spanning headers and match tao's minimum rounding" This reverts commit 7e2393e72cba77d0ef8320cdeb681c064dfc5bbf. Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 67 ++++++++++------------------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index a4a8e436a5..a0cb9bf5d2 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -218,33 +218,27 @@ fn reachable_client( let y = i64::from(client.position.y); let right = x + i64::from(client.size.width.saturating_sub(1)); let bottom = y + i64::from(header_height.saturating_sub(1)); - let corners = [(x, y), (right, y), (x, bottom), (right, bottom)]; - let inside = |area: &PhysicalRect, (x, y): (i64, i64)| { - x >= i64::from(area.position.x) - && y >= i64::from(area.position.y) - && x < i64::from(area.position.x) + i64::from(area.size.width) - && y < i64::from(area.position.y) + i64::from(area.size.height) + // A size below the minimum at a monitor's scale was saved at a lower scale + // and would open clamped there, then re-save that smaller size. + let fits = |scale: f64| { + let size = client.size.to_logical::(scale); + size.width >= config.min_width.unwrap_or(0.0) + && size.height >= config.min_height.unwrap_or(0.0) }; // A header may span adjacent monitors. Both control regions must remain // reachable, but need not belong to the same monitor. - let reachable = corners - .iter() - .all(|&corner| areas.iter().any(|(area, _)| inside(area, corner))); - // A size below the minimum (rounded as tao rounds it) at the scale of every - // monitor under the header was saved at a lower scale. It would open - // clamped and re-save that smaller size. - let min = LogicalSize::new( - config.min_width.unwrap_or(0.0), - config.min_height.unwrap_or(0.0), - ); - let fits = |scale: f64| { - let min = min.to_physical::(scale); - client.size.width >= min.width && client.size.height >= min.height - }; - let fitted = areas - .iter() - .any(|(area, scale)| fits(*scale) && corners.iter().any(|&corner| inside(area, corner))); - if reachable && fitted { + let reachable = [(x, y), (right, y), (x, bottom), (right, bottom)] + .into_iter() + .all(|(x, y)| { + areas.iter().any(|(area, scale)| { + fits(*scale) + && x >= i64::from(area.position.x) + && y >= i64::from(area.position.y) + && x < i64::from(area.position.x) + i64::from(area.size.width) + && y < i64::from(area.position.y) + i64::from(area.size.height) + }) + }); + if reachable { return None; } // Unlike any-corner intersection, this keeps the drag strip and both sets @@ -500,31 +494,6 @@ mod tests { ), Some(rect(-1440, 0, 960, 640)), ); - // A 1x window may span onto a 2x laptop where its size is below the - // minimum. The 1x monitor under the header holds it. - let laptop = rect(0, 50, 3024, 1840); - assert_frame( - super::reachable_client( - rect(-500, 100, 602, 470), - 48, - &[(left, 1.0), (laptop, 2.0)], - (laptop, 2.0), - &config(), - ), - None, - ); - } - - #[test] - fn minimum_size_is_compared_with_the_rounding_tao_applies() { - // At 110 DPI the 480x400 minimum is 550x458.33, and tao enforces 550x458. - let scale = 110.0 / 96.0; - let area = rect(0, 0, 2200, 1200); - let keep = |size: tauri::PhysicalRect| { - super::reachable_client(size, 55, &[(area, scale)], (area, scale), &config()) - }; - assert_frame(keep(rect(100, 100, 550, 458)), None); - assert!(keep(rect(100, 100, 550, 457)).is_some()); } fn saved_state(x: i32, y: i32, width: u32, height: u32) -> Value { From bce3da6927c7781aaa853ba56e1b829de4526850 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 08:42:42 -0800 Subject: [PATCH 06/11] Revert "fix(window): count a monitor only if the saved size fits its scale" This reverts commit 7de4895d195555b443cf4dec8816b62813bac55e. Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 148 ++++++++++++---------------------- 1 file changed, 50 insertions(+), 98 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index a0cb9bf5d2..9ff5cca54f 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -135,12 +135,12 @@ fn restore_geometry_from( let monitors = window.available_monitors()?; let areas: Vec<_> = monitors .iter() - .map(|monitor| (*monitor.work_area(), monitor.scale_factor())) + .map(|monitor| *monitor.work_area()) .collect(); let fallback = window .primary_monitor()? - .map(|monitor| (*monitor.work_area(), monitor.scale_factor())) - .or_else(|| areas.first().copied()); + .or_else(|| monitors.first().cloned()) + .map(|monitor| (*monitor.work_area(), monitor.scale_factor())); if let Some(fallback) = fallback { // Only the client-to-frame origin offset is needed. Linux's initial // outer_size is not reliable until its first configure event. @@ -179,7 +179,7 @@ fn restore_request( saved: &SavedGeometry, client_offset: PhysicalPosition, header_height: u32, - areas: &[(PhysicalRect, f64)], + areas: &[PhysicalRect], fallback: (PhysicalRect, f64), config: &WindowConfig, ) -> (PhysicalPosition, PhysicalSize, Option) { @@ -210,7 +210,7 @@ fn restore_request( fn reachable_client( client: PhysicalRect, header_height: u32, - areas: &[(PhysicalRect, f64)], + areas: &[PhysicalRect], (fallback, fallback_scale): (PhysicalRect, f64), config: &WindowConfig, ) -> Option> { @@ -218,35 +218,33 @@ fn reachable_client( let y = i64::from(client.position.y); let right = x + i64::from(client.size.width.saturating_sub(1)); let bottom = y + i64::from(header_height.saturating_sub(1)); - // A size below the minimum at a monitor's scale was saved at a lower scale - // and would open clamped there, then re-save that smaller size. - let fits = |scale: f64| { - let size = client.size.to_logical::(scale); - size.width >= config.min_width.unwrap_or(0.0) - && size.height >= config.min_height.unwrap_or(0.0) - }; // A header may span adjacent monitors. Both control regions must remain // reachable, but need not belong to the same monitor. let reachable = [(x, y), (right, y), (x, bottom), (right, bottom)] .into_iter() .all(|(x, y)| { - areas.iter().any(|(area, scale)| { - fits(*scale) - && x >= i64::from(area.position.x) + areas.iter().any(|area| { + x >= i64::from(area.position.x) && y >= i64::from(area.position.y) && x < i64::from(area.position.x) + i64::from(area.size.width) && y < i64::from(area.position.y) + i64::from(area.size.height) }) }); - if reachable { + // A saved size below the minimum at the fallback's scale was saved at a + // lower scale. It would open clamped and re-save that smaller size. With a + // single monitor its scale is known even when the rect is still reachable. + let saved = client.size.to_logical::(fallback_scale); + let too_small = saved.width < config.min_width.unwrap_or(0.0) + || saved.height < config.min_height.unwrap_or(0.0); + if reachable && !(too_small && areas.len() == 1) { return None; } // Unlike any-corner intersection, this keeps the drag strip and both sets // of platform controls on-screen, including after removing an upper monitor. - let wanted = if fits(fallback_scale) { - client.size - } else { + let wanted = if too_small { LogicalSize::new(config.width, config.height).to_physical(fallback_scale) + } else { + client.size }; let size = PhysicalSize::new( wanted.width.min(fallback.size.width), @@ -303,18 +301,18 @@ mod tests { } #[test] - fn saved_size_is_kept_only_on_a_monitor_whose_scale_fits_it() { + fn saved_size_is_kept_with_monitors_attached_and_recovered_on_a_lone_monitor() { let external = rect(1512, 0, 1920, 1080); let laptop = rect(0, 50, 3024, 1840); let saved = decode(&saved_state(1600, 100, 602, 470)); let offset = tauri::PhysicalPosition::new(0, 0); - let request = |areas: &[(tauri::PhysicalRect, f64)]| { + let request = |areas: &[tauri::PhysicalRect]| { super::restore_request(&saved, offset, 96, areas, (laptop, 2.0), &config()) }; - // The 1x external overlaps the 2x laptop's physical rect. The saved size - // only fits the external's scale, which still holds the rect: keep it. + // The 1x external overlaps the 2x laptop's physical rect, so the scale + // this rect was saved at is ambiguous. Keep it as saved. assert_eq!( - request(&[(laptop, 2.0), (external, 1.0)]), + request(&[laptop, external]), ( tauri::PhysicalPosition::new(1600, 100), tauri::PhysicalSize::new(602, 470), @@ -322,25 +320,23 @@ mod tests { ), ); // Unplugged: the laptop's physical rect still contains the saved rect, - // but 301x235 points is below the minimum at its scale. Plan the - // default on the laptop and report its scale for the setters. - let relocated = ( - tauri::PhysicalPosition::new(312, 50), - tauri::PhysicalSize::new(2400, 1600), - Some(2.0), + // but 301x235 points is below the minimum at its known scale. Send the + // default in the laptop's logical units. + assert_eq!( + request(&[laptop]), + ( + tauri::PhysicalPosition::new(312, 50), + tauri::PhysicalSize::new(2400, 1600), + Some(2.0) + ), ); - assert_eq!(request(&[(laptop, 2.0)]), relocated); - // Another 2x monitor does not change that. - let left = rect(-3024, 0, 3024, 1964); - assert_eq!(request(&[(laptop, 2.0), (left, 2.0)]), relocated); } #[test] fn fallback_uses_default_size_when_saved_size_is_below_minimum_at_its_scale() { let laptop = rect(0, 50, 3024, 1840); - let fallback = |client, area| { - super::reachable_client(client, 96, &[(area, 2.0)], (area, 2.0), &config()) - }; + let fallback = + |client, area| super::reachable_client(client, 96, &[area], (area, 2.0), &config()); // Saved on a 1x monitor that is gone: 301x235 points on a 2x laptop. assert_frame( fallback(rect(5530, 100, 602, 470), laptop), @@ -366,18 +362,12 @@ mod tests { let laptop = rect(0, 25, 1440, 875); let saved = rect(100, -450, 960, 640); assert_frame( - super::reachable_client(saved, 48, &[(laptop, 1.0)], (laptop, 1.0), &config()), + super::reachable_client(saved, 48, &[laptop], (laptop, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); let upper = rect(0, -900, 1440, 900); assert_frame( - super::reachable_client( - saved, - 48, - &[(laptop, 1.0), (upper, 1.0)], - (laptop, 1.0), - &config(), - ), + super::reachable_client(saved, 48, &[laptop, upper], (laptop, 1.0), &config()), None, ); } @@ -388,13 +378,7 @@ mod tests { let left = rect(-1920, 0, 1920, 1080); for saved in [rect(100, 120, 960, 640), rect(-1800, 80, 960, 640)] { assert_frame( - super::reachable_client( - saved, - 48, - &[(primary, 1.0), (left, 1.0)], - (primary, 1.0), - &config(), - ), + super::reachable_client(saved, 48, &[primary, left], (primary, 1.0), &config()), None, ); } @@ -410,7 +394,7 @@ mod tests { rect(100, 860, 960, 640), ] { assert_frame( - super::reachable_client(saved, 48, &[(area, 1.0)], (area, 1.0), &config()), + super::reachable_client(saved, 48, &[area], (area, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); } @@ -420,20 +404,14 @@ mod tests { fn oversized_window_fits_remaining_work_area_and_scaled_header_is_checked() { let area = rect(0, 50, 1440, 850); assert_frame( - super::reachable_client( - rect(0, 50, 2800, 1800), - 96, - &[(area, 1.0)], - (area, 1.0), - &config(), - ), + super::reachable_client(rect(0, 50, 2800, 1800), 96, &[area], (area, 1.0), &config()), Some(area), ); assert_frame( super::reachable_client( rect(100, 820, 960, 640), 48, - &[(area, 1.0)], + &[area], (area, 1.0), &config(), ), @@ -443,7 +421,7 @@ mod tests { super::reachable_client( rect(100, 820, 960, 640), 96, - &[(area, 1.0)], + &[area], (area, 1.0), &config(), ), @@ -460,7 +438,7 @@ mod tests { &saved, tauri::PhysicalPosition::new(7, 0), 48, - &[(area, 1.0)], + &[area], (area, 1.0), &config() ), @@ -474,24 +452,12 @@ mod tests { let right = rect(0, 0, 1920, 1080); let spanning = rect(-480, 100, 960, 640); assert_frame( - super::reachable_client( - spanning, - 48, - &[(left, 1.0), (right, 1.0)], - (right, 1.0), - &config(), - ), + super::reachable_client(spanning, 48, &[left, right], (right, 1.0), &config()), None, ); let below = rect(0, 300, 1920, 1080); assert_frame( - super::reachable_client( - spanning, - 48, - &[(left, 1.0), (below, 1.0)], - (left, 1.0), - &config(), - ), + super::reachable_client(spanning, 48, &[left, below], (left, 1.0), &config()), Some(rect(-1440, 0, 960, 640)), ); } @@ -516,7 +482,7 @@ mod tests { let saved = decode(&saved_state(100, -450, 960, 640)); // The old post-set guard approves this stale, reachable startup frame. assert_frame( - super::reachable_client(startup, 48, &[(area, 1.0)], (area, 1.0), &config()), + super::reachable_client(startup, 48, &[area], (area, 1.0), &config()), None, ); // The planner accepts no current bounds: they can be either startup or @@ -528,19 +494,19 @@ mod tests { Some(1.0), ); assert_frame( - super::reachable_client(completed, 48, &[(area, 1.0)], (area, 1.0), &config()), + super::reachable_client(completed, 48, &[area], (area, 1.0), &config()), Some(rect(240, 25, 960, 640)), ); let offset = tauri::PhysicalPosition::new(0, 0); assert_eq!( - super::restore_request(&saved, offset, 48, &[(area, 1.0)], (area, 1.0), &config()), + super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), expected ); // Resize is asynchronous too: validating only startup width misses // the right-hand controls of a wider saved client. let saved = decode(&saved_state(100, 120, 1800, 800)); assert_eq!( - super::restore_request(&saved, offset, 48, &[(area, 1.0)], (area, 1.0), &config()), + super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), ( area.position, tauri::PhysicalSize::new(1440, 800), @@ -555,7 +521,7 @@ mod tests { let offset = tauri::PhysicalPosition::new(7, 0); let saved = decode(&saved_state(-7, 0, 1920, 1000)); assert_eq!( - super::restore_request(&saved, offset, 48, &[(area, 1.0)], (area, 1.0), &config()), + super::restore_request(&saved, offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(-7, 0), tauri::PhysicalSize::new(1920, 1000), @@ -567,14 +533,7 @@ mod tests { state["main"]["prev_x"] = json!(100); state["main"]["prev_y"] = json!(-450); assert_eq!( - super::restore_request( - &decode(&state), - offset, - 48, - &[(area, 1.0)], - (area, 1.0), - &config() - ), + super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(473, 0), tauri::PhysicalSize::new(960, 640), @@ -585,14 +544,7 @@ mod tests { state["main"]["maximized"] = json!(false); state["main"]["fullscreen"] = json!(true); assert_eq!( - super::restore_request( - &decode(&state), - offset, - 48, - &[(area, 1.0)], - (area, 1.0), - &config() - ), + super::restore_request(&decode(&state), offset, 48, &[area], (area, 1.0), &config()), ( tauri::PhysicalPosition::new(0, 0), tauri::PhysicalSize::new(960, 640), From eb0d34b174914867d70ff0957bec3264b88ade0a Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 08:46:42 -0800 Subject: [PATCH 07/11] fix(window): compare the minimum as tao rounds it and keep the restore fix to the core cases Move the sub-minimum check into restore_request. It converts the configured minimum to pixels at the fallback monitor's scale with the rounding tao uses, so a lone monitor at a custom Windows scale keeps a window that is exactly at the minimum. reachable_client now only gets the default size to use, and the setters send a relocated window in logical units through one branch. Behavior for the unplugged external monitor case is unchanged. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 140 ++++++++++++++++------------------ 1 file changed, 66 insertions(+), 74 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index 9ff5cca54f..eb744e15a1 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -1,6 +1,5 @@ use serde::Deserialize; use std::collections::HashMap; -use tauri::utils::config::WindowConfig; use tauri::{LogicalSize, Manager, PhysicalPosition, PhysicalRect, PhysicalSize, Runtime, Window}; use tauri_plugin_window_state::{AppHandleExt, Builder, StateFlags, WindowExt}; @@ -152,23 +151,18 @@ fn restore_geometry_from( ); // AppShell owns a 48 logical-pixel header on all desktop platforms. let header_height = (48.0 * window.scale_factor()?).ceil() as u32; - let default = WindowConfig::default(); - let config = window.config().app.windows.first().unwrap_or(&default); + let windows = &window.config().app.windows; + let config = windows.first().cloned().unwrap_or_default(); let (position, size, relocated) = - restore_request(&saved, offset, header_height, &areas, fallback, config); + restore_request(&saved, offset, header_height, &areas, fallback, &config); // Validate the requested geometry BEFORE sending it. Native setters // may apply asynchronously (Linux/macOS); getters can still report the - // startup frame. macOS and GTK convert physical values at the startup - // scale, so a relocated window is sent in its monitor's logical units. - let (position, size): (tauri::Position, tauri::Size) = - match relocated.filter(|_| cfg!(not(windows))) { - Some(scale) => ( - position.to_logical::(scale).into(), - size.to_logical::(scale).into(), - ), - None => (position.into(), size.into()), - }; - // Move first: a Windows DPI change rescales the window. + // startup frame. + // Move first: Windows rescales on DPI change. macOS/GTK convert at the startup scale. + if let Some(scale) = relocated.filter(|_| cfg!(not(windows))) { + window.set_position(position.to_logical::(scale))?; + return window.set_size(size.to_logical::(scale)); + } window.set_position(position)?; window.set_size(size)?; } @@ -180,8 +174,8 @@ fn restore_request( client_offset: PhysicalPosition, header_height: u32, areas: &[PhysicalRect], - fallback: (PhysicalRect, f64), - config: &WindowConfig, + (fallback, scale): (PhysicalRect, f64), + config: &tauri::utils::config::WindowConfig, ) -> (PhysicalPosition, PhysicalSize, Option) { let position = if saved.maximized { PhysicalPosition::new(saved.prev_x, saved.prev_y) @@ -195,7 +189,12 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - let relocated = reachable_client(client, header_height, areas, fallback, config); + // A size below the minimum at the fallback's scale (in pixels, rounded as + // tao rounds it) was saved at a lower scale and would reopen clamped. + let px = |logical: Option| (logical.unwrap_or(0.0) * scale).round() as u32; + let default = (saved.width < px(config.min_width) || saved.height < px(config.min_height)) + .then(|| LogicalSize::new(config.width, config.height).to_physical(scale)); + let relocated = reachable_client(client, header_height, areas, fallback, default); let target = relocated.unwrap_or(client); ( PhysicalPosition::new( @@ -203,16 +202,16 @@ fn restore_request( target.position.y.saturating_sub(client_offset.y), ), target.size, - relocated.map(|_| fallback.1), + relocated.map(|_| scale), ) } fn reachable_client( - client: PhysicalRect, + mut client: PhysicalRect, header_height: u32, areas: &[PhysicalRect], - (fallback, fallback_scale): (PhysicalRect, f64), - config: &WindowConfig, + fallback: PhysicalRect, + default: Option>, ) -> Option> { let x = i64::from(client.position.x); let y = i64::from(client.position.y); @@ -230,25 +229,16 @@ fn reachable_client( && y < i64::from(area.position.y) + i64::from(area.size.height) }) }); - // A saved size below the minimum at the fallback's scale was saved at a - // lower scale. It would open clamped and re-save that smaller size. With a - // single monitor its scale is known even when the rect is still reachable. - let saved = client.size.to_logical::(fallback_scale); - let too_small = saved.width < config.min_width.unwrap_or(0.0) - || saved.height < config.min_height.unwrap_or(0.0); - if reachable && !(too_small && areas.len() == 1) { + // A lone monitor's scale is known, so a sub-minimum size is replaced even there. + if reachable && !(default.is_some() && areas.len() == 1) { return None; } + client.size = default.unwrap_or(client.size); // Unlike any-corner intersection, this keeps the drag strip and both sets // of platform controls on-screen, including after removing an upper monitor. - let wanted = if too_small { - LogicalSize::new(config.width, config.height).to_physical(fallback_scale) - } else { - client.size - }; let size = PhysicalSize::new( - wanted.width.min(fallback.size.width), - wanted.height.min(fallback.size.height), + client.size.width.min(fallback.size.width), + client.size.height.min(fallback.size.height), ); Some(PhysicalRect { position: PhysicalPosition::new( @@ -335,26 +325,40 @@ mod tests { #[test] fn fallback_uses_default_size_when_saved_size_is_below_minimum_at_its_scale() { let laptop = rect(0, 50, 3024, 1840); - let fallback = - |client, area| super::reachable_client(client, 96, &[area], (area, 2.0), &config()); + let left = rect(-3024, 0, 3024, 1964); + let fallback = |width, height, area| { + let saved = decode(&saved_state(5530, 100, width, height)); + let offset = tauri::PhysicalPosition::new(0, 0); + let (position, size, scale) = + super::restore_request(&saved, offset, 96, &[area, left], (area, 2.0), &config()); + assert_eq!(scale, Some(2.0)); + Some(tauri::PhysicalRect { position, size }) + }; // Saved on a 1x monitor that is gone: 301x235 points on a 2x laptop. - assert_frame( - fallback(rect(5530, 100, 602, 470), laptop), - Some(rect(312, 50, 2400, 1600)), - ); + assert_frame(fallback(602, 470, laptop), Some(rect(312, 50, 2400, 1600))); // One axis below the minimum is enough. - assert_frame( - fallback(rect(5530, 100, 960, 799), laptop), - Some(rect(312, 50, 2400, 1600)), - ); + assert_frame(fallback(960, 799, laptop), Some(rect(312, 50, 2400, 1600))); // At or above the minimum at the fallback's scale, the saved size stays. - assert_frame( - fallback(rect(5530, 100, 960, 800), laptop), - Some(rect(1032, 50, 960, 800)), - ); + assert_frame(fallback(960, 800, laptop), Some(rect(1032, 50, 960, 800))); // The default still fits inside a small fallback work area. let small = rect(0, 25, 1440, 875); - assert_frame(fallback(rect(5530, 100, 602, 470), small), Some(small)); + assert_frame(fallback(602, 470, small), Some(small)); + } + + #[test] + fn minimum_size_is_compared_with_the_rounding_tao_applies() { + // At 110 DPI the 480x400 minimum is 550x458.33, and tao enforces 550x458. + let scale = 110.0 / 96.0; + let area = rect(0, 0, 2200, 1200); + let offset = tauri::PhysicalPosition::new(0, 0); + let relocated = |width, height| { + let saved = decode(&saved_state(100, 100, width, height)); + super::restore_request(&saved, offset, 55, &[area], (area, scale), &config()) + .2 + .is_some() + }; + assert!(!relocated(550, 458)); + assert!(relocated(550, 457)); } #[test] @@ -362,12 +366,12 @@ mod tests { let laptop = rect(0, 25, 1440, 875); let saved = rect(100, -450, 960, 640); assert_frame( - super::reachable_client(saved, 48, &[laptop], (laptop, 1.0), &config()), + super::reachable_client(saved, 48, &[laptop], laptop, None), Some(rect(240, 25, 960, 640)), ); let upper = rect(0, -900, 1440, 900); assert_frame( - super::reachable_client(saved, 48, &[laptop, upper], (laptop, 1.0), &config()), + super::reachable_client(saved, 48, &[laptop, upper], laptop, None), None, ); } @@ -378,7 +382,7 @@ mod tests { let left = rect(-1920, 0, 1920, 1080); for saved in [rect(100, 120, 960, 640), rect(-1800, 80, 960, 640)] { assert_frame( - super::reachable_client(saved, 48, &[primary, left], (primary, 1.0), &config()), + super::reachable_client(saved, 48, &[primary, left], primary, None), None, ); } @@ -394,7 +398,7 @@ mod tests { rect(100, 860, 960, 640), ] { assert_frame( - super::reachable_client(saved, 48, &[area], (area, 1.0), &config()), + super::reachable_client(saved, 48, &[area], area, None), Some(rect(240, 25, 960, 640)), ); } @@ -404,27 +408,15 @@ mod tests { fn oversized_window_fits_remaining_work_area_and_scaled_header_is_checked() { let area = rect(0, 50, 1440, 850); assert_frame( - super::reachable_client(rect(0, 50, 2800, 1800), 96, &[area], (area, 1.0), &config()), + super::reachable_client(rect(0, 50, 2800, 1800), 96, &[area], area, None), Some(area), ); assert_frame( - super::reachable_client( - rect(100, 820, 960, 640), - 48, - &[area], - (area, 1.0), - &config(), - ), + super::reachable_client(rect(100, 820, 960, 640), 48, &[area], area, None), None, ); assert_frame( - super::reachable_client( - rect(100, 820, 960, 640), - 96, - &[area], - (area, 1.0), - &config(), - ), + super::reachable_client(rect(100, 820, 960, 640), 96, &[area], area, None), Some(rect(240, 50, 960, 640)), ); } @@ -452,12 +444,12 @@ mod tests { let right = rect(0, 0, 1920, 1080); let spanning = rect(-480, 100, 960, 640); assert_frame( - super::reachable_client(spanning, 48, &[left, right], (right, 1.0), &config()), + super::reachable_client(spanning, 48, &[left, right], right, None), None, ); let below = rect(0, 300, 1920, 1080); assert_frame( - super::reachable_client(spanning, 48, &[left, below], (left, 1.0), &config()), + super::reachable_client(spanning, 48, &[left, below], left, None), Some(rect(-1440, 0, 960, 640)), ); } @@ -482,7 +474,7 @@ mod tests { let saved = decode(&saved_state(100, -450, 960, 640)); // The old post-set guard approves this stale, reachable startup frame. assert_frame( - super::reachable_client(startup, 48, &[area], (area, 1.0), &config()), + super::reachable_client(startup, 48, &[area], area, None), None, ); // The planner accepts no current bounds: they can be either startup or @@ -494,7 +486,7 @@ mod tests { Some(1.0), ); assert_frame( - super::reachable_client(completed, 48, &[area], (area, 1.0), &config()), + super::reachable_client(completed, 48, &[area], area, None), Some(rect(240, 25, 960, 640)), ); let offset = tauri::PhysicalPosition::new(0, 0); From 51c72f3060b035292cf0b83237d8d95914d2fd3b Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 08:59:50 -0800 Subject: [PATCH 08/11] fix(window): keep a Windows window that was saved at the minimum size On Windows, tao applies minWidth/minHeight to the undecorated frame and the shadow insets come off the client, so the plugin saves a client a few pixels below the minimum at the same scale. The lone-monitor rule then reset that window to the default size on every launch. Treat a size as saved at a lower scale only when it is more than 8% below the minimum at the fallback's scale. The insets are about 2 to 3%; standard scale steps are at least 11% apart, and macOS and GTK enforce the minimum on the client. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 36 ++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index eb744e15a1..4f0fccf32f 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -189,9 +189,11 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - // A size below the minimum at the fallback's scale (in pixels, rounded as - // tao rounds it) was saved at a lower scale and would reopen clamped. - let px = |logical: Option| (logical.unwrap_or(0.0) * scale).round() as u32; + // A size more than 8% below the minimum at the fallback's scale was saved at a + // lower scale (scale steps are at least 11% apart) and would reopen clamped. + // A Windows client at the minimum is a few percent below it: tao applies the + // minimum to the frame, and the undecorated shadow insets come off the client. + let px = |logical: Option| (logical.unwrap_or(0.0) * scale * 0.92) as u32; let default = (saved.width < px(config.min_width) || saved.height < px(config.min_height)) .then(|| LogicalSize::new(config.width, config.height).to_physical(scale)); let relocated = reachable_client(client, header_height, areas, fallback, default); @@ -336,8 +338,8 @@ mod tests { }; // Saved on a 1x monitor that is gone: 301x235 points on a 2x laptop. assert_frame(fallback(602, 470, laptop), Some(rect(312, 50, 2400, 1600))); - // One axis below the minimum is enough. - assert_frame(fallback(960, 799, laptop), Some(rect(312, 50, 2400, 1600))); + // One axis clearly below the minimum is enough. + assert_frame(fallback(960, 700, laptop), Some(rect(312, 50, 2400, 1600))); // At or above the minimum at the fallback's scale, the saved size stays. assert_frame(fallback(960, 800, laptop), Some(rect(1032, 50, 960, 800))); // The default still fits inside a small fallback work area. @@ -346,19 +348,27 @@ mod tests { } #[test] - fn minimum_size_is_compared_with_the_rounding_tao_applies() { - // At 110 DPI the 480x400 minimum is 550x458.33, and tao enforces 550x458. - let scale = 110.0 / 96.0; - let area = rect(0, 0, 2200, 1200); + fn windows_client_at_the_minimum_is_kept_but_a_lower_scale_size_is_replaced() { + let area = rect(0, 0, 3840, 2160); let offset = tauri::PhysicalPosition::new(0, 0); - let relocated = |width, height| { + let relocated = |scale, width, height| { let saved = decode(&saved_state(100, 100, width, height)); - super::restore_request(&saved, offset, 55, &[area], (area, scale), &config()) + super::restore_request(&saved, offset, 96, &[area], (area, scale), &config()) .2 .is_some() }; - assert!(!relocated(550, 458)); - assert!(relocated(550, 457)); + // tao applies the 480x400 minimum to an undecorated Windows frame. The + // shadow insets (size frame plus padded border on the sides and bottom, + // one scaled pixel on top on Windows 11) come off the saved client. + assert!(!relocated(1.0, 464, 391)); + assert!(!relocated(1.25, 580, 489)); + assert!(!relocated(2.0, 928, 782)); + // A client saved at the minimum one scale step lower is still replaced, + // including across the smallest step, 225% to 250%. + assert!(relocated(1.25, 464, 391)); + assert!(relocated(2.5, 1044, 880)); + // Without insets (macOS, GTK) the step is just as clear. + assert!(relocated(2.5, 1080, 900)); } #[test] From 5d34ecf1f3e8bdbfbdccadd9eb50e4858fbd94a5 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 09:11:03 -0800 Subject: [PATCH 09/11] fix(window): allow the shadow inset slack only on Windows macOS does not clamp a programmatic resize to the minimum, so the 8% allowance let a 1x save of 900x750 open at 450x375 points on a 2x laptop. Keep the exact minimum (rounded as tao rounds it) on macOS and Linux, and allow 8% only on Windows, where tao applies the minimum to the undecorated frame and the shadow insets come off the saved client. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index 4f0fccf32f..e8d028019b 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -189,11 +189,12 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - // A size more than 8% below the minimum at the fallback's scale was saved at a - // lower scale (scale steps are at least 11% apart) and would reopen clamped. - // A Windows client at the minimum is a few percent below it: tao applies the - // minimum to the frame, and the undecorated shadow insets come off the client. - let px = |logical: Option| (logical.unwrap_or(0.0) * scale * 0.92) as u32; + // A size below the minimum at the fallback's scale (in pixels, rounded as tao + // rounds it) was saved at a lower scale and would reopen at or below it. Windows + // applies the minimum to the undecorated frame and the shadow insets come off + // the saved client, so allow 8% there (scale steps are at least 11% apart). + let slack = if cfg!(windows) { 0.92 } else { 1.0 }; + let px = |logical: Option| (logical.unwrap_or(0.0) * scale * slack).round() as u32; let default = (saved.width < px(config.min_width) || saved.height < px(config.min_height)) .then(|| LogicalSize::new(config.width, config.height).to_physical(scale)); let relocated = reachable_client(client, header_height, areas, fallback, default); @@ -348,7 +349,7 @@ mod tests { } #[test] - fn windows_client_at_the_minimum_is_kept_but_a_lower_scale_size_is_replaced() { + fn minimum_allows_windows_shadow_insets_but_a_lower_scale_size_is_replaced() { let area = rect(0, 0, 3840, 2160); let offset = tauri::PhysicalPosition::new(0, 0); let relocated = |scale, width, height| { @@ -360,15 +361,18 @@ mod tests { // tao applies the 480x400 minimum to an undecorated Windows frame. The // shadow insets (size frame plus padded border on the sides and bottom, // one scaled pixel on top on Windows 11) come off the saved client. - assert!(!relocated(1.0, 464, 391)); - assert!(!relocated(1.25, 580, 489)); - assert!(!relocated(2.0, 928, 782)); - // A client saved at the minimum one scale step lower is still replaced, - // including across the smallest step, 225% to 250%. + for (scale, width, height) in [(1.0, 464, 391), (1.25, 580, 489), (2.0, 928, 782)] { + assert_eq!(relocated(scale, width, height), !cfg!(windows)); + } + // macOS does not clamp a programmatic resize, so a 1x save of 900x750 + // would open at 450x375 points on a 2x laptop. Windows clamps the frame. + assert_eq!(relocated(2.0, 900, 750), !cfg!(windows)); + // A client saved at the minimum one scale step lower is replaced + // everywhere, including across the smallest step, 225% to 250%. assert!(relocated(1.25, 464, 391)); assert!(relocated(2.5, 1044, 880)); - // Without insets (macOS, GTK) the step is just as clear. assert!(relocated(2.5, 1080, 900)); + assert!(!relocated(2.0, 960, 800)); } #[test] From 94bc460e525f38c2982a22b97999351850dc0599 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 09:39:04 -0800 Subject: [PATCH 10/11] fix(window): compare a Windows client with the minimum minus tao's shadow insets The 8% allowance kept sizes that were already below the minimum, such as a 1x save of 900x750 on a lone 2x laptop. tao does not clamp a programmatic resize on Windows either: it answers WM_WINDOWPOSCHANGING itself without DefWindowProc. Compare instead against the client tao leaves at the minimum, the frame minimum minus the undecorated shadow insets from calculate_insets_for_dpi with default metrics and the Windows 11 top inset. macOS and Linux still use the rounded minimum. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 42 ++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index e8d028019b..511fcd52fe 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -189,14 +189,16 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - // A size below the minimum at the fallback's scale (in pixels, rounded as tao - // rounds it) was saved at a lower scale and would reopen at or below it. Windows - // applies the minimum to the undecorated frame and the shadow insets come off - // the saved client, so allow 8% there (scale steps are at least 11% apart). - let slack = if cfg!(windows) { 0.92 } else { 1.0 }; - let px = |logical: Option| (logical.unwrap_or(0.0) * scale * slack).round() as u32; - let default = (saved.width < px(config.min_width) || saved.height < px(config.min_height)) - .then(|| LogicalSize::new(config.width, config.height).to_physical(scale)); + // A size below the minimum at the fallback's scale (rounded as tao rounds it) was + // saved at a lower scale and would reopen below it. Windows applies the minimum to + // the frame: take off tao's shadow insets (default metrics, Windows 11 top). + let win = u32::from(cfg!(windows)); + let side = win * (4 * (scale * 96.0).round() as u32 + 48) / 96 * 2; + let top = win * scale.round() as u32; + let px = |logical: Option| (logical.unwrap_or(0.0) * scale).round() as u32; + let below = saved.width < px(config.min_width).saturating_sub(2 * side) + || saved.height < px(config.min_height).saturating_sub(side + top); + let default = below.then(|| LogicalSize::new(config.width, config.height).to_physical(scale)); let relocated = reachable_client(client, header_height, areas, fallback, default); let target = relocated.unwrap_or(client); ( @@ -339,8 +341,8 @@ mod tests { }; // Saved on a 1x monitor that is gone: 301x235 points on a 2x laptop. assert_frame(fallback(602, 470, laptop), Some(rect(312, 50, 2400, 1600))); - // One axis clearly below the minimum is enough. - assert_frame(fallback(960, 700, laptop), Some(rect(312, 50, 2400, 1600))); + // One axis below the minimum is enough (781 is under the Windows client too). + assert_frame(fallback(960, 781, laptop), Some(rect(312, 50, 2400, 1600))); // At or above the minimum at the fallback's scale, the saved size stays. assert_frame(fallback(960, 800, laptop), Some(rect(1032, 50, 960, 800))); // The default still fits inside a small fallback work area. @@ -364,9 +366,23 @@ mod tests { for (scale, width, height) in [(1.0, 464, 391), (1.25, 580, 489), (2.0, 928, 782)] { assert_eq!(relocated(scale, width, height), !cfg!(windows)); } - // macOS does not clamp a programmatic resize, so a 1x save of 900x750 - // would open at 450x375 points on a 2x laptop. Windows clamps the frame. - assert_eq!(relocated(2.0, 900, 750), !cfg!(windows)); + // One pixel under that client is replaced everywhere. + assert!(relocated(1.0, 463, 391)); + assert!(relocated(1.0, 464, 390)); + assert!(relocated(2.0, 927, 782)); + // Neither tao on Windows nor macOS clamps a programmatic resize, so a 1x + // save of 900x750 would open at 450x375 points on a 2x laptop. + assert!(relocated(2.0, 900, 750)); + // At 110 DPI the minimum rounds to 550x458, and the Windows client to 530x447. + let scale = 110.0 / 96.0; + assert!(!relocated(scale, 550, 458)); + assert_eq!(relocated(scale, 550, 457), !cfg!(windows)); + assert_eq!(relocated(scale, 530, 447), !cfg!(windows)); + assert!(relocated(scale, 529, 447)); + // At 100 DPI the height rounds up (416.67 to 417; 408 on Windows). + let scale = 100.0 / 96.0; + assert_eq!(relocated(scale, 500, 416), !cfg!(windows)); + assert!(relocated(scale, 484, 407)); // A client saved at the minimum one scale step lower is replaced // everywhere, including across the smallest step, 225% to 250%. assert!(relocated(1.25, 464, 391)); From f30a250e51c15cfabb092cc446ad7ecb41837cfd Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 16:39:30 -0800 Subject: [PATCH 11/11] ci: rerun against current main Signed-off-by: Cheng Peng Co-authored-by: Cursor