Skip to main content

concinnity_render/
keymap.rs

1//! The runtime, rebindable key map for the gameplay movement keys. Each backend
2//! decodes physical keys into the same semantic booleans (forward, jump, ...);
3//! this map says which canonical InputKey drives each action, so the settings menu can
4//! remap them at runtime. The map is canonical (backend-agnostic InputKey values); a
5//! backend resolves it to its own native key codes when it is pushed via
6//! `RenderBackend::set_keymap`.
7
8use crate::components::InputKey;
9use serde::{Deserialize, Serialize};
10
11/// A rebindable gameplay action. The four movement directions, sprint, jump, and
12/// interact. Pause (Escape) is deliberately not here: it carries cursor-release /
13/// menu semantics that are fixed per-backend.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Bindable {
16    /// Move forward.
17    Forward,
18    /// Move backward.
19    Backward,
20    /// Strafe left.
21    Left,
22    /// Strafe right.
23    Right,
24    /// Hold to move faster.
25    Sprint,
26    /// Jump.
27    Jump,
28    /// Interact with the entity under the cursor.
29    Interact,
30}
31
32impl Bindable {
33    /// Every rebindable action, in Controls-tab row order.
34    pub const ALL: [Bindable; 7] = [
35        Bindable::Forward,
36        Bindable::Backward,
37        Bindable::Left,
38        Bindable::Right,
39        Bindable::Sprint,
40        Bindable::Jump,
41        Bindable::Interact,
42    ];
43
44    /// The settings key string used in `setting:<key>:rebind` actions and the
45    /// engine settings registry.
46    pub fn setting_key(self) -> &'static str {
47        match self {
48            Bindable::Forward => "key_forward",
49            Bindable::Backward => "key_backward",
50            Bindable::Left => "key_left",
51            Bindable::Right => "key_right",
52            Bindable::Sprint => "key_sprint",
53            Bindable::Jump => "key_jump",
54            Bindable::Interact => "key_interact",
55        }
56    }
57
58    /// The action for a settings key string, or `None` if it is not a rebind key.
59    pub fn from_setting_key(key: &str) -> Option<Bindable> {
60        Bindable::ALL.into_iter().find(|b| b.setting_key() == key)
61    }
62}
63
64/// The canonical action -> key map. Persisted in `ControlsSettings` and pushed to
65/// the active backend. Each field is `#[serde(default)]` so adding an action in a
66/// future build never invalidates an existing settings file (a missing field
67/// falls back to its default rather than failing the whole load).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69pub struct KeyMap {
70    #[serde(default = "def_forward")]
71    /// InputKey bound to [`Bindable::Forward`].
72    pub forward: InputKey,
73    #[serde(default = "def_backward")]
74    /// InputKey bound to [`Bindable::Backward`].
75    pub backward: InputKey,
76    #[serde(default = "def_left")]
77    /// InputKey bound to [`Bindable::Left`].
78    pub left: InputKey,
79    #[serde(default = "def_right")]
80    /// InputKey bound to [`Bindable::Right`].
81    pub right: InputKey,
82    #[serde(default = "def_sprint")]
83    /// InputKey bound to [`Bindable::Sprint`].
84    pub sprint: InputKey,
85    #[serde(default = "def_jump")]
86    /// InputKey bound to [`Bindable::Jump`].
87    pub jump: InputKey,
88    #[serde(default = "def_interact")]
89    /// InputKey bound to [`Bindable::Interact`].
90    pub interact: InputKey,
91}
92
93impl KeyMap {
94    /// The default bindings: the keys that were hardcoded before rebinding.
95    pub const DEFAULT: KeyMap = KeyMap {
96        forward: InputKey::W,
97        backward: InputKey::S,
98        left: InputKey::A,
99        right: InputKey::D,
100        sprint: InputKey::Shift,
101        jump: InputKey::Space,
102        interact: InputKey::E,
103    };
104
105    /// The key currently bound to an action.
106    pub fn get(self, action: Bindable) -> InputKey {
107        match action {
108            Bindable::Forward => self.forward,
109            Bindable::Backward => self.backward,
110            Bindable::Left => self.left,
111            Bindable::Right => self.right,
112            Bindable::Sprint => self.sprint,
113            Bindable::Jump => self.jump,
114            Bindable::Interact => self.interact,
115        }
116    }
117
118    /// Bind an action to a key directly (no conflict handling).
119    pub fn set(&mut self, action: Bindable, key: InputKey) {
120        match action {
121            Bindable::Forward => self.forward = key,
122            Bindable::Backward => self.backward = key,
123            Bindable::Left => self.left = key,
124            Bindable::Right => self.right = key,
125            Bindable::Sprint => self.sprint = key,
126            Bindable::Jump => self.jump = key,
127            Bindable::Interact => self.interact = key,
128        }
129    }
130
131    /// The action a key is bound to, or `None` if unbound. The map keeps each key
132    /// bound to at most one action (the invariant `rebind` maintains), so this is
133    /// the unique holder.
134    pub fn action_for_key(self, key: InputKey) -> Option<Bindable> {
135        Bindable::ALL.into_iter().find(|&b| self.get(b) == key)
136    }
137
138    /// Bind `action` to `new_key`, swapping with whichever action already holds
139    /// `new_key` so every action stays bound. Rebinding an action to its own key
140    /// is a no-op.
141    pub fn rebind(&mut self, action: Bindable, new_key: InputKey) {
142        let old_key = self.get(action);
143        if old_key == new_key {
144            return;
145        }
146        if let Some(other) = self.action_for_key(new_key)
147            && other != action
148        {
149            self.set(other, old_key);
150        }
151        self.set(action, new_key);
152    }
153}
154
155impl Default for KeyMap {
156    fn default() -> Self {
157        Self::DEFAULT
158    }
159}
160
161fn def_forward() -> InputKey {
162    KeyMap::DEFAULT.forward
163}
164fn def_backward() -> InputKey {
165    KeyMap::DEFAULT.backward
166}
167fn def_left() -> InputKey {
168    KeyMap::DEFAULT.left
169}
170fn def_right() -> InputKey {
171    KeyMap::DEFAULT.right
172}
173fn def_sprint() -> InputKey {
174    KeyMap::DEFAULT.sprint
175}
176fn def_jump() -> InputKey {
177    KeyMap::DEFAULT.jump
178}
179fn def_interact() -> InputKey {
180    KeyMap::DEFAULT.interact
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    use alloc::string::String;
188    use alloc::vec::Vec;
189    #[test]
190    fn default_is_wasd_shift_space_e() {
191        let m = KeyMap::default();
192        assert_eq!(m.forward, InputKey::W);
193        assert_eq!(m.backward, InputKey::S);
194        assert_eq!(m.left, InputKey::A);
195        assert_eq!(m.right, InputKey::D);
196        assert_eq!(m.sprint, InputKey::Shift);
197        assert_eq!(m.jump, InputKey::Space);
198        assert_eq!(m.interact, InputKey::E);
199    }
200
201    #[test]
202    fn setting_key_round_trips() {
203        for b in Bindable::ALL {
204            assert_eq!(Bindable::from_setting_key(b.setting_key()), Some(b));
205        }
206        assert_eq!(Bindable::from_setting_key("vsync"), None);
207        assert_eq!(Bindable::from_setting_key("key_nope"), None);
208    }
209
210    #[test]
211    fn get_set_round_trip() {
212        let mut m = KeyMap::default();
213        m.set(Bindable::Forward, InputKey::Up);
214        assert_eq!(m.get(Bindable::Forward), InputKey::Up);
215    }
216
217    #[test]
218    fn set_get_cover_every_action_arm() {
219        // Drive set + get across all seven actions with distinct keys, hitting
220        // every match arm in both methods.
221        let keys = [
222            InputKey::Up,
223            InputKey::Down,
224            InputKey::Left,
225            InputKey::Right,
226            InputKey::Q,
227            InputKey::R,
228            InputKey::T,
229        ];
230        let mut m = KeyMap::default();
231        for (b, k) in Bindable::ALL.into_iter().zip(keys) {
232            m.set(b, k);
233        }
234        for (b, k) in Bindable::ALL.into_iter().zip(keys) {
235            assert_eq!(m.get(b), k);
236        }
237    }
238
239    #[test]
240    fn empty_cbor_map_uses_all_defaults() {
241        // A settings file predating every field (an empty map) still loads, each
242        // field falling back through its `serde(default = "def_*")` helper.
243        let empty: alloc::collections::BTreeMap<String, InputKey> =
244            alloc::collections::BTreeMap::new();
245        let mut bytes = Vec::new();
246        ciborium::into_writer(&empty, &mut bytes).unwrap();
247        let loaded: KeyMap = ciborium::from_reader(&bytes[..]).unwrap();
248        assert_eq!(loaded, KeyMap::DEFAULT);
249    }
250
251    #[test]
252    fn action_for_key_finds_the_holder() {
253        let m = KeyMap::default();
254        assert_eq!(m.action_for_key(InputKey::W), Some(Bindable::Forward));
255        assert_eq!(m.action_for_key(InputKey::Space), Some(Bindable::Jump));
256        // A key bound to nothing.
257        assert_eq!(m.action_for_key(InputKey::Q), None);
258    }
259
260    #[test]
261    fn rebind_to_free_key_just_sets_it() {
262        let mut m = KeyMap::default();
263        m.rebind(Bindable::Forward, InputKey::Q);
264        assert_eq!(m.forward, InputKey::Q);
265        // The others are untouched.
266        assert_eq!(m.backward, InputKey::S);
267    }
268
269    #[test]
270    fn rebind_to_own_key_is_a_noop() {
271        let mut m = KeyMap::default();
272        m.rebind(Bindable::Forward, InputKey::W);
273        assert_eq!(m, KeyMap::default());
274    }
275
276    #[test]
277    fn rebind_to_occupied_key_swaps() {
278        // Bind Forward to S, which Backward holds: they swap, so Backward
279        // inherits Forward's old key (W) and every action stays bound.
280        let mut m = KeyMap::default();
281        m.rebind(Bindable::Forward, InputKey::S);
282        assert_eq!(m.forward, InputKey::S);
283        assert_eq!(m.backward, InputKey::W);
284        // No key is bound twice.
285        for b in Bindable::ALL {
286            assert_eq!(m.action_for_key(m.get(b)), Some(b));
287        }
288    }
289
290    #[test]
291    fn cbor_round_trip_and_missing_field_defaults() {
292        // A full map survives a CBOR round trip.
293        let m = KeyMap {
294            forward: InputKey::Up,
295            ..KeyMap::default()
296        };
297        let mut bytes = Vec::new();
298        ciborium::into_writer(&m, &mut bytes).unwrap();
299        let back: KeyMap = ciborium::from_reader(&bytes[..]).unwrap();
300        assert_eq!(back, m);
301
302        // A map written without one field (an older build) still loads, the
303        // missing field falling back to its default rather than failing.
304        #[derive(Serialize)]
305        struct Partial {
306            forward: InputKey,
307            backward: InputKey,
308            left: InputKey,
309            right: InputKey,
310            sprint: InputKey,
311            jump: InputKey,
312            // `interact` omitted.
313        }
314        let partial = Partial {
315            forward: InputKey::Up,
316            backward: InputKey::S,
317            left: InputKey::A,
318            right: InputKey::D,
319            sprint: InputKey::Shift,
320            jump: InputKey::Space,
321        };
322        let mut bytes = Vec::new();
323        ciborium::into_writer(&partial, &mut bytes).unwrap();
324        let loaded: KeyMap = ciborium::from_reader(&bytes[..]).unwrap();
325        assert_eq!(loaded.forward, InputKey::Up);
326        assert_eq!(loaded.interact, InputKey::E);
327    }
328}