2024 lines
70 KiB
Rust
2024 lines
70 KiB
Rust
//! 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<PlatformRequest>,
|
|
endpoint_events: ruin_runtime::channel::mpsc::UnboundedSender<PlatformEvent>,
|
|
_run_loop_source: RunLoopSourceRegistration,
|
|
shutdown_requested: bool,
|
|
windows: BTreeMap<WindowId, MacosWindow>,
|
|
}
|
|
|
|
struct RunLoopSourceRegistration {
|
|
_run_loop: CFRetained<CFRunLoop>,
|
|
_source: CFRetained<CFRunLoopSource>,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct RunLoopWakeHandle {
|
|
run_loop: usize,
|
|
source: usize,
|
|
}
|
|
|
|
struct MacosWindow {
|
|
spec: WindowSpec,
|
|
lifecycle: WindowLifecycle,
|
|
configuration: WindowConfigured,
|
|
latest_scene: Option<SceneSnapshot>,
|
|
clipboard_text: Option<String>,
|
|
desired_visible: bool,
|
|
cursor_icon: CursorIcon,
|
|
close_requested_sent: bool,
|
|
close_in_progress: bool,
|
|
has_presented_frame: bool,
|
|
resize_pipeline: ResizePipeline,
|
|
native: Option<NativeWindow>,
|
|
}
|
|
|
|
struct NativeWindow {
|
|
window: *mut AnyObject,
|
|
#[allow(dead_code)]
|
|
view: *mut AnyObject,
|
|
delegate: *mut AnyObject,
|
|
_metal_layer: Retained<CAMetalLayer>,
|
|
renderer: Option<WgpuSceneRenderer>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct DelegateState {
|
|
pending_close_requests: Vec<usize>,
|
|
pending_closed_windows: Vec<usize>,
|
|
pending_resize_events: Vec<DelegateResizeEvent>,
|
|
pending_input_events: Vec<DelegateInputEvent>,
|
|
}
|
|
|
|
#[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<Option<Rc<RefCell<MacosState>>>> = const { RefCell::new(None) };
|
|
static APPKIT_PUMP_ACTIVE: Cell<bool> = const { Cell::new(false) };
|
|
static IMMEDIATE_DELEGATE_FLUSH_ACTIVE: Cell<bool> = const { Cell::new(false) };
|
|
}
|
|
|
|
fn run_loop_wake_handle() -> &'static Mutex<Option<RunLoopWakeHandle>> {
|
|
static HANDLE: OnceLock<Mutex<Option<RunLoopWakeHandle>>> = OnceLock::new();
|
|
HANDLE.get_or_init(|| Mutex::new(None))
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
struct ResizePipeline {
|
|
active_resize: Option<UiSize>,
|
|
next_resize: Option<UiSize>,
|
|
}
|
|
|
|
impl ResizePipeline {
|
|
fn note_resize(&mut self, viewport: UiSize) -> Option<UiSize> {
|
|
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<UiSize> {
|
|
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<UiSize> {
|
|
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<DisplayHandle<'_>, HandleError> {
|
|
let raw = RawDisplayHandle::AppKit(AppKitDisplayHandle::new());
|
|
Ok(unsafe { DisplayHandle::borrow_raw(raw) })
|
|
}
|
|
}
|
|
|
|
impl HasWindowHandle for AppKitSurfaceTarget {
|
|
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
|
let ptr = NonNull::new(self.view.cast::<c_void>()).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<RunLoopSourceRegistration> {
|
|
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::<c_void>() as usize,
|
|
source: CFRetained::as_ptr(&source).as_ptr().cast::<c_void>() 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::<AnyObject, _>(
|
|
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<usize> = 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<DelegateState> {
|
|
static DELEGATE_STATE: OnceLock<Mutex<DelegateState>> = 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<usize> {
|
|
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<usize> {
|
|
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<DelegateResizeEvent> {
|
|
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<DelegateInputEvent> {
|
|
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::<AnyObject, _>(sel!(windowShouldClose:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = did_close;
|
|
builder.add_method::<AnyObject, _>(sel!(windowWillClose:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = did_resize;
|
|
builder.add_method::<AnyObject, _>(sel!(windowDidResize:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = did_change_occlusion_state;
|
|
builder.add_method::<AnyObject, _>(sel!(windowDidChangeOcclusionState:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = did_become_key;
|
|
builder.add_method::<AnyObject, _>(sel!(windowDidBecomeKey:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = did_miniaturize;
|
|
builder.add_method::<AnyObject, _>(sel!(windowDidMiniaturize:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = did_deminiaturize;
|
|
builder.add_method::<AnyObject, _>(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::<AnyObject, _>(sel!(acceptsFirstResponder), method);
|
|
let method: extern "C-unwind" fn(_, _) -> _ = is_flipped;
|
|
builder.add_method::<AnyObject, _>(sel!(isFlipped), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
|
builder.add_method::<AnyObject, _>(sel!(mouseMoved:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
|
builder.add_method::<AnyObject, _>(sel!(rightMouseDragged:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = left_mouse_down;
|
|
builder.add_method::<AnyObject, _>(sel!(mouseDown:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = left_mouse_up;
|
|
builder.add_method::<AnyObject, _>(sel!(mouseUp:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = left_mouse_dragged;
|
|
builder.add_method::<AnyObject, _>(sel!(mouseDragged:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = other_mouse_down;
|
|
builder.add_method::<AnyObject, _>(sel!(otherMouseDown:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = other_mouse_up;
|
|
builder.add_method::<AnyObject, _>(sel!(otherMouseUp:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = other_mouse_dragged;
|
|
builder.add_method::<AnyObject, _>(sel!(otherMouseDragged:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = scroll_wheel;
|
|
builder.add_method::<AnyObject, _>(sel!(scrollWheel:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = key_down;
|
|
builder.add_method::<AnyObject, _>(sel!(keyDown:), method);
|
|
let method: extern "C-unwind" fn(_, _, _) = key_up;
|
|
builder.add_method::<AnyObject, _>(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<PlatformEvent>,
|
|
pending: &mut Vec<PlatformEvent>,
|
|
) {
|
|
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<AppKitPumpGuard> {
|
|
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<RefCell<MacosState>>) {
|
|
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<RefCell<MacosState>>) -> Option<PlatformRequest> {
|
|
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<RefCell<MacosState>>) -> 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::<AnyObject>()];
|
|
}
|
|
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<RefCell<MacosState>>, 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<RefCell<MacosState>>,
|
|
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<RefCell<MacosState>>,
|
|
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<PlatformEvent>,
|
|
) -> 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<NativeWindow> {
|
|
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::<AnyObject>()];
|
|
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::<AnyObject>()];
|
|
}
|
|
} else {
|
|
unsafe {
|
|
let _: () =
|
|
objc2::msg_send![native.window, orderOut: std::ptr::null_mut::<AnyObject>()];
|
|
}
|
|
}
|
|
}
|
|
|
|
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<RefCell<MacosState>>, event: PlatformEvent) {
|
|
let sender = state.borrow().endpoint_events.clone();
|
|
let _ = sender.send(event);
|
|
}
|
|
|
|
fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent>) {
|
|
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::<AnyObject>()];
|
|
}
|
|
}
|
|
}
|
|
|
|
fn note_window_resize(
|
|
window_id: WindowId,
|
|
window: &mut MacosWindow,
|
|
viewport: UiSize,
|
|
pending: &mut Vec<PlatformEvent>,
|
|
) {
|
|
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<PlatformEvent>,
|
|
) {
|
|
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<UiSize> {
|
|
window.resize_pipeline.note_presented_scene(presented)
|
|
}
|
|
|
|
fn configure_metal_surface_layer(view: *mut AnyObject) -> Option<Retained<CAMetalLayer>> {
|
|
let view_ptr = NonNull::new(view.cast::<c_void>())?;
|
|
let layer = unsafe { MetalLayer::from_ns_view(view_ptr) };
|
|
let layer_ptr = layer.into_raw().as_ptr().cast::<CAMetalLayer>();
|
|
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<PlatformEvent>,
|
|
) {
|
|
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<String> {
|
|
let string: *mut AnyObject = unsafe { objc2::msg_send![event, characters] };
|
|
nsstring_to_string(string)
|
|
}
|
|
|
|
fn nsstring_to_string(string: *mut AnyObject) -> Option<String> {
|
|
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<PlatformEvent>,
|
|
) {
|
|
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::<AnyObject>()];
|
|
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));
|
|
}
|
|
}
|