Skip to main content

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;