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    // Aliased: this file also defines Pebble's own opaque `Window` wrapper
11    // around it, and having both named `Window` in the same file would be
12    // ambiguous.
13    window::{Fullscreen, Window as OsWindow, WindowBuilder},
14};
15use winit_input_helper::WinitInputHelper;
16
17use crate::ecs::plugin::Plugin;
18use crate::rendering::window::{PresentableWindow, WindowConfig, WindowProvider, WindowResource, WindowRunner};
19use crate::wgpu::cursor::{CursorGrabMode, CursorIcon};
20use crate::wgpu::keycode::{KeyCode, MouseButton};
21
22/// The frame's keyboard/mouse/window input state.
23///
24/// A self-contained ECS resource — fetch it directly with `Res<Input>`,
25/// no need to go through `WindowResource<W>` or name a concrete backend
26/// type. Cheap to clone (an `Arc` internally), and every accessor locks
27/// internally and hands back a plain value, so there's no guard type to
28/// hold onto: `input.key_held(KeyCode::KeyW)` just returns `bool`.
29///
30/// State is refreshed once per step, before systems run, so every accessor
31/// below reflects that step's input.
32#[derive(Clone)]
33pub struct Input(Arc<Mutex<WinitInputHelper>>);
34
35impl Input {
36    fn new() -> Self {
37        Self(Arc::new(Mutex::new(WinitInputHelper::new())))
38    }
39
40    fn update(&self, event: &Event<()>) -> bool {
41        self.0.lock().unwrap().update(event)
42    }
43
44    /// True the step a key goes from "not pressed" to "pressed". Uses
45    /// physical keys (layout-independent), so this is the one to reach for
46    /// game controls rather than text entry.
47    pub fn key_pressed(&self, key: KeyCode) -> bool {
48        self.0.lock().unwrap().key_pressed(key.into())
49    }
50
51    /// True the step a key goes from "pressed" to "not pressed".
52    pub fn key_released(&self, key: KeyCode) -> bool {
53        self.0.lock().unwrap().key_released(key.into())
54    }
55
56    /// True for every step the key remains pressed.
57    pub fn key_held(&self, key: KeyCode) -> bool {
58        self.0.lock().unwrap().key_held(key.into())
59    }
60
61    /// True while either shift key is held.
62    pub fn held_shift(&self) -> bool {
63        self.0.lock().unwrap().held_shift()
64    }
65
66    /// True while either control key is held.
67    pub fn held_control(&self) -> bool {
68        self.0.lock().unwrap().held_control()
69    }
70
71    /// True while either alt key is held.
72    pub fn held_alt(&self) -> bool {
73        self.0.lock().unwrap().held_alt()
74    }
75
76    /// True the step a mouse button goes from "not pressed" to "pressed".
77    pub fn mouse_pressed(&self, button: MouseButton) -> bool {
78        self.0.lock().unwrap().mouse_pressed(button.into())
79    }
80
81    /// True the step a mouse button goes from "pressed" to "not pressed".
82    pub fn mouse_released(&self, button: MouseButton) -> bool {
83        self.0.lock().unwrap().mouse_released(button.into())
84    }
85
86    /// True for every step the mouse button remains pressed.
87    pub fn mouse_held(&self, button: MouseButton) -> bool {
88        self.0.lock().unwrap().mouse_held(button.into())
89    }
90
91    /// Cursor position in pixels, or `None` if the window isn't focused (or
92    /// the cursor is off-window and no button is held).
93    pub fn cursor(&self) -> Option<(f32, f32)> {
94        self.0.lock().unwrap().cursor()
95    }
96
97    /// Change in cursor position since the last step. `(0.0, 0.0)` under the
98    /// same conditions [`Input::cursor`] returns `None`.
99    pub fn cursor_diff(&self) -> (f32, f32) {
100        self.0.lock().unwrap().cursor_diff()
101    }
102
103    /// Change in raw mouse motion since the last step — driven by device
104    /// events rather than cursor position, so this is the one to reach for
105    /// a captured-mouse first-person camera.
106    pub fn mouse_diff(&self) -> (f32, f32) {
107        self.0.lock().unwrap().mouse_diff()
108    }
109
110    /// Scroll wheel delta `(horizontal, vertical)` since the last step.
111    pub fn scroll_diff(&self) -> (f32, f32) {
112        self.0.lock().unwrap().scroll_diff()
113    }
114
115    /// True if the OS requested the window close this step (e.g. the title
116    /// bar's close button).
117    pub fn close_requested(&self) -> bool {
118        self.0.lock().unwrap().close_requested()
119    }
120
121    /// Current window resolution, or `None` before the first resize event.
122    pub fn resolution(&self) -> Option<(u32, u32)> {
123        self.0.lock().unwrap().resolution()
124    }
125
126    /// Path of a file dropped onto the window this step, if any.
127    pub fn dropped_file(&self) -> Option<PathBuf> {
128        self.0.lock().unwrap().dropped_file()
129    }
130
131    /// Time elapsed since the last step, or `None` while the first step is
132    /// still in progress.
133    pub fn delta_time(&self) -> Option<Duration> {
134        self.0.lock().unwrap().delta_time()
135    }
136}
137
138/// Runtime control over the OS window — cursor, title, size, fullscreen,
139/// and the like.
140///
141/// A self-contained ECS resource — `Res<Window>`, same as [`Input`] — not
142/// `WindowResource<WinitWindow>::handle`, which is `Arc<winit::window::Window>`
143/// and every raw `winit` method that comes with it. Cheap to clone (an
144/// `Arc` internally); every method forwards straight to the OS window, no
145/// locking needed since none of this is polled state like [`Input`] is.
146#[derive(Clone)]
147pub struct Window(Arc<OsWindow>);
148
149impl Window {
150    fn new(handle: Arc<OsWindow>) -> Self {
151        Self(handle)
152    }
153
154    /// Set the title shown in the window's title bar.
155    pub fn set_title(&self, title: &str) {
156        self.0.set_title(title);
157    }
158
159    /// The window's current inner size, in physical pixels.
160    pub fn inner_size(&self) -> (u32, u32) {
161        let size = self.0.inner_size();
162        (size.width, size.height)
163    }
164
165    /// Request a new inner size. The OS may not grant it exactly (or at
166    /// all, e.g. a maximized/tiled window) — check [`Window::inner_size`]
167    /// afterward for whatever size actually resulted.
168    pub fn set_inner_size(&self, width: u32, height: u32) {
169        let _ = self.0.request_inner_size(PhysicalSize::new(width, height));
170    }
171
172    /// Lower bound on manual/OS resizing. `None` clears it.
173    pub fn set_min_inner_size(&self, size: Option<(u32, u32)>) {
174        self.0.set_min_inner_size(size.map(|(w, h)| PhysicalSize::new(w, h)));
175    }
176
177    /// Upper bound on manual/OS resizing. `None` clears it.
178    pub fn set_max_inner_size(&self, size: Option<(u32, u32)>) {
179        self.0.set_max_inner_size(size.map(|(w, h)| PhysicalSize::new(w, h)));
180    }
181
182    /// Whether the user can resize the window by dragging its edges.
183    pub fn set_resizable(&self, resizable: bool) {
184        self.0.set_resizable(resizable);
185    }
186
187    /// Show or hide the window entirely.
188    pub fn set_visible(&self, visible: bool) {
189        self.0.set_visible(visible);
190    }
191
192    /// Minimize or restore the window.
193    pub fn set_minimized(&self, minimized: bool) {
194        self.0.set_minimized(minimized);
195    }
196
197    /// Maximize or restore the window.
198    pub fn set_maximized(&self, maximized: bool) {
199        self.0.set_maximized(maximized);
200    }
201
202    /// Show or hide the title bar/border.
203    pub fn set_decorations(&self, decorations: bool) {
204        self.0.set_decorations(decorations);
205    }
206
207    /// Request OS input focus.
208    pub fn focus(&self) {
209        self.0.focus_window();
210    }
211
212    /// Toggle borderless fullscreen on the window's current monitor, or
213    /// return to windowed mode.
214    pub fn set_fullscreen(&self, fullscreen: bool) {
215        self.0.set_fullscreen(fullscreen.then_some(Fullscreen::Borderless(None)));
216    }
217
218    /// Whether the window is currently fullscreen.
219    pub fn is_fullscreen(&self) -> bool {
220        self.0.fullscreen().is_some()
221    }
222
223    /// Change the mouse cursor's icon.
224    pub fn set_cursor_icon(&self, icon: CursorIcon) {
225        self.0.set_cursor_icon(icon.into());
226    }
227
228    /// Show or hide the mouse cursor while it's over the window.
229    pub fn set_cursor_visible(&self, visible: bool) {
230        self.0.set_cursor_visible(visible);
231    }
232
233    /// Confine or lock the cursor (see [`CursorGrabMode`]) — the usual pair
234    /// with `set_cursor_visible(false)` for a captured-mouse camera.
235    /// Returns `false` instead of panicking if the platform doesn't support
236    /// the requested mode (see `CursorGrabMode`'s variant docs).
237    pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> bool {
238        self.0.set_cursor_grab(mode.into()).is_ok()
239    }
240
241    /// Move the cursor to a position within the window, in physical pixels.
242    /// Returns `false` instead of panicking if the platform doesn't support
243    /// it.
244    pub fn set_cursor_position(&self, x: f64, y: f64) -> bool {
245        self.0.set_cursor_position(winit::dpi::PhysicalPosition::new(x, y)).is_ok()
246    }
247
248    /// Request that the window be redrawn on the next frame — rarely needed
249    /// directly (the render loop already drives this), but available for a
250    /// backend/window setup that needs to force a redraw out of band.
251    pub fn request_redraw(&self) {
252        self.0.request_redraw();
253    }
254}
255
256/// Inserts [`Window`] as a resource, wrapping the same handle already in
257/// `WindowResource<WinitWindow>`. `WGPUPlugin` adds this automatically,
258/// right after `WindowPlugin<WinitWindow>` — add it yourself only if you're
259/// composing `WindowPlugin<WinitWindow>` without going through `WGPUPlugin`
260/// (see the book's "Owning the graphics backend yourself").
261pub struct WindowControlPlugin;
262
263impl Plugin for WindowControlPlugin {
264    fn build(&self, app: &mut crate::prelude::App) {
265        let handle = app.get_resource::<WindowResource<WinitWindow>>().handle.clone();
266        app.add_resource(Window::new(handle));
267    }
268}
269
270pub struct WinitWindow {
271    window: Arc<OsWindow>,
272    event_loop: EventLoop<()>,
273    input: Input,
274}
275
276impl WindowProvider for WinitWindow {
277    type Handle = Arc<OsWindow>;
278    type Exposed = Input;
279
280    fn create(config: &WindowConfig) -> Self {
281        let event_loop = EventLoop::new().unwrap();
282        event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
283
284        #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
285        let mut window_builder = WindowBuilder::new().with_title(config.title.clone());
286
287        #[cfg(not(target_arch = "wasm32"))]
288        {
289            window_builder =
290                window_builder.with_inner_size(PhysicalSize::new(config.width, config.height));
291        }
292
293        #[cfg(target_arch = "wasm32")]
294        let window = {
295            use wasm_bindgen::JsCast;
296            use winit::platform::web::WindowBuilderExtWebSys;
297
298            // Without this, a panic anywhere in the app — including the
299            // `.expect()`s a few lines below, which are exactly the ones
300            // most likely to fire on a real misconfiguration (no matching
301            // canvas element) — shows up in the browser console as an
302            // opaque, unhelpful trap instead of the actual message and a
303            // Rust-side stack trace. Idempotent, so it's safe to call even
304            // if something else already installed a hook first.
305            console_error_panic_hook::set_once();
306
307            let web_window = web_sys::window().expect("no global `window` exists");
308            let document = web_window
309                .document()
310                .expect("should have a document on window");
311            let canvas = document
312                .get_element_by_id("wgpu_canvas")
313                .expect("no element with id `wgpu_canvas` found — add <canvas id=\"wgpu_canvas\"></canvas> to index.html")
314                .unchecked_into::<web_sys::HtmlCanvasElement>();
315
316            let window = Arc::new(
317                window_builder
318                    .with_canvas(Some(canvas))
319                    .build(&event_loop)
320                    .unwrap(),
321            );
322
323            // winit doesn't track the browser viewport for a caller-supplied
324            // canvas, so the window (and canvas) would stay stuck at its
325            // initial size forever. Size it to the viewport now, then keep it
326            // in sync on every `resize` event.
327            let sync_size = {
328                let window = window.clone();
329                move || {
330                    let web_window = web_sys::window().expect("no global `window` exists");
331                    let width = web_window.inner_width().unwrap().as_f64().unwrap();
332                    let height = web_window.inner_height().unwrap().as_f64().unwrap();
333                    let _ = window.request_inner_size(winit::dpi::LogicalSize::new(width, height));
334                }
335            };
336            sync_size();
337
338            let closure =
339                wasm_bindgen::closure::Closure::<dyn FnMut()>::new(sync_size).into_js_value();
340            web_window
341                .add_event_listener_with_callback("resize", closure.unchecked_ref())
342                .expect("failed to add `resize` listener");
343
344            window
345        };
346
347        #[cfg(not(target_arch = "wasm32"))]
348        let window = Arc::new(window_builder.build(&event_loop).unwrap());
349
350        Self {
351            window,
352            event_loop,
353            input: Input::new(),
354        }
355    }
356
357    fn size(handle: &Self::Handle) -> (u32, u32) {
358        let s = handle.inner_size();
359        (s.width, s.height)
360    }
361
362    fn exposed(&self) -> Self::Exposed {
363        self.input.clone()
364    }
365
366    fn handle(&self) -> &Self::Handle {
367        &self.window
368    }
369}
370
371impl WindowRunner for WinitWindow {
372    fn run(self, mut on_frame: impl FnMut() + 'static) {
373        let Self {
374            window,
375            event_loop,
376            input,
377        } = self;
378
379        // On web, `ControlFlow::Poll` doesn't tie the loop to vsync — winit's
380        // web backend pumps `AboutToWait` (which `stepped` fires on) via an
381        // unthrottled task-scheduler loop, so driving frames off it runs the
382        // whole ECS tick + GPU submit hundreds of times a second, competing
383        // with the browser's compositor on the same thread. `RedrawRequested`
384        // is the one event winit paces via `requestAnimationFrame` on web, so
385        // drive frames from that instead and keep re-requesting it each time.
386        #[cfg(target_arch = "wasm32")]
387        window.request_redraw();
388
389        event_loop
390            .run(move |event, elwt| {
391                let stepped = input.update(&event);
392
393                match &event {
394                    Event::WindowEvent {
395                        event: WindowEvent::CloseRequested,
396                        ..
397                    } => elwt.exit(),
398                    #[cfg(target_arch = "wasm32")]
399                    Event::WindowEvent {
400                        event: WindowEvent::RedrawRequested,
401                        ..
402                    } => {
403                        on_frame();
404                        window.request_redraw();
405                    }
406                    _ => {}
407                }
408
409                #[cfg(not(target_arch = "wasm32"))]
410                if stepped {
411                    on_frame();
412                    window.request_redraw();
413                }
414                #[cfg(target_arch = "wasm32")]
415                let _ = stepped;
416            })
417            .unwrap();
418    }
419}
420
421impl PresentableWindow for WinitWindow {}