From d4e0a39b8a1c05084a00d90ed9e9a020fe9fedf8 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 00:03:29 -0800 Subject: [PATCH 1/3] 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. (cherry picked from commit 7de4895d1, resolved onto the restructured restore_request / reachable_client from #2405) Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng (cherry picked from commit 55d8657c963ff64ffd6107fd9bf9e93a83c1ea77) --- src-tauri/src/window_state.rs | 144 +++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 54 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index 511fcd52fe..c80fe2d4cc 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -134,7 +134,7 @@ 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()? @@ -173,7 +173,7 @@ fn restore_request( saved: &SavedGeometry, client_offset: PhysicalPosition, header_height: u32, - areas: &[PhysicalRect], + areas: &[(PhysicalRect, f64)], (fallback, scale): (PhysicalRect, f64), config: &tauri::utils::config::WindowConfig, ) -> (PhysicalPosition, PhysicalSize, Option) { @@ -189,17 +189,24 @@ fn restore_request( ), size: PhysicalSize::new(saved.width, saved.height), }; - // 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). + // A size below the minimum at a monitor's scale (rounded as tao rounds it) was + // saved at a lower scale and would reopen below it there. 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 fits = |scale: f64| { + 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; + saved.width >= px(config.min_width).saturating_sub(2 * side) + && saved.height >= px(config.min_height).saturating_sub(side + top) + }; + let areas: Vec<_> = areas + .iter() + .map(|&(area, scale)| (area, fits(scale))) + .collect(); + let default = + (!fits(scale)).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( @@ -214,7 +221,7 @@ fn restore_request( fn reachable_client( mut client: PhysicalRect, header_height: u32, - areas: &[PhysicalRect], + areas: &[(PhysicalRect, bool)], fallback: PhysicalRect, default: Option>, ) -> Option> { @@ -223,19 +230,20 @@ fn reachable_client( let right = x + i64::from(client.size.width.saturating_sub(1)); let bottom = y + i64::from(header_height.saturating_sub(1)); // A header may span adjacent monitors. Both control regions must remain - // reachable, but need not belong to the same monitor. + // reachable, but need not belong to the same monitor. A monitor only holds + // them if the saved size fits its scale. 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, fits)| { + *fits + && 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 lone monitor's scale is known, so a sub-minimum size is replaced even there. - if reachable && !(default.is_some() && areas.len() == 1) { + if reachable { return None; } client.size = default.unwrap_or(client.size); @@ -296,18 +304,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), @@ -315,16 +323,17 @@ 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] @@ -334,8 +343,14 @@ mod tests { 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()); + let (position, size, scale) = super::restore_request( + &saved, + offset, + 96, + &[(area, 2.0), (left, 2.0)], + (area, 2.0), + &config(), + ); assert_eq!(scale, Some(2.0)); Some(tauri::PhysicalRect { position, size }) }; @@ -356,9 +371,16 @@ mod tests { let offset = tauri::PhysicalPosition::new(0, 0); let relocated = |scale, width, height| { let saved = decode(&saved_state(100, 100, width, height)); - super::restore_request(&saved, offset, 96, &[area], (area, scale), &config()) - .2 - .is_some() + super::restore_request( + &saved, + offset, + 96, + &[(area, scale)], + (area, scale), + &config(), + ) + .2 + .is_some() }; // tao applies the 480x400 minimum to an undecorated Windows frame. The // shadow insets (size frame plus padded border on the sides and bottom, @@ -396,12 +418,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, None), + super::reachable_client(saved, 48, &[(laptop, true)], 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, None), + super::reachable_client(saved, 48, &[(laptop, true), (upper, true)], laptop, None), None, ); } @@ -412,7 +434,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, None), + super::reachable_client(saved, 48, &[(primary, true), (left, true)], primary, None), None, ); } @@ -428,7 +450,7 @@ mod tests { rect(100, 860, 960, 640), ] { assert_frame( - super::reachable_client(saved, 48, &[area], area, None), + super::reachable_client(saved, 48, &[(area, true)], area, None), Some(rect(240, 25, 960, 640)), ); } @@ -438,15 +460,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, None), + super::reachable_client(rect(0, 50, 2800, 1800), 96, &[(area, true)], area, None), Some(area), ); assert_frame( - super::reachable_client(rect(100, 820, 960, 640), 48, &[area], area, None), + super::reachable_client(rect(100, 820, 960, 640), 48, &[(area, true)], area, None), None, ); assert_frame( - super::reachable_client(rect(100, 820, 960, 640), 96, &[area], area, None), + super::reachable_client(rect(100, 820, 960, 640), 96, &[(area, true)], area, None), Some(rect(240, 50, 960, 640)), ); } @@ -460,7 +482,7 @@ mod tests { &saved, tauri::PhysicalPosition::new(7, 0), 48, - &[area], + &[(area, 1.0)], (area, 1.0), &config() ), @@ -474,12 +496,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, None), + super::reachable_client(spanning, 48, &[(left, true), (right, true)], right, None), None, ); let below = rect(0, 300, 1920, 1080); assert_frame( - super::reachable_client(spanning, 48, &[left, below], left, None), + super::reachable_client(spanning, 48, &[(left, true), (below, true)], left, None), Some(rect(-1440, 0, 960, 640)), ); } @@ -504,7 +526,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, None), + super::reachable_client(startup, 48, &[(area, true)], area, None), None, ); // The planner accepts no current bounds: they can be either startup or @@ -516,19 +538,19 @@ mod tests { Some(1.0), ); assert_frame( - super::reachable_client(completed, 48, &[area], area, None), + super::reachable_client(completed, 48, &[(area, true)], area, None), 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), @@ -543,7 +565,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), @@ -555,7 +577,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), @@ -566,7 +595,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 7e83a54018dbfbc8c8a50b304dbff903eb3cd39a Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Wed, 4 Nov 2026 00:15:36 -0800 Subject: [PATCH 2/3] fix(window): keep mixed-scale spanning headers 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. (cherry picked from commit 7e2393e72, resolved onto the restructured restore_request / reachable_client from #2405; its minimum rounding part is already in #2405) Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng (cherry picked from commit 6f018b1b1e1d9411a9dec75f64909acab033c790) --- src-tauri/src/window_state.rs | 47 ++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index c80fe2d4cc..a51bc07da5 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -229,21 +229,23 @@ 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 header may span adjacent monitors. Both control regions must remain - // reachable, but need not belong to the same monitor. A monitor only holds - // them if the saved size fits its scale. - let reachable = [(x, y), (right, y), (x, bottom), (right, bottom)] - .into_iter() - .all(|(x, y)| { - areas.iter().any(|(area, fits)| { - *fits - && 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 { + // reachable, but need not belong to the same monitor. + let reachable = corners + .iter() + .all(|&corner| areas.iter().any(|(area, _)| inside(area, corner))); + // At least one monitor under the header must have a scale the saved size fits. + let fitted = areas + .iter() + .any(|(area, fits)| *fits && corners.iter().any(|&corner| inside(area, corner))); + if reachable && fitted { return None; } client.size = default.unwrap_or(client.size); @@ -504,6 +506,23 @@ mod tests { super::reachable_client(spanning, 48, &[(left, true), (below, true)], left, None), 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); + let saved = decode(&saved_state(-500, 100, 602, 470)); + let offset = tauri::PhysicalPosition::new(0, 0); + assert_eq!( + super::restore_request( + &saved, + offset, + 48, + &[(left, 1.0), (laptop, 2.0)], + (laptop, 2.0), + &config() + ) + .2, + None, + ); } fn saved_state(x: i32, y: i32, width: u32, height: u32) -> Value { From 7013f8fee820cb4e809d99ec4d6e9aec50f89a29 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Thu, 5 Nov 2026 19:59:00 -0800 Subject: [PATCH 3/3] fix(window): judge the saved scale by any monitor under the window The saved size comes from the scale of the monitor that held most of the window, which need not hold the header. Count a monitor whose scale fits the saved size when it intersects the saved client, not only when it holds a header corner. A window hanging below a 2x monitor onto a 1x one, or spanning a 1x monitor between two 2x ones, now stays put. Header visibility is main's check again. Co-authored-by: Claude Opus 5.5 Co-authored-by: Cursor Signed-off-by: Cheng Peng --- src-tauri/src/window_state.rs | 75 ++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/window_state.rs b/src-tauri/src/window_state.rs index a51bc07da5..997675b3a3 100644 --- a/src-tauri/src/window_state.rs +++ b/src-tauri/src/window_state.rs @@ -229,22 +229,28 @@ 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 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))); - // At least one monitor under the header must have a scale the saved size fits. - let fitted = areas - .iter() - .any(|(area, fits)| *fits && corners.iter().any(|&corner| inside(area, corner))); + 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) + && 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) + }) + }); + // The size was saved at the scale of the monitor holding most of the window, + // which need not hold the header. Some monitor under the window must fit it. + let fitted = areas.iter().any(|(area, fits)| { + let (left, top) = (i64::from(area.position.x), i64::from(area.position.y)); + *fits + && x < left + i64::from(area.size.width) + && y < top + i64::from(area.size.height) + && left <= right + && top < y + i64::from(client.size.height) + }); if reachable && fitted { return None; } @@ -336,6 +342,47 @@ mod tests { // Another 2x monitor does not change that. let left = rect(-3024, 0, 3024, 1964); assert_eq!(request(&[(laptop, 2.0), (left, 2.0)]), relocated); + // A 1x monitor that holds none of the window does not keep it. As the + // primary it receives the window, at the saved size that fits its scale. + let primary = rect(0, 982, 1920, 1080); + assert_eq!( + super::restore_request( + &saved, + offset, + 96, + &[(laptop, 2.0), (left, 2.0), (primary, 1.0)], + (primary, 1.0), + &config() + ), + ( + tauri::PhysicalPosition::new(659, 982), + tauri::PhysicalSize::new(602, 470), + Some(1.0) + ), + ); + // The size was saved at the scale of the monitor holding most of the + // window, which need not hold the header: a 1x monitor below a 2x one, + // or between two 2x ones. Both windows stay. + let (upper, below) = (rect(0, -2160, 3840, 2160), rect(0, 0, 1920, 1040)); + let (west, middle, east) = ( + rect(0, 0, 2560, 1440), + rect(2560, 0, 1920, 1080), + rect(4480, 0, 2560, 1440), + ); + for (areas, state) in [ + ( + vec![(upper, 2.0), (below, 1.0)], + saved_state(500, -100, 900, 700), + ), + ( + vec![(west, 2.0), (middle, 1.0), (east, 2.0)], + saved_state(2500, 100, 2200, 600), + ), + ] { + let saved = decode(&state); + let request = super::restore_request(&saved, offset, 96, &areas, areas[0], &config()); + assert_eq!(request.2, None); + } } #[test]