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}