Winit 0.20, the state of windowing in Rust, and a request for help

Hello, all!

I’m one of the maintainers of Winit, the main pure-Rust window creation library. Even if you haven’t used it directly, you’ve probably heard of projects that depend on it - Servo and Alacritty being the best-known applications that depend on our codebase. If you’ve done any graphics programming in Rust using Glutin (or dependent projects including gfx-rs, Glium, and Amethyst) we’ve been the ones making the windows actually show up on your desktop.

This announcement details the major changes since Winit 0.19. Also, we are looking for new contributors! If you are interested in working on the foundations of Rust’s GUI story, now is a great time to join the project.

Winit 0.20 alpha 1 and the new event loop

The highlight of this release is the final major overhaul of Winit’s core Event Loop API. This
change vastly improves Winit’s stability and introduces a few quality-of-life upgrades, such as
native timer support and an API for injecting custom user events into the Winit event loop. For example:

#[derive(Debug, Clone, Copy)]
enum CustomEvent {
    Timer,
}

let event_loop = EventLoop::<CustomEvent>::new_user_event();
// `EventLoopProxy` allows dispatching custom events
/// to the WInit event loop from any thread.
let event_loop_proxy = event_loop.create_proxy();

let timer_length = Duration::new(1, 0);

event_loop.run(move |event, _, control_flow| {
    match event {
        // When the event loop starts running, queue the timer.
        Event::NewEvents(StartCause::Init) => {
            *control_flow = ControlFlow::WaitUntil(Instant::now() + timer_length);
        },

        // When the timer expires, dispatch a timer event and queue a new timer.
        Event::NewEvents(StartCause::ResumeTimeReached{..}) => {
            event_loop_proxy.send_event(CustomEvent::Timer).ok();
            *control_flow = ControlFlow::WaitUntil(Instant::now() + timer_length);
        },

        Event::UserEvent(event) => println!("user event: {:?}", event),

        Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
            *control_flow = ControlFlow::Exit;
        },

        _ => ()
    }
});

This release also cleans up Winit’s API, universally improving both its internal consistency and its consistency with the rest of the Rust ecosystem.

Updating

You can get 0.20.0-alpha1 from crates.io by adding the following lines to your Cargo.toml:

[dependencies]
winit = "0.20.0-alpha1"

If you’d like to use the new features through Glutin, add the following lines instead:

[dependencies]
glutin = "0.22.0-alpha1"

Why an Alpha release?

There are a few reasons we’re introducing this as an alpha:

  1. All implementations still need thorough testing, and some platforms still have major, application-breaking bugs.
  2. We’d like to merge an overhaul of Winit’s HiDPI API, but the overhaul is only implemented on Windows.

However, we lack the personnel necessary to implement those changes!

A call for help

That segues nicely into the second point I’d like to bring up: for such a core piece of infrastructure in the Rust ecosystem, we have astonishingly little manpower. Only three of the seven platforms we support have active maintainers: X11, macOS, Android, iOS, and WASM are all largely unmaintained, despite their importance! Additionally, Glutin is currently maintained by just a single person, despite being a project no lone individual has the time to handle. This had led to a great deal of issue buildup on those platforms, since we lack contributors with enough time to debug and resolve issues on those platforms. Major new features and API improvements can’t land on crates.io since we currently can’t implement them across all platforms - gamepad support being the most significant (although certainly not only) example.

If you are interested in Rust becoming a larger force in graphics ecosystem, please take a look at the issues below and find one you can contribute to! We encourage all types of contributions, so go out and write, test, review, and submit PRs, add and review documentation, and whatever else you would like to do. No matter what you do, your time would be very much appreciated, and would result in widespread improvements across the Rust community.

High-level issues:

FAQ

Why remove poll_events completely?

poll_events only functions as expected on Linux.

All other platforms break in some way with applications built around poll_events - some breaking in subtle ways, others breaking completely. For example, when a user resizes the application’s window, Windows* and macOS freeze the main event loop completely if the event loop uses poll_events. On the more extreme end, Mobile and Web platforms don’t ever return from poll_events, completely breaking any application that relies on poll_events functioning.

