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;