Skip to main content

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}