1use glam::Vec2;
7use jugar_input::{
8 ButtonState, GamepadAxis, GamepadButton, InputState, KeyCode, MouseButton, TouchEvent,
9 TouchPhase,
10};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14#[derive(Error, Debug, Clone, PartialEq, Eq)]
16pub enum InputTranslationError {
17 #[error("Failed to parse input JSON: {0}")]
19 InvalidJson(String),
20 #[error("Unknown event type: {0}")]
22 UnknownEventType(String),
23 #[error("Invalid event data: {0}")]
25 InvalidData(String),
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct BrowserInputEvent {
31 pub event_type: String,
33 pub timestamp: f64,
35 pub data: BrowserEventData,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(untagged)]
45pub enum BrowserEventData {
46 Key {
48 key: String,
50 },
51 GamepadAxis {
53 gamepad: u8,
55 axis: u8,
57 value: f32,
59 },
60 Touch {
62 id: u32,
64 x: f32,
66 y: f32,
68 },
69 MouseButton {
71 button: u8,
73 x: f32,
75 y: f32,
77 },
78 GamepadButton {
80 gamepad: u8,
82 button: u8,
84 },
85 MouseMove {
87 x: f32,
89 y: f32,
91 },
92}
93
94#[must_use]
96pub fn translate_key(js_key: &str) -> Option<KeyCode> {
97 match js_key {
98 "ArrowUp" => Some(KeyCode::Up),
100 "ArrowDown" => Some(KeyCode::Down),
101 "ArrowLeft" => Some(KeyCode::Left),
102 "ArrowRight" => Some(KeyCode::Right),
103
104 "Space" => Some(KeyCode::Space),
106 "Enter" => Some(KeyCode::Enter),
107 "Escape" => Some(KeyCode::Escape),
108
109 key if key.starts_with("Key") && key.len() == 4 => {
111 let c = key.chars().nth(3)?;
112 if c.is_ascii_uppercase() {
113 Some(KeyCode::Letter(c))
114 } else {
115 None
116 }
117 }
118
119 key if key.starts_with("Digit") && key.len() == 6 => {
121 let c = key.chars().nth(5)?;
122 let n = c.to_digit(10)? as u8;
123 Some(KeyCode::Number(n))
124 }
125
126 key if key.starts_with('F') && key.len() <= 3 => {
128 let n: u8 = key[1..].parse().ok()?;
129 if (1..=12).contains(&n) {
130 Some(KeyCode::Function(n))
131 } else {
132 None
133 }
134 }
135
136 _ => None,
137 }
138}
139
140#[must_use]
142pub const fn translate_mouse_button(button: u8) -> MouseButton {
143 match button {
144 0 => MouseButton::Left,
145 1 => MouseButton::Middle,
146 2 => MouseButton::Right,
147 n => MouseButton::Extra(n.saturating_sub(3)),
148 }
149}
150
151#[must_use]
153pub const fn translate_gamepad_button(button: u8) -> Option<GamepadButton> {
154 match button {
155 0 => Some(GamepadButton::South),
156 1 => Some(GamepadButton::East),
157 2 => Some(GamepadButton::West),
158 3 => Some(GamepadButton::North),
159 4 => Some(GamepadButton::LeftBumper),
160 5 => Some(GamepadButton::RightBumper),
161 8 => Some(GamepadButton::Select),
162 9 => Some(GamepadButton::Start),
163 10 => Some(GamepadButton::LeftStick),
164 11 => Some(GamepadButton::RightStick),
165 12 => Some(GamepadButton::DPadUp),
166 13 => Some(GamepadButton::DPadDown),
167 14 => Some(GamepadButton::DPadLeft),
168 15 => Some(GamepadButton::DPadRight),
169 _ => None,
170 }
171}
172
173#[must_use]
175pub const fn translate_gamepad_axis(axis: u8) -> Option<GamepadAxis> {
176 match axis {
177 0 => Some(GamepadAxis::LeftStickX),
178 1 => Some(GamepadAxis::LeftStickY),
179 2 => Some(GamepadAxis::RightStickX),
180 3 => Some(GamepadAxis::RightStickY),
181 _ => None,
182 }
183}
184
185pub fn process_input_events(
197 events_json: &str,
198 state: &mut InputState,
199 canvas_offset: Vec2,
200) -> Result<(), InputTranslationError> {
201 if events_json.is_empty() || events_json == "[]" {
202 return Ok(());
203 }
204
205 let events: Vec<BrowserInputEvent> = serde_json::from_str(events_json)
206 .map_err(|e| InputTranslationError::InvalidJson(e.to_string()))?;
207
208 for event in events {
209 process_single_event(&event, state, canvas_offset)?;
210 }
211
212 Ok(())
213}
214
215#[allow(clippy::too_many_lines)]
223fn process_single_event(
224 event: &BrowserInputEvent,
225 state: &mut InputState,
226 offset: Vec2,
227) -> Result<(), InputTranslationError> {
228 match event.event_type.as_str() {
229 "KeyDown" => {
230 if let BrowserEventData::Key { key } = &event.data {
231 if let Some(key_code) = translate_key(key) {
232 let current = state.key(key_code);
233 if !current.is_down() {
234 state.set_key(key_code, ButtonState::JustPressed);
235 }
236 }
237 }
238 }
239 "KeyUp" => {
240 if let BrowserEventData::Key { key } = &event.data {
241 if let Some(key_code) = translate_key(key) {
242 state.set_key(key_code, ButtonState::JustReleased);
243 }
244 }
245 }
246 "MouseMove" => {
247 if let BrowserEventData::MouseMove { x, y } = &event.data {
248 let old_pos = state.mouse_position;
249 state.mouse_position = Vec2::new(*x - offset.x, *y - offset.y);
251 state.mouse_delta = state.mouse_position - old_pos;
252 }
253 }
254 "MouseDown" => {
255 if let BrowserEventData::MouseButton { button, x, y } = &event.data {
256 state.mouse_position = Vec2::new(*x - offset.x, *y - offset.y);
258 let idx = mouse_button_index(*button);
259 if idx < state.mouse_buttons.len() {
260 state.mouse_buttons[idx] = ButtonState::JustPressed;
261 }
262 }
263 }
264 "MouseUp" => {
265 if let BrowserEventData::MouseButton { button, x, y } = &event.data {
266 state.mouse_position = Vec2::new(*x - offset.x, *y - offset.y);
268 let idx = mouse_button_index(*button);
269 if idx < state.mouse_buttons.len() {
270 state.mouse_buttons[idx] = ButtonState::JustReleased;
271 }
272 }
273 }
274 "TouchStart" => {
275 if let BrowserEventData::Touch { id, x, y } = &event.data {
276 let canvas_pos = Vec2::new(*x - offset.x, *y - offset.y);
278 state.touches.push(
279 TouchEvent::new(canvas_pos)
280 .with_id(*id)
281 .with_phase(TouchPhase::Started),
282 );
283 }
284 }
285 "TouchMove" => {
286 if let BrowserEventData::Touch { id, x, y } = &event.data {
287 let canvas_pos = Vec2::new(*x - offset.x, *y - offset.y);
289 if let Some(touch) = state.touches.iter_mut().find(|t| t.id == *id) {
291 touch.delta = canvas_pos - touch.position;
292 touch.position = canvas_pos;
293 touch.phase = TouchPhase::Moved;
294 }
295 }
296 }
297 "TouchEnd" => {
298 if let BrowserEventData::Touch { id, .. } = &event.data {
299 if let Some(touch) = state.touches.iter_mut().find(|t| t.id == *id) {
300 touch.phase = TouchPhase::Ended;
301 }
302 }
303 }
304 "TouchCancel" => {
305 if let BrowserEventData::Touch { id, .. } = &event.data {
306 if let Some(touch) = state.touches.iter_mut().find(|t| t.id == *id) {
307 touch.phase = TouchPhase::Cancelled;
308 }
309 }
310 }
311 "GamepadButtonDown" => {
312 if let BrowserEventData::GamepadButton { gamepad, button } = &event.data {
313 let gp_idx = *gamepad as usize;
314 if gp_idx < state.gamepads.len() {
315 if let Some(btn) = translate_gamepad_button(*button) {
316 let btn_idx = btn as usize;
317 if btn_idx < state.gamepads[gp_idx].buttons.len() {
318 state.gamepads[gp_idx].buttons[btn_idx] = ButtonState::JustPressed;
319 state.gamepads[gp_idx].connected = true;
320 }
321 }
322 }
323 }
324 }
325 "GamepadButtonUp" => {
326 if let BrowserEventData::GamepadButton { gamepad, button } = &event.data {
327 let gp_idx = *gamepad as usize;
328 if gp_idx < state.gamepads.len() {
329 if let Some(btn) = translate_gamepad_button(*button) {
330 let btn_idx = btn as usize;
331 if btn_idx < state.gamepads[gp_idx].buttons.len() {
332 state.gamepads[gp_idx].buttons[btn_idx] = ButtonState::JustReleased;
333 }
334 }
335 }
336 }
337 }
338 "GamepadAxisMove" => {
339 if let BrowserEventData::GamepadAxis {
340 gamepad,
341 axis,
342 value,
343 } = &event.data
344 {
345 let gp_idx = *gamepad as usize;
346 if gp_idx < state.gamepads.len() {
347 if let Some(ax) = translate_gamepad_axis(*axis) {
348 let ax_idx = ax as usize;
349 if ax_idx < state.gamepads[gp_idx].axes.len() {
350 state.gamepads[gp_idx].axes[ax_idx] = *value;
351 state.gamepads[gp_idx].connected = true;
352 }
353 }
354 }
355 }
356 }
357 "GamepadConnected" => {
358 if let BrowserEventData::GamepadButton { gamepad, .. } = &event.data {
359 let gp_idx = *gamepad as usize;
360 if gp_idx < state.gamepads.len() {
361 state.gamepads[gp_idx].connected = true;
362 }
363 }
364 }
365 "GamepadDisconnected" => {
366 if let BrowserEventData::GamepadButton { gamepad, .. } = &event.data {
367 let gp_idx = *gamepad as usize;
368 if gp_idx < state.gamepads.len() {
369 state.gamepads[gp_idx].connected = false;
370 }
371 }
372 }
373 unknown => {
374 return Err(InputTranslationError::UnknownEventType(unknown.to_string()));
375 }
376 }
377
378 Ok(())
379}
380
381const fn mouse_button_index(button: u8) -> usize {
383 match button {
384 0 => 0, 1 => 2, 2 => 1, n => (n as usize).saturating_sub(3).saturating_add(3),
388 }
389}
390
391#[cfg(test)]
392#[allow(clippy::unwrap_used, clippy::expect_used)]
393mod tests {
394 use super::*;
395
396 #[test]
399 fn test_translate_arrow_keys() {
400 assert_eq!(translate_key("ArrowUp"), Some(KeyCode::Up));
401 assert_eq!(translate_key("ArrowDown"), Some(KeyCode::Down));
402 assert_eq!(translate_key("ArrowLeft"), Some(KeyCode::Left));
403 assert_eq!(translate_key("ArrowRight"), Some(KeyCode::Right));
404 }
405
406 #[test]
407 fn test_translate_special_keys() {
408 assert_eq!(translate_key("Space"), Some(KeyCode::Space));
409 assert_eq!(translate_key("Enter"), Some(KeyCode::Enter));
410 assert_eq!(translate_key("Escape"), Some(KeyCode::Escape));
411 }
412
413 #[test]
414 fn test_translate_letter_keys() {
415 assert_eq!(translate_key("KeyA"), Some(KeyCode::Letter('A')));
416 assert_eq!(translate_key("KeyW"), Some(KeyCode::Letter('W')));
417 assert_eq!(translate_key("KeyS"), Some(KeyCode::Letter('S')));
418 assert_eq!(translate_key("KeyD"), Some(KeyCode::Letter('D')));
419 assert_eq!(translate_key("KeyZ"), Some(KeyCode::Letter('Z')));
420 }
421
422 #[test]
423 fn test_translate_number_keys() {
424 assert_eq!(translate_key("Digit0"), Some(KeyCode::Number(0)));
425 assert_eq!(translate_key("Digit1"), Some(KeyCode::Number(1)));
426 assert_eq!(translate_key("Digit9"), Some(KeyCode::Number(9)));
427 }
428
429 #[test]
430 fn test_translate_function_keys() {
431 assert_eq!(translate_key("F1"), Some(KeyCode::Function(1)));
432 assert_eq!(translate_key("F5"), Some(KeyCode::Function(5)));
433 assert_eq!(translate_key("F12"), Some(KeyCode::Function(12)));
434 }
435
436 #[test]
437 fn test_translate_unknown_key() {
438 assert_eq!(translate_key("Unknown"), None);
439 assert_eq!(translate_key(""), None);
440 assert_eq!(translate_key("Key"), None); assert_eq!(translate_key("KeyAB"), None); assert_eq!(translate_key("F13"), None); assert_eq!(translate_key("F0"), None); }
445
446 #[test]
449 fn test_translate_mouse_buttons() {
450 assert_eq!(translate_mouse_button(0), MouseButton::Left);
451 assert_eq!(translate_mouse_button(1), MouseButton::Middle);
452 assert_eq!(translate_mouse_button(2), MouseButton::Right);
453 assert_eq!(translate_mouse_button(3), MouseButton::Extra(0));
454 assert_eq!(translate_mouse_button(4), MouseButton::Extra(1));
455 }
456
457 #[test]
460 fn test_translate_gamepad_buttons() {
461 assert_eq!(translate_gamepad_button(0), Some(GamepadButton::South));
462 assert_eq!(translate_gamepad_button(1), Some(GamepadButton::East));
463 assert_eq!(translate_gamepad_button(2), Some(GamepadButton::West));
464 assert_eq!(translate_gamepad_button(3), Some(GamepadButton::North));
465 assert_eq!(translate_gamepad_button(4), Some(GamepadButton::LeftBumper));
466 assert_eq!(
467 translate_gamepad_button(5),
468 Some(GamepadButton::RightBumper)
469 );
470 assert_eq!(translate_gamepad_button(8), Some(GamepadButton::Select));
471 assert_eq!(translate_gamepad_button(9), Some(GamepadButton::Start));
472 assert_eq!(translate_gamepad_button(12), Some(GamepadButton::DPadUp));
473 assert_eq!(translate_gamepad_button(13), Some(GamepadButton::DPadDown));
474 assert_eq!(translate_gamepad_button(14), Some(GamepadButton::DPadLeft));
475 assert_eq!(translate_gamepad_button(15), Some(GamepadButton::DPadRight));
476 }
477
478 #[test]
479 fn test_translate_gamepad_button_invalid() {
480 assert_eq!(translate_gamepad_button(6), None);
481 assert_eq!(translate_gamepad_button(7), None);
482 assert_eq!(translate_gamepad_button(16), None);
483 assert_eq!(translate_gamepad_button(255), None);
484 }
485
486 #[test]
489 fn test_translate_gamepad_axes() {
490 assert_eq!(translate_gamepad_axis(0), Some(GamepadAxis::LeftStickX));
491 assert_eq!(translate_gamepad_axis(1), Some(GamepadAxis::LeftStickY));
492 assert_eq!(translate_gamepad_axis(2), Some(GamepadAxis::RightStickX));
493 assert_eq!(translate_gamepad_axis(3), Some(GamepadAxis::RightStickY));
494 }
495
496 #[test]
497 fn test_translate_gamepad_axis_invalid() {
498 assert_eq!(translate_gamepad_axis(4), None);
499 assert_eq!(translate_gamepad_axis(255), None);
500 }
501
502 #[test]
505 fn test_process_empty_events() {
506 let mut state = InputState::new();
507 assert!(process_input_events("", &mut state, Vec2::ZERO).is_ok());
508 assert!(process_input_events("[]", &mut state, Vec2::ZERO).is_ok());
509 }
510
511 #[test]
512 fn test_process_key_down() {
513 let mut state = InputState::new();
514 let events = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"Space"}}]"#;
515
516 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
517 assert!(state.key(KeyCode::Space).just_pressed());
518 }
519
520 #[test]
521 fn test_process_key_up() {
522 let mut state = InputState::new();
523 state.set_key(KeyCode::Space, ButtonState::Pressed);
524
525 let events = r#"[{"event_type":"KeyUp","timestamp":0,"data":{"key":"Space"}}]"#;
526
527 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
528 assert!(state.key(KeyCode::Space).just_released());
529 }
530
531 #[test]
532 fn test_process_mouse_move() {
533 let mut state = InputState::new();
534 let events = r#"[{"event_type":"MouseMove","timestamp":0,"data":{"x":100.0,"y":200.0}}]"#;
535
536 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
537 assert!((state.mouse_position.x - 100.0).abs() < f32::EPSILON);
538 assert!((state.mouse_position.y - 200.0).abs() < f32::EPSILON);
539 }
540
541 #[test]
542 fn test_process_mouse_down() {
543 let mut state = InputState::new();
544 let events =
545 r#"[{"event_type":"MouseDown","timestamp":0,"data":{"button":0,"x":50.0,"y":60.0}}]"#;
546
547 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
548 assert!(state.mouse_button(MouseButton::Left).just_pressed());
549 assert!((state.mouse_position.x - 50.0).abs() < f32::EPSILON);
550 }
551
552 #[test]
553 fn test_process_mouse_up() {
554 let mut state = InputState::new();
555 state.mouse_buttons[0] = ButtonState::Pressed;
556
557 let events =
558 r#"[{"event_type":"MouseUp","timestamp":0,"data":{"button":0,"x":50.0,"y":60.0}}]"#;
559
560 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
561 assert!(state.mouse_button(MouseButton::Left).just_released());
562 }
563
564 #[test]
565 fn test_process_touch_start() {
566 let mut state = InputState::new();
567 let events =
568 r#"[{"event_type":"TouchStart","timestamp":0,"data":{"id":1,"x":100.0,"y":200.0}}]"#;
569
570 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
571 assert_eq!(state.touches.len(), 1);
572 assert_eq!(state.touches[0].phase, TouchPhase::Started);
574 }
575
576 #[test]
577 fn test_process_touch_move() {
578 let mut state = InputState::new();
579 state
581 .touches
582 .push(TouchEvent::new(Vec2::new(100.0, 200.0)).with_id(1));
583
584 let events =
585 r#"[{"event_type":"TouchMove","timestamp":0,"data":{"id":1,"x":150.0,"y":250.0}}]"#;
586
587 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
588 assert_eq!(state.touches[0].phase, TouchPhase::Moved);
590 assert!((state.touches[0].position.x - 150.0).abs() < f32::EPSILON);
591 }
592
593 #[test]
594 fn test_process_touch_end() {
595 let mut state = InputState::new();
596 state
597 .touches
598 .push(TouchEvent::new(Vec2::new(100.0, 200.0)).with_id(1));
599
600 let events = r#"[{"event_type":"TouchEnd","timestamp":0,"data":{"id":1,"x":0.0,"y":0.0}}]"#;
601
602 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
603 assert_eq!(state.touches[0].phase, TouchPhase::Ended);
604 }
605
606 #[test]
607 fn test_process_gamepad_button() {
608 let mut state = InputState::new();
609 let events =
610 r#"[{"event_type":"GamepadButtonDown","timestamp":0,"data":{"gamepad":0,"button":0}}]"#;
611
612 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
613 assert!(state.gamepads[0].connected);
614 assert!(state.gamepads[0]
615 .button(GamepadButton::South)
616 .just_pressed());
617 }
618
619 #[test]
620 fn test_process_gamepad_axis() {
621 let mut state = InputState::new();
622 let events = r#"[{"event_type":"GamepadAxisMove","timestamp":0,"data":{"gamepad":0,"axis":0,"value":0.75}}]"#;
623
624 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
625 assert!(state.gamepads[0].connected);
626 assert!((state.gamepads[0].axis(GamepadAxis::LeftStickX) - 0.75).abs() < f32::EPSILON);
627 }
628
629 #[test]
630 fn test_process_multiple_events() {
631 let mut state = InputState::new();
632 let events = r#"[
633 {"event_type":"KeyDown","timestamp":0,"data":{"key":"KeyW"}},
634 {"event_type":"KeyDown","timestamp":1,"data":{"key":"Space"}},
635 {"event_type":"MouseMove","timestamp":2,"data":{"x":400.0,"y":300.0}}
636 ]"#;
637
638 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
639 assert!(state.key(KeyCode::Letter('W')).just_pressed());
640 assert!(state.key(KeyCode::Space).just_pressed());
641 assert!((state.mouse_position.x - 400.0).abs() < f32::EPSILON);
642 }
643
644 #[test]
645 fn test_process_invalid_json() {
646 let mut state = InputState::new();
647 let result = process_input_events("not json", &mut state, Vec2::ZERO);
648 assert!(matches!(result, Err(InputTranslationError::InvalidJson(_))));
649 }
650
651 #[test]
652 fn test_process_unknown_event_type() {
653 let mut state = InputState::new();
654 let events = r#"[{"event_type":"Unknown","timestamp":0,"data":{"key":"Space"}}]"#;
655 let result = process_input_events(events, &mut state, Vec2::ZERO);
656 assert!(matches!(
657 result,
658 Err(InputTranslationError::UnknownEventType(_))
659 ));
660 }
661
662 #[test]
665 fn test_canvas_offset_mouse_move() {
666 let mut state = InputState::new();
667 let canvas_offset = Vec2::new(100.0, 50.0);
669 let events = r#"[{"event_type":"MouseMove","timestamp":0,"data":{"x":200.0,"y":150.0}}]"#;
671
672 assert!(process_input_events(events, &mut state, canvas_offset).is_ok());
673 assert!((state.mouse_position.x - 100.0).abs() < f32::EPSILON);
675 assert!((state.mouse_position.y - 100.0).abs() < f32::EPSILON);
676 }
677
678 #[test]
679 fn test_canvas_offset_mouse_down() {
680 let mut state = InputState::new();
681 let canvas_offset = Vec2::new(50.0, 25.0);
682 let events =
684 r#"[{"event_type":"MouseDown","timestamp":0,"data":{"button":0,"x":150.0,"y":125.0}}]"#;
685
686 assert!(process_input_events(events, &mut state, canvas_offset).is_ok());
687 assert!((state.mouse_position.x - 100.0).abs() < f32::EPSILON);
689 assert!((state.mouse_position.y - 100.0).abs() < f32::EPSILON);
690 }
691
692 #[test]
693 fn test_canvas_offset_touch_start() {
694 let mut state = InputState::new();
695 let canvas_offset = Vec2::new(30.0, 40.0);
696 let events =
698 r#"[{"event_type":"TouchStart","timestamp":0,"data":{"id":1,"x":130.0,"y":140.0}}]"#;
699
700 assert!(process_input_events(events, &mut state, canvas_offset).is_ok());
701 assert_eq!(state.touches.len(), 1);
702 assert!((state.touches[0].position.x - 100.0).abs() < f32::EPSILON);
704 assert!((state.touches[0].position.y - 100.0).abs() < f32::EPSILON);
705 }
706
707 #[test]
708 fn test_canvas_offset_touch_move() {
709 let mut state = InputState::new();
710 state
711 .touches
712 .push(TouchEvent::new(Vec2::new(100.0, 100.0)).with_id(1));
713
714 let canvas_offset = Vec2::new(20.0, 10.0);
715 let events =
717 r#"[{"event_type":"TouchMove","timestamp":0,"data":{"id":1,"x":170.0,"y":160.0}}]"#;
718
719 assert!(process_input_events(events, &mut state, canvas_offset).is_ok());
720 assert!((state.touches[0].position.x - 150.0).abs() < f32::EPSILON);
721 assert!((state.touches[0].position.y - 150.0).abs() < f32::EPSILON);
722 }
723
724 #[test]
727 fn test_error_display() {
728 let err = InputTranslationError::InvalidJson("test".to_string());
729 assert!(format!("{err}").contains("test"));
730
731 let err = InputTranslationError::UnknownEventType("foo".to_string());
732 assert!(format!("{err}").contains("foo"));
733
734 let err = InputTranslationError::InvalidData("bar".to_string());
735 assert!(format!("{err}").contains("bar"));
736 }
737
738 #[test]
741 fn test_mouse_button_index() {
742 assert_eq!(mouse_button_index(0), 0); assert_eq!(mouse_button_index(1), 2); assert_eq!(mouse_button_index(2), 1); assert_eq!(mouse_button_index(3), 3); }
747
748 #[test]
751 fn test_translate_key_lowercase_letter_returns_none() {
752 assert!(translate_key("Keya").is_none());
754 assert!(translate_key("Keyz").is_none());
755 }
756
757 #[test]
758 fn test_translate_gamepad_button_stick_buttons() {
759 assert_eq!(translate_gamepad_button(10), Some(GamepadButton::LeftStick));
761 assert_eq!(
762 translate_gamepad_button(11),
763 Some(GamepadButton::RightStick)
764 );
765 }
766
767 #[test]
768 fn test_key_down_unknown_key_does_not_set_state() {
769 let mut state = InputState::new();
771 let events = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"UnknownKey"}}]"#;
772
773 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
774 assert!(!state.key(KeyCode::Space).is_down());
776 }
777
778 #[test]
779 fn test_key_down_already_pressed_no_double_just_pressed() {
780 let mut state = InputState::new();
782 state.set_key(KeyCode::Space, ButtonState::Pressed);
783
784 let events = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"Space"}}]"#;
785 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
786
787 assert!(state.key(KeyCode::Space).is_down());
789 }
790
791 #[test]
794 fn test_touch_end_sets_ended_phase() {
795 let mut state = InputState::new();
796 state
797 .touches
798 .push(TouchEvent::new(Vec2::new(100.0, 100.0)).with_id(5));
799
800 let events =
801 r#"[{"event_type":"TouchEnd","timestamp":0,"data":{"id":5,"x":100.0,"y":100.0}}]"#;
802 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
803
804 assert_eq!(state.touches[0].phase, TouchPhase::Ended);
805 }
806
807 #[test]
808 fn test_touch_cancel_sets_cancelled_phase() {
809 let mut state = InputState::new();
810 state
811 .touches
812 .push(TouchEvent::new(Vec2::new(100.0, 100.0)).with_id(7));
813
814 let events =
815 r#"[{"event_type":"TouchCancel","timestamp":0,"data":{"id":7,"x":100.0,"y":100.0}}]"#;
816 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
817
818 assert_eq!(state.touches[0].phase, TouchPhase::Cancelled);
819 }
820
821 #[test]
824 fn test_mouse_up_event() {
825 let mut state = InputState::new();
826 state.mouse_buttons[0] = ButtonState::Pressed;
827
828 let events =
829 r#"[{"event_type":"MouseUp","timestamp":0,"data":{"button":0,"x":150.0,"y":200.0}}]"#;
830 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
831
832 assert_eq!(state.mouse_buttons[0], ButtonState::JustReleased);
833 assert!((state.mouse_position.x - 150.0).abs() < f32::EPSILON);
834 assert!((state.mouse_position.y - 200.0).abs() < f32::EPSILON);
835 }
836
837 #[test]
840 fn test_gamepad_connected_event() {
841 let mut state = InputState::new();
842 assert!(!state.gamepads[0].connected);
843
844 let events =
845 r#"[{"event_type":"GamepadConnected","timestamp":0,"data":{"gamepad":0,"button":0}}]"#;
846 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
847
848 assert!(state.gamepads[0].connected);
849 }
850
851 #[test]
852 fn test_gamepad_disconnected_event() {
853 let mut state = InputState::new();
854 state.gamepads[0].connected = true;
855
856 let events = r#"[{"event_type":"GamepadDisconnected","timestamp":0,"data":{"gamepad":0,"button":0}}]"#;
857 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
858
859 assert!(!state.gamepads[0].connected);
860 }
861
862 #[test]
863 fn test_gamepad_button_up_event() {
864 let mut state = InputState::new();
865 state.gamepads[0].buttons[0] = ButtonState::Pressed;
866
867 let events =
868 r#"[{"event_type":"GamepadButtonUp","timestamp":0,"data":{"gamepad":0,"button":0}}]"#;
869 assert!(process_input_events(events, &mut state, Vec2::ZERO).is_ok());
870
871 assert_eq!(state.gamepads[0].buttons[0], ButtonState::JustReleased);
872 }
873}
874
875#[cfg(test)]
878#[allow(clippy::uninlined_format_args)]
879mod property_tests {
880 use super::*;
881 use proptest::prelude::*;
882
883 proptest! {
884 #[test]
886 fn property_canvas_offset_linear(
887 viewport_x in 0.0f32..2000.0,
888 viewport_y in 0.0f32..2000.0,
889 offset_x in 0.0f32..500.0,
890 offset_y in 0.0f32..500.0,
891 ) {
892 let mut state = InputState::new();
893 let canvas_offset = Vec2::new(offset_x, offset_y);
894 let events = format!(
895 r#"[{{"event_type":"MouseMove","timestamp":0,"data":{{"x":{},"y":{}}}}}]"#,
896 viewport_x, viewport_y
897 );
898
899 let result = process_input_events(&events, &mut state, canvas_offset);
900 prop_assert!(result.is_ok());
901
902 let expected_x = viewport_x - offset_x;
904 let expected_y = viewport_y - offset_y;
905 prop_assert!((state.mouse_position.x - expected_x).abs() < 0.001);
906 prop_assert!((state.mouse_position.y - expected_y).abs() < 0.001);
907 }
908
909 #[test]
911 fn property_zero_offset_identity(
912 x in 0.0f32..2000.0,
913 y in 0.0f32..2000.0,
914 ) {
915 let mut state = InputState::new();
916 let events = format!(
917 r#"[{{"event_type":"MouseDown","timestamp":0,"data":{{"button":0,"x":{},"y":{}}}}}]"#,
918 x, y
919 );
920
921 let result = process_input_events(&events, &mut state, Vec2::ZERO);
922 prop_assert!(result.is_ok());
923 prop_assert!((state.mouse_position.x - x).abs() < 0.001);
924 prop_assert!((state.mouse_position.y - y).abs() < 0.001);
925 }
926
927 #[test]
929 fn property_touch_offset_applied(
930 viewport_x in 0.0f32..2000.0,
931 viewport_y in 0.0f32..2000.0,
932 offset_x in 0.0f32..500.0,
933 offset_y in 0.0f32..500.0,
934 touch_id in 0u32..100,
935 ) {
936 let mut state = InputState::new();
937 let canvas_offset = Vec2::new(offset_x, offset_y);
938 let events = format!(
939 r#"[{{"event_type":"TouchStart","timestamp":0,"data":{{"id":{},"x":{},"y":{}}}}}]"#,
940 touch_id, viewport_x, viewport_y
941 );
942
943 let result = process_input_events(&events, &mut state, canvas_offset);
944 prop_assert!(result.is_ok());
945 prop_assert_eq!(state.touches.len(), 1);
946
947 let expected_x = viewport_x - offset_x;
948 let expected_y = viewport_y - offset_y;
949 prop_assert!((state.touches[0].position.x - expected_x).abs() < 0.001);
950 prop_assert!((state.touches[0].position.y - expected_y).abs() < 0.001);
951 }
952
953 #[test]
955 fn property_negative_coords_valid(
956 viewport_x in 0.0f32..100.0,
957 viewport_y in 0.0f32..100.0,
958 offset_x in 100.0f32..500.0,
959 offset_y in 100.0f32..500.0,
960 ) {
961 let mut state = InputState::new();
962 let canvas_offset = Vec2::new(offset_x, offset_y);
963 let events = format!(
964 r#"[{{"event_type":"MouseMove","timestamp":0,"data":{{"x":{},"y":{}}}}}]"#,
965 viewport_x, viewport_y
966 );
967
968 let result = process_input_events(&events, &mut state, canvas_offset);
969 prop_assert!(result.is_ok());
970
971 prop_assert!(state.mouse_position.x < 0.0);
973 prop_assert!(state.mouse_position.y < 0.0);
974 }
975
976 #[test]
978 fn property_multiple_events_consistent_offset(
979 x1 in 0.0f32..1000.0,
980 y1 in 0.0f32..1000.0,
981 x2 in 0.0f32..1000.0,
982 y2 in 0.0f32..1000.0,
983 offset_x in 0.0f32..200.0,
984 offset_y in 0.0f32..200.0,
985 ) {
986 let mut state = InputState::new();
987 let canvas_offset = Vec2::new(offset_x, offset_y);
988 let events = format!(
989 r#"[
990 {{"event_type":"MouseDown","timestamp":0,"data":{{"button":0,"x":{},"y":{}}}}},
991 {{"event_type":"MouseMove","timestamp":1,"data":{{"x":{},"y":{}}}}}
992 ]"#,
993 x1, y1, x2, y2
994 );
995
996 let result = process_input_events(&events, &mut state, canvas_offset);
997 prop_assert!(result.is_ok());
998
999 let expected_x = x2 - offset_x;
1001 let expected_y = y2 - offset_y;
1002 prop_assert!((state.mouse_position.x - expected_x).abs() < 0.001);
1003 prop_assert!((state.mouse_position.y - expected_y).abs() < 0.001);
1004 }
1005 }
1006}