Skip to main content

repame_input/
binding.rs

1//! Physical input bindings: one action fires from many inputs.
2
3use repose_core::input::{GamepadAxis, GamepadButton, PointerButton};
4use repose_core::shortcuts::KeyChord;
5
6/// One physical source that can drive an action. Many-to-one: several
7/// bindings can point at the same action (keyboard + pad + axis).
8#[derive(Clone, Debug)]
9pub enum Binding {
10    /// Keyboard chord (key + modifiers), same shape as repose shortcuts.
11    Key(KeyChord),
12    /// Mouse button press.
13    Mouse(PointerButton),
14    /// Gamepad button press.
15    Pad(GamepadButton),
16    /// Analog axis crossing. Active while `value >= threshold` for a
17    /// positive threshold, `value <= threshold` for a negative one, so
18    /// `-0.5` on `LeftStickX` means "pushed left past half".
19    Axis { axis: GamepadAxis, threshold: f32 },
20}
21
22// Manual `PartialEq` + `Eq` + `Hash`: repose `PointerButton` has no
23// `PartialEq`, so compare/hash mouse variants by discriminant.
24impl PartialEq for Binding {
25    fn eq(&self, other: &Self) -> bool {
26        match (self, other) {
27            (Binding::Key(a), Binding::Key(b)) => a == b,
28            (Binding::Mouse(a), Binding::Mouse(b)) => {
29                std::mem::discriminant(a) == std::mem::discriminant(b)
30            }
31            (Binding::Pad(a), Binding::Pad(b)) => a == b,
32            (
33                Binding::Axis {
34                    axis: a,
35                    threshold: t,
36                },
37                Binding::Axis {
38                    axis: b,
39                    threshold: u,
40                },
41            ) => a == b && t == u,
42            _ => false,
43        }
44    }
45}
46
47impl Eq for Binding {}
48
49impl std::hash::Hash for Binding {
50    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
51        std::mem::discriminant(self).hash(state);
52        match self {
53            Binding::Key(chord) => chord.hash(state),
54            Binding::Mouse(b) => std::mem::discriminant(b).hash(state),
55            Binding::Pad(b) => b.hash(state),
56            Binding::Axis { axis, threshold } => {
57                axis.hash(state);
58                let t = if *threshold == 0.0 { 0.0 } else { *threshold };
59                t.to_bits().hash(state);
60            }
61        }
62    }
63}
64
65impl Binding {
66    /// True while the latest axis value holds past the threshold.
67    /// A zero threshold means "any non-zero deflection" (rest stick at
68    /// exactly 0.0 is inactive), so a centered stick never holds an
69    /// action forever.
70    pub fn axis_active(threshold: f32, value: f32) -> bool {
71        if !value.is_finite() {
72            return false;
73        }
74        if threshold == 0.0 {
75            value != 0.0
76        } else if threshold > 0.0 {
77            value >= threshold
78        } else {
79            value <= threshold
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn axis_threshold_sign_selects_side() {
90        assert!(Binding::axis_active(0.5, 0.7));
91        assert!(!Binding::axis_active(0.5, 0.3));
92        assert!(Binding::axis_active(-0.5, -0.7));
93        assert!(!Binding::axis_active(-0.5, -0.3));
94        assert!(!Binding::axis_active(0.0, 0.0));
95        assert!(Binding::axis_active(0.0, 0.1));
96        assert!(Binding::axis_active(0.0, -0.1));
97        assert!(!Binding::axis_active(0.0, f32::NAN));
98        assert!(!Binding::axis_active(0.5, f32::NAN));
99        assert!(!Binding::axis_active(-0.5, f32::NAN));
100    }
101
102    #[test]
103    fn bindings_hash_for_sets() {
104        use repose_core::input::{GamepadAxis, GamepadButton, Key, Modifiers, PointerButton};
105        use repose_core::shortcuts::KeyChord;
106        use std::collections::HashSet;
107        let mut set = HashSet::new();
108        set.insert(Binding::Pad(GamepadButton::South));
109        assert!(set.contains(&Binding::Pad(GamepadButton::South)));
110        set.insert(Binding::Mouse(PointerButton::Primary));
111        assert!(set.contains(&Binding::Mouse(PointerButton::Primary)));
112        assert!(!set.contains(&Binding::Mouse(PointerButton::Secondary)));
113        set.insert(Binding::Axis {
114            axis: GamepadAxis::LeftStickX,
115            threshold: 0.5,
116        });
117        assert!(set.contains(&Binding::Axis {
118            axis: GamepadAxis::LeftStickX,
119            threshold: 0.5,
120        }));
121        assert!(!set.contains(&Binding::Axis {
122            axis: GamepadAxis::LeftStickX,
123            threshold: 0.6,
124        }));
125        let _ = KeyChord::new(Key::Space, Modifiers::default());
126    }
127}