-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Winit API redesign #3367
Description
Motivation
We need feedback for events and immediate reactions
The current API doesn't really work when you must get some values from the users
in reply to some events, because users has a clear choice to not do so. In some
cases it could lead to undesired behavior and even crashes. An example of such
thing could be:
- the dead key API (it goes via globals now).
- Deal with scale factor changes/configure races on Wayland #2921
- Allow some events to perform cancellation. #3349
- Support
Backbutton/KeyCodeon Android #2304 (require saying that
you've handled, otherwise you'll block everything - Guarantee sending WindowEvent::Resized on window creation? #3192 (you need to ask during
window creation) - Clipboard APIs (we need to pick mime-type), the same with DND.
- Live resizes (when you can adjust the size during resize).
The getting feedback part could be solved vi abuse of Mutex but it's not like
you can force users into doing correct cross-platform code, which actually
behaves the same.
There are more issues due to current massive callback approach, I just picked
few of them.
Monolithic design
In the current form winit is monolithic meaning that if the users want to use
winit they must include the entire crate introducing a lot of dependencies
and increase in compile time.
This is not great since GUIs don't really need all the winit or all the
backends. We also have a situation where you use winit or die, which is also
not great, so having a crate which provides mostly types could benefit.
Other issue of monolithic design is that winit is growing on its backends and
there's a demand to support niche platforms. Unfortunately, we can't provide
good support for all of them with the current maintainers we have. And some
platforms we can't event test reliably (e.g. redox). To solve this
winit should allow writing out-of-tree backends.
Interior mutability mess
Winit historically marks the Window as Send + Sync, while it's actually good
to write multi threaded code, internally it all goes back to event loop thread,
this is true for Windows, Wayland, Web, iOS.
If you even tried contributing into early winit it was event worse on interior
mutabilty and the use of Mutex, which were not need in 99% of the time.
Objects actually have lifetime
Window and other objects created while event loop is running actually has
lifetime and generally can't be used when the event loop is paused.
This issue is pretty clear with the run_on_demand sort of APIs where we must
destroy everything between the runs.
This issue is usually solved with lifetimes or Weak objects. The Weak
objects may require some sort of Upgrade in the API and doing so in
every call possible is strange. While the lifetimes sounds scary here,
it's a matter how you look at them, since one option is to give a
reference into the resource owned by winit, thus you can use object
only via the event loop is running.
The key could be some Id type and you may fail to get the resource if
it's no longer available.
The relevant issue #2903
No way to exclude functionality
The API surface is huge and we need a reliable extension system, so
the users won't end up with functionality they likely don't need.
One approach could be features, but it'll make the testing really hard, since
you'd need to test every per-mutation, the more natural approach would be
IDETs
. But they don't work with the massive callback design or declerative async API.
Proposed solution
To address the raised issues the proposed solution is being worked on
in https://github.com/rust-windowing/winit-next. It's not even remotely
complete but it should show the vector of development.
The user facing API is based around the Application trait, which
may be extended,
pub trait Application: ApplicationWindow {
/// Emitted when new events arrive from the OS to be processed.
fn new_events(&mut self, loop_handle: &mut dyn EventLoopHandle, start_cause: StartCause);
/// Emitted when the event loop is about to block and wait for new events.
fn about_to_wait(&mut self, loop_handle: &mut dyn EventLoopHandle);
/// Emitted when the event loop is being shut down.
fn loop_exiting(&mut self, loop_handle: &mut dyn EventLoopHandle);
// The APIs which we consider optional (IDETs).
#[inline(always)]
fn touch_handler(&mut self) -> Option<&mut dyn TouchInputHandler> {
None
}
#[inline(always)]
fn device_events_handelr(&mut self) -> Option<&mut dyn DeviceEventsHandler> {
None
}
}and the event loop handler.
/// Handle for the event loop.
pub trait EventLoopHandle: HasDisplayHandle {
/// Request to create a window.
fn create_window(&mut self, attributes: &WindowAttributes) -> Result<(), ()>;
fn num_windows(&self) -> usize;
fn get_window(&self, window_id: WindowId) -> Option<&dyn Window>;
fn get_window_mut(&mut self, window_id: WindowId) -> Option<&mut dyn Window>;
fn get_monitor(&self, monitor_id: MonitorId) -> Option<&dyn Monitor>;
fn monitors(&self) -> Vec<&dyn Monitor>;
fn exit(&mut self);
}The backend facing APIs uses the traits like
pub trait Monitor {
/// Return the given monitor id.
fn id(&self) -> MonitorId;
/// Returns a human-readable name of the monitor.
///
/// Returns `None` if the monitor doesn't exist anymore.
fn name(&self) -> Option<String>;
/// Returns the monitor's resolution.
fn size(&self) -> PhysicalSize<u32>;
/// Returns the top-left corner position of the monitor relative to the
/// larger full screen area.
fn position(&self) -> PhysicalPosition<i32>;
/// The monitor refresh rate used by the system.
///
/// Return `Some` if succeed, or `None` if failed, which usually happens
/// when the monitor the window is on is removed.
///
/// When using exclusive fullscreen, the refresh rate of the
/// [`VideoModeHandle`] that was used to enter fullscreen should be used
/// instead.
fn refresh_rate_millihertz(&self) -> Option<u32>;
fn scale_factor(&self) -> f64;
}An example could be seen at the winit-next
repo
What is not clear with the current approach
Multithreaded is not clear, but it's actually solvable. For example we may
have a way to enqueue callbacks into event loop thread or have types which
can perform rendering related requests from non-main thread.
However, we won't be able to provide all Window APIs on non-main thread, but
it is like that on most backends, except X11 and recent Wayland, so having
explicit API to queue callback which will get the relevant state will
work around the same to how it worked before.
We could also have a Weak<View> to pass to other threads, though given
that most drawing libraries Surface types are Send nothing stops
from sending the render target and some fences to ensure that the window
is not dropped in-between of the rendering.
Alternatives
Not aware of other options and given that most toolkits(outside of rust) do
generally similar things, it's not like their approach is bad.
What will not change
Fortunately, not everything will change, the things which likely stay
the same will be:
- Event loop
runAPIs, they work fine and there's no general issue with
them. - POD-like types.
- While the events will spread, they'll still be there.