Skip to main content

pebble/wgpu/
window.rs

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