From 00605ee62aec57407fd8edfa7832edfa38357cb3 Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Tue, 11 Apr 2023 12:50:52 +0100 Subject: [PATCH] Re-work event loop run APIs Overall this re-works the APIs for how an `EventLoop` is run to cover these use-cases, with varying portability caveats: 1. A portable `run()` API that consumes the `EventLoop` and runs the loop on the calling thread until the app exits. This can be supported across _all_ platforms and compared to the previous `run() -> !` API is now able to return a `Result` status on all platforms except iOS and Web. Fixes: #2709 2. A less portable `run_ondmand()` API that covers the use case in #2431 where applications need to be able to re-run a Winit application multiple times against a persistent `EventLoop`. This doesn't allow `Window` state to carry across separate runs of the loop, but does allow orthogonal re-instantiations of a gui application. Available On: Windows, Linux, MacOS Could be supported on Android if there's a use case Incompatible with iOS, Web Fixes: #2431 3. A less portable (and on MacOS, likely less-optimal) `pump_events()` API that covers the use case in #2706 and allows a Winit event loop to be embedded within an external `loop {}`. Applications call `pump_events()` once per iteration of their own external loop to dispatch all pending Winit events, without blocking the external loop. Available On: Windows, Linux, MacOS, Android Incompatible With: iOS, Web Fixes: #2706 Each method of running the loop will behave consistently in terms of how `NewEvents(Init)`, `Resumed` and `LoopDestroyed` events are dispatched (so portable application code wouldn't necessarily need to have any awareness of which method of running the loop was being used) In particular by moving away from `run() -> !` we stop calling `std::process::exit()` internally as a means to kill the process without returning which means it's possible to return an exit status and applications can return from their `main()` function normally. This also fixes Android support where an Activity runs in a thread but we can't assume to have full ownership of the process (other services could be running in separate threads). `run_return` has been removed, and the overlapping use cases that `run_return` previously partially aimed to support have been split across `run_ondemand` and `pump_events`. To help test the changes this adds: examples/window_ondemand examples/window_pump_events examples/window_pump_events_rfd The last _rfd example, tests the interaction between the `rfd` crate and using `pump_events` to embed Winit within an external event loop. Additionally all examples have generally been updated so that `main()` returns a `Result` from `run()` Fixes: #2709 Fixes: #2706 Fixes: #2431 Fixes: #2752 TODO: tidy up macos changes - probably lots of debug stuff left in atm TODO: Linux TODO: ask someone to test orbital changes, made blindly TODO: split patch up --- Cargo.toml | 1 + examples/control_flow.rs | 4 +- examples/cursor.rs | 4 +- examples/cursor_grab.rs | 4 +- examples/custom_events.rs | 4 +- examples/drag_window.rs | 4 +- examples/fullscreen.rs | 4 +- examples/handling_close.rs | 4 +- examples/ime.rs | 4 +- examples/mouse_wheel.rs | 4 +- examples/multithreaded.rs | 2 +- examples/multiwindow.rs | 2 +- examples/request_redraw.rs | 4 +- examples/request_redraw_threaded.rs | 4 +- examples/resizable.rs | 4 +- examples/theme.rs | 4 +- examples/timer.rs | 4 +- examples/touchpad_gestures.rs | 4 +- examples/transparent.rs | 4 +- examples/web.rs | 4 +- examples/window.rs | 4 +- examples/window_buttons.rs | 4 +- examples/window_debug.rs | 4 +- examples/window_icon.rs | 4 +- examples/window_ondemand.rs | 86 ++++++ ...ow_run_return.rs => window_pump_events.rs} | 30 +- examples/window_pump_events_rfd.rs | 72 +++++ examples/window_resize_increments.rs | 4 +- src/error.rs | 6 + src/event.rs | 8 + src/event_loop.rs | 24 +- src/platform/mod.rs | 11 +- src/platform/pump_events.rs | 29 ++ src/platform/run_ondemand.rs | 68 +++++ src/platform/run_return.rs | 53 ---- src/platform_impl/android/mod.rs | 284 +++++++++++------- src/platform_impl/macos/app_state.rs | 233 ++++++++++++-- src/platform_impl/macos/event_loop.rs | 133 +++++++- src/platform_impl/orbital/event_loop.rs | 20 +- src/platform_impl/windows/event_loop.rs | 93 +++++- .../windows/event_loop/runner.rs | 6 +- 41 files changed, 954 insertions(+), 295 deletions(-) create mode 100644 examples/window_ondemand.rs rename examples/{window_run_return.rs => window_pump_events.rs} (63%) create mode 100644 examples/window_pump_events_rfd.rs create mode 100644 src/platform/pump_events.rs create mode 100644 src/platform/run_ondemand.rs delete mode 100644 src/platform/run_return.rs diff --git a/Cargo.toml b/Cargo.toml index c86287cf82e..0599a5fb387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ serde = { version = "1", optional = true, features = ["serde_derive"] } [dev-dependencies] image = { version = "0.24.0", default-features = false, features = ["png"] } simple_logger = { version = "2.1.0", default_features = false } +rfd = { version = "0.11.0" } [target.'cfg(target_os = "android")'.dependencies] # Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 diff --git a/examples/control_flow.rs b/examples/control_flow.rs index bade79d2799..ff8cb90f6b9 100644 --- a/examples/control_flow.rs +++ b/examples/control_flow.rs @@ -19,7 +19,7 @@ enum Mode { const WAIT_TIME: time::Duration = time::Duration::from_millis(100); const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); println!("Press '1' to switch to Wait mode."); @@ -110,5 +110,5 @@ fn main() { } _ => (), } - }); + }) } diff --git a/examples/cursor.rs b/examples/cursor.rs index fdef7ef9713..e4bf17aa8ac 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -7,7 +7,7 @@ use winit::{ window::{CursorIcon, WindowBuilder}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -48,7 +48,7 @@ fn main() { } _ => (), } - }); + }) } const CURSORS: &[CursorIcon] = &[ diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 79253e106a5..fb673303052 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -7,7 +7,7 @@ use winit::{ window::{CursorGrabMode, WindowBuilder}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -66,5 +66,5 @@ fn main() { }, _ => (), } - }); + }) } diff --git a/examples/custom_events.rs b/examples/custom_events.rs index b39353155af..4951999ca6c 100644 --- a/examples/custom_events.rs +++ b/examples/custom_events.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_match)] #[cfg(not(wasm_platform))] -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { use simple_logger::SimpleLogger; use winit::{ event::{Event, WindowEvent}, @@ -46,7 +46,7 @@ fn main() { } => control_flow.set_exit(), _ => (), } - }); + }) } #[cfg(wasm_platform)] diff --git a/examples/drag_window.rs b/examples/drag_window.rs index 813e9b00c99..ee621a0542c 100644 --- a/examples/drag_window.rs +++ b/examples/drag_window.rs @@ -9,7 +9,7 @@ use winit::{ window::{Window, WindowBuilder, WindowId}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -60,7 +60,7 @@ fn main() { _ => (), }, _ => (), - }); + }) } fn name_windows(window_id: WindowId, switched: bool, window_1: &Window, window_2: &Window) { diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index a71d079f284..bf7b8b2ae17 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -8,7 +8,7 @@ use winit::window::{Fullscreen, WindowBuilder}; #[cfg(target_os = "macos")] use winit::platform::macos::WindowExtMacOS; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -118,5 +118,5 @@ fn main() { }, _ => {} } - }); + }) } diff --git a/examples/handling_close.rs b/examples/handling_close.rs index 1fe4ad37083..4f65acaa9d1 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -7,7 +7,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -82,5 +82,5 @@ fn main() { } _ => (), } - }); + }) } diff --git a/examples/ime.rs b/examples/ime.rs index 59f43d4a334..6bb982b4f2f 100644 --- a/examples/ime.rs +++ b/examples/ime.rs @@ -9,7 +9,7 @@ use winit::{ window::{ImePurpose, WindowBuilder}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new() .with_level(LevelFilter::Trace) .init() @@ -108,5 +108,5 @@ fn main() { } _ => (), } - }); + }) } diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs index 239f5a4ec1f..bd64a784f34 100644 --- a/examples/mouse_wheel.rs +++ b/examples/mouse_wheel.rs @@ -7,7 +7,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -58,5 +58,5 @@ In other words, the deltas indicate the direction in which to move the content ( }, _ => (), } - }); + }) } diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 7e03329d837..155fa430b36 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_match)] #[cfg(not(wasm_platform))] -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; use simple_logger::SimpleLogger; diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 3409af5a656..26a1f55b0f5 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -9,7 +9,7 @@ use winit::{ window::Window, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); diff --git a/examples/request_redraw.rs b/examples/request_redraw.rs index aa53050d026..b42b5ef45fa 100644 --- a/examples/request_redraw.rs +++ b/examples/request_redraw.rs @@ -7,7 +7,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -37,5 +37,5 @@ fn main() { } _ => (), } - }); + }) } diff --git a/examples/request_redraw_threaded.rs b/examples/request_redraw_threaded.rs index d92389c5563..31f83f70f0b 100644 --- a/examples/request_redraw_threaded.rs +++ b/examples/request_redraw_threaded.rs @@ -1,7 +1,7 @@ #![allow(clippy::single_match)] #[cfg(not(wasm_platform))] -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { use std::{thread, time}; use simple_logger::SimpleLogger; @@ -39,7 +39,7 @@ fn main() { } _ => (), } - }); + }) } #[cfg(wasm_platform)] diff --git a/examples/resizable.rs b/examples/resizable.rs index 8f16172fd72..2dc839c3a0c 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -8,7 +8,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -46,5 +46,5 @@ fn main() { }, _ => (), }; - }); + }) } diff --git a/examples/theme.rs b/examples/theme.rs index ac8854e2e34..346bed499c1 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -7,7 +7,7 @@ use winit::{ window::{Theme, WindowBuilder}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -67,5 +67,5 @@ fn main() { }, _ => (), } - }); + }) } diff --git a/examples/timer.rs b/examples/timer.rs index 4d609bef23e..138a78e233b 100644 --- a/examples/timer.rs +++ b/examples/timer.rs @@ -10,7 +10,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -38,5 +38,5 @@ fn main() { } => control_flow.set_exit(), _ => (), } - }); + }) } diff --git a/examples/touchpad_gestures.rs b/examples/touchpad_gestures.rs index 1a749a19a60..f38a1ff67a4 100644 --- a/examples/touchpad_gestures.rs +++ b/examples/touchpad_gestures.rs @@ -5,7 +5,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -42,5 +42,5 @@ fn main() { _ => (), } } - }); + }) } diff --git a/examples/transparent.rs b/examples/transparent.rs index 88c69d1b302..16ebb0d6a37 100644 --- a/examples/transparent.rs +++ b/examples/transparent.rs @@ -7,7 +7,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -30,5 +30,5 @@ fn main() { } => control_flow.set_exit(), _ => (), } - }); + }) } diff --git a/examples/web.rs b/examples/web.rs index 5be8af29de4..97a4399669d 100644 --- a/examples/web.rs +++ b/examples/web.rs @@ -6,7 +6,7 @@ use winit::{ window::WindowBuilder, }; -pub fn main() { +pub fn main() -> std::result::Result<(), impl std::error::Error> { let event_loop = EventLoop::new(); let window = WindowBuilder::new() @@ -33,7 +33,7 @@ pub fn main() { } _ => (), } - }); + }) } #[cfg(wasm_platform)] diff --git a/examples/window.rs b/examples/window.rs index e54a2f661c4..3a44bda3929 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -7,7 +7,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -31,5 +31,5 @@ fn main() { } _ => (), } - }); + }) } diff --git a/examples/window_buttons.rs b/examples/window_buttons.rs index 5d41144dbd0..92137ca6179 100644 --- a/examples/window_buttons.rs +++ b/examples/window_buttons.rs @@ -10,7 +10,7 @@ use winit::{ window::{WindowBuilder, WindowButtons}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -64,5 +64,5 @@ fn main() { } if window_id == window.id() => control_flow.set_exit(), _ => (), } - }); + }) } diff --git a/examples/window_debug.rs b/examples/window_debug.rs index 01077a8b6e1..2678d0ca435 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -10,7 +10,7 @@ use winit::{ window::{Fullscreen, WindowBuilder}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -128,5 +128,5 @@ fn main() { } if window_id == window.id() => control_flow.set_exit(), _ => (), } - }); + }) } diff --git a/examples/window_icon.rs b/examples/window_icon.rs index ed98b3d5779..d7c0504a8fb 100644 --- a/examples/window_icon.rs +++ b/examples/window_icon.rs @@ -9,7 +9,7 @@ use winit::{ window::{Icon, WindowBuilder}, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); // You'll have to choose an icon size at your own discretion. On X11, the desired size varies @@ -43,7 +43,7 @@ fn main() { _ => (), } } - }); + }) } fn load_icon(path: &Path) -> Icon { diff --git a/examples/window_ondemand.rs b/examples/window_ondemand.rs new file mode 100644 index 00000000000..f40e7a71c9e --- /dev/null +++ b/examples/window_ondemand.rs @@ -0,0 +1,86 @@ +#![allow(clippy::single_match)] + +use simple_logger::SimpleLogger; +use winit::{ + event::{Event, WindowEvent}, + event_loop::EventLoop, + window::{WindowBuilder, Window}, platform::run_ondemand::EventLoopExtRunOnDemand, +}; + +#[derive(Default)] +struct App { + window: Option +} + +fn main() -> Result<(), impl std::error::Error> { + SimpleLogger::new().init().unwrap(); + let mut event_loop = EventLoop::new(); + + { + let mut app = App::default(); + + event_loop.run_ondemand(move |event, event_loop, control_flow| { + control_flow.set_wait(); + println!("Run 1: {:?}", event); + + if let Some(window) = &app.window { + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } => { + if window_id == window.id() { + control_flow.set_exit() + } + } + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + } else if let Event::Resumed = event { + app.window = Some( + WindowBuilder::new() + .with_title("Fantastic window number one!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .build(event_loop) + .unwrap() + ); + } + })?; + } + + { + let mut app = App::default(); + + event_loop.run_ondemand(move |event, event_loop, control_flow| { + control_flow.set_wait(); + println!("Run 2: {:?}", event); + + if let Some(window) = &app.window { + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } => { + if window_id == window.id() { + control_flow.set_exit() + } + } + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + } else if let Event::Resumed = event { + app.window = Some( + WindowBuilder::new() + .with_title("Fantastic window number two!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .build(event_loop) + .unwrap() + ); + } + }) + } +} diff --git a/examples/window_run_return.rs b/examples/window_pump_events.rs similarity index 63% rename from examples/window_run_return.rs rename to examples/window_pump_events.rs index f088a51437a..c956141bdaf 100644 --- a/examples/window_run_return.rs +++ b/examples/window_pump_events.rs @@ -9,29 +9,26 @@ android_platform, orbital_platform, ))] -fn main() { - use std::{thread::sleep, time::Duration}; +fn main() -> std::process::ExitCode { + use std::{thread::sleep, time::Duration, process::ExitCode}; use simple_logger::SimpleLogger; use winit::{ - event::{Event, WindowEvent}, + event::{Event, WindowEvent, PumpStatus}, event_loop::EventLoop, - platform::run_return::EventLoopExtRunReturn, + platform::pump_events::EventLoopExtPumpEvents, window::WindowBuilder, }; let mut event_loop = EventLoop::new(); SimpleLogger::new().init().unwrap(); - let _window = WindowBuilder::new() + let window = WindowBuilder::new() .with_title("A fantastic window!") .build(&event_loop) .unwrap(); - let mut quit = false; - - while !quit { - event_loop.run_return(|event, _, control_flow| { - control_flow.set_wait(); + 'main: loop { + let status = event_loop.pump_events(|event, _, control_flow| { if let Event::WindowEvent { event, .. } = &event { // Print only Window events to reduce noise @@ -41,16 +38,17 @@ fn main() { match event { Event::WindowEvent { event: WindowEvent::CloseRequested, - .. - } => { - quit = true; - } + window_id, + } if window_id == window.id() => control_flow.set_exit(), Event::MainEventsCleared => { - control_flow.set_exit(); + window.request_redraw(); } _ => (), } }); + if let PumpStatus::Exit(exit_code) = status { + break 'main ExitCode::from(exit_code as u8); + } // Sleep for 1/60 second to simulate rendering println!("rendering"); @@ -60,5 +58,5 @@ fn main() { #[cfg(any(ios_platform, wasm_platform))] fn main() { - println!("This platform doesn't support run_return."); + println!("This platform doesn't support pump_events."); } diff --git a/examples/window_pump_events_rfd.rs b/examples/window_pump_events_rfd.rs new file mode 100644 index 00000000000..66acbf1e61c --- /dev/null +++ b/examples/window_pump_events_rfd.rs @@ -0,0 +1,72 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "android", +))] +fn main() -> std::process::ExitCode { + use std::{thread::sleep, time::Duration, process::ExitCode}; + + use simple_logger::SimpleLogger; + use winit::{ + event::{Event, WindowEvent, PumpStatus}, + event_loop::EventLoop, + platform::pump_events::EventLoopExtPumpEvents, + window::WindowBuilder, + }; + let mut event_loop = EventLoop::new(); + + SimpleLogger::new().init().unwrap(); + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + 'main: loop { + let status = event_loop.pump_events(|event, _, control_flow| { + + if let Event::WindowEvent { event, .. } = &event { + // Print only Window events to reduce noise + println!("{:?}", event); + } + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => control_flow.set_exit(), + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + }); + if let PumpStatus::Exit(exit_code) = status { + break 'main ExitCode::from(exit_code as u8); + } + + // Sleep for 1/60 second to simulate rendering + println!("rendering"); + + // Uncomment: Will never block the loop + let dialog = rfd::MessageDialog::new() + .set_title("Msg!") + .set_description("Description!") + .set_buttons(rfd::MessageButtons::YesNo) + .show(); + + sleep(Duration::from_millis(33)); + } +} + +#[cfg(any(target_os = "ios", target_arch = "wasm32"))] +fn main() { + println!("This platform doesn't support pump_events."); +} diff --git a/examples/window_resize_increments.rs b/examples/window_resize_increments.rs index ce06daf22f9..59c2e411f56 100644 --- a/examples/window_resize_increments.rs +++ b/examples/window_resize_increments.rs @@ -7,7 +7,7 @@ use winit::{ window::WindowBuilder, }; -fn main() { +fn main() -> std::result::Result<(), impl std::error::Error> { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -53,5 +53,5 @@ fn main() { Event::MainEventsCleared => window.request_redraw(), _ => (), } - }); + }) } diff --git a/src/error.rs b/src/error.rs index c039f3b8686..ab63c48bcc3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,10 @@ pub enum ExternalError { NotSupported(NotSupportedError), /// The OS cannot perform the operation. Os(OsError), + /// The event loop can't be re-run while it's already running + AlreadyRunning, + /// Application has exit with an error status. + ExitFailure(i32) } /// The error type for when the requested operation is not supported by the backend. @@ -59,8 +63,10 @@ impl fmt::Display for OsError { impl fmt::Display for ExternalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { + ExternalError::AlreadyRunning => write!(f, "EventLoop is already running"), ExternalError::NotSupported(e) => e.fmt(f), ExternalError::Os(e) => e.fmt(f), + ExternalError::ExitFailure(status) => write!(f, "Exit Failure: {status}"), } } } diff --git a/src/event.rs b/src/event.rs index 96123683457..0412c98b01e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -323,6 +323,14 @@ pub enum StartCause { Init, } +/// The return status for `pump_events` +pub enum PumpStatus { + /// Continue running external loop + Continue, + /// exit external loop + Exit(i32), +} + /// Describes an event from a [`Window`]. #[derive(Debug, PartialEq)] pub enum WindowEvent<'a> { diff --git a/src/event_loop.rs b/src/event_loop.rs index 1f00f7d7ac1..ee9bc3049d7 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -15,6 +15,7 @@ use instant::{Duration, Instant}; use once_cell::sync::OnceCell; use raw_window_handle::{HasRawDisplayHandle, RawDisplayHandle}; +use crate::error::ExternalError; use crate::{event::Event, monitor::MonitorHandle, platform_impl}; /// Provides a way to retrieve events from the system and from the windows that were registered to @@ -282,23 +283,36 @@ impl EventLoop { EventLoopBuilder::::with_user_event().build() } - /// Hijacks the calling thread and initializes the winit event loop with the provided - /// closure. Since the closure is `'static`, it must be a `move` closure if it needs to + /// Runs the event loop in the calling thread and calls the given `event_handler` closure + /// to dispatch any window system events. + /// + /// Since the closure is `'static`, it must be a `move` closure if it needs to /// access any data from the calling context. /// /// See the [`ControlFlow`] docs for information on how changes to `&mut ControlFlow` impact the /// event loop's behavior. /// - /// Any values not passed to this function will *not* be dropped. - /// /// ## Platform-specific /// /// - **X11 / Wayland:** The program terminates with exit code 1 if the display server /// disconnects. + /// - **iOS:** Will never return to the caller and so values not passed to this function will + /// *not* be dropped before the process exits. + /// + /// iOS applications are recommended to call `run_noreturn()` for added clarity that the + /// function will never return. + /// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript exception + /// (that Rust doesn't see) that will also mean that any values not passed to this function + /// will *not* be dropped. + /// + /// Web applications are recommended to use `spawn()` instead of `run()` to avoid the need + /// for the Javascript exception trick, and to make it clearer that the event loop runs + /// asynchronously (via the browser's own, internal, event loop) and doesn't block the + /// current thread of execution like it does on other platforms. /// /// [`ControlFlow`]: crate::event_loop::ControlFlow #[inline] - pub fn run(self, event_handler: F) -> ! + pub fn run(self, event_handler: F) -> Result<(), ExternalError> where F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 91c50777397..b63ed303da3 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -32,12 +32,19 @@ pub mod windows; #[cfg(x11_platform)] pub mod x11; +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, +))] +pub mod run_ondemand; + #[cfg(any( windows_platform, macos_platform, android_platform, x11_platform, wayland_platform, - orbital_platform ))] -pub mod run_return; +pub mod pump_events; \ No newline at end of file diff --git a/src/platform/pump_events.rs b/src/platform/pump_events.rs new file mode 100644 index 00000000000..9a97b8f8789 --- /dev/null +++ b/src/platform/pump_events.rs @@ -0,0 +1,29 @@ +use crate::{ + event::{Event, PumpStatus}, + event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, +}; + +/// Additional methods on [`EventLoop`] for pumping events within an external event loop +pub trait EventLoopExtPumpEvents { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent; + + fn pump_events(&mut self, event_handler: F) -> PumpStatus + where + F: FnMut(Event<'_, Self::UserEvent>, &EventLoopWindowTarget, &mut ControlFlow); +} + +impl EventLoopExtPumpEvents for EventLoop { + type UserEvent = T; + + fn pump_events(&mut self, event_handler: F) -> PumpStatus + where + F: FnMut( + Event<'_, Self::UserEvent>, + &EventLoopWindowTarget, + &mut ControlFlow, + ), + { + self.event_loop.pump_events(event_handler) + } +} diff --git a/src/platform/run_ondemand.rs b/src/platform/run_ondemand.rs new file mode 100644 index 00000000000..0c2c7253ff9 --- /dev/null +++ b/src/platform/run_ondemand.rs @@ -0,0 +1,68 @@ +use crate::{ + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, error::ExternalError, +}; + +/// Additional methods on [`EventLoop`] to return control flow to the caller. +pub trait EventLoopExtRunOnDemand { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent; + + + /// Runs the event loop in the calling thread and calls the given `event_handler` closure + /// to dispatch any window system events. + /// + /// Unlike [`EventLoop::run`], this function accepts non-`'static` (i.e. non-`move`) closures + /// and it is possible to return control back to the caller without + /// consuming the `EventLoop` (by setting the `control_flow` to [`ControlFlow::Exit`]) and + /// so the event loop can be re-run after it has exit. + /// + /// It's expected that each run of the loop will be for orthogonal instantiations of your + /// Winit application, but internally each instantiation may re-use some common window + /// system resources, such as a display server connection. + /// + /// This API is not designed to run an event loop in bursts that you can exit from and return + /// to while maintaining the full state of your application. (If you need something like this + /// you can look at the [`pump_events()`] API) + /// + /// Each time `run_ondemand` is called the `event_handler` can expect to receive a + /// `NewEvents(Init)` and `Resumed` event (even on platforms that have no suspend/resume + /// lifecycle) - which can be used to consistently initialize application state. + /// + /// See the [`ControlFlow`] docs for information on how changes to `&mut ControlFlow` impact the + /// event loop's behavior. + /// + /// # Caveats + /// - This extension isn't available on all platforms, since it's not always possible to + /// return to the caller (specifically this is impossible on iOS and Web - though with + /// the Web backend it is possible to use `spawn()` more than once instead). + /// - No [`Window`] state can be carried between separate runs of the event loop. + /// + /// You are strongly encouraged to use `run` for portability, unless you specifically need + /// the ability to re-run a single event loop more than once + /// + /// ## Platform-specific + /// + /// See the platform specific notes for [`EventLoop::run`] + /// + /// - **Android:** Although this API could technically be supported on Android, it's currently + /// not exposed because it's very unclear how it could be used meaningfully. + fn run_ondemand(&mut self, event_handler: F) -> Result<(), ExternalError> + where + F: FnMut(Event<'_, Self::UserEvent>, &EventLoopWindowTarget, &mut ControlFlow); +} + +impl EventLoopExtRunOnDemand for EventLoop { + type UserEvent = T; + + fn run_ondemand(&mut self, event_handler: F) -> Result<(), ExternalError> + where + F: FnMut( + Event<'_, Self::UserEvent>, + &EventLoopWindowTarget, + &mut ControlFlow, + ), + { + self.event_loop.run_ondemand(event_handler) + } +} diff --git a/src/platform/run_return.rs b/src/platform/run_return.rs deleted file mode 100644 index 750b0416184..00000000000 --- a/src/platform/run_return.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::{ - event::Event, - event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, -}; - -/// Additional methods on [`EventLoop`] to return control flow to the caller. -pub trait EventLoopExtRunReturn { - /// A type provided by the user that can be passed through [`Event::UserEvent`]. - type UserEvent; - - /// Initializes the `winit` event loop. - /// - /// Unlike [`EventLoop::run`], this function accepts non-`'static` (i.e. non-`move`) closures - /// and returns control flow to the caller when `control_flow` is set to [`ControlFlow::Exit`]. - /// - /// # Caveats - /// - /// Despite its appearance at first glance, this is *not* a perfect replacement for - /// `poll_events`. For example, this function will not return on Windows or macOS while a - /// window is getting resized, resulting in all application logic outside of the - /// `event_handler` closure not running until the resize operation ends. Other OS operations - /// may also result in such freezes. This behavior is caused by fundamental limitations in the - /// underlying OS APIs, which cannot be hidden by `winit` without severe stability repercussions. - /// - /// You are strongly encouraged to use `run`, unless the use of this is absolutely necessary. - /// - /// ## Platform-specific - /// - /// - **X11 / Wayland:** This function returns `1` upon disconnection from - /// the display server. - fn run_return(&mut self, event_handler: F) -> i32 - where - F: FnMut( - Event<'_, Self::UserEvent>, - &EventLoopWindowTarget, - &mut ControlFlow, - ); -} - -impl EventLoopExtRunReturn for EventLoop { - type UserEvent = T; - - fn run_return(&mut self, event_handler: F) -> i32 - where - F: FnMut( - Event<'_, Self::UserEvent>, - &EventLoopWindowTarget, - &mut ControlFlow, - ), - { - self.event_loop.run_return(event_handler) - } -} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index d501f95fc2b..bb7c4d69476 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -196,6 +196,10 @@ fn ndk_keycode_to_virtualkeycode(keycode: Keycode) -> Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + struct PeekableReceiver { recv: mpsc::Receiver, first: Option, @@ -293,7 +297,11 @@ pub struct EventLoop { redraw_flag: SharedFlag, user_events_sender: mpsc::Sender, user_events_receiver: PeekableReceiver, //must wake looper whenever something gets sent + loop_running: bool, // Dispatched `NewEvents` running: bool, + pending_redraw: bool, + control_flow: ControlFlow, + cause: StartCause } #[derive(Default, Debug, Clone, PartialEq)] @@ -347,7 +355,11 @@ impl EventLoop { redraw_flag, user_events_sender, user_events_receiver: PeekableReceiver::from_recv(user_events_receiver), + loop_running: false, running: false, + pending_redraw: false, + control_flow: Default::default(), + cause: StartCause::Init } } @@ -358,7 +370,7 @@ impl EventLoop { pending_redraw: &mut bool, cause: &mut StartCause, callback: &mut F, - ) -> IterationResult + ) where F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), { @@ -640,146 +652,186 @@ impl EventLoop { control_flow, callback, ); + } - let start = Instant::now(); - let (deadline, timeout); + pub fn run(mut self, event_handler: F) -> Result<(), ExternalError> + where + F: 'static + + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + { + self.run_ondemand(event_handler) + } - match control_flow { - ControlFlow::ExitWithCode(_) => { - deadline = None; - timeout = None; - } - ControlFlow::Poll => { - *cause = StartCause::Poll; - deadline = None; - timeout = Some(Duration::from_millis(0)); - } - ControlFlow::Wait => { - *cause = StartCause::WaitCancelled { - start, - requested_resume: None, - }; - deadline = None; - timeout = None; - } - ControlFlow::WaitUntil(wait_deadline) => { - *cause = StartCause::ResumeTimeReached { - start, - requested_resume: *wait_deadline, - }; - timeout = if *wait_deadline > start { - Some(*wait_deadline - start) - } else { - Some(Duration::from_millis(0)) - }; - deadline = Some(*wait_deadline); - } + // Private for now, since it's hard to imagine applications using this on Android + fn run_ondemand(&mut self, mut event_handler: F) -> Result<(), ExternalError> + where + F: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + { + if self.loop_running { + return Err(ExternalError::AlreadyRunning); } - IterationResult { - wait_start: start, - deadline, - timeout, + loop { + match self.pump_events_with_timeout(None, event_handler) { + PumpStatus::Exit(code) => { + + // On other platforms we would check here that the application has destroyed all windows + // but that's not really meaningful on Android + + if code == 0 { + break Ok(()) + } else { + break Err(ExternalError::ExitFailure(exit_code)) + } + } + _ => {} + } } } - pub fn run(mut self, event_handler: F) -> ! + pub fn pump_events(&mut self, mut event_handler: F) -> PumpStatus where - F: 'static - + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); + self.pump_events_with_timeout(Some(Duration::ZERO), event_handler) } - pub fn run_return(&mut self, mut callback: F) -> i32 + fn pump_events_with_timeout(&mut self, timeout: Option, mut callback: F) -> PumpStatus where F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), { - let mut control_flow = ControlFlow::default(); - let mut cause = StartCause::Init; - let mut pending_redraw = false; - - // run the initial loop iteration - let mut iter_result = self.single_iteration( - &mut control_flow, - None, - &mut pending_redraw, - &mut cause, - &mut callback, - ); + if !self.loop_running { + self.loop_running = true; + + // Reset the internal state for the loop as we start running to + // ensure consistent behaviour in case the loop runs and exits more + // than once + self.pending_redraw = false; + self.cause = StartCause::Init; + self.control_flow = ControlFlow::Poll; + + // run the initial loop iteration + let mut iter_result = self.single_iteration( + &mut self.control_flow, + None, + &mut self.pending_redraw, + &mut self.cause, + &mut callback, + ); + } - let exit_code = loop { - if let ControlFlow::ExitWithCode(code) = control_flow { - break code; - } + self.poll_events_with_timeout(timeout, callback); + if let ControlFlow::ExitWithCode(code) = self.control_flow { + self.loop_running = false; - let mut timeout = iter_result.timeout; + sticky_exit_callback( + event::Event::LoopDestroyed, + self.window_target(), + &mut self.control_flow, + &mut callback, + ); - // If we already have work to do then we don't want to block on the next poll... - pending_redraw |= self.redraw_flag.get_and_reset(); - if self.running && (pending_redraw || self.user_events_receiver.has_incoming()) { - timeout = Some(Duration::from_millis(0)) - } + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } + + fn poll_events_with_timeout(&mut self, + timeout: Option, + mut callback: F, + ) + where + F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), + { + let start = Instant::now(); - let app = self.android_app.clone(); // Don't borrow self as part of poll expression - app.poll_events(timeout, |poll_event| { - let mut main_event = None; - - match poll_event { - android_activity::PollEvent::Wake => { - // In the X11 backend it's noted that too many false-positive wake ups - // would cause the event loop to run continuously. They handle this by re-checking - // for pending events (assuming they cover all valid reasons for a wake up). - // - // For now, user_events and redraw_requests are the only reasons to expect - // a wake up here so we can ignore the wake up if there are no events/requests. - // We also ignore wake ups while suspended. - pending_redraw |= self.redraw_flag.get_and_reset(); - if !self.running - || (!pending_redraw && !self.user_events_receiver.has_incoming()) - { - return; + self.pending_redraw |= self.redraw_flag.get_and_reset(); + + timeout = + if self.running && (self.pending_redraw || self.user_events_receiver.has_incoming()) { + // If we already have work to do then we don't want to block on the next poll + Some(Duration::from_millis(0)) + } else { + let control_flow_timeout = match self.control_flow { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::from_millis(0)), + ControlFlow::WaitUntil(wait_deadline) => { + if wait_deadline > start { + Some(wait_deadline - start) + } else { + Some(Duration::from_millis(0)) } } - android_activity::PollEvent::Timeout => {} - android_activity::PollEvent::Main(event) => { - main_event = Some(event); - } - unknown_event => { - warn!("Unknown poll event {unknown_event:?} (ignored)"); - } - } + // `ExitWithCode()` will be reset to `Poll` before polling + ControlFlow::ExitWithCode(code) => unreachable!(), + }; - let wait_cancelled = iter_result - .deadline - .map_or(false, |deadline| Instant::now() < deadline); + min_timeout(control_flow_timeout, timeout) + }; - if wait_cancelled { - cause = StartCause::WaitCancelled { - start: iter_result.wait_start, - requested_resume: iter_result.deadline, - }; + let app = self.android_app.clone(); // Don't borrow self as part of poll expression + app.poll_events(timeout, |poll_event| { + let mut main_event = None; + + match poll_event { + android_activity::PollEvent::Wake => { + // In the X11 backend it's noted that too many false-positive wake ups + // would cause the event loop to run continuously. They handle this by re-checking + // for pending events (assuming they cover all valid reasons for a wake up). + // + // For now, user_events and redraw_requests are the only reasons to expect + // a wake up here so we can ignore the wake up if there are no events/requests. + // We also ignore wake ups while suspended. + self.pending_redraw |= self.redraw_flag.get_and_reset(); + if !self.running + || (!self.pending_redraw && !self.user_events_receiver.has_incoming()) + { + return; + } } + android_activity::PollEvent::Timeout => {} + android_activity::PollEvent::Main(event) => { + main_event = Some(event); + } + unknown_event => { + warn!("Unknown poll event {unknown_event:?} (ignored)"); + } + } - iter_result = self.single_iteration( - &mut control_flow, - main_event, - &mut pending_redraw, - &mut cause, - &mut callback, - ); - }); - }; - - sticky_exit_callback( - event::Event::LoopDestroyed, - self.window_target(), - &mut control_flow, - &mut callback, - ); + self.cause = match self.control_flow { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => { + StartCause::WaitCancelled { + start, + requested_resume: None, + } + }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() > deadline { + StartCause::WaitCancelled { + start, + requested_resume: None, + } + } else { + StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + } + } + } + // `ExitWithCode()` will be reset to `Poll` before polling + ControlFlow::ExitWithCode(code) => unreachable!(), + }; - exit_code + self.single_iteration( + &mut self.control_flow, + main_event, + &mut self.pending_redraw, + &mut self.cause, + &mut callback, + ); + }); } pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 1a85b40f63f..99bc1e10541 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -11,8 +11,8 @@ use std::{ time::Instant, }; -use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp}; -use objc2::foundation::{is_main_thread, NSSize}; +use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp, CFRunLoopStop, CFRunLoopGetCurrent}; +use objc2::{foundation::{is_main_thread, NSSize}, msg_send, class}; use objc2::rc::autoreleasepool; use once_cell::sync::Lazy; @@ -69,9 +69,20 @@ impl EventLoopHandler { let callback = callback.borrow_mut(); (f)(self, callback); } else { + // The `NSApp` 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. + // + // If an application were to use the `rfd` crate to open a file dialog after + // exiting an `EventLoop` we'd also hit this case and don't want to spam + // the log. + // + // We shouldn't `panic!` in this situation. + //log::trace!( panic!( "Tried to dispatch an event, but the event loop that \ - owned the event handler callback seems to be destroyed" + owned the event handler callback has exit" ); } } @@ -90,9 +101,12 @@ impl EventHandler for EventLoopHandler { fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { self.with_callback(|this, mut callback| { if let ControlFlow::ExitWithCode(code) = *control_flow { + // XXX: why isn't event dispatching simply skipped after control_flow = ExitWithCode? + log::warn!("Dispatching non-user event with frozen control_flow, after control_flow set to ExitWithCode({code}): {:?}", event); let dummy = &mut ControlFlow::ExitWithCode(code); (callback)(event.userify(), &this.window_target, dummy); } else { + log::warn!("Dispatching non-user event: {:?}", event); (callback)(event.userify(), &this.window_target, control_flow); } }); @@ -102,9 +116,11 @@ impl EventHandler for EventLoopHandler { self.with_callback(|this, mut callback| { for event in this.window_target.p.receiver.try_iter() { if let ControlFlow::ExitWithCode(code) = *control_flow { + log::warn!("Dispatching user event with frozen control_flow, after control_flow set to ExitWithCode({code})"); let dummy = &mut ControlFlow::ExitWithCode(code); (callback)(Event::UserEvent(event), &this.window_target, dummy); } else { + log::warn!("Dispatching user event"); (callback)(Event::UserEvent(event), &this.window_target, control_flow); } } @@ -114,7 +130,10 @@ impl EventHandler for EventLoopHandler { #[derive(Default)] struct Handler { - ready: AtomicBool, + stop_runloop_on_launch: AtomicBool, + stop_runloop_before_wait: AtomicBool, + launched: AtomicBool, + running: AtomicBool, in_callback: AtomicBool, control_flow: Mutex, control_flow_prev: Mutex, @@ -141,12 +160,39 @@ impl Handler { self.waker.lock().unwrap() } - fn is_ready(&self) -> bool { - self.ready.load(Ordering::Acquire) + /// `true` after `ApplicationDelegate::applicationDidFinishLaunching` called + /// + /// NB: This is global / `NSApp` 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 `NSApp` before it is launched. + fn is_launched(&self) -> bool { + self.launched.load(Ordering::Acquire) } - fn set_ready(&self) { - self.ready.store(true, Ordering::Release); + /// Set via `ApplicationDelegate::applicationDidFinishLaunching` + fn set_launched(&self) { + log::warn!("Set app 'launched'"); + self.launched.store(true, Ordering::Release); + } + + /// `true` if an `EventLoop` is currently running + /// + /// NB: This is global / `NSApp` 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 `NSApp` is launched + /// + /// # Caveat + /// This is only intended to be called from the main thread + fn set_running(&self) { + log::warn!("Set app 'running'"); + self.running.store(true, Ordering::Relaxed); } fn should_exit(&self) -> bool { @@ -156,6 +202,61 @@ impl Handler { ) } + /// 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 `NSApp` 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 exit(&self) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interiour mutability + // + // XXX: As an asside; having each individial 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 unwitingly 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.control_flow_prev.lock().unwrap() = ControlFlow::default(); + *self.control_flow.lock().unwrap() = ControlFlow::default(); + log::warn!("Set app as not 'running' + reset control_flow"); + } + + pub fn request_stop_runloop_on_launch(&self) { + //self.stop_on_launch = true; + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interiour mutability + self.stop_runloop_on_launch.store(true, Ordering::Relaxed); + } + + pub fn should_stop_runloop_on_launch(&self) -> bool { + //self.stop_on_launch + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interiour mutability + self.stop_runloop_on_launch.load(Ordering::Relaxed) + } + + pub fn set_stop_runloop_before_wait(&self, stop_before_wait: bool) { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interiour mutability + self.stop_runloop_before_wait.store(stop_before_wait, Ordering::Relaxed); + } + + pub fn should_stop_runloop_before_wait(&self) -> bool { + // Relaxed ordering because we don't actually have multiple threads involved, we just want + // interiour mutability + self.stop_runloop_before_wait.load(Ordering::Relaxed) + } + fn get_control_flow_and_update_prev(&self) -> ControlFlow { let control_flow = self.control_flow.lock().unwrap(); *self.control_flow_prev.lock().unwrap() = *control_flow; @@ -192,6 +293,10 @@ impl Handler { self.in_callback.store(in_callback, Ordering::Release); } + fn have_callback(&self) -> bool { + self.callback.lock().unwrap().is_some() + } + fn handle_nonuser_event(&self, wrapper: EventWrapper) { if let Some(ref mut callback) = *self.callback.lock().unwrap() { match wrapper { @@ -260,11 +365,39 @@ impl AppState { })); } + 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 `[NSApp run]` but will use `CFRunLoopRunInMode` for subsequent calls to + // `pump_events` + pub fn request_stop_on_launch() { + HANDLER.request_stop_runloop_on_launch(); + } + + pub fn set_stop_runloop_before_wait(stop_before_wait: bool) { + HANDLER.set_stop_runloop_before_wait(stop_before_wait); + } + + pub fn control_flow() -> ControlFlow { + HANDLER.get_old_and_new_control_flow().1 + } + + pub fn clear_callback() { + HANDLER.callback.lock().unwrap().take(); + } + pub fn exit() -> i32 { HANDLER.set_in_callback(true); HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::LoopDestroyed)); HANDLER.set_in_callback(false); - HANDLER.callback.lock().unwrap().take(); + HANDLER.exit(); + Self::clear_callback(); if let ControlFlow::ExitWithCode(code) = HANDLER.get_old_and_new_control_flow().1 { code } else { @@ -272,6 +405,25 @@ impl AppState { } } + pub fn dispatch_init_events() + { + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(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(EventWrapper::StaticEvent(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, @@ -286,30 +438,48 @@ impl AppState { window_activation_hack(&app); app.activateIgnoringOtherApps(activate_ignoring_other_apps); - HANDLER.set_ready(); + 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(); } - HANDLER.set_in_callback(true); - HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(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(EventWrapper::StaticEvent(Event::Resumed)); - HANDLER.set_in_callback(false); + + Self::start_running(); + + // If the `NSApp` is being launched via `EventLoop::pump_events()` then we'll + // want to stop the `CFRunLoop` once the app is launched. + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. + if HANDLER.should_stop_runloop_on_launch() { + // Note: we don't want to do a high-level `[NSApp stop]` since that seems + // to result in events no longer being dispatched if we then try and + // manually run the underlying `RunLoop`. Instead it's less of an + // interference if we stop the underlying `RunLoop` and leave the `NSApp` + // in a "running" state. + + // XXX: can't get this to work :/ I guess the implementation of `NSApp::run` + // just re-starts the loop it it sees its internal `RunLoop` stop. + //log::warn!("Issuing CFRunLoopStop for main run loop"); + //unsafe { + // let rl = CFRunLoopGetMain(); + // CFRunLoopStop(rl); + // let rl = CFRunLoopGetCurrent(); + // CFRunLoopStop(rl); + //} + 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.is_ready() || HANDLER.get_in_callback() { + if panic_info.is_panicking() || !HANDLER.have_callback() || !HANDLER.is_running() || HANDLER.get_in_callback() { return; } let start = HANDLER.get_start_time().unwrap(); @@ -368,13 +538,25 @@ impl AppState { HANDLER.events().push_back(wrapper); } + pub fn stop() { + let app = NSApp(); + autoreleasepool(|_| { + app.stop(None); + // To stop event loop immediately, we need to post some event here. + app.postEvent_atStart(&NSEvent::dummy(), 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 - if panic_info.is_panicking() || !HANDLER.is_ready() || HANDLER.get_in_callback() { + // 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.have_callback() || !HANDLER.is_running() || HANDLER.get_in_callback() { return; } @@ -392,12 +574,11 @@ impl AppState { HANDLER.set_in_callback(false); if HANDLER.should_exit() { - let app = NSApp(); - autoreleasepool(|_| { - app.stop(None); - // To stop event loop immediately, we need to post some event here. - app.postEvent_atStart(&NSEvent::dummy(), true); - }); + Self::stop(); + } + + if HANDLER.should_stop_runloop_before_wait() { + Self::stop(); } HANDLER.update_start_time(); match HANDLER.get_old_and_new_control_flow() { diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 72434ac478b..f8aff32ab65 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -11,9 +11,9 @@ use std::{ sync::mpsc, }; -use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::{base::{CFIndex, CFRelease}, runloop::{CFRunLoopGetCurrent, CFRunLoopRef}, date::CFTimeInterval}; use core_foundation::runloop::{ - kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext, + kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopRun, CFRunLoopRunInMode, kCFRunLoopDefaultMode, CFRunLoopSourceContext, CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, }; use objc2::foundation::is_main_thread; @@ -23,7 +23,7 @@ use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle}; use super::appkit::{NSApp, NSApplicationActivationPolicy, NSEvent}; use crate::{ - event::Event, + event::{Event, PumpStatus}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget}, platform::macos::ActivationPolicy, platform_impl::platform::{ @@ -32,7 +32,7 @@ use crate::{ app_state::{AppState, Callback}, monitor::{self, MonitorHandle}, observer::setup_control_flow_observers, - }, + }, error::ExternalError, }; #[derive(Default)] @@ -183,18 +183,41 @@ impl EventLoop { &self.window_target } - pub fn run(mut self, callback: F) -> ! + /// Returns the "main" `CFRunLoop` which must also be the current `CFRunLoop` + /// + /// # Panics + /// + /// Will panic if the current run loop isn't also the main run loop (I.e. + /// if not called from the main thread) + /* + fn main_loop() -> CFRunLoopRef { + unsafe { + let rl = CFRunLoopGetMain(); + assert_eq!(rl, CFRunLoopGetCurrent()); + rl + } + } + */ + + pub fn run(mut self, callback: F) -> Result<(), ExternalError> where F: 'static + FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { - let exit_code = self.run_return(callback); - process::exit(exit_code); + self.run_ondemand(callback) } - pub fn run_return(&mut self, callback: F) -> i32 + // NB: we don't base this on `pump_events` because for `MacOs` we can't support + // `pump_events` elegantly (we just ask to run the loop for a "short" amount of + // time and so a layered implementation would end up using a lot of CPU due to + // redundant wake ups. + pub fn run_ondemand(&mut self, callback: F) -> Result<(), ExternalError> where F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { + if AppState::is_running() { + return Err(ExternalError::AlreadyRunning); + } + // This transmute is always safe, in case it was reached through `run`, since our // lifetime will be already 'static. In other cases caller should ensure that all data // they passed to callback will actually outlive it, some apps just can't move @@ -217,6 +240,14 @@ impl EventLoop { drop(callback); AppState::set_callback(weak_cb, Rc::clone(&self.window_target)); + + if AppState::is_launched() { + debug_assert!(!AppState::is_running()); + + log::debug!("Starting loop with pre-launched NSApp..."); + AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed` + } + AppState::set_stop_runloop_before_wait(false); unsafe { app.run() }; if let Some(panic) = self.panic_info.take() { @@ -227,7 +258,89 @@ impl EventLoop { }); drop(self._callback.take()); - exit_code + if exit_code == 0 { + Ok(()) + } else { + Err(ExternalError::ExitFailure(exit_code)) + } + } + + pub fn pump_events(&mut self, callback: F) -> PumpStatus + where + F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), + { + log::debug!("Called pump_events"); + + // This transmute is always safe, in case it was reached through `run`, since our + // lifetime will be already 'static. In other cases caller should ensure that all data + // they passed to callback will actually outlive it, some apps just can't move + // everything to event loop, so this is something that they should care about. + let callback = unsafe { + mem::transmute::< + Rc, &RootWindowTarget, &mut ControlFlow)>>, + Rc, &RootWindowTarget, &mut ControlFlow)>>, + >(Rc::new(RefCell::new(callback))) + }; + + self._callback = Some(Rc::clone(&callback)); + + autoreleasepool(|_| { + let app = NSApp(); + + // A bit of juggling with the callback references to make sure + // that `self.callback` is the only owner of the callback. + let weak_cb: Weak<_> = Rc::downgrade(&callback); + drop(callback); + + AppState::set_callback(weak_cb, Rc::clone(&self.window_target)); + + // Note: there are two possible `Init` conditions we have to handle - either the + // `NSApp` is not yet launched, or else the `EventLoop` is not yet running. + + // As a special case, if the `NSApp` hasn't been launched yet then we at least run + // the loop until it has fully launched. + if !AppState::is_launched() { + log::warn!("Launching NSApp..."); + debug_assert!(!AppState::is_running()); + + AppState::request_stop_on_launch(); + unsafe { app.run(); } + + // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the `NSApp` has launched + } else if !AppState::is_running() { + log::warn!("Starting loop with pre-launched NSApp..."); + AppState::start_running(); // Set is_running = true + dispatch `NewEvents(Init)` + `Resumed` + } else { + log::debug!("Running loop until wait"); + AppState::set_stop_runloop_before_wait(true); + unsafe { app.run(); } + log::debug!("Finished running loop until wait"); + + // This kludge timeout is gratuitously borrowed from SDL, since SDL we assume + // this has been found to work reliably enough in the wild: + // XXX: actually this didn't work in conjunction with [NSApp run] + //const SECONDS: CFTimeInterval = 0.000002; + //const SECONDS: CFTimeInterval = 0.2; + //CFRunLoopRunInMode(kCFRunLoopDefaultMode, SECONDS, 1); + } + + if let Some(panic) = self.panic_info.take() { + drop(self._callback.take()); + resume_unwind(panic); + } + AppState::clear_callback(); + }); + drop(self._callback.take()); + + let status = if let ControlFlow::ExitWithCode(code) = AppState::control_flow() { + AppState::exit(); + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + }; + + log::debug!("Finished pump_events"); + status } pub fn create_proxy(&self) -> EventLoopProxy { @@ -245,6 +358,8 @@ pub fn stop_app_on_panic R + UnwindSafe, R>( match catch_unwind(f) { Ok(r) => Some(r), Err(e) => { + log::error!("Stopping NSApp due to a panic!: {:?}", panic_info); + // It's important that we set the panic before requesting a `stop` // because some callback are still called during the `stop` message // and we need to know in those callbacks if the application is currently diff --git a/src/platform_impl/orbital/event_loop.rs b/src/platform_impl/orbital/event_loop.rs index f0ad43d5b3a..846394132de 100644 --- a/src/platform_impl/orbital/event_loop.rs +++ b/src/platform_impl/orbital/event_loop.rs @@ -253,15 +253,6 @@ impl EventLoop { } } - pub fn run(mut self, event_handler: F) -> ! - where - F: 'static - + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), - { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); - } - fn process_event( window_id: WindowId, event_option: EventOption, @@ -394,9 +385,10 @@ impl EventLoop { } } - pub fn run_return(&mut self, mut event_handler_inner: F) -> i32 + pub fn run(mut self, mut event_handler_inner: F) -> Result<(), ExternalError> where - F: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + F: 'static + + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), { // Wrapper for event handler function that prevents ExitWithCode from being unset. let mut event_handler = @@ -639,7 +631,11 @@ impl EventLoop { &mut control_flow, ); - code + if code == 0 { + break Ok(()) + } else { + break Err(ExternalError::ExitFailure(exit_code)) + } } pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 6d5f9d44a53..e98d5dd6330 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -76,7 +76,7 @@ use windows_sys::Win32::{ use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - event::{DeviceEvent, Event, Force, Ime, KeyboardInput, Touch, TouchPhase, WindowEvent}, + event::{DeviceEvent, Event, Force, Ime, KeyboardInput, Touch, TouchPhase, WindowEvent, PumpStatus}, event_loop::{ ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, }, @@ -92,11 +92,13 @@ use crate::{ window_state::{CursorFlags, ImeState, WindowFlags, WindowState}, wrap_device_id, Fullscreen, WindowId, DEVICE_ID, }, - window::WindowId as RootWindowId, + window::WindowId as RootWindowId, error::ExternalError, }; use runner::{EventLoopRunner, EventLoopRunnerShared}; -use super::window::set_skip_taskbar; +use self::runner::RunnerState; + +use super::{window::set_skip_taskbar}; type GetPointerFrameInfoHistory = unsafe extern "system" fn( pointerId: u32, @@ -238,18 +240,23 @@ impl EventLoop { &self.window_target } - pub fn run(mut self, event_handler: F) -> ! + pub fn run(mut self, event_handler: F) -> Result<(), ExternalError> where F: 'static + FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); + self.run_ondemand(event_handler) } - pub fn run_return(&mut self, mut event_handler: F) -> i32 + pub fn run_ondemand(&mut self, mut event_handler: F) -> Result<(), ExternalError> where F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), { + let runner = &self.window_target.p.runner_shared; + + if runner.state() != RunnerState::Uninitialized { + return Err(ExternalError::AlreadyRunning); + } + let event_loop_windows_ref = &self.window_target; unsafe { @@ -261,7 +268,6 @@ impl EventLoop { }); } - let runner = &self.window_target.p.runner_shared; let exit_code = unsafe { let mut msg = mem::zeroed(); @@ -300,7 +306,76 @@ impl EventLoop { } runner.reset_runner(); - exit_code + + if exit_code == 0 { + Ok(()) + } else { + Err(ExternalError::ExitFailure(exit_code)) + } + } + + pub fn pump_events(&mut self, mut event_handler: F) -> PumpStatus + where + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + { + let runner = &self.window_target.p.runner_shared; + + if runner.state() == RunnerState::Uninitialized { + let event_loop_windows_ref = &self.window_target; + unsafe { + self.window_target + .p + .runner_shared + .set_event_handler(move |event, control_flow| { + event_handler(event, event_loop_windows_ref, control_flow) + }); + runner.poll(); + } + } + + unsafe { + let mut msg = mem::zeroed(); + + 'pump: loop { + if PeekMessageW(&mut msg, 0, 0, 0, PM_REMOVE) == false.into() { + break 'pump; + } + + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + + if let ControlFlow::ExitWithCode(_code) = runner.control_flow() { + if !runner.handling_events() { + break 'pump; + } + } + } + }; + + if let ControlFlow::ExitWithCode(code) = runner.control_flow() { + unsafe { + runner.loop_destroyed(); + + // Immediately reset the internal state for the loop to allow + // the loop to be run more than once. + runner.reset_runner(); + } + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } } pub fn create_proxy(&self) -> EventLoopProxy { diff --git a/src/platform_impl/windows/event_loop/runner.rs b/src/platform_impl/windows/event_loop/runner.rs index c4ac1eb3484..ebef5b111af 100644 --- a/src/platform_impl/windows/event_loop/runner.rs +++ b/src/platform_impl/windows/event_loop/runner.rs @@ -47,7 +47,7 @@ pub type PanicError = Box; /// See `move_state_to` function for details on how the state loop works. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum RunnerState { +pub(crate) enum RunnerState { /// The event loop has just been created, and an `Init` event must be sent. Uninitialized, /// The event loop is idling. @@ -133,6 +133,10 @@ impl EventLoopRunner { } } + pub fn state(&self) -> RunnerState { + self.runner_state.get() + } + pub fn control_flow(&self) -> ControlFlow { self.control_flow.get() }