sable-platform 0.1.0

Platform abstraction layer for Sable Engine - windowing, input, and events
Documentation
//! Input state tracking for keyboard, mouse, and gamepad.

use std::collections::HashSet;

/// Input events that can occur.
#[derive(Debug, Clone)]
pub enum InputEvent {
    /// A key was pressed.
    KeyPressed(KeyCode),
    /// A key was released.
    KeyReleased(KeyCode),
    /// A mouse button was pressed.
    MousePressed(MouseButton),
    /// A mouse button was released.
    MouseReleased(MouseButton),
    /// The mouse cursor moved.
    MouseMoved {
        /// X position in physical pixels.
        x: f64,
        /// Y position in physical pixels.
        y: f64,
    },
    /// The mouse wheel was scrolled.
    MouseWheel {
        /// Horizontal scroll delta.
        delta_x: f64,
        /// Vertical scroll delta.
        delta_y: f64,
    },
}

/// Keyboard key codes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs)]
pub enum KeyCode {
    // Letters
    A,
    B,
    C,
    D,
    E,
    F,
    G,
    H,
    I,
    J,
    K,
    L,
    M,
    N,
    O,
    P,
    Q,
    R,
    S,
    T,
    U,
    V,
    W,
    X,
    Y,
    Z,

    // Numbers
    Num0,
    Num1,
    Num2,
    Num3,
    Num4,
    Num5,
    Num6,
    Num7,
    Num8,
    Num9,

    // Function keys
    F1,
    F2,
    F3,
    F4,
    F5,
    F6,
    F7,
    F8,
    F9,
    F10,
    F11,
    F12,

    // Special keys
    Escape,
    Enter,
    Space,
    Tab,
    Backspace,
    Shift,
    Control,
    Alt,

    // Arrow keys
    Up,
    Down,
    Left,
    Right,
}

/// Mouse button identifiers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MouseButton {
    /// Left mouse button.
    Left,
    /// Right mouse button.
    Right,
    /// Middle mouse button (scroll wheel click).
    Middle,
    /// Back button (side button).
    Back,
    /// Forward button (side button).
    Forward,
    /// Other mouse button with a numeric identifier.
    Other(u16),
}

/// Tracks the current state of input devices.
#[derive(Debug, Default)]
pub struct InputState {
    /// Currently pressed keys.
    pressed_keys: HashSet<KeyCode>,
    /// Currently pressed mouse buttons.
    pressed_mouse_buttons: HashSet<MouseButton>,
    /// Current mouse position.
    mouse_position: (f64, f64),
    /// Mouse scroll delta accumulated this frame.
    scroll_delta: (f64, f64),
}

impl InputState {
    /// Create a new input state.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Process an input event and update the state.
    pub fn process_event(&mut self, event: &InputEvent) {
        match event {
            InputEvent::KeyPressed(key) => {
                self.pressed_keys.insert(*key);
            }
            InputEvent::KeyReleased(key) => {
                self.pressed_keys.remove(key);
            }
            InputEvent::MousePressed(button) => {
                self.pressed_mouse_buttons.insert(*button);
            }
            InputEvent::MouseReleased(button) => {
                self.pressed_mouse_buttons.remove(button);
            }
            InputEvent::MouseMoved { x, y } => {
                self.mouse_position = (*x, *y);
            }
            InputEvent::MouseWheel { delta_x, delta_y } => {
                self.scroll_delta.0 += delta_x;
                self.scroll_delta.1 += delta_y;
            }
        }
    }

    /// Check if a key is currently pressed.
    #[must_use]
    pub fn is_key_pressed(&self, key: KeyCode) -> bool {
        self.pressed_keys.contains(&key)
    }

    /// Check if a mouse button is currently pressed.
    #[must_use]
    pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool {
        self.pressed_mouse_buttons.contains(&button)
    }

    /// Get the current mouse position in physical pixels.
    #[must_use]
    pub fn mouse_position(&self) -> (f64, f64) {
        self.mouse_position
    }

