Skip to main content

pebble/wgpu/
window.rs

1use std::ops::Deref;
2use std::sync::{Arc, Mutex, MutexGuard};
3
4#[cfg(not(target_arch = "wasm32"))]
5use winit::dpi::PhysicalSize;
6use winit::{
7    event::{Event, WindowEvent},
8    event_loop::EventLoop,
9    window::{Window, WindowBuilder},
10};
11use winit_input_helper::WinitInputHelper;
12
13use crate::rendering::window::{PresentableWindow, WindowConfig, WindowProvider, WindowRunner};
14
15/// Shared handle to the frame's input state.
16///
17/// Cheap to clone (an `Arc` internally). Call [`Input::get`] to read it —
18/// the returned [`InputGuard`] derefs straight to [`WinitInputHelper`], so
19/// there's no `.lock().unwrap()` at every call site.
20#[derive(Clone)]
21pub struct Input(Arc<Mutex<WinitInputHelper>>);
22
23impl Input {
24    fn new() -> Self {
25        Self(Arc::new(Mutex::new(WinitInputHelper::new())))
26    }
27
28    /// Borrow the current input state. Panics if the lock is poisoned (a
29    /// prior holder panicked while holding it), matching how the rest of
30    /// this codebase treats poisoning as an unrecoverable bug.
31    pub fn get(&self) -> InputGuard<'_> {
32        InputGuard(self.0.lock().unwrap())
33    }
34
35    fn update(&self, event: &Event<()>) -> bool {
36        return self.0.lock().unwrap().update(event);
37    }
38}
39
40pub struct InputGuard<'a>(MutexGuard<'a, WinitInputHelper>);
41
42impl Deref for InputGuard<'_> {
43    type Target = WinitInputHelper;
44    fn deref(&self) -> &Self::Target {
45        &self.0
46    }
47}
48
49pub struct WinitWindow {
50    window: Arc<Window>,
51    event_loop: EventLoop<()>,
52    input: Input,
53}
54
55impl WindowProvider for WinitWindow {
56    type Handle = Arc<Window>;
57    type Exposed = Input;
58
59    fn create(config: &WindowConfig) -> Self {
60        let event_loop = EventLoop::new().unwrap();
61        event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
62
63        #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
64        let mut window_builder = WindowBuilder::new().with_title(config.title.clone());
65
66        #[cfg(not(target_arch = "wasm32"))]
67        {
68            window_builder =
69                window_builder.with_inner_size(PhysicalSize::new(config.width, config.height));
70        }
71
72        #[cfg(target_arch = "wasm32")]
73        let window = {
74            use wasm_bindgen::JsCast;
75            use winit::platform::web::WindowBuilderExtWebSys;
76
77            // Without this, a panic anywhere in the app — including the
78            // `.expect()`s a few lines below, which are exactly the ones
79            // most likely to fire on a real misconfiguration (no matching
80            // canvas element) — shows up in the browser console as an
81            // opaque, unhelpful trap instead of the actual message and a
82            // Rust-side stack trace. Idempotent, so it's safe to call even
83            // if something else already installed a hook first.
84            console_error_panic_hook::set_once();
85
86            let web_window = web_sys::window().expect("no global `window` exists");
87            let document = web_window
88                .document()
89                .expect("should have a document on window");
90            let canvas = document
91                .get_element_by_id("wgpu_canvas")
92                .expect("no element with id `wgpu_canvas` found — add <canvas id=\"wgpu_canvas\"></canvas> to index.html")
93                .unchecked_into::<web_sys::HtmlCanvasElement>();
94
95            let window = Arc::new(
96                window_builder
97                    .with_canvas(Some(canvas))
98                    .build(&event_loop)
99                    .unwrap(),
100            );
101
102            // winit doesn't track the browser viewport for a caller-supplied
103            // canvas, so the window (and canvas) would stay stuck at its
104            // initial size forever. Size it to the viewport now, then keep it
105            // in sync on every `resize` event.
106            let sync_size = {
107                let window = window.clone();
108                move || {
109                    let web_window = web_sys::window().expect("no global `window` exists");
110                    let width = web_window.inner_width().unwrap().as_f64().unwrap();
111                    let height = web_window.inner_height().unwrap().as_f64().unwrap();
112                    let _ = window.request_inner_size(winit::dpi::LogicalSize::new(width, height));
113                }
114            };
115            sync_size();
116
117            let closure =
118                wasm_bindgen::closure::Closure::<dyn FnMut()>::new(sync_size).into_js_value();
119            web_window
120                .add_event_listener_with_callback("resize", closure.unchecked_ref())
121                .expect("failed to add `resize` listener");
122
123            window
124        };
125
126        #[cfg(not(target_arch = "wasm32"))]
127        let window = Arc::new(window_builder.build(&event_loop).unwrap());
128
129        Self {
130            window,
131            event_loop,
132            input: Input::new(),
133        }
134    }
135
136    fn size(handle: &Self::Handle) -> (u32, u32) {
137        let s = handle.inner_size();
138        (s.width, s.height)
139    }
140
141    fn exposed(&self) -> Self::Exposed {
142        self.input.clone()
143    }
144
145    fn handle(&self) -> &Self::Handle {
146        &self.window
147    }
148}
149
150impl WindowRunner for WinitWindow {
151    fn run(self, mut on_frame: impl FnMut() + 'static) {
152        let Self {
153            window,
154            event_loop,
155            input,
156        } = self;
157
158        // On web, `ControlFlow::Poll` doesn't tie the loop to vsync — winit's
159        // web backend pumps `AboutToWait` (which `stepped` fires on) via an
160        // unthrottled task-scheduler loop, so driving frames off it runs the
161        // whole ECS tick + GPU submit hundreds of times a second, competing
162        // with the browser's compositor on the same thread. `RedrawRequested`
163        // is the one event winit paces via `requestAnimationFrame` on web, so
164        // drive frames from that instead and keep re-requesting it each time.
165        #[cfg(target_arch = "wasm32")]
166        window.request_redraw();
167
168        event_loop
169            .run(move |event, elwt| {
170                let stepped = input.update(&event);
171
172                match &event {
173                    Event::WindowEvent {
174                        event: WindowEvent::CloseRequested,
175                        ..
176                    } => elwt.exit(),
177                    #[cfg(target_arch = "wasm32")]
178                    Event::WindowEvent {
179                        event: WindowEvent::RedrawRequested,
180                        ..
181                    } => {
182                        on_frame();
183                        window.request_redraw();
184                    }
185                    _ => {}
186                }
187
188                #[cfg(not(target_arch = "wasm32"))]
189                if stepped {
190                    on_frame();
191                    window.request_redraw();
192                }
193                #[cfg(target_arch = "wasm32")]
194                let _ = stepped;
195            })
196            .unwrap();
197    }
198}
199
200impl PresentableWindow for WinitWindow {}