As much as we’d like to expose poll_events as an API, the reality is that we cannot expose it without fundamentally lying about its cross-platform functionality.

* You might notice that calling poll_events on Windows in legacy Winit doesn’t freeze the event loop. This is because legacy Winit spawns an entire background thread to run the Windows event loop in order to hide the freezing behavior. This has caused innumerable amounts of stability and UX problems, and moving to the new event loop model is the only way to fix those issues without creating an unstable tower of hacks.

Since poll_events is dead, is there any sort of replacement?

Yep! The run_forever API has been renamed to run, and received a few major usability upgrades:

  1. You now receive an event when the platform event queue has been drained, allowing you to bundle your application logic into a single event handler.
  2. New windows can be created within the event handler closure, using the EventLoopWindowTarget field.
  3. You can now set timers in the event loop, adding proper support for framerate limiters and other timer-based functionality.

These new features make run actually usable in a real application, and should more than compensate for poll_events’ removal.

Why does the new run function not return?

It was the only way for us to expose a single API that more or less behaves the same way on all platforms.

Admittedly, it isn’t necessary on desktop platforms (Windows, macOS, and Linux). However, Android, iOS, and WASM all require the host platform take exclusive control over the application’s event loop, and this API allows us to expose that behavior under the same API as our desktop backends.

If, for whatever reason, you absolutely must be able to return from the run function, we expose run_return on our desktop platforms. We’d discourage using that unless absolutely necessary, since it breaks cross-platform compatibility and can lead to subtle bugs on desktop platforms if used improperly.

Why break the API’s backwards compatibility now?

There were several small details in Winit’s public API that we weren’t happy with, but required breaking changes to fix. Since we were already breaking downstream applications with the new event loop API, we decided to try bundling all our desired breaking changes into a single release instead of painfully staggering them out over several point releases. This will make upgrading to 0.20 a bit more of a hassle, but should make the API significantly easier to use going forward.

Unfortunately we didn’t quite manage to include all the breaking changes we wanted this release, but breaking changes in the future should be significantly less disruptive and will be bundled with major feature releases.

Why is control_flow passed by reference to the event loop callback, instead of returned?

With a return-based solution, there isn’t one clear “correct” way of handling the loop’s control flow. Consider the following event loop:

event_loop.run(|event| {
    match event {
        Event::EventsCleared => ControlFlow::Poll,
        _ => ControlFlow::Wait
    }
});

That situation leads to return patterns like Wait, Wait, Wait, {...}, Poll. What do you do with the Waits that are returned before the Poll? You could interpret them as “halt the event loop between each call to the function”, which is pretty clearly nonsense. However, it’s the only solution that doesn’t involve throwing away values. Other solutions could be to have some sort of “control flow precedence pattern” where returning one type overrides future returns of another type, or to ignore all of the return values except for the one from EventsCleared (in which case what’s the point of having it return anyway!).

As a persistent parameter, there are no values we have to ignore and it’s clear when the control flow of the loop actually changes. Under this, Wait, WaitUntil, and Poll only impact control flow when the event queue has been cleared, and Exit kills the loop immediately.

49 Likes

Thanks for this! Graphics is not my forte, so I can’t really pitch in, but:

You’re missing a link :slight_smile:

1 Like

This sounds pretty interesting. I’ve been intending to get into something GUI for a while, and my two month summer vacation just started today. I personally use X11, so I can probably help somewhere. :slight_smile:

6 Likes

Thanks so much for taking the lead on winit of late @Osspial! In my opinion winit is one of the most fundamental crates of the Rust ecosystem and you’ve been doing excellent work alongside vberger, zgentry @gentz and many others I sure I’m forgetting right now.

Is there some way myself and other winit appreciators might donate to the cause other than time? I have contributed a lot in the past, but lately my focus has been on nannou and atm cpal - winit has been working “well enough” so that I haven’t had to dive in in quite a while :slight_smile: that said, I’m sure I’ll dive back in at some point. In the meantime, please let us know if you do open up an avenue for regular donations such as patreon or opencollective!

