euv_engine/input/impl.rs
1use super::*;
2
3/// Implements static event extraction methods on the `Input` namespace struct.
4impl Input {
5 /// Extracts the key code string from a keyboard event.
6 ///
7 /// # Arguments
8 ///
9 /// - `&Event` - The keyboard event.
10 ///
11 /// # Returns
12 ///
13 /// - `String` - The key code string (e.g., `"KeyA"`, `"Space"`, `"ArrowLeft"`).
14 pub fn extract_key_code(event: &Event) -> String {
15 // OPT 39: typed web-sys getter avoids the JS Reflect::get crossing.
16 event.unchecked_ref::<KeyboardEvent>().code()
17 }
18
19 /// Extracts the mouse button enum from a mouse event.
20 ///
21 /// # Arguments
22 ///
23 /// - `&Event` - The mouse event.
24 ///
25 /// # Returns
26 ///
27 /// - `MouseButton` - The mouse button that was pressed or released.
28 pub fn extract_mouse_button(event: &Event) -> MouseButton {
29 // OPT 39: typed web-sys `MouseEvent::button` getter instead of
30 // Reflect::get + as_f64 cast.
31 let button_value: i16 = event.unchecked_ref::<MouseEvent>().button();
32 match button_value {
33 0 => MouseButton::Left,
34 1 => MouseButton::Middle,
35 2 => MouseButton::Right,
36 3 => MouseButton::Button4,
37 4 => MouseButton::Button5,
38 _ => MouseButton::Left,
39 }
40 }
41
42 /// Extracts the client (viewport) coordinates from a mouse event.
43 ///
44 /// # Arguments
45 ///
46 /// - `&Event` - The mouse event.
47 ///
48 /// # Returns
49 ///
50 /// - `Vector2D` - The `(x, y)` client coordinates.
51 pub fn extract_mouse_position(event: &Event) -> Vector2D {
52 // OPT 39: typed web-sys `MouseEvent::client_x` / `client_y` getters
53 // instead of two Reflect::get + as_f64 casts per mouse event.
54 let mouse_event: &MouseEvent = event.unchecked_ref::<MouseEvent>();
55 let client_x: f64 = f64::from(mouse_event.client_x());
56 let client_y: f64 = f64::from(mouse_event.client_y());
57 Vector2D::new(client_x, client_y)
58 }
59}
60
61/// Implements DOM event listener registration on the `Input` namespace struct.
62///
63/// This is the wiring layer that routes DOM events into [`InputState`]:
64/// keyboard events bind to `window` (a `<canvas>` is not focusable by
65/// default), while mouse and touch events bind to the canvas element so
66/// hit-testing coordinates stay canvas-local. Registered closures are
67/// `.forget()`-ed and stay alive for the lifetime of the document,
68/// matching the engine's mount-only convention.
69impl Input {
70 /// Attaches all input listeners and returns the shared state cell.
71 ///
72 /// # Arguments
73 ///
74 /// - `InputStateCell` - The shared input state to mutate from event handlers.
75 /// - `&Window` - The global window, receiving keyboard events.
76 /// - `&EventTarget` - The pointer target (typically the canvas element).
77 ///
78 /// # Returns
79 ///
80 /// - `InputStateCell` - The same cell passed in, for convenient chaining.
81 pub fn attach(
82 state_cell: InputStateCell,
83 window: &Window,
84 pointer_target: &EventTarget,
85 ) -> InputStateCell {
86 Self::attach_keyboard(&state_cell, window);
87 Self::attach_pointer(&state_cell, pointer_target);
88 state_cell
89 }
90
91 /// Binds `keydown` / `keyup` listeners to `window`.
92 ///
93 /// Keyboard events must bind to `window` rather than the canvas: a
94 /// `<canvas>` element is not focusable unless `tabindex` is set and the
95 /// user clicks it, so canvas-bound key listeners would never fire.
96 ///
97 /// # Arguments
98 ///
99 /// - `&InputStateCell` - The shared input state.
100 /// - `&Window` - The global window.
101 pub fn attach_keyboard(state_cell: &InputStateCell, window: &Window) {
102 let state_keydown: InputStateCell = state_cell.clone();
103 let keydown_closure: Closure<dyn FnMut(Event)> =
104 Closure::wrap(Box::new(move |event: Event| {
105 let code: String = Input::extract_key_code(&event);
106 if code.is_empty() {
107 return;
108 }
109 let state: &mut InputState = state_keydown.get_mut();
110 state.press_key(code);
111 }));
112 Self::register_listener(window, INPUT_EVENT_KEYDOWN, keydown_closure);
113 let state_keyup: InputStateCell = state_cell.clone();
114 let keyup_closure: Closure<dyn FnMut(Event)> =
115 Closure::wrap(Box::new(move |event: Event| {
116 let code: String = Input::extract_key_code(&event);
117 if code.is_empty() {
118 return;
119 }
120 let state: &mut InputState = state_keyup.get_mut();
121 state.release_key(code);
122 }));
123 Self::register_listener(window, INPUT_EVENT_KEYUP, keyup_closure);
124 }
125
126 /// Binds mouse / touch / context-menu listeners to the pointer target.
127 ///
128 /// `touchstart` and `touchmove` call `prevent_default()` so the browser
129 /// does not interpret touches as scroll/zoom gestures before the engine
130 /// sees them. `contextmenu` is suppressed so right-click reaches the
131 /// engine as `MouseButton::Right` instead of opening the browser menu.
132 ///
133 /// # Arguments
134 ///
135 /// - `&InputStateCell` - The shared input state.
136 /// - `&EventTarget` - The pointer target (typically the canvas element).
137 pub fn attach_pointer(state_cell: &InputStateCell, target: &EventTarget) {
138 let state_mousedown: InputStateCell = state_cell.clone();
139 let mousedown_closure: Closure<dyn FnMut(Event)> =
140 Closure::wrap(Box::new(move |event: Event| {
141 let button: MouseButton = Input::extract_mouse_button(&event);
142 let position: Vector2D = Input::extract_mouse_position(&event);
143 let state: &mut InputState = state_mousedown.get_mut();
144 state.press_mouse_button(button, position);
145 }));
146 Self::register_listener(target, INPUT_EVENT_MOUSEDOWN, mousedown_closure);
147 let state_mouseup: InputStateCell = state_cell.clone();
148 let mouseup_closure: Closure<dyn FnMut(Event)> =
149 Closure::wrap(Box::new(move |event: Event| {
150 let button: MouseButton = Input::extract_mouse_button(&event);
151 let state: &mut InputState = state_mouseup.get_mut();
152 state.release_mouse_button(button);
153 }));
154 Self::register_listener(target, INPUT_EVENT_MOUSEUP, mouseup_closure);
155 let state_mousemove: InputStateCell = state_cell.clone();
156 let mousemove_closure: Closure<dyn FnMut(Event)> =
157 Closure::wrap(Box::new(move |event: Event| {
158 let position: Vector2D = Input::extract_mouse_position(&event);
159 let state: &mut InputState = state_mousemove.get_mut();
160 state.update_mouse_position(position);
161 }));
162 Self::register_listener(target, INPUT_EVENT_MOUSEMOVE, mousemove_closure);
163 let state_mouseleave: InputStateCell = state_cell.clone();
164 let mouseleave_closure: Closure<dyn FnMut(Event)> =
165 Closure::wrap(Box::new(move |_: Event| {
166 let state: &mut InputState = state_mouseleave.get_mut();
167 state.set_mouse_moved(false);
168 state.set_mouse_delta(Vector2D::zero());
169 }));
170 Self::register_listener(target, INPUT_EVENT_MOUSELEAVE, mouseleave_closure);
171 let state_touchstart: InputStateCell = state_cell.clone();
172 let touchstart_closure: Closure<dyn FnMut(Event)> =
173 Closure::wrap(Box::new(move |event: Event| {
174 event.prevent_default();
175 let state: &mut InputState = state_touchstart.get_mut();
176 for (identifier, position) in Input::extract_touch_positions(&event) {
177 state.start_touch(identifier, position);
178 }
179 }));
180 Self::register_listener(target, INPUT_EVENT_TOUCHSTART, touchstart_closure);
181 let state_touchmove: InputStateCell = state_cell.clone();
182 let touchmove_closure: Closure<dyn FnMut(Event)> =
183 Closure::wrap(Box::new(move |event: Event| {
184 event.prevent_default();
185 let state: &mut InputState = state_touchmove.get_mut();
186 for (identifier, position) in Input::extract_touch_positions(&event) {
187 state.update_touch(identifier, position);
188 }
189 }));
190 Self::register_listener(target, INPUT_EVENT_TOUCHMOVE, touchmove_closure);
191 let state_touchend: InputStateCell = state_cell.clone();
192 let touchend_closure: Closure<dyn FnMut(Event)> =
193 Closure::wrap(Box::new(move |event: Event| {
194 let state: &mut InputState = state_touchend.get_mut();
195 for identifier in Input::extract_touch_identifiers(&event) {
196 state.end_touch(identifier);
197 }
198 }));
199 Self::register_listener(target, INPUT_EVENT_TOUCHEND, touchend_closure);
200 let contextmenu_closure: Closure<dyn FnMut(Event)> =
201 Closure::wrap(Box::new(move |event: Event| {
202 event.prevent_default();
203 }));
204 Self::register_listener(target, INPUT_EVENT_CONTEXTMENU, contextmenu_closure);
205 }
206
207 /// Extracts `(identifier, position)` pairs for every changed touch.
208 ///
209 /// A single touch event can carry multiple changed touches, so this
210 /// iterates the whole `changedTouches` list rather than reading index 0.
211 ///
212 /// # Arguments
213 ///
214 /// - `&Event` - The touch event.
215 ///
216 /// # Returns
217 ///
218 /// - `Vec<(i32, Vector2D)>` - The identifier and client position of each changed touch.
219 fn extract_touch_positions(event: &Event) -> Vec<(i32, Vector2D)> {
220 let touch_event: &TouchEvent = event.unchecked_ref();
221 let touches: TouchList = touch_event.changed_touches();
222 let length: u32 = touches.length();
223 let mut out: Vec<(i32, Vector2D)> = Vec::with_capacity(length as usize);
224 for index in 0..length {
225 let Some(touch) = touches.get(index) else {
226 continue;
227 };
228 let identifier: i32 = touch.identifier();
229 let position: Vector2D =
230 Vector2D::new(f64::from(touch.client_x()), f64::from(touch.client_y()));
231 out.push((identifier, position));
232 }
233 out
234 }
235
236 /// Extracts the identifier of every changed touch (for `touchend`).
237 ///
238 /// # Arguments
239 ///
240 /// - `&Event` - The touch event.
241 ///
242 /// # Returns
243 ///
244 /// - `Vec<i32>` - The identifier of each changed touch.
245 fn extract_touch_identifiers(event: &Event) -> Vec<i32> {
246 let touch_event: &TouchEvent = event.unchecked_ref();
247 let touches: TouchList = touch_event.changed_touches();
248 let length: u32 = touches.length();
249 let mut out: Vec<i32> = Vec::with_capacity(length as usize);
250 for index in 0..length {
251 let Some(touch) = touches.get(index) else {
252 continue;
253 };
254 out.push(touch.identifier());
255 }
256 out
257 }
258
259 /// Registers a closure on the target and leaks it for the document's lifetime.
260 ///
261 /// The engine follows the wasm single-page-mount convention: listeners
262 /// are never detached, so the closure is `.forget()`-ed immediately
263 /// after registration (its clone of the state cell keeps the cell
264 /// reachable through the listeners even if the caller drops its `Rc`).
265 ///
266 /// # Arguments
267 ///
268 /// - `&EventTarget` - The DOM target to listen on.
269 /// - `&str` - The DOM event name.
270 /// - `Closure<dyn FnMut(Event)>` - The handler to register.
271 fn register_listener(
272 target: &EventTarget,
273 event_name: &str,
274 closure: Closure<dyn FnMut(Event)>,
275 ) {
276 let _: Result<(), JsValue> =
277 target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref());
278 closure.forget();
279 }
280}
281
282/// Implements input state management for `InputState`.
283impl InputState {
284 /// Records a key press event, adding to `keys_pressed` and `keys_held`.
285 ///
286 /// # Arguments
287 ///
288 /// - `String` - The key code string (e.g., `"KeyA"`, `"Space"`).
289 pub fn press_key(&mut self, key_code: String) {
290 if !self.get_keys_held().contains(&key_code) {
291 self.get_mut_keys_pressed().insert(key_code.clone());
292 }
293 self.get_mut_keys_held().insert(key_code);
294 }
295
296 /// Records a key release event, moving from `keys_held` to `keys_released`.
297 ///
298 /// # Arguments
299 ///
300 /// - `String` - The key code string.
301 pub fn release_key(&mut self, key_code: String) {
302 self.get_mut_keys_held().remove(&key_code);
303 self.get_mut_keys_released().insert(key_code);
304 }
305
306 /// Tests whether a key was pressed during this frame.
307 ///
308 /// # Arguments
309 ///
310 /// - `&str` - The key code string.
311 ///
312 /// # Returns
313 ///
314 /// - `bool` - True if the key was pressed this frame.
315 pub fn is_key_pressed<K>(&self, key_code: K) -> bool
316 where
317 K: AsRef<str>,
318 {
319 self.get_keys_pressed().contains(key_code.as_ref())
320 }
321
322 /// Tests whether a key is currently held down.
323 ///
324 /// # Arguments
325 ///
326 /// - `K: AsRef<str>` - The key code string.
327 ///
328 /// # Returns
329 ///
330 /// - `bool` - True if the key is held.
331 pub fn is_key_held<K>(&self, key_code: K) -> bool
332 where
333 K: AsRef<str>,
334 {
335 self.get_keys_held().contains(key_code.as_ref())
336 }
337
338 /// Tests whether a key was released during this frame.
339 ///
340 /// # Arguments
341 ///
342 /// - `K: AsRef<str>` - The key code string.
343 ///
344 /// # Returns
345 ///
346 /// - `bool` - True if the key was released this frame.
347 pub fn is_key_released<K>(&self, key_code: K) -> bool
348 where
349 K: AsRef<str>,
350 {
351 self.get_keys_released().contains(key_code.as_ref())
352 }
353
354 /// Records a mouse button press at the given position.
355 ///
356 /// # Arguments
357 ///
358 /// - `MouseButton` - The button that was pressed.
359 /// - `Vector2D` - The mouse position.
360 pub fn press_mouse_button(&mut self, button: MouseButton, position: Vector2D) {
361 if !self.get_mouse_buttons_held().contains(&button) {
362 self.get_mut_mouse_buttons_pressed().insert(button);
363 }
364 self.get_mut_mouse_buttons_held().insert(button);
365 self.set_mouse_position(position);
366 }
367
368 /// Records a mouse button release.
369 ///
370 /// # Arguments
371 ///
372 /// - `MouseButton` - The button that was released.
373 pub fn release_mouse_button(&mut self, button: MouseButton) {
374 self.get_mut_mouse_buttons_held().remove(&button);
375 self.get_mut_mouse_buttons_released().insert(button);
376 }
377
378 /// Updates the mouse position and computes the delta from the previous position.
379 ///
380 /// # Arguments
381 ///
382 /// - `Vector2D` - The new mouse position.
383 pub fn update_mouse_position(&mut self, position: Vector2D) {
384 self.set_mouse_delta(position - self.get_mouse_position());
385 self.set_mouse_position(position);
386 self.set_mouse_moved(true);
387 }
388
389 /// Tests whether a mouse button was pressed during this frame.
390 ///
391 /// # Arguments
392 ///
393 /// - `MouseButton` - The button to check.
394 ///
395 /// # Returns
396 ///
397 /// - `bool` - True if the button was pressed this frame.
398 pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool {
399 self.get_mouse_buttons_pressed().contains(&button)
400 }
401
402 /// Tests whether a mouse button is currently held down.
403 ///
404 /// # Arguments
405 ///
406 /// - `MouseButton` - The button to check.
407 ///
408 /// # Returns
409 ///
410 /// - `bool` - True if the button is held.
411 pub fn is_mouse_button_held(&self, button: MouseButton) -> bool {
412 self.get_mouse_buttons_held().contains(&button)
413 }
414
415 /// Adds or updates a touch point.
416 ///
417 /// # Arguments
418 ///
419 /// - `i32` - The touch identifier.
420 /// - `Vector2D` - The touch position.
421 pub fn update_touch(&mut self, identifier: i32, position: Vector2D) {
422 self.get_mut_touch_points().insert(identifier, position);
423 }
424
425 /// Records a new touch point that started this frame.
426 ///
427 /// # Arguments
428 ///
429 /// - `i32` - The touch identifier.
430 /// - `Vector2D` - The touch position.
431 pub fn start_touch(&mut self, identifier: i32, position: Vector2D) {
432 self.get_mut_touch_points().insert(identifier, position);
433 self.get_mut_touch_started().insert(identifier);
434 }
435
436 /// Removes a touch point and marks it as ended this frame.
437 ///
438 /// # Arguments
439 ///
440 /// - `i32` - The touch identifier.
441 pub fn end_touch(&mut self, identifier: i32) {
442 self.get_mut_touch_points().remove(&identifier);
443 self.get_mut_touch_ended().insert(identifier);
444 }
445
446 /// Returns the position of the lowest-identifier active touch point.
447 ///
448 /// Pointer-style consumers (the example pages' interactive demos) treat
449 /// the primary touch like a mouse cursor: touch events never update
450 /// `mouse_position`, so this accessor is the only public way to read a
451 /// touch position.
452 ///
453 /// # Returns
454 ///
455 /// - `Option<Vector2D>` - The client-space position of the primary
456 /// touch, or `None` when no touch is active.
457 pub fn primary_touch_position(&self) -> Option<Vector2D> {
458 self.get_touch_points()
459 .iter()
460 .min_by_key(|(identifier, _)| **identifier)
461 .map(|(_, position)| *position)
462 }
463
464 /// Clears all per-frame input data (pressed, released, deltas).
465 ///
466 /// Should be called at the end of each game frame after all input has been processed.
467 pub fn end_frame(&mut self) {
468 self.get_mut_keys_pressed().clear();
469 self.get_mut_keys_released().clear();
470 self.get_mut_mouse_buttons_pressed().clear();
471 self.get_mut_mouse_buttons_released().clear();
472 self.set_mouse_delta(Vector2D::zero());
473 self.set_mouse_moved(false);
474 self.get_mut_touch_started().clear();
475 self.get_mut_touch_ended().clear();
476 }
477}
478
479/// Implements `Default` for `InputState` as a fresh empty state.
480impl Default for InputState {
481 /// Constructs a default [`InputState`] value.
482 ///
483 /// # Returns
484 ///
485 /// - `InputState` - A default-constructed instance with the documented initial state.
486 fn default() -> InputState {
487 InputState::new()
488 }
489}
490
491/// Implements the per-pad query surface on `GamepadState`.
492impl GamepadState {
493 /// Reads one analog axis with the deadzone applied.
494 ///
495 /// # Arguments
496 ///
497 /// - `u32` - The axis index to read.
498 ///
499 /// # Returns
500 ///
501 /// - `f64` - The deadzone-corrected reading, or `0.0` when the pad
502 /// has no such axis.
503 pub fn axis(&self, axis: u32) -> f64 {
504 apply_axis_deadzone(read_raw_axis(self.get_axes(), axis))
505 }
506
507 /// Reads one button's raw pressure without any deadzone.
508 ///
509 /// # Arguments
510 ///
511 /// - `u32` - The button index to read.
512 ///
513 /// # Returns
514 ///
515 /// - `f64` - The pressure in `[0.0, 1.0]`, or `0.0` when the pad
516 /// has no such button.
517 pub fn button_value(&self, button: u32) -> f64 {
518 read_raw_axis(self.get_button_values(), button)
519 }
520
521 /// Tests whether a button went down on this frame.
522 ///
523 /// # Arguments
524 ///
525 /// - `u32` - The button index to check.
526 ///
527 /// # Returns
528 ///
529 /// - `bool` - True if the button was pressed this frame.
530 pub fn is_button_pressed(&self, button: u32) -> bool {
531 self.get_buttons_pressed().contains(&button)
532 }
533
534 /// Tests whether a button is currently held down.
535 ///
536 /// # Arguments
537 ///
538 /// - `u32` - The button index to check.
539 ///
540 /// # Returns
541 ///
542 /// - `bool` - True if the button is held.
543 pub fn is_button_held(&self, button: u32) -> bool {
544 self.get_buttons_held().contains(&button)
545 }
546
547 /// Tests whether a button came up on this frame.
548 ///
549 /// # Arguments
550 ///
551 /// - `u32` - The button index to check.
552 ///
553 /// # Returns
554 ///
555 /// - `bool` - True if the button was released this frame.
556 pub fn is_button_released(&self, button: u32) -> bool {
557 self.get_buttons_released().contains(&button)
558 }
559
560 /// Resolves a button's current state into the enum form.
561 ///
562 /// # Arguments
563 ///
564 /// - `u32` - The button index to check.
565 ///
566 /// # Returns
567 ///
568 /// - `InputAction` - The edge state of the button, `Pressed` taking
569 /// precedence over `Released` when a connect and a release land on
570 /// the same frame.
571 pub fn button_action(&self, button: u32) -> InputAction {
572 if self.is_button_pressed(button) {
573 return InputAction::Pressed;
574 }
575 if self.is_button_released(button) {
576 return InputAction::Released;
577 }
578 if self.is_button_held(button) {
579 return InputAction::Held;
580 }
581 InputAction::Idle
582 }
583
584 /// Tests whether a direction pair reads as a positive deflection.
585 ///
586 /// # Arguments
587 ///
588 /// - `u32` - The index of the axis to read.
589 /// - `f64` - The deflection that counts as a press.
590 ///
591 /// # Returns
592 ///
593 /// - `bool` - True if the corrected axis reading reaches
594 /// `threshold`.
595 pub fn axis_pressed(&self, axis: u32, threshold: f64) -> bool {
596 self.axis(axis) >= threshold
597 }
598
599 /// Clears this frame's per-frame data, keeping the held set.
600 ///
601 /// Mirrors [`InputState::end_frame`]: the pressed and released
602 /// sets are per-frame and are dropped, while the held set survives
603 /// so a button that is still down stays held on the next frame.
604 pub fn end_frame(&mut self) {
605 self.get_mut_buttons_pressed().clear();
606 self.get_mut_buttons_released().clear();
607 }
608}
609
610/// Implements the manager query surface on `GamepadManager`.
611impl GamepadManager {
612 /// Looks up the state of one gamepad by index.
613 ///
614 /// # Arguments
615 ///
616 /// - `u32` - The browser-assigned gamepad index.
617 ///
618 /// # Returns
619 ///
620 /// - `Option<&GamepadState>` - The stored state, or `None` when
621 /// this index has never been seen.
622 pub fn state(&self, index: u32) -> Option<&GamepadState> {
623 self.get_states().get(&index)
624 }
625
626 /// Looks up the state of one gamepad by index, mutably.
627 ///
628 /// # Arguments
629 ///
630 /// - `u32` - The browser-assigned gamepad index.
631 ///
632 /// # Returns
633 ///
634 /// - `Option<&mut GamepadState>` - The stored state, or `None` when
635 /// this index has never been seen.
636 pub fn state_mut(&mut self, index: u32) -> Option<&mut GamepadState> {
637 self.get_mut_states().get_mut(&index)
638 }
639
640 /// Tests whether a gamepad index is currently connected.
641 ///
642 /// # Arguments
643 ///
644 /// - `u32` - The browser-assigned gamepad index.
645 ///
646 /// # Returns
647 ///
648 /// - `bool` - True if the pad is connected.
649 pub fn is_connected(&self, index: u32) -> bool {
650 self.state(index)
651 .map(|pad: &GamepadState| pad.get_connected())
652 == Some(true)
653 }
654
655 /// Reads one analog axis of one gamepad with the deadzone applied.
656 ///
657 /// # Arguments
658 ///
659 /// - `u32` - The browser-assigned gamepad index.
660 /// - `u32` - The axis index to read.
661 ///
662 /// # Returns
663 ///
664 /// - `Option<f64>` - The deadzone-corrected reading, or `None` when
665 /// the index is unknown.
666 pub fn axis(&self, index: u32, axis: u32) -> Option<f64> {
667 self.state(index).map(|pad: &GamepadState| pad.axis(axis))
668 }
669
670 /// Reads one button's raw pressure on one gamepad.
671 ///
672 /// # Arguments
673 ///
674 /// - `u32` - The browser-assigned gamepad index.
675 /// - `u32` - The button index to read.
676 ///
677 /// # Returns
678 ///
679 /// - `Option<f64>` - The pressure, or `None` when the index is
680 /// unknown.
681 pub fn button_value(&self, index: u32, button: u32) -> Option<f64> {
682 self.state(index)
683 .map(|pad: &GamepadState| pad.button_value(button))
684 }
685
686 /// Tests whether a button on one gamepad went down on this frame.
687 ///
688 /// # Arguments
689 ///
690 /// - `u32` - The browser-assigned gamepad index.
691 /// - `u32` - The button index to check.
692 ///
693 /// # Returns
694 ///
695 /// - `bool` - True if the button was pressed this frame, false
696 /// when the index is unknown.
697 pub fn is_button_pressed(&self, index: u32, button: u32) -> bool {
698 self.state(index)
699 .map(|pad: &GamepadState| pad.is_button_pressed(button))
700 == Some(true)
701 }
702
703 /// Tests whether a button on one gamepad is currently held down.
704 ///
705 /// # Arguments
706 ///
707 /// - `u32` - The browser-assigned gamepad index.
708 /// - `u32` - The button index to check.
709 ///
710 /// # Returns
711 ///
712 /// - `bool` - True if the button is held, false when the index is
713 /// unknown.
714 pub fn is_button_held(&self, index: u32, button: u32) -> bool {
715 self.state(index)
716 .map(|pad: &GamepadState| pad.is_button_held(button))
717 == Some(true)
718 }
719
720 /// Tests whether a button on one gamepad came up on this frame.
721 ///
722 /// # Arguments
723 ///
724 /// - `u32` - The browser-assigned gamepad index.
725 /// - `u32` - The button index to check.
726 ///
727 /// # Returns
728 ///
729 /// - `bool` - True if the button was released this frame, false
730 /// when the index is unknown.
731 pub fn is_button_released(&self, index: u32, button: u32) -> bool {
732 self.state(index)
733 .map(|pad: &GamepadState| pad.is_button_released(button))
734 == Some(true)
735 }
736
737 /// Resolves a button's current state on one gamepad into enum form.
738 ///
739 /// # Arguments
740 ///
741 /// - `u32` - The browser-assigned gamepad index.
742 /// - `u32` - The button index to check.
743 ///
744 /// # Returns
745 ///
746 /// - `InputAction` - The edge state, `Idle` when the index is
747 /// unknown.
748 pub fn button_action(&self, index: u32, button: u32) -> InputAction {
749 self.state(index)
750 .map(|pad: &GamepadState| pad.button_action(button))
751 .unwrap_or_default()
752 }
753
754 /// Counts the gamepads that are currently connected.
755 ///
756 /// # Returns
757 ///
758 /// - `usize` - The number of connected gamepads.
759 pub fn connected_count(&self) -> usize {
760 self.get_states()
761 .values()
762 .filter(|pad: &&GamepadState| pad.get_connected())
763 .count()
764 }
765
766 /// Clears every pad's per-frame data and the manager's edge sets.
767 ///
768 /// Mirrors [`InputState::end_frame`]: pressed and released are
769 /// per-frame and are dropped, while held sets and the connected
770 /// flags survive.
771 pub fn end_frame(&mut self) {
772 for pad in self.get_mut_states().values_mut() {
773 pad.end_frame();
774 }
775 self.get_mut_connected().clear();
776 self.get_mut_disconnected().clear();
777 }
778}
779
780/// Implements the DOM wiring layer on `GamepadManager`.
781///
782/// The Gamepad API is a polling API: the browser only mutates
783/// `navigator.get_gamepads()` snapshots and never emits per-button
784/// events, so button edges must be derived by diffing one frame's
785/// snapshot against the last. The two connect / disconnect events are
786/// still bound so a device that appears or vanishes between two polls
787/// is reflected in `connected` / `disconnected` on the same frame
788/// rather than one frame late; a poll alone would also detect both, so
789/// the events are a latency optimisation and the poll is the
790/// authority.
791impl GamepadManager {
792 /// Re-reads every gamepad from the DOM and recomputes this
793 /// frame's button edges.
794 ///
795 /// A pad that the browser no longer lists is marked disconnected
796 /// and its edge sets flushed, so a yanked controller cannot keep
797 /// reporting held buttons forever.
798 ///
799 /// # Arguments
800 ///
801 /// - `&Window` - The global window, used to reach `navigator`.
802 pub fn poll(&mut self, window: &Window) {
803 let pads: Array = match Self::read_gamepads(window) {
804 Ok(pads) => pads,
805 Err(_) => return,
806 };
807 let length: u32 = pads.length();
808 // Phase 1: fold every live pad into the stored state.
809 let mut live: GamepadIndexSet = GamepadIndexSet::new();
810 for slot in 0..length {
811 let value: JsValue = pads.get(slot);
812 if value.is_null() || value.is_undefined() {
813 continue;
814 }
815 let pad: &Gamepad = value.unchecked_ref();
816 let index: u32 = pad.index();
817 live.insert(index);
818 self.sync_pad(index, pad);
819 }
820 // Phase 2: flush every pad the browser stopped listing.
821 let stale: Vec<u32> = self
822 .get_states()
823 .keys()
824 .filter(|index: &&u32| !live.contains(index))
825 .copied()
826 .collect();
827 for index in stale {
828 self.release_pad(index);
829 }
830 }
831
832 /// Attaches the gamepad listeners and returns the shared cell.
833 ///
834 /// # Arguments
835 ///
836 /// - `GamepadManagerCell` - The shared manager to mutate.
837 /// - `&Window` - The global window, receiving the connect and
838 /// disconnect events.
839 ///
840 /// # Returns
841 ///
842 /// - `GamepadManagerCell` - The same cell passed in, for
843 /// convenient chaining.
844 pub fn attach(manager_cell: GamepadManagerCell, window: &Window) -> GamepadManagerCell {
845 Self::attach_gamepad(&manager_cell, window);
846 manager_cell
847 }
848
849 /// Binds the connect / disconnect listeners to `window`.
850 ///
851 /// # Arguments
852 ///
853 /// - `&GamepadManagerCell` - The shared gamepad manager.
854 /// - `&Window` - The global window.
855 pub fn attach_gamepad(manager_cell: &GamepadManagerCell, window: &Window) {
856 let cell_connected: GamepadManagerCell = manager_cell.clone();
857 let connected_closure: Closure<dyn FnMut(Event)> =
858 Closure::wrap(Box::new(move |event: Event| {
859 let pad: Gamepad = event.unchecked_ref::<GamepadEvent>().gamepad().unwrap();
860 let manager: &mut GamepadManager = cell_connected.get_mut();
861 manager.sync_pad(pad.index(), &pad);
862 manager.get_mut_connected().insert(pad.index());
863 }));
864 Self::register_listener(window, INPUT_EVENT_GAMEPADCONNECTED, connected_closure);
865 let cell_disconnected: GamepadManagerCell = manager_cell.clone();
866 let disconnected_closure: Closure<dyn FnMut(Event)> =
867 Closure::wrap(Box::new(move |event: Event| {
868 let pad: Gamepad = event.unchecked_ref::<GamepadEvent>().gamepad().unwrap();
869 let index: u32 = pad.index();
870 let manager: &mut GamepadManager = cell_disconnected.get_mut();
871 manager.release_pad(index);
872 }));
873 Self::register_listener(
874 window,
875 INPUT_EVENT_GAMEPADDISCONNECTED,
876 disconnected_closure,
877 );
878 }
879
880 /// Registers a closure on the target and leaks it for the
881 /// document's lifetime.
882 ///
883 /// # Arguments
884 ///
885 /// - `&EventTarget` - The DOM target to listen on.
886 /// - `&str` - The DOM event name.
887 /// - `Closure<dyn FnMut(Event)>` - The handler to register.
888 fn register_listener(
889 target: &EventTarget,
890 event_name: &str,
891 closure: Closure<dyn FnMut(Event)>,
892 ) {
893 let _: Result<(), JsValue> =
894 target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref());
895 closure.forget();
896 }
897
898 /// Reads the browser's gamepad snapshot, mapping a thrown error
899 /// onto `None` so a sandboxed document degrades to "no gamepads".
900 ///
901 /// # Arguments
902 ///
903 /// - `&Window` - The global window, used to reach `navigator`.
904 ///
905 /// # Returns
906 ///
907 /// - `Result<Array, JsValue>` - The snapshot, or the thrown value.
908 fn read_gamepads(window: &Window) -> Result<Array, JsValue> {
909 window.navigator().get_gamepads()
910 }
911
912 /// Folds one pad's raw DOM reading into its stored state,
913 /// computing this frame's button edges from the previous frame's
914 /// held set.
915 ///
916 /// # Arguments
917 ///
918 /// - `u32` - The browser-assigned gamepad index.
919 /// - `&Gamepad` - The live pad to read.
920 fn sync_pad(&mut self, index: u32, pad: &Gamepad) {
921 let axes: Vec<f64> = Self::extract_axis_values(&pad.axes());
922 let values: Vec<f64> = Self::extract_button_values(&pad.buttons());
923 let previous_held: GamepadButtonSet = self
924 .state(index)
925 .map(|state: &GamepadState| state.get_buttons_held().clone())
926 .unwrap_or_default();
927 let pressed: GamepadButtonSet = compute_pressed_buttons(&previous_held, &values);
928 let released: GamepadButtonSet = compute_released_buttons(&previous_held, &values);
929 let held: GamepadButtonSet = compute_held_buttons(&values);
930 let id: String = pad.id();
931 self.get_mut_states().entry(index).or_default();
932 let Some(state) = self.state_mut(index) else {
933 return;
934 };
935 state.set_connected(true);
936 state.set_id(id);
937 state.set_axes(axes);
938 state.set_button_values(values);
939 *state.get_mut_buttons_pressed() = pressed;
940 *state.get_mut_buttons_released() = released;
941 *state.get_mut_buttons_held() = held;
942 }
943
944 /// Marks a pad as disconnected and releases every held button.
945 ///
946 /// A yanked controller stops appearing in the browser snapshot
947 /// while its buttons are still physically down, so the held set
948 /// is flushed and the flush is reported as a release: that is what
949 /// stops a character from running forever after the pad is
950 /// unplugged.
951 ///
952 /// # Arguments
953 ///
954 /// - `u32` - The browser-assigned gamepad index.
955 fn release_pad(&mut self, index: u32) {
956 let was_connected: bool = self.is_connected(index);
957 let held: GamepadButtonSet = self
958 .state(index)
959 .map(|state: &GamepadState| state.get_buttons_held().clone())
960 .unwrap_or_default();
961 if let Some(state) = self.state_mut(index) {
962 state.set_connected(false);
963 *state.get_mut_buttons_pressed() = GamepadButtonSet::new();
964 *state.get_mut_buttons_released() = held;
965 state.get_mut_buttons_held().clear();
966 }
967 if was_connected {
968 self.get_mut_disconnected().insert(index);
969 }
970 }
971
972 /// Extracts the analog readings out of a `Gamepad.axes` list.
973 ///
974 /// # Arguments
975 ///
976 /// - `&Array` - The live DOM `axes` list.
977 ///
978 /// # Returns
979 ///
980 /// - `Vec<f64>` - The readings, one per axis.
981 fn extract_axis_values(axes: &Array) -> Vec<f64> {
982 let length: u32 = axes.length();
983 let mut out: Vec<f64> = Vec::with_capacity(length as usize);
984 for index in 0..length {
985 let value: JsValue = axes.get(index);
986 out.push(value.as_f64().unwrap_or(0.0));
987 }
988 out
989 }
990
991 /// Extracts the pressures out of a `Gamepad.buttons` list.
992 ///
993 /// # Arguments
994 ///
995 /// - `&Array` - The live DOM `buttons` list.
996 ///
997 /// # Returns
998 ///
999 /// - `Vec<f64>` - The pressures, one per button.
1000 fn extract_button_values(buttons: &Array) -> Vec<f64> {
1001 let length: u32 = buttons.length();
1002 let mut out: Vec<f64> = Vec::with_capacity(length as usize);
1003 for index in 0..length {
1004 let value: JsValue = buttons.get(index);
1005 if value.is_null() || value.is_undefined() {
1006 out.push(0.0);
1007 continue;
1008 }
1009 out.push(value.unchecked_ref::<GamepadButton>().value());
1010 }
1011 out
1012 }
1013}