Skip to main content

jugar_input/
lib.rs

1//! # jugar-input
2//!
3//! Unified input handling for touch, mouse, and gamepad.
4
5#![forbid(unsafe_code)]
6#![warn(missing_docs)]
7
8use glam::Vec2;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12/// Input errors
13#[derive(Error, Debug, Clone, PartialEq, Eq)]
14pub enum InputError {
15    /// Invalid gamepad index
16    #[error("Gamepad {0} not connected")]
17    GamepadNotConnected(u32),
18}
19
20/// Result type for input operations
21pub type Result<T> = core::result::Result<T, InputError>;
22
23/// Input device type
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum InputDevice {
26    /// Touch screen
27    Touch,
28    /// Mouse
29    Mouse,
30    /// Keyboard
31    Keyboard,
32    /// Gamepad/Controller
33    Gamepad(u32),
34}
35
36/// Touch/Mouse button state
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
38pub enum ButtonState {
39    /// Not pressed
40    #[default]
41    Released,
42    /// Just pressed this frame
43    JustPressed,
44    /// Held down
45    Pressed,
46    /// Just released this frame
47    JustReleased,
48}
49
50impl ButtonState {
51    /// Returns true if the button is currently down
52    #[must_use]
53    pub const fn is_down(self) -> bool {
54        matches!(self, Self::JustPressed | Self::Pressed)
55    }
56
57    /// Returns true if the button was just pressed
58    #[must_use]
59    pub const fn just_pressed(self) -> bool {
60        matches!(self, Self::JustPressed)
61    }
62
63    /// Returns true if the button was just released
64    #[must_use]
65    pub const fn just_released(self) -> bool {
66        matches!(self, Self::JustReleased)
67    }
68
69    /// Advances the state after a frame
70    #[must_use]
71    pub const fn advance(self) -> Self {
72        match self {
73            Self::JustPressed => Self::Pressed,
74            Self::JustReleased => Self::Released,
75            other => other,
76        }
77    }
78}
79
80/// Mouse button identifier
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub enum MouseButton {
83    /// Left mouse button
84    Left,
85    /// Right mouse button
86    Right,
87    /// Middle mouse button
88    Middle,
89    /// Extra button (index)
90    Extra(u8),
91}
92
93/// Touch event
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct TouchEvent {
96    /// Touch identifier
97    pub id: u32,
98    /// Position in screen coordinates
99    pub position: Vec2,
100    /// Change in position since last event
101    pub delta: Vec2,
102    /// Touch phase
103    pub phase: TouchPhase,
104    /// Pressure (0.0 to 1.0, if available)
105    pub pressure: f32,
106}
107
108impl TouchEvent {
109    /// Creates a new touch event
110    #[must_use]
111    pub const fn new(position: Vec2) -> Self {
112        Self {
113            id: 0,
114            position,
115            delta: Vec2::ZERO,
116            phase: TouchPhase::Started,
117            pressure: 1.0,
118        }
119    }
120
121    /// Sets the touch ID
122    #[must_use]
123    pub const fn with_id(mut self, id: u32) -> Self {
124        self.id = id;
125        self
126    }
127
128    /// Sets the phase
129    #[must_use]
130    pub const fn with_phase(mut self, phase: TouchPhase) -> Self {
131        self.phase = phase;
132        self
133    }
134}
135
136/// Touch phase
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub enum TouchPhase {
139    /// Touch just started
140    Started,
141    /// Touch moved
142    Moved,
143    /// Touch ended normally
144    Ended,
145    /// Touch was cancelled
146    Cancelled,
147}
148
149/// Keyboard key code
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
151pub enum KeyCode {
152    /// Arrow keys
153    Up,
154    /// Arrow down
155    Down,
156    /// Arrow left
157    Left,
158    /// Arrow right
159    Right,
160    /// Space bar
161    Space,
162    /// Enter/Return
163    Enter,
164    /// Escape
165    Escape,
166    /// Letter keys (A-Z)
167    Letter(char),
168    /// Number keys (0-9)
169    Number(u8),
170    /// Function keys (F1-F12)
171    Function(u8),
172}
173
174/// Gamepad button
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
176pub enum GamepadButton {
177    /// A button (Xbox) / Cross (`PlayStation`)
178    South,
179    /// B button (Xbox) / Circle (`PlayStation`)
180    East,
181    /// X button (Xbox) / Square (`PlayStation`)
182    West,
183    /// Y button (Xbox) / Triangle (`PlayStation`)
184    North,
185    /// Left bumper/shoulder
186    LeftBumper,
187    /// Right bumper/shoulder
188    RightBumper,
189    /// Left stick press
190    LeftStick,
191    /// Right stick press
192    RightStick,
193    /// Start/Options
194    Start,
195    /// Select/Share
196    Select,
197    /// D-pad up
198    DPadUp,
199    /// D-pad down
200    DPadDown,
201    /// D-pad left
202    DPadLeft,
203    /// D-pad right
204    DPadRight,
205}
206
207/// Gamepad axis
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
209pub enum GamepadAxis {
210    /// Left stick X
211    LeftStickX,
212    /// Left stick Y
213    LeftStickY,
214    /// Right stick X
215    RightStickX,
216    /// Right stick Y
217    RightStickY,
218    /// Left trigger
219    LeftTrigger,
220    /// Right trigger
221    RightTrigger,
222}
223
224/// Gamepad state
225#[derive(Debug, Clone, Default)]
226pub struct GamepadState {
227    /// Connected status
228    pub connected: bool,
229    /// Button states
230    pub buttons: [ButtonState; 14],
231    /// Axis values (-1.0 to 1.0 for sticks, 0.0 to 1.0 for triggers)
232    pub axes: [f32; 6],
233}
234
235impl GamepadState {
236    /// Gets a button state
237    #[must_use]
238    pub const fn button(&self, button: GamepadButton) -> ButtonState {
239        let idx = button as usize;
240        if idx < self.buttons.len() {
241            self.buttons[idx]
242        } else {
243            ButtonState::Released
244        }
245    }
246
247    /// Gets an axis value
248    #[must_use]
249    pub const fn axis(&self, axis: GamepadAxis) -> f32 {
250        let idx = axis as usize;
251        if idx < self.axes.len() {
252            self.axes[idx]
253        } else {
254            0.0
255        }
256    }
257
258    /// Gets left stick as a Vec2
259    #[must_use]
260    pub const fn left_stick(&self) -> Vec2 {
261        Vec2::new(
262            self.axis(GamepadAxis::LeftStickX),
263            self.axis(GamepadAxis::LeftStickY),
264        )
265    }
266
267    /// Gets right stick as a Vec2
268    #[must_use]
269    pub const fn right_stick(&self) -> Vec2 {
270        Vec2::new(
271            self.axis(GamepadAxis::RightStickX),
272            self.axis(GamepadAxis::RightStickY),
273        )
274    }
275}
276
277/// Unified input state manager
278#[derive(Debug, Default)]
279pub struct InputState {
280    /// Mouse position
281    pub mouse_position: Vec2,
282    /// Mouse delta
283    pub mouse_delta: Vec2,
284    /// Mouse button states
285    pub mouse_buttons: [ButtonState; 5],
286    /// Active touches
287    pub touches: Vec<TouchEvent>,
288    /// Keyboard key states
289    keys: std::collections::HashMap<KeyCode, ButtonState>,
290    /// Gamepad states (up to 4)
291    pub gamepads: [GamepadState; 4],
292}
293
294impl InputState {
295    /// Creates a new input state
296    #[must_use]
297    pub fn new() -> Self {
298        Self::default()
299    }
300
301    /// Gets mouse button state
302    #[must_use]
303    pub const fn mouse_button(&self, button: MouseButton) -> ButtonState {
304        let idx = match button {
305            MouseButton::Left => 0,
306            MouseButton::Right => 1,
307            MouseButton::Middle => 2,
308            MouseButton::Extra(n) => 3 + n as usize,
309        };
310        if idx < self.mouse_buttons.len() {
311            self.mouse_buttons[idx]
312        } else {
313            ButtonState::Released
314        }
315    }
316
317    /// Gets key state
318    #[must_use]
319    pub fn key(&self, key: KeyCode) -> ButtonState {
320        self.keys
321            .get(&key)
322            .copied()
323            .unwrap_or(ButtonState::Released)
324    }
325
326    /// Sets key state
327    pub fn set_key(&mut self, key: KeyCode, state: ButtonState) {
328        let _ = self.keys.insert(key, state);
329    }
330
331    /// Gets primary touch (or mouse as touch)
332    #[must_use]
333    pub fn primary_pointer(&self) -> Option<Vec2> {
334        self.touches
335            .first()
336            .map(|touch| touch.position)
337            .or_else(|| {
338                if self.mouse_button(MouseButton::Left).is_down() {
339                    Some(self.mouse_position)
340                } else {
341                    None
342                }
343            })
344    }
345
346    /// Checks if there's any input this frame
347    #[must_use]
348    pub fn has_input(&self) -> bool {
349        !self.touches.is_empty()
350            || self.mouse_buttons.iter().any(|b| b.is_down())
351            || self.keys.values().any(|b| b.is_down())
352            || self.gamepads.iter().any(|g| g.connected)
353    }
354
355    /// Advances button states after a frame
356    pub fn advance_frame(&mut self) {
357        for button in &mut self.mouse_buttons {
358            *button = button.advance();
359        }
360        for state in self.keys.values_mut() {
361            *state = state.advance();
362        }
363        for gamepad in &mut self.gamepads {
364            for button in &mut gamepad.buttons {
365                *button = button.advance();
366            }
367        }
368        self.mouse_delta = Vec2::ZERO;
369    }
370
371    /// Clears all touches
372    pub fn clear_touches(&mut self) {
373        self.touches.clear();
374    }
375
376    /// Checks if a key is currently pressed (down)
377    #[must_use]
378    pub fn is_key_pressed(&self, key: KeyCode) -> bool {
379        self.key(key).is_down()
380    }
381
382    /// Sets a key as pressed or released
383    pub fn set_key_pressed(&mut self, key: KeyCode, pressed: bool) {
384        let state = if pressed {
385            ButtonState::Pressed
386        } else {
387            ButtonState::Released
388        };
389        self.set_key(key, state);
390    }
391
392    /// Clears transient input events (advances frame states, clears touches with ended/cancelled phase)
393    pub fn clear_events(&mut self) {
394        // Remove ended/cancelled touches
395        self.touches
396            .retain(|t| !matches!(t.phase, TouchPhase::Ended | TouchPhase::Cancelled));
397
398        // Advance button states (JustPressed -> Pressed, JustReleased -> Released)
399        self.advance_frame();
400    }
401}
402
403/// Action binding for input abstraction
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405pub struct InputAction {
406    /// Action name
407    pub name: String,
408    /// Bound keys
409    pub keys: Vec<KeyCode>,
410    /// Bound mouse buttons
411    pub mouse_buttons: Vec<MouseButton>,
412    /// Bound gamepad buttons
413    pub gamepad_buttons: Vec<GamepadButton>,
414}
415
416impl InputAction {
417    /// Creates a new action
418    #[must_use]
419    pub fn new(name: impl Into<String>) -> Self {
420        Self {
421            name: name.into(),
422            keys: Vec::new(),
423            mouse_buttons: Vec::new(),
424            gamepad_buttons: Vec::new(),
425        }
426    }
427
428    /// Adds a key binding
429    #[must_use]
430    pub fn with_key(mut self, key: KeyCode) -> Self {
431        self.keys.push(key);
432        self
433    }
434
435    /// Adds a mouse button binding
436    #[must_use]
437    pub fn with_mouse_button(mut self, button: MouseButton) -> Self {
438        self.mouse_buttons.push(button);
439        self
440    }
441
442    /// Adds a gamepad button binding
443    #[must_use]
444    pub fn with_gamepad_button(mut self, button: GamepadButton) -> Self {
445        self.gamepad_buttons.push(button);
446        self
447    }
448
449    /// Checks if the action is active
450    #[must_use]
451    pub fn is_active(&self, input: &InputState) -> bool {
452        self.keys.iter().any(|k| input.key(*k).is_down())
453            || self
454                .mouse_buttons
455                .iter()
456                .any(|b| input.mouse_button(*b).is_down())
457            || self.gamepad_buttons.iter().any(|b| {
458                input
459                    .gamepads
460                    .iter()
461                    .any(|g| g.connected && g.button(*b).is_down())
462            })
463    }
464}
465
466#[cfg(test)]
467#[allow(clippy::unwrap_used, clippy::expect_used)]
468mod tests {
469    use super::*;
470
471    // ==================== BUTTON STATE TESTS ====================
472
473    #[test]
474    fn test_button_state_advance() {
475        assert_eq!(ButtonState::JustPressed.advance(), ButtonState::Pressed);
476        assert_eq!(ButtonState::JustReleased.advance(), ButtonState::Released);
477        assert_eq!(ButtonState::Pressed.advance(), ButtonState::Pressed);
478        assert_eq!(ButtonState::Released.advance(), ButtonState::Released);
479    }
480
481    #[test]
482    fn test_button_state_queries() {
483        assert!(ButtonState::JustPressed.is_down());
484        assert!(ButtonState::Pressed.is_down());
485        assert!(!ButtonState::Released.is_down());
486        assert!(!ButtonState::JustReleased.is_down());
487        assert!(ButtonState::JustPressed.just_pressed());
488        assert!(!ButtonState::Pressed.just_pressed());
489        assert!(ButtonState::JustReleased.just_released());
490        assert!(!ButtonState::Released.just_released());
491    }
492
493    #[test]
494    fn test_button_state_default() {
495        let state = ButtonState::default();
496        assert_eq!(state, ButtonState::Released);
497    }
498
499    // ==================== TOUCH EVENT TESTS ====================
500
501    #[test]
502    fn test_touch_event() {
503        let touch = TouchEvent::new(Vec2::new(100.0, 200.0))
504            .with_id(1)
505            .with_phase(TouchPhase::Moved);
506
507        assert_eq!(touch.id, 1);
508        assert_eq!(touch.phase, TouchPhase::Moved);
509        assert!((touch.position.x - 100.0).abs() < f32::EPSILON);
510        assert!((touch.position.y - 200.0).abs() < f32::EPSILON);
511        assert!((touch.pressure - 1.0).abs() < f32::EPSILON);
512        assert_eq!(touch.delta, Vec2::ZERO);
513    }
514
515    #[test]
516    fn test_touch_event_defaults() {
517        let touch = TouchEvent::new(Vec2::new(50.0, 50.0));
518        assert_eq!(touch.id, 0);
519        assert_eq!(touch.phase, TouchPhase::Started);
520    }
521
522    // ==================== INPUT STATE TESTS ====================
523
524    #[test]
525    fn test_input_state_new() {
526        let state = InputState::new();
527        assert_eq!(state.mouse_position, Vec2::ZERO);
528        assert_eq!(state.mouse_delta, Vec2::ZERO);
529        assert!(state.touches.is_empty());
530    }
531
532    #[test]
533    fn test_input_state_mouse() {
534        let mut state = InputState::new();
535        state.mouse_position = Vec2::new(100.0, 200.0);
536        state.mouse_buttons[0] = ButtonState::JustPressed;
537
538        assert!(state.mouse_button(MouseButton::Left).is_down());
539        assert!(!state.mouse_button(MouseButton::Right).is_down());
540        assert!(!state.mouse_button(MouseButton::Middle).is_down());
541    }
542
543    #[test]
544    fn test_input_state_mouse_extra_buttons() {
545        let mut state = InputState::new();
546        state.mouse_buttons[3] = ButtonState::Pressed;
547        state.mouse_buttons[4] = ButtonState::Pressed;
548
549        assert!(state.mouse_button(MouseButton::Extra(0)).is_down());
550        assert!(state.mouse_button(MouseButton::Extra(1)).is_down());
551        // Out of range should return Released
552        assert!(!state.mouse_button(MouseButton::Extra(10)).is_down());
553    }
554
555    #[test]
556    fn test_input_state_keyboard() {
557        let mut state = InputState::new();
558        state.set_key(KeyCode::Space, ButtonState::JustPressed);
559
560        assert!(state.key(KeyCode::Space).just_pressed());
561        assert!(!state.key(KeyCode::Enter).is_down());
562    }
563
564    #[test]
565    fn test_input_state_advance() {
566        let mut state = InputState::new();
567        state.mouse_buttons[0] = ButtonState::JustPressed;
568        state.set_key(KeyCode::Space, ButtonState::JustPressed);
569        state.mouse_delta = Vec2::new(10.0, 20.0);
570
571        state.advance_frame();
572
573        assert_eq!(state.mouse_buttons[0], ButtonState::Pressed);
574        assert_eq!(state.key(KeyCode::Space), ButtonState::Pressed);
575        assert_eq!(state.mouse_delta, Vec2::ZERO);
576    }
577
578    #[test]
579    fn test_input_state_advance_gamepad() {
580        let mut state = InputState::new();
581        state.gamepads[0].connected = true;
582        state.gamepads[0].buttons[0] = ButtonState::JustPressed;
583
584        state.advance_frame();
585
586        assert_eq!(state.gamepads[0].buttons[0], ButtonState::Pressed);
587    }
588
589    #[test]
590    fn test_input_state_clear_touches() {
591        let mut state = InputState::new();
592        state.touches.push(TouchEvent::new(Vec2::new(50.0, 50.0)));
593        state.touches.push(TouchEvent::new(Vec2::new(100.0, 100.0)));
594
595        state.clear_touches();
596
597        assert!(state.touches.is_empty());
598    }
599
600    #[test]
601    fn test_input_state_has_input() {
602        let mut state = InputState::new();
603        assert!(!state.has_input());
604
605        state.mouse_buttons[0] = ButtonState::Pressed;
606        assert!(state.has_input());
607
608        state.mouse_buttons[0] = ButtonState::Released;
609        state.touches.push(TouchEvent::new(Vec2::ZERO));
610        assert!(state.has_input());
611    }
612
613    #[test]
614    fn test_input_state_has_input_gamepad() {
615        let mut state = InputState::new();
616        assert!(!state.has_input());
617
618        state.gamepads[0].connected = true;
619        assert!(state.has_input());
620    }
621
622    #[test]
623    fn test_input_state_has_input_keys() {
624        let mut state = InputState::new();
625        assert!(!state.has_input());
626
627        state.set_key(KeyCode::Space, ButtonState::Pressed);
628        assert!(state.has_input());
629    }
630
631    // ==================== PRIMARY POINTER TESTS ====================
632
633    #[test]
634    fn test_primary_pointer_touch() {
635        let mut state = InputState::new();
636        state.touches.push(TouchEvent::new(Vec2::new(50.0, 50.0)));
637
638        let pointer = state.primary_pointer();
639        assert_eq!(pointer, Some(Vec2::new(50.0, 50.0)));
640    }
641
642    #[test]
643    fn test_primary_pointer_mouse() {
644        let mut state = InputState::new();
645        state.mouse_position = Vec2::new(100.0, 100.0);
646        state.mouse_buttons[0] = ButtonState::Pressed;
647
648        let pointer = state.primary_pointer();
649        assert_eq!(pointer, Some(Vec2::new(100.0, 100.0)));
650    }
651
652    #[test]
653    fn test_primary_pointer_none() {
654        let state = InputState::new();
655        let pointer = state.primary_pointer();
656        assert_eq!(pointer, None);
657    }
658
659    #[test]
660    fn test_primary_pointer_touch_priority() {
661        let mut state = InputState::new();
662        state.mouse_position = Vec2::new(100.0, 100.0);
663        state.mouse_buttons[0] = ButtonState::Pressed;
664        state.touches.push(TouchEvent::new(Vec2::new(50.0, 50.0)));
665
666        // Touch should take priority over mouse
667        let pointer = state.primary_pointer();
668        assert_eq!(pointer, Some(Vec2::new(50.0, 50.0)));
669    }
670
671    // ==================== GAMEPAD STATE TESTS ====================
672
673    #[test]
674    fn test_gamepad_state() {
675        let mut gamepad = GamepadState::default();
676        gamepad.axes[0] = 0.5; // Left stick X
677        gamepad.axes[1] = -0.3; // Left stick Y
678
679        let stick = gamepad.left_stick();
680        assert!((stick.x - 0.5).abs() < f32::EPSILON);
681        assert!((stick.y - (-0.3)).abs() < f32::EPSILON);
682    }
683
684    #[test]
685    fn test_gamepad_right_stick() {
686        let mut gamepad = GamepadState::default();
687        gamepad.axes[2] = 0.7; // Right stick X
688        gamepad.axes[3] = 0.8; // Right stick Y
689
690        let stick = gamepad.right_stick();
691        assert!((stick.x - 0.7).abs() < f32::EPSILON);
692        assert!((stick.y - 0.8).abs() < f32::EPSILON);
693    }
694
695    #[test]
696    fn test_gamepad_button() {
697        let mut gamepad = GamepadState::default();
698        gamepad.buttons[0] = ButtonState::Pressed;
699
700        assert!(gamepad.button(GamepadButton::South).is_down());
701        assert!(!gamepad.button(GamepadButton::North).is_down());
702    }
703
704    #[test]
705    fn test_gamepad_axis() {
706        let mut gamepad = GamepadState::default();
707        gamepad.axes[4] = 0.5; // Left trigger
708        gamepad.axes[5] = 0.8; // Right trigger
709
710        assert!((gamepad.axis(GamepadAxis::LeftTrigger) - 0.5).abs() < f32::EPSILON);
711        assert!((gamepad.axis(GamepadAxis::RightTrigger) - 0.8).abs() < f32::EPSILON);
712    }
713
714    // ==================== INPUT ACTION TESTS ====================
715
716    #[test]
717    fn test_input_action() {
718        let action = InputAction::new("jump")
719            .with_key(KeyCode::Space)
720            .with_gamepad_button(GamepadButton::South);
721
722        let mut state = InputState::new();
723        assert!(!action.is_active(&state));
724
725        state.set_key(KeyCode::Space, ButtonState::Pressed);
726        assert!(action.is_active(&state));
727    }
728
729    #[test]
730    fn test_input_action_with_mouse() {
731        let action = InputAction::new("fire").with_mouse_button(MouseButton::Left);
732
733        let mut state = InputState::new();
734        assert!(!action.is_active(&state));
735
736        state.mouse_buttons[0] = ButtonState::Pressed;
737        assert!(action.is_active(&state));
738    }
739
740    #[test]
741    fn test_input_action_with_gamepad() {
742        let action = InputAction::new("jump").with_gamepad_button(GamepadButton::South);
743
744        let mut state = InputState::new();
745        assert!(!action.is_active(&state));
746
747        state.gamepads[0].connected = true;
748        state.gamepads[0].buttons[0] = ButtonState::Pressed;
749        assert!(action.is_active(&state));
750    }
751
752    #[test]
753    fn test_input_action_name() {
754        let action = InputAction::new("test_action");
755        assert_eq!(action.name, "test_action");
756    }
757
758    // ==================== INPUT DEVICE TESTS ====================
759
760    #[test]
761    fn test_input_device_variants() {
762        // Test that all variants can be constructed
763        assert_eq!(InputDevice::Touch, InputDevice::Touch);
764        assert_eq!(InputDevice::Mouse, InputDevice::Mouse);
765        assert_eq!(InputDevice::Keyboard, InputDevice::Keyboard);
766        assert_eq!(InputDevice::Gamepad(0), InputDevice::Gamepad(0));
767        assert_ne!(InputDevice::Gamepad(0), InputDevice::Gamepad(1));
768    }
769
770    // ==================== INPUT ERROR TESTS ====================
771
772    #[test]
773    fn test_input_error_display() {
774        let err = InputError::GamepadNotConnected(2);
775        let msg = format!("{err}");
776        assert!(msg.contains("Gamepad"));
777        assert!(msg.contains('2'));
778        assert!(msg.contains("not connected"));
779    }
780}