From 056f356065b436879c51074ef716e0a2955b1ee4 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Sat, 16 May 2026 15:16:49 -0400 Subject: [PATCH] Review pass 3 --- lib/runtime/src/sys/macos/net.rs | 74 ++++++- lib/ui/src/window.rs | 5 + lib/ui_platform_macos/src/lib.rs | 347 +++++++++++++++++++------------ 3 files changed, 286 insertions(+), 140 deletions(-) diff --git a/lib/runtime/src/sys/macos/net.rs b/lib/runtime/src/sys/macos/net.rs index d43520f..081b2a0 100644 --- a/lib/runtime/src/sys/macos/net.rs +++ b/lib/runtime/src/sys/macos/net.rs @@ -9,6 +9,7 @@ use std::net::{ }; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::pin::Pin; +use std::sync::{Arc, Mutex, OnceLock, mpsc}; use std::thread; use std::time::Duration; @@ -16,10 +17,14 @@ use crate::op::completion::completion_for_current_thread; use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram}; const DEFAULT_LISTENER_BACKLOG: i32 = 1024; +const DNS_QUEUE_CAPACITY: usize = 256; type RecvFuture = Pin>> + 'static>>; type SendFuture = Pin> + 'static>>; type ShutdownFuture = Pin> + 'static>>; +type BlockingTask = Box; + +static DNS_POOL: OnceLock> = OnceLock::new(); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ExecutionPath { @@ -391,16 +396,73 @@ async fn offload( work: impl FnOnce() -> io::Result + Send + 'static, ) -> io::Result { let (future, handle) = completion_for_current_thread::>(); - let handle_for_thread = handle.clone(); - if let Err(error) = thread::Builder::new() - .name("ruin-runtime-net-offload".into()) - .spawn(move || handle_for_thread.complete(work())) - { - handle.complete(Err(io::Error::other(error))); + if let Err(error) = dns_pool().and_then(|pool| { + pool.spawn(Box::new({ + let handle = handle.clone(); + move || handle.complete(work()) + })) + }) { + handle.complete(Err(error)); } future.await } +struct BlockingPool { + sender: mpsc::SyncSender, +} + +impl BlockingPool { + fn spawn(&self, task: BlockingTask) -> io::Result<()> { + self.sender.try_send(task).map_err(|error| match error { + mpsc::TrySendError::Full(_) => io::Error::new( + io::ErrorKind::WouldBlock, + "DNS resolver blocking worker queue is full", + ), + mpsc::TrySendError::Disconnected(_) => io::Error::new( + io::ErrorKind::BrokenPipe, + "DNS resolver blocking worker pool has stopped", + ), + }) + } +} + +fn dns_pool() -> io::Result<&'static BlockingPool> { + match DNS_POOL.get_or_init(create_dns_pool) { + Ok(pool) => Ok(pool), + Err(error) => Err(io::Error::new(error.kind(), error.to_string())), + } +} + +fn create_dns_pool() -> io::Result { + let (sender, receiver) = mpsc::sync_channel::(DNS_QUEUE_CAPACITY); + let receiver = Arc::new(Mutex::new(receiver)); + let worker_count = std::thread::available_parallelism() + .map(usize::from) + .unwrap_or(2) + .clamp(2, 4); + + for index in 0..worker_count { + let receiver = Arc::clone(&receiver); + thread::Builder::new() + .name(format!("ruin-runtime-dns-{index}")) + .spawn(move || { + loop { + let task = { + let receiver = receiver.lock().expect("DNS worker queue mutex poisoned"); + receiver.recv() + }; + match task { + Ok(task) => task(), + Err(_) => break, + } + } + }) + .map_err(io::Error::other)?; + } + + Ok(BlockingPool { sender }) +} + async fn io_timeout( timeout: Duration, future: impl Future>, diff --git a/lib/ui/src/window.rs b/lib/ui/src/window.rs index 96a09cb..4747f4c 100644 --- a/lib/ui/src/window.rs +++ b/lib/ui/src/window.rs @@ -34,6 +34,11 @@ pub enum WindowLifecycle { #[derive(Clone, Copy, Debug, PartialEq)] pub struct WindowConfigured { + /// Current logical content size and platform presentation scale. + /// + /// Backends should emit a new configuration when either the logical size or + /// scale changes. This lets hosts relayout for size changes while renderers + /// can update drawable/text scale when a window moves between displays. pub actual_inner_size: UiSize, pub scale_factor: f32, pub visible: bool, diff --git a/lib/ui_platform_macos/src/lib.rs b/lib/ui_platform_macos/src/lib.rs index 745f9dc..5703ca1 100644 --- a/lib/ui_platform_macos/src/lib.rs +++ b/lib/ui_platform_macos/src/lib.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use std::ffi::{CStr, CString, c_void}; use std::ptr::NonNull; use std::rc::Rc; -use std::sync::{Arc, Mutex, Once, OnceLock}; +use std::sync::{Arc, Mutex, MutexGuard, Once, OnceLock}; use objc2::rc::Retained; use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, Sel}; @@ -65,12 +65,17 @@ struct RunLoopSourceRegistration { _source: CFRetained, } -#[derive(Clone, Copy)] +#[derive(Clone)] struct RunLoopWakeHandle { - run_loop: usize, - source: usize, + run_loop: CFRetained, + source: CFRetained, } +// CFRunLoopSourceSignal and CFRunLoopWakeUp are designed for cross-thread wakeups. +// The handle owns retained references and never exposes mutable access to the CF objects. +unsafe impl Send for RunLoopWakeHandle {} +unsafe impl Sync for RunLoopWakeHandle {} + struct MacosWindow { spec: WindowSpec, state: MacosWindowState, @@ -321,35 +326,41 @@ fn run_loop_wake_handle() -> &'static Mutex> { #[derive(Clone, Copy, Debug, Default, PartialEq)] struct ResizePipeline { - active_resize: Option, - next_resize: Option, + active_resize: Option, + next_resize: Option, } impl ResizePipeline { - fn note_resize(&mut self, viewport: UiSize) -> Option { + fn note_resize(&mut self, configuration: WindowConfigured) -> Option { if self.active_resize.is_some() { - self.next_resize = Some(viewport); + self.next_resize = Some(configuration); return None; } - self.active_resize = Some(viewport); - Some(viewport) + self.active_resize = Some(configuration); + Some(configuration) } - fn ensure_initial_request(&mut self, viewport: UiSize) -> Option { + fn ensure_initial_request( + &mut self, + configuration: WindowConfigured, + ) -> Option { if self.active_resize.is_some() || self.next_resize.is_some() { return None; } - self.active_resize = Some(viewport); - Some(viewport) + self.active_resize = Some(configuration); + Some(configuration) } - fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option { - if self.active_resize != Some(scene_viewport) { + fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option { + let Some(presented_configuration) = self.active_resize else { + return None; + }; + if presented_configuration.actual_inner_size != scene_viewport { return None; } self.active_resize = None; - if self.next_resize == Some(scene_viewport) { + if self.next_resize == Some(presented_configuration) { self.next_resize = None; } @@ -398,6 +409,7 @@ unsafe impl Sync for AppKitSurfaceTarget {} impl HasDisplayHandle for AppKitSurfaceTarget { fn display_handle(&self) -> Result, HandleError> { + assert_main_thread(); let raw = RawDisplayHandle::AppKit(AppKitDisplayHandle::new()); Ok(unsafe { DisplayHandle::borrow_raw(raw) }) } @@ -405,6 +417,7 @@ impl HasDisplayHandle for AppKitSurfaceTarget { impl HasWindowHandle for AppKitSurfaceTarget { fn window_handle(&self) -> Result, HandleError> { + assert_main_thread(); let ptr = NonNull::new(self.view.cast::()).ok_or(HandleError::Unavailable)?; let raw = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)); Ok(unsafe { WindowHandle::borrow_raw(raw) }) @@ -467,8 +480,8 @@ fn install_platform_run_loop_source() -> Option { run_loop.add_source(Some(&source), common_modes); let handle = RunLoopWakeHandle { - run_loop: CFRetained::as_ptr(&run_loop).as_ptr().cast::() as usize, - source: CFRetained::as_ptr(&source).as_ptr().cast::() as usize, + run_loop: run_loop.clone(), + source: source.clone(), }; if let Ok(mut slot) = run_loop_wake_handle().lock() { *slot = Some(handle); @@ -489,13 +502,15 @@ extern "C-unwind" fn platform_run_loop_source_perform(_info: *mut c_void) { } fn signal_platform_run_loop_source() { - let Some(handle) = run_loop_wake_handle().lock().ok().and_then(|slot| *slot) else { + let Some(handle) = run_loop_wake_handle() + .lock() + .ok() + .and_then(|slot| slot.clone()) + else { return; }; - let source = unsafe { &*(handle.source as *const CFRunLoopSource) }; - let run_loop = unsafe { &*(handle.run_loop as *const CFRunLoop) }; - source.signal(); - run_loop.wake_up(); + handle.source.signal(); + handle.run_loop.wake_up(); } fn assert_main_thread() { @@ -574,91 +589,98 @@ fn delegate_state() -> &'static Mutex { DELEGATE_STATE.get_or_init(|| Mutex::new(DelegateState::default())) } +fn lock_delegate_state() -> MutexGuard<'static, DelegateState> { + delegate_state() + .lock() + .expect("macOS delegate event state mutex poisoned") +} + fn queue_delegate_close_request(window: *mut AnyObject) { if window.is_null() { return; } - if let Ok(mut state) = delegate_state().lock() { - state.pending_close_requests.push(window as usize); - } + lock_delegate_state() + .pending_close_requests + .push(window as usize); } fn queue_delegate_window_closed(window: *mut AnyObject) { if window.is_null() { return; } - if let Ok(mut state) = delegate_state().lock() { - state.pending_closed_windows.push(window as usize); - } + lock_delegate_state() + .pending_closed_windows + .push(window as usize); } fn queue_delegate_resize(window: *mut AnyObject, size: UiSize, scale_factor: f32) { if window.is_null() { return; } - if let Ok(mut state) = delegate_state().lock() { - state.pending_resize_events.push(DelegateResizeEvent { + lock_delegate_state() + .pending_resize_events + .push(DelegateResizeEvent { window_ptr: window as usize, size, scale_factor, }); +} + +fn queue_delegate_configuration_from_window(window: *mut AnyObject) { + if window.is_null() { + return; } + let view: *mut AnyObject = unsafe { objc2::msg_send![window, contentView] }; + if view.is_null() { + return; + } + let frame: CGRect = unsafe { objc2::msg_send![view, frame] }; + let scale_factor: f64 = unsafe { objc2::msg_send![window, backingScaleFactor] }; + queue_delegate_resize( + window, + UiSize::new(frame.size.width as f32, frame.size.height as f32), + (scale_factor as f32).max(1.0), + ); } fn queue_delegate_pointer_event(window: *mut AnyObject, event: PointerEvent) { if window.is_null() { return; } - if let Ok(mut state) = delegate_state().lock() { - state - .pending_input_events - .push(DelegateInputEvent::Pointer { - window_ptr: window as usize, - event, - }); - } + lock_delegate_state() + .pending_input_events + .push(DelegateInputEvent::Pointer { + window_ptr: window as usize, + event, + }); } fn queue_delegate_keyboard_event(window: *mut AnyObject, event: KeyboardEvent) { if window.is_null() { return; } - if let Ok(mut state) = delegate_state().lock() { - state - .pending_input_events - .push(DelegateInputEvent::Keyboard { - window_ptr: window as usize, - event, - }); - } + lock_delegate_state() + .pending_input_events + .push(DelegateInputEvent::Keyboard { + window_ptr: window as usize, + event, + }); } fn take_delegate_close_requests() -> Vec { - if let Ok(mut state) = delegate_state().lock() { - return std::mem::take(&mut state.pending_close_requests); - } - Vec::new() + std::mem::take(&mut lock_delegate_state().pending_close_requests) } fn take_delegate_closed_windows() -> Vec { - if let Ok(mut state) = delegate_state().lock() { - return std::mem::take(&mut state.pending_closed_windows); - } - Vec::new() + std::mem::take(&mut lock_delegate_state().pending_closed_windows) } fn take_delegate_resize_events() -> Vec { - if let Ok(mut state) = delegate_state().lock() { - return std::mem::take(&mut state.pending_resize_events); - } - Vec::new() + std::mem::take(&mut lock_delegate_state().pending_resize_events) } fn take_delegate_input_events() -> Vec { - if let Ok(mut state) = delegate_state().lock() { - return std::mem::take(&mut state.pending_input_events); - } - Vec::new() + std::mem::take(&mut lock_delegate_state().pending_input_events) } fn window_delegate_class() -> &'static AnyClass { @@ -696,28 +718,26 @@ fn window_delegate_class() -> &'static AnyClass { notification: *mut AnyObject, ) { let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; - if window.is_null() { - return; - } - let view: *mut AnyObject = unsafe { objc2::msg_send![window, contentView] }; - if view.is_null() { - return; - } - let frame: CGRect = unsafe { objc2::msg_send![view, frame] }; - let scale_factor: f64 = unsafe { objc2::msg_send![window, backingScaleFactor] }; trace!( target: "ruin_ui_platform_macos::resize", window_ptr = window as usize, - width = frame.size.width, - height = frame.size.height, - scale_factor, "windowDidResize:" ); - queue_delegate_resize( - window, - UiSize::new(frame.size.width as f32, frame.size.height as f32), - (scale_factor as f32).max(1.0), + queue_delegate_configuration_from_window(window); + signal_platform_run_loop_source(); + } + extern "C-unwind" fn did_change_screen( + _this: &AnyObject, + _cmd: Sel, + notification: *mut AnyObject, + ) { + let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; + trace!( + target: "ruin_ui_platform_macos::resize", + window_ptr = window as usize, + "windowDidChangeScreen:" ); + queue_delegate_configuration_from_window(window); signal_platform_run_loop_source(); } extern "C-unwind" fn did_change_occlusion_state( @@ -782,6 +802,8 @@ fn window_delegate_class() -> &'static AnyClass { builder.add_method::(sel!(windowWillClose:), method); let method: extern "C-unwind" fn(_, _, _) = did_resize; builder.add_method::(sel!(windowDidResize:), method); + let method: extern "C-unwind" fn(_, _, _) = did_change_screen; + builder.add_method::(sel!(windowDidChangeScreen:), method); let method: extern "C-unwind" fn(_, _, _) = did_change_occlusion_state; builder.add_method::(sel!(windowDidChangeOcclusionState:), method); let method: extern "C-unwind" fn(_, _, _) = did_become_key; @@ -817,6 +839,17 @@ fn content_view_class() -> &'static AnyClass { Bool::YES } + extern "C-unwind" fn did_change_backing_properties(this: &AnyObject, _cmd: Sel) { + let window: *mut AnyObject = unsafe { objc2::msg_send![this, window] }; + trace!( + target: "ruin_ui_platform_macos::resize", + window_ptr = window as usize, + "viewDidChangeBackingProperties" + ); + queue_delegate_configuration_from_window(window); + signal_platform_run_loop_source(); + } + extern "C-unwind" fn mouse_moved(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { queue_pointer_from_view_event(this, event, PointerEventKind::Move); signal_platform_run_loop_source(); @@ -923,6 +956,8 @@ fn content_view_class() -> &'static AnyClass { builder.add_method::(sel!(acceptsFirstResponder), method); let method: extern "C-unwind" fn(_, _) -> _ = is_flipped; builder.add_method::(sel!(isFlipped), method); + let method: extern "C-unwind" fn(_, _) = did_change_backing_properties; + builder.add_method::(sel!(viewDidChangeBackingProperties), method); let method: extern "C-unwind" fn(_, _, _) = mouse_moved; builder.add_method::(sel!(mouseMoved:), method); let method: extern "C-unwind" fn(_, _, _) = mouse_moved; @@ -1167,7 +1202,7 @@ fn handle_create_window(state: &Rc>, window_id: WindowId, sp window.configuration.visible = visible; let _ = window .resize_pipeline - .ensure_initial_request(window.configuration.actual_inner_size); + .ensure_initial_request(window.configuration); sync_visibility(window); apply_cursor_icon(window.cursor_icon); emit_opened = true; @@ -1265,13 +1300,15 @@ fn handle_update_window( resize_native_window(native, size); } repaint_retained_scene_for_resize(window_id, window); - if let Some(active_resize) = window.resize_pipeline.note_resize(size) { + if let Some(active_resize) = + window.resize_pipeline.note_resize(window.configuration) + { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, - width = active_resize.width, - height = active_resize.height, - scale_factor = window.configuration.scale_factor, + width = active_resize.actual_inner_size.width, + height = active_resize.actual_inner_size.height, + scale_factor = active_resize.scale_factor, "emitting Configured from window update resize" ); emit_configured = Some(window.configuration); @@ -1279,10 +1316,10 @@ fn handle_update_window( trace!( target: "ruin_ui_platform_macos::resize", ?window_id, - active_width = window.resize_pipeline.active_resize.map(|s| s.width), - active_height = window.resize_pipeline.active_resize.map(|s| s.height), - next_width = window.resize_pipeline.next_resize.map(|s| s.width), - next_height = window.resize_pipeline.next_resize.map(|s| s.height), + active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width), + active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height), + next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width), + next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height), "staged next resize from window update while another layout is in flight" ); } @@ -1371,10 +1408,10 @@ fn try_present_scene( scene_height = scene_viewport.height, current_width = current_viewport.width, current_height = current_viewport.height, - active_width = window.resize_pipeline.active_resize.map(|s| s.width), - active_height = window.resize_pipeline.active_resize.map(|s| s.height), - next_width = window.resize_pipeline.next_resize.map(|s| s.width), - next_height = window.resize_pipeline.next_resize.map(|s| s.height), + active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width), + active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height), + next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width), + next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height), "handle_replace_scene start" ); let render_succeeded = render_scene_to_current_drawable(window_id, window, scene); @@ -1391,11 +1428,12 @@ fn try_present_scene( ?window_id, scene_width = scene_viewport.width, scene_height = scene_viewport.height, - next_width = next_resize.width, - next_height = next_resize.height, + next_width = next_resize.actual_inner_size.width, + next_height = next_resize.actual_inner_size.height, + next_scale = next_resize.scale_factor, "presented active resize scene; promoting staged next resize" ); - emit_resize_request(window_id, window, next_resize, followup_events); + emit_resize_request(window_id, next_resize, followup_events); } Some((scene_version, item_count, window.configuration.visible)) } @@ -1495,8 +1533,8 @@ fn render_scene_to_current_drawable( ?window_id, scene_width = scene_viewport.width, scene_height = scene_viewport.height, - active_width = window.resize_pipeline.active_resize.map(|s| s.width), - active_height = window.resize_pipeline.active_resize.map(|s| s.height), + active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width), + active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height), current_width = current_viewport.width, current_height = current_viewport.height, render_succeeded, @@ -1754,42 +1792,47 @@ fn note_window_resize( viewport: UiSize, pending: &mut Vec, ) { - if let Some(active_resize) = window.resize_pipeline.note_resize(viewport) { - emit_resize_request(window_id, window, active_resize, pending); + debug_assert_eq!(window.configuration.actual_inner_size, viewport); + if let Some(active_resize) = window.resize_pipeline.note_resize(window.configuration) { + emit_resize_request(window_id, active_resize, pending); } else { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, - active_width = window.resize_pipeline.active_resize.map(|s| s.width), - active_height = window.resize_pipeline.active_resize.map(|s| s.height), - next_width = window.resize_pipeline.next_resize.map(|s| s.width), - next_height = window.resize_pipeline.next_resize.map(|s| s.height), - "staged next resize while another layout is in flight" + active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width), + active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height), + active_scale = window.resize_pipeline.active_resize.map(|s| s.scale_factor), + next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width), + next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height), + next_scale = window.resize_pipeline.next_resize.map(|s| s.scale_factor), + "staged next configuration while another layout is in flight" ); } } fn emit_resize_request( window_id: WindowId, - window: &mut MacosWindow, - viewport: UiSize, + configuration: WindowConfigured, pending: &mut Vec, ) { pending.push(PlatformEvent::Configured { window_id, - configuration: window.configuration, + configuration, }); trace!( target: "ruin_ui_platform_macos::resize", ?window_id, - width = viewport.width, - height = viewport.height, - scale_factor = window.configuration.scale_factor, - "emitting Configured from delegate resize" + width = configuration.actual_inner_size.width, + height = configuration.actual_inner_size.height, + scale_factor = configuration.scale_factor, + "emitting Configured from platform configuration change" ); } -fn finish_presented_resize(window: &mut MacosWindow, presented: UiSize) -> Option { +fn finish_presented_resize( + window: &mut MacosWindow, + presented: UiSize, +) -> Option { window.resize_pipeline.note_presented_scene(presented) } @@ -1926,16 +1969,19 @@ fn collect_window_state_events( && let Some(scene) = window.latest_scene.clone() && (became_visible || !window.state.has_presented_frame - || window.resize_pipeline.active_resize == Some(scene.logical_size)) + || window + .resize_pipeline + .active_resize + .is_some_and(|configuration| configuration.actual_inner_size == scene.logical_size)) { let _ = try_present_scene(window_id, window, &scene, pending); } else if is_visible && !window.state.has_presented_frame && let Some(initial_viewport) = window .resize_pipeline - .ensure_initial_request(window.configuration.actual_inner_size) + .ensure_initial_request(window.configuration) { - emit_resize_request(window_id, window, initial_viewport, pending); + emit_resize_request(window_id, initial_viewport, pending); } } @@ -2067,38 +2113,51 @@ mod tests { use super::{ MacosWindowState, PresentationFlushPolicy, ResizePipeline, presentation_flush_policy, }; - use ruin_ui::{UiSize, WindowLifecycle}; + use ruin_ui::{UiSize, WindowConfigured, WindowLifecycle}; + + fn configuration(width: f32, height: f32, scale_factor: f32) -> WindowConfigured { + WindowConfigured { + actual_inner_size: UiSize::new(width, height), + scale_factor, + visible: true, + maximized: false, + fullscreen: false, + } + } #[test] fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() { let mut pipeline = ResizePipeline::default(); + let first = configuration(640.0, 480.0, 1.0); + let second = configuration(800.0, 600.0, 1.0); + let third = configuration(1024.0, 768.0, 1.0); - assert_eq!( - pipeline.note_resize(UiSize::new(640.0, 480.0)), - Some(UiSize::new(640.0, 480.0)) - ); - assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0))); + assert_eq!(pipeline.note_resize(first), Some(first)); + assert_eq!(pipeline.active_resize, Some(first)); assert_eq!(pipeline.next_resize, None); - assert_eq!(pipeline.note_resize(UiSize::new(800.0, 600.0)), None); - assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0))); - assert_eq!(pipeline.next_resize, Some(UiSize::new(800.0, 600.0))); + assert_eq!(pipeline.note_resize(second), None); + assert_eq!(pipeline.active_resize, Some(first)); + assert_eq!(pipeline.next_resize, Some(second)); - assert_eq!(pipeline.note_resize(UiSize::new(1024.0, 768.0)), None); - assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0))); - assert_eq!(pipeline.next_resize, Some(UiSize::new(1024.0, 768.0))); + assert_eq!(pipeline.note_resize(third), None); + assert_eq!(pipeline.active_resize, Some(first)); + assert_eq!(pipeline.next_resize, Some(third)); } #[test] fn resize_pipeline_promotes_next_resize_after_presenting_active_scene() { let mut pipeline = ResizePipeline::default(); - let first = UiSize::new(640.0, 480.0); - let second = UiSize::new(1024.0, 768.0); + let first = configuration(640.0, 480.0, 1.0); + let second = configuration(1024.0, 768.0, 1.0); assert_eq!(pipeline.note_resize(first), Some(first)); assert_eq!(pipeline.note_resize(second), None); - assert_eq!(pipeline.note_presented_scene(first), Some(second)); + assert_eq!( + pipeline.note_presented_scene(first.actual_inner_size), + Some(second) + ); assert_eq!(pipeline.active_resize, Some(second)); assert_eq!(pipeline.next_resize, None); } @@ -2106,21 +2165,41 @@ mod tests { #[test] fn resize_pipeline_drops_duplicate_staged_resize_when_present_already_satisfies_it() { let mut pipeline = ResizePipeline::default(); - let viewport = UiSize::new(800.0, 600.0); + let viewport = configuration(800.0, 600.0, 1.0); assert_eq!(pipeline.note_resize(viewport), Some(viewport)); assert_eq!(pipeline.note_resize(viewport), None); - assert_eq!(pipeline.note_presented_scene(viewport), None); + assert_eq!( + pipeline.note_presented_scene(viewport.actual_inner_size), + None + ); assert_eq!(pipeline.active_resize, None); assert_eq!(pipeline.next_resize, None); } + #[test] + fn resize_pipeline_preserves_same_size_scale_changes() { + let mut pipeline = ResizePipeline::default(); + let one_x = configuration(800.0, 600.0, 1.0); + let two_x = configuration(800.0, 600.0, 2.0); + + assert_eq!(pipeline.note_resize(one_x), Some(one_x)); + assert_eq!(pipeline.note_resize(two_x), None); + + assert_eq!( + pipeline.note_presented_scene(one_x.actual_inner_size), + Some(two_x) + ); + assert_eq!(pipeline.active_resize, Some(two_x)); + assert_eq!(pipeline.next_resize, None); + } + #[test] fn resize_pipeline_ignores_presents_for_non_active_scene_sizes() { let mut pipeline = ResizePipeline::default(); - let active = UiSize::new(640.0, 480.0); - let next = UiSize::new(900.0, 700.0); + let active = configuration(640.0, 480.0, 1.0); + let next = configuration(900.0, 700.0, 1.0); assert_eq!(pipeline.note_resize(active), Some(active)); assert_eq!(pipeline.note_resize(next), None);