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