1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::sync::{Arc, Mutex};
4use std::time::Duration;
5
6#[cfg(not(target_arch = "wasm32"))]
7use winit::dpi::PhysicalSize;
8use winit::{
9 event::{Event, Touch as WinitTouch, TouchPhase as WinitTouchPhase, WindowEvent},
10 event_loop::EventLoop,
11 window::{Fullscreen, Window as OsWindow, WindowBuilder},
15};
16use winit_input_helper::WinitInputHelper;
17
18use crate::ecs::plugin::Plugin;
19use crate::rendering::window::{PresentableWindow, WindowConfig, WindowProvider, WindowResource, WindowRunner};
20use crate::wgpu::cursor::{CursorGrabMode, CursorIcon};
21use crate::wgpu::keycode::{KeyCode, MouseButton};
22
23#[derive(Copy, Clone, Debug, PartialEq, Eq)]
25pub enum TouchPhase {
26 Started,
27 Moved,
28 Ended,
29 Cancelled,
30}
31
32impl From<WinitTouchPhase> for TouchPhase {
33 fn from(value: WinitTouchPhase) -> Self {
34 match value {
35 WinitTouchPhase::Started => Self::Started,
36 WinitTouchPhase::Moved => Self::Moved,
37 WinitTouchPhase::Ended => Self::Ended,
38 WinitTouchPhase::Cancelled => Self::Cancelled,
39 }
40 }
41}
42
43#[derive(Copy, Clone, Debug)]
46pub struct TouchPoint {
47 pub id: u64,
48 pub position: (f32, f32),
49 pub phase: TouchPhase,
50}
51
52struct InputState {
53 helper: WinitInputHelper,
54 touches: HashMap<u64, TouchPoint>,
58}
59
60#[derive(Clone)]
71pub struct Input(Arc<Mutex<InputState>>);
72
73impl Input {
74 fn new() -> Self {
75 Self(Arc::new(Mutex::new(InputState { helper: WinitInputHelper::new(), touches: HashMap::new() })))
76 }
77
78 fn update(&self, event: &Event<()>) -> bool {
79 self.0.lock().unwrap().helper.update(event)
80 }
81
82 fn handle_touch(&self, touch: &WinitTouch) {
83 let mut state = self.0.lock().unwrap();
84 let point = TouchPoint {
85 id: touch.id,
86 position: (touch.location.x as f32, touch.location.y as f32),
87 phase: touch.phase.into(),
88 };
89 match touch.phase {
90 WinitTouchPhase::Started | WinitTouchPhase::Moved => {
91 state.touches.insert(touch.id, point);
92 }
93 WinitTouchPhase::Ended | WinitTouchPhase::Cancelled => {
94 state.touches.remove(&touch.id);
95 }
96 }
97 }
98
99 pub fn key_pressed(&self, key: KeyCode) -> bool {
103 self.0.lock().unwrap().helper.key_pressed(key.into())
104 }
105
106 pub fn key_released(&self, key: KeyCode) -> bool {
108 self.0.lock().unwrap().helper.key_released(key.into())
109 }
110
111 pub fn key_held(&self, key: KeyCode) -> bool {
113 self.0.lock().unwrap().helper.key_held(key.into())
114 }
115
116 pub fn held_shift(&self) -> bool {
118 self.0.lock().unwrap().helper.held_shift()
119 }
120
121 pub fn held_control(&self) -> bool {
123 self.0.lock().unwrap().helper.held_control()
124 }
125
126 pub fn held_alt(&self) -> bool {
128 self.0.lock().unwrap().helper.held_alt()
129 }
130
131 pub fn mouse_pressed(&self, button: MouseButton) -> bool {
133 self.0.lock().unwrap().helper.mouse_pressed(button.into())
134 }
135
136 pub fn mouse_released(&self, button: MouseButton) -> bool {
138 self.0.lock().unwrap().helper.mouse_released(button.into())
139 }
140
141 pub fn mouse_held(&self, button: MouseButton) -> bool {
143 self.0.lock().unwrap().helper.mouse_held(button.into())
144 }
145
146 pub fn cursor(&self) -> Option<(f32, f32)> {
149 self.0.lock().unwrap().helper.cursor()
150 }
151
152 pub fn cursor_diff(&self) -> (f32, f32) {
155 self.0.lock().unwrap().helper.cursor_diff()
156 }
157
158 pub fn mouse_diff(&self) -> (f32, f32) {
162 self.0.lock().unwrap().helper.mouse_diff()
163 }
164
165 pub fn scroll_diff(&self) -> (f32, f32) {
167 self.0.lock().unwrap().helper.scroll_diff()
168 }
169
170 pub fn close_requested(&self) -> bool {
173 self.0.lock().unwrap().helper.close_requested()
174 }
175
176 pub fn resolution(&self) -> Option<(u32, u32)> {
178 self.0.lock().unwrap().helper.resolution()
179 }
180
181 pub fn dropped_file(&self) -> Option<PathBuf> {
183 self.0.lock().unwrap().helper.dropped_file()
184 }
185
186 pub fn delta_time(&self) -> Option<Duration> {
189 self.0.lock().unwrap().helper.delta_time()
190 }
191
192 pub fn touches(&self) -> Vec<TouchPoint> {
196 self.0.lock().unwrap().touches.values().copied().collect()
197 }
198
199 pub fn touch_count(&self) -> usize {
201 self.0.lock().unwrap().touches.len()
202 }
203}
204
205#[derive(Clone)]
214pub struct Window(Arc<OsWindow>);
215
216impl Window {
217 fn new(handle: Arc<OsWindow>) -> Self {
218 Self(handle)
219 }
220
221 pub fn set_title(&self, title: &str) {
223 self.0.set_title(title);
224 }
225
226 pub fn inner_size(&self) -> (u32, u32) {
228 let size = self.0.inner_size();
229 (size.width, size.height)
230 }
231
232 pub fn set_inner_size(&self, width: u32, height: u32) {
236 let _ = self.0.request_inner_size(PhysicalSize::new(width, height));
237 }
238
239 pub fn set_min_inner_size(&self, size: Option<(u32, u32)>) {
241 self.0.set_min_inner_size(size.map(|(w, h)| PhysicalSize::new(w, h)));
242 }
243
244 pub fn set_max_inner_size(&self, size: Option<(u32, u32)>) {
246 self.0.set_max_inner_size(size.map(|(w, h)| PhysicalSize::new(w, h)));
247 }
248
249 pub fn set_resizable(&self, resizable: bool) {
251 self.0.set_resizable(resizable);
252 }
253
254 pub fn set_visible(&self, visible: bool) {
256 self.0.set_visible(visible);
257 }
258
259 pub fn set_minimized(&self, minimized: bool) {
261 self.0.set_minimized(minimized);
262 }
263
264 pub fn set_maximized(&self, maximized: bool) {
266 self.0.set_maximized(maximized);
267 }
268
269 pub fn set_decorations(&self, decorations: bool) {
271 self.0.set_decorations(decorations);
272 }
273
274 pub fn focus(&self) {
276 self.0.focus_window();
277 }
278
279 pub fn set_fullscreen(&self, fullscreen: bool) {
282 self.0.set_fullscreen(fullscreen.then_some(Fullscreen::Borderless(None)));
283 }
284
285 pub fn is_fullscreen(&self) -> bool {
287 self.0.fullscreen().is_some()
288 }
289
290 pub fn set_cursor_icon(&self, icon: CursorIcon) {
292 self.0.set_cursor_icon(icon.into());
293 }
294
295 pub fn set_cursor_visible(&self, visible: bool) {
297 self.0.set_cursor_visible(visible);
298 }
299
300 pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> bool {
305 self.0.set_cursor_grab(mode.into()).is_ok()
306 }
307
308 pub fn set_cursor_position(&self, x: f64, y: f64) -> bool {
312 self.0.set_cursor_position(winit::dpi::PhysicalPosition::new(x, y)).is_ok()
313 }
314
315 pub fn request_redraw(&self) {
319 self.0.request_redraw();
320 }
321}
322
323pub struct WindowControlPlugin;
329
330impl Plugin for WindowControlPlugin {
331 fn build(&self, app: &mut crate::prelude::App) {
332 let handle = app.get_resource::<WindowResource<WinitWindow>>().handle.clone();
333 app.add_resource(Window::new(handle));
334 }
335}
336
337pub struct WinitWindow {
338 window: Arc<OsWindow>,
339 event_loop: EventLoop<()>,
340 input: Input,
341}
342
343impl WindowProvider for WinitWindow {
344 type Handle = Arc<OsWindow>;
345 type Exposed = Input;
346
347 fn create(config: &WindowConfig) -> Self {
348 let event_loop = EventLoop::new().unwrap();
349 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
350
351 #[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
352 let mut window_builder = WindowBuilder::new().with_title(config.title.clone());
353
354 #[cfg(not(target_arch = "wasm32"))]
355 {
356 window_builder =
357 window_builder.with_inner_size(PhysicalSize::new(config.width, config.height));
358 }
359
360 #[cfg(target_arch = "wasm32")]
361 let window = {
362 use wasm_bindgen::JsCast;
363 use winit::platform::web::WindowBuilderExtWebSys;
364
365 console_error_panic_hook::set_once();
373
374 let web_window = web_sys::window().expect("no global `window` exists");
375 let document = web_window
376 .document()
377 .expect("should have a document on window");
378 let canvas = document
379 .get_element_by_id("wgpu_canvas")
380 .expect("no element with id `wgpu_canvas` found — add <canvas id=\"wgpu_canvas\"></canvas> to index.html")
381 .unchecked_into::<web_sys::HtmlCanvasElement>();
382
383 let window = Arc::new(
384 window_builder
385 .with_canvas(Some(canvas))
386 .build(&event_loop)
387 .unwrap(),
388 );
389
390 let sync_size = {
395 let window = window.clone();
396 move || {
397 let web_window = web_sys::window().expect("no global `window` exists");
398 let width = web_window.inner_width().unwrap().as_f64().unwrap();
399 let height = web_window.inner_height().unwrap().as_f64().unwrap();
400 let _ = window.request_inner_size(winit::dpi::LogicalSize::new(width, height));
401 }
402 };
403 sync_size();
404
405 let closure =
406 wasm_bindgen::closure::Closure::<dyn FnMut()>::new(sync_size).into_js_value();
407 web_window
408 .add_event_listener_with_callback("resize", closure.unchecked_ref())
409 .expect("failed to add `resize` listener");
410
411 window
412 };
413
414 #[cfg(not(target_arch = "wasm32"))]
415 let window = Arc::new(window_builder.build(&event_loop).unwrap());
416
417 Self {
418 window,
419 event_loop,
420 input: Input::new(),
421 }
422 }
423
424 fn size(handle: &Self::Handle) -> (u32, u32) {
425 let s = handle.inner_size();
426 (s.width, s.height)
427 }
428
429 fn exposed(&self) -> Self::Exposed {
430 self.input.clone()
431 }
432
433 fn handle(&self) -> &Self::Handle {
434 &self.window
435 }
436}
437
438impl WindowRunner for WinitWindow {
439 fn run(self, mut on_frame: impl FnMut() + 'static) {
440 let Self {
441 window,
442 event_loop,
443 input,
444 } = self;
445
446 #[cfg(target_arch = "wasm32")]
454 window.request_redraw();
455
456 event_loop
457 .run(move |event, elwt| {
458 let stepped = input.update(&event);
459
460 match &event {
461 Event::WindowEvent {
462 event: WindowEvent::CloseRequested,
463 ..
464 } => elwt.exit(),
465 Event::WindowEvent {
466 event: WindowEvent::Touch(touch),
467 ..
468 } => input.handle_touch(touch),
469 #[cfg(target_arch = "wasm32")]
470 Event::WindowEvent {
471 event: WindowEvent::RedrawRequested,
472 ..
473 } => {
474 on_frame();
475 window.request_redraw();
476 }
477 _ => {}
478 }
479
480 #[cfg(not(target_arch = "wasm32"))]
481 if stepped {
482 on_frame();
483 window.request_redraw();
484 }
485 #[cfg(target_arch = "wasm32")]
486 let _ = stepped;
487 })
488 .unwrap();
489 }
490}
491
492impl PresentableWindow for WinitWindow {}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 fn touch(id: u64, phase: WinitTouchPhase, x: f64, y: f64) -> WinitTouch {
503 WinitTouch {
504 device_id: unsafe { winit::event::DeviceId::dummy() },
505 phase,
506 location: winit::dpi::PhysicalPosition::new(x, y),
507 force: None,
508 id,
509 }
510 }
511
512 #[test]
513 fn a_started_touch_appears_in_touches() {
514 let input = Input::new();
515 input.handle_touch(&touch(1, WinitTouchPhase::Started, 10.0, 20.0));
516
517 assert_eq!(input.touch_count(), 1);
518 let points = input.touches();
519 assert_eq!(points[0].id, 1);
520 assert_eq!(points[0].position, (10.0, 20.0));
521 assert_eq!(points[0].phase, TouchPhase::Started);
522 }
523
524 #[test]
525 fn a_moved_touch_updates_the_same_id_in_place() {
526 let input = Input::new();
527 input.handle_touch(&touch(1, WinitTouchPhase::Started, 0.0, 0.0));
528 input.handle_touch(&touch(1, WinitTouchPhase::Moved, 5.0, 5.0));
529
530 assert_eq!(input.touch_count(), 1, "a moved touch must not create a second point");
531 assert_eq!(input.touches()[0].position, (5.0, 5.0));
532 }
533
534 #[test]
535 fn an_ended_touch_is_removed() {
536 let input = Input::new();
537 input.handle_touch(&touch(1, WinitTouchPhase::Started, 0.0, 0.0));
538 input.handle_touch(&touch(1, WinitTouchPhase::Ended, 0.0, 0.0));
539
540 assert_eq!(input.touch_count(), 0);
541 }
542
543 #[test]
544 fn a_cancelled_touch_is_removed() {
545 let input = Input::new();
546 input.handle_touch(&touch(1, WinitTouchPhase::Started, 0.0, 0.0));
547 input.handle_touch(&touch(1, WinitTouchPhase::Cancelled, 0.0, 0.0));
548
549 assert_eq!(input.touch_count(), 0);
550 }
551
552 #[test]
553 fn multiple_simultaneous_touches_are_tracked_independently() {
554 let input = Input::new();
555 input.handle_touch(&touch(1, WinitTouchPhase::Started, 0.0, 0.0));
556 input.handle_touch(&touch(2, WinitTouchPhase::Started, 100.0, 100.0));
557
558 assert_eq!(input.touch_count(), 2);
559 input.handle_touch(&touch(1, WinitTouchPhase::Ended, 0.0, 0.0));
560 assert_eq!(input.touch_count(), 1);
561 assert_eq!(input.touches()[0].id, 2);
562 }
563}