Window & Input Handling
We use the winit crate to create a window and handle inputs (keyboard/mouse). This is the bridge between the OS and our application.
Create src/core/window.rs. This file will:
- Define the
Winstruct which holds our application state. - Implement the
ApplicationHandlertrait to respond to OS events. - Handle cross-platform differences (Native vs WASM).
The Code
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
#[cfg(target_arch = "wasm32")]
use winit::event_loop::EventLoop;
use winit::{
application::ApplicationHandler,
event::{DeviceEvent, ElementState, KeyEvent, MouseButton, WindowEvent},
keyboard::PhysicalKey,
window::Window,
};
// We will create this in the next chapter
use crate::core::renderer::Ren;
#[allow(dead_code)]
pub struct Win {
// WASM requires a proxy to send events back to the main loop
#[cfg(target_arch = "wasm32")]
proxy: Option<winit::event_loop::EventLoopProxy<Ren>>,
ren: Option<Ren>, // Our Renderer instance
// FPS counting state
last_fps_update: Option<Instant>,
frame_count: u32,
last_frame_time: Option<Instant>,
frame_time: f32,
// Input state
pointer_pos: [u32; 2],
mouse_right_pressed: bool,
}
#[allow(dead_code)]
impl Win {
pub fn new(#[cfg(target_arch = "wasm32")] event_loop: &EventLoop<Ren>) -> Self {
#[cfg(target_arch = "wasm32")]
let proxy = Some(event_loop.create_proxy());
Self {
ren: None,
#[cfg(target_arch = "wasm32")]
proxy,
last_fps_update: None,
frame_count: 0,
last_frame_time: None,
frame_time: 0.0,
pointer_pos: [0, 0],
mouse_right_pressed: false,
}
}
}
impl ApplicationHandler<Ren> for Win {
// Called when the app starts or resumes
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
#[allow(unused_mut)]
let mut window_attributes = Window::default_attributes();
// WASM-specific: Attach to the canvas element in HTML
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowAttributesExtWebSys;
const CANVAS_ID: &str = "canvas";
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let canvas = document.get_element_by_id(CANVAS_ID).unwrap();
let html_canvas_element = canvas.unchecked_into();
window_attributes = window_attributes.with_canvas(Some(html_canvas_element));
}
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
// Native: Initialize renderer synchronously
#[cfg(not(target_arch = "wasm32"))]
{
self.ren = Some(pollster::block_on(Ren::new(window)).unwrap());
}
// WASM: Initialize renderer asynchronously via proxy
#[cfg(target_arch = "wasm32")]
{
if let Some(proxy) = self.proxy.take() {
let w_clone = window.clone();
wasm_bindgen_futures::spawn_local(async move {
let renderer = Ren::new(w_clone).await.expect("unable to create canvas");
proxy.send_event(renderer).expect("failed to send renderer");
});
}
}
}
// Called when the custom event (our Renderer) arrives (WASM only)
fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, mut _event: Ren) {
#[cfg(target_arch = "wasm32")]
{
_event.window.request_redraw();
let size = _event.window.inner_size();
_event.resize(size.width, size.height);
}
self.ren = Some(_event);
}
// Main event loop
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
let ren = match &mut self.ren {
Some(canvas) => canvas,
None => return,
};
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => ren.resize(size.width, size.height),
// The core render loop
WindowEvent::RedrawRequested => {
// FPS Calculation
self.frame_count += 1;
let now = Instant::now();
if let Some(last) = self.last_frame_time {
self.frame_time = now.duration_since(last).as_secs_f32();
}
self.last_frame_time = Some(now);
if self.last_fps_update.is_none() { self.last_fps_update = Some(now); }
if let Some(last_fps) = self.last_fps_update {
let elapsed = now.duration_since(last_fps).as_secs_f32();
if elapsed >= 1.0 {
// Update window title with FPS
let fps = self.frame_count as f32 / elapsed;
let title = format!("Unreal Majid - FPS: {:.2}", fps);
ren.window.set_title(&title);
self.frame_count = 0;
self.last_fps_update = Some(now);
}
}
ren.update(); // Update physics/uniforms
match ren.render(self.frame_time, self.pointer_pos) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
let size = ren.window.inner_size();
ren.resize(size.width, size.height);
}
Err(e) => log::error!("Render error: {}", e),
}
// Request next frame immediately
ren.window.request_redraw();
}
// Input Handling
WindowEvent::KeyboardInput {
event: KeyEvent { physical_key: PhysicalKey::Code(code), state, .. }, ..
} => ren.handle_key(event_loop, code, state.is_pressed()),
WindowEvent::CursorMoved { position, .. } => {
self.pointer_pos = [position.x as u32, position.y as u32];
}
WindowEvent::MouseInput { state, button, .. } => {
if button == MouseButton::Left {
self.mouse_right_pressed = state == ElementState::Pressed;
}
}
_ => {}
}
}
// Handle mouse motion for camera rotation
fn device_event(
&mut self,
_event_loop: &winit::event_loop::ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
if let Some(ren) = &mut self.ren {
if let DeviceEvent::MouseMotion { delta } = event {
// Only rotate camera if mouse is pressed
if self.mouse_right_pressed {
ren.handle_mouse_motion(delta.0, delta.1);
}
}
}
}
}
Code Breakdown
#[cfg(target_arch = "wasm32")]: These attributes conditionally compile code. For WASM, we need special logic to attach the window to an HTML<canvas>element.resumed: This is where the window is actually created. In Android/iOS/Web, windows can be destroyed and recreated, so we initialize here rather than innew().pollster::block_on:wgpuinitialization is async. On native, we block the thread to wait for it. On WASM, we can't block the main thread, so we usewasm_bindgen_futures::spawn_local.request_redraw(): Tells winit we want to render a new frame. Calling this at the end ofRedrawRequestedcreates an infinite render loop.