Rust GUI frameworks
2016 Windowing 2026: Near-universal, but 0.31 is stuck in beta
04

winit: the window everyone borrows

Almost every framework in this book stands on the same window library.

winit creates windows and delivers input: keyboard, mouse, touch, resize, close. It does not draw anything. That narrow job is why so much of this book is built on it. When we resolved the latest release of egui, iced, Slint, Xilem, Dioxus Native and Freya, every one landed on winit 0.30.13. Here is the smallest complete program.

winit-window/src/bin/one.rs ✔ compiled · winit 0.30 · rustc 1.95.0
use book_error::{Error, Result};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};

#[derive(Default)]
struct App {
    window: Option<Window>,
    // Handler methods return `()`, so they cannot use `?`. Stash the failure
    // here and let `main` report it after the loop exits.
    error: Option<Error>,
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let attrs = Window::default_attributes().with_title("hello winit");
        match event_loop.create_window(attrs) {
            Ok(window) => self.window = Some(window),
            Err(e) => {
                self.error = Some(e.into());
                event_loop.exit();
            }
        }
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
        if let WindowEvent::CloseRequested = event {
            event_loop.exit();
        }
    }
}

fn main() -> Result<()> {
    let mut app = App::default();
    EventLoop::new()?.run_app(&mut app)?;
    app.error.take().map_or(Ok(()), Err)
}

Version 0.30 replaced the old closure-based event loop with the ApplicationHandler trait. Windows are created in resumed rather than at startup because mobile and web platforms can suspend and resume an app, and the window may not exist yet when main runs. Every toolkit in this book, directly or indirectly, implements something like this handler. Note how it handles failure: handler methods return (), so they cannot use ?. If creating the window fails, the app stores the error and exits the loop, and main returns it afterwards. The pattern is explained on the error-handling page.

winit-window/src/bin/two.rs ✔ compiled · winit 0.30 · rustc 1.95.0
use book_error::{Error, Result};
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::window::{Window, WindowId};

struct App {
    window: Option<Window>,
    size: PhysicalSize<u32>,
    error: Option<Error>,
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        match event_loop.create_window(Window::default_attributes()) {
            Ok(window) => self.window = Some(window),
            Err(e) => {
                self.error = Some(e.into());
                event_loop.exit();
            }
        }
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
        match event {
            // A renderer resizes its swapchain here; a UI toolkit re-runs layout.
            WindowEvent::Resized(size) => self.size = size,
            // The toolkit paints here, then asks for the next frame if animating.
            WindowEvent::RedrawRequested => {
                if let Some(window) = &self.window {
                    let scale = window.scale_factor();
                    println!("paint {}x{} @ {scale}x", self.size.width, self.size.height);
                }
            }
            WindowEvent::CloseRequested => event_loop.exit(),
            _ => {}
        }
    }
}

fn main() -> Result<()> {
    let mut app = App { window: None, size: PhysicalSize::new(0, 0), error: None };
    EventLoop::new()?.run_app(&mut app)?;
    app.error.take().map_or(Ok(()), Err)
}

This is the seam where a window library meets a toolkit. Resized is where a renderer recreates its swapchain and a layout engine re-runs. RedrawRequested is where the toolkit paints. Some frameworks do not use winit at all: Tauri uses tao, its fork of winit; Floem ships a fork (floem-winit); and gpui, Makepad and Druid have their own platform layers. The crate's 0.31 beta has been open for months as of this writing, which makes winit the most load-bearing pre-1.0 crate in the stack.

crates.io first publish. See also the July–Aug 2026 ecosystem report. Source →