Winit Code Examples: A Practical Guide
Introduction
Winit is a window creation and event loop management library for Rust. It supports a variety of platforms, including Windows, macOS, Linux, and WebAssembly. Winit is a low-level library, which means that it gives you a lot of control over how your application's windows and events are handled. This can be useful for creating custom windowing systems or for integrating with other libraries that require low-level access to the windowing system.
This guide provides practical code examples for using Winit, focusing on common tasks such as window creation, event handling, and basic rendering.
Setting Up Winit
First, you need to add Winit as a dependency in your Cargo.toml file:
[dependencies]
winit = "0.28"
Then, in your main.rs file, you can start using Winit:
use winit::{event::*, event_loop::*, window::*};
fn main() {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("A fantastic window!")
.build(&event_loop)
.unwrap();
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id,
} if window_id == window.id() => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
// Application update code.
// Queue a RedrawRequested event.
window.request_redraw();
}
Event::RedrawRequested(window_id) if window_id == window.id() => {
// Redraw the application.
//
// It's preferable to render in a separate thread, so that
// rendering doesn't block the event loop, and vice-versa.
//
// For now, just print a message to the console.
println!("redrawing!");
}
_ => (),
}
});
}
This code sets up a basic event loop and creates a window. The event loop waits for events, such as window close requests, and then processes them. Let's break down the key parts of this example.
Explanation of the Code
- Dependencies: We import necessary modules from the
winitcrate, includingevent,event_loop, andwindow. - EventLoop Creation: We create an
EventLoop, which is the core of Winit's event handling. - Window Creation: We build a new window using
WindowBuilder. Here, we set the title of the window. Thebuildmethod returns aResult, so we useunwrapfor simplicity (error handling is crucial in real applications). - Event Loop Run: The
event_loop.runmethod takes a closure that is called for every event. We set theControlFlowtoWait, which means the application will wait for new events. - Event Matching: Inside the closure, we match on the
eventtype:WindowEvent::CloseRequested: This event is triggered when the user tries to close the window. We setControlFlow::Exitto terminate the application.MainEventsCleared: This event is triggered when all pending events have been processed. It's a good place to put application update code.RedrawRequested: This event is triggered when the window needs to be redrawn. We callwindow.request_redraw()to queue a redraw.
Handling Window Events
Winit provides a variety of events that you can handle, including keyboard input, mouse input, and window resizing. Here's an example of how to handle keyboard input:
use winit::{event::*, event_loop::*, window::*};
fn main() {
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("Keyboard Input Example")
.build(&event_loop)
.unwrap();
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id
} if window_id == window.id() => *control_flow = ControlFlow::Exit,
Event::WindowEvent {
event: WindowEvent::KeyboardInput {
input: KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
},
..
},
window_id
} if window_id == window.id() => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
window.request_redraw();
}
Event::RedrawRequested(window_id) if window_id == window.id() => {
println!("redrawing!");
}
_ => (),
}
});
}
Explanation of Keyboard Input Handling
- KeyboardInput Event: We add a new match arm for
WindowEvent::KeyboardInput. - Matching Key Presses: We check if the
stateisElementState::Pressedand thevirtual_keycodeisVirtualKeyCode::Escape. - Exiting on Escape: If the Escape key is pressed, we set
ControlFlow::Exitto close the application.
This example demonstrates how to listen for specific key presses. You can extend this to handle other keys and input events. — Hayward, CA: Find Your Next Rental Home
Basic Rendering with Winit and a Graphics API
Winit doesn't include a rendering API itself, but it provides the necessary hooks to integrate with popular graphics libraries like wgpu (WebGPU), OpenGL, or Vulkan. Here's a basic example using wgpu to render a simple colored triangle:
Dependencies
Add the following dependencies to your Cargo.toml file:
winit = "0.28"
wgpu = "0.19"
env_logger = "0.10"
log = "0.4"
future = "0.3"
Code Example
use winit::{event::*, event_loop::*, window::*};
wgpu::SurfaceError;
use wgpu::util::DeviceExt;
use std::time::Instant;
use log::info;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
color: [f32; 3],
}
impl Vertex {
const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
0 => Float32x3,
1 => Float32x3,
];
fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}
const VERTICES: &[Vertex] = &[
Vertex { position: [-0.0868241, 0.49240386, 0.0], color: [0.5, 0.0, 0.5] }, // Triangle top center
Vertex { position: [-0.49513406, 0.06958647, 0.0], color: [0.5, 0.0, 0.5] }, // Triangle left corner
Vertex { position: [-0.21918549, -0.44939706, 0.0], color: [0.5, 0.0, 0.5] }, // Triangle right bottom
];
const INDICES: &[u16] = &[0, 1, 2];
struct State {
surface: wgpu::Surface,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
size: winit::dpi::PhysicalSize<u32>,
render_pipeline: wgpu::RenderPipeline,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
num_indices: u32,
#[cfg(target_arch = "wasm32")]
window: Window, // Store the window for WASM
}
impl State {
// Creating some of the wgpu types requires async code
async fn new(window: Window) -> Self {
let size = window.inner_size();
// The instance is a handle to our GPU
// BackendBit::PRIMARY => Automatic selection of backend
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
dx12_shader_compiler: wgpu::Dx12Compiler::default(),
});
// # Safety
//
// The surface needs to live as long as the window that created it.
// State owns the window so this should be safe.
let surface = unsafe { instance.create_surface(&window) }.unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
// WebGL doesn't support all of wgpu's features, so the requested
// feature set must be a subset of the available features.
limits: if cfg!(target_arch = "wasm32") {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
wgpu::Limits::default()
},
},
None, // Trace path
)
.await
.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
// Shader code in this tutorial assumes an sRGB surface texture.
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
};
surface.configure(&device, &config);
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
});
let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Render Pipeline Layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main", // 1.
buffers: &[Vertex::desc()], // 2.
},
fragment: Some(wgpu::FragmentState {
// 3. Required for output
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent::REPLACE,
alpha: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList, // 1.
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw, // 2.
cull_mode: Some(wgpu::Face::Back),
// Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE
polygon_mode: wgpu::PolygonMode::Fill,
// Requires Features::DEPTH_CLIP_CONTROL
unclipped_depth_buffer_replace: false,
// Requires Features::CONSERVATIVE_RASTERIZATION
conservative: false,
},
depth_stencil: None, // 4.
multisample: wgpu::MultisampleState {
count: 1, // 5.
mask: !0, // 6.
alpha_to_coverage_enabled: false, // 7.
},
multiview: None, // 8.
});
let vertex_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsages::VERTEX,
}
);
let index_buffer = device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(INDICES),
usage: wgpu::BufferUsages::INDEX,
}
);
let num_indices = INDICES.len() as u32;
Self {
surface,
device,
queue,
config,
size,
render_pipeline,
vertex_buffer,
index_buffer,
num_indices,
#[cfg(target_arch = "wasm32")]
window
}
}
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
self.config.width = new_size.width;
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
}
}
fn input(&mut self, _event: &WindowEvent) -> bool {
false
}
fn update(&mut self) {
// no-op
}
fn render(&mut self) -> Result<(), SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[
Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(
wgpu::Color {
r: 0.1,
g: 0.2,
b: 0.3,
a: 1.0,
}
),
store: wgpu::StoreOp::Store,
},
})
],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
render_pass.draw_indexed(0..self.num_indices, 0, 0..1);
}
let command_buffer = encoder.finish();
self.queue.submit(std::iter::once(command_buffer));
output.present();
Ok(())
}
}
#[cfg_attr(target_arch="wasm32", wasm_bindgen(start))]
pub async fn run() {
cfg_if::cfg_if! {
if #[cfg(target_arch = "wasm32")] {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Warn).expect("Could't initialize logger");
} else {
env_logger::init();
}
}
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("WGPU Triangle Demo")
.build(&event_loop)
.unwrap();
#[cfg(target_arch = "wasm32")]
{
// Winit prevents sizing with CSS, so we have to
// set the size manually when on the web.
use winit::dpi::LogicalSize;
use winit::platform::web::WindowExtWebSys;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
window.set_inner_size(LogicalSize::new(450, 400));
let canvas = window.create_element("canvas").unwrap();
canvas.set_width(450);
canvas.set_height(400);
body.append_child(&canvas).unwrap();
use wasm_bindgen::JsCast;
let web_sys_window = window.clone();
let web_sys_canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
let _ = window.document().unwrap().body().unwrap()
.append_child(&web_sys_canvas);
let window = window.canvas().unwrap();
}
let mut state = State::new(window).await;
let event_loop_window_target = event_loop.window_target();
let mut last_render_time = Instant::now();
event_loop.run(move |event, target| {
target.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent {
ref event,
window_id,
} if window_id == state.surface.get_texture().unwrap().id() => {
if !state.input(event) {
match event {
WindowEvent::CloseRequested
| WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(VirtualKeyCode::Escape),
..
},
..
} => target.exit(),
WindowEvent::Resized(physical_size) => {
state.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged {
new_inner_size, ..
} => {
// new_inner_size is &mut so we have to dereference it twice
state.resize(**new_inner_size);
}
_ => {}
}
}
}
Event::RedrawRequested(window_id) if window_id == state.surface.get_texture().unwrap().id() => {
let now = Instant::now();
let dt = now - last_render_time;
last_render_time = now;
state.update();
match state.render() {
Ok(_) => {},
// Reconfigure the surface if lost
Err(wgpu::SurfaceError::Lost) => state.resize(state.size),
// The system is out of memory - we should probably quit
Err(wgpu::SurfaceError::OutOfMemory) => target.exit(),
// All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e),
}
}
Event::MainEventsCleared => {
// RedrawRequested will only trigger once, unless we manually
// request it.
state.surface.get_texture().unwrap().request_redraw();
}
_ => {},
}
});
}
fn main() {
#[cfg(not(target_arch="wasm32"))]
{
//env_logger::init();
env_logger::init_from_env(
env_logger::Env::default().filter_or("my_program", "trace"));
// Temporarily avoid srgb formats for the surface on the web
if let Err(e) = pollster::block_on(run()) {
eprintln!("{}", e);
}
}
#[cfg(target_arch="wasm32")]
{
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Warn).expect("Could't initialize logger");
wasm_bindgen_futures::spawn_local(run());
}
}
WGSL Shader (shader.wgsl)
Create a file named shader.wgsl in the same directory as your main.rs and add the following content:
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(
model: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4(model.position, 1.0);
out.color = model.color;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4(in.color, 1.0);
}
Explanation of the Rendering Code
- Dependencies: We include
wgpufor graphics rendering. - Vertex Definition: We define a
Vertexstruct with position and color attributes. - Shader Setup: We load a WGSL shader and create a render pipeline.
- Buffers: We create vertex and index buffers to hold the triangle data.
- State Struct: The
Statestruct holds all the necessary resources for rendering. - Rendering: The
renderfunction retrieves the current texture, creates a view, sets up a render pass, and draws the triangle. - Event Handling: The event loop handles window events and redraw requests.
FAQ Section
Q1: What is Winit?
Winit is a window creation and event loop management library for Rust. It allows you to create and manage windows across various platforms. — Oil City, PA Weather: Forecast & Updates
Q2: Why use Winit over other windowing libraries?
Winit provides low-level control over windowing and event handling, making it suitable for custom windowing systems and integration with graphics libraries like wgpu, OpenGL, and Vulkan. It is cross-platform and supports Windows, macOS, Linux, and WebAssembly.
Q3: How do I handle different types of events in Winit?
Winit uses an event loop that processes events. You can match on the event type and handle it accordingly. Common events include window close requests, keyboard input, mouse input, and window resizing.
Q4: Can Winit be used for rendering graphics?
Winit does not include a rendering API itself, but it provides hooks to integrate with graphics libraries like wgpu. You can use Winit to create a window and then use a graphics library to render content inside it.
Q5: How do I set up Winit in my project?
To set up Winit, add it as a dependency in your Cargo.toml file: winit = "0.28". Then, import the necessary modules in your main.rs file and create an event loop and a window. — National Taco Day 2025: Get Free Tacos!
Conclusion
Winit is a powerful library for creating and managing windows in Rust applications. This guide provided practical code examples for window creation, event handling, and basic rendering with wgpu. By using Winit, you can build cross-platform applications with fine-grained control over windowing and event handling.