It might also be worth exploring what kind of grants Mozilla have on offer, I’d imagine winit would have a good chance considering higher level projects based on winit have been having success recently (amethyst comes to mind). Then again, I have very little idea how these things work!

4 Likes

Vulkan could use VisualID on XLib.

I was just looking to get into graphics/gui development in Rust. :smiley:

I haven’t done much of this kind of development since I graduated two years ago (which is a shame I’m trying to fix). But I’ll read through and see if I’m able to pitch in yet.

2 Likes

zgentry

I had to signup just to say that that’s been the most creative butchering of my handle that I’ve ever seen. :laughing:

3 Likes

That’s actually pretty surprising that we don’t already expose that. Have you filed an issue with us yet, so we can track it better?

@cheako could you open an issue for that in our issue tracker? It’ll be easier to keep track of progress and notify contributors that it’s necessary if it’s in there.

@alice @CDMcGwire Thank you so much! The issues tagged Good first issue should be decent places to start out, since you guys aren’t familiar with the codebase. If you’ve got any questions when looking at them, don’t hesitate to ask.


@mindtree Thanks for offering! Having some way to donate is an idea that’s been thrown around a lot, and it’s something that I’d be interested in setting up. However, accepting and distributing money (particularly, figuring out who to pay and in what amounts) for what has has largely been volunteer work is a prospect that scares me and is something that I’d like some help getting off the ground. Perhaps that’s just my inexperience with financials talking - it’s hard to say - but regardless, it’s a venture I’m wary about diving into.

In the meanwhile, I’ll take a look at Patreon, Mozilla grants, and Opencollective.There are a few people I can probe irl that I think should be helpful, but any links to additional articles or people that can help with this would be hugely appreciated.

2 Likes

I’m so sorry, memory definitely failed me here! Let me patch it :blush:

1 Like

I’d also be happy to help in any way I can. Hardly an expert but I’ve used glutin before for a little project, know most of OpenGL, am starting playing around with amethyst engine a little bit atm.

Hi! I’d be glad to help with maintenance and bug fixing for macOS

4 Likes

I’ll try doing tests on X11 and I could test on android, but I’m completely new to this area.

2 Likes

@vbogaevsky Thanks! If you’d like to start getting familiar with the macOS codebase, I’d suggest taking a look at issue #939 - that should give you a nice tour through the codebase, as guided by error messages.

@proc Hey, we’ve all had to start somewhere :slight_smile: . If you want to get pinged for testing, add yourself to this list and we’ll message you when an issue for X11 comes up.

Hi! I’d love to help with Android support, though I’m by no means any expert with the Android native interface.

Does the android_glue package also need maintenance, or just Winit?

2 Likes

I’ll start right away

2 Likes

Added myself.
If that helps anything: I’ve got a hybrid nvidia/intel stack running on X11 with Wayland on top (KDE).

2 Likes

Thanks!

As far as I understand it, android_glue needs some additional work to enable some features in Winit, but that work should be directed by Winit implementation work.

Hi @Osspial , I am concerned about the new run model and how it fits into a “game loop” style such as:

loop {
     poll_events();
     poll_custom_events(); // for input events not supported by Winit
     update();
     render();
     wait_vsync();
}

I use Winit within this model on a multiplatform (Linux/Win/macOS) app. What is the pattern to switch from this to the new model? Are the migration paths documented?

Also, how does the run_forever model works with GUI frameworks which also have their own run_forever loop?

Thanks for your work!

2 Likes

Could EventLoop::create_proxy(), which creates an EventLoopProxy that can be used to inject custom events into the winit event loop, enable the used cases that you are after?

The ControlFlow parameter to EventLoop::run() also enables one to setup a continuous polling mode, which avoids unwanted blocking when you’re polling multiple event sources and doing your own blocking behind the scenes (as with wait_vsync() in your example).