Skip to main content

crcbl_input/
binding_text.rs

1//! A [`Binding`]'s stable text form, both ways — what a player's saved rebinds
2//! are written in.
3//!
4//! [`Binding`]'s `Display` prints it and its `FromStr` reads it back, so a
5//! binding round-trips through a settings file whatever holds the file. Like
6//! [`KeyCode::as_str`], which every key here is spelled with, **it is a
7//! serialization format, not a display string**: a rebind menu shows the
8//! keysym or the pad glyph, never this.
9//!
10//! | Binding                | Text                                   |
11//! | ---------------------- | -------------------------------------- |
12//! | `Key`                  | `KeyR`                                 |
13//! | `Chord`                | `Alt+KeyR` (`Shift`, `Control`, `Alt`, `Super`) |
14//! | `ScrollChord`          | `ControlLeft+Scroll`                   |
15//! | `MouseButton`          | `Mouse:Left`, `Mouse:Back`, `Mouse:9`  |
16//! | `ButtonChord`          | `Alt+Mouse:Right` (a `Chord`'s modifier, a `MouseButton`'s button) |
17//! | `MouseMotion`          | `MouseMotion`                          |
18//! | `MouseScroll`          | `MouseScroll`                          |
19//! | `PointerPosition`      | `Pointer:X`, `Pointer:Y`               |
20//! | `KeyAxis`              | `KeyAxis:KeyS,KeyW` (negative, positive) |
21//! | `Wasd`                 | `Wasd:KeyW,KeyS,KeyA,KeyD` (up, down, left, right) |
22//! | `Virtual`              | `Virtual:` and the control's id, verbatim |
23//! | `PadButton`            | `Pad:South`, by [`PadButton`] variant  |
24//! | `PadChord`             | `Pad:LeftShoulder+South` (modifier, then button) |
25//! | `PadDpad`              | `Pad:Dpad`                             |
26//! | `PadStick`             | `PadStick:Left>0.2` (the dead zone)    |
27//! | `PadTrigger`           | `PadTrigger:Right>0.25` (the threshold) |
28//!
29//! A dead zone or threshold prints as Rust's shortest `f32` form, which reads
30//! back to the same bits. Parsing checks the *spelling* only: a threshold
31//! outside `0.0..1.0` parses, and [`ActionMap::rebind`] refuses it as it
32//! refuses one built in code. A name this build does not know is a
33//! [`BindingParseError`], never a panic, so a file written by a newer build
34//! costs the one binding and not the load.
35//!
36//! [`ActionMap::rebind`]: crate::ActionMap::rebind
37
38use core::fmt;
39use core::str::FromStr;
40
41use crcbl_core::input::{KeyCode, PointerButton};
42
43use crate::{Binding, Modifier, PadButton, PointerAxis, Stick, Trigger};
44
45/// Why a string is not a [`Binding`]'s text form — see the
46/// [module docs](self).
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct BindingParseError {
49    /// The text that did not parse.
50    pub text: String,
51    /// What was wrong with it.
52    pub reason: &'static str,
53}
54
55impl fmt::Display for BindingParseError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(f, "{:?} is not a binding: {}", self.text, self.reason)
58    }
59}
60
61impl std::error::Error for BindingParseError {}
62
63impl fmt::Display for Binding {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::Key(key) => f.write_str(key.as_str()),
67            Self::Chord { modifier, key } => {
68                write!(f, "{}+{}", modifier_name(*modifier), key.as_str())
69            }
70            Self::ScrollChord { held } => write!(f, "{}+Scroll", held.as_str()),
71            Self::MouseButton(button) => write_mouse_button(f, *button),
72            Self::ButtonChord { modifier, button } => {
73                write!(f, "{}+", modifier_name(*modifier))?;
74                write_mouse_button(f, *button)
75            }
76            Self::MouseMotion => f.write_str("MouseMotion"),
77            Self::MouseScroll => f.write_str("MouseScroll"),
78            Self::PointerPosition { axis } => match axis {
79                PointerAxis::X => f.write_str("Pointer:X"),
80                PointerAxis::Y => f.write_str("Pointer:Y"),
81            },
82            Self::KeyAxis { negative, positive } => {
83                write!(f, "KeyAxis:{},{}", negative.as_str(), positive.as_str())
84            }
85            Self::Wasd {
86                up,
87                down,
88                left,
89                right,
90            } => write!(
91                f,
92                "Wasd:{},{},{},{}",
93                up.as_str(),
94                down.as_str(),
95                left.as_str(),
96                right.as_str()
97            ),
98            Self::Virtual(id) => write!(f, "Virtual:{id}"),
99            Self::PadButton(button) => write!(f, "Pad:{}", pad_button_name(*button)),
100            Self::PadChord { modifier, button } => write!(
101                f,
102                "Pad:{}+{}",
103                pad_button_name(*modifier),
104                pad_button_name(*button)
105            ),
106            Self::PadDpad => f.write_str("Pad:Dpad"),
107            Self::PadStick { stick, deadzone } => {
108                write!(f, "PadStick:{}>{deadzone}", stick_name(*stick))
109            }
110            Self::PadTrigger { trigger, threshold } => {
111                write!(f, "PadTrigger:{}>{threshold}", trigger_name(*trigger))
112            }
113        }
114    }
115}
116
117impl FromStr for Binding {
118    type Err = BindingParseError;
119
120    fn from_str(text: &str) -> Result<Self, Self::Err> {
121        let fail = |reason| BindingParseError {
122            text: text.to_owned(),
123            reason,
124        };
125        match text {
126            "MouseMotion" => return Ok(Self::MouseMotion),
127            "MouseScroll" => return Ok(Self::MouseScroll),
128            "Pad:Dpad" => return Ok(Self::PadDpad),
129            _ => {}
130        }
131        if let Some((kind, rest)) = text.split_once(':') {
132            if let Some(modifier) = kind.strip_suffix("+Mouse") {
133                let modifier = modifier_named(modifier).ok_or_else(|| fail("no such modifier"))?;
134                let button =
135                    mouse_button_named(rest).ok_or_else(|| fail("no such mouse button"))?;
136                return Ok(Self::ButtonChord { modifier, button });
137            }
138            return match kind {
139                "Mouse" => mouse_button_named(rest)
140                    .map(Self::MouseButton)
141                    .ok_or_else(|| fail("no such mouse button")),
142                "Pointer" => match rest {
143                    "X" => Ok(Self::PointerPosition {
144                        axis: PointerAxis::X,
145                    }),
146                    "Y" => Ok(Self::PointerPosition {
147                        axis: PointerAxis::Y,
148                    }),
149                    _ => Err(fail("a pointer axis is X or Y")),
150                },
151                "KeyAxis" => match keys::<2>(rest) {
152                    Some([negative, positive]) => Ok(Self::KeyAxis { negative, positive }),
153                    None => Err(fail("a key axis is two key names")),
154                },
155                "Wasd" => match keys::<4>(rest) {
156                    Some([up, down, left, right]) => Ok(Self::Wasd {
157                        up,
158                        down,
159                        left,
160                        right,
161                    }),
162                    None => Err(fail("a wasd binding is four key names")),
163                },
164                "Virtual" => Ok(Self::Virtual(rest.to_owned())),
165                "Pad" => match rest.split_once('+') {
166                    Some((modifier, button)) => pad_button_named(modifier)
167                        .zip(pad_button_named(button))
168                        .map(|(modifier, button)| Self::PadChord { modifier, button })
169                        .ok_or_else(|| fail("a pad chord is two pad buttons")),
170                    None => pad_button_named(rest)
171                        .map(Self::PadButton)
172                        .ok_or_else(|| fail("no such pad button")),
173                },
174                "PadStick" => {
175                    let (stick, deadzone) = named_level(rest, stick_named)
176                        .ok_or_else(|| fail("a stick is Left or Right, then > and a dead zone"))?;
177                    Ok(Self::PadStick { stick, deadzone })
178                }
179                "PadTrigger" => {
180                    let (trigger, threshold) =
181                        named_level(rest, trigger_named).ok_or_else(|| {
182                            fail("a trigger is Left or Right, then > and a threshold")
183                        })?;
184                    Ok(Self::PadTrigger { trigger, threshold })
185                }
186                _ => Err(fail("no such binding kind")),
187            };
188        }
189        if let Some((first, second)) = text.split_once('+') {
190            if second == "Scroll" {
191                return KeyCode::from_name(first)
192                    .map(|held| Self::ScrollChord { held })
193                    .ok_or_else(|| fail("no such key before +Scroll"));
194            }
195            let modifier = modifier_named(first).ok_or_else(|| fail("no such modifier"))?;
196            let key = KeyCode::from_name(second).ok_or_else(|| fail("no such key"))?;
197            return Ok(Self::Chord { modifier, key });
198        }
199        KeyCode::from_name(text)
200            .map(Self::Key)
201            .ok_or_else(|| fail("no such key"))
202    }
203}
204
205/// `N` comma-separated key names.
206fn keys<const N: usize>(text: &str) -> Option<[KeyCode; N]> {
207    let mut out = [KeyCode::KeyA; N];
208    let mut names = text.split(',');
209    for slot in &mut out {
210        *slot = KeyCode::from_name(names.next()?)?;
211    }
212    names.next().is_none().then_some(out)
213}
214
215/// `Name>level`, with the name read by `named`.
216fn named_level<T>(text: &str, named: fn(&str) -> Option<T>) -> Option<(T, f32)> {
217    let (name, level) = text.split_once('>')?;
218    Some((named(name)?, level.parse().ok()?))
219}
220
221const fn modifier_name(modifier: Modifier) -> &'static str {
222    match modifier {
223        Modifier::Shift => "Shift",
224        Modifier::Control => "Control",
225        Modifier::Alt => "Alt",
226        Modifier::Super => "Super",
227    }
228}
229
230fn modifier_named(name: &str) -> Option<Modifier> {
231    [
232        Modifier::Shift,
233        Modifier::Control,
234        Modifier::Alt,
235        Modifier::Super,
236    ]
237    .into_iter()
238    .find(|modifier| modifier_name(*modifier) == name)
239}
240
241/// A mouse button's text, `Mouse:Right` or `Mouse:9` — a
242/// [`Binding::MouseButton`]'s whole form, and a [`Binding::ButtonChord`]'s
243/// after its modifier.
244fn write_mouse_button(f: &mut fmt::Formatter<'_>, button: PointerButton) -> fmt::Result {
245    match button {
246        PointerButton::Other(index) => write!(f, "Mouse:{index}"),
247        named => write!(f, "Mouse:{}", pointer_button_name(named)),
248    }
249}
250
251/// The button after `Mouse:`: an index for `Other`, a name for the rest.
252fn mouse_button_named(text: &str) -> Option<PointerButton> {
253    text.parse()
254        .map(PointerButton::Other)
255        .ok()
256        .or_else(|| pointer_button_named(text))
257}
258
259/// A named [`PointerButton`]'s name; `Other` prints as its index instead.
260const fn pointer_button_name(button: PointerButton) -> &'static str {
261    match button {
262        PointerButton::Left => "Left",
263        PointerButton::Right => "Right",
264        PointerButton::Middle => "Middle",
265        PointerButton::Back => "Back",
266        PointerButton::Forward => "Forward",
267        PointerButton::Other(_) => "Other",
268    }
269}
270
271fn pointer_button_named(name: &str) -> Option<PointerButton> {
272    [
273        PointerButton::Left,
274        PointerButton::Right,
275        PointerButton::Middle,
276        PointerButton::Back,
277        PointerButton::Forward,
278    ]
279    .into_iter()
280    .find(|button| pointer_button_name(*button) == name)
281}
282
283const fn pad_button_name(button: PadButton) -> &'static str {
284    match button {
285        PadButton::South => "South",
286        PadButton::East => "East",
287        PadButton::West => "West",
288        PadButton::North => "North",
289        PadButton::LeftShoulder => "LeftShoulder",
290        PadButton::RightShoulder => "RightShoulder",
291        PadButton::LeftStick => "LeftStick",
292        PadButton::RightStick => "RightStick",
293        PadButton::Start => "Start",
294        PadButton::Select => "Select",
295        PadButton::DpadUp => "DpadUp",
296        PadButton::DpadDown => "DpadDown",
297        PadButton::DpadLeft => "DpadLeft",
298        PadButton::DpadRight => "DpadRight",
299        PadButton::Guide => "Guide",
300    }
301}
302
303fn pad_button_named(name: &str) -> Option<PadButton> {
304    PadButton::ALL
305        .into_iter()
306        .find(|button| pad_button_name(*button) == name)
307}
308
309pub(crate) const fn stick_name(stick: Stick) -> &'static str {
310    match stick {
311        Stick::Left => "Left",
312        Stick::Right => "Right",
313    }
314}
315
316fn stick_named(name: &str) -> Option<Stick> {
317    [Stick::Left, Stick::Right]
318        .into_iter()
319        .find(|stick| stick_name(*stick) == name)
320}
321
322pub(crate) const fn trigger_name(trigger: Trigger) -> &'static str {
323    match trigger {
324        Trigger::Left => "Left",
325        Trigger::Right => "Right",
326    }
327}
328
329fn trigger_named(name: &str) -> Option<Trigger> {
330    [Trigger::Left, Trigger::Right]
331        .into_iter()
332        .find(|trigger| trigger_name(*trigger) == name)
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    /// One of every variant, and every name each one can carry.
340    fn every_binding() -> Vec<Binding> {
341        let mut all = vec![
342            Binding::MouseMotion,
343            Binding::MouseScroll,
344            Binding::PointerPosition {
345                axis: PointerAxis::X,
346            },
347            Binding::PointerPosition {
348                axis: PointerAxis::Y,
349            },
350            Binding::KeyAxis {
351                negative: KeyCode::KeyS,
352                positive: KeyCode::KeyW,
353            },
354            Binding::Wasd {
355                up: KeyCode::ArrowUp,
356                down: KeyCode::ArrowDown,
357                left: KeyCode::ArrowLeft,
358                right: KeyCode::ArrowRight,
359            },
360            Binding::Virtual("fire".to_owned()),
361            Binding::Virtual("odd:id+with>marks,".to_owned()),
362            Binding::Virtual(String::new()),
363            Binding::PadDpad,
364            Binding::MouseButton(PointerButton::Other(9)),
365        ];
366        all.extend(KeyCode::ALL.iter().map(|key| Binding::Key(*key)));
367        all.extend(
368            KeyCode::ALL
369                .iter()
370                .map(|key| Binding::ScrollChord { held: *key }),
371        );
372        for modifier in [
373            Modifier::Shift,
374            Modifier::Control,
375            Modifier::Alt,
376            Modifier::Super,
377        ] {
378            all.push(Binding::Chord {
379                modifier,
380                key: KeyCode::Tab,
381            });
382        }
383        all.extend(
384            [
385                PointerButton::Left,
386                PointerButton::Right,
387                PointerButton::Middle,
388                PointerButton::Back,
389                PointerButton::Forward,
390            ]
391            .map(Binding::MouseButton),
392        );
393        for button in [
394            PointerButton::Left,
395            PointerButton::Right,
396            PointerButton::Middle,
397            PointerButton::Back,
398            PointerButton::Forward,
399            PointerButton::Other(9),
400        ] {
401            all.push(Binding::ButtonChord {
402                modifier: Modifier::Alt,
403                button,
404            });
405        }
406        all.extend(
407            [Modifier::Shift, Modifier::Control, Modifier::Super].map(|modifier| {
408                Binding::ButtonChord {
409                    modifier,
410                    button: PointerButton::Right,
411                }
412            }),
413        );
414        all.extend(PadButton::ALL.map(Binding::PadButton));
415        all.extend(PadButton::ALL.map(|button| Binding::PadChord {
416            modifier: PadButton::LeftShoulder,
417            button,
418        }));
419        for (stick, trigger) in [(Stick::Left, Trigger::Left), (Stick::Right, Trigger::Right)] {
420            for level in [0.0, 0.1, 0.25, 1.0 / 3.0, 0.999_999_9] {
421                all.push(Binding::PadStick {
422                    stick,
423                    deadzone: level,
424                });
425                all.push(Binding::PadTrigger {
426                    trigger,
427                    threshold: level,
428                });
429            }
430        }
431        all
432    }
433
434    #[test]
435    fn every_binding_reads_back_as_itself() {
436        let all = every_binding();
437        for binding in &all {
438            let text = binding.to_string();
439            assert_eq!(
440                text.parse::<Binding>().as_ref(),
441                Ok(binding),
442                "{binding:?} printed as {text:?}"
443            );
444        }
445        // Every variant was reached: a new one fails to compile the `match`
446        // in `Display`, and this makes sure the list tests each one we have.
447        let kinds: std::collections::HashSet<_> = all.iter().map(std::mem::discriminant).collect();
448        assert_eq!(kinds.len(), 16, "one of each Binding variant");
449    }
450
451    #[test]
452    fn the_spelling_is_the_documented_one() {
453        for (binding, text) in [
454            (Binding::Key(KeyCode::KeyR), "KeyR"),
455            (
456                Binding::Chord {
457                    modifier: Modifier::Alt,
458                    key: KeyCode::KeyR,
459                },
460                "Alt+KeyR",
461            ),
462            (
463                Binding::ScrollChord {
464                    held: KeyCode::ControlLeft,
465                },
466                "ControlLeft+Scroll",
467            ),
468            (Binding::MouseButton(PointerButton::Left), "Mouse:Left"),
469            (
470                Binding::ButtonChord {
471                    modifier: Modifier::Alt,
472                    button: PointerButton::Right,
473                },
474                "Alt+Mouse:Right",
475            ),
476            (
477                Binding::ButtonChord {
478                    modifier: Modifier::Control,
479                    button: PointerButton::Other(9),
480                },
481                "Control+Mouse:9",
482            ),
483            (Binding::PadButton(PadButton::South), "Pad:South"),
484            (
485                Binding::PadChord {
486                    modifier: PadButton::LeftShoulder,
487                    button: PadButton::South,
488                },
489                "Pad:LeftShoulder+South",
490            ),
491            (
492                Binding::PadTrigger {
493                    trigger: Trigger::Right,
494                    threshold: 0.25,
495                },
496                "PadTrigger:Right>0.25",
497            ),
498            (
499                Binding::PadStick {
500                    stick: Stick::Left,
501                    deadzone: 0.2,
502                },
503                "PadStick:Left>0.2",
504            ),
505            (
506                Binding::Wasd {
507                    up: KeyCode::KeyW,
508                    down: KeyCode::KeyS,
509                    left: KeyCode::KeyA,
510                    right: KeyCode::KeyD,
511                },
512                "Wasd:KeyW,KeyS,KeyA,KeyD",
513            ),
514        ] {
515            assert_eq!(binding.to_string(), text);
516        }
517    }
518
519    #[test]
520    fn a_name_this_build_does_not_know_is_an_error_naming_the_text() {
521        for text in [
522            "",
523            "KeyNotAKey",
524            "Hyper+KeyR",
525            "Alt+KeyNotAKey",
526            "NotAKey+Scroll",
527            "Mouse:Thumb3",
528            "Mouse:-1",
529            "Alt+Mouse:Thumb3",
530            "Hyper+Mouse:Right",
531            "+Mouse:Right",
532            "AltMouse:Right",
533            "Pointer:Z",
534            "KeyAxis:KeyS",
535            "KeyAxis:KeyS,KeyW,KeyA",
536            "Wasd:KeyW,KeyS,KeyA",
537            "Pad:Paddle1",
538            "Pad:LeftShoulder+",
539            "Pad:Paddle1+South",
540            "PadStick:Middle>0.2",
541            "PadStick:Left",
542            "PadTrigger:Right>much",
543            "Joystick:Left",
544        ] {
545            let error = text.parse::<Binding>().expect_err(text);
546            assert_eq!(error.text, text);
547            assert!(error.to_string().contains("is not a binding"), "{error}");
548        }
549    }
550
551    #[test]
552    fn a_level_parses_whatever_rebind_will_refuse() {
553        // The spelling is fine; the value is `rebind`'s to refuse.
554        assert_eq!(
555            "PadTrigger:Left>1.5".parse::<Binding>(),
556            Ok(Binding::PadTrigger {
557                trigger: Trigger::Left,
558                threshold: 1.5
559            })
560        );
561    }
562}