diff --git a/src/error.rs b/src/error.rs index c039f3b8686..bfddb7733ea 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 4111e1f64ae..32ea144647d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -329,6 +329,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/platform/pump_events.rs b/src/platform/pump_events.rs new file mode 100644 index 00000000000..188c681e589 --- /dev/null +++ b/src/platform/pump_events.rs @@ -0,0 +1,152 @@ +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; + + /// Pump the `EventLoop` to check for and dispatch pending events, without blocking + /// + /// This API is designed to enable applications to integrate Winit into an + /// external event loop, for platforms that can support this. + /// + /// ```rust + /// use winit::{event::{Event, PumpStatus, WindowEvent}, event_loop::EventLoop, window::WindowBuilder }; + /// use winit::platform::pump_events::EventLoopExtPumpEvents; + /// let mut event_loop = EventLoop::new(); + /// + /// let window = WindowBuilder::new() + /// .with_title("A fantastic window!") + /// .build(&event_loop) + /// .unwrap(); + /// + /// 'main: loop { + /// let status = event_loop.pump_events(|event, _, control_flow| { + /// 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 application work + /// // + /// // Since `pump_events` doesn't block it will be important to + /// // throttle the loop in the app somehow. + /// println!("Update()"); + /// sleep(Duration::from_millis(16)); + /// } + /// ``` + /// + /// **Note:** This is not a portable API, and its usage involves a number of + /// caveats and trade offs that should be considered before using this API! + /// + /// You almost certainly shouldn't use this API, unless you absolutely know it's + /// the only practical option you have. + /// + /// # Synchronous events + /// + /// Some events _must_ only be handled synchronously via the closure that + /// is passed to Winit so that the handler will also be synchronized with + /// the window system and operating system. + /// + /// This is because some events are driven by a window system callback + /// where the window systems expects the application to have handled the + /// event before returning. + /// + /// **These events can not be buffered and handled outside of the closure + /// passed to Winit** + /// + /// As a general rule it is not recommended to ever buffer events to handle + /// them outside of the closure passed to Winit since it's difficult to + /// provide guarantees about which events are safe to buffer across all + /// operating systems. + /// + /// Notable events that will certainly create portability problems if + /// buffered and handled outside of Winit include: + /// - `RenderRequested` events, used to schedule rendering. + /// + /// macOS for example uses a `drawRect` callback to drive rendering + /// within applications and expects rendering to be finished before + /// the `drawRect` callback returns. + /// + /// For portability it's strongly recommended that applications should + /// keep their rendering inside the closure provided to Winit. + /// - Any lifecycle events, such as `Suspended` / `Resumed`. + /// + /// The handling of these events needs to be synchronized with the + /// operating system and it would never be appropriate to buffer a + /// notification that your application has been suspended or resumed and + /// then handled that later since there would always be a chance that + /// other lifecycle events occur while the event is buffered. + /// + /// # Supported Platforms + /// - Windows + /// - Linux + /// - MacOS + /// - Android + /// + /// # Unsupported Platforms + /// - Web: This API is fundamentally incompatible with the event-based way in which + /// Web browsers work because it's not possible to have a long-running external + /// loop that would block the browser and there is nothing that can be + /// polled to ask for new new events. Events are delivered via callbacks based + /// on an event loop that is internal to the browser itself. + /// - iOS: It's not possible to stop and start an `NSApplication` repeatedly on iOS so + /// there's no way to support the same approach to polling as on MacOS. + /// + /// # Platform-specific + /// - **Windows**: The implementation will use `PeekMessage` when checking for + /// window messages to avoid blocking your external event loop. + /// + /// - **MacOS**: The implementation works in terms of stopping the global `NSApp` + /// whenever the application `RunLoop` indicates that it is preparing to block + /// and wait for new events. + /// + /// This is very different to the polling APIs that are available on other + /// platforms (the lower level polling primitives on MacOS are private + /// implementation details for `NSApp` which aren't accessible to application + /// developers) + /// + /// It's likely this will be less efficient than polling on other OSs and + /// it also means the `NSApp` is stopped while outside of the Winit + /// event loop - and that's observable (for example to crates like `rfd`) + /// because the `NSApp` is global state. + /// + /// If you render outside of Winit you are likely to see window resizing artifacts + /// since MacOS expects applications to render synchronously during any `drawRect` + /// callback. + 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..13ce4ac78f9 --- /dev/null +++ b/src/platform/run_ondemand.rs @@ -0,0 +1,79 @@ +use crate::{ + error::ExternalError, + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, +}; + +/// 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 + /// + /// # Supported Platforms + /// - Windows + /// - Linux + /// - macOS + /// - Android + /// + /// # Unsupported Platforms + /// - Web: This API is fundamentally incompatible with the event-based way in which + /// Web browsers work because it's not possible to have a long-running external + /// loop that would block the browser and there is nothing that can be + /// polled to ask for new new events. Events are delivered via callbacks based + /// on an event loop that is internal to the browser itself. + /// - iOS: It's not possible to stop and start an `NSApplication` repeatedly on iOS. + 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) + } +}