diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index 4fab8d53f6..9bb929154f 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -8,7 +8,7 @@ use dispatch2::MainThreadBound; use objc2::MainThreadMarker; use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication}; use objc2_foundation::NSNotification; -use winit_common::core_foundation::EventLoopProxy; +use winit_common::core_foundation::{EventLoopProxy, MainRunLoop}; use winit_common::event_handler::EventHandler; use winit_core::application::ApplicationHandler; use winit_core::event::{StartCause, WindowEvent}; @@ -17,7 +17,7 @@ use winit_core::window::WindowId; use super::event_loop::{notify_windows_of_exit, stop_app_immediately, ActiveEventLoop}; use super::menu; -use super::observer::{EventLoopWaker, RunLoop}; +use super::observer::EventLoopWaker; #[derive(Debug)] pub(super) struct AppState { @@ -25,7 +25,7 @@ pub(super) struct AppState { activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, - run_loop: RunLoop, + run_loop: MainRunLoop, event_loop_proxy: Arc, event_handler: EventHandler, stop_on_launch: Cell, @@ -68,7 +68,7 @@ impl AppState { activation_policy, default_menu, activate_ignoring_other_apps, - run_loop: RunLoop::main(mtm), + run_loop: MainRunLoop::get(mtm), event_loop_proxy, event_handler: EventHandler::new(), stop_on_launch: Cell::new(false), @@ -265,7 +265,7 @@ impl AppState { if !pending_redraw.contains(&window_id) { pending_redraw.push(window_id); } - self.run_loop.wakeup(); + self.run_loop.wake_up(); } #[track_caller] @@ -310,6 +310,7 @@ impl AppState { // Called by RunLoopObserver after finishing waiting for new events pub fn wakeup(self: &Rc) { // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + // (we have registered to observe all modes, including modal event loops). if !self.event_handler.ready() || !self.is_running() { return; } @@ -338,8 +339,7 @@ impl AppState { // Called by RunLoopObserver before waiting for new events pub fn cleared(self: &Rc) { // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 - // XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if - // we're about to return to the `CFRunLoop` to poll for new events? + // (we have registered to observe all modes, including modal event loops). if !self.event_handler.ready() || !self.is_running() { return; } diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index a12e844623..299137d3b3 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -9,8 +9,10 @@ use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification, NSApplicationWillTerminateNotification, NSWindow, }; +use objc2_core_foundation::{kCFRunLoopCommonModes, CFIndex, CFRunLoopActivity}; use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; use rwh_06::HasDisplayHandle; +use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver}; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; use winit_core::error::{EventLoopError, RequestError}; @@ -28,7 +30,6 @@ use super::cursor::CustomCursor; use super::event::dummy_event; use super::monitor; use super::notification_center::create_observer; -use super::observer::setup_control_flow_observers; use crate::window::Window; use crate::ActivationPolicy; @@ -150,6 +151,9 @@ pub struct EventLoop { // Though we do still need to keep the observers around to prevent them from being deallocated. _did_finish_launching_observer: Retained>, _will_terminate_observer: Retained>, + + _before_waiting_observer: MainRunLoopObserver, + _after_waiting_observer: MainRunLoopObserver, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -217,7 +221,31 @@ impl EventLoop { }, ); - setup_control_flow_observers(mtm); + let main_loop = MainRunLoop::get(mtm); + let mode = unsafe { kCFRunLoopCommonModes }.unwrap(); + + let app_state_clone = Rc::clone(&app_state); + let _before_waiting_observer = MainRunLoopObserver::new( + mtm, + CFRunLoopActivity::BeforeWaiting, + true, + // Queued with the lowest priority to ensure it is processed after other observers. + // Without that, we'd get a `LoopExiting` after `AboutToWait`. + CFIndex::MAX, + move |_| app_state_clone.cleared(), + ); + main_loop.add_observer(&_before_waiting_observer, mode); + + let app_state_clone = Rc::clone(&app_state); + let _after_waiting_observer = MainRunLoopObserver::new( + mtm, + CFRunLoopActivity::AfterWaiting, + true, + // Queued with the highest priority to ensure it is processed before other observers. + CFIndex::MIN, + move |_| app_state_clone.wakeup(), + ); + main_loop.add_observer(&_after_waiting_observer, mode); Ok(EventLoop { app, @@ -225,6 +253,8 @@ impl EventLoop { window_target: ActiveEventLoop { app_state, mtm }, _did_finish_launching_observer, _will_terminate_observer, + _before_waiting_observer, + _after_waiting_observer, }) } diff --git a/winit-appkit/src/observer.rs b/winit-appkit/src/observer.rs index 3629187c7c..76d37c5f42 100644 --- a/winit-appkit/src/observer.rs +++ b/winit-appkit/src/observer.rs @@ -1,171 +1,10 @@ -//! Utilities for working with `CFRunLoop`. -//! -//! See Apple's documentation on Run Loops for details: -//! -use std::cell::Cell; use std::ffi::c_void; use std::ptr; use std::time::Instant; -use objc2::MainThreadMarker; use objc2_core_foundation::{ - kCFRunLoopCommonModes, kCFRunLoopDefaultMode, CFAbsoluteTimeGetCurrent, CFIndex, CFRetained, - CFRunLoop, CFRunLoopActivity, CFRunLoopObserver, CFRunLoopObserverCallBack, - CFRunLoopObserverContext, CFRunLoopTimer, + kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRetained, CFRunLoop, CFRunLoopTimer, }; -use tracing::error; - -use super::app_state::AppState; - -// begin is queued with the highest priority to ensure it is processed before other observers -extern "C-unwind" fn control_flow_begin_handler( - _: *mut CFRunLoopObserver, - activity: CFRunLoopActivity, - _info: *mut c_void, -) { - match activity { - CFRunLoopActivity::AfterWaiting => { - AppState::get(MainThreadMarker::new().unwrap()).wakeup(); - }, - _ => unreachable!(), - } -} - -// end is queued with the lowest priority to ensure it is processed after other observers -// without that, LoopExiting would get sent after AboutToWait -extern "C-unwind" fn control_flow_end_handler( - _: *mut CFRunLoopObserver, - activity: CFRunLoopActivity, - _info: *mut c_void, -) { - match activity { - CFRunLoopActivity::BeforeWaiting => { - AppState::get(MainThreadMarker::new().unwrap()).cleared(); - }, - CFRunLoopActivity::Exit => (), // unimplemented!(), // not expected to ever happen - _ => unreachable!(), - } -} - -#[derive(Debug)] -pub struct RunLoop(CFRetained); - -impl RunLoop { - pub fn main(mtm: MainThreadMarker) -> Self { - // SAFETY: We have a MainThreadMarker here, which means we know we're on the main thread, so - // scheduling (and scheduling a non-`Send` block) to that thread is allowed. - let _ = mtm; - RunLoop(CFRunLoop::main().unwrap()) - } - - pub fn wakeup(&self) { - self.0.wake_up(); - } - - unsafe fn add_observer( - &self, - flags: CFRunLoopActivity, - // The lower the value, the sooner this will run - priority: CFIndex, - handler: CFRunLoopObserverCallBack, - context: *mut CFRunLoopObserverContext, - ) { - let observer = - unsafe { CFRunLoopObserver::new(None, flags.0, true, priority, handler, context) } - .unwrap(); - self.0.add_observer(Some(&observer), unsafe { kCFRunLoopCommonModes }); - } - - /// Submit a closure to run on the main thread as the next step in the run loop, before other - /// event sources are processed. - /// - /// This is used for running event handlers, as those are not allowed to run re-entrantly. - /// - /// # Implementation - /// - /// This queuing could be implemented in the following several ways with subtle differences in - /// timing. This list is sorted in rough order in which they are run: - /// - /// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`. - /// - /// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the - /// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both - /// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop). - /// - /// a. `atStart = true`. - /// - /// b. `atStart = false`. - /// - /// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not - /// respect the ordering that runloop events have. - /// - /// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we - /// want the event to be queued in a way that preserves the order the events originally arrived - /// in. - /// - /// As an example, let's assume that we receive two events from the user, a mouse click which we - /// handled by queuing it, and a window resize which we handled immediately. If we allowed - /// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of - /// the queue, and the events would appear out of order to the user of Winit. So we must instead - /// put the event at the very front of the queue, to be handled as soon as possible after - /// handling whatever event it's currently handling. - pub fn queue_closure(&self, closure: impl FnOnce() + 'static) { - // Convert `FnOnce()` to `Block`. - let closure = Cell::new(Some(closure)); - let block = block2::RcBlock::new(move || { - if let Some(closure) = closure.take() { - closure() - } else { - error!("tried to execute queued closure on main thread twice"); - } - }); - - // There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa: - // - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`. - // - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window. - // - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop. - // - `NSConnectionReplyMode`: TODO. - // - // We only want to run event handlers in the default mode, as we support running a blocking - // modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and - // resizing such panel window enters the event tracking run loop mode, so we can't directly - // trigger events inside that mode either. - // - // Any events that are queued while running a modal or when live-resizing will instead wait, - // and be delivered to the application afterwards. - // - // [#1779]: https://github.com/rust-windowing/winit/issues/1779 - let mode = unsafe { kCFRunLoopDefaultMode.unwrap() }; - - // SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`. - unsafe { self.0.perform_block(Some(mode), Some(&block)) } - } -} - -pub fn setup_control_flow_observers(mtm: MainThreadMarker) { - let run_loop = RunLoop::main(mtm); - unsafe { - let mut context = CFRunLoopObserverContext { - info: ptr::null_mut(), - version: 0, - retain: None, - release: None, - copyDescription: None, - }; - run_loop.add_observer( - CFRunLoopActivity::AfterWaiting, - CFIndex::MIN, - Some(control_flow_begin_handler), - &mut context as *mut _, - ); - run_loop.add_observer( - CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting, - CFIndex::MAX, - Some(control_flow_end_handler), - &mut context as *mut _, - ); - } -} #[derive(Debug)] pub struct EventLoopWaker { diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 9d9825f40d..a10ba5f8fd 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -40,6 +40,7 @@ use objc2_foundation::{ NSRect, NSSize, NSString, }; use tracing::{trace, warn}; +use winit_common::core_foundation::MainRunLoop; use winit_core::cursor::Cursor; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; @@ -54,7 +55,6 @@ use super::app_state::AppState; use super::cursor::{cursor_from_icon, CustomCursor}; use super::ffi; use super::monitor::{self, flip_window_screen_coordinates, get_display_id, MonitorHandle}; -use super::observer::RunLoop; use super::util::cgerr; use super::view::WinitView; use super::window::{window_id, WinitPanel, WinitWindow}; @@ -175,7 +175,7 @@ define_class!( let mtm = MainThreadMarker::from(self); let this = self.retain(); - RunLoop::main(mtm).queue_closure(move || { + MainRunLoop::get(mtm).queue_closure(move || { this.handle_scale_factor_changed(scale_factor); }); } diff --git a/winit-common/Cargo.toml b/winit-common/Cargo.toml index a2e4a674f3..2195b15ad5 100644 --- a/winit-common/Cargo.toml +++ b/winit-common/Cargo.toml @@ -18,7 +18,7 @@ x11 = ["xkbcommon-dl?/x11", "dep:x11-dl"] xkb = ["dep:xkbcommon-dl", "dep:smol_str"] # CoreFoundation -core-foundation = ["dep:objc2", "dep:objc2-core-foundation"] +core-foundation = ["dep:block2", "dep:objc2", "dep:objc2-core-foundation"] [dependencies] smol_str = { workspace = true, optional = true } @@ -31,9 +31,11 @@ x11-dl = { workspace = true, optional = true } xkbcommon-dl = { workspace = true, optional = true } # CoreFoundation +block2 = { workspace = true, optional = true } objc2 = { workspace = true, optional = true } objc2-core-foundation = { workspace = true, optional = true, features = [ "std", + "block2", "CFRunLoop", "CFString", ] } diff --git a/winit-common/src/core_foundation/main.rs b/winit-common/src/core_foundation/main.rs new file mode 100644 index 0000000000..90b637ca56 --- /dev/null +++ b/winit-common/src/core_foundation/main.rs @@ -0,0 +1,179 @@ +//! Various utilities for interfacing with the main run loop. +//! +//! These allow for using closures without the `Send + Sync` requirement that is otherwise required +//! when interfacing with run loops. +//! +//! See also for figuring this out at a lower level. + +use std::cell::Cell; + +use block2::RcBlock; +use objc2::MainThreadMarker; +use objc2_core_foundation::{ + kCFAllocatorDefault, kCFRunLoopDefaultMode, CFIndex, CFRetained, CFRunLoop, CFRunLoopActivity, + CFRunLoopMode, CFRunLoopObserver, +}; +use tracing::error; + +/// Wrapper around [`CFRunLoop::main`]. +#[derive(Debug)] +pub struct MainRunLoop { + /// This is on the main thread. + _mtm: MainThreadMarker, + /// A cached reference to the main run loop. + main_run_loop: CFRetained, +} + +impl MainRunLoop { + /// Get the main run loop. + pub fn get(_mtm: MainThreadMarker) -> Self { + let main_run_loop = CFRunLoop::main().unwrap(); + Self { _mtm, main_run_loop } + } + + /// Get a reference to the underlying [`CFRunLoop`]. + pub fn run_loop(&self) -> &CFRunLoop { + &self.main_run_loop + } + + /// Wake the main run loop. + pub fn wake_up(&self) { + self.main_run_loop.wake_up(); + } + + /// Submit a closure to run on the main thread as the next step in the run loop, before other + /// event sources are processed. + /// + /// This is used for running event handlers, as those are not allowed to run re-entrantly. + /// + /// # Implementation + /// + /// This queuing could be implemented in the following several ways with subtle differences in + /// timing. This list is sorted in rough order in which they are run: + /// + /// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`. + /// + /// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the + /// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both + /// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop). + /// + /// a. `atStart = true`. + /// + /// b. `atStart = false`. + /// + /// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not + /// respect the ordering that runloop events have. + /// + /// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we + /// want the event to be queued in a way that preserves the order the events originally arrived + /// in. + /// + /// As an example, let's assume that we receive two events from the user, a mouse click which we + /// handled by queuing it, and a window resize which we handled immediately. If we allowed + /// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of + /// the queue, and the events would appear out of order to the user of Winit. So we must instead + /// put the event at the very front of the queue, to be handled as soon as possible after + /// handling whatever event it's currently handling. + pub fn queue_closure(&self, closure: impl FnOnce() + 'static) { + // Convert `FnOnce()` to `Block`. + let closure = Cell::new(Some(closure)); + let block = block2::RcBlock::new(move || { + debug_assert!(MainThreadMarker::new().is_some()); + if let Some(closure) = closure.take() { + closure() + } else { + error!("tried to execute queued closure on main thread twice"); + } + }); + + // There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa: + // - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`. + // - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window. + // - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop. + // - `NSConnectionReplyMode`: TODO. + // + // We only want to run event handlers in the default mode, as we support running a blocking + // modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and + // resizing such panel window enters the event tracking run loop mode, so we can't directly + // trigger events inside that mode either. + // + // Any events that are queued while running a modal or when live-resizing will instead wait, + // and be delivered to the application afterwards. + // + // [#1779]: https://github.com/rust-windowing/winit/issues/1779 + let mode = unsafe { kCFRunLoopDefaultMode.unwrap() }; + + let _ = self._mtm; + // SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`. + // + // Additionally, we have a `MainThreadMarker` here, which means we know we're on the main + // thread. We also know that the run loop is the main-thread run loop, so scheduling a + // non-`Send` block to that is allowed. + unsafe { self.main_run_loop.perform_block(Some(mode), Some(&block)) } + } + + /// Add an observer to the main run loop. + pub fn add_observer(&self, observer: &MainRunLoopObserver, mode: &CFRunLoopMode) { + // Accessing the `MainObserver`'s observer is fine here, since we're adding it to the main + // run loop (which is on the same thread that the observer was created on). + self.main_run_loop.add_observer(Some(&observer.observer), Some(mode)); + } + + /// Remove an observer from the main run loop. + /// + /// This is also done automatically when the [`MainRunLoopObserver`] is dropped. + pub fn remove_observer(&self, observer: &MainRunLoopObserver, mode: &CFRunLoopMode) { + // Same as in `add_observer`, accessing the main loop's observer is fine. + self.main_run_loop.add_observer(Some(&observer.observer), Some(mode)); + } +} + +/// A [`CFRunLoopObserver`] on the main thread. +/// +/// This type has "ownership" semantics, and invalidates the observer when dropped. +#[derive(Debug)] +pub struct MainRunLoopObserver { + /// This must be private, otherwise the user might add it to an arbitrary run loop (but this + /// observer is not designed to only be on the main thread). + observer: CFRetained, +} + +impl MainRunLoopObserver { + /// Create a new run loop observer that observes the main run loop. + pub fn new( + mtm: MainThreadMarker, + activities: CFRunLoopActivity, + repeats: bool, + // The lower the value, the sooner this will run (inverse of a "priority"). + order: CFIndex, + callback: impl Fn(CFRunLoopActivity) + 'static, + ) -> Self { + let block = RcBlock::new(move |_: *mut _, activity| { + debug_assert!(MainThreadMarker::new().is_some()); + callback(activity) + }); + + let _ = mtm; + // SAFETY: The callback is not Send + Sync, which would normally be unsound, but since we + // restrict the callback to only ever be on the main thread (by taking `MainThreadMarker`, + // and in `MainRunLoop::add_observer`), the callback doesn't have to be thread safe. + let observer = unsafe { + CFRunLoopObserver::with_handler( + kCFAllocatorDefault, + activities.0, + repeats, + order, + Some(&block), + ) + } + .unwrap(); + + MainRunLoopObserver { observer } + } +} + +impl Drop for MainRunLoopObserver { + fn drop(&mut self) { + self.observer.invalidate(); + } +} diff --git a/winit-common/src/core_foundation/mod.rs b/winit-common/src/core_foundation/mod.rs index 3b7091759c..11f280970d 100644 --- a/winit-common/src/core_foundation/mod.rs +++ b/winit-common/src/core_foundation/mod.rs @@ -1,3 +1,10 @@ +//! Various wrappers around [`CFRunLoop`][objc2_core_foundation::CFRunLoop]. +//! +//! See Apple's documentation on Run Loops for details: +//! + mod event_loop_proxy; +mod main; -pub use event_loop_proxy::*; +pub use self::event_loop_proxy::*; +pub use self::main::*; diff --git a/winit-uikit/src/event_loop.rs b/winit-uikit/src/event_loop.rs index 09ed87a621..39e2082409 100644 --- a/winit-uikit/src/event_loop.rs +++ b/winit-uikit/src/event_loop.rs @@ -1,13 +1,9 @@ -use std::ffi::c_void; -use std::ptr; use std::sync::Arc; use objc2::rc::Retained; use objc2::runtime::ProtocolObject; use objc2::{msg_send, ClassType, MainThreadMarker}; -use objc2_core_foundation::{ - kCFRunLoopDefaultMode, CFIndex, CFRunLoop, CFRunLoopActivity, CFRunLoopObserver, -}; +use objc2_core_foundation::{kCFRunLoopDefaultMode, CFIndex, CFRunLoopActivity}; use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; use objc2_ui_kit::{ UIApplication, UIApplicationDidBecomeActiveNotification, @@ -16,6 +12,7 @@ use objc2_ui_kit::{ UIApplicationWillResignActiveNotification, UIApplicationWillTerminateNotification, UIScreen, }; use rwh_06::HasDisplayHandle; +use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver}; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor, CustomCursorSource}; use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; @@ -136,6 +133,10 @@ pub struct EventLoop { _did_enter_background_observer: Retained>, _will_terminate_observer: Retained>, _did_receive_memory_warning_observer: Retained>, + + _wakeup_observer: MainRunLoopObserver, + _main_events_cleared_observer: MainRunLoopObserver, + _events_cleared_observer: MainRunLoopObserver, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -151,9 +152,6 @@ impl EventLoop { return Err(EventLoopError::RecreationAttempt); } - // this line sets up the main run loop before `UIApplicationMain` - setup_control_flow_observers(); - let center = unsafe { NSNotificationCenter::defaultCenter() }; let _did_finish_launching_observer = create_observer( @@ -224,6 +222,50 @@ impl EventLoop { move |_| app_state::handle_memory_warning(mtm), ); + let main_loop = MainRunLoop::get(mtm); + let mode = unsafe { kCFRunLoopDefaultMode }.unwrap(); + + let _wakeup_observer = MainRunLoopObserver::new( + mtm, + CFRunLoopActivity::AfterWaiting, + true, + // Queued with the highest priority to ensure it is processed before other observers. + CFIndex::MIN, + move |_| app_state::handle_wakeup_transition(mtm), + ); + main_loop.add_observer(&_wakeup_observer, mode); + + let _main_events_cleared_observer = MainRunLoopObserver::new( + mtm, + CFRunLoopActivity::BeforeWaiting, + true, + // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in + // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the + // main_end priority to be 0, in order to send `AboutToWait` before `RedrawRequested`. + // This value was chosen conservatively to guard against apple using different + // priorities for their redraw observers in different OS's or on different devices. If + // it so happens that it's too conservative, the main symptom would be non-redraw + // events coming in after `AboutToWait`. + // + // The value of `0x1e8480` was determined by inspecting stack traces and the associated + // registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4. + // + // Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4. + 0, + move |_| app_state::handle_main_events_cleared(mtm), + ); + main_loop.add_observer(&_main_events_cleared_observer, mode); + + let _events_cleared_observer = MainRunLoopObserver::new( + mtm, + CFRunLoopActivity::BeforeWaiting, + true, + // Queued with the lowest priority to ensure it is processed after other observers. + CFIndex::MAX, + move |_| app_state::handle_events_cleared(mtm), + ); + main_loop.add_observer(&_events_cleared_observer, mode); + Ok(EventLoop { mtm, window_target: ActiveEventLoop { mtm }, @@ -234,6 +276,9 @@ impl EventLoop { _did_enter_background_observer, _will_terminate_observer, _did_receive_memory_warning_observer, + _wakeup_observer, + _main_events_cleared_observer, + _events_cleared_observer, }) } @@ -256,97 +301,3 @@ impl EventLoop { &self.window_target } } - -fn setup_control_flow_observers() { - unsafe { - // begin is queued with the highest priority to ensure it is processed before other - // observers - extern "C-unwind" fn control_flow_begin_handler( - _: *mut CFRunLoopObserver, - activity: CFRunLoopActivity, - _: *mut c_void, - ) { - let mtm = MainThreadMarker::new().unwrap(); - #[allow(non_upper_case_globals)] - match activity { - CFRunLoopActivity::AfterWaiting => app_state::handle_wakeup_transition(mtm), - _ => unreachable!(), - } - } - - // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in - // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end - // priority to be 0, in order to send AboutToWait before RedrawRequested. This value was - // chosen conservatively to guard against apple using different priorities for their redraw - // observers in different OS's or on different devices. If it so happens that it's too - // conservative, the main symptom would be non-redraw events coming in after `AboutToWait`. - // - // The value of `0x1e8480` was determined by inspecting stack traces and the associated - // registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4. - // - // Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4. - extern "C-unwind" fn control_flow_main_end_handler( - _: *mut CFRunLoopObserver, - activity: CFRunLoopActivity, - _: *mut c_void, - ) { - let mtm = MainThreadMarker::new().unwrap(); - #[allow(non_upper_case_globals)] - match activity { - CFRunLoopActivity::BeforeWaiting => app_state::handle_main_events_cleared(mtm), - CFRunLoopActivity::Exit => {}, // may happen when running on macOS - _ => unreachable!(), - } - } - - // end is queued with the lowest priority to ensure it is processed after other observers - extern "C-unwind" fn control_flow_end_handler( - _: *mut CFRunLoopObserver, - activity: CFRunLoopActivity, - _: *mut c_void, - ) { - let mtm = MainThreadMarker::new().unwrap(); - #[allow(non_upper_case_globals)] - match activity { - CFRunLoopActivity::BeforeWaiting => app_state::handle_events_cleared(mtm), - CFRunLoopActivity::Exit => {}, // may happen when running on macOS - _ => unreachable!(), - } - } - - let main_loop = CFRunLoop::main().unwrap(); - - let begin_observer = CFRunLoopObserver::new( - None, - CFRunLoopActivity::AfterWaiting.0, - true, - CFIndex::MIN, - Some(control_flow_begin_handler), - ptr::null_mut(), - ) - .unwrap(); - main_loop.add_observer(Some(&begin_observer), kCFRunLoopDefaultMode); - - let main_end_observer = CFRunLoopObserver::new( - None, - (CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting).0, - true, - 0, // see comment on `control_flow_main_end_handler` - Some(control_flow_main_end_handler), - ptr::null_mut(), - ) - .unwrap(); - main_loop.add_observer(Some(&main_end_observer), kCFRunLoopDefaultMode); - - let end_observer = CFRunLoopObserver::new( - None, - (CFRunLoopActivity::Exit | CFRunLoopActivity::BeforeWaiting).0, - true, - CFIndex::MAX, - Some(control_flow_end_handler), - ptr::null_mut(), - ) - .unwrap(); - main_loop.add_observer(Some(&end_observer), kCFRunLoopDefaultMode); - } -}