4 releases

0.1.3 Nov 29, 2021
0.1.2 Nov 29, 2021
0.1.1 Jan 11, 2021
0.1.0 Jan 11, 2021

#413 in Procedural macros

Custom license

20KB
276 lines

Winit-main

This is a winit utility which abstracts away winit's event-loop inversion of control.

Rationale

Winit necessarily hijacks the main thread due to platform constraints, creating a "don't call us, we'll call you" situation. Inversions of control have some undesirable properties, including:

  • It's difficult to add inversion of control to a program after the fact, as it tends to fundamentally affect the program's architecture.
  • For the above reason, it's difficult to write programs which are generic between inversions-of-control, or to modify a program from using one inversion-of-control framework to using a different framework.
  • It's tricky to use several inversions of control simultaneously. For example, it would be difficult to combine tokio with winit without creating additional abstractions.

Solution

This library spawns your code on a second thread (a "simulated main thread"), hijacks the real main thread with winit's event loop, and provides your code handles to communicate with the main event loop. This allows you to write your program as you would any other program, treating winit's event loop as an iterator of events and a handle with which to create windows and ask about the system. When the simulated main thread exits, it triggers the event loop to exit, shutting down the process, just like if it were the real main thread.

Handling of Control Flow

Blockers

The simulated main thread receives winit Events through an EventReceiver. In these events, the user event type is a Blocker. This is a concurrency structure emitted by the main thread which blocks the event loop from processing further winit events until the Blocker is dropped. This is a way to synchronize the event loop with the simulated main thread to some extent, such as to synchronize the presenting of images.

Whenever the event loop encounters a RedrawRequested event, it immediately emits a Blocker, and thus will not proceed until the simulated main thread receives and drops that Blocker.

ControlFlow

This library keeps the winit event loop in the ControlFlow::Wait state. Therefore, if you want to redraw a window in a loop, you should call Window::request_redraw after every draw.

Example

Without winit-main:

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-main (no proc macro):

use winit_main::reexports::{
    event::{Event, WindowEvent},
    window::WindowAttributes,
};

fn main() {
    winit_main::run(|event_loop, events| {
        let window = event_loop
            .create_window(WindowAttributes::default())
            .unwrap();

        for event in events.iter() {
            if matches!(
                event,
                Event::WindowEvent {
                    event: WindowEvent::CloseRequested,
                    window_id,
                } if window_id == window.id()
            ) {
                break;
            }
        }
    });
}

With winit-main (with proc macro):

use winit_main::{
    reexports::{
        event::{Event, WindowEvent},
        window::WindowAttributes,
    },
    EventLoopHandle,
    EventReceiver,
};


#[winit_main::main]
fn main(event_loop: EventLoopHandle, events: EventReceiver) {
    let window = event_loop
        .create_window(WindowAttributes::default())
        .unwrap();

    for event in events.iter() {
        if matches!(
            event,
            Event::WindowEvent {
                event: WindowEvent::CloseRequested,
                window_id,
            } if window_id == window.id()
        ) {
            break;
        }
    }
}

Project Maturity

This project is young, barely tested, and probably poorly optimized. The main form of test is the wgpu-example package in this repository, which is the triangle example from the wgpu project, modified to utilize this abstraction.

Dependencies

~2.3–3.5MB
~77K SLoC