Skip to content

Commit 164dce2

Browse files
ribkchibisov
authored andcommitted
Add EventLoopExtPumpEvents and EventLoopExtRunOnDemand
This adds two new extensions for running a Winit event loop which will replace `EventLoopExtRunReturn` The `run_return` API is trying to solve multiple problems and address multiple, unrelated, use cases but in doing so it is not succeeding at addressing any of them fully. The notable use cases we have are: 1. Applications want to be able to implement their own external event loop and call some Winit API to poll / pump events, once per iteration of their own loop, without blocking the outer, external loop. Addressing #2706 2. Applications want to be able to re-run separate instantiations of some Winit-based GUI and want to allow the event loop to exit with a status, and then later be able to run the loop again for a new instantiation of their GUI. Addressing #2431 It's very notable that these use cases can't be supported across all platforms and so they are extensions, similar to `EventLoopExtRunReturn` The intention is to support these extensions on: - Windows - Linux (X11 + Wayland) - macOS - Android These extensions aren't compatible with Web or iOS though. 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) Once all backends have support for these extensions then we can remove `EventLoopExtRunReturn` For simplicity, the extensions are documented with the assumption that the above platforms will be supported. This patch makes no functional change, it only introduces these new extensions so we can then handle adding platform-specific backends in separate pull requests, so the work can be landed in stages.
1 parent 0ba4283 commit 164dce2

File tree

3 files changed

+303
-1
lines changed

3 files changed

+303
-1
lines changed

src/error.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::{error, fmt};
22

33
use crate::platform_impl;
44

5-
/// An error whose cause it outside Winit's control.
5+
// TODO: Rename
6+
/// An error that may be generated when requesting Winit state
67
#[derive(Debug)]
78
pub enum ExternalError {
89
/// The operation is not supported by the backend.
@@ -25,6 +26,19 @@ pub struct OsError {
2526
error: platform_impl::OsError,
2627
}
2728

29+
/// A general error that may occur while running the Winit event loop
30+
#[derive(Debug)]
31+
pub enum RunLoopError {
32+
/// The operation is not supported by the backend.
33+
NotSupported(NotSupportedError),
34+
/// The OS cannot perform the operation.
35+
Os(OsError),
36+
/// The event loop can't be re-run while it's already running
37+
AlreadyRunning,
38+
/// Application has exit with an error status.
39+
ExitFailure(i32),
40+
}
41+
2842
impl NotSupportedError {
2943
#[inline]
3044
#[allow(dead_code)]
@@ -77,6 +91,18 @@ impl fmt::Display for NotSupportedError {
7791
}
7892
}
7993

94+
impl fmt::Display for RunLoopError {
95+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
96+
match self {
97+
RunLoopError::AlreadyRunning => write!(f, "EventLoop is already running"),
98+
RunLoopError::NotSupported(e) => e.fmt(f),
99+
RunLoopError::Os(e) => e.fmt(f),
100+
RunLoopError::ExitFailure(status) => write!(f, "Exit Failure: {status}"),
101+
}
102+
}
103+
}
104+
80105
impl error::Error for OsError {}
81106
impl error::Error for ExternalError {}
82107
impl error::Error for NotSupportedError {}
108+
impl error::Error for RunLoopError {}

