repame-input 0.3.25

sim-side action map, edge state, and contexts for all games
Documentation
//! Physical input bindings: one action fires from many inputs.

use repose_core::input::{GamepadAxis, GamepadButton, PhysicalKey, PointerButton};
use repose_core::shortcuts::KeyChord;

/// One physical source that can drive an action: keyboard, pad, or axis.
/// Several bindings can point at the same action.
#[derive(Clone, Debug)]
pub enum Binding {
    /// Keyboard chord (key + modifiers), same shape as repose shortcuts.
    /// Logical meaning: shortcuts, text entry, glyph-bound actions.
    Key(KeyChord),
    /// Physical key position, layout-independent. Movement and other
    /// position-bound actions: `PhysicalKey::KeyW` is the US W position
    /// (AZERTY Z), so non-US layouts move the same.
    Physical(PhysicalKey),
    /// Mouse button press.
    Mouse(PointerButton),
    /// Gamepad button press.
    Pad(GamepadButton),
    /// Active while `value >= threshold` for positive threshold,
    /// `value <= threshold` for negative; `-0.5` on `LeftStickX` is left past half.
    Axis { axis: GamepadAxis, threshold: f32 },
}

// Manual `PartialEq`/`Eq`/`Hash`: `PointerButton` lacks them,
// so mouse variants compare by discriminant.
impl PartialEq for Binding {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Binding::Key(a), Binding::Key(b)) => a == b,
            (Binding::Physical(a), Binding::Physical(b)) => a == b,
            (Binding::Mouse(a), Binding::Mouse(b)) => {
                std::mem::discriminant(a) == std::mem::discriminant(b)
            }
            (Binding::Pad(a), Binding::Pad(b)) => a == b,
            (
                Binding::Axis {
                    axis: a,
                    threshold: t,
                },
                Binding::Axis {
                    axis: b,
                    threshold: u,
                },
            ) => a == b && t == u,
            _ => false,
        }
    }
}

impl Eq for Binding {}

impl std::hash::Hash for Binding {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        std::mem::discriminant(self).hash(state);
        match self {
            Binding::Key(chord) => chord.hash(state),
            Binding::Physical(key) => key.hash(state),
            Binding::Mouse(b) => std::mem::discriminant(b).hash(state),
            Binding::Pad(b) => b.hash(state),
            Binding::Axis { axis, threshold } => {
                axis.hash(state);
                let t = if *threshold == 0.0 { 0.0 } else { *threshold };
                t.to_bits().hash(state);
            }
        }
    }
}

impl Binding {
    pub fn from_entry(entry: &super::keymap::KeymapEntry) -> Option<Self> {
        use super::keymap::KeymapEntry;
        Some(match entry {
            KeymapEntry::None => return None,
            KeymapEntry::Key(chord) => Binding::Key(chord.clone()),
            KeymapEntry::Physical(key) => Binding::Physical(*key),
            KeymapEntry::Mouse(button) => Binding::Mouse(*button),
            KeymapEntry::Pad(button) => Binding::Pad(*button),
            KeymapEntry::Axis { axis, threshold } => Binding::Axis {
                axis: *axis,
                threshold: *threshold,
            },
        })
    }

    /// True while the latest axis value holds past the threshold.
    /// Zero threshold means any non-zero deflection; rest at 0.0 reads inactive.
    pub fn axis_active(threshold: f32, value: f32) -> bool {
        if !value.is_finite() {
            return false;
        }
        if threshold == 0.0 {
            value != 0.0
        } else if threshold > 0.0 {
            value >= threshold
        } else {
            value <= threshold
        }
    }
}

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

    #[test]
    fn axis_threshold_sign_selects_side() {
        assert!(Binding::axis_active(0.5, 0.7));
        assert!(!Binding::axis_active(0.5, 0.3));
        assert!(Binding::axis_active(-0.5, -0.7));
        assert!(!Binding::axis_active(-0.5, -0.3));
        assert!(!Binding::axis_active(0.0, 0.0));
        assert!(Binding::axis_active(0.0, 0.1));
        assert!(Binding::axis_active(0.0, -0.1));
        assert!(!Binding::axis_active(0.0, f32::NAN));
        assert!(!Binding::axis_active(0.5, f32::NAN));
        assert!(!Binding::axis_active(-0.5, f32::NAN));
    }

    #[test]
    fn bindings_hash_for_sets() {
        use repose_core::input::{GamepadAxis, GamepadButton, Key, Modifiers, PointerButton};
        use repose_core::shortcuts::KeyChord;
        use std::collections::HashSet;
        let mut set = HashSet::new();
        set.insert(Binding::Pad(GamepadButton::South));
        assert!(set.contains(&Binding::Pad(GamepadButton::South)));
        set.insert(Binding::Mouse(PointerButton::Primary));
        assert!(set.contains(&Binding::Mouse(PointerButton::Primary)));
        assert!(!set.contains(&Binding::Mouse(PointerButton::Secondary)));
        set.insert(Binding::Axis {
            axis: GamepadAxis::LeftStickX,
            threshold: 0.5,
        });
        assert!(set.contains(&Binding::Axis {
            axis: GamepadAxis::LeftStickX,
            threshold: 0.5,
        }));
        assert!(!set.contains(&Binding::Axis {
            axis: GamepadAxis::LeftStickX,
            threshold: 0.6,
        }));
        let _ = KeyChord::new(Key::Space, Modifiers::default());
    }
}