Skip to main content

crcbl_input/hint/
labels.rs

1//! The words a [`Hint`](super::Hint) prints: [`HintLabels`], whose provided
2//! methods are the engine's tables, and [`DefaultLabels`], which uses them
3//! unchanged.
4
5use std::borrow::Cow;
6
7use crcbl_core::input::{KeyCode, PointerButton};
8
9use crate::{Binding, PadButton, PadKind, PointerAxis, Stick, Trigger};
10
11/// What a hint calls each input — a label table a game can override entry by
12/// entry.
13///
14/// **Every method has the engine's answer as its default**, so a game
15/// implements this on its own type and overrides only what it would print
16/// differently: a pad family's buttons as the symbols its font carries, the
17/// keys in the player's language. [`HintLabels::label`] composes the entries
18/// into a whole binding's label — `Shift+Tab`, `WASD`, `LB+A` — and is
19/// itself overridable for a game that spells composites its own way.
20///
21/// Keys are named by **position**, after the US-QWERTY legend, because that is
22/// all a [`KeyCode`] knows: on AZERTY, [`KeyCode::KeyW`] prints `W` over the
23/// key labelled Z. The layout's own legend is the shell's to know, and a game
24/// that has it overrides [`HintLabels::key`].
25///
26/// ```
27/// use std::borrow::Cow;
28/// use crcbl_input::{HintLabels, PadButton, PadKind};
29///
30/// /// A game whose font has the circled letters.
31/// struct Circled;
32///
33/// impl HintLabels for Circled {
34///     fn pad_button(&self, kind: PadKind, button: PadButton) -> Cow<'static, str> {
35///         match (kind, button) {
36///             (PadKind::Xbox, PadButton::South) => "Ⓐ".into(),
37///             _ => crcbl_input::DefaultLabels.pad_button(kind, button),
38///         }
39///     }
40/// }
41///
42/// assert_eq!(Circled.pad_button(PadKind::Xbox, PadButton::South), "Ⓐ");
43/// assert_eq!(Circled.pad_button(PadKind::Xbox, PadButton::East), "B");
44/// ```
45pub trait HintLabels {
46    /// A key's label — `Space`, `W`, `Shift`, `Up`.
47    fn key(&self, key: KeyCode) -> Cow<'static, str> {
48        key_label(key).into()
49    }
50
51    /// A mouse button's label — `LMB`, `RMB`, `MMB`, `Mouse 4`.
52    fn mouse_button(&self, button: PointerButton) -> Cow<'static, str> {
53        match button {
54            PointerButton::Left => "LMB".into(),
55            PointerButton::Right => "RMB".into(),
56            PointerButton::Middle => "MMB".into(),
57            PointerButton::Back => "Mouse 4".into(),
58            PointerButton::Forward => "Mouse 5".into(),
59            PointerButton::Other(index) => format!("Mouse {index}").into(),
60        }
61    }
62
63    /// [`Binding::MouseMotion`]'s label.
64    fn mouse_motion(&self) -> Cow<'static, str> {
65        "Mouse".into()
66    }
67
68    /// The wheel's label, for [`Binding::MouseScroll`] and after a
69    /// [`Binding::ScrollChord`]'s key.
70    fn wheel(&self) -> Cow<'static, str> {
71        "Wheel".into()
72    }
73
74    /// [`Binding::PointerPosition`]'s label, whichever axis it reads.
75    fn pointer_position(&self, axis: PointerAxis) -> Cow<'static, str> {
76        let _ = axis;
77        "Pointer".into()
78    }
79
80    /// An on-screen control's label: by default its id, verbatim, since the
81    /// id is the only name the engine has for it.
82    fn control(&self, id: &str) -> Cow<'static, str> {
83        id.to_owned().into()
84    }
85
86    /// A pad button's label on a pad of `kind`: what that family prints on
87    /// the button in that position.
88    ///
89    /// | Button          | Xbox | PlayStation | Switch | Steam Deck | Generic            |
90    /// | --------------- | ---- | ----------- | ------ | ---------- | ------------------ |
91    /// | `South`         | A    | Cross       | B      | A          | South              |
92    /// | `East`          | B    | Circle      | A      | B          | East               |
93    /// | `West`          | X    | Square      | Y      | X          | West               |
94    /// | `North`         | Y    | Triangle    | X      | Y          | North              |
95    /// | `LeftShoulder`  | LB   | L1          | L      | L1         | Left bumper        |
96    /// | `RightShoulder` | RB   | R1          | R      | R1         | Right bumper       |
97    /// | `LeftStick`     | LS   | L3          | LS     | L3         | Left stick button  |
98    /// | `RightStick`    | RS   | R3          | RS     | R3         | Right stick button |
99    /// | `Start`         | Menu | Options     | +      | Menu       | Start              |
100    /// | `Select`        | View | Share       | -      | View       | Select             |
101    /// | `Guide`         | Xbox | PS          | Home   | Steam      | Guide              |
102    ///
103    /// The d-pad is `D-pad up`, `D-pad down`, `D-pad left` and `D-pad right`
104    /// on every family. The face buttons are where the families disagree, and
105    /// why bindings are positional: a Switch's `B` is where an Xbox pad's `A`
106    /// is. `Select` is `Share` on a DualShock 4 and `Create` on a DualSense,
107    /// which a [`PadKind`] does not tell apart, so the older word is printed;
108    /// the Switch's `−` is printed as an ASCII hyphen, which every font has.
109    fn pad_button(&self, kind: PadKind, button: PadButton) -> Cow<'static, str> {
110        pad_button_label(kind, button).into()
111    }
112
113    /// [`Binding::PadDpad`]'s label on a pad of `kind`.
114    fn dpad(&self, kind: PadKind) -> Cow<'static, str> {
115        let _ = kind;
116        "D-pad".into()
117    }
118
119    /// A stick's label on a pad of `kind`. Every family prints the same
120    /// words; `kind` is there for a game whose art differs by family.
121    fn stick(&self, kind: PadKind, stick: Stick) -> Cow<'static, str> {
122        let _ = kind;
123        match stick {
124            Stick::Left => "Left stick".into(),
125            Stick::Right => "Right stick".into(),
126        }
127    }
128
129    /// A trigger's label on a pad of `kind`: `LT`/`RT` on Xbox, `L2`/`R2` on
130    /// PlayStation and the Steam Deck, `ZL`/`ZR` on Switch, and
131    /// `Left trigger`/`Right trigger` on a pad the backend could not place.
132    fn trigger(&self, kind: PadKind, trigger: Trigger) -> Cow<'static, str> {
133        trigger_label(kind, trigger).into()
134    }
135
136    /// A whole binding's label, composed from the entries above: a chord as
137    /// its two halves joined by `+`, a [`Binding::KeyAxis`] as its two keys
138    /// joined by `/`, and a [`Binding::Wasd`] as its four keys in up, left,
139    /// down, right order — run together when each is one character (`WASD`),
140    /// joined by `/` when not (`Up/Left/Down/Right`). A pad binding is named
141    /// for a pad of `kind`; nothing else reads it.
142    fn label(&self, binding: &Binding, kind: PadKind) -> String {
143        match binding {
144            Binding::Key(key) => self.key(*key).into_owned(),
145            Binding::Chord { modifier, key } => {
146                format!("{}+{}", self.key(modifier.keys()[0]), self.key(*key))
147            }
148            Binding::KeyAxis { negative, positive } => {
149                format!("{}/{}", self.key(*negative), self.key(*positive))
150            }
151            Binding::Wasd {
152                up,
153                down,
154                left,
155                right,
156            } => {
157                let keys = [*up, *left, *down, *right].map(|key| self.key(key));
158                let separator = if keys.iter().all(|key| key.chars().count() == 1) {
159                    ""
160                } else {
161                    "/"
162                };
163                keys.join(separator)
164            }
165            Binding::ScrollChord { held } => format!("{}+{}", self.key(*held), self.wheel()),
166            Binding::MouseButton(button) => self.mouse_button(*button).into_owned(),
167            Binding::ButtonChord { modifier, button } => format!(
168                "{}+{}",
169                self.key(modifier.keys()[0]),
170                self.mouse_button(*button)
171            ),
172            Binding::MouseMotion => self.mouse_motion().into_owned(),
173            Binding::MouseScroll => self.wheel().into_owned(),
174            Binding::PointerPosition { axis } => self.pointer_position(*axis).into_owned(),
175            Binding::Virtual(id) => self.control(id).into_owned(),
176            Binding::PadButton(button) => self.pad_button(kind, *button).into_owned(),
177            Binding::PadChord { modifier, button } => format!(
178                "{}+{}",
179                self.pad_button(kind, *modifier),
180                self.pad_button(kind, *button)
181            ),
182            Binding::PadDpad => self.dpad(kind).into_owned(),
183            Binding::PadStick { stick, .. } => self.stick(kind, *stick).into_owned(),
184            Binding::PadTrigger { trigger, .. } => self.trigger(kind, *trigger).into_owned(),
185        }
186    }
187}
188
189/// The engine's labels, every entry of [`HintLabels`] left as it is — what
190/// [`ActionMap::hint`](crate::ActionMap::hint) uses.
191#[derive(Clone, Copy, Debug, Default)]
192pub struct DefaultLabels;
193
194impl HintLabels for DefaultLabels {}
195
196/// A key's label: its US-QWERTY legend where it has a short one, and its
197/// stable name ([`KeyCode::as_str`]) where that already reads as one — `Tab`,
198/// `Enter`, `F1`, `PageUp`.
199fn key_label(key: KeyCode) -> &'static str {
200    match key {
201        KeyCode::Backquote => "`",
202        KeyCode::Minus => "-",
203        KeyCode::Equal => "=",
204        KeyCode::BracketLeft => "[",
205        KeyCode::BracketRight => "]",
206        KeyCode::Backslash => "\\",
207        KeyCode::Semicolon => ";",
208        KeyCode::Quote => "'",
209        KeyCode::Comma => ",",
210        KeyCode::Period => ".",
211        KeyCode::Slash => "/",
212        KeyCode::Escape => "Esc",
213        KeyCode::ShiftLeft | KeyCode::ShiftRight => "Shift",
214        KeyCode::ControlLeft | KeyCode::ControlRight => "Ctrl",
215        KeyCode::AltLeft | KeyCode::AltRight => "Alt",
216        KeyCode::SuperLeft | KeyCode::SuperRight => "Super",
217        KeyCode::ContextMenu => "Menu",
218        KeyCode::ArrowUp => "Up",
219        KeyCode::ArrowDown => "Down",
220        KeyCode::ArrowLeft => "Left",
221        KeyCode::ArrowRight => "Right",
222        KeyCode::Numpad0 => "Num 0",
223        KeyCode::Numpad1 => "Num 1",
224        KeyCode::Numpad2 => "Num 2",
225        KeyCode::Numpad3 => "Num 3",
226        KeyCode::Numpad4 => "Num 4",
227        KeyCode::Numpad5 => "Num 5",
228        KeyCode::Numpad6 => "Num 6",
229        KeyCode::Numpad7 => "Num 7",
230        KeyCode::Numpad8 => "Num 8",
231        KeyCode::Numpad9 => "Num 9",
232        KeyCode::NumpadDivide => "Num /",
233        KeyCode::NumpadMultiply => "Num *",
234        KeyCode::NumpadSubtract => "Num -",
235        KeyCode::NumpadAdd => "Num +",
236        KeyCode::NumpadEnter => "Num Enter",
237        KeyCode::NumpadDecimal => "Num .",
238        // `KeyA` → `A`, `Digit1` → `1`: the stable names put the legend after
239        // a prefix, and only the letters and the digit row carry these two.
240        other => {
241            let name = other.as_str();
242            name.strip_prefix("Key")
243                .or_else(|| name.strip_prefix("Digit"))
244                .unwrap_or(name)
245        }
246    }
247}
248
249/// What is printed on `button` of a pad of `kind` — the table
250/// [`HintLabels::pad_button`] documents.
251const fn pad_button_label(kind: PadKind, button: PadButton) -> &'static str {
252    use PadKind::{Generic, PlayStation, SteamDeck, Switch, Xbox};
253    match button {
254        PadButton::South => match kind {
255            Xbox | SteamDeck => "A",
256            PlayStation => "Cross",
257            Switch => "B",
258            Generic => "South",
259        },
260        PadButton::East => match kind {
261            Xbox | SteamDeck => "B",
262            PlayStation => "Circle",
263            Switch => "A",
264            Generic => "East",
265        },
266        PadButton::West => match kind {
267            Xbox | SteamDeck => "X",
268            PlayStation => "Square",
269            Switch => "Y",
270            Generic => "West",
271        },
272        PadButton::North => match kind {
273            Xbox | SteamDeck => "Y",
274            PlayStation => "Triangle",
275            Switch => "X",
276            Generic => "North",
277        },
278        PadButton::LeftShoulder => match kind {
279            Xbox => "LB",
280            PlayStation | SteamDeck => "L1",
281            Switch => "L",
282            Generic => "Left bumper",
283        },
284        PadButton::RightShoulder => match kind {
285            Xbox => "RB",
286            PlayStation | SteamDeck => "R1",
287            Switch => "R",
288            Generic => "Right bumper",
289        },
290        PadButton::LeftStick => match kind {
291            Xbox | Switch => "LS",
292            PlayStation | SteamDeck => "L3",
293            Generic => "Left stick button",
294        },
295        PadButton::RightStick => match kind {
296            Xbox | Switch => "RS",
297            PlayStation | SteamDeck => "R3",
298            Generic => "Right stick button",
299        },
300        PadButton::Start => match kind {
301            Xbox | SteamDeck => "Menu",
302            PlayStation => "Options",
303            Switch => "+",
304            Generic => "Start",
305        },
306        PadButton::Select => match kind {
307            Xbox | SteamDeck => "View",
308            PlayStation => "Share",
309            Switch => "-",
310            Generic => "Select",
311        },
312        PadButton::Guide => match kind {
313            Xbox => "Xbox",
314            PlayStation => "PS",
315            Switch => "Home",
316            SteamDeck => "Steam",
317            Generic => "Guide",
318        },
319        PadButton::DpadUp => "D-pad up",
320        PadButton::DpadDown => "D-pad down",
321        PadButton::DpadLeft => "D-pad left",
322        PadButton::DpadRight => "D-pad right",
323    }
324}
325
326/// What is printed on `trigger` of a pad of `kind` — the list
327/// [`HintLabels::trigger`] documents.
328const fn trigger_label(kind: PadKind, trigger: Trigger) -> &'static str {
329    match (kind, trigger) {
330        (PadKind::Xbox, Trigger::Left) => "LT",
331        (PadKind::Xbox, Trigger::Right) => "RT",
332        (PadKind::PlayStation | PadKind::SteamDeck, Trigger::Left) => "L2",
333        (PadKind::PlayStation | PadKind::SteamDeck, Trigger::Right) => "R2",
334        (PadKind::Switch, Trigger::Left) => "ZL",
335        (PadKind::Switch, Trigger::Right) => "ZR",
336        (PadKind::Generic, Trigger::Left) => "Left trigger",
337        (PadKind::Generic, Trigger::Right) => "Right trigger",
338    }
339}