Skip to main content

crcbl_input/
gamepad.rs

1//! The gamepad seam: the one vocabulary every pad backend speaks, and how an
2//! [`ActionMap`] resolves it.
3//!
4//! **Every backend — XInput, evdev, GameController, the Web Gamepad API, Steam
5//! Input — emits [`GamepadEvent`]s and nothing else**, by the conventions
6//! below, so a game written against them cannot tell which backend spoke.
7//!
8//! # Two ways to consume them
9//!
10//! - **Directly.** The events are plain data: a game with its own input
11//!   adapter matches on them and reads [`GamepadSnapshot::stick`] and
12//!   [`GamepadSnapshot::trigger`], raw and finite, and never builds an
13//!   [`ActionMap`] at all.
14//! - **Through an [`ActionMap`]**, the optional layer on top:
15//!   [`ActionMap::gamepad_event`] resolves them against
16//!   [`Binding::PadButton`], [`Binding::PadDpad`], [`Binding::PadStick`] and
17//!   [`Binding::PadTrigger`], with dead zones, contexts and `last_device`.
18//!
19//! A game can do both from one stream, since an event is `Copy`:
20//!
21//! ```
22//! use crcbl_input::{GamepadEvent, Stick};
23//!
24//! /// A game's own adapter: the left stick of the pad that last moved.
25//! #[derive(Default)]
26//! struct Adapter {
27//!     left: Option<(u64, (f32, f32))>,
28//! }
29//!
30//! impl Adapter {
31//!     fn feed(&mut self, event: &GamepadEvent) {
32//!         match *event {
33//!             GamepadEvent::State { id, snapshot } => {
34//!                 self.left = Some((u64::from(id.0), snapshot.stick(Stick::Left)));
35//!             }
36//!             GamepadEvent::Disconnected { id } => {
37//!                 self.left = self.left.filter(|(pad, _)| *pad != u64::from(id.0));
38//!             }
39//!             GamepadEvent::Connected { .. } => {}
40//!         }
41//!     }
42//! }
43//!
44//! # let events: Vec<GamepadEvent> = Vec::new();
45//! let mut adapter = Adapter::default();
46//! let mut map = crcbl_input::ActionMap::new(); // optional
47//! for event in &events {
48//!     adapter.feed(event);
49//!     map.gamepad_event(event);
50//! }
51//! ```
52//!
53//! # Conventions a backend must meet
54//!
55//! - **Buttons are positional.** [`PadButton::South`] is the bottom face button
56//!   whatever is printed on it — A on an Xbox pad, Cross on a PlayStation one,
57//!   B on a Nintendo one. What the button is *called* is a hint's business
58//!   ([`ActionMap::hint`]), not a binding's (`docs/notes/simulation.md`).
59//! - **Sticks are −1…1 with +X right and +Y up**, the convention
60//!   [`ActionMap::virtual_stick`] and [`Binding::PointerPosition`] already use.
61//!   A backend whose device reports +Y down flips it; nothing downstream does.
62//! - **Triggers are 0…1**, 0 at rest.
63//! - **Axes are raw.** No dead zone is applied by a backend: the dead zone is a
64//!   binding's ([`Binding::PadStick`]'s `deadzone`, [`Binding::PadTrigger`]'s
65//!   `threshold`), so two actions on one stick can disagree about it, and a game
66//!   that filters for itself reads the same raw values a binding does.
67//! - **Axes are finite.** A direct consumer can rely on it from a backend in
68//!   this workspace, and [`GamepadSnapshot::is_finite`] checks it; the map
69//!   drops a snapshot with a non-finite axis anyway and keeps the pad's
70//!   previous state, as [`ActionMap::mouse_motion`] drops a `NaN` delta.
71//!
72//! # A snapshot is a level
73//!
74//! A [`GamepadEvent::State`] says what the pad is doing *now*, not what changed:
75//! a backend may send one per poll or only when something moved, and the map
76//! holds the last one until the next arrives — so a backend that sends on change
77//! costs nothing while the pad is idle, and one that re-sends an identical
78//! snapshot changes nothing. Edges are the map's to find, by comparing a
79//! snapshot against the one before it.
80//!
81//! # Every pad drives every pad binding
82//!
83//! Until local-multiplayer device assignment lands, a [`Binding::PadButton`] is
84//! down while *any* connected pad holds it, a trigger reads the furthest-pulled
85//! pad, and a [`Binding::PadStick`] sums every pad's filtered deflection into
86//! the same unit disc a [`Binding::Wasd`] and a [`Binding::PadDpad`] share
87//! with it.
88//!
89//! # Focus loss and disconnection release
90//!
91//! [`ActionMap::release_gamepads`] drops every pad to neutral — the pad half of
92//! what `crcbl::engine::lose_focus` does for keys — and a pad button held
93//! through it is **withheld until it is released**, exactly as a key held across
94//! a context push is (`context.rs`): the player coming back from an alt-tab with
95//! A still down does not jump. Sticks and triggers are levels and are not
96//! withheld by it; the pad's next snapshot restores them. Only
97//! [`ActionMap::suppress_held`] withholds a stick or trigger, until it rests
98//! (`context.rs`). A [`GamepadEvent::Disconnected`]
99//! pad's held state is released on the spot.
100
101use std::sync::atomic::{AtomicU32, Ordering};
102
103use super::{ActionMap, Binding, Device, Pads, TracedInput};
104
105/// One physical pad, for as long as it stays connected.
106///
107/// Allocated by the input layer with [`GamepadId::allocate`], so ids from two
108/// backends never collide. A pad that is unplugged and plugged back in is a
109/// new id wherever the backend cannot tell it is the same device — XInput, for
110/// one, identifies a slot and not a controller.
111#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub struct GamepadId(pub u32);
113
114impl GamepadId {
115    /// A fresh id, never returned before in this process.
116    ///
117    /// # Panics
118    /// If the process has allocated every `u32`, which at one pad a second is
119    /// over a century of plugging.
120    #[must_use]
121    pub fn allocate() -> Self {
122        static NEXT: AtomicU32 = AtomicU32::new(1);
123        let id = NEXT.fetch_add(1, Ordering::Relaxed);
124        assert_ne!(id, 0, "gamepad ids wrapped");
125        Self(id)
126    }
127}
128
129/// A pad button, by **position** — see the module docs.
130#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
131pub enum PadButton {
132    /// The bottom face button: A (Xbox), Cross (PlayStation), B (Nintendo).
133    South,
134    /// The right face button: B (Xbox), Circle (PlayStation), A (Nintendo).
135    East,
136    /// The left face button: X (Xbox), Square (PlayStation), Y (Nintendo).
137    West,
138    /// The top face button: Y (Xbox), Triangle (PlayStation), X (Nintendo).
139    North,
140    /// The left bumper: LB, L1, L.
141    LeftShoulder,
142    /// The right bumper: RB, R1, R.
143    RightShoulder,
144    /// The left stick pressed in: LS, L3.
145    LeftStick,
146    /// The right stick pressed in: RS, R3.
147    RightStick,
148    /// The right centre button: Menu, Options, +.
149    Start,
150    /// The left centre button: View, Share/Create, −.
151    Select,
152    /// The d-pad's up.
153    DpadUp,
154    /// The d-pad's down.
155    DpadDown,
156    /// The d-pad's left.
157    DpadLeft,
158    /// The d-pad's right.
159    DpadRight,
160    /// The logo button: Xbox, PS, Home. Many platforms keep it for themselves,
161    /// so a backend that never sees it never reports it.
162    Guide,
163}
164
165impl PadButton {
166    /// Every button, in declaration order.
167    pub const ALL: [Self; 15] = [
168        Self::South,
169        Self::East,
170        Self::West,
171        Self::North,
172        Self::LeftShoulder,
173        Self::RightShoulder,
174        Self::LeftStick,
175        Self::RightStick,
176        Self::Start,
177        Self::Select,
178        Self::DpadUp,
179        Self::DpadDown,
180        Self::DpadLeft,
181        Self::DpadRight,
182        Self::Guide,
183    ];
184
185    /// The four buttons a [`Binding::PadDpad`] reads.
186    pub(crate) const DPAD: [Self; 4] = [
187        Self::DpadUp,
188        Self::DpadDown,
189        Self::DpadLeft,
190        Self::DpadRight,
191    ];
192
193    const fn bit(self) -> u16 {
194        1 << self as u16
195    }
196}
197
198/// A set of [`PadButton`]s: the ones a pad holds down.
199#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
200pub struct PadButtons(u16);
201
202impl PadButtons {
203    /// No button held.
204    pub const EMPTY: Self = Self(0);
205
206    /// Whether `button` is in the set.
207    #[must_use]
208    pub const fn contains(self, button: PadButton) -> bool {
209        self.0 & button.bit() != 0
210    }
211
212    /// Adds `button` to the set.
213    pub const fn insert(&mut self, button: PadButton) {
214        self.0 |= button.bit();
215    }
216
217    /// Removes `button` from the set.
218    pub const fn remove(&mut self, button: PadButton) {
219        self.0 &= !button.bit();
220    }
221
222    /// Whether the set is empty.
223    #[must_use]
224    pub const fn is_empty(self) -> bool {
225        self.0 == 0
226    }
227
228    pub(crate) const fn union(self, other: Self) -> Self {
229        Self(self.0 | other.0)
230    }
231
232    const fn intersection(self, other: Self) -> Self {
233        Self(self.0 & other.0)
234    }
235
236    const fn difference(self, other: Self) -> Self {
237        Self(self.0 & !other.0)
238    }
239}
240
241impl FromIterator<PadButton> for PadButtons {
242    fn from_iter<I: IntoIterator<Item = PadButton>>(buttons: I) -> Self {
243        let mut set = Self::EMPTY;
244        for button in buttons {
245            set.insert(button);
246        }
247        set
248    }
249}
250
251/// One of a pad's analog axes, and its index into [`GamepadSnapshot::axes`].
252#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
253pub enum PadAxis {
254    /// The left stick's X, −1…1, +X right.
255    LeftX,
256    /// The left stick's Y, −1…1, **+Y up**.
257    LeftY,
258    /// The right stick's X, −1…1, +X right.
259    RightX,
260    /// The right stick's Y, −1…1, **+Y up**.
261    RightY,
262    /// The left trigger, 0…1.
263    LeftTrigger,
264    /// The right trigger, 0…1.
265    RightTrigger,
266}
267
268/// Which stick a [`Binding::PadStick`] reads.
269#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
270pub enum Stick {
271    /// The left stick.
272    Left,
273    /// The right stick.
274    Right,
275}
276
277/// Which trigger a [`Binding::PadTrigger`] reads.
278#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
279pub enum Trigger {
280    /// The left trigger: LT, L2, ZL.
281    Left,
282    /// The right trigger: RT, R2, ZR.
283    Right,
284}
285
286/// What family a pad is, for the labels a hint shows
287/// ([`ActionMap::hint`]). Bindings never read it: buttons are positional.
288#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
289pub enum PadKind {
290    /// An Xbox-layout pad, and anything XInput reports.
291    Xbox,
292    /// A DualShock or DualSense.
293    PlayStation,
294    /// A Switch Pro controller or Joy-Con pair.
295    Switch,
296    /// The Steam Deck's built-in controls.
297    SteamDeck,
298    /// Anything the backend could not place.
299    #[default]
300    Generic,
301}
302
303/// What one pad is doing now — a level, not a delta, like
304/// [`ActionMap::virtual_stick`]'s deflection. See the module docs for the
305/// conventions every field follows.
306#[derive(Clone, Copy, Debug, Default, PartialEq)]
307pub struct GamepadSnapshot {
308    /// The buttons held down.
309    pub buttons: PadButtons,
310    /// Every axis, indexed by [`PadAxis`] (`axes[PadAxis::LeftY as usize]`):
311    /// sticks −1…1 with +Y up, triggers 0…1, all raw.
312    pub axes: [f32; 6],
313    /// The pad's family.
314    pub kind: PadKind,
315}
316
317impl GamepadSnapshot {
318    /// A pad of `kind` at rest: nothing held, sticks centred, triggers out.
319    #[must_use]
320    pub const fn neutral(kind: PadKind) -> Self {
321        Self {
322            buttons: PadButtons::EMPTY,
323            axes: [0.0; 6],
324            kind,
325        }
326    }
327
328    /// One axis's value.
329    #[must_use]
330    pub const fn axis(&self, axis: PadAxis) -> f32 {
331        self.axes[axis as usize]
332    }
333
334    /// A stick's deflection as `(x, y)`, +Y up, with no dead zone applied.
335    #[must_use]
336    pub const fn stick(&self, stick: Stick) -> (f32, f32) {
337        match stick {
338            Stick::Left => (self.axis(PadAxis::LeftX), self.axis(PadAxis::LeftY)),
339            Stick::Right => (self.axis(PadAxis::RightX), self.axis(PadAxis::RightY)),
340        }
341    }
342
343    /// A trigger's pull, 0…1, with no threshold applied.
344    #[must_use]
345    pub const fn trigger(&self, trigger: Trigger) -> f32 {
346        match trigger {
347            Trigger::Left => self.axis(PadAxis::LeftTrigger),
348            Trigger::Right => self.axis(PadAxis::RightTrigger),
349        }
350    }
351
352    /// Whether every axis is finite — what the map requires of a snapshot
353    /// before it takes one.
354    #[must_use]
355    pub fn is_finite(&self) -> bool {
356        self.axes.iter().all(|value| value.is_finite())
357    }
358}
359
360/// What a backend reports about a pad.
361#[derive(Clone, Copy, Debug, PartialEq)]
362pub enum GamepadEvent {
363    /// A pad appeared, at rest.
364    Connected {
365        /// The pad.
366        id: GamepadId,
367        /// Its family.
368        kind: PadKind,
369    },
370    /// A pad went away; whatever it held is released.
371    Disconnected {
372        /// The pad.
373        id: GamepadId,
374    },
375    /// What a pad is doing now.
376    State {
377        /// The pad.
378        id: GamepadId,
379        /// Its buttons and axes.
380        snapshot: GamepadSnapshot,
381    },
382}
383
384/// How far a stick or trigger must travel before the pad counts as the device
385/// that last spoke — see [`ActionMap::last_device`].
386///
387/// Past any stick's rest noise: XInput's own suggested dead zones put a worn
388/// stick's drift at about a quarter of the throw (`XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE`
389/// is 7849 of 32767), and a pad that took the glyphs back from the keyboard
390/// every time its stick settled would flicker the whole UI.
391pub const PAD_ACTIVITY_THRESHOLD: f32 = 0.5;
392
393/// A stick's `(x, y)` with a **scaled radial** dead zone applied: zero within
394/// `deadzone` of centre, then rescaled so the edge of the dead zone reads 0 and
395/// full throw reads 1, clamped to the unit disc, with the direction kept.
396///
397/// Radial rather than per-axis, because a per-axis dead zone snaps a slightly
398/// diagonal push onto the nearest axis; rescaled rather than cut, because a cut
399/// jumps from 0 straight to `deadzone` the moment the stick leaves it.
400///
401/// `deadzone` belongs in `0.0..1.0`; at or past 1.0 nothing gets through.
402#[must_use]
403pub fn radial_deadzone((x, y): (f32, f32), deadzone: f32) -> (f32, f32) {
404    let magnitude = x.hypot(y);
405    if !magnitude.is_finite() || magnitude <= deadzone {
406        return (0.0, 0.0);
407    }
408    let scaled = (magnitude.min(1.0) - deadzone) / (1.0 - deadzone);
409    (x / magnitude * scaled, y / magnitude * scaled)
410}
411
412/// A trigger's pull with `threshold` applied: zero at or below it, then
413/// rescaled so the threshold reads 0 and full pull reads 1.
414pub(crate) fn trigger_past(value: f32, threshold: f32) -> f32 {
415    let value = value.clamp(0.0, 1.0);
416    if value <= threshold {
417        0.0
418    } else {
419        (value - threshold) / (1.0 - threshold)
420    }
421}
422
423/// Whether a dead zone or threshold is one a binding can use: finite, and in
424/// `0.0..1.0` — at 1.0 nothing could ever get past it.
425pub(crate) fn valid_deadzone(value: f32) -> bool {
426    (0.0..1.0).contains(&value)
427}
428
429/// The input each of [`deflected`]'s answers is about, in its order.
430const DEFLECTED_INPUTS: [TracedInput; 4] = [
431    TracedInput::PadStick(Stick::Left),
432    TracedInput::PadStick(Stick::Right),
433    TracedInput::PadTrigger(Trigger::Left),
434    TracedInput::PadTrigger(Trigger::Right),
435];
436
437/// Whether each stick and trigger is past [`PAD_ACTIVITY_THRESHOLD`], in
438/// [`DEFLECTED_INPUTS`]' order.
439fn deflected(snapshot: &GamepadSnapshot) -> [bool; 4] {
440    let stick = |stick| {
441        let (x, y) = snapshot.stick(stick);
442        x.hypot(y) > PAD_ACTIVITY_THRESHOLD
443    };
444    [
445        stick(Stick::Left),
446        stick(Stick::Right),
447        snapshot.trigger(Trigger::Left) > PAD_ACTIVITY_THRESHOLD,
448        snapshot.trigger(Trigger::Right) > PAD_ACTIVITY_THRESHOLD,
449    ]
450}
451
452impl ActionMap {
453    /// Feed a pad event from any backend — the one way pads reach a map.
454    ///
455    /// Optional: the events mean the same to a game that reads them directly
456    /// and never calls this (see the module docs).
457    ///
458    /// **Activity** makes [`Device::Gamepad`] the last device, and the pad's
459    /// [`PadKind`] the [`ActionMap::last_pad_kind`], by the rule `device.rs`
460    /// states for the others: a button going down, or a stick or
461    /// trigger moving out past [`PAD_ACTIVITY_THRESHOLD`]. A release does not,
462    /// a stick returning to centre does not, and neither does a stick held
463    /// where it already was — holding it over is like holding a key down, and
464    /// the pad spoke when it went over. A connection or disconnection is not
465    /// the player speaking either.
466    ///
467    /// A [`GamepadEvent::State`] with a non-finite axis is dropped, and the
468    /// pad keeps its previous state. One for a pad never announced as
469    /// [`GamepadEvent::Connected`] connects it.
470    pub fn gamepad_event(&mut self, event: &GamepadEvent) {
471        // What this event pressed, for the trace once it has resolved.
472        let mut pressed = PadButtons::EMPTY;
473        let mut went_over = [false; 4];
474        match *event {
475            GamepadEvent::Connected { id, kind } => {
476                self.pads.insert(id, GamepadSnapshot::neutral(kind));
477            }
478            GamepadEvent::Disconnected { id } => {
479                self.pads.remove(&id);
480            }
481            GamepadEvent::State { id, snapshot } => {
482                if !snapshot.is_finite() {
483                    return;
484                }
485                let before = self
486                    .pads
487                    .insert(id, snapshot)
488                    .unwrap_or(GamepadSnapshot::neutral(snapshot.kind));
489                pressed = snapshot.buttons.difference(before.buttons);
490                for ((over, was), is) in went_over
491                    .iter_mut()
492                    .zip(deflected(&before))
493                    .zip(deflected(&snapshot))
494                {
495                    *over = is && !was;
496                }
497                if !pressed.is_empty() || went_over.contains(&true) {
498                    self.spoke(Device::Gamepad);
499                    self.last_pad_kind = Some(snapshot.kind);
500                }
501            }
502        }
503        self.repad();
504        for button in PadButton::ALL {
505            if pressed.contains(button) {
506                self.traced(|| TracedInput::PadButton(button));
507            }
508        }
509        for (over, input) in went_over.into_iter().zip(DEFLECTED_INPUTS) {
510            if over {
511                self.traced(|| input);
512            }
513        }
514    }
515
516    /// Drop every pad to neutral — what focus loss owes the pads, called beside
517    /// `crcbl::engine::lose_focus`.
518    ///
519    /// Each pad stays connected at rest until its next snapshot. **A button
520    /// held through this is withheld until it is released**, so a pad still
521    /// holding A when the window comes back does not press A again; sticks and
522    /// triggers are levels and come back with the next snapshot. The engine
523    /// loop calls this on the map a game hands it through `Game::actions`; a
524    /// game that feeds pads into a map it keeps to itself calls it on focus
525    /// loss too.
526    pub fn release_gamepads(&mut self) {
527        self.suppressed.pad_buttons = self.suppressed.pad_buttons.union(self.held_pad_buttons);
528        for snapshot in self.pads.values_mut() {
529            *snapshot = GamepadSnapshot::neutral(snapshot.kind);
530        }
531        // Not `repad`, which would lift the withholding just added: nothing is
532        // held now, and the lift waits for a snapshot that says so.
533        self.held_pad_buttons = PadButtons::EMPTY;
534        self.resolve_matching(Binding::reads_gamepad);
535    }
536
537    /// The pads currently connected, and what each last reported.
538    pub fn gamepads(&self) -> impl Iterator<Item = (GamepadId, &GamepadSnapshot)> {
539        self.pads.iter().map(|(&id, snapshot)| (id, snapshot))
540    }
541
542    /// Recompute what the pads hold between them, withhold a held button an
543    /// outranking chord's modifier handed to another context, lift the
544    /// withholding of any button no pad holds any more and of any stick or
545    /// trigger back at rest, and re-resolve every pad binding.
546    fn repad(&mut self) {
547        let before = self.held_pad_buttons;
548        self.held_pad_buttons = self
549            .pads
550            .values()
551            .fold(PadButtons::EMPTY, |held, pad| held.union(pad.buttons));
552        // The modifier of an outranking chord changes which context reads a
553        // button held through it — `context.rs`'s change of owner — and the
554        // new reader does not see it go down.
555        let held = self.held_pad_buttons;
556        for button in PadButton::ALL {
557            if before.contains(button)
558                && held.contains(button)
559                && self.routes.pad_reader(button, before) != self.routes.pad_reader(button, held)
560            {
561                self.suppressed.pad_buttons.insert(button);
562            }
563        }
564        self.suppressed.pad_buttons = self
565            .suppressed
566            .pad_buttons
567            .intersection(self.held_pad_buttons);
568        for stick in [Stick::Left, Stick::Right] {
569            if self.stick_rests(stick) {
570                self.suppressed.pad_sticks.remove(&stick);
571            }
572        }
573        for trigger in [Trigger::Left, Trigger::Right] {
574            if self.trigger_rests(trigger) {
575                self.suppressed.pad_triggers.remove(&trigger);
576            }
577        }
578        self.resolve_matching(Binding::reads_gamepad);
579    }
580
581    /// Whether `stick` is at rest on every pad: inside the dead zone of every
582    /// [`Binding::PadStick`] on it — reading zero through each of them — or
583    /// exactly centred if none reads it. What lifts a suppressed stick.
584    pub(crate) fn stick_rests(&self, stick: Stick) -> bool {
585        let deadzone = self
586            .bindings_all()
587            .filter_map(|binding| match *binding {
588                Binding::PadStick {
589                    stick: bound,
590                    deadzone,
591                } if bound == stick => Some(deadzone),
592                _ => None,
593            })
594            .reduce(f32::min)
595            .unwrap_or(0.0);
596        self.pads.values().all(|pad| {
597            let (x, y) = pad.stick(stick);
598            x.hypot(y) <= deadzone
599        })
600    }
601
602    /// Whether `trigger` is at rest on every pad: at or below the threshold of
603    /// every [`Binding::PadTrigger`] on it, or fully out if none reads it.
604    /// What lifts a suppressed trigger.
605    pub(crate) fn trigger_rests(&self, trigger: Trigger) -> bool {
606        let threshold = self
607            .bindings_all()
608            .filter_map(|binding| match *binding {
609                Binding::PadTrigger {
610                    trigger: bound,
611                    threshold,
612                } if bound == trigger => Some(threshold),
613                _ => None,
614            })
615            .reduce(f32::min)
616            .unwrap_or(0.0);
617        self.pads
618            .values()
619            .all(|pad| pad.trigger(trigger) <= threshold)
620    }
621
622    /// Every binding of every declared action.
623    fn bindings_all(&self) -> impl Iterator<Item = &Binding> {
624        self.slots.iter().flat_map(|slot| slot.decl.bindings.iter())
625    }
626}
627
628/// The furthest any pad pulls `trigger`.
629pub(crate) fn pad_trigger(pads: &Pads, trigger: Trigger) -> f32 {
630    pads.values()
631        .map(|pad| pad.trigger(trigger))
632        .fold(0.0, f32::max)
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    use crate::{ActionDecl, ActionKind, ActionMapError};
639    use crcbl_core::input::KeyCode;
640
641    const TICK: f32 = 1.0 / 60.0;
642    const PAD: GamepadId = GamepadId(7);
643
644    fn action(name: &str, kind: ActionKind, bindings: Vec<Binding>) -> ActionDecl {
645        ActionDecl {
646            name: name.to_owned(),
647            kind,
648            bindings,
649        }
650    }
651
652    fn state(id: GamepadId, snapshot: GamepadSnapshot) -> GamepadEvent {
653        GamepadEvent::State { id, snapshot }
654    }
655
656    fn holding(buttons: &[PadButton]) -> GamepadSnapshot {
657        GamepadSnapshot {
658            buttons: buttons.iter().copied().collect(),
659            ..GamepadSnapshot::neutral(PadKind::Xbox)
660        }
661    }
662
663    fn with_axis(mut snapshot: GamepadSnapshot, axis: PadAxis, value: f32) -> GamepadSnapshot {
664        snapshot.axes[axis as usize] = value;
665        snapshot
666    }
667
668    /// A map with `jump` on South and `connected` pad [`PAD`].
669    fn jump_map() -> ActionMap {
670        let mut map = ActionMap::new();
671        map.declare(action(
672            "jump",
673            ActionKind::Button,
674            vec![Binding::PadButton(PadButton::South)],
675        ));
676        map.gamepad_event(&GamepadEvent::Connected {
677            id: PAD,
678            kind: PadKind::Xbox,
679        });
680        map
681    }
682
683    fn close(actual: (f32, f32), expected: (f32, f32)) -> bool {
684        (actual.0 - expected.0).abs() < 1e-6 && (actual.1 - expected.1).abs() < 1e-6
685    }
686
687    /// **A pad button has the edges a key has**: pressed on the snapshot it
688    /// went down in, held while snapshots keep it down, released on the one
689    /// that lets go — and an unrelated button changes nothing.
690    #[test]
691    fn a_pad_button_presses_holds_and_releases_through_its_binding() {
692        let mut map = jump_map();
693        map.begin_tick(TICK);
694        map.gamepad_event(&state(PAD, holding(&[PadButton::East])));
695        assert!(!map.button_held("jump"), "East is not South");
696
697        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
698        assert!(map.just_pressed("jump"));
699        assert!(map.button_held("jump"));
700
701        map.begin_tick(TICK);
702        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
703        assert!(!map.just_pressed("jump"), "a re-sent level is not a press");
704        assert!(map.button_held("jump"));
705
706        map.begin_tick(TICK);
707        map.gamepad_event(&state(PAD, holding(&[])));
708        assert!(map.just_released("jump"));
709        assert!(!map.button_held("jump"));
710    }
711
712    /// Two pads both drive the binding, and it stays down until neither holds
713    /// it — the multiple-keys rule, for pads.
714    #[test]
715    fn a_button_held_on_two_pads_is_released_by_the_last_to_let_go() {
716        let mut map = jump_map();
717        let other = GamepadId(8);
718        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
719        map.gamepad_event(&state(other, holding(&[PadButton::South])));
720        map.gamepad_event(&state(PAD, holding(&[])));
721        assert!(map.button_held("jump"), "the other pad still holds it");
722        map.gamepad_event(&state(other, holding(&[])));
723        assert!(!map.button_held("jump"));
724
725        // And in the other order, so neither pad's id is what decides.
726        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
727        map.gamepad_event(&state(other, holding(&[PadButton::South])));
728        map.gamepad_event(&state(other, holding(&[])));
729        assert!(map.button_held("jump"), "the first pad still holds it");
730        map.gamepad_event(&state(PAD, holding(&[])));
731        assert!(!map.button_held("jump"));
732    }
733
734    /// The scaled radial dead zone against hand-worked values: zero inside it,
735    /// rescaled from its edge, direction kept, and clamped to the disc.
736    #[test]
737    fn the_radial_deadzone_zeroes_inside_rescales_outside_and_clamps() {
738        assert_eq!(radial_deadzone((0.1, 0.0), 0.2), (0.0, 0.0));
739        assert_eq!(radial_deadzone((0.0, -0.2), 0.2), (0.0, 0.0), "on the edge");
740        // (0.6 − 0.2) / (1 − 0.2) = 0.5
741        assert!(close(radial_deadzone((0.6, 0.0), 0.2), (0.5, 0.0)));
742        assert!(close(radial_deadzone((0.0, -1.0), 0.2), (0.0, -1.0)));
743        // Magnitude 1 exactly: kept as is.
744        assert!(close(radial_deadzone((0.6, 0.8), 0.2), (0.6, 0.8)));
745        // Magnitude 0.5 along (3, 4)/5: (0.5 − 0.2) / 0.8 = 0.375.
746        assert!(close(radial_deadzone((0.3, 0.4), 0.2), (0.225, 0.3)));
747        // Past the disc: clamped to it, direction kept.
748        assert!(close(radial_deadzone((3.0, 4.0), 0.2), (0.6, 0.8)));
749        assert!(
750            close(radial_deadzone((0.3, 0.4), 0.0), (0.3, 0.4)),
751            "no zone"
752        );
753    }
754
755    /// The trigger threshold against hand-worked values.
756    #[test]
757    fn the_trigger_threshold_zeroes_below_and_rescales_above() {
758        assert_eq!(trigger_past(0.0, 0.25), 0.0);
759        assert_eq!(trigger_past(0.25, 0.25), 0.0, "at the threshold");
760        // (0.5 − 0.25) / 0.75 = 1/3
761        assert!((trigger_past(0.5, 0.25) - 1.0 / 3.0).abs() < 1e-6);
762        assert_eq!(trigger_past(1.0, 0.25), 1.0);
763        assert_eq!(trigger_past(2.0, 0.25), 1.0, "clamped to full pull");
764    }
765
766    /// **Stick and trigger bindings resolve through their dead zones**, on the
767    /// kinds that read them.
768    #[test]
769    fn stick_and_trigger_bindings_resolve_through_their_deadzones() {
770        let mut map = ActionMap::new();
771        map.declare(action(
772            "move",
773            ActionKind::Axis2,
774            vec![Binding::PadStick {
775                stick: Stick::Left,
776                deadzone: 0.2,
777            }],
778        ));
779        map.declare(action(
780            "throttle",
781            ActionKind::Axis1,
782            vec![Binding::PadTrigger {
783                trigger: Trigger::Right,
784                threshold: 0.25,
785            }],
786        ));
787        map.declare(action(
788            "fire",
789            ActionKind::Button,
790            vec![Binding::PadTrigger {
791                trigger: Trigger::Right,
792                threshold: 0.25,
793            }],
794        ));
795
796        let rest = GamepadSnapshot::neutral(PadKind::Xbox);
797        let drift = with_axis(rest, PadAxis::LeftX, 0.15);
798        map.gamepad_event(&state(PAD, drift));
799        assert_eq!(map.axis2("move"), (0.0, 0.0), "drift inside the zone");
800
801        let pushed = with_axis(rest, PadAxis::LeftX, 0.6);
802        let pulled = with_axis(pushed, PadAxis::RightTrigger, 0.5);
803        map.gamepad_event(&state(PAD, pulled));
804        assert!(
805            close(map.axis2("move"), (0.5, 0.0)),
806            "{:?}",
807            map.axis2("move")
808        );
809        assert!((map.axis1("throttle") - 1.0 / 3.0).abs() < 1e-6);
810        assert!(map.just_pressed("fire"), "past the threshold is down");
811
812        map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::RightTrigger, 0.25)));
813        assert!(map.just_released("fire"), "at the threshold is up");
814        assert_eq!(map.axis1("throttle"), 0.0);
815    }
816
817    /// **+Y is up**: a stick pushed up drives the same direction as the key
818    /// bound to `up` on the same action, and a pushed-down one the other way.
819    #[test]
820    fn a_stick_pushed_up_reads_positive_y_like_the_up_key() {
821        let mut map = ActionMap::new();
822        map.declare(action(
823            "move",
824            ActionKind::Axis2,
825            vec![
826                Binding::Wasd {
827                    up: KeyCode::KeyW,
828                    down: KeyCode::KeyS,
829                    left: KeyCode::KeyA,
830                    right: KeyCode::KeyD,
831                },
832                Binding::PadStick {
833                    stick: Stick::Left,
834                    deadzone: 0.0,
835                },
836            ],
837        ));
838        let rest = GamepadSnapshot::neutral(PadKind::Xbox);
839
840        map.key_event(KeyCode::KeyW, true);
841        let by_key = map.axis2("move");
842        map.key_event(KeyCode::KeyW, false);
843        map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::LeftY, 1.0)));
844        assert_eq!(map.axis2("move"), by_key, "stick up is W");
845        assert_eq!(map.axis2("move"), (0.0, 1.0));
846
847        map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::LeftY, -0.5)));
848        assert_eq!(map.axis2("move"), (0.0, -0.5));
849    }
850
851    /// **The d-pad resolves exactly as the WASD composite does**: +Y up, a
852    /// diagonal a unit vector, opposite directions cancelling — checked
853    /// against hand-worked values and against a [`Binding::Wasd`] fed the same
854    /// directions.
855    #[test]
856    fn the_dpad_resolves_to_an_axis2_like_wasd() {
857        use PadButton::{DpadDown, DpadLeft, DpadRight, DpadUp};
858        let mut map = ActionMap::new();
859        map.declare(action("move", ActionKind::Axis2, vec![Binding::PadDpad]));
860        map.declare(action(
861            "walk",
862            ActionKind::Axis2,
863            vec![Binding::Wasd {
864                up: KeyCode::KeyW,
865                down: KeyCode::KeyS,
866                left: KeyCode::KeyA,
867                right: KeyCode::KeyD,
868            }],
869        ));
870        let key = |button| match button {
871            DpadUp => KeyCode::KeyW,
872            DpadDown => KeyCode::KeyS,
873            DpadLeft => KeyCode::KeyA,
874            _ => KeyCode::KeyD,
875        };
876        let half = std::f32::consts::FRAC_1_SQRT_2;
877        let cases: [(&[PadButton], (f32, f32)); 9] = [
878            (&[DpadUp], (0.0, 1.0)),
879            (&[DpadDown], (0.0, -1.0)),
880            (&[DpadLeft], (-1.0, 0.0)),
881            (&[DpadRight], (1.0, 0.0)),
882            (&[DpadUp, DpadRight], (half, half)),
883            (&[DpadUp, DpadLeft], (-half, half)),
884            (&[DpadDown, DpadRight], (half, -half)),
885            (&[DpadDown, DpadLeft], (-half, -half)),
886            (&[DpadUp, DpadDown], (0.0, 0.0)),
887        ];
888        for (buttons, expected) in cases {
889            map.gamepad_event(&state(PAD, holding(buttons)));
890            for &button in buttons {
891                map.key_event(key(button), true);
892            }
893            assert!(
894                close(map.axis2("move"), expected),
895                "{buttons:?}: {:?}",
896                map.axis2("move"),
897            );
898            assert_eq!(map.axis2("move"), map.axis2("walk"), "{buttons:?}");
899            for &button in buttons {
900                map.key_event(key(button), false);
901            }
902        }
903        map.gamepad_event(&state(PAD, holding(&[])));
904        assert_eq!(map.axis2("move"), (0.0, 0.0), "let go");
905    }
906
907    /// On a button the d-pad is down while any of its four is, as `Wasd` is
908    /// while any of its keys is; a face button is not one of them.
909    #[test]
910    fn the_dpad_on_a_button_is_down_while_any_direction_is() {
911        let mut map = ActionMap::new();
912        map.declare(action("any", ActionKind::Button, vec![Binding::PadDpad]));
913        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
914        assert!(!map.button_held("any"));
915        for button in PadButton::DPAD {
916            map.gamepad_event(&state(PAD, holding(&[button])));
917            assert!(map.button_held("any"), "{button:?}");
918            map.gamepad_event(&state(PAD, holding(&[])));
919            assert!(!map.button_held("any"));
920        }
921    }
922
923    /// **Keys, the d-pad and the stick share one unit disc**: they sum, then
924    /// clamp, so pushing two of them the same way is not faster and pushing
925    /// them apart is a diagonal or a cancel.
926    #[test]
927    fn keys_the_dpad_and_the_stick_sum_into_one_unit_disc() {
928        let mut map = ActionMap::new();
929        map.declare(action(
930            "move",
931            ActionKind::Axis2,
932            vec![
933                Binding::Wasd {
934                    up: KeyCode::KeyW,
935                    down: KeyCode::KeyS,
936                    left: KeyCode::KeyA,
937                    right: KeyCode::KeyD,
938                },
939                Binding::PadDpad,
940                Binding::PadStick {
941                    stick: Stick::Left,
942                    deadzone: 0.0,
943                },
944            ],
945        ));
946        let dpad = |button| holding(&[button]);
947        let half = std::f32::consts::FRAC_1_SQRT_2;
948
949        // The same way: clamped, not doubled.
950        map.gamepad_event(&state(
951            PAD,
952            with_axis(dpad(PadButton::DpadRight), PadAxis::LeftX, 0.5),
953        ));
954        assert!(
955            close(map.axis2("move"), (1.0, 0.0)),
956            "{:?}",
957            map.axis2("move")
958        );
959
960        // Crossed: a unit diagonal.
961        map.gamepad_event(&state(
962            PAD,
963            with_axis(dpad(PadButton::DpadRight), PadAxis::LeftY, 1.0),
964        ));
965        assert!(
966            close(map.axis2("move"), (half, half)),
967            "{:?}",
968            map.axis2("move")
969        );
970
971        // Opposed, inside the disc: the difference.
972        map.gamepad_event(&state(
973            PAD,
974            with_axis(dpad(PadButton::DpadLeft), PadAxis::LeftX, 0.5),
975        ));
976        assert!(
977            close(map.axis2("move"), (-0.5, 0.0)),
978            "{:?}",
979            map.axis2("move")
980        );
981
982        // A key against the d-pad cancels it; the three together up are up.
983        map.gamepad_event(&state(PAD, dpad(PadButton::DpadRight)));
984        map.key_event(KeyCode::KeyA, true);
985        assert_eq!(map.axis2("move"), (0.0, 0.0));
986        map.key_event(KeyCode::KeyA, false);
987        map.key_event(KeyCode::KeyW, true);
988        map.gamepad_event(&state(
989            PAD,
990            with_axis(dpad(PadButton::DpadUp), PadAxis::LeftY, 1.0),
991        ));
992        assert!(
993            close(map.axis2("move"), (0.0, 1.0)),
994            "{:?}",
995            map.axis2("move")
996        );
997    }
998
999    /// A d-pad press makes the pad the last device, as any pad button does,
1000    /// and letting go of it does not.
1001    #[test]
1002    fn a_dpad_press_makes_the_pad_the_last_device() {
1003        let mut map = ActionMap::new();
1004        map.declare(action("move", ActionKind::Axis2, vec![Binding::PadDpad]));
1005        map.key_event(KeyCode::KeyQ, true);
1006        assert_eq!(map.last_device(), Some(Device::Keyboard));
1007        map.gamepad_event(&state(PAD, holding(&[PadButton::DpadLeft])));
1008        assert_eq!(map.last_device(), Some(Device::Gamepad));
1009
1010        map.key_event(KeyCode::KeyQ, false);
1011        map.key_event(KeyCode::KeyQ, true);
1012        map.gamepad_event(&state(PAD, holding(&[])));
1013        assert_eq!(
1014            map.last_device(),
1015            Some(Device::Keyboard),
1016            "letting go of the d-pad is not the pad speaking",
1017        );
1018    }
1019
1020    /// A d-pad composite in a pushed context takes the four d-pad buttons from
1021    /// a plain [`Binding::PadButton`] beneath it, and leaves the rest.
1022    #[test]
1023    fn a_pushed_dpad_takes_its_four_buttons_from_the_context_beneath() {
1024        let mut map = jump_map();
1025        map.declare(action(
1026            "up",
1027            ActionKind::Button,
1028            vec![Binding::PadButton(PadButton::DpadUp)],
1029        ));
1030        map.declare_in(
1031            "menu",
1032            action("move", ActionKind::Axis2, vec![Binding::PadDpad]),
1033        );
1034        map.push_context("menu").expect("declared");
1035        map.gamepad_event(&state(PAD, holding(&[PadButton::DpadUp, PadButton::South])));
1036        assert_eq!(map.axis2("move"), (0.0, 1.0));
1037        assert!(!map.button_held("up"), "the menu owns DpadUp");
1038        assert!(map.button_held("jump"), "and not South");
1039    }
1040
1041    /// **The pad becomes the last device on activity and not on release**, as
1042    /// `device.rs` states for the others — and resting drift is not activity.
1043    #[test]
1044    fn the_last_device_becomes_the_pad_on_activity_and_not_on_release() {
1045        let mut map = ActionMap::new();
1046        map.gamepad_event(&GamepadEvent::Connected {
1047            id: PAD,
1048            kind: PadKind::Xbox,
1049        });
1050        assert_eq!(map.last_device(), None, "plugging in is not speaking");
1051
1052        map.gamepad_event(&state(PAD, holding(&[PadButton::North])));
1053        assert_eq!(
1054            map.last_device(),
1055            Some(Device::Gamepad),
1056            "nothing binds North"
1057        );
1058
1059        map.key_event(KeyCode::KeyQ, true);
1060        map.gamepad_event(&state(PAD, holding(&[])));
1061        assert_eq!(
1062            map.last_device(),
1063            Some(Device::Keyboard),
1064            "letting go of a pad button is not the pad speaking",
1065        );
1066
1067        let rest = GamepadSnapshot::neutral(PadKind::Xbox);
1068        let drift = with_axis(rest, PadAxis::RightX, 0.3);
1069        map.gamepad_event(&state(PAD, drift));
1070        assert_eq!(
1071            map.last_device(),
1072            Some(Device::Keyboard),
1073            "drift is not a push"
1074        );
1075
1076        let over = with_axis(rest, PadAxis::RightX, 0.9);
1077        map.gamepad_event(&state(PAD, over));
1078        assert_eq!(map.last_device(), Some(Device::Gamepad), "a push is");
1079
1080        map.key_event(KeyCode::KeyQ, true);
1081        map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::RightX, 0.95)));
1082        assert_eq!(
1083            map.last_device(),
1084            Some(Device::Keyboard),
1085            "a stick held over already spoke",
1086        );
1087        map.gamepad_event(&state(PAD, rest));
1088        assert_eq!(
1089            map.last_device(),
1090            Some(Device::Keyboard),
1091            "nor does centring"
1092        );
1093
1094        map.gamepad_event(&state(PAD, with_axis(rest, PadAxis::LeftTrigger, 0.8)));
1095        assert_eq!(
1096            map.last_device(),
1097            Some(Device::Gamepad),
1098            "a trigger pull is"
1099        );
1100    }
1101
1102    /// **Focus loss releases a held pad button, and it stays released until
1103    /// the player lets go** — the pad still holding it when snapshots resume is
1104    /// not a new press.
1105    #[test]
1106    fn focus_loss_releases_a_held_pad_button_until_it_is_let_go() {
1107        let mut map = jump_map();
1108        map.begin_tick(TICK);
1109        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1110        assert!(map.button_held("jump"));
1111
1112        map.begin_tick(TICK);
1113        map.release_gamepads();
1114        assert!(map.just_released("jump"));
1115        assert!(!map.button_held("jump"));
1116        assert_eq!(
1117            map.gamepads().collect::<Vec<_>>(),
1118            [(PAD, &GamepadSnapshot::neutral(PadKind::Xbox))],
1119            "still connected, at rest",
1120        );
1121
1122        map.begin_tick(TICK);
1123        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1124        assert!(!map.button_held("jump"), "withheld until released");
1125
1126        map.gamepad_event(&state(PAD, holding(&[])));
1127        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1128        assert!(map.just_pressed("jump"), "a fresh press after the lift");
1129    }
1130
1131    /// Sticks are levels: focus loss centres them, and the next snapshot puts
1132    /// them back without waiting for a lift.
1133    #[test]
1134    fn focus_loss_centres_a_stick_until_the_next_snapshot() {
1135        let mut map = ActionMap::new();
1136        map.declare(action(
1137            "move",
1138            ActionKind::Axis2,
1139            vec![Binding::PadStick {
1140                stick: Stick::Left,
1141                deadzone: 0.0,
1142            }],
1143        ));
1144        let up = with_axis(GamepadSnapshot::neutral(PadKind::Xbox), PadAxis::LeftY, 1.0);
1145        map.gamepad_event(&state(PAD, up));
1146        map.release_gamepads();
1147        assert_eq!(map.axis2("move"), (0.0, 0.0));
1148        map.gamepad_event(&state(PAD, up));
1149        assert_eq!(map.axis2("move"), (0.0, 1.0));
1150    }
1151
1152    /// **A disconnected pad's held state is released** — its button and its
1153    /// stick — and it is gone from the map.
1154    #[test]
1155    fn a_disconnected_pad_releases_what_it_held() {
1156        let mut map = jump_map();
1157        map.declare(action(
1158            "move",
1159            ActionKind::Axis2,
1160            vec![Binding::PadStick {
1161                stick: Stick::Left,
1162                deadzone: 0.1,
1163            }],
1164        ));
1165        map.begin_tick(TICK);
1166        let held = with_axis(holding(&[PadButton::South]), PadAxis::LeftX, 1.0);
1167        map.gamepad_event(&state(PAD, held));
1168        assert!(map.button_held("jump"));
1169        assert_eq!(map.axis2("move"), (1.0, 0.0));
1170
1171        map.begin_tick(TICK);
1172        map.gamepad_event(&GamepadEvent::Disconnected { id: PAD });
1173        assert!(map.just_released("jump"));
1174        assert_eq!(map.axis2("move"), (0.0, 0.0));
1175        assert_eq!(map.gamepads().count(), 0);
1176
1177        map.begin_tick(TICK);
1178        assert!(
1179            !map.button_held("jump"),
1180            "and not re-pressed by the next tick"
1181        );
1182    }
1183
1184    /// A non-finite snapshot is dropped and the pad keeps the one before it.
1185    #[test]
1186    fn a_non_finite_snapshot_is_dropped() {
1187        let mut map = jump_map();
1188        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1189        let poisoned = with_axis(holding(&[]), PadAxis::LeftX, f32::NAN);
1190        map.gamepad_event(&state(PAD, poisoned));
1191        assert!(map.button_held("jump"));
1192    }
1193
1194    /// A pad button is consumed by the topmost context binding it, and one
1195    /// held across a push is withheld from the new owner, like a key.
1196    #[test]
1197    fn a_pad_button_is_routed_and_withheld_like_a_key() {
1198        let mut map = jump_map();
1199        map.declare_in(
1200            "menu",
1201            action(
1202                "accept",
1203                ActionKind::Button,
1204                vec![Binding::PadButton(PadButton::South)],
1205            ),
1206        );
1207        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1208        map.push_context("menu").expect("declared");
1209        assert!(map.just_released("jump"), "the menu took South");
1210        assert!(!map.button_held("accept"), "withheld until released");
1211
1212        map.gamepad_event(&state(PAD, holding(&[])));
1213        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1214        assert!(map.just_pressed("accept"));
1215        assert!(!map.button_held("jump"));
1216    }
1217
1218    /// A dead zone or threshold nothing could get past, or a `NaN` one, is
1219    /// refused when it is declared or rebound rather than silently inert.
1220    #[test]
1221    fn an_unusable_deadzone_is_refused() {
1222        let mut map = ActionMap::new();
1223        for deadzone in [1.0, -0.1, f32::NAN] {
1224            assert_eq!(
1225                map.try_declare(action(
1226                    "move",
1227                    ActionKind::Axis2,
1228                    vec![Binding::PadStick {
1229                        stick: Stick::Left,
1230                        deadzone,
1231                    }],
1232                )),
1233                Err(ActionMapError::InvalidDeadzone("move".to_owned())),
1234            );
1235        }
1236        map.declare(action("fire", ActionKind::Button, Vec::new()));
1237        assert_eq!(
1238            map.rebind(
1239                "fire",
1240                vec![Binding::PadTrigger {
1241                    trigger: Trigger::Left,
1242                    threshold: f32::INFINITY,
1243                }],
1244            ),
1245            Err(ActionMapError::InvalidDeadzone("fire".to_owned())),
1246        );
1247    }
1248
1249    /// Ids from the allocator are never repeated.
1250    #[test]
1251    fn allocated_ids_are_distinct() {
1252        let a = GamepadId::allocate();
1253        let b = GamepadId::allocate();
1254        assert_ne!(a, b);
1255    }
1256
1257    /// A map with `jump` on South, `vault` on LB+South and `lean` on LB alone.
1258    fn chord_map() -> ActionMap {
1259        let mut map = jump_map();
1260        map.declare(action(
1261            "vault",
1262            ActionKind::Button,
1263            vec![Binding::PadChord {
1264                modifier: PadButton::LeftShoulder,
1265                button: PadButton::South,
1266            }],
1267        ));
1268        map.declare(action(
1269            "lean",
1270            ActionKind::Button,
1271            vec![Binding::PadButton(PadButton::LeftShoulder)],
1272        ));
1273        map
1274    }
1275
1276    /// **The chord takes the button, and reads its modifier without
1277    /// consuming it**: South alone jumps, LB+South vaults and does not jump,
1278    /// and LB's own binding sees LB either way.
1279    #[test]
1280    fn a_pad_chord_takes_its_button_and_leaves_its_modifier_alone() {
1281        let mut map = chord_map();
1282        map.begin_tick(TICK);
1283        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1284        assert!(map.button_held("jump"));
1285        assert!(!map.button_held("vault"));
1286
1287        map.begin_tick(TICK);
1288        map.gamepad_event(&state(PAD, holding(&[])));
1289        map.gamepad_event(&state(
1290            PAD,
1291            holding(&[PadButton::LeftShoulder, PadButton::South]),
1292        ));
1293        assert!(map.button_held("vault"));
1294        assert!(!map.button_held("jump"), "the plain binding fired too");
1295        assert!(map.button_held("lean"), "the modifier was consumed");
1296    }
1297
1298    /// **Holding the button and then the modifier hands it over**: the plain
1299    /// binding releases and the chord presses, as a key's chord does.
1300    #[test]
1301    fn a_modifier_pressed_over_a_held_button_hands_the_button_to_the_chord() {
1302        let mut map = chord_map();
1303        map.begin_tick(TICK);
1304        map.gamepad_event(&state(PAD, holding(&[PadButton::South])));
1305        assert!(map.button_held("jump"));
1306
1307        map.begin_tick(TICK);
1308        map.gamepad_event(&state(
1309            PAD,
1310            holding(&[PadButton::South, PadButton::LeftShoulder]),
1311        ));
1312        assert!(map.just_released("jump"));
1313        assert!(map.just_pressed("vault"));
1314    }
1315
1316    /// **A pad chord adds +1.0 to an axis**, as a key's chord does.
1317    #[test]
1318    fn a_pad_chord_drives_an_axis1() {
1319        let mut map = jump_map();
1320        map.declare(action(
1321            "throttle",
1322            ActionKind::Axis1,
1323            vec![Binding::PadChord {
1324                modifier: PadButton::RightShoulder,
1325                button: PadButton::North,
1326            }],
1327        ));
1328        map.gamepad_event(&state(PAD, holding(&[PadButton::North])));
1329        assert!(map.axis1("throttle").abs() < 1e-6, "no modifier, no chord");
1330        map.gamepad_event(&state(
1331            PAD,
1332            holding(&[PadButton::RightShoulder, PadButton::North]),
1333        ));
1334        assert!((map.axis1("throttle") - 1.0).abs() < 1e-6);
1335    }
1336
1337    /// **A chord shadows plain bindings only in the context that owns the
1338    /// button**: declared in a context off the stack, it leaves South with
1339    /// the base; pushed, it takes South.
1340    #[test]
1341    fn a_pad_chord_shadows_only_in_the_context_that_owns_its_button() {
1342        let mut map = jump_map();
1343        map.declare_in(
1344            "vehicle",
1345            action(
1346                "boost",
1347                ActionKind::Button,
1348                vec![Binding::PadChord {
1349                    modifier: PadButton::LeftShoulder,
1350                    button: PadButton::South,
1351                }],
1352            ),
1353        );
1354        let both = holding(&[PadButton::LeftShoulder, PadButton::South]);
1355        map.gamepad_event(&state(PAD, both));
1356        assert!(
1357            map.button_held("jump"),
1358            "a chord off the stack shadowed South"
1359        );
1360
1361        map.gamepad_event(&state(PAD, holding(&[])));
1362        map.push_context("vehicle").unwrap();
1363        map.gamepad_event(&state(PAD, both));
1364        assert!(map.button_held("boost"));
1365        assert!(!map.button_held("jump"));
1366    }
1367}