#events #winit #thread #run-time #windowing #proxy #separate

nightly winit-modular

proxy winit event loops which can be run at the same time, on separate threads

2 releases

0.1.1 Aug 10, 2022
0.1.0 Aug 10, 2022

#1052 in GUI

Apache-2.0

56KB
724 lines

winit-modular: proxy winit event loops which can be run at the same time, on separate threads

Provides an API very similar to winit except the EventLoop type can be created on multiple threads, multiple event loops can exist simultaneously, you can poll for events or receive them asynchronously, and more.

Notice: This library is still very early in development, and certain features are not fully tested. If you encounter any issues, please submit an issue or PR on github.

The problem

winit is the de-facto way to create windows and listen to system events in Rust. However the core struct EventLoop, which allows you to create windows and listen to events, has some annoying restrictions:

  • It must be created on the main thread
  • It must be created only once
  • When EventLoop::run ends, your application exits.

which imply more restrictions:

  • You can't run multiple event loops simultanously
  • You can't stop an event loop and later run another one
  • You can't have multiple dependencies which create event loops.

There is also the issue of inversion of control which winit-main explains and attempts to solve.

The solution

This crate fixes winit's lack of modularity, sort of. It provides EventLoops which have a similar api to winit::event_loop::EventLoop, but:

  • Can exist and run simultaneously on separate threads or even the same thread (see run)
  • Can run asynchronously (see run_async)
  • Can be polled (see run_immediate), fixing the "inversion of control" issue
  • You can stop calls to any of these and drop the event loops without exiting your entire application.

This works as these EventLoops are actually proxies, which forward their calls and recieve events from the main event loop using asynchronous channels and atomics.

Drawbacks

This doesn't completely alleviate winit's issues and is not always drop-in replacement.

Most importantly, you must call winit_modular::run exactly once in your application, on the main thread, before using the event loops in this crate. If you don't you will get a panic message explaining this. as winit_modular::run hijacks the main thread, it provides a callback to run the rest of your code on the background thread.

Also the performance penalty from using multiple threads and sending messages across channels. The proxy event loops must communicate with the actual winit event loop across thread bounds, for every operation or intercepted event. That means, often once every frame refresh. Fortunately, modern hardware is generally fast enough and threading is good enough that even then it's a minor performance penalty. But on embedded systems, or if you are spawning a lot of proxy event loops simultaneously, it could be an issue.

Example (originally from winit-main)

Without winit_modular:

use winit::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    window::WindowBuilder,
};

fn main() {
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new().build(&event_loop).unwrap();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        if matches!(
            event,
            Event::WindowEvent {
                event: WindowEvent::CloseRequested,
                window_id,
            } if window_id == window.id()
        ) {
            *control_flow = ControlFlow::Exit;
        }
    });
}

With winit-modular:

use winit_modular::{
    event::{Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop}
};
use pollster::block_on;

fn main() {
    winit_modular::run(|| block_on(async {
        let event_loop = EventLoop::new().await;
        
        let window = event_loop
            .create_window(|builder| builder)
            .await
            .unwrap();

        event_loop.run_async(|event, control_flow| {
            if matches!(
                event,
                Event::WindowEvent {
                    event: WindowEvent::CloseRequested,
                    window_id,
                } if window_id == window.id()
            ) {
                *control_flow = ControlFlow::ExitApp;
            }
        }).await;
    }));
}

Dependencies

~3–6.5MB
~123K SLoC