Skip to main content

pebble/wgpu/
window.rs

1use std::path::PathBuf;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5#[cfg(not(target_arch = "wasm32"))]
6use winit::dpi::PhysicalSize;
7use winit::{
8    event::{Event, WindowEvent},
9    event_loop::EventLoop,
10    window::{Window, WindowBuilder},
11};
12use winit_input_helper::WinitInputHelper;
13
14use crate::rendering::window::{PresentableWindow, WindowConfig, WindowProvider, WindowRunner};
15use crate::wgpu::keycode::{KeyCode, MouseButton};
16
17/// The frame's keyboard/mouse/window input state.
18///
19/// A self-contained ECS resource — fetch it directly with `Res<Input>`,
20/// no need to go through `WindowResource<W>` or name a concrete backend
21/// type. Cheap to clone (an `Arc` internally), and every accessor locks
22/// internally and hands back a plain value, so there's no guard type to
23/// hold onto: `input.key_held(KeyCode::KeyW)` just returns `bool`.
24///
25/// State is refreshed once per step, before systems run, so every accessor
26/// below reflects that step's input.
27#[derive(Clone)]
28pub struct Input(Arc<Mutex<WinitInputHelper>>);
29
30impl Input {
31    fn new() -> Self {
32        Self(Arc::new(Mutex::new(WinitInputHelper::new())))
33    }
34
35    fn update(&self, event: &Event<()>) -> bool {
36        self.0.lock().unwrap().update(event)
37    }
38
39    /// True the step a key goes from "not pressed" to "pressed". Uses
40    /// physical keys (layout-independent), so this is the one to reach for
41    /// game controls rather than text entry.
42    pub fn key_pressed(&self, key: KeyCode) -> bool {
43        self.0.lock().unwrap().key_pressed(key.into())
44    }
45
46    /// True the step a key goes from "pressed" to "not pressed".
47    pub fn key_released(&self, key: KeyCode) -> bool {
48        self.0.lock().unwrap().key_released(key.into())
49    }
50
51    /// True for every step the key remains pressed.
52    pub fn key_held(&self, key: KeyCode) -> bool {
53        self.0.lock().unwrap().key_held(key.into())
54    }
55
56    /// True while either shift key is held.
57    pub fn held_shift(&self) -> bool {
58        self.0.lock().unwrap().held_shift()
59    }
60
61    /// True while either control key is held.
62    pub fn held_control(&self) -> bool {
63        self.0.lock().unwrap().held_control()
64    }
65
66    /// True while either alt key is held.
67    pub fn held_alt(&self) -> bool {
68        self.0.lock().unwrap().held_alt()
69    }
70
71    /// True the step a mouse button goes from "not pressed" to "pressed".
72    pub fn mouse_pressed(&self, button: MouseButton) -> bool {
73        self.0.lock().unwrap().mouse_pressed(button.into())
74    }
75
76    /// True the step a mouse button goes from "pressed" to "not pressed".
77    pub fn mouse_released(&self, button: MouseButton) -> bool {
78        self.0.lock().unwrap().mouse_released(button.into())
79    }
80
81    /// True for every step the mouse button remains pressed.
82    pub fn mouse_held(&self, button: MouseButton) -> bool {
83        self.0.lock().unwrap().mouse_held(button.into())
84    }
85
86    /// Cursor position in pixels, or `None` if the window isn't focused (or
87    /// the cursor is off-window and no button is held).
88    pub fn cursor(&self) -> Option<(f32, f32)> {
89        self.0.lock().unwrap().cursor()
90    }
91
92    /// Change in cursor position since the last step. `(0.0, 0.0)` under the
93    /// same conditions [`Input::cursor`] returns `None`.
94    pub fn cursor_diff(&self) -> (f32, f32) {
95        self.0.lock().unwrap().cursor_diff()
96    }
97
98    /// Change in raw mouse motion since the last step — driven by device
99    /// events rather than cursor position, so this is the one to reach for
100    /// a captured-mouse first-person camera.
101    pub fn mouse_diff(&self) -> (f32, f32) {
102        self.0.lock().unwrap().mouse_diff()
103    }
104
105    /// Scroll wheel delta `(horizontal, vertical)` since the last step.
106    pub fn scroll_diff(&self) -> (f32, f32) {
107        self.0.lock().unwrap().scroll_diff()
108    }
109
110    /// True if the OS requested the window close this step (e.g. the title
111    /// bar's close button).
112    pub fn close_requested(&self) -> bool {
113        self.0.lock().unwrap().close_requested()
114    }
115
116    /// Current window resolution, or `None` before the first resize event.
117    pub fn resolution(&self) -> Option<(u32, u32)> {
118        self.0.lock().unwrap().resolution()
119    }
120
121    /// Path of a file dropped onto the window this step, if any.
122    pub fn dropped_file(&self) -> Option<PathBuf> {
123        self.0.lock().unwrap().dropped_file()
124    }
125
126    /// Time elapsed since the last step, or `None` while the first step is
127    /// still in progress.
128    pub fn delta_time(&self) -> Option<Duration> {
129        self.0.lock().unwrap().delta_time()
130    }
131}
132
133pub struct WinitWindow {
134    window: Arc<Window>,
135    event_loop: EventLoop<()>,
136    input: Input,
137}
138
139impl WindowProvider for WinitWindow {
140    type Handle = Arc<Window>;
141    type Exposed = Input;
142
143    fn create(config: &WindowConfig) -> Self {
144        let event_loop = EventLoop::new().unwrap();
145        event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
146
147        #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
148        let mut window_builder = WindowBuilder::new().with_title(config.title.clone());
149
150        #[cfg(not(target_arch = "wasm32"))]
151        {
152            window_builder =
153                window_builder.with_inner_size(PhysicalSize::new(config.width, config.height));
154        }
155
156        #[cfg(target_arch = "wasm32")]
157        let window = {
158            use wasm_bindgen::JsCast;
159            use winit::platform::web::WindowBuilderExtWebSys;
160
161            // Without this, a panic anywhere in the app — including the
162            // `.expect()`s a few lines below, which are exactly the ones
163            // most likely to fire on a real misconfiguration (no matching
164            // canvas element) — shows up in the browser console as an
165            // opaque, unhelpful trap instead of the actual message and a
166            // Rust-side stack trace. Idempotent, so it's safe to call even
167            // if something else already installed a hook first.
168            console_error_panic_hook::set_once();
169
170            let web_window = web_sys::window().expect("no global `window` exists");
171            let document = web_window
172                .document()
173                .expect("should have a document on window");
174            let canvas = document
175                .get_element_by_id("wgpu_canvas")
176                .expect("no element with id `wgpu_canvas` found — add <canvas id=\"wgpu_canvas\"></canvas> to index.html")
177                .unchecked_into::<web_sys::HtmlCanvasElement>();
178
179            let window = Arc::new(
180                window_builder
181                    .with_canvas(Some(canvas))
182                    .build(&event_loop)
183                    .unwrap(),
184            );
185
186            // winit doesn't track the browser viewport for a caller-supplied
187            // canvas, so the window (and canvas) would stay stuck at its
188            // initial size forever. Size it to the viewport now, then keep it
189            // in sync on every `resize` event.
190            let sync_size = {
191                let window = window.clone();
192                move || {
193                    let web_window = web_sys::window().expect("no global `window` exists");
194                    let width = web_window.inner_width().unwrap().as_f64().unwrap();
195                    let height = web_window.inner_height().unwrap().as_f64().unwrap();
196                    let _ = window.request_inner_size(winit::dpi::LogicalSize::new(width, height));
197                }
198            };
199            sync_size();
200
201            let closure =
202                wasm_bindgen::closure::Closure::<dyn FnMut()>::new(sync_size).into_js_value();
203            web_window
204                .add_event_listener_with_callback("resize", closure.unchecked_ref())
205                .expect("failed to add `resize` listener");
206
207            window
208        };
209
210        #[cfg(not(target_arch = "wasm32"))]
211        let window = Arc::new(window_builder.build(&event_loop).unwrap());
212
213        Self {
214            window,
215            event_loop,
216            input: Input::new(),
217        }
218    }
219
220    fn size(handle: &Self::Handle) -> (u32, u32) {
221        let s = handle.inner_size();
222        (s.width, s.height)
223    }
224
225    fn exposed(&self) -> Self::Exposed {
226        self.input.clone()
227    }
228
229    fn handle(&self) -> &Self::Handle {
230        &self.window
231    }
232}
233
234impl WindowRunner for WinitWindow {
235    fn run(self, mut on_frame: impl FnMut() + 'static) {
236        let Self {
237            window,
238            event_loop,
239            input,
240        } = self;
241
242        // On web, `ControlFlow::Poll` doesn't tie the loop to vsync — winit's
243        // web backend pumps `AboutToWait` (which `stepped` fires on) via an
244        // unthrottled task-scheduler loop, so driving frames off it runs the
245        // whole ECS tick + GPU submit hundreds of times a second, competing
246        // with the browser's compositor on the same thread. `RedrawRequested`
247        // is the one event winit paces via `requestAnimationFrame` on web, so
248        // drive frames from that instead and keep re-requesting it each time.
249        #[cfg(target_arch = "wasm32")]
250        window.request_redraw();
251
252        event_loop
253            .run(move |event, elwt| {
254                let stepped = input.update(&event);
255
256                match &event {
257                    Event::WindowEvent {
258                        event: WindowEvent::CloseRequested,
259                        ..
260                    } => elwt.exit(),
261                    #[cfg(target_arch = "wasm32")]
262                    Event::WindowEvent {
263                        event: WindowEvent::RedrawRequested,
264                        ..
265                    } => {
266                        on_frame();
267                        window.request_redraw();
268                    }
269                    _ => {}
270                }
271
272                #[cfg(not(target_arch = "wasm32"))]
273                if stepped {
274                    on_frame();
275                    window.request_redraw();
276                }
277                #[cfg(target_arch = "wasm32")]
278                let _ = stepped;
279            })
280            .unwrap();
281    }
282}
283
284impl PresentableWindow for WinitWindow {}