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}