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}