reaction 0.2.0

Universal low-latency input handling for game engines
Documentation
use crate::types::MouseButton;
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct MouseButtonState {
    pub is_down: bool,
    pub just_pressed_frame: Option<u32>,
    pub just_released_frame: Option<u32>,
    pub held_duration_ms: f32,
}

impl Default for MouseButtonState {
    fn default() -> Self {
        Self {
            is_down: false,
            just_pressed_frame: None,
            just_released_frame: None,
            held_duration_ms: 0.0,
        }
    }
}

pub struct MouseState {
    pub position: (f32, f32),
    pub delta: (f32, f32),
    pub buttons: HashMap<MouseButton, MouseButtonState>,
    pub scroll: (f32, f32),
}

impl Default for MouseState {
    fn default() -> Self {
        Self::new()
    }
}

impl MouseState {
    pub fn new() -> Self {
        Self {
            position: (0.0, 0.0),
            delta: (0.0, 0.0),
            buttons: HashMap::new(),
            scroll: (0.0, 0.0),
        }
    }

    /// Move to absolute position, accumulating delta.
    /// ✅ CORRECT: Additive to support multiple calls per frame
    pub fn move_to(&mut self, x: f32, y: f32) {
        let delta_x = x - self.position.0;
        let delta_y = y - self.position.1;
        self.delta.0 += delta_x;
        self.delta.1 += delta_y;
        self.position = (x, y);
    }

    /// Move relative from current position.
    /// Useful for direct relative input from raw input providers.
    pub fn move_rel(&mut self, dx: f32, dy: f32) {
        self.position.0 += dx;
        self.position.1 += dy;
        self.delta.0 += dx;
        self.delta.1 += dy;
    }

    pub fn button_press(&mut self, button: MouseButton, frame: u32) {
        let state = self.buttons.entry(button).or_default();
        if !state.is_down {
            state.is_down = true;
            state.just_pressed_frame = Some(frame);
            state.held_duration_ms = 0.0;
        }
    }

    pub fn button_release(&mut self, button: MouseButton, frame: u32) {
        let state = self.buttons.entry(button).or_default();
        if state.is_down {
            state.is_down = false;
            state.just_released_frame = Some(frame);
        }
    }

    pub fn scroll(&mut self, delta_x: f32, delta_y: f32) {
        self.scroll.0 += delta_x;
        self.scroll.1 += delta_y;
    }

    /// Resets transient state (delta, scroll) for the next frame.
    ///
    /// ⚠️ CRITICAL: Must be called AFTER game logic has consumed the current frame's deltas.
    pub fn end_frame(&mut self, delta_time_ms: f32) {
        self.delta = (0.0, 0.0);
        self.scroll = (0.0, 0.0);
        for state in self.buttons.values_mut() {
            state.just_pressed_frame = None;
            state.just_released_frame = None;
            if state.is_down {
                state.held_duration_ms += delta_time_ms;
            } else {
                state.held_duration_ms = 0.0;
            }
        }
    }

    pub fn is_button_down(&self, button: MouseButton) -> bool {
        self.buttons
            .get(&button)
            .map(|b| b.is_down)
            .unwrap_or(false)
    }

    pub fn was_button_just_pressed(&self, button: MouseButton) -> bool {
        self.buttons
            .get(&button)
            .map(|b| b.just_pressed_frame.is_some())
            .unwrap_or(false)
    }

    pub fn was_button_just_released(&self, button: MouseButton) -> bool {
        self.buttons
            .get(&button)
            .map(|b| b.just_released_frame.is_some())
            .unwrap_or(false)
    }
}