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:

  1. Define the Win struct which holds our application state.
  2. Implement the ApplicationHandler trait to respond to OS events.
  3. 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 in new().
  • pollster::block_on: wgpu initialization is async. On native, we block the thread to wait for it. On WASM, we can't block the main thread, so we use wasm_bindgen_futures::spawn_local.
  • request_redraw(): Tells winit we want to render a new frame. Calling this at the end of RedrawRequested creates an infinite render loop.