reaction 0.2.0

Universal low-latency input handling for game engines
Documentation
use crate::mapping::binding::InputBinding;
use crate::state::advanced::AdvancedInputState;
use crate::trigger::trigger::InputTrigger;
use crate::types::GamepadId;

pub trait Triggerable {
    fn evaluate(
        &self,
        trigger: &InputTrigger,
        input: &AdvancedInputState,
        gamepad_id: Option<GamepadId>,
    ) -> bool;
}

impl Triggerable for InputBinding {
    fn evaluate(
        &self,
        trigger: &InputTrigger,
        input: &AdvancedInputState,
        gamepad_id: Option<GamepadId>,
    ) -> bool {
        match trigger {
            InputTrigger::Down => is_down(self, input, gamepad_id),
            InputTrigger::Pressed => is_just_pressed(self, input, gamepad_id),
            InputTrigger::Released => is_just_released(self, input, gamepad_id),
            InputTrigger::Held { min_duration } => is_held(self, input, gamepad_id, *min_duration),
            InputTrigger::Chord(triggers) => {
                // For a chord on a single binding, it doesn't make much sense unless the binding itself is a "Meta" binding
                // effectively, but simpler: verify this binding meets ALL triggers.
                // A more common chord is "Key A + Key B".
                // Our current architecture binds Actions to Bindings.
                // So if an Action Binding is "Ctrl + C", that's a chord of two inputs.
                // But here InputTrigger is applied to a single InputBinding?
                // Let's assume InputTrigger modifies HOW the binding is activated.

                triggers.iter().all(|t| self.evaluate(t, input, gamepad_id))
            }
        }
    }
}

fn is_down(
    binding: &InputBinding,
    input: &AdvancedInputState,
    gamepad_id: Option<GamepadId>,
) -> bool {
    match binding {
        InputBinding::Key(k) => input.is_key_down(*k),
        InputBinding::MouseButton(b) => input.is_mouse_button_down(*b),
        InputBinding::GamepadButton(b) => gamepad_id
            .and_then(|id| input.gamepads.get(id))
            .map(|gp| gp.is_button_down(*b))
            .unwrap_or(false),
        InputBinding::GamepadAxis(a) => gamepad_id
            .and_then(|id| input.gamepads.get(id))
            .map(|gp| gp.get_axis(*a).abs() > 0.1)
            .unwrap_or(false),
    }
}

fn is_just_pressed(
    binding: &InputBinding,
    input: &AdvancedInputState,
    gamepad_id: Option<GamepadId>,
) -> bool {
    match binding {
        InputBinding::Key(k) => input.was_key_just_pressed(*k),
        InputBinding::MouseButton(b) => input.mouse.was_button_just_pressed(*b),
        InputBinding::GamepadButton(b) => gamepad_id
            .and_then(|id| input.gamepads.get(id))
            .map(|gp| {
                gp.buttons
                    .get(b)
                    .map(|s| s.just_pressed_frame.is_some())
                    .unwrap_or(false)
            })
            .unwrap_or(false),
        _ => false, // Axis just pressed is simpler to model as threshold crossing, skipping for now
    }
}

fn is_just_released(
    binding: &InputBinding,
    input: &AdvancedInputState,
    gamepad_id: Option<GamepadId>,
) -> bool {
    match binding {
        InputBinding::Key(k) => input.keyboard.was_just_released(*k),
        InputBinding::MouseButton(b) => input.mouse.was_button_just_released(*b),
        InputBinding::GamepadButton(b) => gamepad_id
            .and_then(|id| input.gamepads.get(id))
            .map(|gp| {
                gp.buttons
                    .get(b)
                    .map(|s| s.just_released_frame.is_some())
                    .unwrap_or(false)
            })
            .unwrap_or(false),
        _ => false,
    }
}

fn is_held(
    binding: &InputBinding,
    input: &AdvancedInputState,
    _gamepad_id: Option<GamepadId>,
    min_duration_ms: f32,
) -> bool {
    let (is_down, duration) = match binding {
        InputBinding::Key(k) => (
            input.keyboard.is_down(*k),
            input.keyboard.get_held_duration(*k),
        ),
        InputBinding::MouseButton(b) => input
            .mouse
            .buttons
            .get(b)
            .map(|s| (s.is_down, s.held_duration_ms))
            .unwrap_or((false, 0.0)),
        _ => (false, 0.0),
    };
    is_down && duration >= min_duration_ms
}