From 40b61d2d92c1f55458b6b1a12ea6459eef0894ec Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 14 Jan 2024 03:37:53 +0100 Subject: [PATCH] macOS: Remove global `HANDLER` and `AppState` (#3389) --- src/platform_impl/macos/app.rs | 29 +- src/platform_impl/macos/app_delegate.rs | 546 +++++++++++++++- src/platform_impl/macos/app_state.rs | 712 --------------------- src/platform_impl/macos/event_loop.rs | 136 ++-- src/platform_impl/macos/mod.rs | 1 - src/platform_impl/macos/observer.rs | 26 +- src/platform_impl/macos/view.rs | 43 +- src/platform_impl/macos/window.rs | 52 +- src/platform_impl/macos/window_delegate.rs | 16 +- 9 files changed, 683 insertions(+), 878 deletions(-) delete mode 100644 src/platform_impl/macos/app_state.rs diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index 2e2b3fcc..d3dee982 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -6,12 +6,12 @@ use icrate::AppKit::{ NSEventTypeOtherMouseDown, NSEventTypeOtherMouseDragged, NSEventTypeOtherMouseUp, NSEventTypeRightMouseDown, NSEventTypeRightMouseDragged, NSEventTypeRightMouseUp, NSResponder, }; -use icrate::Foundation::NSObject; +use icrate::Foundation::{MainThreadMarker, NSObject}; use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass}; +use super::app_delegate::ApplicationDelegate; use super::event::flags_contains; -use super::{app_state::AppState, DEVICE_ID}; -use crate::event::{DeviceEvent, ElementState, Event}; +use crate::event::{DeviceEvent, ElementState}; declare_class!( pub(super) struct WinitApplication; @@ -43,14 +43,15 @@ declare_class!( key_window.sendEvent(event); } } else { - maybe_dispatch_device_event(event); + let delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + maybe_dispatch_device_event(&delegate, event); unsafe { msg_send![super(self), sendEvent: event] } } } } ); -fn maybe_dispatch_device_event(event: &NSEvent) { +fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) { let event_type = unsafe { event.r#type() }; #[allow(non_upper_case_globals)] match event_type { @@ -62,33 +63,33 @@ fn maybe_dispatch_device_event(event: &NSEvent) { let delta_y = unsafe { event.deltaY() } as f64; if delta_x != 0.0 { - queue_device_event(DeviceEvent::Motion { + delegate.queue_device_event(DeviceEvent::Motion { axis: 0, value: delta_x, }); } if delta_y != 0.0 { - queue_device_event(DeviceEvent::Motion { + delegate.queue_device_event(DeviceEvent::Motion { axis: 1, value: delta_y, }) } if delta_x != 0.0 || delta_y != 0.0 { - queue_device_event(DeviceEvent::MouseMotion { + delegate.queue_device_event(DeviceEvent::MouseMotion { delta: (delta_x, delta_y), }); } } NSEventTypeLeftMouseDown | NSEventTypeRightMouseDown | NSEventTypeOtherMouseDown => { - queue_device_event(DeviceEvent::Button { + delegate.queue_device_event(DeviceEvent::Button { button: unsafe { event.buttonNumber() } as u32, state: ElementState::Pressed, }); } NSEventTypeLeftMouseUp | NSEventTypeRightMouseUp | NSEventTypeOtherMouseUp => { - queue_device_event(DeviceEvent::Button { + delegate.queue_device_event(DeviceEvent::Button { button: unsafe { event.buttonNumber() } as u32, state: ElementState::Released, }); @@ -96,11 +97,3 @@ fn maybe_dispatch_device_event(event: &NSEvent) { _ => (), } } - -fn queue_device_event(event: DeviceEvent) { - let event = Event::DeviceEvent { - device_id: DEVICE_ID, - event, - }; - AppState::queue_event(event); -} diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index 5439c0e9..c6d239e0 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,19 +1,56 @@ -use icrate::AppKit::{NSApplicationActivationPolicy, NSApplicationDelegate}; -use icrate::Foundation::{MainThreadMarker, NSObject, NSObjectProtocol}; +use std::cell::{Cell, RefCell, RefMut}; +use std::collections::VecDeque; +use std::fmt; +use std::mem; +use std::rc::{Rc, Weak}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Instant; + +use icrate::AppKit::{NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate}; +use icrate::Foundation::{MainThreadMarker, NSObject, NSObjectProtocol, NSSize}; use objc2::rc::Id; use objc2::runtime::AnyObject; use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; -use super::app_state::AppState; +use super::event_loop::{stop_app_immediately, PanicInfo}; +use super::observer::{EventLoopWaker, RunLoop}; +use super::util::Never; +use super::window::WinitWindow; +use super::{menu, WindowId, DEVICE_ID}; +use crate::dpi::PhysicalSize; +use crate::event::{DeviceEvent, Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}; +use crate::window::WindowId as RootWindowId; -#[derive(Debug)] +#[derive(Debug, Default)] pub(super) struct State { activation_policy: NSApplicationActivationPolicy, default_menu: bool, activate_ignoring_other_apps: bool, + /// Whether the application is currently executing a callback. + in_callback: Cell, + /// The lifetime-erased callback. + callback: RefCell>>, + stop_on_launch: Cell, + stop_before_wait: Cell, + stop_after_wait: Cell, + stop_on_redraw: Cell, + /// Whether `applicationDidFinishLaunching:` has been run or not. + is_launched: Cell, + /// Whether an `EventLoop` is currently running. + is_running: Cell, + /// Whether the user has requested the event loop to exit. + exit: Cell, + control_flow: Cell, + waker: RefCell, + start_time: Cell>, + wait_timeout: Cell>, + pending_events: RefCell>, + pending_redraw: RefCell>, } declare_class!( + #[derive(Debug)] pub(super) struct ApplicationDelegate; unsafe impl ClassType for ApplicationDelegate { @@ -29,21 +66,56 @@ declare_class!( unsafe impl NSObjectProtocol for ApplicationDelegate {} unsafe impl NSApplicationDelegate for ApplicationDelegate { + // Note: This will, globally, only be run once, no matter how many + // `EventLoop`s the user creates. #[method(applicationDidFinishLaunching:)] fn did_finish_launching(&self, _sender: Option<&AnyObject>) { trace_scope!("applicationDidFinishLaunching:"); - AppState::launched( - self.ivars().activation_policy, - self.ivars().default_menu, - self.ivars().activate_ignoring_other_apps, - ); + self.ivars().is_launched.set(true); + + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar is initially unresponsive on macOS 10.15. + app.setActivationPolicy(self.ivars().activation_policy); + + window_activation_hack(&app); + #[allow(deprecated)] + app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps); + + if self.ivars().default_menu { + // The menubar initialization should be before the `NewEvents` event, to allow + // overriding of the default menu even if it's created + menu::initialize(&app); + } + + self.ivars().waker.borrow_mut().start(); + + self.set_is_running(true); + self.dispatch_init_events(); + + // If the application is being launched via `EventLoop::pump_events()` then we'll + // want to stop the app once it is launched (and return to the external loop) + // + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. + if self.ivars().stop_on_launch.get() { + // Note: the original idea had been to only stop the underlying `RunLoop` + // for the app but that didn't work as expected (`-[NSApplication run]` + // effectively ignored the attempt to stop the RunLoop and re-started it). + // + // So we return from `pump_events` by stopping the application. + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } } #[method(applicationWillTerminate:)] fn will_terminate(&self, _sender: Option<&AnyObject>) { trace_scope!("applicationWillTerminate:"); // TODO: Notify every window that it will be destroyed, like done in iOS? - AppState::internal_exit(); + self.internal_exit(); } } ); @@ -59,7 +131,461 @@ impl ApplicationDelegate { activation_policy, default_menu, activate_ignoring_other_apps, + ..Default::default() }); unsafe { msg_send_id![super(this), init] } } + + pub fn get(mtm: MainThreadMarker) -> Id { + let app = NSApplication::sharedApplication(mtm); + let delegate = + unsafe { app.delegate() }.expect("a delegate was not configured on the application"); + if delegate.is_kind_of::() { + // SAFETY: Just checked that the delegate is an instance of `ApplicationDelegate` + unsafe { Id::cast(delegate) } + } else { + panic!("tried to get a delegate that was not the one Winit has registered") + } + } + + /// Associate the application's event callback with the application delegate. + /// + /// # Safety + /// This is ignoring the lifetime of the application callback (which may not be 'static) + /// and can lead to undefined behaviour if the callback is not cleared before the end of + /// its real lifetime. + /// + /// All public APIs that take an event callback (`run`, `run_on_demand`, + /// `pump_events`) _must_ pair a call to `set_callback` with + /// a call to `clear_callback` before returning to avoid undefined behaviour. + pub unsafe fn set_callback( + &self, + callback: Weak>, + window_target: Rc, + receiver: Rc>, + ) { + *self.ivars().callback.borrow_mut() = Some(Box::new(EventLoopHandler { + callback, + window_target, + receiver, + })); + } + + pub fn clear_callback(&self) { + self.ivars().callback.borrow_mut().take(); + } + + fn have_callback(&self) -> bool { + self.ivars().callback.borrow().is_some() + } + + /// If `pump_events` is called to progress the event loop then we + /// bootstrap the event loop via `-[NSAppplication run]` but will use + /// `CFRunLoopRunInMode` for subsequent calls to `pump_events`. + pub fn set_stop_on_launch(&self) { + self.ivars().stop_on_launch.set(true); + } + + pub fn set_stop_before_wait(&self, value: bool) { + self.ivars().stop_before_wait.set(value) + } + + pub fn set_stop_after_wait(&self, value: bool) { + self.ivars().stop_after_wait.set(value) + } + + pub fn set_stop_on_redraw(&self, value: bool) { + self.ivars().stop_on_redraw.set(value) + } + + pub fn set_wait_timeout(&self, value: Option) { + self.ivars().wait_timeout.set(value) + } + + /// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits. + /// + /// Note: that if the `NSApplication` has been launched then that state is preserved, + /// and we won't need to re-launch the app if subsequent EventLoops are run. + pub fn internal_exit(&self) { + self.set_in_callback(true); + self.handle_nonuser_event(Event::LoopExiting); + self.set_in_callback(false); + + self.set_is_running(false); + self.set_stop_on_redraw(false); + self.set_stop_before_wait(false); + self.set_stop_after_wait(false); + self.set_wait_timeout(None); + self.clear_callback(); + } + + pub fn is_launched(&self) -> bool { + self.ivars().is_launched.get() + } + + pub fn set_is_running(&self, value: bool) { + self.ivars().is_running.set(value) + } + + pub fn is_running(&self) -> bool { + self.ivars().is_running.get() + } + + pub fn exit(&self) { + self.ivars().exit.set(true) + } + + pub fn clear_exit(&self) { + self.ivars().exit.set(false) + } + + pub fn exiting(&self) -> bool { + self.ivars().exit.get() + } + + fn set_in_callback(&self, value: bool) { + self.ivars().in_callback.set(value) + } + + pub fn set_control_flow(&self, value: ControlFlow) { + self.ivars().control_flow.set(value) + } + + pub fn control_flow(&self) -> ControlFlow { + self.ivars().control_flow.get() + } + + pub fn queue_window_event(&self, window_id: WindowId, event: WindowEvent) { + self.ivars() + .pending_events + .borrow_mut() + .push_back(QueuedEvent::WindowEvent(window_id, event)); + } + + pub fn queue_device_event(&self, event: DeviceEvent) { + self.ivars() + .pending_events + .borrow_mut() + .push_back(QueuedEvent::DeviceEvent(event)); + } + + pub fn queue_static_scale_factor_changed_event( + &self, + window: Id, + suggested_size: PhysicalSize, + scale_factor: f64, + ) { + self.ivars() + .pending_events + .borrow_mut() + .push_back(QueuedEvent::ScaleFactorChanged { + window, + suggested_size, + scale_factor, + }); + } + + pub fn handle_redraw(&self, window_id: WindowId) { + let mtm = MainThreadMarker::from(self); + // Redraw request might come out of order from the OS. + // -> Don't go back into the callback when our callstack originates from there + if !self.ivars().in_callback.get() { + self.handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + self.ivars().in_callback.set(false); + + // `pump_events` will request to stop immediately _after_ dispatching RedrawRequested events + // as a way to ensure that `pump_events` can't block an external loop indefinitely + if self.ivars().stop_on_redraw.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + } + + pub fn queue_redraw(&self, window_id: WindowId) { + let mut pending_redraw = self.ivars().pending_redraw.borrow_mut(); + if !pending_redraw.contains(&window_id) { + pending_redraw.push(window_id); + } + unsafe { RunLoop::get() }.wakeup(); + } + + fn handle_nonuser_event(&self, event: Event) { + if let Some(ref mut callback) = *self.ivars().callback.borrow_mut() { + callback.handle_nonuser_event(event) + } + } + + /// dispatch `NewEvents(Init)` + `Resumed` + pub fn dispatch_init_events(&self) { + self.set_in_callback(true); + self.handle_nonuser_event(Event::NewEvents(StartCause::Init)); + // NB: For consistency all platforms must emit a 'resumed' event even though macOS + // applications don't themselves have a formal suspend/resume lifecycle. + self.handle_nonuser_event(Event::Resumed); + self.set_in_callback(false); + } + + // Called by RunLoopObserver after finishing waiting for new events + pub fn wakeup(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + + // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 + if panic_info.is_panicking() + || self.ivars().in_callback.get() + || !self.have_callback() + || !self.is_running() + { + return; + } + + if self.ivars().stop_after_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + + let start = self.ivars().start_time.get().unwrap(); + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(requested_resume) => { + if Instant::now() >= requested_resume { + StartCause::ResumeTimeReached { + start, + requested_resume, + } + } else { + StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + } + } + } + }; + + self.set_in_callback(true); + self.handle_nonuser_event(Event::NewEvents(cause)); + self.set_in_callback(false); + } + + // Called by RunLoopObserver before waiting for new events + pub fn cleared(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + + // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 + // XXX: how does it make sense that `in_callback()` can ever return `true` here if we're + // about to return to the `CFRunLoop` to poll for new events? + if panic_info.is_panicking() + || self.ivars().in_callback.get() + || !self.have_callback() + || !self.is_running() + { + return; + } + + self.set_in_callback(true); + if let Some(ref mut callback) = *self.ivars().callback.borrow_mut() { + callback.handle_user_events(); + } + + let events = mem::take(&mut *self.ivars().pending_events.borrow_mut()); + for event in events { + match event { + QueuedEvent::WindowEvent(window_id, event) => { + self.handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event, + }); + } + QueuedEvent::DeviceEvent(event) => { + self.handle_nonuser_event(Event::DeviceEvent { + device_id: DEVICE_ID, + event, + }); + } + QueuedEvent::ScaleFactorChanged { + window, + suggested_size, + scale_factor, + } => { + if let Some(ref mut callback) = *self.ivars().callback.borrow_mut() { + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + let scale_factor_changed_event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }; + + callback.handle_nonuser_event(scale_factor_changed_event); + + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + let logical_size = physical_size.to_logical(scale_factor); + let size = NSSize::new(logical_size.width, logical_size.height); + window.setContentSize(size); + + let resized_event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Resized(physical_size), + }; + callback.handle_nonuser_event(resized_event); + } + } + } + } + + let redraw = mem::take(&mut *self.ivars().pending_redraw.borrow_mut()); + for window_id in redraw { + self.handle_nonuser_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_nonuser_event(Event::AboutToWait); + self.set_in_callback(false); + + if self.exiting() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + + if self.ivars().stop_before_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + self.ivars().start_time.set(Some(Instant::now())); + let wait_timeout = self.ivars().wait_timeout.get(); // configured by pump_events + let app_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Instant::now()), + ControlFlow::WaitUntil(instant) => Some(instant), + }; + self.ivars() + .waker + .borrow_mut() + .start_at(min_timeout(wait_timeout, app_timeout)); + } +} + +#[derive(Debug)] +pub(crate) enum QueuedEvent { + WindowEvent(WindowId, WindowEvent), + DeviceEvent(DeviceEvent), + ScaleFactorChanged { + window: Id, + suggested_size: PhysicalSize, + scale_factor: f64, + }, +} + +trait EventHandler: fmt::Debug { + // Not sure probably it should accept Event<'static, Never> + fn handle_nonuser_event(&mut self, event: Event); + fn handle_user_events(&mut self); +} + +pub(super) type Callback = RefCell, &RootWindowTarget)>; + +struct EventLoopHandler { + callback: Weak>, + window_target: Rc, + receiver: Rc>, +} + +impl fmt::Debug for EventLoopHandler { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("EventLoopHandler") + .field("window_target", &self.window_target) + .finish_non_exhaustive() + } +} + +impl EventLoopHandler { + fn with_callback(&mut self, f: F) + where + F: FnOnce(&mut EventLoopHandler, RefMut<'_, dyn FnMut(Event, &RootWindowTarget)>), + { + // `NSApplication` and our app delegate are global state and so it's possible + // that we could get a delegate callback after the application has exit an + // `EventLoop`. If the loop has been exit then our weak `self.callback` + // will fail to upgrade. + // + // We don't want to panic or output any verbose logging if we fail to + // upgrade the weak reference since it might be valid that the application + // re-starts the `NSApplication` after exiting a Winit `EventLoop` + if let Some(callback) = self.callback.upgrade() { + let callback = callback.borrow_mut(); + (f)(self, callback); + } + } +} + +impl EventHandler for EventLoopHandler { + fn handle_nonuser_event(&mut self, event: Event) { + // `Never` can't be constructed, so the `UserEvent` variant can't + // be present here. + let event = event.map_nonuser_event().unwrap_or_else(|_| unreachable!()); + + self.with_callback(|this, mut callback| { + (callback)(event, &this.window_target); + }); + } + + fn handle_user_events(&mut self) { + self.with_callback(|this, mut callback| { + for event in this.receiver.try_iter() { + (callback)(Event::UserEvent(event), &this.window_target); + } + }); + } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| { + b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) + }) +} + +/// A hack to make activation of multiple windows work when creating them before +/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. +/// +/// Alternative to this would be the user calling `window.set_visible(true)` in +/// `StartCause::Init`. +/// +/// If this becomes too bothersome to maintain, it can probably be removed +/// without too much damage. +fn window_activation_hack(app: &NSApplication) { + // TODO: Proper ordering of the windows + app.windows().into_iter().for_each(|window| { + // Call `makeKeyAndOrderFront` if it was called on the window in `WinitWindow::new` + // This way we preserve the user's desired initial visiblity status + // TODO: Also filter on the type/"level" of the window, and maybe other things? + if window.isVisible() { + log::trace!("Activating visible window"); + window.makeKeyAndOrderFront(None); + } else { + log::trace!("Skipping activating invisible window"); + } + }) } diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs deleted file mode 100644 index aaa4b94c..00000000 --- a/src/platform_impl/macos/app_state.rs +++ /dev/null @@ -1,712 +0,0 @@ -use std::{ - cell::{RefCell, RefMut}, - collections::VecDeque, - fmt::{self, Debug}, - mem, - rc::{Rc, Weak}, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc, Arc, Mutex, MutexGuard, - }, - time::Instant, -}; - -use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp}; -use icrate::AppKit::{NSApplication, NSApplicationActivationPolicy}; -use icrate::Foundation::{is_main_thread, MainThreadMarker, NSSize}; -use log::trace; -use objc2::rc::{autoreleasepool, Id}; -use once_cell::sync::Lazy; - -use super::{ - event::dummy_event, event_loop::PanicInfo, menu, observer::EventLoopWaker, util::Never, - window::WinitWindow, -}; -use crate::{ - dpi::PhysicalSize, - event::{Event, InnerSizeWriter, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, - window::WindowId, -}; - -static HANDLER: Lazy = Lazy::new(Default::default); - -impl Event { - fn userify(self) -> Event { - self.map_nonuser_event() - // `Never` can't be constructed, so the `UserEvent` variant can't - // be present here. - .unwrap_or_else(|_| unreachable!()) - } -} - -pub trait EventHandler: Debug { - // Not sure probably it should accept Event<'static, Never> - fn handle_nonuser_event(&mut self, event: Event); - fn handle_user_events(&mut self); -} - -pub(crate) type Callback = RefCell, &RootWindowTarget)>; - -struct EventLoopHandler { - callback: Weak>, - window_target: Rc, - receiver: Rc>, -} - -impl EventLoopHandler { - fn with_callback(&mut self, f: F) - where - F: FnOnce(&mut EventLoopHandler, RefMut<'_, dyn FnMut(Event, &RootWindowTarget)>), - { - // `NSApplication` and our `HANDLER` are global state and so it's possible - // that we could get a delegate callback after the application has exit an - // `EventLoop`. If the loop has been exit then our weak `self.callback` - // will fail to upgrade. - // - // We don't want to panic or output any verbose logging if we fail to - // upgrade the weak reference since it might be valid that the application - // re-starts the `NSApplication` after exiting a Winit `EventLoop` - if let Some(callback) = self.callback.upgrade() { - let callback = callback.borrow_mut(); - (f)(self, callback); - } - } -} - -impl Debug for EventLoopHandler { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("EventLoopHandler") - .field("window_target", &self.window_target) - .finish() - } -} - -impl EventHandler for EventLoopHandler { - fn handle_nonuser_event(&mut self, event: Event) { - self.with_callback(|this, mut callback| { - (callback)(event.userify(), &this.window_target); - }); - } - - fn handle_user_events(&mut self) { - self.with_callback(|this, mut callback| { - for event in this.receiver.try_iter() { - (callback)(Event::UserEvent(event), &this.window_target); - } - }); - } -} - -#[derive(Debug)] -enum EventWrapper { - StaticEvent(Event), - ScaleFactorChanged { - window: Id, - suggested_size: PhysicalSize, - scale_factor: f64, - }, -} - -#[derive(Default)] -struct Handler { - stop_app_on_launch: AtomicBool, - stop_app_before_wait: AtomicBool, - stop_app_after_wait: AtomicBool, - stop_app_on_redraw: AtomicBool, - launched: AtomicBool, - running: AtomicBool, - in_callback: AtomicBool, - control_flow: Mutex, - exit: AtomicBool, - start_time: Mutex>, - callback: Mutex>>, - pending_events: Mutex>, - pending_redraw: Mutex>, - wait_timeout: Mutex>, - waker: Mutex, -} - -unsafe impl Send for Handler {} -unsafe impl Sync for Handler {} - -impl Handler { - fn events(&self) -> MutexGuard<'_, VecDeque> { - self.pending_events.lock().unwrap() - } - - fn redraw(&self) -> MutexGuard<'_, Vec> { - self.pending_redraw.lock().unwrap() - } - - fn waker(&self) -> MutexGuard<'_, EventLoopWaker> { - self.waker.lock().unwrap() - } - - /// `true` after `ApplicationDelegate::applicationDidFinishLaunching` called - /// - /// NB: This is global / `NSApplication` state and since the app will only - /// be launched once but an `EventLoop` may be run more than once then only - /// the first `EventLoop` will observe the application before it is launched. - fn is_launched(&self) -> bool { - self.launched.load(Ordering::Acquire) - } - - /// Set via `ApplicationDelegate::applicationDidFinishLaunching` - fn set_launched(&self) { - self.launched.store(true, Ordering::Release); - } - - /// `true` if an `EventLoop` is currently running - /// - /// NB: This is global / `NSApplication` state and may persist beyond the - /// lifetime of a running `EventLoop`. - /// - /// # Caveat - /// This is only intended to be called from the main thread - fn is_running(&self) -> bool { - self.running.load(Ordering::Relaxed) - } - - /// Set when an `EventLoop` starts running, after the `NSApplication` is launched - /// - /// # Caveat - /// This is only intended to be called from the main thread - fn set_running(&self) { - self.running.store(true, Ordering::Relaxed); - } - - /// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits - /// - /// Since an `EventLoop` may be run more than once we need make sure to reset the - /// `control_flow` state back to `Poll` each time the loop exits. - /// - /// Note: that if the `NSApplication` has been launched then that state is preserved, - /// and we won't need to re-launch the app if subsequent EventLoops are run. - /// - /// # Caveat - /// This is only intended to be called from the main thread - fn internal_exit(&self) { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interiour mutability - // - // XXX: As an aside; having each individual bit of state for `Handler` be atomic or wrapped in a - // `Mutex` for the sake of interior mutability seems a bit odd, and also a potential foot - // gun in case the state is unwittingly accessed across threads because the fine-grained locking - // wouldn't ensure that there's interior consistency. - // - // Maybe the whole thing should just be put in a static `Mutex<>` to make it clear - // the we can mutate more than one peice of state while maintaining consistency. (though it - // looks like there have been recuring re-entrancy issues with callback handling that might - // make that awkward) - self.running.store(false, Ordering::Relaxed); - self.set_stop_app_on_redraw_requested(false); - self.set_stop_app_before_wait(false); - self.set_stop_app_after_wait(false); - self.set_wait_timeout(None); - } - - pub fn exit(&self) { - self.exit.store(true, Ordering::Relaxed) - } - - pub fn clear_exit(&self) { - self.exit.store(false, Ordering::Relaxed) - } - - pub fn exiting(&self) -> bool { - self.exit.load(Ordering::Relaxed) - } - - pub fn request_stop_app_on_launch(&self) { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_on_launch.store(true, Ordering::Relaxed); - } - - pub fn should_stop_app_on_launch(&self) -> bool { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_on_launch.load(Ordering::Relaxed) - } - - pub fn set_stop_app_before_wait(&self, stop_before_wait: bool) { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_before_wait - .store(stop_before_wait, Ordering::Relaxed); - } - - pub fn should_stop_app_before_wait(&self) -> bool { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_before_wait.load(Ordering::Relaxed) - } - - pub fn set_stop_app_after_wait(&self, stop_after_wait: bool) { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_after_wait - .store(stop_after_wait, Ordering::Relaxed); - } - - pub fn set_wait_timeout(&self, new_timeout: Option) { - let mut timeout = self.wait_timeout.lock().unwrap(); - *timeout = new_timeout; - } - - pub fn wait_timeout(&self) -> Option { - *self.wait_timeout.lock().unwrap() - } - - pub fn should_stop_app_after_wait(&self) -> bool { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_after_wait.load(Ordering::Relaxed) - } - - pub fn set_stop_app_on_redraw_requested(&self, stop_on_redraw: bool) { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_on_redraw - .store(stop_on_redraw, Ordering::Relaxed); - } - - pub fn should_stop_app_on_redraw_requested(&self) -> bool { - // Relaxed ordering because we don't actually have multiple threads involved, we just want - // interior mutability - self.stop_app_on_redraw.load(Ordering::Relaxed) - } - - fn set_control_flow(&self, new_control_flow: ControlFlow) { - *self.control_flow.lock().unwrap() = new_control_flow - } - - fn control_flow(&self) -> ControlFlow { - *self.control_flow.lock().unwrap() - } - - fn get_start_time(&self) -> Option { - *self.start_time.lock().unwrap() - } - - fn update_start_time(&self) { - *self.start_time.lock().unwrap() = Some(Instant::now()); - } - - fn take_events(&self) -> VecDeque { - mem::take(&mut *self.events()) - } - - fn should_redraw(&self) -> Vec { - mem::take(&mut *self.redraw()) - } - - fn get_in_callback(&self) -> bool { - self.in_callback.load(Ordering::Acquire) - } - - fn set_in_callback(&self, in_callback: bool) { - self.in_callback.store(in_callback, Ordering::Release); - } - - fn have_callback(&self) -> bool { - self.callback.lock().unwrap().is_some() - } - - fn handle_nonuser_event(&self, event: Event) { - if let Some(ref mut callback) = *self.callback.lock().unwrap() { - callback.handle_nonuser_event(event) - } - } - - fn handle_user_events(&self) { - if let Some(ref mut callback) = *self.callback.lock().unwrap() { - callback.handle_user_events(); - } - } - - fn handle_scale_factor_changed_event( - &self, - window: &WinitWindow, - suggested_size: PhysicalSize, - scale_factor: f64, - ) { - if let Some(ref mut callback) = *self.callback.lock().unwrap() { - let new_inner_size = Arc::new(Mutex::new(suggested_size)); - let scale_factor_changed_event = Event::WindowEvent { - window_id: WindowId(window.id()), - event: WindowEvent::ScaleFactorChanged { - scale_factor, - inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), - }, - }; - - callback.handle_nonuser_event(scale_factor_changed_event); - - let physical_size = *new_inner_size.lock().unwrap(); - drop(new_inner_size); - let logical_size = physical_size.to_logical(scale_factor); - let size = NSSize::new(logical_size.width, logical_size.height); - window.setContentSize(size); - - let resized_event = Event::WindowEvent { - window_id: WindowId(window.id()), - event: WindowEvent::Resized(physical_size), - }; - callback.handle_nonuser_event(resized_event); - } - } -} - -pub(crate) enum AppState {} - -impl AppState { - /// Associate the application's event callback with the (global static) Handler state - /// - /// # Safety - /// This is ignoring the lifetime of the application callback (which may not be 'static) - /// and can lead to undefined behaviour if the callback is not cleared before the end of - /// its real lifetime. - /// - /// All public APIs that take an event callback (`run`, `run_on_demand`, - /// `pump_events`) _must_ pair a call to `set_callback` with - /// a call to `clear_callback` before returning to avoid undefined behaviour. - pub unsafe fn set_callback( - callback: Weak>, - window_target: Rc, - receiver: Rc>, - ) { - *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { - callback, - window_target, - receiver, - })); - } - - pub fn clear_callback() { - HANDLER.callback.lock().unwrap().take(); - } - - pub fn is_launched() -> bool { - HANDLER.is_launched() - } - - pub fn is_running() -> bool { - HANDLER.is_running() - } - - // If `pump_events` is called to progress the event loop then we bootstrap the event - // loop via `-[NSAppplication run]` but will use `CFRunLoopRunInMode` for subsequent calls to - // `pump_events` - pub fn request_stop_on_launch() { - HANDLER.request_stop_app_on_launch(); - } - - pub fn set_stop_app_before_wait(stop_before_wait: bool) { - HANDLER.set_stop_app_before_wait(stop_before_wait); - } - - pub fn set_stop_app_after_wait(stop_after_wait: bool) { - HANDLER.set_stop_app_after_wait(stop_after_wait); - } - - pub fn set_wait_timeout(timeout: Option) { - HANDLER.set_wait_timeout(timeout); - } - - pub fn set_stop_app_on_redraw_requested(stop_on_redraw: bool) { - HANDLER.set_stop_app_on_redraw_requested(stop_on_redraw); - } - - pub fn set_control_flow(control_flow: ControlFlow) { - HANDLER.set_control_flow(control_flow) - } - - pub fn control_flow() -> ControlFlow { - HANDLER.control_flow() - } - - pub fn internal_exit() { - HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(Event::LoopExiting); - HANDLER.set_in_callback(false); - HANDLER.internal_exit(); - Self::clear_callback(); - } - - pub fn exit() { - HANDLER.exit() - } - - pub fn clear_exit() { - HANDLER.clear_exit() - } - - pub fn exiting() -> bool { - HANDLER.exiting() - } - - pub fn dispatch_init_events() { - HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(Event::NewEvents(StartCause::Init)); - // NB: For consistency all platforms must emit a 'resumed' event even though macOS - // applications don't themselves have a formal suspend/resume lifecycle. - HANDLER.handle_nonuser_event(Event::Resumed); - HANDLER.set_in_callback(false); - } - - pub fn start_running() { - debug_assert!(HANDLER.is_launched()); - - HANDLER.set_running(); - Self::dispatch_init_events() - } - - pub fn launched( - activation_policy: NSApplicationActivationPolicy, - create_default_menu: bool, - activate_ignoring_other_apps: bool, - ) { - let mtm = MainThreadMarker::new().unwrap(); - let app = NSApplication::sharedApplication(mtm); - // We need to delay setting the activation policy and activating the app - // until `applicationDidFinishLaunching` has been called. Otherwise the - // menu bar is initially unresponsive on macOS 10.15. - app.setActivationPolicy(activation_policy); - - window_activation_hack(&app); - #[allow(deprecated)] - app.activateIgnoringOtherApps(activate_ignoring_other_apps); - - HANDLER.set_launched(); - HANDLER.waker().start(); - if create_default_menu { - // The menubar initialization should be before the `NewEvents` event, to allow - // overriding of the default menu even if it's created - menu::initialize(&app); - } - - Self::start_running(); - - // If the application is being launched via `EventLoop::pump_events()` then we'll - // want to stop the app once it is launched (and return to the external loop) - // - // In this case we still want to consider Winit's `EventLoop` to be "running", - // so we call `start_running()` above. - if HANDLER.should_stop_app_on_launch() { - // Note: the original idea had been to only stop the underlying `RunLoop` - // for the app but that didn't work as expected (`-[NSApplication run]` - // effectively ignored the attempt to stop the RunLoop and re-started it). - // - // So we return from `pump_events` by stopping the application. - Self::stop(); - } - } - - // Called by RunLoopObserver after finishing waiting for new events - pub fn wakeup(panic_info: Weak) { - let panic_info = panic_info - .upgrade() - .expect("The panic info must exist here. This failure indicates a developer error."); - - // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 - if panic_info.is_panicking() - || HANDLER.get_in_callback() - || !HANDLER.have_callback() - || !HANDLER.is_running() - { - return; - } - - if HANDLER.should_stop_app_after_wait() { - Self::stop(); - } - - let start = HANDLER.get_start_time().unwrap(); - let cause = match HANDLER.control_flow() { - ControlFlow::Poll => StartCause::Poll, - ControlFlow::Wait => StartCause::WaitCancelled { - start, - requested_resume: None, - }, - ControlFlow::WaitUntil(requested_resume) => { - if Instant::now() >= requested_resume { - StartCause::ResumeTimeReached { - start, - requested_resume, - } - } else { - StartCause::WaitCancelled { - start, - requested_resume: Some(requested_resume), - } - } - } - }; - HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(Event::NewEvents(cause)); - HANDLER.set_in_callback(false); - } - - // This is called from multiple threads at present - pub fn queue_redraw(window_id: WindowId) { - let mut pending_redraw = HANDLER.redraw(); - if !pending_redraw.contains(&window_id) { - pending_redraw.push(window_id); - } - unsafe { - let rl = CFRunLoopGetMain(); - CFRunLoopWakeUp(rl); - } - } - - pub fn handle_redraw(window_id: WindowId) { - // Redraw request might come out of order from the OS. - // -> Don't go back into the callback when our callstack originates from there - if !HANDLER.in_callback.swap(true, Ordering::AcqRel) { - HANDLER.handle_nonuser_event(Event::WindowEvent { - window_id, - event: WindowEvent::RedrawRequested, - }); - HANDLER.set_in_callback(false); - - // `pump_events` will request to stop immediately _after_ dispatching RedrawRequested events - // as a way to ensure that `pump_events` can't block an external loop indefinitely - if HANDLER.should_stop_app_on_redraw_requested() { - AppState::stop(); - } - } - } - - pub fn queue_event(event: Event) { - if !is_main_thread() { - panic!("Event queued from different thread: {event:#?}"); - } - HANDLER.events().push_back(EventWrapper::StaticEvent(event)); - } - - pub fn queue_static_scale_factor_changed_event( - window: Id, - suggested_size: PhysicalSize, - scale_factor: f64, - ) { - HANDLER - .events() - .push_back(EventWrapper::ScaleFactorChanged { - window, - suggested_size, - scale_factor, - }); - } - - pub fn stop() { - let mtm = MainThreadMarker::new().unwrap(); - let app = NSApplication::sharedApplication(mtm); - autoreleasepool(|_| { - app.stop(None); - // To stop event loop immediately, we need to post some event here. - app.postEvent_atStart(&dummy_event().unwrap(), true); - }); - } - - // Called by RunLoopObserver before waiting for new events - pub fn cleared(panic_info: Weak) { - let panic_info = panic_info - .upgrade() - .expect("The panic info must exist here. This failure indicates a developer error."); - - // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 - // XXX: how does it make sense that `get_in_callback()` can ever return `true` here if we're - // about to return to the `CFRunLoop` to poll for new events? - if panic_info.is_panicking() - || HANDLER.get_in_callback() - || !HANDLER.have_callback() - || !HANDLER.is_running() - { - return; - } - - HANDLER.set_in_callback(true); - HANDLER.handle_user_events(); - for event in HANDLER.take_events() { - match event { - EventWrapper::StaticEvent(event) => { - HANDLER.handle_nonuser_event(event); - } - EventWrapper::ScaleFactorChanged { - window, - suggested_size, - scale_factor, - } => { - HANDLER.handle_scale_factor_changed_event( - &window, - suggested_size, - scale_factor, - ); - } - } - } - - for window_id in HANDLER.should_redraw() { - HANDLER.handle_nonuser_event(Event::WindowEvent { - window_id, - event: WindowEvent::RedrawRequested, - }); - } - - HANDLER.handle_nonuser_event(Event::AboutToWait); - HANDLER.set_in_callback(false); - - if HANDLER.exiting() { - Self::stop(); - } - - if HANDLER.should_stop_app_before_wait() { - Self::stop(); - } - HANDLER.update_start_time(); - let wait_timeout = HANDLER.wait_timeout(); // configured by pump_events - let app_timeout = match HANDLER.control_flow() { - ControlFlow::Wait => None, - ControlFlow::Poll => Some(Instant::now()), - ControlFlow::WaitUntil(instant) => Some(instant), - }; - HANDLER - .waker() - .start_at(min_timeout(wait_timeout, app_timeout)); - } -} - -/// Returns the minimum `Option`, taking into account that `None` -/// equates to an infinite timeout, not a zero timeout (so can't just use -/// `Option::min`) -fn min_timeout(a: Option, b: Option) -> Option { - a.map_or(b, |a_timeout| { - b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout))) - }) -} - -/// A hack to make activation of multiple windows work when creating them before -/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. -/// -/// Alternative to this would be the user calling `window.set_visible(true)` in -/// `StartCause::Init`. -/// -/// If this becomes too bothersome to maintain, it can probably be removed -/// without too much damage. -fn window_activation_hack(app: &NSApplication) { - // TODO: Proper ordering of the windows - app.windows().into_iter().for_each(|window| { - // Call `makeKeyAndOrderFront` if it was called on the window in `WinitWindow::new` - // This way we preserve the user's desired initial visiblity status - // TODO: Also filter on the type/"level" of the window, and maybe other things? - if window.isVisible() { - trace!("Activating visible window"); - window.makeKeyAndOrderFront(None); - } else { - trace!("Skipping activating invisible window"); - } - }) -} diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 8464bd0d..2e4e5e4c 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -29,6 +29,12 @@ use objc2::{ }; use super::event::dummy_event; +use super::{ + app::WinitApplication, + app_delegate::{ApplicationDelegate, Callback}, + monitor::{self, MonitorHandle}, + observer::setup_control_flow_observers, +}; use crate::{ error::EventLoopError, event::Event, @@ -36,13 +42,6 @@ use crate::{ ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget, }, platform::{macos::ActivationPolicy, pump_events::PumpStatus}, - platform_impl::platform::{ - app::WinitApplication, - app_delegate::ApplicationDelegate, - app_state::{AppState, Callback}, - monitor::{self, MonitorHandle}, - observer::setup_control_flow_observers, - }, }; #[derive(Default)] @@ -75,6 +74,7 @@ impl PanicInfo { #[derive(Debug)] pub struct EventLoopWindowTarget { + delegate: Id, mtm: MainThreadMarker, } @@ -110,23 +110,23 @@ impl EventLoopWindowTarget { } pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { - AppState::set_control_flow(control_flow) + self.delegate.set_control_flow(control_flow) } pub(crate) fn control_flow(&self) -> ControlFlow { - AppState::control_flow() + self.delegate.control_flow() } pub(crate) fn exit(&self) { - AppState::exit() + self.delegate.exit() } pub(crate) fn clear_exit(&self) { - AppState::clear_exit() + self.delegate.clear_exit() } pub(crate) fn exiting(&self) -> bool { - AppState::exiting() + self.delegate.exiting() } } @@ -154,9 +154,11 @@ pub struct EventLoop { /// We intentially don't store `WinitApplication` since we want to have /// the possiblity of swapping that out at some point. app: Id, - /// The delegate is only weakly referenced by NSApplication, so we keep - /// it around here as well. - _delegate: Id, + /// The application delegate that we've registered. + /// + /// The delegate is only weakly referenced by NSApplication, so we must + /// keep it around here as well. + delegate: Id, // Event sender and receiver, used for EventLoopProxy. sender: mpsc::Sender, @@ -227,11 +229,11 @@ impl EventLoop { let (sender, receiver) = mpsc::channel(); Ok(EventLoop { app, - _delegate: delegate, + delegate: delegate.clone(), sender, receiver: Rc::new(receiver), window_target: Rc::new(RootWindowTarget { - p: EventLoopWindowTarget { mtm }, + p: EventLoopWindowTarget { delegate, mtm }, _marker: PhantomData, }), panic_info, @@ -258,18 +260,18 @@ impl EventLoop { where F: FnMut(Event, &RootWindowTarget), { - if AppState::is_running() { + if self.delegate.is_running() { return Err(EventLoopError::AlreadyRunning); } // # Safety // We are erasing the lifetime of the application callback here so that we - // can (temporarily) store it within 'static global `AppState` that's + // can (temporarily) store it within 'static app delegate that's // accessible to objc delegate callbacks. // // The safety of this depends on on making sure to also clear the callback - // from the global `AppState` before we return from here, ensuring that - // we don't retain a reference beyond the real lifetime of the callback. + // from the app delegate before we return from here, ensuring that we don't + // retain a reference beyond the real lifetime of the callback. let callback = unsafe { mem::transmute::< @@ -287,9 +289,9 @@ impl EventLoop { drop(callback); // # Safety - // We make sure to call `AppState::clear_callback` before returning + // We make sure to call `delegate.clear_callback` before returning unsafe { - AppState::set_callback( + self.delegate.set_callback( weak_cb, Rc::clone(&self.window_target), Rc::clone(&self.receiver), @@ -297,17 +299,18 @@ impl EventLoop { } // catch panics to make sure we can't unwind without clearing the set callback - // (which would leave the global `AppState` in an undefined, unsafe state) + // (which would leave the app delegate in an undefined, unsafe state) let catch_result = catch_unwind(AssertUnwindSafe(|| { // clear / normalize pump_events state - AppState::set_wait_timeout(None); - AppState::set_stop_app_before_wait(false); - AppState::set_stop_app_after_wait(false); - AppState::set_stop_app_on_redraw_requested(false); + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(false); + self.delegate.set_stop_on_redraw(false); - if AppState::is_launched() { - debug_assert!(!AppState::is_running()); - AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed` + if self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); } unsafe { self.app.run() }; @@ -320,15 +323,14 @@ impl EventLoop { resume_unwind(panic); } - AppState::internal_exit() + self.delegate.internal_exit() })); // # Safety // This pairs up with the `unsafe` call to `set_callback` above and ensures that - // we always clear the application callback from the global `AppState` before - // returning + // we always clear the application callback from the app delegate before returning. drop(self._callback.take()); - AppState::clear_callback(); + self.delegate.clear_callback(); if let Err(payload) = catch_result { resume_unwind(payload) @@ -344,12 +346,12 @@ impl EventLoop { { // # Safety // We are erasing the lifetime of the application callback here so that we - // can (temporarily) store it within 'static global `AppState` that's + // can (temporarily) store it within 'static global app delegate that's // accessible to objc delegate callbacks. // // The safety of this depends on on making sure to also clear the callback - // from the global `AppState` before we return from here, ensuring that - // we don't retain a reference beyond the real lifetime of the callback. + // from the app delegate before we return from here, ensuring that we don't + // retain a reference beyond the real lifetime of the callback. let callback = unsafe { mem::transmute::< @@ -367,11 +369,11 @@ impl EventLoop { drop(callback); // # Safety - // We will make sure to call `AppState::clear_callback` before returning + // We will make sure to call `delegate.clear_callback` before returning // to ensure that we don't hold on to the callback beyond its (erased) // lifetime unsafe { - AppState::set_callback( + self.delegate.set_callback( weak_cb, Rc::clone(&self.window_target), Rc::clone(&self.receiver), @@ -379,44 +381,45 @@ impl EventLoop { } // catch panics to make sure we can't unwind without clearing the set callback - // (which would leave the global `AppState` in an undefined, unsafe state) + // (which would leave the app delegate in an undefined, unsafe state) let catch_result = catch_unwind(AssertUnwindSafe(|| { // As a special case, if the application hasn't been launched yet then we at least run // the loop until it has fully launched. - if !AppState::is_launched() { - debug_assert!(!AppState::is_running()); + if !self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); - AppState::request_stop_on_launch(); + self.delegate.set_stop_on_launch(); unsafe { self.app.run(); } // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application has launched - } else if !AppState::is_running() { + } else if !self.delegate.is_running() { // Even though the application may have been launched, it's possible we aren't running // if the `EventLoop` was run before and has since exited. This indicates that // we just starting to re-run the same `EventLoop` again. - AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed` + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); } else { // Only run for as long as the given `Duration` allows so we don't block the external loop. match timeout { Some(Duration::ZERO) => { - AppState::set_wait_timeout(None); - AppState::set_stop_app_before_wait(true); + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(true); } Some(duration) => { - AppState::set_stop_app_before_wait(false); + self.delegate.set_stop_before_wait(false); let timeout = Instant::now() + duration; - AppState::set_wait_timeout(Some(timeout)); - AppState::set_stop_app_after_wait(true); + self.delegate.set_wait_timeout(Some(timeout)); + self.delegate.set_stop_after_wait(true); } None => { - AppState::set_wait_timeout(None); - AppState::set_stop_app_before_wait(false); - AppState::set_stop_app_after_wait(true); + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(true); } } - AppState::set_stop_app_on_redraw_requested(true); + self.delegate.set_stop_on_redraw(true); unsafe { self.app.run(); } @@ -431,8 +434,8 @@ impl EventLoop { resume_unwind(panic); } - if AppState::exiting() { - AppState::internal_exit(); + if self.delegate.exiting() { + self.delegate.internal_exit(); PumpStatus::Exit(0) } else { PumpStatus::Continue @@ -441,9 +444,8 @@ impl EventLoop { // # Safety // This pairs up with the `unsafe` call to `set_callback` above and ensures that - // we always clear the application callback from the global `AppState` before - // returning - AppState::clear_callback(); + // we always clear the application callback from the app delegate before returning + self.delegate.clear_callback(); drop(self._callback.take()); match catch_result { @@ -458,6 +460,15 @@ impl EventLoop { } } +pub(super) fn stop_app_immediately(app: &NSApplication) { + autoreleasepool(|_| { + app.stop(None); + // To stop event loop immediately, we need to post some event here. + // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 + app.postEvent_atStart(&dummy_event().unwrap(), true); + }); +} + /// Catches panics that happen inside `f` and when a panic /// happens, stops the `sharedApplication` #[inline] @@ -478,10 +489,7 @@ pub fn stop_app_on_panic R + UnwindSafe, R>( panic_info.set_panic(e); } let app = NSApplication::sharedApplication(mtm); - app.stop(None); - // Posting a dummy event to get `stop` to take effect immediately. - // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 - app.postEvent_atStart(&dummy_event().unwrap(), true); + stop_app_immediately(&app); None } } diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 13cd8d27..d29e19f4 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -3,7 +3,6 @@ mod util; mod app; mod app_delegate; -mod app_state; mod cursor; mod event; mod event_loop; diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index 2b15ac5f..1dd5ff2e 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -7,11 +7,6 @@ use std::{ time::Instant, }; -use crate::platform_impl::platform::{ - app_state::AppState, - event_loop::{stop_app_on_panic, PanicInfo}, - ffi, -}; use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease}; use core_foundation::date::CFAbsoluteTimeGetCurrent; use core_foundation::runloop::{ @@ -19,10 +14,16 @@ use core_foundation::runloop::{ CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, - CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CFRunLoopWakeUp, }; use icrate::Foundation::MainThreadMarker; +use super::ffi; +use super::{ + app_delegate::ApplicationDelegate, + event_loop::{stop_app_on_panic, PanicInfo}, +}; + unsafe fn control_flow_handler(panic_info: *mut c_void, f: F) where F: FnOnce(Weak) + UnwindSafe, @@ -56,7 +57,7 @@ extern "C" fn control_flow_begin_handler( match activity { kCFRunLoopAfterWaiting => { //trace!("Triggered `CFRunLoopAfterWaiting`"); - AppState::wakeup(panic_info); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()).wakeup(panic_info); //trace!("Completed `CFRunLoopAfterWaiting`"); } _ => unreachable!(), @@ -78,7 +79,7 @@ extern "C" fn control_flow_end_handler( match activity { kCFRunLoopBeforeWaiting => { //trace!("Triggered `CFRunLoopBeforeWaiting`"); - AppState::cleared(panic_info); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()).cleared(panic_info); //trace!("Completed `CFRunLoopBeforeWaiting`"); } kCFRunLoopExit => (), //unimplemented!(), // not expected to ever happen @@ -88,13 +89,17 @@ extern "C" fn control_flow_end_handler( } } -struct RunLoop(CFRunLoopRef); +pub struct RunLoop(CFRunLoopRef); impl RunLoop { - unsafe fn get() -> Self { + pub unsafe fn get() -> Self { RunLoop(unsafe { CFRunLoopGetMain() }) } + pub fn wakeup(&self) { + unsafe { CFRunLoopWakeUp(self.0) } + } + unsafe fn add_observer( &self, flags: CFOptionFlags, @@ -141,6 +146,7 @@ pub fn setup_control_flow_observers(panic_info: Weak) { } } +#[derive(Debug)] pub struct EventLoopWaker { timer: CFRunLoopTimerRef, diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 585950e7..0b7d0027 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -19,26 +19,22 @@ use objc2::{ class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass, }; +use super::app_delegate::ApplicationDelegate; use super::cursor::{default_cursor, invisible_cursor}; -use super::event::{code_to_key, code_to_location}; -use super::event::{lalt_pressed, ralt_pressed}; -use crate::platform_impl::scancode_to_physicalkey; +use super::event::{ + code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed, + scancode_to_physicalkey, +}; +use super::window::WinitWindow; +use super::{util, DEVICE_ID}; use crate::{ dpi::{LogicalPosition, LogicalSize}, event::{ - DeviceEvent, ElementState, Event, Ime, Modifiers, MouseButton, MouseScrollDelta, - TouchPhase, WindowEvent, + DeviceEvent, ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, + WindowEvent, }, keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}, platform::macos::{OptionAsAlt, WindowExtMacOS}, - platform_impl::platform::{ - app_state::AppState, - event::{create_key_event, event_mods}, - util, - window::WinitWindow, - DEVICE_ID, - }, - window::WindowId, }; #[derive(Debug)] @@ -207,7 +203,8 @@ declare_class!( // It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`. if let Some(window) = self.ivars()._ns_window.load() { - AppState::handle_redraw(WindowId(window.id())); + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.handle_redraw(window.id()); } #[allow(clippy::let_unit_value)] @@ -805,24 +802,14 @@ impl WinitView { .expect("view to have a window") } - fn window_id(&self) -> WindowId { - WindowId(self.window().id()) - } - fn queue_event(&self, event: WindowEvent) { - let event = Event::WindowEvent { - window_id: self.window_id(), - event, - }; - AppState::queue_event(event); + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.queue_window_event(self.window().id(), event); } fn queue_device_event(&self, event: DeviceEvent) { - let event = Event::DeviceEvent { - device_id: DEVICE_ID, - event, - }; - AppState::queue_event(event); + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.queue_device_event(event); } fn scale_factor(&self) -> f64 { diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 7f8266a4..c1b888fe 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -5,29 +5,6 @@ use std::f64; use std::ops; use std::sync::{Mutex, MutexGuard}; -use crate::{ - cursor::Cursor, - dpi::{ - LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, - }, - error::{ExternalError, NotSupportedError, OsError as RootOsError}, - event::WindowEvent, - icon::Icon, - platform::macos::{OptionAsAlt, WindowExtMacOS}, - platform_impl::platform::{ - app_state::AppState, - event_loop::EventLoopWindowTarget, - ffi, - monitor::{self, MonitorHandle, VideoModeHandle}, - view::WinitView, - window_delegate::WinitWindowDelegate, - Fullscreen, OsError, - }, - window::{ - CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowId as RootWindowId, WindowLevel, - }, -}; use core_graphics::display::{CGDisplay, CGPoint}; use icrate::AppKit::{ NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSApplication, @@ -50,10 +27,32 @@ use log::trace; use objc2::rc::{autoreleasepool, Id}; use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use super::app_delegate::ApplicationDelegate; use super::cursor::cursor_from_icon; -use super::ffi::kCGFloatingWindowLevel; -use super::ffi::{kCGNormalWindowLevel, CGSMainConnectionID, CGSSetWindowBackgroundBlurRadius}; +use super::event_loop::EventLoopWindowTarget; +use super::ffi::{ + self, kCGFloatingWindowLevel, kCGNormalWindowLevel, CGSMainConnectionID, + CGSSetWindowBackgroundBlurRadius, +}; +use super::monitor::{self, MonitorHandle, VideoModeHandle}; use super::monitor::{flip_window_screen_coordinates, get_display_id}; +use super::view::WinitView; +use super::window_delegate::WinitWindowDelegate; +use super::{Fullscreen, OsError}; +use crate::{ + cursor::Cursor, + dpi::{ + LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, + }, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event::WindowEvent, + icon::Icon, + platform::macos::{OptionAsAlt, WindowExtMacOS}, + window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, + }, +}; pub(crate) struct Window { window: MainThreadBound>, @@ -616,7 +615,8 @@ impl WinitWindow { } pub fn request_redraw(&self) { - AppState::queue_redraw(RootWindowId(self.id())); + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.queue_redraw(self.id()); } #[inline] diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index e4cad2c2..01bcb1a4 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -16,16 +16,15 @@ use objc2::{ class, declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass, }; +use super::app_delegate::ApplicationDelegate; use super::monitor::flip_window_screen_coordinates; use super::{ - app_state::AppState, window::{get_ns_theme, WinitWindow}, Fullscreen, }; use crate::{ dpi::{LogicalPosition, LogicalSize}, - event::{Event, WindowEvent}, - window::WindowId, + event::WindowEvent, }; #[derive(Debug)] @@ -446,11 +445,8 @@ impl WinitWindowDelegate { } pub(crate) fn queue_event(&self, event: WindowEvent) { - let event = Event::WindowEvent { - window_id: WindowId(self.ivars().window.id()), - event, - }; - AppState::queue_event(event); + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.queue_window_event(self.ivars().window.id(), event); } fn queue_static_scale_factor_changed_event(&self) { @@ -463,7 +459,9 @@ impl WinitWindowDelegate { self.ivars().previous_scale_factor.set(scale_factor); let content_size = window.contentRectForFrameRect(window.frame()).size; let content_size = LogicalSize::new(content_size.width, content_size.height); - AppState::queue_static_scale_factor_changed_event( + + let app_delegate = ApplicationDelegate::get(MainThreadMarker::from(self)); + app_delegate.queue_static_scale_factor_changed_event( window.clone(), content_size.to_physical(scale_factor), scale_factor,