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        Reflect::get(event.as_ref(), &JsValue::from_str(INPUT_KEY_CODE_PROPERTY))
16            .ok()
17            .and_then(|value: JsValue| value.as_string())
18            .unwrap_or_default()
19    }
20
21    /// Extracts the mouse button enum from a mouse event.
22    ///
23    /// # Arguments
24    ///
25    /// - `&Event` - The mouse event.
26    ///
27    /// # Returns
28    ///
29    /// - `MouseButton` - The mouse button that was pressed or released.
30    pub fn extract_mouse_button(event: &Event) -> MouseButton {
31        let button_value: i32 = Reflect::get(
32            event.as_ref(),
33            &JsValue::from_str(INPUT_MOUSE_BUTTON_PROPERTY),
34        )
35        .ok()
36        .and_then(|value: JsValue| value.as_f64())
37        .map(|float: f64| float as i32)
38        .unwrap_or(0);
39        match button_value {
40            0 => MouseButton::Left,
41            1 => MouseButton::Middle,
42            2 => MouseButton::Right,
43            3 => MouseButton::Button4,
44            4 => MouseButton::Button5,
45            _ => MouseButton::Left,
46        }
47    }
48
49    /// Extracts the client (viewport) coordinates from a mouse event.
50    ///
51    /// # Arguments
52    ///
53    /// - `&Event` - The mouse event.
54    ///
55    /// # Returns
56    ///
57    /// - `Vector2D` - The `(x, y)` client coordinates.
58    pub fn extract_mouse_position(event: &Event) -> Vector2D {
59        let client_x: f64 =
60            Reflect::get(event.as_ref(), &JsValue::from_str(INPUT_CLIENT_X_PROPERTY))
61                .ok()
62                .and_then(|value: JsValue| value.as_f64())
63                .unwrap_or(0.0);
64        let client_y: f64 =
65            Reflect::get(event.as_ref(), &JsValue::from_str(INPUT_CLIENT_Y_PROPERTY))
66                .ok()
67                .and_then(|value: JsValue| value.as_f64())
68                .unwrap_or(0.0);
69        Vector2D::new(client_x, client_y)
70    }
71}
72
73/// Implements DOM event listener registration on the `Input` namespace struct.
74///
75/// This is the wiring layer that routes DOM events into [`InputState`]:
76/// keyboard events bind to `window` (a `<canvas>` is not focusable by
77/// default), while mouse and touch events bind to the canvas element so
78/// hit-testing coordinates stay canvas-local. Registered closures are
79/// `.forget()`-ed and stay alive for the lifetime of the document,
80/// matching the engine's mount-only convention.
81impl Input {
82    /// Attaches all input listeners and returns the shared state cell.
83    ///
84    /// # Arguments
85    ///
86    /// - `InputStateCell` - The shared input state to mutate from event handlers.
87    /// - `&Window` - The global window, receiving keyboard events.
88    /// - `&EventTarget` - The pointer target (typically the canvas element).
89    ///
90    /// # Returns
91    ///
92    /// - `InputStateCell` - The same cell passed in, for convenient chaining.
93    pub fn attach(
94        state_cell: InputStateCell,
95        window: &Window,
96        pointer_target: &EventTarget,
97    ) -> InputStateCell {
98        Self::attach_keyboard(&state_cell, window);
99        Self::attach_pointer(&state_cell, pointer_target);
100        state_cell
101    }
102
103    /// Binds `keydown` / `keyup` listeners to `window`.
104    ///
105    /// Keyboard events must bind to `window` rather than the canvas: a
106    /// `<canvas>` element is not focusable unless `tabindex` is set and the
107    /// user clicks it, so canvas-bound key listeners would never fire.
108    ///
109    /// # Arguments
110    ///
111    /// - `&InputStateCell` - The shared input state.
112    /// - `&Window` - The global window.
113    pub fn attach_keyboard(state_cell: &InputStateCell, window: &Window) {
114        let state_keydown: InputStateCell = state_cell.clone();
115        let keydown_closure: Closure<dyn FnMut(Event)> =
116            Closure::wrap(Box::new(move |event: Event| {
117                let code: String = Input::extract_key_code(&event);
118                if code.is_empty() {
119                    return;
120                }
121                let state: &mut InputState = state_keydown.get_mut();
122                state.press_key(code);
123            }));
124        Self::register_listener(window, INPUT_EVENT_KEYDOWN, keydown_closure);
125        let state_keyup: InputStateCell = state_cell.clone();
126        let keyup_closure: Closure<dyn FnMut(Event)> =
127            Closure::wrap(Box::new(move |event: Event| {
128                let code: String = Input::extract_key_code(&event);
129                if code.is_empty() {
130                    return;
131                }
132                let state: &mut InputState = state_keyup.get_mut();
133                state.release_key(code);
134            }));
135        Self::register_listener(window, INPUT_EVENT_KEYUP, keyup_closure);
136    }
137
138    /// Binds mouse / touch / context-menu listeners to the pointer target.
139    ///
140    /// `touchstart` and `touchmove` call `prevent_default()` so the browser
141    /// does not interpret touches as scroll/zoom gestures before the engine
142    /// sees them. `contextmenu` is suppressed so right-click reaches the
143    /// engine as `MouseButton::Right` instead of opening the browser menu.
144    ///
145    /// # Arguments
146    ///
147    /// - `&InputStateCell` - The shared input state.
148    /// - `&EventTarget` - The pointer target (typically the canvas element).
149    pub fn attach_pointer(state_cell: &InputStateCell, target: &EventTarget) {
150        let state_mousedown: InputStateCell = state_cell.clone();
151        let mousedown_closure: Closure<dyn FnMut(Event)> =
152            Closure::wrap(Box::new(move |event: Event| {
153                let button: MouseButton = Input::extract_mouse_button(&event);
154                let position: Vector2D = Input::extract_mouse_position(&event);
155                let state: &mut InputState = state_mousedown.get_mut();
156                state.press_mouse_button(button, position);
157            }));
158        Self::register_listener(target, INPUT_EVENT_MOUSEDOWN, mousedown_closure);
159        let state_mouseup: InputStateCell = state_cell.clone();
160        let mouseup_closure: Closure<dyn FnMut(Event)> =
161            Closure::wrap(Box::new(move |event: Event| {
162                let button: MouseButton = Input::extract_mouse_button(&event);
163                let state: &mut InputState = state_mouseup.get_mut();
164                state.release_mouse_button(button);
165            }));
166        Self::register_listener(target, INPUT_EVENT_MOUSEUP, mouseup_closure);
167        let state_mousemove: InputStateCell = state_cell.clone();
168        let mousemove_closure: Closure<dyn FnMut(Event)> =
169            Closure::wrap(Box::new(move |event: Event| {
170                let position: Vector2D = Input::extract_mouse_position(&event);
171                let state: &mut InputState = state_mousemove.get_mut();
172                state.update_mouse_position(position);
173            }));
174        Self::register_listener(target, INPUT_EVENT_MOUSEMOVE, mousemove_closure);
175        let state_mouseleave: InputStateCell = state_cell.clone();
176        let mouseleave_closure: Closure<dyn FnMut(Event)> =
177            Closure::wrap(Box::new(move |_: Event| {
178                let state: &mut InputState = state_mouseleave.get_mut();
179                state.set_mouse_moved(false);
180                state.set_mouse_delta(Vector2D::zero());
181            }));
182        Self::register_listener(target, INPUT_EVENT_MOUSELEAVE, mouseleave_closure);
183        let state_touchstart: InputStateCell = state_cell.clone();
184        let touchstart_closure: Closure<dyn FnMut(Event)> =
185            Closure::wrap(Box::new(move |event: Event| {
186                event.prevent_default();
187                let state: &mut InputState = state_touchstart.get_mut();
188                for (identifier, position) in Input::extract_touch_positions(&event) {
189                    state.start_touch(identifier, position);
190                }
191            }));
192        Self::register_listener(target, INPUT_EVENT_TOUCHSTART, touchstart_closure);
193        let state_touchmove: InputStateCell = state_cell.clone();
194        let touchmove_closure: Closure<dyn FnMut(Event)> =
195            Closure::wrap(Box::new(move |event: Event| {
196                event.prevent_default();
197                let state: &mut InputState = state_touchmove.get_mut();
198                for (identifier, position) in Input::extract_touch_positions(&event) {
199                    state.update_touch(identifier, position);
200                }
201            }));
202        Self::register_listener(target, INPUT_EVENT_TOUCHMOVE, touchmove_closure);
203        let state_touchend: InputStateCell = state_cell.clone();
204        let touchend_closure: Closure<dyn FnMut(Event)> =
205            Closure::wrap(Box::new(move |event: Event| {
206                let state: &mut InputState = state_touchend.get_mut();
207                for identifier in Input::extract_touch_identifiers(&event) {
208                    state.end_touch(identifier);
209                }
210            }));
211        Self::register_listener(target, INPUT_EVENT_TOUCHEND, touchend_closure);
212        let contextmenu_closure: Closure<dyn FnMut(Event)> =
213            Closure::wrap(Box::new(move |event: Event| {
214                event.prevent_default();
215            }));
216        Self::register_listener(target, INPUT_EVENT_CONTEXTMENU, contextmenu_closure);
217    }
218
219    /// Extracts `(identifier, position)` pairs for every changed touch.
220    ///
221    /// A single touch event can carry multiple changed touches, so this
222    /// iterates the whole `changedTouches` list rather than reading index 0.
223    ///
224    /// # Arguments
225    ///
226    /// - `&Event` - The touch event.
227    ///
228    /// # Returns
229    ///
230    /// - `Vec<(i32, Vector2D)>` - The identifier and client position of each changed touch.
231    fn extract_touch_positions(event: &Event) -> Vec<(i32, Vector2D)> {
232        let touch_event: &TouchEvent = event.unchecked_ref();
233        let touches: TouchList = touch_event.changed_touches();
234        let length: u32 = touches.length();
235        let mut out: Vec<(i32, Vector2D)> = Vec::with_capacity(length as usize);
236        for index in 0..length {
237            let Some(touch) = touches.get(index) else {
238                continue;
239            };
240            let identifier: i32 = touch.identifier();
241            let position: Vector2D =
242                Vector2D::new(f64::from(touch.client_x()), f64::from(touch.client_y()));
243            out.push((identifier, position));
244        }
245        out
246    }
247
248    /// Extracts the identifier of every changed touch (for `touchend`).
249    ///
250    /// # Arguments
251    ///
252    /// - `&Event` - The touch event.
253    ///
254    /// # Returns
255    ///
256    /// - `Vec<i32>` - The identifier of each changed touch.
257    fn extract_touch_identifiers(event: &Event) -> Vec<i32> {
258        let touch_event: &TouchEvent = event.unchecked_ref();
259        let touches: TouchList = touch_event.changed_touches();
260        let length: u32 = touches.length();
261        let mut out: Vec<i32> = Vec::with_capacity(length as usize);
262        for index in 0..length {
263            let Some(touch) = touches.get(index) else {
264                continue;
265            };
266            out.push(touch.identifier());
267        }
268        out
269    }
270
271    /// Registers a closure on the target and leaks it for the document's lifetime.
272    ///
273    /// The engine follows the wasm single-page-mount convention: listeners
274    /// are never detached, so the closure is `.forget()`-ed immediately
275    /// after registration (its clone of the state cell keeps the cell
276    /// reachable through the listeners even if the caller drops its `Rc`).
277    ///
278    /// # Arguments
279    ///
280    /// - `&EventTarget` - The DOM target to listen on.
281    /// - `&str` - The DOM event name.
282    /// - `Closure<dyn FnMut(Event)>` - The handler to register.
283    fn register_listener(
284        target: &EventTarget,
285        event_name: &str,
286        closure: Closure<dyn FnMut(Event)>,
287    ) {
288        let _: Result<(), JsValue> =
289            target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref());
290        closure.forget();
291    }
292}
293
294/// Implements input state management for `InputState`.
295impl InputState {
296    /// Records a key press event, adding to `keys_pressed` and `keys_held`.
297    ///
298    /// # Arguments
299    ///
300    /// - `String` - The key code string (e.g., `"KeyA"`, `"Space"`).
301    pub fn press_key(&mut self, key_code: String) {
302        if !self.get_keys_held().contains(&key_code) {
303            self.get_mut_keys_pressed().insert(key_code.clone());
304        }
305        self.get_mut_keys_held().insert(key_code);
306    }
307
308    /// Records a key release event, moving from `keys_held` to `keys_released`.
309    ///
310    /// # Arguments
311    ///
312    /// - `String` - The key code string.
313    pub fn release_key(&mut self, key_code: String) {
314        self.get_mut_keys_held().remove(&key_code);
315        self.get_mut_keys_released().insert(key_code);
316    }
317
318    /// Tests whether a key was pressed during this frame.
319    ///
320    /// # Arguments
321    ///
322    /// - `&str` - The key code string.
323    ///
324    /// # Returns
325    ///
326    /// - `bool` - True if the key was pressed this frame.
327    pub fn is_key_pressed<K>(&self, key_code: K) -> bool
328    where
329        K: AsRef<str>,
330    {
331        self.get_keys_pressed().contains(key_code.as_ref())
332    }
333
334    /// Tests whether a key is currently held down.
335    ///
336    /// # Arguments
337    ///
338    /// - `K: AsRef<str>` - The key code string.
339    ///
340    /// # Returns
341    ///
342    /// - `bool` - True if the key is held.
343    pub fn is_key_held<K>(&self, key_code: K) -> bool
344    where
345        K: AsRef<str>,
346    {
347        self.get_keys_held().contains(key_code.as_ref())
348    }
349
350    /// Tests whether a key was released during this frame.
351    ///
352    /// # Arguments
353    ///
354    /// - `K: AsRef<str>` - The key code string.
355    ///
356    /// # Returns
357    ///
358    /// - `bool` - True if the key was released this frame.
359    pub fn is_key_released<K>(&self, key_code: K) -> bool
360    where
361        K: AsRef<str>,
362    {
363        self.get_keys_released().contains(key_code.as_ref())
364    }
365
366    /// Records a mouse button press at the given position.
367    ///
368    /// # Arguments
369    ///
370    /// - `MouseButton` - The button that was pressed.
371    /// - `Vector2D` - The mouse position.
372    pub fn press_mouse_button(&mut self, button: MouseButton, position: Vector2D) {
373        if !self.get_mouse_buttons_held().contains(&button) {
374            self.get_mut_mouse_buttons_pressed().insert(button);
375        }
376        self.get_mut_mouse_buttons_held().insert(button);
377        self.set_mouse_position(position);
378    }
379
380    /// Records a mouse button release.
381    ///
382    /// # Arguments
383    ///
384    /// - `MouseButton` - The button that was released.
385    pub fn release_mouse_button(&mut self, button: MouseButton) {
386        self.get_mut_mouse_buttons_held().remove(&button);
387        self.get_mut_mouse_buttons_released().insert(button);
388    }
389
390    /// Updates the mouse position and computes the delta from the previous position.
391    ///
392    /// # Arguments
393    ///
394    /// - `Vector2D` - The new mouse position.
395    pub fn update_mouse_position(&mut self, position: Vector2D) {
396        self.set_mouse_delta(position - self.get_mouse_position());
397        self.set_mouse_position(position);
398        self.set_mouse_moved(true);
399    }
400
401    /// Tests whether a mouse button was pressed during this frame.
402    ///
403    /// # Arguments
404    ///
405    /// - `MouseButton` - The button to check.
406    ///
407    /// # Returns
408    ///
409    /// - `bool` - True if the button was pressed this frame.
410    pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool {
411        self.get_mouse_buttons_pressed().contains(&button)
412    }
413
414    /// Tests whether a mouse button is currently held down.
415    ///
416    /// # Arguments
417    ///
418    /// - `MouseButton` - The button to check.
419    ///
420    /// # Returns
421    ///
422    /// - `bool` - True if the button is held.
423    pub fn is_mouse_button_held(&self, button: MouseButton) -> bool {
424        self.get_mouse_buttons_held().contains(&button)
425    }
426
427    /// Adds or updates a touch point.
428    ///
429    /// # Arguments
430    ///
431    /// - `i32` - The touch identifier.
432    /// - `Vector2D` - The touch position.
433    pub fn update_touch(&mut self, identifier: i32, position: Vector2D) {
434        self.get_mut_touch_points().insert(identifier, position);
435    }
436
437    /// Records a new touch point that started this frame.
438    ///
439    /// # Arguments
440    ///
441    /// - `i32` - The touch identifier.
442    /// - `Vector2D` - The touch position.
443    pub fn start_touch(&mut self, identifier: i32, position: Vector2D) {
444        self.get_mut_touch_points().insert(identifier, position);
445        self.get_mut_touch_started().insert(identifier);
446    }
447
448    /// Removes a touch point and marks it as ended this frame.
449    ///
450    /// # Arguments
451    ///
452    /// - `i32` - The touch identifier.
453    pub fn end_touch(&mut self, identifier: i32) {
454        self.get_mut_touch_points().remove(&identifier);
455        self.get_mut_touch_ended().insert(identifier);
456    }
457
458    /// Returns the position of the lowest-identifier active touch point.
459    ///
460    /// Pointer-style consumers (the example pages' interactive demos) treat
461    /// the primary touch like a mouse cursor: touch events never update
462    /// `mouse_position`, so this accessor is the only public way to read a
463    /// touch position.
464    ///
465    /// # Returns
466    ///
467    /// - `Option<Vector2D>` - The client-space position of the primary
468    ///   touch, or `None` when no touch is active.
469    pub fn primary_touch_position(&self) -> Option<Vector2D> {
470        self.touch_points
471            .iter()
472            .min_by_key(|(identifier, _)| **identifier)
473            .map(|(_, position)| *position)
474    }
475
476    /// Clears all per-frame input data (pressed, released, deltas).
477    ///
478    /// Should be called at the end of each game frame after all input has been processed.
479    pub fn end_frame(&mut self) {
480        self.get_mut_keys_pressed().clear();
481        self.get_mut_keys_released().clear();
482        self.get_mut_mouse_buttons_pressed().clear();
483        self.get_mut_mouse_buttons_released().clear();
484        self.set_mouse_delta(Vector2D::zero());
485        self.set_mouse_moved(false);
486        self.get_mut_touch_started().clear();
487        self.get_mut_touch_ended().clear();
488    }
489}
490
491/// Implements `Default` for `InputState` as a fresh empty state.
492impl Default for InputState {
493    fn default() -> InputState {
494        InputState::new()
495    }
496}