diff --git a/crates/gpui/src/app/test_app.rs b/crates/gpui/src/app/test_app.rs index d241863883..bf265fd7bf 100644 --- a/crates/gpui/src/app/test_app.rs +++ b/crates/gpui/src/app/test_app.rs @@ -481,6 +481,24 @@ impl TestAppWindow { self.background_executor.run_until_parked(); } + /// Simulate the window moving to a display with a different scale factor. + pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) { + let window_id = self.handle.window_id(); + let test_window = { + let mut app = self.app.borrow_mut(); + app.windows + .get_mut(window_id) + .and_then(|window| window.as_mut()) + .and_then(|window| window.platform_window.as_test()) + .cloned() + }; + // The resize callback needs to borrow the app again synchronously. + if let Some(mut test_window) = test_window { + test_window.simulate_scale_factor_change(scale_factor); + } + self.background_executor.run_until_parked(); + } + /// Force a redraw of the window. pub fn draw(&mut self) { let mut app = self.app.borrow_mut(); @@ -557,6 +575,26 @@ mod tests { app.update(|cx| cx.shutdown()); } + #[test] + fn test_simulate_scale_factor_change() { + let mut app = TestApp::new(); + let mut window = app.open_window(|_, _| crate::EmptyView); + let viewport_size = window.update(|_, window, _| { + assert_eq!(window.scale_factor(), 2.0); + window.viewport_size() + }); + + window.simulate_scale_factor_change(1.0); + + window.update(|_, window, _| { + assert_eq!(window.scale_factor(), 1.0); + assert_eq!(window.viewport_size(), viewport_size); + }); + + drop(window); + app.update(|cx| cx.shutdown()); + } + #[test] fn test_entity_creation() { let mut app = TestApp::new(); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 1dceee09d7..a34a779007 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -403,6 +403,16 @@ impl TestAppContext { self.test_window(window_handle).simulate_resize(size); } + /// Simulates the window moving to a display with a different scale factor. + pub fn simulate_window_scale_factor_change( + &self, + window_handle: AnyWindowHandle, + scale_factor: f32, + ) { + self.test_window(window_handle) + .simulate_scale_factor_change(scale_factor); + } + /// Returns true if there's an alert dialog open. pub fn expect_restart(&self) -> oneshot::Receiver<(Option, Vec)> { let (tx, rx) = futures::channel::oneshot::channel(); @@ -884,6 +894,11 @@ impl VisualTestContext { self.simulate_window_resize(self.window, size) } + /// Simulates the window moving to a display with a different scale factor. + pub fn simulate_scale_factor_change(&self, scale_factor: f32) { + self.simulate_window_scale_factor_change(self.window, scale_factor) + } + /// debug_bounds returns the bounds of the element with the given selector. pub fn debug_bounds(&mut self, selector: &'static str) -> Option> { self.update(|window, _| window.rendered_frame.debug_bounds.get(selector).copied()) diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 38926b649c..5a07bc5c01 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -46,6 +46,7 @@ pub(crate) struct TestWindowState { text_input_configurations: Vec, text_input_state_changes: Vec, is_fullscreen: bool, + scale_factor: f32, appearance: WindowAppearance, external_drag_files: Vec<(PathBuf, bool)>, start_external_drag_result: bool, @@ -110,6 +111,8 @@ impl TestWindow { text_input_configurations: Vec::new(), text_input_state_changes: Vec::new(), is_fullscreen: false, + // Preserve the test platform's historical 2x default. + scale_factor: 2.0, appearance: WindowAppearance::Light, external_drag_files: Vec::new(), start_external_drag_result: false, @@ -160,6 +163,16 @@ impl TestWindow { self.0.lock().resize_callback = Some(callback); } + /// Simulates a display scale change through the resize callback, preserving logical bounds. + pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) { + let size = { + let mut lock = self.0.lock(); + lock.scale_factor = scale_factor; + lock.bounds.size + }; + self.simulate_resize(size); + } + pub(crate) fn simulate_active_status_change(&self, active: bool) { let mut lock = self.0.lock(); let Some(mut callback) = lock.active_status_change_callback.take() else { @@ -241,7 +254,7 @@ impl PlatformWindow for TestWindow { } fn scale_factor(&self) -> f32 { - 2.0 + self.0.lock().scale_factor } fn appearance(&self) -> WindowAppearance { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index cf8eded704..606623b5b0 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -7452,6 +7452,45 @@ mod tests { .unwrap(); } + #[test] + fn test_scale_factor_change_preserves_bounds_and_survives_resize() { + let mut cx = TestAppContext::single(); + let window = cx.add_window(|_, _| EmptyView); + let handle: AnyWindowHandle = window.into(); + let window_state = |cx: &mut TestAppContext| { + cx.update_window(handle, |_, window, _| { + ( + window.scale_factor(), + window.bounds(), + window.viewport_size(), + ) + }) + .unwrap() + }; + + let (scale_factor, mut expected_bounds, _) = window_state(&mut cx); + assert_eq!(scale_factor, 2.0); + + for (scale_factor, resized_size) in [ + (1.0, size(px(800.), px(600.))), + (1.25, size(px(640.), px(480.))), + (2.0, size(px(1024.), px(768.))), + ] { + cx.simulate_window_scale_factor_change(handle, scale_factor); + assert_eq!( + window_state(&mut cx), + (scale_factor, expected_bounds, expected_bounds.size) + ); + + cx.simulate_window_resize(handle, resized_size); + expected_bounds.size = resized_size; + assert_eq!( + window_state(&mut cx), + (scale_factor, expected_bounds, resized_size) + ); + } + } + /// Platforms that stop requesting frames for idle windows (currently web) /// rely on the frame waker firing whenever frame demand arises; a demand /// source that skips the waker shows up there as a window that silently