shuyi_ui/input/help_keys.rs
1//! Help text that names keys and controller buttons. Names always come
2//! from the active [`Keymap`], never string literals, because each layout
3//! binds actions to different keys (game ADR-0015) and the player can rebind
4//! them (game ADR-0031); and they follow the [`Device`] the player pressed last
5//! (`docs/design/controls.md`, *Controller*): keys on the keyboard, the
6//! pad's own button names on a controller.
7//!
8//! Generic over the game's [`ActionSet`] (ticket 1106).
9
10use crate::prelude::*;
11use core::ops::Deref;
12use shuyi_assets::keymap::ActionSet;
13
14use super::keymap::Keymap;
15use super::state::Device;
16
17/// The English name of the four arrow keys together: what
18/// [`HelpKeys::new`] starts with, and what tests compare with. The game
19/// shows the language file's word. Lives here because it names keys (only
20/// `input` may).
21// check-text: not player text (the default of `HelpKeys::new`; a game gives the language's)
22pub const ARROWS: &str = "arrows";
23
24/// What help text names keys from: the bindings, the device whose keys
25/// or buttons to name, the two words a key's name can be
26/// (`! not mapped`, `arrows`) and what joins a button chord's two buttons
27/// (`A+B`). A game gives its screens one with the words
28/// in the player's language ([`worded`](Self::worded)). Dereferences to the
29/// [`Keymap`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct HelpKeys<'a, A: ActionSet> {
32 keymap: &'a Keymap<A>,
33 device: Device,
34 not_mapped: &'a str,
35 arrows: &'a str,
36 chord_join: &'a str,
37}
38
39impl<'a, A: ActionSet> HelpKeys<'a, A> {
40 /// `keymap`'s keys, or its buttons as a pad names them, by `device`,
41 /// with the English words ([`NOT_MAPPED`], [`ARROWS`]).
42 pub fn new(keymap: &'a Keymap<A>, device: Device) -> Self {
43 Self {
44 keymap,
45 device,
46 not_mapped: NOT_MAPPED,
47 arrows: ARROWS,
48 chord_join: CHORD_JOIN,
49 }
50 }
51
52 /// These keys, named with `not_mapped` for an action with no key and
53 /// `arrows` for the four arrow keys: the player's language's words.
54 #[must_use]
55 pub fn worded(self, not_mapped: &'a str, arrows: &'a str) -> Self {
56 Self {
57 not_mapped,
58 arrows,
59 ..self
60 }
61 }
62
63 /// These keys, with `join` between a button chord's two buttons
64 /// (`A+B`): the player's language's word.
65 #[must_use]
66 pub fn chord_joined(self, join: &'a str) -> Self {
67 Self {
68 chord_join: join,
69 ..self
70 }
71 }
72
73 /// These words for another set of bindings (a screen showing a keymap
74 /// that isn't the active one).
75 #[must_use]
76 pub fn of(self, keymap: &'a Keymap<A>) -> Self {
77 Self { keymap, ..self }
78 }
79
80 /// These bindings' keyboard keys, whatever the player is using.
81 #[must_use]
82 pub fn on_keyboard(self) -> Self {
83 Self {
84 device: Device::Keyboard,
85 ..self
86 }
87 }
88
89 /// What is shown for an action with no key, or no button on a
90 /// controller (`docs/design/controls.md`, *Rebinding keys*).
91 pub fn not_mapped(self) -> &'a str {
92 self.not_mapped
93 }
94
95 /// `keymap`'s keyboard keys, whatever the player is using (a picture
96 /// of the keyboard, a keyboard-only key).
97 pub fn keyboard(keymap: &'a Keymap<A>) -> Self {
98 Self::new(keymap, Device::Keyboard)
99 }
100
101 /// The device whose keys or buttons are named.
102 pub fn device(self) -> Device {
103 self.device
104 }
105}
106
107impl<A: ActionSet> Deref for HelpKeys<'_, A> {
108 type Target = Keymap<A>;
109
110 fn deref(&self) -> &Keymap<A> {
111 self.keymap
112 }
113}
114
115/// Separator between the parts of a help line.
116pub const SEPARATOR: &str = " ยท ";
117
118/// The English of [`HelpKeys::not_mapped`]: what [`HelpKeys::new`] starts
119/// with, and what tests compare with. The game shows
120/// `keys.not_mapped` of the language file.
121// check-text: not player text (the default of `HelpKeys::new`; a game gives the language's)
122pub const NOT_MAPPED: &str = "! not mapped";
123
124/// The English of what joins a button chord's two buttons (`A+B`): what
125/// [`HelpKeys::new`] starts with, and what tests compare with. The game
126/// shows the language file's.
127// check-text: not player text (the default of `HelpKeys::new`; a game gives the language's)
128pub const CHORD_JOIN: &str = "+";
129
130/// The name of the key for `action` (its [`Keymap::primary`] chord), e.g.
131/// `f` or `Shift+Space`; on a controller, of its button or button chord
132/// ([`Keymap::primary_button`]) as that pad labels it, e.g. `A` or `A+B`.
133/// [`HelpKeys::not_mapped`] if it has none.
134pub fn key_name<A: ActionSet>(keys: HelpKeys<'_, A>, action: A) -> String {
135 let name = match keys.device {
136 Device::Keyboard => keys.primary(action).map(|c| c.to_string()),
137 Device::Pad(kind) => {
138 let button = keys.primary_button(action);
139 button.map(|b| kind.chord_name(b, keys.chord_join))
140 }
141 };
142 name.unwrap_or_else(|| keys.not_mapped.to_owned())
143}
144
145/// Every key for `action`, joined with `/` (e.g. `f/j/Enter/Space`): its
146/// slots in [`Keymap::chords_for`] order, then its fixed keys
147/// ([`Keymap::fixed_chords_for`]). On a controller, every button for it
148/// ([`Keymap::buttons_for`]). [`HelpKeys::not_mapped`] if its slots are empty.
149pub fn all_key_names<A: ActionSet>(keys: HelpKeys<'_, A>, action: A) -> String {
150 let names: Vec<String> = match keys.device {
151 Device::Keyboard => {
152 let own = keys.chords_for(action);
153 // The fixed keys alone don't count as a key of its own.
154 let fixed = if own.is_empty() {
155 Vec::new()
156 } else {
157 Keymap::<A>::fixed_chords_for(action)
158 };
159 own.iter().chain(&fixed).map(ToString::to_string).collect()
160 }
161 Device::Pad(kind) => {
162 let buttons = keys.buttons_for(action);
163 let name = |&b| kind.chord_name(b, keys.chord_join);
164 buttons.iter().map(name).collect()
165 }
166 };
167 if names.is_empty() {
168 return keys.not_mapped.to_owned();
169 }
170 names.join("/")
171}
172
173/// What moves the cursor: `arrows` when the four cursor actions are on the
174/// arrow keys, otherwise their keys in up-left-down-right order (`wasd`).
175/// On a controller, `D-pad/L-stick` or whatever the cursor is on
176/// ([`Keymap::cursor_buttons_name`]). [`HelpKeys::not_mapped`] if any
177/// cursor action has no key. See [`Keymap::cursor_keys_name`].
178pub fn cursor_keys_name<A: ActionSet>(keys: HelpKeys<'_, A>) -> String {
179 let name = match keys.device {
180 Device::Keyboard => keys.cursor_keys_name(keys.arrows),
181 Device::Pad(kind) => keys.cursor_buttons_name(kind),
182 };
183 name.unwrap_or_else(|| keys.not_mapped.to_owned())
184}
185
186/// Joins `key label` hints with [`SEPARATOR`], leaving out hints whose key
187/// is `None` (hidden by the caller, e.g. a key that does nothing here).
188pub fn help_line(hints: &[(Option<String>, &str)]) -> String {
189 hints
190 .iter()
191 .filter_map(|(key, label)| key.as_ref().map(|k| format!("{k} {label}")))
192 .collect::<Vec<_>>()
193 .join(SEPARATOR)
194}
195
196#[cfg(test)]
197mod tests;