    /// Get and reset the accumulated scroll delta for this frame.
    pub fn take_scroll_delta(&mut self) -> (f64, f64) {
        let delta = self.scroll_delta;
        self.scroll_delta = (0.0, 0.0);
        delta
    }

    /// Clear all input state (useful when window loses focus).
    pub fn clear(&mut self) {
        self.pressed_keys.clear();
        self.pressed_mouse_buttons.clear();
        self.scroll_delta = (0.0, 0.0);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_input_state_default() {
        let state = InputState::new();
        assert!(!state.is_key_pressed(KeyCode::A));
        assert!(!state.is_mouse_button_pressed(MouseButton::Left));
        assert_eq!(state.mouse_position(), (0.0, 0.0));
    }

    #[test]
    fn test_key_press_release() {
        let mut state = InputState::new();

        // Press key
        state.process_event(&InputEvent::KeyPressed(KeyCode::W));
        assert!(state.is_key_pressed(KeyCode::W));
        assert!(!state.is_key_pressed(KeyCode::A));

        // Press another key
        state.process_event(&InputEvent::KeyPressed(KeyCode::A));
        assert!(state.is_key_pressed(KeyCode::W));
        assert!(state.is_key_pressed(KeyCode::A));

        // Release first key
        state.process_event(&InputEvent::KeyReleased(KeyCode::W));
        assert!(!state.is_key_pressed(KeyCode::W));
        assert!(state.is_key_pressed(KeyCode::A));
    }

    #[test]
    fn test_mouse_button_press_release() {
        let mut state = InputState::new();

        state.process_event(&InputEvent::MousePressed(MouseButton::Left));
        assert!(state.is_mouse_button_pressed(MouseButton::Left));
        assert!(!state.is_mouse_button_pressed(MouseButton::Right));

        state.process_event(&InputEvent::MouseReleased(MouseButton::Left));
        assert!(!state.is_mouse_button_pressed(MouseButton::Left));
    }

    #[test]
    fn test_mouse_movement() {
        let mut state = InputState::new();

        state.process_event(&InputEvent::MouseMoved { x: 100.0, y: 200.0 });
        assert_eq!(state.mouse_position(), (100.0, 200.0));

        state.process_event(&InputEvent::MouseMoved { x: 150.0, y: 250.0 });
        assert_eq!(state.mouse_position(), (150.0, 250.0));
    }

    #[test]
    fn test_scroll_accumulation() {
        let mut state = InputState::new();

        state.process_event(&InputEvent::MouseWheel {
            delta_x: 1.0,
            delta_y: 2.0,
        });
        state.process_event(&InputEvent::MouseWheel {
            delta_x: 0.5,
            delta_y: 1.0,
        });

        let delta = state.take_scroll_delta();
        assert!((delta.0 - 1.5).abs() < f64::EPSILON);
        assert!((delta.1 - 3.0).abs() < f64::EPSILON);

        // After taking, delta should be reset
        let delta = state.take_scroll_delta();
        assert!((delta.0).abs() < f64::EPSILON);
        assert!((delta.1).abs() < f64::EPSILON);
    }

    #[test]
    fn test_clear() {
        let mut state = InputState::new();

        state.process_event(&InputEvent::KeyPressed(KeyCode::Space));
        state.process_event(&InputEvent::MousePressed(MouseButton::Left));
        state.process_event(&InputEvent::MouseWheel {
            delta_x: 1.0,
            delta_y: 1.0,
        });

        assert!(state.is_key_pressed(KeyCode::Space));
        assert!(state.is_mouse_button_pressed(MouseButton::Left));

        state.clear();

        assert!(!state.is_key_pressed(KeyCode::Space));
        assert!(!state.is_mouse_button_pressed(MouseButton::Left));
        let delta = state.take_scroll_delta();
        assert!((delta.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_mouse_button_other() {
        let mut state = InputState::new();

        state.process_event(&InputEvent::MousePressed(MouseButton::Other(5)));
        assert!(state.is_mouse_button_pressed(MouseButton::Other(5)));
        assert!(!state.is_mouse_button_pressed(MouseButton::Other(6)));
    }
}