diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index 5b057b79b3..4b51cbf199 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -6,8 +6,7 @@ use std::time::Instant; use dispatch2::MainThreadBound; use objc2::MainThreadMarker; -use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication}; -use objc2_foundation::NSNotification; +use objc2_app_kit::NSApplication; use winit_common::core_foundation::{EventLoopProxy, MainRunLoop}; use winit_common::event_handler::EventHandler; use winit_core::application::ApplicationHandler; @@ -16,24 +15,17 @@ use winit_core::event_loop::ControlFlow; use winit_core::window::WindowId; use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately}; -use super::menu; use super::observer::EventLoopWaker; #[derive(Debug)] pub(super) struct AppState { mtm: MainThreadMarker, - activation_policy: Option, - default_menu: bool, - activate_ignoring_other_apps: bool, run_loop: MainRunLoop, event_loop_proxy: Arc, event_handler: EventHandler, - 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. @@ -53,29 +45,19 @@ static GLOBAL: MainThreadBound>> = MainThreadBound::new(OnceCell::new(), unsafe { MainThreadMarker::new_unchecked() }); impl AppState { - pub(super) fn setup_global( - mtm: MainThreadMarker, - activation_policy: Option, - default_menu: bool, - activate_ignoring_other_apps: bool, - ) -> Option> { + pub(super) fn setup_global(mtm: MainThreadMarker) -> Option> { let event_loop_proxy = Arc::new(EventLoopProxy::new(mtm, move || { Self::get(mtm).with_handler(|app, event_loop| app.proxy_wake_up(event_loop)); })); let this = Rc::new(Self { mtm, - activation_policy, - default_menu, - activate_ignoring_other_apps, run_loop: MainRunLoop::get(mtm), event_loop_proxy, event_handler: EventHandler::new(), - stop_on_launch: Cell::new(false), stop_before_wait: Cell::new(false), stop_after_wait: Cell::new(false), stop_on_redraw: Cell::new(false), - is_launched: Cell::new(false), is_running: Cell::new(false), exit: Cell::new(false), control_flow: Cell::new(ControlFlow::default()), @@ -96,69 +78,6 @@ impl AppState { .clone() } - // NOTE: This notification will, globally, only be emitted once, - // no matter how many `EventLoop`s the user creates. - pub fn did_finish_launching(self: &Rc, _notification: &NSNotification) { - self.is_launched.set(true); - - let app = NSApplication::sharedApplication(self.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. - if let Some(activation_policy) = self.activation_policy { - app.setActivationPolicy(activation_policy); - } else { - // If no activation policy is explicitly provided, and the application - // is bundled, do not set the activation policy at all, to allow the - // package manifest to define the behavior via LSUIElement. - // - // See: - // - https://github.com/rust-windowing/winit/issues/261 - // - https://github.com/rust-windowing/winit/issues/3958 - let is_bundled = - NSRunningApplication::currentApplication().bundleIdentifier().is_some(); - if !is_bundled { - app.setActivationPolicy(NSApplicationActivationPolicy::Regular); - } - } - - #[allow(deprecated)] - app.activateIgnoringOtherApps(self.activate_ignoring_other_apps); - - if self.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.waker.borrow_mut().start(); - - self.set_is_running(true); - self.dispatch_init_events(); - - // If the application is being launched via `EventLoop::pump_app_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.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(self.mtm); - stop_app_immediately(&app); - } - } - - pub fn will_terminate(self: &Rc, _notification: &NSNotification) { - let app = NSApplication::sharedApplication(self.mtm); - notify_windows_of_exit(&app); - self.event_handler.terminate(); - self.internal_exit(); - } - /// Place the event handler in the application state for the duration /// of the given closure. pub fn set_event_handler( @@ -169,15 +88,12 @@ impl AppState { self.event_handler.set(Box::new(handler), closure) } - pub fn event_loop_proxy(&self) -> &Arc { - &self.event_loop_proxy + pub fn terminate_event_handler(&self) { + self.event_handler.terminate(); } - /// If `pump_events` is called to progress the event loop then we - /// bootstrap the event loop via `-[NSApplication run]` but will use - /// `CFRunLoopRunInMode` for subsequent calls to `pump_events`. - pub fn set_stop_on_launch(&self) { - self.stop_on_launch.set(true); + pub fn event_loop_proxy(&self) -> &Arc { + &self.event_loop_proxy } pub fn set_stop_before_wait(&self, value: bool) { @@ -208,10 +124,6 @@ impl AppState { self.set_wait_timeout(None); } - pub fn is_launched(&self) -> bool { - self.is_launched.get() - } - pub fn set_is_running(&self, value: bool) { self.is_running.set(value) } diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index bc2a3fd21a..7d9885dac2 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -7,7 +7,7 @@ use objc2::runtime::ProtocolObject; use objc2::{MainThreadMarker, available}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification, - NSApplicationWillTerminateNotification, NSWindow, + NSApplicationWillTerminateNotification, NSRunningApplication, NSWindow, }; use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes}; use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; @@ -31,8 +31,8 @@ use super::cursor::CustomCursor; use super::event::dummy_event; use super::monitor; use super::notification_center::create_observer; -use crate::ActivationPolicy; use crate::window::Window; +use crate::{ActivationPolicy, menu}; #[derive(Debug)] pub struct ActiveEventLoop { @@ -150,7 +150,6 @@ pub struct EventLoop { // the system instead cleans it up next time it would have posted a notification to it. // // 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>, _tracing_observers: Option<(MainRunLoopObserver, MainRunLoopObserver)>, @@ -176,20 +175,8 @@ impl EventLoop { let mtm = MainThreadMarker::new() .expect("on macOS, `EventLoop` must be created on the main thread!"); - let activation_policy = match attributes.activation_policy { - None => None, - Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular), - Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory), - Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited), - }; - - let app_state = AppState::setup_global( - mtm, - activation_policy, - attributes.default_menu, - attributes.activate_ignoring_other_apps, - ) - .ok_or_else(|| EventLoopError::RecreationAttempt)?; + let app_state = + AppState::setup_global(mtm).ok_or_else(|| EventLoopError::RecreationAttempt)?; // Initialize the application (if it has not already been). let app = NSApplication::sharedApplication(mtm); @@ -199,32 +186,33 @@ impl EventLoop { let center = NSNotificationCenter::defaultCenter(); - let weak_app_state = Rc::downgrade(&app_state); - let _did_finish_launching_observer = create_observer( - ¢er, - // `applicationDidFinishLaunching:` - unsafe { NSApplicationDidFinishLaunchingNotification }, - move |notification| { - let _entered = debug_span!("NSApplicationDidFinishLaunchingNotification").entered(); - if let Some(app_state) = weak_app_state.upgrade() { - app_state.did_finish_launching(notification); - } - }, - ); - + // Handle `terminate:`. This may happen if: + // - The user uses the context menu in the Dock icon. + // - Or the `Quit` menu item we install with the default menu (including via. the keyboard + // shortcut). + // - Maybe other cases? + // + // In these cases, AppKit is going to call `std::process::exit`, so we won't get the chance + // to return to the user from `EventLoop::run_app`. So we have to clean up and drop their + // windows and application here too. let weak_app_state = Rc::downgrade(&app_state); let _will_terminate_observer = create_observer( ¢er, - // `applicationWillTerminate:` unsafe { NSApplicationWillTerminateNotification }, move |notification| { - let _entered = debug_span!("NSApplicationWillTerminateNotification").entered(); + let _entered = debug_span!("applicationWillTerminate").entered(); + + let app = notification.object().unwrap().downcast::().unwrap(); + notify_windows_of_exit(&app); + if let Some(app_state) = weak_app_state.upgrade() { - app_state.will_terminate(notification); + app_state.terminate_event_handler(); + app_state.internal_exit(); } }, ); + // Set up run loop observers for calling `new_events` and `about_to_wait`. let main_loop = MainRunLoop::get(mtm); let mode = unsafe { kCFRunLoopCommonModes }.unwrap(); @@ -258,11 +246,100 @@ impl EventLoop { ); main_loop.add_observer(&_after_waiting_observer, mode); + // Run `finishLaunching` just in case it works. + app.finishLaunching(); + // Now _ideally_, calling `finishLaunching` should be enough for the application to, you + // know, launch (create the a dock icon etc.), but unfortunately, this doesn't happen for + // various godforsaken reasons... The only way to make the application properly launch is by + // calling `NSApplication::run`. + // + // So we check if the application hasn't finished launching, and if it hasn't, we run it + // once to finish it. + // + // This is _very_ important, there's a _lot_ of weird and subtle state that requires that + // the application is launched properly, including window creation, the menu bar, + // activation, see: + // - https://github.com/rust-windowing/winit/pull/1903 + // - https://github.com/rust-windowing/winit/pull/1922 + // - https://github.com/rust-windowing/winit/issues/2238 + // - https://github.com/rust-windowing/winit/issues/2051 + // - https://github.com/rust-windowing/winit/issues/2087 + // - https://developer.apple.com/forums/thread/772169 + // + // This approach is similar to what other cross-platform windowing libraries do (except that + // we do it without a delegate to allow users to override that): + // - GLFW delegate: https://github.com/glfw/glfw/blob/3.4/src/cocoa_init.m#L439-L443 + // - GLFW launch: https://github.com/glfw/glfw/blob/3.4/src/cocoa_init.m#L634-L635 + // - FLTK delegate: https://github.com/fltk/fltk/blob/release-1.4.4/src/Fl_cocoa.mm#L1604-L1607 + // - FLTK launch: https://github.com/fltk/fltk/blob/release-1.4.4/src/Fl_cocoa.mm#L1903-L1919 + // - Stackoverflow issue: https://stackoverflow.com/questions/48020222/how-to-make-nsapp-run-not-block/67626393#67626393 + if !NSRunningApplication::currentApplication().isFinishedLaunching() { + // Register an observer to stop the application immediately after launching. + // + // NOTE: This notification will, globally, only be emitted once, no matter how many + // `EventLoop`s the user creates. We detect it with `isFinishedLaunching` above. + let did_finish_launching_observer = create_observer( + ¢er, + unsafe { NSApplicationDidFinishLaunchingNotification }, + move |notification| { + let _entered = debug_span!("applicationDidFinishLaunching").entered(); + + let app = notification.object().unwrap().downcast::().unwrap(); + + // Stop the application, to make the `app.run()` call below return. + stop_app_immediately(&app); + }, + ); + + // We call `stop_app_immediately` above, so this should return after launching. + app.run(); + + // The observer should've been called at this point. + drop(did_finish_launching_observer); + + // We _could_ keep trying if we failed to initialize, but that would potentially lead + // to an infinite loop, it's probably better to just continue. + debug_assert!(NSRunningApplication::currentApplication().isFinishedLaunching()); + } + + // 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. + if let Some(activation_policy) = attributes.activation_policy { + app.setActivationPolicy(match activation_policy { + ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, + ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, + ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, + }); + } else { + // If no activation policy is explicitly provided, and the application + // is bundled, do not set the activation policy at all, to allow the + // package manifest to define the behavior via LSUIElement. + // + // See: + // - https://github.com/rust-windowing/winit/issues/261 + // - https://github.com/rust-windowing/winit/issues/3958 + let is_bundled = + NSRunningApplication::currentApplication().bundleIdentifier().is_some(); + if !is_bundled { + app.setActivationPolicy(NSApplicationActivationPolicy::Regular); + } + } + + // TODO: Use `app.activate()` instead on newer OS versions? + #[expect(deprecated)] + app.activateIgnoringOtherApps(attributes.activate_ignoring_other_apps); + + if attributes.default_menu { + // The default menubar initialization should be before everything else, to allow + // overriding it even if it's created. + menu::initialize(&app); + } + Ok(EventLoop { app, app_state: app_state.clone(), window_target: ActiveEventLoop { app_state, mtm }, - _did_finish_launching_observer, _will_terminate_observer, _tracing_observers, _before_waiting_observer, @@ -282,6 +359,7 @@ impl EventLoop { &mut self, app: A, ) -> Result<(), EventLoopError> { + let _entered = debug_span!("run_app_on_demand").entered(); self.app_state.clear_exit(); self.app_state.set_event_handler(app, || { autoreleasepool(|_| { @@ -291,11 +369,9 @@ impl EventLoop { self.app_state.set_stop_after_wait(false); self.app_state.set_stop_on_redraw(false); - if self.app_state.is_launched() { - debug_assert!(!self.app_state.is_running()); - self.app_state.set_is_running(true); - self.app_state.dispatch_init_events(); - } + debug_assert!(!self.app_state.is_running()); + self.app_state.set_is_running(true); + self.app_state.dispatch_init_events(); // NOTE: Make sure to not run the application re-entrantly, as that'd be confusing. self.app.run(); @@ -312,19 +388,10 @@ impl EventLoop { timeout: Option, app: A, ) -> PumpStatus { + let _entered = debug_span!("pump_app_events").entered(); self.app_state.set_event_handler(app, || { autoreleasepool(|_| { - // 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 !self.app_state.is_launched() { - debug_assert!(!self.app_state.is_running()); - - self.app_state.set_stop_on_launch(); - self.app.run(); - - // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application - // has launched - } else if !self.app_state.is_running() { + if !self.app_state.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 diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index 949e2f799b..e4d326acdf 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -50,15 +50,13 @@ //! } //! //! fn main() -> Result<(), Box> { -//! let event_loop = EventLoop::new()?; -//! +//! // Register the delegate before Winit gets a chance to touch things. //! let mtm = MainThreadMarker::new().unwrap(); //! let delegate = AppDelegate::new(mtm); -//! // Important: Call `sharedApplication` after `EventLoop::new`, -//! // doing it before is not yet supported. //! let app = NSApplication::sharedApplication(mtm); //! app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); //! +//! let event_loop = EventLoop::new()?; //! // event_loop.run_app(&mut my_app); //! Ok(()) //! } diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index a99ce6628b..df395c2ea3 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -52,6 +52,7 @@ changelog entry. - Updated `windows-sys` to `v0.61`. - On older macOS versions (tested up to 12.7.6), applications now receive mouse movement events for unfocused windows, matching the behavior on other platforms. +- On macOS, the application is now launched in `EventLoop::new` instead of `EventLoop::run_app`. If you're registering a custom delegate, you should now register it before `EventLoop::new`. ### Fixed diff --git a/winit/src/event_loop.rs b/winit/src/event_loop.rs index 18b5ee9137..52b4c057ec 100644 --- a/winit/src/event_loop.rs +++ b/winit/src/event_loop.rs @@ -75,6 +75,9 @@ impl EventLoopBuilder { /// `DISPLAY` respectively when building the event loop. /// - **Android:** must be configured with an `AndroidApp` from `android_main()` by calling /// [`.with_android_app(app)`] before calling `.build()`, otherwise it'll panic. + /// - **macOS:** this will launch the application, so if you want to register a custom delegate, + /// or otherwise do stuff before `applicationDidFinishLaunching:`, you should do it before + /// this function is called. /// /// [`platform`]: crate::platform #[cfg_attr(