Skip to main content

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;