crcbl_input/hint.rs
1//! How an action's binding is shown to the player right now:
2//! [`ActionMap::hint`] answers with a [`Hint`] — `Space` while the player is
3//! on the keyboard, `A` once they pick up an Xbox pad, `Cross` on a
4//! PlayStation one.
5//!
6//! # Which binding
7//!
8//! **The binding for the device the player last used.** [`ActionMap::hint`]
9//! takes the first of the action's bindings whose [`Binding::device`] is
10//! [`ActionMap::last_device`]. An action with nothing on that device falls
11//! back to the device used before it, and so on back through every device
12//! that has spoken — a player who nudged the mouse while playing on the pad is
13//! still shown the pad's button for an action the mouse cannot press — and
14//! then, before any device has spoken or when none it heard from is bound, to
15//! the action's first binding.
16//!
17//! # Labels, not artwork
18//!
19//! A hint's [`label`](Hint::label) is text. The engine ships **no glyph
20//! images**: what a button looks like on screen is a game's art direction,
21//! and the families' own button art is theirs to license, so that is the
22//! game's call and not one an engine can make for every game built on it. A
23//! game with an icon atlas maps the rest of the hint — the
24//! [`binding`](Hint::binding) and the [`pad`](Hint::pad) family — to its own
25//! sprites.
26//!
27//! The default labels are plain ASCII words (`A`, `Cross`, `LB`, `Space`),
28//! not symbols such as `Ⓐ`, because a font that lacks a symbol draws nothing
29//! or a box where the hint should be. A game whose font has them supplies its
30//! own [`HintLabels`] to [`ActionMap::hint_with`], overriding only the entries
31//! it prints differently.
32//!
33//! # Which pad family
34//!
35//! A pad button is named for the pad that last spoke
36//! ([`ActionMap::last_pad_kind`]); before any pad spoke, for the first
37//! connected one; with none connected, as [`PadKind::Generic`]'s positional
38//! names. Bindings never read the family — buttons are positional
39//! (`gamepad.rs`) — so this is the one place it matters.
40
41mod labels;
42
43pub use labels::{DefaultLabels, HintLabels};
44
45use crate::{ActionMap, Binding, Device, PadKind};
46
47/// How one action's binding is shown to the player — see the
48/// [module docs](self).
49#[derive(Clone, Debug, PartialEq)]
50pub struct Hint {
51 /// The kind of device the binding listens to — [`Binding::device`] of
52 /// [`Hint::binding`].
53 pub device: Device,
54 /// The binding shown.
55 pub binding: Binding,
56 /// The pad family the label names the binding for, when the binding is a
57 /// pad's; `None` for every other device.
58 pub pad: Option<PadKind>,
59 /// What to print: the binding as the player's device names it.
60 pub label: String,
61}
62
63impl ActionMap {
64 /// How the named action's binding is shown to the player now, in the
65 /// engine's own [`DefaultLabels`] — see the [module docs](self) for which
66 /// binding is picked. `None` if no such action is declared or it has no
67 /// bindings.
68 ///
69 /// ```
70 /// use crcbl_core::input::KeyCode;
71 /// use crcbl_input::{ActionMap, GamepadEvent, GamepadId, GamepadSnapshot, PadButton, PadKind};
72 ///
73 /// let mut map = ActionMap::from_ron(
74 /// r#"[(action: "jump", kind: Button, keyboard: ["Space"], gamepad: ["Pad:South"])]"#,
75 /// )?;
76 /// map.key_event(KeyCode::KeyQ, true);
77 /// assert_eq!(map.hint("jump").map(|hint| hint.label).as_deref(), Some("Space"));
78 ///
79 /// let mut snapshot = GamepadSnapshot::neutral(PadKind::PlayStation);
80 /// snapshot.buttons.insert(PadButton::South);
81 /// map.gamepad_event(&GamepadEvent::State { id: GamepadId(1), snapshot });
82 /// assert_eq!(map.hint("jump").map(|hint| hint.label).as_deref(), Some("Cross"));
83 /// # Ok::<(), crcbl_input::BindingAssetError>(())
84 /// ```
85 #[must_use]
86 pub fn hint(&self, action: &str) -> Option<Hint> {
87 self.hint_with(action, &DefaultLabels)
88 }
89
90 /// [`ActionMap::hint`], with every label taken from `labels` — a game's
91 /// own table, which overrides the engine's entry by entry.
92 #[must_use]
93 pub fn hint_with(&self, action: &str, labels: &(impl HintLabels + ?Sized)) -> Option<Hint> {
94 let bindings = self.bindings(action)?;
95 let binding = self
96 .devices
97 .iter()
98 .find_map(|&device| bindings.iter().find(|binding| binding.device() == device))
99 .or_else(|| bindings.first())?;
100 let device = binding.device();
101 let pad = (device == Device::Gamepad).then(|| self.hint_pad_kind());
102 Some(Hint {
103 device,
104 binding: binding.clone(),
105 pad,
106 label: labels.label(binding, pad.unwrap_or_default()),
107 })
108 }
109
110 /// The family a pad hint is named for — see the [module docs](self).
111 fn hint_pad_kind(&self) -> PadKind {
112 self.last_pad_kind
113 .or_else(|| self.pads.values().next().map(|pad| pad.kind))
114 .unwrap_or_default()
115 }
116}
117
118#[cfg(test)]
119mod tests;