Skip to main content

shuyi_ui/input/
pad.rs

1//! Controllers (ticket 0219, game ADR-0034): from each connected pad's raw state
2//! to the [`Button`] presses and releases [`InputState`] is fed. Pure:
3//! `app` fills a [`PadState`] per pad every frame (gilrs on native, the
4//! browser's Gamepad API on web) and [`Pads::update`] does the rest, so both
5//! platforms share these rules (`docs/design/controls.md`, *Controller*):
6//!
7//! - **Sticks are 4-way buttons.** A stick pushed far enough counts as one
8//!   held direction button, the direction it is pushed furthest, so it
9//!   repeats like a held key and never moves diagonally.
10//! - **Switch-style pads swap Confirm and Cancel.** Bindings name button
11//!   *positions*; on a [`PadKind::Nintendo`] pad the bottom and right face
12//!   buttons trade places before the keymap sees them.
13//! - **Several pads drive the game as one.** A button held on two pads is
14//!   one press, released when the last pad lets go; unplugging a pad
15//!   releases what it held.
16//!
17//! [`InputState`]: super::InputState
18
19use crate::prelude::*;
20use alloc::collections::BTreeMap;
21
22use super::{Button, ButtonChord, StickDef};
23
24/// USB vendor ids of the pad makers the game tells apart.
25const MICROSOFT: u16 = 0x045e;
26const SONY: u16 = 0x054c;
27const NINTENDO: u16 = 0x057e;
28
29/// USB product ids of Sony's PS4 pad (the `DualShock` 4): its two models
30/// and its wireless adaptor. Every other Sony pad counts as a PS5 one
31/// (ticket 0229).
32const DUALSHOCK_4: [u16; 3] = [0x05c4, 0x09cc, 0x0ba0];
33
34/// Who made a pad, which decides the Confirm / Cancel swap here and the
35/// button names shown on screen (ticket 0220).
36#[derive(
37    Debug,
38    Clone,
39    Copy,
40    PartialEq,
41    Eq,
42    PartialOrd,
43    Ord,
44    Hash,
45    Default,
46    serde::Serialize,
47    serde::Deserialize,
48)]
49pub enum PadKind {
50    /// A Microsoft pad.
51    Xbox,
52    /// A Sony pad other than a PS4 one (a PS5 pad).
53    PlayStation,
54    /// A Sony PS4 pad: named as a [`PlayStation`](Self::PlayStation) pad,
55    /// except its left centre button is `Share`.
56    PlayStation4,
57    /// A Nintendo pad (Switch Pro controller, Joy-Cons).
58    Nintendo,
59    /// The Playdate's own buttons (game ADR-0070): the D-pad, A and B. Its
60    /// backend reports A at the binding position `East` and B at `West`
61    /// (`targets/playdate.ron`, ADR-0008 §1), with no swap, so a game binds
62    /// them in its keymap's `targets."playdate"` table.
63    Playdate,
64    /// Anything else, or a pad that doesn't say. Treated like an Xbox pad.
65    #[default]
66    Generic,
67}
68
69impl PadKind {
70    /// Every kind.
71    pub const ALL: [PadKind; 6] = [
72        Self::Xbox,
73        Self::PlayStation,
74        Self::PlayStation4,
75        Self::Nintendo,
76        Self::Generic,
77        Self::Playdate,
78    ];
79
80    /// The kind of a pad from its USB vendor and product ids; unknown
81    /// vendors (and 0, "not reported") are [`Generic`](Self::Generic). The
82    /// product only matters for Sony, to tell a PS4 pad apart.
83    pub fn from_ids(vendor: u16, product: u16) -> Self {
84        match vendor {
85            MICROSOFT => Self::Xbox,
86            SONY if DUALSHOCK_4.contains(&product) => Self::PlayStation4,
87            SONY => Self::PlayStation,
88            NINTENDO => Self::Nintendo,
89            _ => Self::Generic,
90        }
91    }
92
93    /// The position bindings use for the button physically at `button` on
94    /// this kind of pad: on a Nintendo pad the bottom and right face buttons
95    /// swap (so its right button, labelled `A`, is `South` and confirms by
96    /// default); every other button, and every other pad, is unchanged.
97    pub fn position(self, button: Button) -> Button {
98        match (self, button) {
99            (Self::Nintendo, Button::South) => Button::East,
100            (Self::Nintendo, Button::East) => Button::South,
101            _ => button,
102        }
103    }
104
105    /// What help text calls the button at binding position `button` on
106    /// this kind of pad (`docs/design/controls.md`, *Button names on
107    /// screen*): Xbox letters (also on generic pads), Sony's shapes,
108    /// or Nintendo's letters (a PS4 pad says `Share` where later Sony pads
109    /// say `Create`). The Playdate's `A` is `East` and its `B` is `West`, as
110    /// printed on it; every other name is the Xbox one, its D-pad's and its
111    /// crank's among them, but `South`'s, which is the `X` that leaves free:
112    /// `A` would name a button its A doesn't press. It has none of the other
113    /// buttons (their actions move to chords and menus in tickets 1034 and
114    /// 1035). The only place buttons get their names.
115    // check-text: not player text (a button's name is what is printed on the pad)
116    pub fn button_name(self, button: Button) -> &'static str {
117        let [xbox, playstation, nintendo] = names(button);
118        match self {
119            Self::Playdate if button == Button::East => "A",
120            Self::Playdate if button == Button::West => "B",
121            Self::Playdate if button == Button::South => "X",
122            Self::Xbox | Self::Generic | Self::Playdate => xbox,
123            Self::PlayStation4 if button == Button::Select => "Share",
124            Self::PlayStation | Self::PlayStation4 => playstation,
125            Self::Nintendo => nintendo,
126        }
127    }
128
129    /// What help text calls `chord` on this kind of pad: its
130    /// [button's name](Self::button_name), or a button chord's two names
131    /// with `join` between them (`A+B`, `B+←` with `+`), in the chord's
132    /// order.
133    pub fn chord_name(self, chord: ButtonChord, join: &str) -> String {
134        match chord.buttons() {
135            Some((a, b)) => format!("{}{join}{}", self.button_name(a), self.button_name(b)),
136            None => self.button_name(chord.first()).to_owned(),
137        }
138    }
139}
140
141/// A Sony pad's face buttons' shapes: glyphs of our own in the font
142/// (`assets-src/fonts/pad-shapes.bdf`, ticket 0220).
143const CROSS: &str = "\u{2715}";
144const CIRCLE: &str = "\u{25ef}";
145const SQUARE: &str = "\u{25a1}";
146const TRIANGLE: &str = "\u{25b3}";
147
148/// `button`'s name on an Xbox, a Sony and a Nintendo pad, by
149/// binding position: on a Nintendo pad `South` is the right face button
150/// (see [`PadKind::position`]), so it is its `A`. Directions and stick
151/// presses are named the same on every pad, except Sony's `L3` /
152/// `R3`.
153// check-text: not player text (a button's name is what is printed on the pad)
154fn names(button: Button) -> [&'static str; 3] {
155    match button {
156        Button::South => ["A", CROSS, "A"],
157        Button::East => ["B", CIRCLE, "B"],
158        Button::West => ["X", SQUARE, "Y"],
159        Button::North => ["Y", TRIANGLE, "X"],
160        Button::LeftShoulder => ["LB", "L1", "L"],
161        Button::RightShoulder => ["RB", "R1", "R"],
162        Button::LeftTrigger => ["LT", "L2", "ZL"],
163        Button::RightTrigger => ["RT", "R2", "ZR"],
164        Button::Select => ["Back", "Create", "\u{2212}"],
165        Button::Start => ["Start", "Options", "+"],
166        Button::LeftStickPress => ["LS", "L3", "LS"],
167        Button::RightStickPress => ["RS", "R3", "RS"],
168        Button::DpadUp => ["↑"; 3],
169        Button::DpadDown => ["↓"; 3],
170        Button::DpadLeft => ["←"; 3],
171        Button::DpadRight => ["→"; 3],
172        Button::LeftStickUp => ["L-stick ↑"; 3],
173        Button::LeftStickDown => ["L-stick ↓"; 3],
174        Button::LeftStickLeft => ["L-stick ←"; 3],
175        Button::LeftStickRight => ["L-stick →"; 3],
176        Button::RightStickUp => ["R-stick ↑"; 3],
177        Button::RightStickDown => ["R-stick ↓"; 3],
178        Button::RightStickLeft => ["R-stick ←"; 3],
179        Button::RightStickRight => ["R-stick →"; 3],
180        Button::CrankBack => ["Crank back"; 3],
181        Button::CrankForward => ["Crank forward"; 3],
182        Button::SystemMenu0 => ["Menu item 1"; 3],
183        Button::SystemMenu1 => ["Menu item 2"; 3],
184        Button::SystemMenu2 => ["Menu item 3"; 3],
185        Button::SystemMenu3 => ["Menu item 4"; 3],
186        Button::SystemMenu4 => ["Menu item 5"; 3],
187        Button::SystemMenu5 => ["Menu item 6"; 3],
188        Button::SystemMenu6 => ["Menu item 7"; 3],
189        Button::SystemMenu7 => ["Menu item 8"; 3],
190    }
191}
192
193/// A set of [`Button`]s.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
195pub struct ButtonSet(u64);
196
197impl ButtonSet {
198    /// No buttons.
199    pub const EMPTY: Self = Self(0);
200
201    /// Adds `button`.
202    pub fn insert(&mut self, button: Button) {
203        self.0 |= bit(button);
204    }
205
206    /// Takes `button` out.
207    pub fn remove(&mut self, button: Button) {
208        self.0 &= !bit(button);
209    }
210
211    /// Whether `button` is in the set.
212    pub fn contains(self, button: Button) -> bool {
213        self.0 & bit(button) != 0
214    }
215
216    /// The buttons in either set.
217    #[must_use]
218    pub fn union(self, other: Self) -> Self {
219        Self(self.0 | other.0)
220    }
221
222    /// The buttons in the set, in [`Button::ALL`] order.
223    pub fn iter(self) -> impl Iterator<Item = Button> {
224        Button::ALL
225            .iter()
226            .copied()
227            .filter(move |&b| self.contains(b))
228    }
229}
230
231/// `button`'s bit: its index in [`Button::ALL`].
232fn bit(button: Button) -> u64 {
233    1 << (button as u64)
234}
235
236impl FromIterator<Button> for ButtonSet {
237    fn from_iter<I: IntoIterator<Item = Button>>(buttons: I) -> Self {
238        let mut set = Self::EMPTY;
239        for button in buttons {
240            set.insert(button);
241        }
242        set
243    }
244}
245
246/// How many buttons the browser Gamepad API's "standard" mapping has before
247/// its optional extras: the first 16 of [`Button::ALL`], in the same order.
248const STANDARD_BUTTONS: u32 = 16;
249
250/// One pad's raw state in one frame, as the platform reports it.
251#[derive(Debug, Clone, Copy, PartialEq, Default)]
252pub struct PadState {
253    /// The real buttons held, by where they physically are on the pad (the
254    /// stick directions come from the sticks below).
255    pub buttons: ButtonSet,
256    /// The left stick as `(x, y)`, each from -1 to 1: `+x` is right, `+y`
257    /// is **down**.
258    pub left_stick: (f32, f32),
259    /// The right stick, like [`left_stick`](Self::left_stick).
260    pub right_stick: (f32, f32),
261}
262
263impl PadState {
264    /// A pad the browser reports in the Gamepad API's "standard" mapping:
265    /// bit `i` of `buttons` is set while its `buttons[i]` is pressed (only
266    /// the first 16 count), and `axes` are its first four axes (left stick
267    /// x, y, right stick x, y; down is positive).
268    pub fn from_standard(buttons: u32, axes: [f32; 4]) -> Self {
269        let [left_x, left_y, right_x, right_y] = axes;
270        Self {
271            buttons: ButtonSet(u64::from(buttons) & ((1 << STANDARD_BUTTONS) - 1)),
272            left_stick: (left_x, left_y),
273            right_stick: (right_x, right_y),
274        }
275    }
276}
277
278/// A stick's direction buttons: up, down, left, right.
279pub type StickButtons = [Button; 4];
280
281const DPAD: StickButtons = [
282    Button::DpadUp,
283    Button::DpadDown,
284    Button::DpadLeft,
285    Button::DpadRight,
286];
287const LEFT_STICK: StickButtons = [
288    Button::LeftStickUp,
289    Button::LeftStickDown,
290    Button::LeftStickLeft,
291    Button::LeftStickRight,
292];
293const RIGHT_STICK: StickButtons = [
294    Button::RightStickUp,
295    Button::RightStickDown,
296    Button::RightStickLeft,
297    Button::RightStickRight,
298];
299
300/// A pad's three sets of direction buttons, each with the name help text
301/// gives the whole set.
302// check-text: not player text
303pub const DIRECTION_SETS: [(&str, StickButtons); 3] = [
304    ("D-pad", DPAD),
305    ("L-stick", LEFT_STICK),
306    ("R-stick", RIGHT_STICK),
307];
308
309/// What moves the cursor on a pad of `kind`, for help text. `bound` is the
310/// buttons of the cursor's up, down, left and right. Every whole set of
311/// directions among them is named, joined with `/` (`D-pad/L-stick` with the
312/// defaults); if there is none, each direction's first button, in
313/// up-left-down-right order. `None` if a direction has no button.
314pub(super) fn cursor_buttons_name(kind: PadKind, bound: &[Vec<Button>; 4]) -> Option<String> {
315    let [up, down, left, right] = bound.each_ref().map(|buttons| buttons.first());
316    let first = [up?, left?, down?, right?];
317    let whole: Vec<&str> = DIRECTION_SETS
318        .iter()
319        .filter(|(_, set)| set.iter().zip(bound).all(|(b, list)| list.contains(b)))
320        .map(|&(name, _)| name)
321        .collect();
322    let names = if whole.is_empty() {
323        first.map(|&b| kind.button_name(b)).to_vec()
324    } else {
325        whole
326    };
327    Some(names.join("/"))
328}
329
330/// The one direction button a stick at `(x, y)` holds, if any. `held` is
331/// the one it held last frame.
332///
333/// A direction starts when the stick is pushed at least `press_percent` its
334/// way, and it is the direction pushed furthest (left or right on a perfect
335/// diagonal). It then stays held until the stick falls below
336/// `release_percent` that way, or another direction is pushed further by
337/// the gap between the two thresholds, so neither the edge of the dead zone
338/// nor a diagonal flickers.
339fn pushed(
340    held: Option<Button>,
341    (x, y): (f32, f32),
342    [up, down, left, right]: StickButtons,
343    def: StickDef,
344) -> Option<Button> {
345    let press = f32::from(def.press_percent) / 100.0;
346    let release = f32::from(def.release_percent) / 100.0;
347    // How far the stick is pushed each way. On a tie the last wins.
348    let amounts = [(up, -y), (down, y), (left, -x), (right, x)];
349    let along = |button| {
350        let found = amounts.iter().find(|&&(b, _)| b == button);
351        found.map_or(0.0, |&(_, amount)| amount)
352    };
353    let (furthest, amount) = amounts.iter().copied().max_by(|a, b| a.1.total_cmp(&b.1))?;
354    let fresh = (amount >= press).then_some(furthest);
355    let Some(held) = held.filter(|&h| along(h) >= release) else {
356        return fresh;
357    };
358    match fresh {
359        Some(new) if amount >= along(held) + (press - release) => Some(new),
360        _ => Some(held),
361    }
362}
363
364/// The buttons a pad of `kind` holds, by binding position, stick directions
365/// included. `was` is what it held last frame (for the sticks).
366fn resolve(was: ButtonSet, kind: PadKind, state: &PadState, stick: StickDef) -> ButtonSet {
367    let mut down: ButtonSet = state.buttons.iter().map(|b| kind.position(b)).collect();
368    for (buttons, at) in [
369        (LEFT_STICK, state.left_stick),
370        (RIGHT_STICK, state.right_stick),
371    ] {
372        let held = buttons.into_iter().find(|&b| was.contains(b));
373        if let Some(button) = pushed(held, at, buttons, stick) {
374            down.insert(button);
375        }
376    }
377    down
378}
379
380/// What tells one connected pad from another: the platform's own number for
381/// it, stable while the pad stays plugged in.
382pub type PadId = usize;
383
384/// Every connected pad, merged into one stream of button presses and
385/// releases (see the [module docs](self)).
386#[derive(Debug, Clone)]
387pub struct Pads {
388    stick: StickDef,
389    /// Each connected pad's buttons held, by binding position.
390    held: BTreeMap<PadId, ButtonSet>,
391}
392
393impl Pads {
394    /// No pads yet, with the sticks' thresholds from the keymap.
395    pub fn new(stick: StickDef) -> Self {
396        Self {
397            stick,
398            held: BTreeMap::new(),
399        }
400    }
401
402    /// Takes this frame's pads (`connected`: every pad plugged in, with its
403    /// kind and state; a pad left out was removed) and returns what changed
404    /// since the last call as `(button, pressed)`, releases first, each in
405    /// [`Button::ALL`] order. Buttons are binding positions: feed them to
406    /// [`InputState::pad_down`] and [`pad_up`].
407    ///
408    /// [`InputState::pad_down`]: super::InputState::pad_down
409    /// [`pad_up`]: super::InputState::pad_up
410    pub fn update(&mut self, connected: &[(PadId, PadKind, PadState)]) -> Vec<(Button, bool)> {
411        let before = self.down();
412        let mut held = BTreeMap::new();
413        for (id, kind, state) in connected {
414            let was = self.held.get(id).copied().unwrap_or_default();
415            held.insert(*id, resolve(was, *kind, state, self.stick));
416        }
417        self.held = held;
418        let after = self.down();
419        let released = before.iter().filter(|&b| !after.contains(b));
420        let pressed = after.iter().filter(|&b| !before.contains(b));
421        released
422            .map(|b| (b, false))
423            .chain(pressed.map(|b| (b, true)))
424            .collect()
425    }
426
427    /// The kind of the pad holding `button` (a binding position), the
428    /// first of `connected` if several do: for a button [`update`] has
429    /// just reported pressed, the pad that pressed it. `connected` is what
430    /// `update` was given. `None` if no pad holds it.
431    ///
432    /// [`update`]: Self::update
433    pub fn kind_holding(
434        &self,
435        button: Button,
436        connected: &[(PadId, PadKind, PadState)],
437    ) -> Option<PadKind> {
438        let holds = |id| self.held.get(id).is_some_and(|set| set.contains(button));
439        let pad = connected.iter().find(|(id, _, _)| holds(id));
440        pad.map(|&(_, kind, _)| kind)
441    }
442
443    /// The buttons held on any pad, by binding position.
444    pub fn down(&self) -> ButtonSet {
445        self.held
446            .values()
447            .fold(ButtonSet::EMPTY, |all, &pad| all.union(pad))
448    }
449}
450
451#[cfg(test)]
452mod tests;