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, PhysicalKey, PointerButton};
4use repose_core::shortcuts::KeyChord;
5
6/// One physical source that can drive an action: keyboard, pad, or axis.
7/// Several bindings can point at the same action.
8#[derive(Clone, Debug)]
9pub enum Binding {
10    /// Keyboard chord (key + modifiers), same shape as repose shortcuts.
11    /// Logical meaning: shortcuts, text entry, glyph-bound actions.
12    Key(KeyChord),
13    /// Physical key position, layout-independent. Movement and other
14    /// position-bound actions: `PhysicalKey::KeyW` is the US W position
15    /// (AZERTY Z), so non-US layouts move the same.
16    Physical(PhysicalKey),
17    /// Mouse button press.
18    Mouse(PointerButton),
19    /// Gamepad button press.
20    Pad(GamepadButton),
21    /// Active while `value >= threshold` for positive threshold,
22    /// `value <= threshold` for negative; `-0.5` on `LeftStickX` is left past half.
23    Axis { axis: GamepadAxis, threshold: f32 },
24}
25
26// Manual `PartialEq`/`Eq`/`Hash`: `PointerButton` lacks them,
27// so mouse variants compare by discriminant.
28impl PartialEq for Binding {
29    fn eq(&self, other: &Self) -> bool {
30        match (self, other) {
31            (Binding::Key(a), Binding::Key(b)) => a == b,
32            (Binding::Physical(a), Binding::Physical(b)) => a == b,
33            (Binding::Mouse(a), Binding::Mouse(b)) => {
34                std::mem::discriminant(a) == std::mem::discriminant(b)
35            }
36            (Binding::Pad(a), Binding::Pad(b)) => a == b,
37            (
38                Binding::Axis {
39                    axis: a,
40                    threshold: t,
41                },
42                Binding::Axis {
43                    axis: b,
44                    threshold: u,
45                },
46            ) => a == b && t == u,
47            _ => false,
48        }
49    }
50}
51
52impl Eq for Binding {}
53
54impl std::hash::Hash for Binding {
55    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
56        std::mem::discriminant(self).hash(state);
57        match self {
58            Binding::Key(chord) => chord.hash(state),
59            Binding::Physical(key) => key.hash(state),
60            Binding::Mouse(b) => std::mem::discriminant(b).hash(state),
61            Binding::Pad(b) => b.hash(state),
62            Binding::Axis { axis, threshold } => {
63                axis.hash(state);
64                let t = if *threshold == 0.0 { 0.0 } else { *threshold };
65                t.to_bits().hash(state);
66            }
67        }
68    }
69}
70
71impl Binding {
72    pub fn from_entry(entry: &super::keymap::KeymapEntry) -> Option<Self> {
73        use super::keymap::KeymapEntry;
74        Some(match entry {
75            KeymapEntry::None => return None,
76            KeymapEntry::Key(chord) => Binding::Key(chord.clone()),
77            KeymapEntry::Physical(key) => Binding::Physical(*key),
78            KeymapEntry::Mouse(button) => Binding::Mouse(*button),
79            KeymapEntry::Pad(button) => Binding::Pad(*button),
80            KeymapEntry::Axis { axis, threshold } => Binding::Axis {
81                axis: *axis,
82                threshold: *threshold,
83            },
84        })
85    }
86
87    /// True while the latest axis value holds past the threshold.
88    /// Zero threshold means any non-zero deflection; rest at 0.0 reads inactive.
89    pub fn axis_active(threshold: f32, value: f32) -> bool {
90        if !value.is_finite() {
91            return false;
92        }
93        if threshold == 0.0 {
94            value != 0.0
95        } else if threshold > 0.0 {
96            value >= threshold
97        } else {
98            value <= threshold
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn axis_threshold_sign_selects_side() {
109        assert!(Binding::axis_active(0.5, 0.7));
110        assert!(!Binding::axis_active(0.5, 0.3));
111        assert!(Binding::axis_active(-0.5, -0.7));
112        assert!(!Binding::axis_active(-0.5, -0.3));
113        assert!(!Binding::axis_active(0.0, 0.0));
114        assert!(Binding::axis_active(0.0, 0.1));
115        assert!(Binding::axis_active(0.0, -0.1));
116        assert!(!Binding::axis_active(0.0, f32::NAN));
117        assert!(!Binding::axis_active(0.5, f32::NAN));
118        assert!(!Binding::axis_active(-0.5, f32::NAN));
119    }
120
121    #[test]
122    fn bindings_hash_for_sets() {
123        use repose_core::input::{GamepadAxis, GamepadButton, Key, Modifiers, PointerButton};
124        use repose_core::shortcuts::KeyChord;
125        use std::collections::HashSet;
126        let mut set = HashSet::new();
127        set.insert(Binding::Pad(GamepadButton::South));
128        assert!(set.contains(&Binding::Pad(GamepadButton::South)));
129        set.insert(Binding::Mouse(PointerButton::Primary));
130        assert!(set.contains(&Binding::Mouse(PointerButton::Primary)));
131        assert!(!set.contains(&Binding::Mouse(PointerButton::Secondary)));
132        set.insert(Binding::Axis {
133            axis: GamepadAxis::LeftStickX,
134            threshold: 0.5,
135        });
136        assert!(set.contains(&Binding::Axis {
137            axis: GamepadAxis::LeftStickX,
138            threshold: 0.5,
139        }));
140        assert!(!set.contains(&Binding::Axis {
141            axis: GamepadAxis::LeftStickX,
142            threshold: 0.6,
143        }));
144        let _ = KeyChord::new(Key::Space, Modifiers::default());
145    }
146}