src/platform/pump_events.rs

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
use crate::{
2+
event::Event,
3+
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
4+
};
5+
6+
/// The return status for `pump_events`
7+
pub enum PumpStatus {
8+
/// Continue running external loop.
9+
Continue,
10+
/// Exit external loop.
11+
Exit(i32),
12+
}
13+
14+
/// Additional methods on [`EventLoop`] for pumping events within an external event loop
15+
pub trait EventLoopExtPumpEvents {
16+
/// A type provided by the user that can be passed through [`Event::UserEvent`].
17+
type UserEvent;
18+
19+
/// Pump the `EventLoop` to check for and dispatch pending events.
20+
///
21+
/// This API is designed to enable applications to integrate Winit into an
22+
/// external event loop, for platforms that can support this.
23+
///
24+
/// The given `timeout` limits how long it may block waiting for new events.
25+
///
26+
/// Passing a `timeout` of `Some(Duration::ZERO)` would ensure your external
27+
/// event loop is never blocked but you would likely need to consider how
28+
/// to throttle your own external loop.
29+
///
30+
/// Passing a `timeout` of `None` means that it may wait indefinitely for new
31+
/// events before returning control back to the external loop.
32+
///
33+
/// ## Example
34+
///
35+
/// ```rust,no_run
36+
/// # // Copied from examples/window_pump_events.rs
37+
/// # #[cfg(any(
38+
/// # windows_platform,
39+
/// # macos_platform,
40+
/// # x11_platform,
41+
/// # wayland_platform,
42+
/// # android_platform,
43+
/// # ))]
44+
/// fn main() -> std::process::ExitCode {
45+
/// # use std::{process::ExitCode, thread::sleep, time::Duration};
46+
/// #
47+
/// # use simple_logger::SimpleLogger;
48+
/// # use winit::{
49+
/// # event::{Event, WindowEvent},
50+
/// # event_loop::EventLoop,
51+
/// # platform::pump_events::{EventLoopExtPumpEvents, PumpStatus},
52+
/// # window::WindowBuilder,
53+
/// # };
54+
/// let mut event_loop = EventLoop::new();
55+
/// #
56+
/// # SimpleLogger::new().init().unwrap();
57+
/// let window = WindowBuilder::new()
58+
/// .with_title("A fantastic window!")
59+
/// .build(&event_loop)
60+
/// .unwrap();
61+
///
62+
/// 'main: loop {
63+
/// let status = event_loop.pump_events(|event, _, control_flow| {
64+
/// # if let Event::WindowEvent { event, .. } = &event {
65+
/// # // Print only Window events to reduce noise
66+
/// # println!("{event:?}");
67+
/// # }
68+
/// #
69+
/// match event {
70+
/// Event::WindowEvent {
71+
/// event: WindowEvent::CloseRequested,
72+
/// window_id,
73+
/// } if window_id == window.id() => control_flow.set_exit(),
74+
/// Event::MainEventsCleared => {
75+
/// window.request_redraw();
76+
/// }
77+
/// _ => (),
78+
/// }
79+
/// });
80+
/// if let PumpStatus::Exit(exit_code) = status {
81+
/// break 'main ExitCode::from(exit_code as u8);
82+
/// }
83+
///
84+
/// // Sleep for 1/60 second to simulate application work
85+
/// //
86+
/// // Since `pump_events` doesn't block it will be important to
87+
/// // throttle the loop in the app somehow.
88+
/// println!("Update()");
89+
/// sleep(Duration::from_millis(16));
90+
/// }
91+
/// }
92+
/// ```
93+
///
94+
/// **Note:** This is not a portable API, and its usage involves a number of
95+
/// caveats and trade offs that should be considered before using this API!
96+
///
97+
/// You almost certainly shouldn't use this API, unless you absolutely know it's
98+
/// the only practical option you have.
99+
///
100+
/// ## Synchronous events
101+
///
102+
/// Some events _must_ only be handled synchronously via the closure that
103+
/// is passed to Winit so that the handler will also be synchronized with
104+
/// the window system and operating system.
105+
///
106+
/// This is because some events are driven by a window system callback
107+
/// where the window systems expects the application to have handled the
108+
/// event before returning.
109+
///
110+
/// **These events can not be buffered and handled outside of the closure
111+
/// passed to Winit.**
112+
///
113+
/// As a general rule it is not recommended to ever buffer events to handle
114+
/// them outside of the closure passed to Winit since it's difficult to
115+
/// provide guarantees about which events are safe to buffer across all
116+
/// operating systems.
117+
///
118+
/// Notable events that will certainly create portability problems if
119+
/// buffered and handled outside of Winit include:
120+
/// - `RedrawRequested` events, used to schedule rendering.
121+
///
122+
/// macOS for example uses a `drawRect` callback to drive rendering
123+
/// within applications and expects rendering to be finished before
124+
/// the `drawRect` callback returns.
125+
///
126+
/// For portability it's strongly recommended that applications should
127+
/// keep their rendering inside the closure provided to Winit.
128+
/// - Any lifecycle events, such as `Suspended` / `Resumed`.
129+
///
130+
/// The handling of these events needs to be synchronized with the
131+
/// operating system and it would never be appropriate to buffer a
132+
/// notification that your application has been suspended or resumed and
133+
/// then handled that later since there would always be a chance that
134+
/// other lifecycle events occur while the event is buffered.
135+
///
136+
/// ## Supported Platforms
137+
/// - Windows
138+
/// - Linux
139+
/// - MacOS
140+
/// - Android
141+
///
142+
/// ## Unsupported Platforms
143+
/// - **Web:** This API is fundamentally incompatible with the event-based way in which
144+
/// Web browsers work because it's not possible to have a long-running external
145+
/// loop that would block the browser and there is nothing that can be
146+
/// polled to ask for new new events. Events are delivered via callbacks based
147+
/// on an event loop that is internal to the browser itself.
148+
/// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so
149+
/// there's no way to support the same approach to polling as on MacOS.
150+
///
151+
/// ## Platform-specific
152+
/// - **Windows**: The implementation will use `PeekMessage` when checking for
153+
/// window messages to avoid blocking your external event loop.
154+
///
155+
/// - **MacOS**: The implementation works in terms of stopping the global `NSApp`
156+
/// whenever the application `RunLoop` indicates that it is preparing to block
157+
/// and wait for new events.
158+
///
159+
/// This is very different to the polling APIs that are available on other
160+
/// platforms (the lower level polling primitives on MacOS are private
161+
/// implementation details for `NSApp` which aren't accessible to application
162+
/// developers)
163+
///
164+
/// It's likely this will be less efficient than polling on other OSs and
165+
/// it also means the `NSApp` is stopped while outside of the Winit
166+
/// event loop - and that's observable (for example to crates like `rfd`)
167+
/// because the `NSApp` is global state.
168+
///
169+
/// If you render outside of Winit you are likely to see window resizing artifacts
170+
/// since MacOS expects applications to render synchronously during any `drawRect`
171+
/// callback.
172+
fn pump_events<F>(&mut self, event_handler: F) -> PumpStatus
173+
where
174+
F: FnMut(
175+
Event<'_, Self::UserEvent>,
176+
&EventLoopWindowTarget<Self::UserEvent>,
177+
&mut ControlFlow,
178+
);
179+
}
180+
181+
impl<T> EventLoopExtPumpEvents for EventLoop<T> {
182+
type UserEvent = T;
183+
184+
fn pump_events<F>(&mut self, event_handler: F) -> PumpStatus
185+
where
186+
F: FnMut(
187+
Event<'_, Self::UserEvent>,
188+
&EventLoopWindowTarget<Self::UserEvent>,
189+
&mut ControlFlow,
190+
),
191+
{
192+
self.event_loop.pump_events(event_handler)
193+
}
194+
}

