//! macOS platform backend entrypoints. //! //! Native backend implemented on top of Cocoa/AppKit runtime messaging. use std::cell::{Cell, RefCell}; 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 objc2::rc::Retained; use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, Sel}; use objc2::sel; use objc2_core_foundation::{ CFRetained, CFRunLoop, CFRunLoopSource, CFRunLoopSourceContext, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes, }; use objc2_foundation::{NSString, ns_string}; use objc2_quartz_core::{CAMetalLayer, kCAGravityTopLeft}; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle, }; use raw_window_metal::Layer as MetalLayer; use ruin_ui::{ CursorIcon, KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PlatformEndpoint, PlatformEvent, PlatformRequest, PlatformRuntime, Point, PointerButton, PointerEvent, PointerEventKind, SceneSnapshot, UiRuntime, UiSize, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate, set_command_wake_hook, }; use ruin_ui_renderer_wgpu::{RenderError, WgpuSceneRenderer}; use tracing::{debug, trace}; #[link(name = "AppKit", kind = "framework")] unsafe extern "C" {} const NS_APPLICATION_ACTIVATION_POLICY_REGULAR: isize = 0; const NS_BACKING_STORE_BUFFERED: usize = 2; const NS_WINDOW_STYLE_TITLED: usize = 1 << 0; const NS_WINDOW_STYLE_CLOSABLE: usize = 1 << 1; const NS_WINDOW_STYLE_MINIATURIZABLE: usize = 1 << 2; const NS_WINDOW_STYLE_RESIZABLE: usize = 1 << 3; const NS_VIEW_WIDTH_SIZABLE: usize = 1 << 1; const NS_VIEW_HEIGHT_SIZABLE: usize = 1 << 4; const NS_VIEW_LAYER_CONTENTS_REDRAW_DURING_VIEW_RESIZE: usize = 2; const NS_VIEW_LAYER_CONTENTS_PLACEMENT_TOP_LEFT: usize = 11; const NSEVENT_MODIFIER_FLAG_SHIFT: usize = 1 << 17; const NSEVENT_MODIFIER_FLAG_CONTROL: usize = 1 << 18; const NSEVENT_MODIFIER_FLAG_OPTION: usize = 1 << 19; const NSEVENT_MODIFIER_FLAG_COMMAND: usize = 1 << 20; struct MacosState { app: *mut AnyObject, commands: ruin_runtime::channel::mpsc::Receiver, endpoint_events: ruin_runtime::channel::mpsc::UnboundedSender, _run_loop_source: RunLoopSourceRegistration, shutdown_requested: bool, windows: BTreeMap, } struct RunLoopSourceRegistration { _run_loop: CFRetained, _source: CFRetained, } #[derive(Clone, Copy)] struct RunLoopWakeHandle { run_loop: usize, source: usize, } struct MacosWindow { spec: WindowSpec, lifecycle: WindowLifecycle, configuration: WindowConfigured, latest_scene: Option, clipboard_text: Option, desired_visible: bool, cursor_icon: CursorIcon, close_requested_sent: bool, close_in_progress: bool, has_presented_frame: bool, resize_pipeline: ResizePipeline, native: Option, } struct NativeWindow { window: *mut AnyObject, #[allow(dead_code)] view: *mut AnyObject, delegate: *mut AnyObject, _metal_layer: Retained, renderer: Option, } #[derive(Default)] struct DelegateState { pending_close_requests: Vec, pending_closed_windows: Vec, pending_resize_events: Vec, pending_input_events: Vec, } #[derive(Clone, Copy)] struct DelegateResizeEvent { window_ptr: usize, size: UiSize, scale_factor: f32, } enum DelegateInputEvent { Pointer { window_ptr: usize, event: PointerEvent, }, Keyboard { window_ptr: usize, event: KeyboardEvent, }, } thread_local! { static ACTIVE_MACOS_STATE: RefCell>>> = const { RefCell::new(None) }; static APPKIT_PUMP_ACTIVE: Cell = const { Cell::new(false) }; static IMMEDIATE_DELEGATE_FLUSH_ACTIVE: Cell = const { Cell::new(false) }; } fn run_loop_wake_handle() -> &'static Mutex> { static HANDLE: OnceLock>> = OnceLock::new(); HANDLE.get_or_init(|| Mutex::new(None)) } #[derive(Clone, Copy, Debug, Default, PartialEq)] struct ResizePipeline { active_resize: Option, next_resize: Option, } impl ResizePipeline { fn note_resize(&mut self, viewport: UiSize) -> Option { if self.active_resize.is_some() { self.next_resize = Some(viewport); return None; } self.active_resize = Some(viewport); Some(viewport) } fn ensure_initial_request(&mut self, viewport: UiSize) -> Option { if self.active_resize.is_some() || self.next_resize.is_some() { return None; } self.active_resize = Some(viewport); Some(viewport) } fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option { if self.active_resize != Some(scene_viewport) { return None; } self.active_resize = None; if self.next_resize == Some(scene_viewport) { self.next_resize = None; } let Some(next_resize) = self.next_resize.take() else { return None; }; self.active_resize = Some(next_resize); Some(next_resize) } } impl MacosWindow { fn new(spec: WindowSpec) -> Self { let desired_visible = spec.visible; let actual_inner_size = spec .requested_inner_size .unwrap_or_else(|| UiSize::new(960.0, 640.0)); Self { spec, lifecycle: WindowLifecycle::LogicalClosed, configuration: WindowConfigured { actual_inner_size, scale_factor: 1.0, visible: false, maximized: false, fullscreen: false, }, latest_scene: None, clipboard_text: None, desired_visible, cursor_icon: CursorIcon::Default, close_requested_sent: false, close_in_progress: false, has_presented_frame: false, resize_pipeline: ResizePipeline::default(), native: None, } } } #[derive(Clone)] struct AppKitSurfaceTarget { view: *mut AnyObject, } // NSView pointer is only used on the owning thread by the renderer; this is // required by wgpu surface creation bounds. unsafe impl Send for AppKitSurfaceTarget {} unsafe impl Sync for AppKitSurfaceTarget {} impl HasDisplayHandle for AppKitSurfaceTarget { fn display_handle(&self) -> Result, HandleError> { let raw = RawDisplayHandle::AppKit(AppKitDisplayHandle::new()); Ok(unsafe { DisplayHandle::borrow_raw(raw) }) } } impl HasWindowHandle for AppKitSurfaceTarget { fn window_handle(&self) -> Result, HandleError> { let ptr = NonNull::new(self.view.cast::()).ok_or(HandleError::Unavailable)?; let raw = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)); Ok(unsafe { WindowHandle::borrow_raw(raw) }) } } /// Starts the macOS platform runtime. /// /// Must be called on the process main thread. pub fn start_macos_platform() -> PlatformRuntime { assert_main_thread(); let app = ensure_ns_application(); let run_loop_source = install_platform_run_loop_source().expect("macOS run-loop source must install"); PlatformRuntime::custom_local(|endpoint: PlatformEndpoint| { let state = Rc::new(RefCell::new(MacosState { app, commands: endpoint.commands, endpoint_events: endpoint.events.clone(), _run_loop_source: run_loop_source, shutdown_requested: false, windows: BTreeMap::new(), })); ACTIVE_MACOS_STATE.with(|slot| { *slot.borrow_mut() = Some(Rc::clone(&state)); }); }) } /// Starts a UI runtime backed by the macOS platform runtime. pub fn start_macos_ui() -> UiRuntime { UiRuntime::from_platform(start_macos_platform()) } pub fn run_macos_app_event_loop() { assert_main_thread(); let app = ensure_ns_application(); unsafe { let _: () = objc2::msg_send![app, run]; } } fn install_platform_run_loop_source() -> Option { let run_loop = CFRunLoop::main()?; let mut context = CFRunLoopSourceContext { version: 0, info: std::ptr::null_mut(), retain: None, release: None, copyDescription: None, equal: None, hash: None, schedule: None, cancel: None, perform: Some(platform_run_loop_source_perform), }; let source = unsafe { CFRunLoopSource::new(None, 0, &mut context) }?; let common_modes = unsafe { kCFRunLoopCommonModes }; 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, }; if let Ok(mut slot) = run_loop_wake_handle().lock() { *slot = Some(handle); } set_command_wake_hook(Some(Arc::new(signal_platform_run_loop_source))); Some(RunLoopSourceRegistration { _run_loop: run_loop, _source: source, }) } extern "C-unwind" fn platform_run_loop_source_perform(_info: *mut c_void) { let Some(state) = ACTIVE_MACOS_STATE.with(|slot| slot.borrow().as_ref().cloned()) else { return; }; drain_macos_run_loop_work(&state); } fn signal_platform_run_loop_source() { let Some(handle) = run_loop_wake_handle().lock().ok().and_then(|slot| *slot) 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(); } fn assert_main_thread() { let is_main = unsafe { libc::pthread_main_np() == 1 }; assert!( is_main, "ruin_ui_platform_macos must be started on the process main thread" ); } fn ensure_ns_application() -> *mut AnyObject { let app: *mut AnyObject = unsafe { objc2::msg_send![objc2::class!(NSApplication), sharedApplication] }; install_app_delegate(app); unsafe { let _: bool = objc2::msg_send![app, setActivationPolicy: NS_APPLICATION_ACTIVATION_POLICY_REGULAR]; let _: () = objc2::msg_send![app, finishLaunching]; let _: () = objc2::msg_send![app, activateIgnoringOtherApps: true]; } app } fn app_delegate_class() -> &'static AnyClass { static REGISTER: Once = Once::new(); static CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); REGISTER.call_once(|| { extern "C-unwind" fn terminate_after_last_window_closed( _this: &AnyObject, _cmd: Sel, _sender: *mut AnyObject, ) -> Bool { Bool::YES } let class_name = CString::new("RuinAppDelegate").expect("class name is valid"); if let Some(mut builder) = ClassBuilder::new(&class_name, objc2::class!(NSObject)) { unsafe { let method: extern "C-unwind" fn(_, _, _) -> _ = terminate_after_last_window_closed; builder.add_method::( sel!(applicationShouldTerminateAfterLastWindowClosed:), method, ); } let _ = CLASS.set(builder.register()); } else { let existing = AnyClass::get(class_name.as_c_str()).expect("RuinAppDelegate class must exist"); let _ = CLASS.set(existing); } }); CLASS .get() .expect("RuinAppDelegate class registration must succeed") } fn install_app_delegate(app: *mut AnyObject) { static INSTANCE: OnceLock = OnceLock::new(); let delegate = *INSTANCE.get_or_init(|| { let class = app_delegate_class(); let delegate_obj: *mut AnyObject = unsafe { objc2::msg_send![class, new] }; delegate_obj as usize }) as *mut AnyObject; if delegate.is_null() { return; } unsafe { let _: () = objc2::msg_send![app, setDelegate: delegate]; } } fn delegate_state() -> &'static Mutex { static DELEGATE_STATE: OnceLock> = OnceLock::new(); DELEGATE_STATE.get_or_init(|| Mutex::new(DelegateState::default())) } 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); } } 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); } } 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 { window_ptr: window as usize, size, scale_factor, }); } } 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, }); } } 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, }); } } 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() } 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() } 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() } 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() } fn window_delegate_class() -> &'static AnyClass { static REGISTER: Once = Once::new(); static CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); REGISTER.call_once(|| { extern "C-unwind" fn should_close( _this: &AnyObject, _cmd: Sel, sender: *mut AnyObject, ) -> Bool { debug!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = sender as usize, "windowShouldClose:" ); queue_delegate_close_request(sender); flush_delegate_events_immediately(); Bool::YES } extern "C-unwind" fn did_close(_this: &AnyObject, _cmd: Sel, notification: *mut AnyObject) { let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; debug!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = window as usize, "windowWillClose:" ); queue_delegate_window_closed(window); flush_delegate_events_immediately(); } extern "C-unwind" fn did_resize( _this: &AnyObject, _cmd: Sel, 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), ); flush_delegate_events_immediately(); } extern "C-unwind" fn did_change_occlusion_state( _this: &AnyObject, _cmd: Sel, notification: *mut AnyObject, ) { let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; trace!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = window as usize, "windowDidChangeOcclusionState:" ); signal_platform_run_loop_source(); } extern "C-unwind" fn did_become_key( _this: &AnyObject, _cmd: Sel, notification: *mut AnyObject, ) { let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; trace!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = window as usize, "windowDidBecomeKey:" ); signal_platform_run_loop_source(); } extern "C-unwind" fn did_miniaturize( _this: &AnyObject, _cmd: Sel, notification: *mut AnyObject, ) { let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; trace!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = window as usize, "windowDidMiniaturize:" ); signal_platform_run_loop_source(); } extern "C-unwind" fn did_deminiaturize( _this: &AnyObject, _cmd: Sel, notification: *mut AnyObject, ) { let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] }; trace!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = window as usize, "windowDidDeminiaturize:" ); signal_platform_run_loop_source(); } let class_name = CString::new("RuinWindowDelegate").expect("class name is valid"); if let Some(mut builder) = ClassBuilder::new(&class_name, objc2::class!(NSObject)) { unsafe { let method: extern "C-unwind" fn(_, _, _) -> _ = should_close; builder.add_method::(sel!(windowShouldClose:), method); let method: extern "C-unwind" fn(_, _, _) = did_close; 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_occlusion_state; builder.add_method::(sel!(windowDidChangeOcclusionState:), method); let method: extern "C-unwind" fn(_, _, _) = did_become_key; builder.add_method::(sel!(windowDidBecomeKey:), method); let method: extern "C-unwind" fn(_, _, _) = did_miniaturize; builder.add_method::(sel!(windowDidMiniaturize:), method); let method: extern "C-unwind" fn(_, _, _) = did_deminiaturize; builder.add_method::(sel!(windowDidDeminiaturize:), method); } let _ = CLASS.set(builder.register()); } else { let existing = AnyClass::get(class_name.as_c_str()) .expect("RuinWindowDelegate class must be available"); let _ = CLASS.set(existing); } }); CLASS .get() .expect("RuinWindowDelegate class registration must succeed") } fn content_view_class() -> &'static AnyClass { static REGISTER: Once = Once::new(); static CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); REGISTER.call_once(|| { extern "C-unwind" fn accepts_first_responder(_this: &AnyObject, _cmd: Sel) -> Bool { Bool::YES } extern "C-unwind" fn is_flipped(_this: &AnyObject, _cmd: Sel) -> Bool { Bool::YES } extern "C-unwind" fn mouse_moved(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { queue_pointer_from_view_event(this, event, PointerEventKind::Move); flush_delegate_events_immediately(); } extern "C-unwind" fn left_mouse_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { queue_pointer_from_view_event( this, event, PointerEventKind::Down { button: PointerButton::Primary, }, ); flush_delegate_events_immediately(); } extern "C-unwind" fn left_mouse_up(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { queue_pointer_from_view_event( this, event, PointerEventKind::Up { button: PointerButton::Primary, }, ); flush_delegate_events_immediately(); } extern "C-unwind" fn left_mouse_dragged( this: &AnyObject, _cmd: Sel, event: *mut AnyObject, ) { queue_pointer_from_view_event(this, event, PointerEventKind::Move); flush_delegate_events_immediately(); } extern "C-unwind" fn other_mouse_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { let button_number: usize = unsafe { objc2::msg_send![event, buttonNumber] }; if button_number == 2 { queue_pointer_from_view_event( this, event, PointerEventKind::Down { button: PointerButton::Middle, }, ); flush_delegate_events_immediately(); } } extern "C-unwind" fn other_mouse_up(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { let button_number: usize = unsafe { objc2::msg_send![event, buttonNumber] }; if button_number == 2 { queue_pointer_from_view_event( this, event, PointerEventKind::Up { button: PointerButton::Middle, }, ); flush_delegate_events_immediately(); } } extern "C-unwind" fn other_mouse_dragged( this: &AnyObject, _cmd: Sel, event: *mut AnyObject, ) { let button_number: usize = unsafe { objc2::msg_send![event, buttonNumber] }; if button_number == 2 { queue_pointer_from_view_event(this, event, PointerEventKind::Move); flush_delegate_events_immediately(); } } extern "C-unwind" fn scroll_wheel(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { let delta_x: f64 = unsafe { objc2::msg_send![event, scrollingDeltaX] }; let delta_y: f64 = unsafe { objc2::msg_send![event, scrollingDeltaY] }; queue_pointer_from_view_event( this, event, PointerEventKind::Scroll { delta: Point::new(delta_x as f32, -(delta_y as f32)), }, ); flush_delegate_events_immediately(); } extern "C-unwind" fn key_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { queue_keyboard_from_view_event(this, event, KeyboardEventKind::Pressed); flush_delegate_events_immediately(); } extern "C-unwind" fn key_up(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) { queue_keyboard_from_view_event(this, event, KeyboardEventKind::Released); flush_delegate_events_immediately(); } let class_name = CString::new("RuinContentView").expect("class name is valid"); if let Some(mut builder) = ClassBuilder::new(&class_name, objc2::class!(NSView)) { unsafe { let method: extern "C-unwind" fn(_, _) -> _ = accepts_first_responder; 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(_, _, _) = mouse_moved; builder.add_method::(sel!(mouseMoved:), method); let method: extern "C-unwind" fn(_, _, _) = mouse_moved; builder.add_method::(sel!(rightMouseDragged:), method); let method: extern "C-unwind" fn(_, _, _) = left_mouse_down; builder.add_method::(sel!(mouseDown:), method); let method: extern "C-unwind" fn(_, _, _) = left_mouse_up; builder.add_method::(sel!(mouseUp:), method); let method: extern "C-unwind" fn(_, _, _) = left_mouse_dragged; builder.add_method::(sel!(mouseDragged:), method); let method: extern "C-unwind" fn(_, _, _) = other_mouse_down; builder.add_method::(sel!(otherMouseDown:), method); let method: extern "C-unwind" fn(_, _, _) = other_mouse_up; builder.add_method::(sel!(otherMouseUp:), method); let method: extern "C-unwind" fn(_, _, _) = other_mouse_dragged; builder.add_method::(sel!(otherMouseDragged:), method); let method: extern "C-unwind" fn(_, _, _) = scroll_wheel; builder.add_method::(sel!(scrollWheel:), method); let method: extern "C-unwind" fn(_, _, _) = key_down; builder.add_method::(sel!(keyDown:), method); let method: extern "C-unwind" fn(_, _, _) = key_up; builder.add_method::(sel!(keyUp:), method); } let _ = CLASS.set(builder.register()); } else { let existing = AnyClass::get(class_name.as_c_str()).expect("RuinContentView class must exist"); let _ = CLASS.set(existing); } }); CLASS .get() .expect("RuinContentView class registration must succeed") } fn event_window_for_view(this: &AnyObject, event: *mut AnyObject) -> *mut AnyObject { let mut window: *mut AnyObject = unsafe { objc2::msg_send![event, window] }; if window.is_null() { window = unsafe { objc2::msg_send![this, window] }; } window } fn pointer_position_in_view(this: &AnyObject, event: *mut AnyObject) -> Point { let location: CGPoint = unsafe { objc2::msg_send![event, locationInWindow] }; let frame: CGRect = unsafe { objc2::msg_send![this, frame] }; Point::new( location.x as f32, frame.size.height as f32 - location.y as f32, ) } fn queue_pointer_from_view_event(this: &AnyObject, event: *mut AnyObject, kind: PointerEventKind) { let window = event_window_for_view(this, event); let position = pointer_position_in_view(this, event); queue_delegate_pointer_event(window, PointerEvent::new(0, position, kind)); } fn queue_keyboard_from_view_event( this: &AnyObject, event: *mut AnyObject, kind: KeyboardEventKind, ) { let window = event_window_for_view(this, event); let keycode: u16 = unsafe { objc2::msg_send![event, keyCode] }; let text = if kind == KeyboardEventKind::Pressed { event_characters(event) } else { None }; let key = keyboard_key(keycode, text.as_deref()); queue_delegate_keyboard_event( window, KeyboardEvent::new( u32::from(keycode), kind, key, keyboard_modifiers(event), text, ), ); } fn flush_pending_events( sender: &ruin_runtime::channel::mpsc::UnboundedSender, pending: &mut Vec, ) { for event in pending.drain(..) { let _ = sender.send(event); } } struct AppKitPumpGuard; impl Drop for AppKitPumpGuard { fn drop(&mut self) { APPKIT_PUMP_ACTIVE.with(|flag| flag.set(false)); } } fn try_enter_appkit_pump() -> Option { APPKIT_PUMP_ACTIVE.with(|flag| { if flag.replace(true) { None } else { Some(AppKitPumpGuard) } }) } struct ImmediateDelegateFlushGuard; impl Drop for ImmediateDelegateFlushGuard { fn drop(&mut self) { IMMEDIATE_DELEGATE_FLUSH_ACTIVE.with(|flag| flag.set(false)); } } fn flush_delegate_events_immediately() { let Some(_guard) = IMMEDIATE_DELEGATE_FLUSH_ACTIVE.with(|flag| { if flag.replace(true) { None } else { Some(ImmediateDelegateFlushGuard) } }) else { return; }; let Some(state) = ACTIVE_MACOS_STATE.with(|slot| slot.borrow().as_ref().cloned()) else { return; }; let mut pending = Vec::new(); { let Ok(mut state_ref) = state.try_borrow_mut() else { return; }; apply_delegate_events(&mut state_ref, &mut pending); } if pending.is_empty() { return; } trace!( target: "ruin_ui_platform_macos::events", count = pending.len(), "flushing delegate events immediately during AppKit callback" ); let sender = state.borrow().endpoint_events.clone(); flush_pending_events(&sender, &mut pending); } fn drain_macos_run_loop_work(state: &Rc>) { let Some(_guard) = try_enter_appkit_pump() else { return; }; let (shutdown_requested, sender) = { let Ok(state_ref) = state.try_borrow() else { return; }; ( state_ref.shutdown_requested, state_ref.endpoint_events.clone(), ) }; if shutdown_requested { return; } let mut pending = Vec::new(); let _ = drain_platform_commands(state); { let Ok(mut state_ref) = state.try_borrow_mut() else { return; }; unsafe { let _: () = objc2::msg_send![state_ref.app, updateWindows]; } for (window_id, window) in &mut state_ref.windows { collect_window_state_events(*window_id, window, &mut pending); } apply_delegate_events(&mut state_ref, &mut pending); } flush_pending_events(&sender, &mut pending); } fn take_next_platform_command(state: &Rc>) -> Option { let mut state_ref = state.borrow_mut(); if state_ref.shutdown_requested { return None; } state_ref.commands.try_recv().ok() } fn drain_platform_commands(state: &Rc>) -> bool { let mut handled_any = false; while let Some(command) = take_next_platform_command(state) { handled_any = true; match command { PlatformRequest::CreateWindow { window_id, spec } => { handle_create_window(state, window_id, spec); } PlatformRequest::UpdateWindow { window_id, update } => { handle_update_window(state, window_id, update); } PlatformRequest::ReplaceScene { window_id, scene } => { handle_replace_scene(state, window_id, scene); } PlatformRequest::SetClipboardText { window_id, text } => { if let Some(window) = state.borrow_mut().windows.get_mut(&window_id) { window.clipboard_text = Some(text); } } PlatformRequest::RequestClipboardText { window_id } => { let text = state .borrow() .windows .get(&window_id) .and_then(|window| window.clipboard_text.clone()) .unwrap_or_default(); emit(state, PlatformEvent::ClipboardText { window_id, text }); } PlatformRequest::SetCursorIcon { window_id, cursor } => { if let Some(window) = state.borrow_mut().windows.get_mut(&window_id) { window.cursor_icon = cursor; apply_cursor_icon(cursor); } } PlatformRequest::EmitCloseRequested { window_id } => { emit(state, PlatformEvent::CloseRequested { window_id }); } PlatformRequest::EmitPointerEvent { window_id, event } => { emit(state, PlatformEvent::Pointer { window_id, event }); } PlatformRequest::EmitKeyboardEvent { window_id, event } => { emit(state, PlatformEvent::Keyboard { window_id, event }); } PlatformRequest::EmitWake { window_id, token } => { emit(state, PlatformEvent::Wake { window_id, token }); } #[cfg(target_os = "linux")] PlatformRequest::SetPrimarySelectionText { .. } => {} #[cfg(target_os = "linux")] PlatformRequest::RequestPrimarySelectionText { .. } => {} PlatformRequest::Shutdown => { let app = { let mut state_ref = state.borrow_mut(); state_ref.shutdown_requested = true; state_ref.app }; unsafe { let _: () = objc2::msg_send![app, stop: std::ptr::null_mut::()]; } if let Ok(mut slot) = run_loop_wake_handle().lock() { *slot = None; } set_command_wake_hook(None); break; } } } handled_any } fn handle_create_window(state: &Rc>, window_id: WindowId, spec: WindowSpec) { let open = spec.open; let visible = spec.visible; let mut emit_opened = false; let mut emit_configured = None; let mut emit_visibility = None; { let mut state_ref = state.borrow_mut(); state_ref.windows.insert(window_id, MacosWindow::new(spec)); if open { let app = state_ref.app; if let Some(window) = state_ref.windows.get_mut(&window_id) { ensure_native_window(window, app); sync_window_metrics(window); window.lifecycle = if visible { WindowLifecycle::OpenVisible } else { WindowLifecycle::OpenHidden }; window.configuration.visible = visible; window.desired_visible = visible; window.close_requested_sent = false; window.close_in_progress = false; let _ = window .resize_pipeline .ensure_initial_request(window.configuration.actual_inner_size); sync_visibility(window); apply_cursor_icon(window.cursor_icon); emit_opened = true; emit_configured = Some(window.configuration); emit_visibility = Some(visible); } } } if emit_opened { emit(state, PlatformEvent::Opened { window_id }); } if let Some(configuration) = emit_configured { emit( state, PlatformEvent::Configured { window_id, configuration, }, ); } if let Some(visible) = emit_visibility { emit( state, PlatformEvent::VisibilityChanged { window_id, visible }, ); } } fn handle_update_window( state: &Rc>, window_id: WindowId, update: WindowUpdate, ) { let mut emit_opened = false; let mut emit_visibility = None; let mut emit_configured = None; let mut emit_closed = false; { let mut state_ref = state.borrow_mut(); let app = state_ref.app; let Some(window) = state_ref.windows.get_mut(&window_id) else { return; }; if let Some(title) = update.title { window.spec.title = title; if let Some(native) = window.native.as_ref() { let title = NSString::from_str(&window.spec.title); unsafe { let _: () = objc2::msg_send![native.window, setTitle: &*title]; } } } if let Some(open) = update.open { if open { if window.lifecycle == WindowLifecycle::LogicalClosed { ensure_native_window(window, app); sync_window_metrics(window); window.lifecycle = if window.configuration.visible { WindowLifecycle::OpenVisible } else { WindowLifecycle::OpenHidden }; window.close_requested_sent = false; window.close_in_progress = false; sync_visibility(window); apply_cursor_icon(window.cursor_icon); emit_opened = true; } } else if window.lifecycle != WindowLifecycle::LogicalClosed { debug!( target: "ruin_ui_platform_macos::lifecycle", ?window_id, "platform received open(false), closing native window" ); window.desired_visible = false; if window.native.is_some() { window.close_in_progress = true; close_native_window(window); // Keep behavior aligned with other current platform backends: // treat a requested close as immediately closed for app shutdown. window.lifecycle = WindowLifecycle::LogicalClosed; window.close_in_progress = false; emit_closed = true; } else { window.lifecycle = WindowLifecycle::LogicalClosed; emit_closed = true; } } } if let Some(visible) = update.visible && window.configuration.visible != visible { window.configuration.visible = visible; window.desired_visible = visible; window.lifecycle = if visible { WindowLifecycle::OpenVisible } else { WindowLifecycle::OpenHidden }; if visible { window.close_requested_sent = false; } sync_visibility(window); emit_visibility = Some(visible); } if let Some(requested) = update.requested_inner_size { if let Some(size) = requested { window.configuration.actual_inner_size = size; if let Some(native) = window.native.as_mut() { resize_native_window(native, size); } repaint_retained_scene_for_resize(window_id, window); if let Some(active_resize) = window.resize_pipeline.note_resize(size) { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, width = active_resize.width, height = active_resize.height, scale_factor = window.configuration.scale_factor, "emitting Configured from window update resize" ); emit_configured = Some(window.configuration); } 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 from window update while another layout is in flight" ); } } else { emit_configured = Some(window.configuration); } } } if emit_opened { emit(state, PlatformEvent::Opened { window_id }); } if let Some(visible) = emit_visibility { emit( state, PlatformEvent::VisibilityChanged { window_id, visible }, ); } if let Some(configuration) = emit_configured { emit( state, PlatformEvent::Configured { window_id, configuration, }, ); } if emit_closed { emit(state, PlatformEvent::Closed { window_id }); } } fn handle_replace_scene( state: &Rc>, window_id: WindowId, scene: SceneSnapshot, ) { let (presented, followup_events) = { let mut state_ref = state.borrow_mut(); let app = state_ref.app; let Some(window) = state_ref.windows.get_mut(&window_id) else { return; }; let mut followup_events = Vec::new(); ensure_native_window(window, app); window.latest_scene = Some(scene.clone()); ( try_present_scene(window_id, window, &scene, &mut followup_events), followup_events, ) }; for event in followup_events { emit(state, event); } if let Some((scene_version, item_count, visible)) = presented && visible { emit( state, PlatformEvent::FramePresented { window_id, scene_version, item_count, }, ); } } fn try_present_scene( window_id: WindowId, window: &mut MacosWindow, scene: &SceneSnapshot, followup_events: &mut Vec, ) -> Option<(u64, usize, bool)> { let current_viewport = window.configuration.actual_inner_size; let item_count = scene.item_count(); let scene_version = scene.version; let scene_viewport = scene.logical_size; trace!( target: "ruin_ui_platform_macos::resize", ?window_id, scene_width = scene_viewport.width, 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), "handle_replace_scene start" ); let render_succeeded = render_scene_to_current_drawable(window_id, window, scene); if !render_succeeded { return None; } window.has_presented_frame = true; flush_presented_frame(window); if let Some(next_resize) = finish_presented_resize(window, scene_viewport) { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, scene_width = scene_viewport.width, scene_height = scene_viewport.height, next_width = next_resize.width, next_height = next_resize.height, "presented active resize scene; promoting staged next resize" ); emit_resize_request(window_id, window, next_resize, followup_events); } Some((scene_version, item_count, window.configuration.visible)) } fn repaint_retained_scene_for_resize(window_id: WindowId, window: &mut MacosWindow) { if !window.configuration.visible { return; } let Some(scene) = window.latest_scene.clone() else { return; }; trace!( target: "ruin_ui_platform_macos::resize", ?window_id, scene_width = scene.logical_size.width, scene_height = scene.logical_size.height, current_width = window.configuration.actual_inner_size.width, current_height = window.configuration.actual_inner_size.height, "repainting retained scene into resized drawable" ); if render_scene_to_current_drawable(window_id, window, &scene) { window.has_presented_frame = true; flush_presented_frame(window); } } fn render_scene_to_current_drawable( window_id: WindowId, window: &mut MacosWindow, scene: &SceneSnapshot, ) -> bool { let current_viewport = window.configuration.actual_inner_size; let scene_viewport = scene.logical_size; let mut render_succeeded = false; if let Some(native) = window.native.as_mut() { let Some(renderer) = native.renderer.as_mut() else { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, "render skipped; no renderer" ); return false; }; configure_view_resize_policy(native.view); renderer.set_scale_factor(window.configuration.scale_factor); let scale = window.configuration.scale_factor.max(1.0); let drawable_width = (current_viewport.width * scale).max(1.0) as u32; let drawable_height = (current_viewport.height * scale).max(1.0) as u32; if renderer.size() != (drawable_width, drawable_height) { renderer.resize(drawable_width, drawable_height); } match renderer.render_with_logical_size(scene, current_viewport) { Ok(()) => { render_succeeded = true; } Err(RenderError::Lost | RenderError::Outdated) => { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, "render lost/outdated; retrying after resize" ); renderer.resize(drawable_width, drawable_height); if renderer .render_with_logical_size(scene, current_viewport) .is_ok() { render_succeeded = true; } } Err(RenderError::Timeout) => { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, "render timeout" ); } Err(RenderError::Occluded) => { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, "render occluded" ); } Err(RenderError::Validation) => { trace!( target: "ruin_ui_platform_macos::resize", ?window_id, "render validation failure" ); } } } trace!( target: "ruin_ui_platform_macos::resize", ?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), current_width = current_viewport.width, current_height = current_viewport.height, render_succeeded, "render attempt complete" ); render_succeeded } fn ensure_native_window(window: &mut MacosWindow, app: *mut AnyObject) { if window.native.is_some() { return; } if let Some(native) = create_native_window(window, app) { window.native = Some(native); } } fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option { let size = window.configuration.actual_inner_size; let rect = CGRect::new( CGPoint::new(0.0, 0.0), CGSize::new(size.width as f64, size.height as f64), ); let style = NS_WINDOW_STYLE_TITLED | NS_WINDOW_STYLE_CLOSABLE | NS_WINDOW_STYLE_MINIATURIZABLE | NS_WINDOW_STYLE_RESIZABLE; let window_obj: *mut AnyObject = unsafe { objc2::msg_send![objc2::class!(NSWindow), alloc] }; let window_obj: *mut AnyObject = unsafe { objc2::msg_send![ window_obj, initWithContentRect: rect, styleMask: style, backing: NS_BACKING_STORE_BUFFERED, defer: false ] }; if window_obj.is_null() { return None; } let title = NSString::from_str(&window.spec.title); unsafe { let _: () = objc2::msg_send![window_obj, setTitle: &*title]; let _: () = objc2::msg_send![window_obj, setReleasedWhenClosed: false]; let _: () = objc2::msg_send![window_obj, setAcceptsMouseMovedEvents: true]; } let view_class = content_view_class(); let view_obj: *mut AnyObject = unsafe { objc2::msg_send![view_class, alloc] }; let view_obj: *mut AnyObject = unsafe { objc2::msg_send![view_obj, initWithFrame: rect] }; if view_obj.is_null() { return None; } let delegate_class = window_delegate_class(); let delegate_obj: *mut AnyObject = unsafe { objc2::msg_send![delegate_class, new] }; if delegate_obj.is_null() { return None; } unsafe { let _: () = objc2::msg_send![ view_obj, setAutoresizingMask: (NS_VIEW_WIDTH_SIZABLE | NS_VIEW_HEIGHT_SIZABLE) ]; let _: () = objc2::msg_send![view_obj, setWantsLayer: Bool::YES]; configure_view_resize_policy(view_obj); let _: () = objc2::msg_send![window_obj, setContentView: view_obj]; let _: () = objc2::msg_send![window_obj, setDelegate: delegate_obj]; let _: Bool = objc2::msg_send![window_obj, makeFirstResponder: view_obj]; let _: () = objc2::msg_send![window_obj, makeKeyAndOrderFront: std::ptr::null_mut::()]; let _: () = objc2::msg_send![app, activateIgnoringOtherApps: true]; } let metal_layer = configure_metal_surface_layer(view_obj)?; let scale_factor: f64 = unsafe { objc2::msg_send![window_obj, backingScaleFactor] }; let scale_factor = (scale_factor as f32).max(1.0); let target = AppKitSurfaceTarget { view: view_obj }; let renderer = WgpuSceneRenderer::new( target, (size.width * scale_factor).max(1.0) as u32, (size.height * scale_factor).max(1.0) as u32, ) .ok(); let mut renderer = renderer; if let Some(renderer) = renderer.as_mut() { renderer.set_scale_factor(scale_factor); } Some(NativeWindow { window: window_obj, view: view_obj, delegate: delegate_obj, _metal_layer: metal_layer, renderer, }) } fn resize_native_window(native: &mut NativeWindow, size: UiSize) { let content_size = CGSize::new(size.width as f64, size.height as f64); unsafe { let _: () = objc2::msg_send![native.window, setContentSize: content_size]; } } fn sync_visibility(window: &MacosWindow) { let Some(native) = window.native.as_ref() else { return; }; if window.configuration.visible { unsafe { let _: () = objc2::msg_send![native.window, makeKeyAndOrderFront: std::ptr::null_mut::()]; } } else { unsafe { let _: () = objc2::msg_send![native.window, orderOut: std::ptr::null_mut::()]; } } } fn close_native_window(window: &mut MacosWindow) { if let Some(native) = window.native.as_ref() { debug!( target: "ruin_ui_platform_macos::lifecycle", window_ptr = native.window as usize, "sending NSWindow close" ); unsafe { let _: () = objc2::msg_send![native.window, close]; } } } fn emit(state: &Rc>, event: PlatformEvent) { let sender = state.borrow().endpoint_events.clone(); let _ = sender.send(event); } fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec) { let mut saw_closed = false; for resize in take_delegate_resize_events() { let Some((window_id, window)) = state.windows.iter_mut().find(|(_, window)| { window .native .as_ref() .is_some_and(|native| native.window as usize == resize.window_ptr) }) else { continue; }; if window.lifecycle == WindowLifecycle::LogicalClosed { continue; } let resized = resize.size.width > 0.0 && resize.size.height > 0.0 && resize.size != window.configuration.actual_inner_size; let scale_changed = (window.configuration.scale_factor - resize.scale_factor).abs() > f32::EPSILON; if !(resized || scale_changed) { continue; } window.configuration.actual_inner_size = resize.size; window.configuration.scale_factor = resize.scale_factor.max(1.0); note_window_resize(*window_id, window, resize.size, pending); repaint_retained_scene_for_resize(*window_id, window); } for input in take_delegate_input_events() { match input { DelegateInputEvent::Pointer { window_ptr, event } => { let Some((window_id, window)) = state.windows.iter().find(|(_, window)| { window .native .as_ref() .is_some_and(|native| native.window as usize == window_ptr) }) else { continue; }; if window.lifecycle != WindowLifecycle::LogicalClosed { pending.push(PlatformEvent::Pointer { window_id: *window_id, event, }); } } DelegateInputEvent::Keyboard { window_ptr, event } => { let Some((window_id, window)) = state.windows.iter().find(|(_, window)| { window .native .as_ref() .is_some_and(|native| native.window as usize == window_ptr) }) else { continue; }; if window.lifecycle != WindowLifecycle::LogicalClosed { pending.push(PlatformEvent::Keyboard { window_id: *window_id, event, }); } } } } for window_ptr in take_delegate_close_requests() { let Some((window_id, window)) = state.windows.iter_mut().find(|(_, window)| { window .native .as_ref() .is_some_and(|native| native.window as usize == window_ptr) }) else { continue; }; if !window.close_requested_sent { window.close_requested_sent = true; debug!( target: "ruin_ui_platform_macos::lifecycle", ?window_id, "emitting CloseRequested" ); pending.push(PlatformEvent::CloseRequested { window_id: *window_id, }); } } for window_ptr in take_delegate_closed_windows() { let Some((window_id, window)) = state.windows.iter_mut().find(|(_, window)| { window .native .as_ref() .is_some_and(|native| native.window as usize == window_ptr) }) else { continue; }; finalize_native_close(*window_id, window, pending); saw_closed = true; } if saw_closed && state .windows .values() .all(|window| window.lifecycle == WindowLifecycle::LogicalClosed) { debug!( target: "ruin_ui_platform_macos::lifecycle", "all windows closed; terminating NSApplication" ); unsafe { let _: () = objc2::msg_send![state.app, terminate: std::ptr::null_mut::()]; } } } fn note_window_resize( window_id: WindowId, window: &mut MacosWindow, 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); } 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" ); } } fn emit_resize_request( window_id: WindowId, window: &mut MacosWindow, viewport: UiSize, pending: &mut Vec, ) { pending.push(PlatformEvent::Configured { window_id, configuration: window.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" ); } fn finish_presented_resize(window: &mut MacosWindow, presented: UiSize) -> Option { window.resize_pipeline.note_presented_scene(presented) } fn configure_metal_surface_layer(view: *mut AnyObject) -> Option> { let view_ptr = NonNull::new(view.cast::())?; let layer = unsafe { MetalLayer::from_ns_view(view_ptr) }; let layer_ptr = layer.into_raw().as_ptr().cast::(); let layer = unsafe { Retained::from_raw(layer_ptr) }?; layer.setContentsGravity(unsafe { kCAGravityTopLeft }); layer.setPresentsWithTransaction(false); Some(layer) } fn flush_presented_frame(window: &MacosWindow) { let Some(native) = window.native.as_ref() else { return; }; let in_live_resize: bool = unsafe { objc2::msg_send![native.view, inLiveResize] }; unsafe { let _: () = objc2::msg_send![native.view, displayIfNeeded]; let _: () = objc2::msg_send![native.window, displayIfNeeded]; let _: () = objc2::msg_send![objc2::class!(CATransaction), flush]; } trace!( target: "ruin_ui_platform_macos::resize", in_live_resize, "requested display after presented frame" ); } fn configure_view_resize_policy(view: *mut AnyObject) { if view.is_null() { return; } unsafe { let can_set_redraw: bool = objc2::msg_send![view, respondsToSelector: sel!(setLayerContentsRedrawPolicy:)]; if can_set_redraw { let _: () = objc2::msg_send![ view, setLayerContentsRedrawPolicy: NS_VIEW_LAYER_CONTENTS_REDRAW_DURING_VIEW_RESIZE ]; } let can_set_placement: bool = objc2::msg_send![view, respondsToSelector: sel!(setLayerContentsPlacement:)]; if can_set_placement { let _: () = objc2::msg_send![ view, setLayerContentsPlacement: NS_VIEW_LAYER_CONTENTS_PLACEMENT_TOP_LEFT ]; } let layer: *mut AnyObject = objc2::msg_send![view, layer]; if !layer.is_null() { let can_set_gravity: bool = objc2::msg_send![layer, respondsToSelector: sel!(setContentsGravity:)]; if can_set_gravity { let _: () = objc2::msg_send![layer, setContentsGravity: ns_string!("topLeft")]; } let can_set_needs_display: bool = objc2::msg_send![layer, respondsToSelector: sel!(setNeedsDisplayOnBoundsChange:)]; if can_set_needs_display { let _: () = objc2::msg_send![layer, setNeedsDisplayOnBoundsChange: false]; } } } } fn collect_window_state_events( window_id: WindowId, window: &mut MacosWindow, pending: &mut Vec, ) { let Some((window_ptr, view_ptr)) = window .native .as_ref() .map(|native| (native.window, native.view)) else { return; }; if window.lifecycle == WindowLifecycle::LogicalClosed { return; } let frame: CGRect = unsafe { objc2::msg_send![view_ptr, frame] }; let scale_factor: f64 = unsafe { objc2::msg_send![window_ptr, backingScaleFactor] }; let scale_factor = scale_factor as f32; let content_size = UiSize::new(frame.size.width as f32, frame.size.height as f32); let resized = content_size.width > 0.0 && content_size.height > 0.0 && content_size != window.configuration.actual_inner_size; let scale_changed = (window.configuration.scale_factor - scale_factor).abs() > f32::EPSILON; if resized || scale_changed { window.configuration.actual_inner_size = content_size; window.configuration.scale_factor = scale_factor.max(1.0); note_window_resize(window_id, window, content_size, pending); } let is_visible = native_window_is_logically_visible(window_ptr); let became_visible = is_visible && !window.configuration.visible; if is_visible != window.configuration.visible { window.configuration.visible = is_visible; window.lifecycle = if is_visible { window.close_requested_sent = false; WindowLifecycle::OpenVisible } else { window.has_presented_frame = false; WindowLifecycle::OpenHidden }; pending.push(PlatformEvent::VisibilityChanged { window_id, visible: is_visible, }); } if window.close_in_progress && !is_visible { finalize_native_close(window_id, window, pending); return; } if is_visible && let Some(scene) = window.latest_scene.clone() && (became_visible || !window.has_presented_frame || window.resize_pipeline.active_resize == Some(scene.logical_size)) { let _ = try_present_scene(window_id, window, &scene, pending); } else if is_visible && !window.has_presented_frame && let Some(initial_viewport) = window .resize_pipeline .ensure_initial_request(window.configuration.actual_inner_size) { emit_resize_request(window_id, window, initial_viewport, pending); } } fn native_window_is_logically_visible(window: *mut AnyObject) -> bool { let is_visible: bool = unsafe { objc2::msg_send![window, isVisible] }; let is_miniaturized: bool = unsafe { objc2::msg_send![window, isMiniaturized] }; is_visible && !is_miniaturized } fn keyboard_modifiers(event: *mut AnyObject) -> KeyboardModifiers { let flags: usize = unsafe { objc2::msg_send![event, modifierFlags] }; KeyboardModifiers { shift: flags & NSEVENT_MODIFIER_FLAG_SHIFT != 0, control: flags & NSEVENT_MODIFIER_FLAG_CONTROL != 0, alt: flags & NSEVENT_MODIFIER_FLAG_OPTION != 0, super_key: flags & NSEVENT_MODIFIER_FLAG_COMMAND != 0, } } fn keyboard_key(keycode: u16, text: Option<&str>) -> KeyboardKey { match keycode { 36 => KeyboardKey::Enter, 48 => KeyboardKey::Tab, 53 => KeyboardKey::Escape, 51 => KeyboardKey::Backspace, 117 => KeyboardKey::Delete, 123 => KeyboardKey::ArrowLeft, 124 => KeyboardKey::ArrowRight, 125 => KeyboardKey::ArrowDown, 126 => KeyboardKey::ArrowUp, 115 => KeyboardKey::Home, 119 => KeyboardKey::End, _ => text .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .map(KeyboardKey::Character) .unwrap_or(KeyboardKey::Unknown), } } fn event_characters(event: *mut AnyObject) -> Option { let string: *mut AnyObject = unsafe { objc2::msg_send![event, characters] }; nsstring_to_string(string) } fn nsstring_to_string(string: *mut AnyObject) -> Option { if string.is_null() { return None; } let cstr_ptr: *const i8 = unsafe { objc2::msg_send![string, UTF8String] }; if cstr_ptr.is_null() { return None; } let text = unsafe { CStr::from_ptr(cstr_ptr) } .to_string_lossy() .into_owned(); (!text.is_empty()).then_some(text) } fn apply_cursor_icon(cursor: CursorIcon) { let class = objc2::class!(NSCursor); let cursor_obj: *mut AnyObject = unsafe { match cursor { CursorIcon::Default => objc2::msg_send![class, arrowCursor], CursorIcon::Pointer => objc2::msg_send![class, pointingHandCursor], CursorIcon::Text => objc2::msg_send![class, IBeamCursor], } }; if cursor_obj.is_null() { return; } unsafe { let _: () = objc2::msg_send![cursor_obj, set]; } } fn finalize_native_close( window_id: WindowId, window: &mut MacosWindow, pending: &mut Vec, ) { debug!( target: "ruin_ui_platform_macos::lifecycle", ?window_id, "finalizing native close" ); let should_emit_closed = window.lifecycle != WindowLifecycle::LogicalClosed; if let Some(native) = window.native.take() { unsafe { let _: () = objc2::msg_send![native.window, setDelegate: std::ptr::null_mut::()]; let _: () = objc2::msg_send![native.delegate, release]; } } window.close_in_progress = false; window.lifecycle = WindowLifecycle::LogicalClosed; window.desired_visible = false; if window.configuration.visible { window.configuration.visible = false; pending.push(PlatformEvent::VisibilityChanged { window_id, visible: false, }); } if should_emit_closed { debug!( target: "ruin_ui_platform_macos::lifecycle", ?window_id, "emitting Closed" ); pending.push(PlatformEvent::Closed { window_id }); } } fn sync_window_metrics(window: &mut MacosWindow) { let Some((window_ptr, view_ptr)) = window .native .as_ref() .map(|native| (native.window, native.view)) else { return; }; let frame: CGRect = unsafe { objc2::msg_send![view_ptr, frame] }; let scale_factor: f64 = unsafe { objc2::msg_send![window_ptr, backingScaleFactor] }; window.configuration.actual_inner_size = UiSize::new(frame.size.width as f32, frame.size.height as f32); window.configuration.scale_factor = (scale_factor as f32).max(1.0); } #[cfg(test)] mod tests { use super::ResizePipeline; use ruin_ui::UiSize; #[test] fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() { let mut pipeline = ResizePipeline::default(); 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.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(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))); } #[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); 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.active_resize, Some(second)); assert_eq!(pipeline.next_resize, None); } #[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); 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.active_resize, None); 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); assert_eq!(pipeline.note_resize(active), Some(active)); assert_eq!(pipeline.note_resize(next), None); assert_eq!( pipeline.note_presented_scene(UiSize::new(700.0, 500.0)), None ); assert_eq!(pipeline.active_resize, Some(active)); assert_eq!(pipeline.next_resize, Some(next)); } }