This commit is contained in:
Will Temple
2026-05-15 16:28:06 -07:00
parent 67400f1499
commit bfda24ad0a
13 changed files with 782 additions and 363 deletions

View File

@@ -73,17 +73,83 @@ struct RunLoopWakeHandle {
struct MacosWindow {
spec: WindowSpec,
lifecycle: WindowLifecycle,
state: MacosWindowState,
configuration: WindowConfigured,
latest_scene: Option<SceneSnapshot>,
clipboard_text: Option<String>,
desired_visible: bool,
cursor_icon: CursorIcon,
resize_pipeline: ResizePipeline,
native: Option<NativeWindow>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct MacosWindowState {
lifecycle: WindowLifecycle,
desired_visible: bool,
close_requested_sent: bool,
close_in_progress: bool,
has_presented_frame: bool,
resize_pipeline: ResizePipeline,
native: Option<NativeWindow>,
}
impl MacosWindowState {
fn new(desired_visible: bool) -> Self {
Self {
lifecycle: WindowLifecycle::LogicalClosed,
desired_visible,
close_requested_sent: false,
close_in_progress: false,
has_presented_frame: false,
}
}
fn open_with_visibility(&mut self, visible: bool) {
self.lifecycle = lifecycle_for_visibility(visible);
self.desired_visible = visible;
self.close_requested_sent = false;
self.close_in_progress = false;
}
fn set_native_visibility(&mut self, visible: bool) {
self.lifecycle = lifecycle_for_visibility(visible);
if visible {
self.close_requested_sent = false;
} else {
self.has_presented_frame = false;
}
}
fn request_logical_close(&mut self) {
self.desired_visible = false;
self.close_in_progress = true;
}
fn finalize_close(&mut self) {
self.lifecycle = WindowLifecycle::LogicalClosed;
self.desired_visible = false;
self.close_in_progress = false;
self.has_presented_frame = false;
}
fn note_presented(&mut self) {
self.has_presented_frame = true;
}
fn mark_close_requested_sent(&mut self) -> bool {
if self.close_requested_sent {
false
} else {
self.close_requested_sent = true;
true
}
}
}
fn lifecycle_for_visibility(visible: bool) -> WindowLifecycle {
if visible {
WindowLifecycle::OpenVisible
} else {
WindowLifecycle::OpenHidden
}
}
struct NativeWindow {
@@ -95,6 +161,62 @@ struct NativeWindow {
renderer: Option<WgpuSceneRenderer>,
}
impl NativeWindow {
fn window_ptr(&self) -> usize {
self.window as usize
}
fn set_title(&self, title: &str) {
let title = NSString::from_str(title);
unsafe {
let _: () = objc2::msg_send![self.window, setTitle: &*title];
}
}
fn set_content_size(&self, size: UiSize) {
let content_size = CGSize::new(size.width as f64, size.height as f64);
unsafe {
let _: () = objc2::msg_send![self.window, setContentSize: content_size];
}
}
fn show(&self) {
unsafe {
let _: () = objc2::msg_send![self.window, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
}
}
fn hide(&self) {
unsafe {
let _: () = objc2::msg_send![self.window, orderOut: std::ptr::null_mut::<AnyObject>()];
}
}
fn close(&self) {
unsafe {
let _: () = objc2::msg_send![self.window, close];
}
}
fn flush_display(&self) -> bool {
let in_live_resize: bool = unsafe { objc2::msg_send![self.view, inLiveResize] };
unsafe {
let _: () = objc2::msg_send![self.view, displayIfNeeded];
let _: () = objc2::msg_send![self.window, displayIfNeeded];
let _: () = objc2::msg_send![objc2::class!(CATransaction), flush];
}
in_live_resize
}
fn detach_delegate(&self) {
unsafe {
let _: () =
objc2::msg_send![self.window, setDelegate: std::ptr::null_mut::<AnyObject>()];
let _: () = objc2::msg_send![self.delegate, release];
}
}
}
#[derive(Default)]
struct DelegateState {
pending_close_requests: Vec<usize>,
@@ -124,7 +246,6 @@ enum DelegateInputEvent {
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>> {
@@ -182,7 +303,7 @@ impl MacosWindow {
.unwrap_or_else(|| UiSize::new(960.0, 640.0));
Self {
spec,
lifecycle: WindowLifecycle::LogicalClosed,
state: MacosWindowState::new(desired_visible),
configuration: WindowConfigured {
actual_inner_size,
scale_factor: 1.0,
@@ -192,11 +313,7 @@ impl MacosWindow {
},
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,
}
@@ -494,7 +611,7 @@ fn window_delegate_class() -> &'static AnyClass {
"windowShouldClose:"
);
queue_delegate_close_request(sender);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
Bool::YES
}
extern "C-unwind" fn did_close(_this: &AnyObject, _cmd: Sel, notification: *mut AnyObject) {
@@ -505,7 +622,7 @@ fn window_delegate_class() -> &'static AnyClass {
"windowWillClose:"
);
queue_delegate_window_closed(window);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
extern "C-unwind" fn did_resize(
_this: &AnyObject,
@@ -535,7 +652,7 @@ fn window_delegate_class() -> &'static AnyClass {
UiSize::new(frame.size.width as f32, frame.size.height as f32),
(scale_factor as f32).max(1.0),
);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
extern "C-unwind" fn did_change_occlusion_state(
_this: &AnyObject,
@@ -636,7 +753,7 @@ fn content_view_class() -> &'static AnyClass {
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();
signal_platform_run_loop_source();
}
extern "C-unwind" fn left_mouse_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
@@ -647,7 +764,7 @@ fn content_view_class() -> &'static AnyClass {
button: PointerButton::Primary,
},
);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
extern "C-unwind" fn left_mouse_up(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
@@ -658,7 +775,7 @@ fn content_view_class() -> &'static AnyClass {
button: PointerButton::Primary,
},
);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
extern "C-unwind" fn left_mouse_dragged(
@@ -667,7 +784,7 @@ fn content_view_class() -> &'static AnyClass {
event: *mut AnyObject,
) {
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
extern "C-unwind" fn other_mouse_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
@@ -680,7 +797,7 @@ fn content_view_class() -> &'static AnyClass {
button: PointerButton::Middle,
},
);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
}
@@ -694,7 +811,7 @@ fn content_view_class() -> &'static AnyClass {
button: PointerButton::Middle,
},
);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
}
@@ -706,7 +823,7 @@ fn content_view_class() -> &'static AnyClass {
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();
signal_platform_run_loop_source();
}
}
@@ -720,17 +837,17 @@ fn content_view_class() -> &'static AnyClass {
delta: Point::new(delta_x as f32, -(delta_y as f32)),
},
);
flush_delegate_events_immediately();
signal_platform_run_loop_source();
}
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();
signal_platform_run_loop_source();
}
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();
signal_platform_run_loop_source();
}
let class_name = CString::new("RuinContentView").expect("class name is valid");
@@ -851,47 +968,6 @@ fn try_enter_appkit_pump() -> Option<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;
@@ -1021,15 +1097,8 @@ fn handle_create_window(state: &Rc<RefCell<MacosState>>, window_id: WindowId, sp
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.state.open_with_visibility(visible);
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);
@@ -1082,46 +1151,37 @@ fn handle_update_window(
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];
}
native.set_title(&window.spec.title);
}
}
if let Some(open) = update.open {
if open {
if window.lifecycle == WindowLifecycle::LogicalClosed {
if window.state.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;
window
.state
.open_with_visibility(window.configuration.visible);
sync_visibility(window);
apply_cursor_icon(window.cursor_icon);
emit_opened = true;
}
} else if window.lifecycle != WindowLifecycle::LogicalClosed {
} else if window.state.lifecycle != WindowLifecycle::LogicalClosed {
debug!(
target: "ruin_ui_platform_macos::lifecycle",
?window_id,
"platform received open(false), closing native window"
);
window.desired_visible = false;
window.state.request_logical_close();
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;
window.state.finalize_close();
emit_closed = true;
} else {
window.lifecycle = WindowLifecycle::LogicalClosed;
window.state.finalize_close();
emit_closed = true;
}
}
@@ -1131,15 +1191,7 @@ fn handle_update_window(
&& 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;
}
window.state.open_with_visibility(visible);
sync_visibility(window);
emit_visibility = Some(visible);
}
@@ -1269,7 +1321,7 @@ fn try_present_scene(
return None;
}
window.has_presented_frame = true;
window.state.note_presented();
flush_presented_frame(window);
if let Some(next_resize) = finish_presented_resize(window, scene_viewport) {
trace!(
@@ -1304,7 +1356,7 @@ fn repaint_retained_scene_for_resize(window_id: WindowId, window: &mut MacosWind
"repainting retained scene into resized drawable"
);
if render_scene_to_current_drawable(window_id, window, &scene) {
window.has_presented_frame = true;
window.state.note_presented();
flush_presented_frame(window);
}
}
@@ -1486,10 +1538,7 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
}
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];
}
native.set_content_size(size);
}
fn sync_visibility(window: &MacosWindow) {
@@ -1498,14 +1547,9 @@ fn sync_visibility(window: &MacosWindow) {
};
if window.configuration.visible {
unsafe {
let _: () = objc2::msg_send![native.window, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
}
native.show();
} else {
unsafe {
let _: () =
objc2::msg_send![native.window, orderOut: std::ptr::null_mut::<AnyObject>()];
}
native.hide();
}
}
@@ -1513,12 +1557,10 @@ 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,
window_ptr = native.window_ptr(),
"sending NSWindow close"
);
unsafe {
let _: () = objc2::msg_send![native.window, close];
}
native.close();
}
}
@@ -1534,11 +1576,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
window
.native
.as_ref()
.is_some_and(|native| native.window as usize == resize.window_ptr)
.is_some_and(|native| native.window_ptr() == resize.window_ptr)
}) else {
continue;
};
if window.lifecycle == WindowLifecycle::LogicalClosed {
if window.state.lifecycle == WindowLifecycle::LogicalClosed {
continue;
}
let resized = resize.size.width > 0.0
@@ -1563,11 +1605,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
window
.native
.as_ref()
.is_some_and(|native| native.window as usize == window_ptr)
.is_some_and(|native| native.window_ptr() == window_ptr)
}) else {
continue;
};
if window.lifecycle != WindowLifecycle::LogicalClosed {
if window.state.lifecycle != WindowLifecycle::LogicalClosed {
pending.push(PlatformEvent::Pointer {
window_id: *window_id,
event,
@@ -1579,11 +1621,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
window
.native
.as_ref()
.is_some_and(|native| native.window as usize == window_ptr)
.is_some_and(|native| native.window_ptr() == window_ptr)
}) else {
continue;
};
if window.lifecycle != WindowLifecycle::LogicalClosed {
if window.state.lifecycle != WindowLifecycle::LogicalClosed {
pending.push(PlatformEvent::Keyboard {
window_id: *window_id,
event,
@@ -1598,12 +1640,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
window
.native
.as_ref()
.is_some_and(|native| native.window as usize == window_ptr)
.is_some_and(|native| native.window_ptr() == window_ptr)
}) else {
continue;
};
if !window.close_requested_sent {
window.close_requested_sent = true;
if window.state.mark_close_requested_sent() {
debug!(
target: "ruin_ui_platform_macos::lifecycle",
?window_id,
@@ -1620,7 +1661,7 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
window
.native
.as_ref()
.is_some_and(|native| native.window as usize == window_ptr)
.is_some_and(|native| native.window_ptr() == window_ptr)
}) else {
continue;
};
@@ -1632,7 +1673,7 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
&& state
.windows
.values()
.all(|window| window.lifecycle == WindowLifecycle::LogicalClosed)
.all(|window| window.state.lifecycle == WindowLifecycle::LogicalClosed)
{
debug!(
target: "ruin_ui_platform_macos::lifecycle",
@@ -1703,12 +1744,7 @@ 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];
}
let in_live_resize = native.flush_display();
trace!(
target: "ruin_ui_platform_macos::resize",
in_live_resize,
@@ -1765,7 +1801,7 @@ fn collect_window_state_events(
else {
return;
};
if window.lifecycle == WindowLifecycle::LogicalClosed {
if window.state.lifecycle == WindowLifecycle::LogicalClosed {
return;
}
@@ -1788,20 +1824,14 @@ fn collect_window_state_events(
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
};
window.state.set_native_visibility(is_visible);
pending.push(PlatformEvent::VisibilityChanged {
window_id,
visible: is_visible,
});
}
if window.close_in_progress && !is_visible {
if window.state.close_in_progress && !is_visible {
finalize_native_close(window_id, window, pending);
return;
}
@@ -1809,12 +1839,12 @@ fn collect_window_state_events(
if is_visible
&& let Some(scene) = window.latest_scene.clone()
&& (became_visible
|| !window.has_presented_frame
|| !window.state.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
&& !window.state.has_presented_frame
&& let Some(initial_viewport) = window
.resize_pipeline
.ensure_initial_request(window.configuration.actual_inner_size)
@@ -1907,18 +1937,12 @@ fn finalize_native_close(
?window_id,
"finalizing native close"
);
let should_emit_closed = window.lifecycle != WindowLifecycle::LogicalClosed;
let should_emit_closed = window.state.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];
}
native.detach_delegate();
}
window.close_in_progress = false;
window.lifecycle = WindowLifecycle::LogicalClosed;
window.desired_visible = false;
window.state.finalize_close();
if window.configuration.visible {
window.configuration.visible = false;
@@ -1954,8 +1978,8 @@ fn sync_window_metrics(window: &mut MacosWindow) {
#[cfg(test)]
mod tests {
use super::ResizePipeline;
use ruin_ui::UiSize;
use super::{MacosWindowState, ResizePipeline};
use ruin_ui::{UiSize, WindowLifecycle};
#[test]
fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() {
@@ -2020,4 +2044,40 @@ mod tests {
assert_eq!(pipeline.active_resize, Some(active));
assert_eq!(pipeline.next_resize, Some(next));
}
#[test]
fn window_state_distinguishes_hide_from_close() {
let mut state = MacosWindowState::new(true);
state.open_with_visibility(true);
state.note_presented();
state.set_native_visibility(false);
assert_eq!(state.lifecycle, WindowLifecycle::OpenHidden);
assert!(state.desired_visible);
assert!(!state.close_in_progress);
assert!(!state.has_presented_frame);
state.set_native_visibility(true);
assert_eq!(state.lifecycle, WindowLifecycle::OpenVisible);
assert!(state.desired_visible);
}
#[test]
fn window_state_close_request_is_single_shot_and_finalize_resets_state() {
let mut state = MacosWindowState::new(true);
state.open_with_visibility(true);
assert!(state.mark_close_requested_sent());
assert!(!state.mark_close_requested_sent());
state.request_logical_close();
assert!(!state.desired_visible);
assert!(state.close_in_progress);
state.finalize_close();
assert_eq!(state.lifecycle, WindowLifecycle::LogicalClosed);
assert!(!state.desired_visible);
assert!(!state.close_in_progress);
assert!(!state.has_presented_frame);
}
}