src/platform/run_ondemand.rs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use crate::{
2+
error::RunLoopError,
3+
event::Event,
4+
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
5+
};
6+
7+
#[cfg(doc)]
8+
use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window};
9+
10+
/// Additional methods on [`EventLoop`] to return control flow to the caller.
11+
pub trait EventLoopExtRunOnDemand {
12+
/// A type provided by the user that can be passed through [`Event::UserEvent`].
13+
type UserEvent;
14+
15+
/// Runs the event loop in the calling thread and calls the given `event_handler` closure
16+
/// to dispatch any window system events.
17+
///
18+
/// Unlike [`EventLoop::run`], this function accepts non-`'static` (i.e. non-`move`) closures
19+
/// and it is possible to return control back to the caller without
20+
/// consuming the `EventLoop` (by setting the `control_flow` to [`ControlFlow::Exit`]) and
21+
/// so the event loop can be re-run after it has exit.
22+
///
23+
/// It's expected that each run of the loop will be for orthogonal instantiations of your
24+
/// Winit application, but internally each instantiation may re-use some common window
25+
/// system resources, such as a display server connection.
26+
///
27+
/// This API is not designed to run an event loop in bursts that you can exit from and return
28+
/// to while maintaining the full state of your application. (If you need something like this
29+
/// you can look at the [`EventLoopExtPumpEvents::pump_events()`] API)
30+
///
31+
/// Each time `run_ondemand` is called the `event_handler` can expect to receive a
32+
/// `NewEvents(Init)` and `Resumed` event (even on platforms that have no suspend/resume
33+
/// lifecycle) - which can be used to consistently initialize application state.
34+
///
35+
/// See the [`ControlFlow`] docs for information on how changes to `&mut ControlFlow` impact the
36+
/// event loop's behavior.
37+
///
38+
/// # Caveats
39+
/// - This extension isn't available on all platforms, since it's not always possible to
40+
/// return to the caller (specifically this is impossible on iOS and Web - though with
41+
/// the Web backend it is possible to use `spawn()` more than once instead).
42+
/// - No [`Window`] state can be carried between separate runs of the event loop.
43+
///
44+
/// You are strongly encouraged to use [`EventLoop::run()`] for portability, unless you specifically need
45+
/// the ability to re-run a single event loop more than once
46+
///
47+
/// # Supported Platforms
48+
/// - Windows
49+
/// - Linux
50+
/// - macOS
51+
/// - Android
52+
///
53+
/// # Unsupported Platforms
54+
/// - **Web:** This API is fundamentally incompatible with the event-based way in which
55+
/// Web browsers work because it's not possible to have a long-running external
56+
/// loop that would block the browser and there is nothing that can be
57+
/// polled to ask for new events. Events are delivered via callbacks based
58+
/// on an event loop that is internal to the browser itself.
59+
/// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS.
60+
fn run_ondemand<F>(&mut self, event_handler: F) -> Result<(), RunLoopError>
61+
where
62+
F: FnMut(
63+
Event<'_, Self::UserEvent>,
64+
&EventLoopWindowTarget<Self::UserEvent>,
65+
&mut ControlFlow,
66+
);
67+
}
68+
69+
impl<T> EventLoopExtRunOnDemand for EventLoop<T> {
70+
type UserEvent = T;
71+
72+
fn run_ondemand<F>(&mut self, event_handler: F) -> Result<(), RunLoopError>
73+
where
74+
F: FnMut(
75+
Event<'_, Self::UserEvent>,
76+
&EventLoopWindowTarget<Self::UserEvent>,
77+
&mut ControlFlow,
78+
),
79+
{
80+
self.event_loop.run_ondemand(event_handler)
81+
}
82+
}

0 commit comments

Comments
 (0)