Skip to content

Winit API redesign #3367

@kchibisov

Description

@kchibisov

Discussion summary.

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 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 run APIs, they work fine and there's no general issue with
    them.
  • POD-like types.
  • While the events will spread, they'll still be there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    S - apiDesign and usability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions