Skip to main content

crcbl_input/
lib.rs

1//! Device-agnostic action system: gameplay and UI code consume **actions**,
2//! never raw devices. Keyboard, mouse, gamepad and on-screen controls are
3//! interchangeable binding sources behind one config layer.
4//!
5//! # Contexts
6//!
7//! Every action belongs to one named context: [`ActionMap::declare`] puts it in
8//! [`GAMEPLAY_CONTEXT`], which is always active, and [`ActionMap::declare_in`]
9//! names another. [`ActionMap::push_context`] and [`ActionMap::pop_context`]
10//! stack contexts over it — [`ActionMap::push_context_modal`] one that passes
11//! nothing it does not bind — [`GLOBAL_CONTEXT`] stays above them all, and
12//! **the topmost active context that binds an input consumes it** — `context.rs` has the rules, including what happens to a key
13//! held while the stack changes, and what [`ActionMap::suppress_held`] and
14//! [`ActionMap::suppress_held_action`] withhold on request. [`ui`] declares the
15//! engine's reserved `ui`
16//! context, [`text`] the `text` context a text field pushes over it, and
17//! [`list`] the `list` context an open pop-up list pushes over it.
18//!
19//! # Patterns and devices
20//!
21//! [`ActionMap::set_repeat`] attaches a [`Repeat`] to an action, and
22//! [`ActionMap::set_tap`], [`ActionMap::set_hold`] and
23//! [`ActionMap::set_double_tap`] attach a [`Tap`], a [`Hold`] and a
24//! [`DoubleTap`], all evaluated on the clock [`ActionMap::begin_tick`] advances
25//! (`repeat.rs`, and `patterns.rs` for how the last three share a press and
26//! what [`ActionMap::cancel_patterns`] cancels). [`ActionMap::set_emits`] makes
27//! one of the last three press a named button action when it fires
28//! (`emit.rs`).
29//! [`ActionMap::last_device`] names the kind of [`Device`] that last spoke,
30//! [`ActionMap::last_device_changed`] says it just changed, and
31//! [`ActionMap::hint`] how an action's binding is shown to the player on it
32//! ([`hint`]).
33//!
34//! # Why an input did what it did
35//!
36//! [`ActionMap::set_tracing`] records, for each press, the context that
37//! consumed it and every binding that read it — or that a modal context
38//! stopped it, or that nothing binds it — in a bounded ring an input
39//! inspector reads ([`trace`]).
40//!
41//! # Gamepads
42//!
43//! Every pad backend emits [`GamepadEvent`]s, by the conventions `gamepad.rs`
44//! states — positional buttons, sticks with +Y up, triggers 0…1, raw axes. A
45//! game reads them directly, or feeds [`ActionMap::gamepad_event`], where
46//! [`Binding::PadButton`], [`Binding::PadDpad`], [`Binding::PadStick`] and
47//! [`Binding::PadTrigger`] read them. The Windows backend is `xinput`,
48//! compiled on Windows only, the Linux one is `evdev`, compiled on Linux only,
49//! the macOS one is `game_controller`, compiled on macOS only, and the browser
50//! one is `web_gamepad`, compiled on `wasm32` only: no other target has a
51//! backend yet, and none has a stand-in that would report "no pads" as though
52//! it had looked.
53
54mod active_pad;
55pub mod binding_asset;
56pub mod binding_text;
57mod context;
58mod device;
59mod eight_way;
60mod emit;
61// Linux-only: the `evdev` backend. Compiled into every target's tests too, so
62// its mapping, normalisation, layouts and poller run on the Windows and macOS
63// runners; only the device access is Linux-only.
64#[cfg(any(target_os = "linux", test))]
65pub mod evdev;
66#[cfg(test)]
67mod ffi_layout;
68// The clamp for pad values that arrive as floats, which the browser and
69// GameController backends share.
70#[cfg(any(target_arch = "wasm32", target_os = "macos", test))]
71mod float_axis;
72// macOS-only: the GameController.framework backend. Compiled into every
73// target's tests too, so its mapping, classification and poller run on the
74// Windows and Linux runners; only the framework access is macOS-only.
75#[cfg(any(target_os = "macos", test))]
76pub mod game_controller;
77mod gamepad;
78pub mod hint;
79pub mod list;
80mod overrides;
81mod patterns;
82// The name match the browser and GameController backends name a pad's family
83// from when they have no USB ids.
84#[cfg(any(target_arch = "wasm32", target_os = "macos", test))]
85mod product_name;
86mod repeat;
87pub mod text;
88pub mod trace;
89pub mod ui;
90// The vendor and product ids the evdev and browser backends name a pad's
91// family from; only those two read them.
92#[cfg(any(target_os = "linux", target_arch = "wasm32", test))]
93mod usb;
94// wasm32-only: the Web Gamepad API backend. Compiled into every target's tests
95// too, so its mapping, poller and shim entry points run on the native runners;
96// only the symbols a page calls are exported, and only from a wasm32 build.
97#[cfg(any(target_arch = "wasm32", test))]
98pub mod web_gamepad;
99// Compiled into every target's tests too, so the mapping and the layouts are
100// checked on the Linux and macOS runners; only the loader is Windows-only.
101#[cfg(any(windows, test))]
102pub mod xinput;
103
104pub use active_pad::{ActivePad, StickUpdate};
105pub use binding_asset::{AssetRefusal, AssetWriteError, BindingAssetError};
106pub use binding_text::BindingParseError;
107pub use context::{GAMEPLAY_CONTEXT, GLOBAL_CONTEXT};
108pub use device::Device;
109pub use eight_way::{EIGHT_WAY_SECTOR, eight_way};
110pub use emit::Pattern;
111pub use gamepad::{
112    GamepadEvent, GamepadId, GamepadSnapshot, PAD_ACTIVITY_THRESHOLD, PadAxis, PadButton,
113    PadButtons, PadKind, Stick, Trigger, radial_deadzone,
114};
115pub use hint::{DefaultLabels, Hint, HintLabels};
116pub use overrides::{ActionOverride, OverrideRefusal};
117pub use patterns::{DOUBLE_TAP_WINDOW, DoubleTap, HOLD_TIME, Hold, TAP_TIME, Tap};
118pub use repeat::{Cardinal, REPEAT_DELAY, REPEAT_INTERVAL, Repeat};
119pub use trace::{Outcome, RESOLUTION_TRACE_CAP, TraceEntry, TracedInput, TracedRead};
120
121use context::{Routes, Suppressed, View};
122use crcbl_core::input::{KeyCode, PointerButton};
123use patterns::PatternState;
124use repeat::RepeatState;
125use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
126
127/// Every connected pad and what it last reported, ordered by id so a sum over
128/// them runs in the same order every run.
129type Pads = BTreeMap<GamepadId, GamepadSnapshot>;
130
131/// Every key held down, with the serial of the press that put it there — a
132/// later press has a larger one, which is how a [`Binding::ScrollChord`] finds
133/// the most recently pressed key.
134type HeldKeys = HashMap<KeyCode, u64>;
135
136// ---------------------------------------------------------------------------
137// Action primitives
138// ---------------------------------------------------------------------------
139
140/// A digital action: pressed / released / held with duration.
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub enum ButtonState {
143    /// The button just went down this tick (or is being pressed for the first time).
144    Pressed,
145    /// The button came up this tick.
146    Released,
147    /// The button has been held for `duration` seconds.
148    Held {
149        /// Seconds since the button first went down.
150        duration: f32,
151    },
152}
153
154/// Button action with edge-detection flags.
155///
156/// `just_pressed` and `just_released` are true only on the tick the transition
157/// occurred. They are cleared by [`ActionMap::begin_tick`] (and may also be
158/// cleared earlier by the consumer via [`ActionMap::action_mut`]).
159#[derive(Debug, Clone)]
160pub struct ButtonAction {
161    /// Current button state.
162    pub state: ButtonState,
163    /// True on the tick the button went down.
164    pub just_pressed: bool,
165    /// True on the tick the button came up.
166    pub just_released: bool,
167}
168
169impl ButtonAction {
170    fn reset_edges(&mut self) {
171        self.just_pressed = false;
172        self.just_released = false;
173    }
174}
175
176impl Default for ButtonAction {
177    fn default() -> Self {
178        Self {
179            state: ButtonState::Released,
180            just_pressed: false,
181            just_released: false,
182        }
183    }
184}
185
186/// 1-D analog axis: −1.0 … 1.0 (triggers, scroll wheels, an absolute pointer
187/// coordinate).
188#[derive(Debug, Clone, Copy, Default, PartialEq)]
189pub struct Axis1Action {
190    /// Current axis value.
191    pub value: f32,
192    /// True on the tick a [`Binding::PointerPosition`] on this action reported a
193    /// coordinate different from the one it was holding.
194    ///
195    /// The edge an *absolute* source has and a relative one does not. A scroll
196    /// axis returns to zero every [`ActionMap::begin_tick`], so a non-zero value
197    /// already means "this moved"; a pointer axis holds its position across
198    /// ticks — that is the whole point of it — so its value says where the
199    /// pointer is and nothing about whether the player just asked for anything.
200    ///
201    /// Cleared by [`ActionMap::begin_tick`], like [`ButtonAction::just_pressed`].
202    /// A consumer that drives one thing from both a pointer and the keyboard
203    /// reads this to decide which spoke most recently — see the
204    /// [`Binding::PointerPosition`] docs.
205    pub pointer_moved: bool,
206}
207
208/// 2-D analog axis (WASD composite, stick, mouse motion).
209///
210/// Key-driven composites are normalized to at most unit length; mouse motion is
211/// a raw pixel delta and is not.
212#[derive(Debug, Clone, Copy, Default, PartialEq)]
213pub struct Axis2Action {
214    /// Horizontal component.
215    pub x: f32,
216    /// Vertical component.
217    pub y: f32,
218}
219
220/// A unified action value — one variant per action kind.
221#[derive(Debug, Clone)]
222pub enum ActionValue {
223    /// A button action.
224    Button(ButtonAction),
225    /// A 1-D axis action.
226    Axis1(Axis1Action),
227    /// A 2-D axis action.
228    Axis2(Axis2Action),
229}
230
231// ---------------------------------------------------------------------------
232// Bindings
233// ---------------------------------------------------------------------------
234
235/// Which coordinate of the pointer a [`Binding::PointerPosition`] reads.
236///
237/// One binding with an axis rather than a `PointerX`/`PointerY` pair: the two
238/// would be the same variant twice, and a rebind menu listing both as unrelated
239/// sources is a menu that has to explain why.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum PointerAxis {
242    /// Horizontal: −1.0 at the left edge of the surface, +1.0 at the right.
243    X,
244    /// Vertical: −1.0 at the **bottom** edge, +1.0 at the top.
245    Y,
246}
247
248/// The modifier a [`Binding::Chord`] or a [`Binding::ButtonChord`] waits for:
249/// either of its two keys.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
251pub enum Modifier {
252    /// [`KeyCode::ShiftLeft`] or [`KeyCode::ShiftRight`].
253    Shift,
254    /// [`KeyCode::ControlLeft`] or [`KeyCode::ControlRight`].
255    Control,
256    /// [`KeyCode::AltLeft`] or [`KeyCode::AltRight`].
257    Alt,
258    /// [`KeyCode::SuperLeft`] or [`KeyCode::SuperRight`].
259    Super,
260}
261
262impl Modifier {
263    /// The left and the right key of this modifier.
264    #[must_use]
265    pub const fn keys(self) -> [KeyCode; 2] {
266        match self {
267            Self::Shift => [KeyCode::ShiftLeft, KeyCode::ShiftRight],
268            Self::Control => [KeyCode::ControlLeft, KeyCode::ControlRight],
269            Self::Alt => [KeyCode::AltLeft, KeyCode::AltRight],
270            Self::Super => [KeyCode::SuperLeft, KeyCode::SuperRight],
271        }
272    }
273
274    /// The modifier `key` is one side of, if it is one.
275    #[must_use]
276    pub const fn of(key: KeyCode) -> Option<Self> {
277        match key {
278            KeyCode::ShiftLeft | KeyCode::ShiftRight => Some(Self::Shift),
279            KeyCode::ControlLeft | KeyCode::ControlRight => Some(Self::Control),
280            KeyCode::AltLeft | KeyCode::AltRight => Some(Self::Alt),
281            KeyCode::SuperLeft | KeyCode::SuperRight => Some(Self::Super),
282            _ => None,
283        }
284    }
285
286    /// Whether either side is in `held`.
287    fn held(self, held: &HeldKeys) -> bool {
288        self.keys().iter().any(|key| held.contains_key(key))
289    }
290}
291
292/// A binding source: what raw input triggers this action.
293///
294/// Keyboard, mouse, gamepad and on-screen controls. A game
295/// played with **one** finger needs nothing touch-specific — the platforms
296/// deliver the primary contact as an ordinary pointer, so
297/// [`Binding::MouseButton`] and [`Binding::PointerPosition`] are what a phone
298/// plays it through. [`Binding::Virtual`] is what a game needs when one finger
299/// is not enough.
300///
301/// `PartialEq` and not `Eq`: a pad binding's dead zone is an `f32`.
302///
303/// Its `Display` and `FromStr` are a stable text form, `KeyR` or `Pad:South`,
304/// for a player's saved rebinds; [`binding_text`] has the spelling of each.
305#[derive(Debug, Clone, PartialEq)]
306pub enum Binding {
307    /// A single key.
308    Key(KeyCode),
309    /// A mouse button.
310    MouseButton(PointerButton),
311    /// Mouse motion (delta in pixels).
312    MouseMotion,
313    /// Mouse scroll wheel.
314    MouseScroll,
315    /// Where the pointer **is**, normalised to the surface it is over: −1.0 at
316    /// one edge, +1.0 at the other, with +X right and +Y up.
317    ///
318    /// Drives an [`ActionKind::Axis1`], whose −1…1 range is already exactly a
319    /// normalised coordinate. Feeding an `Axis2` instead would put a *place* in
320    /// the same value as [`Binding::Wasd`]'s *direction*, and a consumer handed
321    /// `(0.5, 0.0)` could not tell "half way to the right edge" from "moving
322    /// right at half speed"; a fourth [`ActionKind`] for one binding would be a
323    /// whole value shape for a case `Axis1` already covers. Any other kind
324    /// ignores this binding, as [`Binding::MouseMotion`] does on a button.
325    ///
326    /// **Normalised, not pixels**, because pixels are the surface's business:
327    /// a device-pixel coordinate makes every consumer redo the DPI arithmetic
328    /// the windowing layer already did, and get it wrong on exactly the displays
329    /// nobody develops on. What the consumer still owns is the step from the
330    /// surface to its own world — a camera's half width is not the map's to
331    /// know.
332    ///
333    /// **A pointer that leaves keeps its last position.** Leaving is not a
334    /// command: the platform reports it with no coordinate at all, so there is
335    /// nothing to resolve, and an axis that recentred itself would walk a
336    /// paddle to the middle of the field every time a finger lifted.
337    ///
338    /// **On an axis that also has a relative source** — a [`Binding::KeyAxis`],
339    /// a [`Binding::MouseScroll`] — this one wins whenever the pointer has a
340    /// position, because summing them would add a place to a rate and produce
341    /// neither. That is a resolution, not an arbitration: a consumer whose
342    /// keyboard means something genuinely different from its pointer (a paddle
343    /// driven left at a speed, versus a paddle put at a coordinate) declares two
344    /// actions and picks between them with
345    /// [`Axis1Action::pointer_moved`].
346    PointerPosition {
347        /// Which coordinate this action reads.
348        axis: PointerAxis,
349    },
350    /// Two keys driving one 1-D axis: `negative` contributes −1.0, `positive`
351    /// +1.0, and both at once cancel.
352    ///
353    /// A single [`Binding::Key`] on an `Axis1` can only ever push the axis
354    /// positive, so a keyboard-driven throttle or zoom needs this.
355    KeyAxis {
356        /// Key for the −1.0 direction.
357        negative: KeyCode,
358        /// Key for the +1.0 direction.
359        positive: KeyCode,
360    },
361    /// A key that counts only while a [`Modifier`] is held — Shift+Tab.
362    ///
363    /// **The more specific binding takes the key.** While the modifier is
364    /// held, every plain [`Binding::Key`], [`Binding::KeyAxis`] and
365    /// [`Binding::Wasd`] on the same key, in the context that owns the key,
366    /// reads it as up — so `ui_next` on Tab and `ui_prev` on Shift+Tab never
367    /// both fire. Holding Tab and then pressing Shift therefore releases the
368    /// one and presses the other.
369    ///
370    /// **The modifier is read, not consumed**: a chord owns its `key` and
371    /// nothing else, so a Shift+Tab in a pushed context leaves a sprint bound
372    /// to Shift in the context beneath it alone.
373    Chord {
374        /// The modifier that must be held.
375        modifier: Modifier,
376        /// The key the chord owns.
377        key: KeyCode,
378    },
379    /// A mouse button that counts only while a [`Modifier`] is held —
380    /// Alt+right click beside a plain right click. The mouse's
381    /// [`Binding::Chord`], on the same two rules, and read as
382    /// [`Binding::MouseButton`] is: down on an [`ActionKind::Button`], inert
383    /// on the other kinds.
384    ///
385    /// **The more specific binding takes the button.** While the modifier is
386    /// held, every plain [`Binding::MouseButton`] on the same button, in the
387    /// context that owns the button, reads it as up — so an aim on the right
388    /// button and a zoom on Alt+right never both fire. Holding the button and
389    /// then pressing the modifier releases the one and presses the other;
390    /// letting the modifier go first hands the button back.
391    ///
392    /// **The modifier is read, not consumed**: a button chord owns its
393    /// `button` and nothing else, so a lean bound to Alt still sees every
394    /// press of Alt, chorded or not.
395    ButtonChord {
396        /// The modifier that must be held.
397        modifier: Modifier,
398        /// The mouse button the chord owns.
399        button: PointerButton,
400    },
401    /// The mouse wheel, read only while `held` is down — Z+wheel, Ctrl+wheel.
402    ///
403    /// Read as [`Binding::MouseScroll`] is: the vertical delta on an
404    /// [`ActionKind::Axis1`], inert on the other kinds. It reads the wheel as
405    /// one, too, so it takes the wheel for its context as a `MouseScroll`
406    /// would.
407    ///
408    /// **The more specific binding takes the wheel**, as it takes the key for a
409    /// [`Binding::Chord`]: while the `held` key of any scroll chord in the
410    /// context that owns the wheel is down, every plain `MouseScroll` in that
411    /// context reads zero. **With several of those keys down, the most recently
412    /// pressed one takes the wheel** and the chords on the others read zero,
413    /// so letting go of it hands the wheel back to the one pressed before. An
414    /// OS auto-repeat of a key already down is not a press and takes nothing.
415    ///
416    /// **The held key is read, not consumed**, as a chord's modifier is: a
417    /// scroll chord owns the wheel and nothing else, so a [`Binding::Key`] on
418    /// Z still sees Z — a game that also taps Z cancels that tap with
419    /// [`ActionMap::cancel_patterns`] when the wheel turns. A held key
420    /// withheld from its owner (see `context.rs`) is up for this read too.
421    ///
422    /// **Only an enabled action's scroll chord competes.** A key whose action
423    /// is disabled ([`ActionMap::set_enabled`]) takes the wheel from nothing
424    /// and silences no plain `MouseScroll`, however recently it was pressed:
425    /// Ctrl held for one chord keeps the wheel when Alt goes down for a chord
426    /// that cannot act right now. Enabling it again restores its place by
427    /// when its key went down, not by when it was enabled.
428    ScrollChord {
429        /// The key that must be down; any key, modifiers included.
430        held: KeyCode,
431    },
432    /// An **on-screen control**, by the id the widget drawing it was given.
433    ///
434    /// The touch row of a binding table, and the one binding whose device is a
435    /// piece of the game's own UI: a `crcbl-ui` control hit-tests the raw
436    /// contacts, decides what its geometry makes of them, and reports the
437    /// result here through [`ActionMap::virtual_button`] or
438    /// [`ActionMap::virtual_stick`]. From this side it is a device like any
439    /// other — the same action can carry a key, a mouse button and one of
440    /// these, and nothing downstream can tell which of them spoke.
441    ///
442    /// That split is deliberate and it is why there is no binding on a raw
443    /// contact. A finger reports a *place on the glass*; what it means is a
444    /// question only the thing drawn under it can answer, and an [`ActionMap`]
445    /// that hit-tested widgets would have to be told where every control was
446    /// drawn this frame.
447    ///
448    /// **A control is a button or a stick, and an action reads whichever
449    /// matches its own kind**: an [`ActionKind::Button`] is down while the
450    /// named control is held, an [`ActionKind::Axis2`] takes the named
451    /// control's deflection, and an [`ActionKind::Axis1`] takes **nothing** —
452    /// reducing a stick to one axis is a choice the control would have to make
453    /// and none of them makes it. A binding whose kind does not match is inert,
454    /// the way [`Binding::MouseMotion`] is on a button.
455    ///
456    /// The vector arrives already inside the unit disc, and is clamped to it
457    /// again here: a control with a longer throw than its own radius is a
458    /// broken control, not a player asking to run faster.
459    Virtual(String),
460    /// WASD composite: four keys mapped to a 2-D axis.
461    ///
462    /// The composite is normalized, so a diagonal is a unit vector rather than
463    /// `(1, 1)` — otherwise diagonal movement is 41% faster than cardinal.
464    Wasd {
465        /// Key for the +Y direction (forward / up).
466        up: KeyCode,
467        /// Key for the −Y direction (back / down).
468        down: KeyCode,
469        /// Key for the −X direction (left).
470        left: KeyCode,
471        /// Key for the +X direction (right).
472        right: KeyCode,
473    },
474    /// A pad button, by position — see [`PadButton`].
475    ///
476    /// Read like a [`Binding::Key`]: down on an [`ActionKind::Button`], +1.0 on
477    /// an [`ActionKind::Axis1`], nothing on an [`ActionKind::Axis2`] — the
478    /// d-pad's `Axis2` is [`Binding::PadDpad`]. Down while any connected pad
479    /// holds it.
480    PadButton(PadButton),
481    /// The d-pad as one 2-D axis: [`Binding::Wasd`] over
482    /// [`PadButton::DpadUp`], [`PadButton::DpadDown`], [`PadButton::DpadLeft`]
483    /// and [`PadButton::DpadRight`], +Y up.
484    ///
485    /// Resolved exactly as `Wasd` is: into the same unit disc as the keys and
486    /// the sticks on the action, so a diagonal is a unit vector and opposite
487    /// directions cancel; down on an [`ActionKind::Button`] while any of the
488    /// four is held; inert on an [`ActionKind::Axis1`]. It owns the four
489    /// buttons for routing, as `Wasd` owns its four keys.
490    PadDpad,
491    /// A pad stick's deflection, +Y up, through a **scaled radial** dead zone:
492    /// zero within `deadzone` of centre, rescaled so the zone's edge reads 0
493    /// and full throw reads 1.
494    ///
495    /// Drives an [`ActionKind::Axis2`], into the same unit disc as a
496    /// [`Binding::Wasd`] or a [`Binding::Virtual`] stick on the action, so a
497    /// stick and a key pushed together ask for one direction. Inert on the
498    /// other kinds, as a [`Binding::Virtual`] stick is on an `Axis1`.
499    PadStick {
500        /// Which stick.
501        stick: Stick,
502        /// The dead zone's radius, in `0.0..1.0`; anything else is refused
503        /// with [`ActionMapError::InvalidDeadzone`].
504        deadzone: f32,
505    },
506    /// A pad button that counts only while another pad button is held — a
507    /// held shoulder giving the face buttons a second layer. The pad's
508    /// [`Binding::Chord`], on the same two rules:
509    ///
510    /// **The more specific binding takes the button.** While `modifier` is
511    /// held, every plain [`Binding::PadButton`] on `button` (and a
512    /// [`Binding::PadDpad`] reading it), in the context that owns the button,
513    /// reads it as up — so a jump on South and a vault on LB+South never both
514    /// fire. Holding South and then pressing LB releases the one and presses
515    /// the other. A chord in a context beneath the owner reads nothing,
516    /// unless its action is marked with [`ActionMap::set_pad_chords_outrank`]:
517    /// then it takes the button from the contexts above while `modifier` is
518    /// held (`context.rs`).
519    ///
520    /// **The modifier is read, not consumed**: a pad chord owns its `button`
521    /// and nothing else, so `modifier` can still carry a plain binding of its
522    /// own, which sees every press of it, chorded or not.
523    PadChord {
524        /// The pad button that must be held.
525        modifier: PadButton,
526        /// The pad button the chord owns.
527        button: PadButton,
528    },
529    /// A pad trigger past a threshold.
530    ///
531    /// On an [`ActionKind::Button`], down while the pull is past `threshold`;
532    /// on an [`ActionKind::Axis1`], the pull rescaled so the threshold reads 0
533    /// and full pull reads 1. Inert on an [`ActionKind::Axis2`].
534    PadTrigger {
535        /// Which trigger.
536        trigger: Trigger,
537        /// The threshold, in `0.0..1.0`; anything else is refused with
538        /// [`ActionMapError::InvalidDeadzone`].
539        threshold: f32,
540    },
541}
542
543impl Binding {
544    /// Calls `visit` with every key this binding owns — the keys a context
545    /// consumes by binding it. A [`Binding::Chord`]'s modifier is not one, and
546    /// neither is a [`Binding::ScrollChord`]'s held key.
547    pub fn visit_keys(&self, mut visit: impl FnMut(KeyCode)) {
548        match self {
549            Self::Key(key) | Self::Chord { key, .. } => visit(*key),
550            Self::KeyAxis { negative, positive } => {
551                visit(*negative);
552                visit(*positive);
553            }
554            Self::Wasd {
555                up,
556                down,
557                left,
558                right,
559            } => {
560                visit(*up);
561                visit(*down);
562                visit(*left);
563                visit(*right);
564            }
565            Self::MouseButton(_)
566            | Self::ButtonChord { .. }
567            | Self::MouseMotion
568            | Self::MouseScroll
569            | Self::ScrollChord { .. }
570            | Self::PointerPosition { .. }
571            | Self::Virtual(_)
572            | Self::PadButton(_)
573            | Self::PadChord { .. }
574            | Self::PadDpad
575            | Self::PadStick { .. }
576            | Self::PadTrigger { .. } => {}
577        }
578    }
579
580    /// Whether this binding reads the mouse wheel.
581    const fn reads_wheel(&self) -> bool {
582        matches!(self, Self::MouseScroll | Self::ScrollChord { .. })
583    }
584
585    /// The mouse button this binding owns, if it reads one: a
586    /// [`Binding::ButtonChord`]'s button as much as a
587    /// [`Binding::MouseButton`]'s.
588    const fn mouse_button(&self) -> Option<PointerButton> {
589        match *self {
590            Self::MouseButton(button) | Self::ButtonChord { button, .. } => Some(button),
591            _ => None,
592        }
593    }
594
595    /// Whether this binding reads a gamepad.
596    #[must_use]
597    pub const fn reads_gamepad(&self) -> bool {
598        matches!(
599            self,
600            Self::PadButton(_)
601                | Self::PadChord { .. }
602                | Self::PadDpad
603                | Self::PadStick { .. }
604                | Self::PadTrigger { .. }
605        )
606    }
607
608    /// Whether this binding's dead zone or threshold is usable, if it has one.
609    fn deadzone_is_valid(&self) -> bool {
610        match *self {
611            Self::PadStick { deadzone, .. } => gamepad::valid_deadzone(deadzone),
612            Self::PadTrigger { threshold, .. } => gamepad::valid_deadzone(threshold),
613            _ => true,
614        }
615    }
616
617    /// Whether this binding reads the keyboard at all.
618    fn reads_keyboard(&self) -> bool {
619        let mut any = false;
620        self.visit_keys(|_| any = true);
621        any
622    }
623
624    /// Whether this binding owns `key`.
625    #[must_use]
626    pub fn owns_key(&self, key: KeyCode) -> bool {
627        let mut owns = false;
628        self.visit_keys(|owned| owns |= owned == key);
629        owns
630    }
631}
632
633// ---------------------------------------------------------------------------
634// Action declaration
635// ---------------------------------------------------------------------------
636
637/// What kind of value an action produces.
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
639pub enum ActionKind {
640    /// A digital on/off action with edge detection.
641    Button,
642    /// A 1-D analog axis.
643    Axis1,
644    /// A 2-D analog axis.
645    Axis2,
646}
647
648/// A declared action: name + kind + bindings.
649#[derive(Debug, Clone)]
650pub struct ActionDecl {
651    /// Human-readable name, used as the lookup key in [`ActionMap`].
652    pub name: String,
653    /// What kind of value this action produces.
654    pub kind: ActionKind,
655    /// The raw inputs that drive this action.
656    pub bindings: Vec<Binding>,
657}
658
659// ---------------------------------------------------------------------------
660// Internal per-action state
661// ---------------------------------------------------------------------------
662
663/// Tracks the resolved value and edge-transition bookkeeping for one action.
664#[derive(Debug, Clone)]
665struct ActionSlot {
666    decl: ActionDecl,
667    /// The bindings it was declared with, which [`ActionMap::rebind`] leaves
668    /// alone: the defaults a player's overrides are a diff over.
669    defaults: Vec<Binding>,
670    /// Index into [`ActionMap::contexts`] of the context this action is in.
671    context: usize,
672    /// The repeat pattern, if one is attached — see [`ActionMap::set_repeat`].
673    repeat: Option<RepeatState>,
674    /// The tap, hold and double-tap patterns — see [`ActionMap::set_tap`].
675    patterns: PatternState,
676    /// Whether a pattern emitting this action fired since the last
677    /// [`ActionMap::begin_tick`], which holds it down until then — see
678    /// `emit.rs`.
679    emitted: bool,
680    /// The current resolved value.
681    value: ActionValue,
682    /// True when at least one binding for this action is "down" (key held,
683    /// button held, etc.). Used to detect press / release edges.
684    active: bool,
685    /// Accumulated time at which the action became active (None when inactive).
686    /// Used to compute `Held { duration }`.
687    hold_start: Option<f64>,
688    /// Whether this action reacts to input at all.
689    ///
690    /// A flag on the slot rather than a `HashSet<String>` on the map: the set
691    /// was hashed once per slot per raw event, and `set_enabled` on a name that
692    /// was never declared grew it forever.
693    enabled: bool,
694    /// Whether its pad chords take their button from a context above — see
695    /// [`ActionMap::set_pad_chords_outrank`].
696    pad_chords_outrank: bool,
697}
698
699impl ActionSlot {
700    fn new(decl: ActionDecl, context: usize) -> Self {
701        let value = match decl.kind {
702            ActionKind::Button => ActionValue::Button(ButtonAction::default()),
703            ActionKind::Axis1 => ActionValue::Axis1(Axis1Action::default()),
704            ActionKind::Axis2 => ActionValue::Axis2(Axis2Action::default()),
705        };
706        Self {
707            defaults: decl.bindings.clone(),
708            decl,
709            context,
710            repeat: None,
711            patterns: PatternState::default(),
712            emitted: false,
713            value,
714            active: false,
715            hold_start: None,
716            enabled: true,
717            pad_chords_outrank: false,
718        }
719    }
720
721    /// Returns the declared kind.
722    fn kind(&self) -> ActionKind {
723        self.decl.kind
724    }
725
726    /// Force the action back to its idle value and drop any hold state.
727    fn reset(&mut self) {
728        self.active = false;
729        self.hold_start = None;
730        if let Some(repeat) = &mut self.repeat {
731            repeat.reset();
732        }
733        self.patterns.reset();
734        self.emitted = false;
735        match &mut self.value {
736            ActionValue::Button(a) => {
737                a.state = ButtonState::Released;
738                a.just_pressed = false;
739                a.just_released = false;
740            }
741            ActionValue::Axis1(a) => {
742                a.value = 0.0;
743                a.pointer_moved = false;
744            }
745            ActionValue::Axis2(a) => {
746                a.x = 0.0;
747                a.y = 0.0;
748            }
749        }
750    }
751}
752
753// ---------------------------------------------------------------------------
754// Errors
755// ---------------------------------------------------------------------------
756
757/// What can go wrong when mutating an [`ActionMap`].
758#[derive(Debug, Clone, PartialEq, Eq)]
759pub enum ActionMapError {
760    /// An action with this name is already declared.
761    DuplicateName(String),
762    /// No action with this name has been declared.
763    UnknownAction(String),
764    /// No context with this name has been declared.
765    UnknownContext(String),
766    /// [`ActionMap::push_context`] was asked for a context already active —
767    /// [`GAMEPLAY_CONTEXT`] and [`GLOBAL_CONTEXT`] always are.
768    ContextAlreadyActive(String),
769    /// [`ActionMap::pop_context`] was asked for a context that is not the
770    /// topmost pushed one — [`GAMEPLAY_CONTEXT`] and [`GLOBAL_CONTEXT`] are
771    /// never pushed.
772    ContextNotOnTop(String),
773    /// A [`Binding::PadStick`]'s dead zone or a [`Binding::PadTrigger`]'s
774    /// threshold on the named action is not finite and in `0.0..1.0`.
775    InvalidDeadzone(String),
776    /// [`ActionMap::set_emits`] was asked to make a pattern emit an action
777    /// that is not an [`ActionKind::Button`].
778    NotAButton(String),
779    /// [`ActionMap::set_emits`] was asked to make a pattern emit the action it
780    /// is attached to.
781    EmitsItself(String),
782}
783
784impl std::fmt::Display for ActionMapError {
785    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
786        match self {
787            Self::DuplicateName(name) => write!(f, "duplicate action name: {name}"),
788            Self::UnknownAction(name) => write!(f, "no such action: {name}"),
789            Self::UnknownContext(name) => write!(f, "no such context: {name}"),
790            Self::ContextAlreadyActive(name) => write!(f, "context already active: {name}"),
791            Self::ContextNotOnTop(name) => {
792                write!(f, "context is not the topmost pushed one: {name}")
793            }
794            Self::InvalidDeadzone(name) => {
795                write!(f, "dead zone or threshold not in 0..1: {name}")
796            }
797            Self::NotAButton(name) => {
798                write!(f, "a pattern can emit only a button action: {name}")
799            }
800            Self::EmitsItself(name) => {
801                write!(f, "a pattern cannot emit the action it is on: {name}")
802            }
803        }
804    }
805}
806
807impl std::error::Error for ActionMapError {}
808
809// ---------------------------------------------------------------------------
810// ActionMap
811// ---------------------------------------------------------------------------
812
813/// Resolves raw input events to action values.
814///
815/// Bindings are declared once via [`ActionMap::declare`] and then driven by
816/// feeding raw events (`key_event`, `mouse_button`, `mouse_motion`,
817/// `mouse_scroll`).  Per-tick bookkeeping is done with [`ActionMap::begin_tick`].
818pub struct ActionMap {
819    /// All declared actions, in registration order.
820    slots: Vec<ActionSlot>,
821    /// name → index into `slots`.
822    name_to_idx: HashMap<String, usize>,
823
824    // Contexts ---------------------------------------------------------------
825    /// Every declared context's name, in declaration order;
826    /// [`GAMEPLAY_CONTEXT`] is index 0 and [`GLOBAL_CONTEXT`] index 1.
827    contexts: Vec<String>,
828    /// The pushable stack as indices into `contexts`, bottom first. Never
829    /// empty: index 0 is the base and is never popped. [`GLOBAL_CONTEXT`] is
830    /// always active and is not on it.
831    stack: Vec<usize>,
832    /// The contexts on `stack` that were pushed modal.
833    modal: Vec<usize>,
834    /// Which active context owns each bound input, rebuilt whenever the stack
835    /// or a binding changes.
836    routes: Routes,
837    /// Held inputs withheld from their owner until released, because the
838    /// stack changed under them.
839    suppressed: Suppressed,
840    /// Every kind of device that has spoken, most recent first and each
841    /// once — see [`ActionMap::last_device`], and `hint.rs` for why the ones
842    /// behind it are kept.
843    devices: Vec<Device>,
844    /// The family of the pad that last spoke — see
845    /// [`ActionMap::last_pad_kind`].
846    last_pad_kind: Option<PadKind>,
847    /// Whether the front of `devices` changed since the last
848    /// [`ActionMap::begin_tick`] — see [`ActionMap::last_device_changed`].
849    device_changed: bool,
850    /// The resolution trace, while [`ActionMap::set_tracing`] has it on — see
851    /// `trace.rs`.
852    trace: Option<VecDeque<TraceEntry>>,
853
854    // Raw input state -------------------------------------------------------
855    held_keys: HeldKeys,
856    /// The serial of the last key press — see [`HeldKeys`].
857    key_presses: u64,
858    held_buttons: HashSet<PointerButton>,
859    /// On-screen controls currently held — see [`Binding::Virtual`].
860    held_controls: HashSet<String>,
861    /// Where each on-screen stick is deflected to, inside the unit disc.
862    ///
863    /// A *level*, like [`ActionMap::pointer`] and unlike the two deltas below:
864    /// a finger resting on a stick reports nothing on the frames it does not
865    /// move, so a value cleared by [`ActionMap::begin_tick`] would centre the
866    /// stick under a finger that never let go.
867    control_sticks: HashMap<String, (f32, f32)>,
868    /// Every connected pad's last snapshot — a level, like `control_sticks`.
869    /// See `gamepad.rs`.
870    pads: Pads,
871    /// Every button some pad holds, as one set for routing to read.
872    held_pad_buttons: PadButtons,
873    mouse_delta: (f32, f32),
874    scroll_delta: (f32, f32),
875    /// Where the pointer is, normalised to the surface, or `None` until one has
876    /// ever reported a position.
877    ///
878    /// Not cleared by [`ActionMap::begin_tick`] and not cleared when the pointer
879    /// leaves — unlike the two deltas above, this is a *level* and it is the
880    /// last thing the player actually asked for. See
881    /// [`Binding::PointerPosition`].
882    pointer: Option<(f32, f32)>,
883    /// Slots a pattern fired an emission at, waiting to be pressed — see
884    /// `emit.rs`. Empty between calls.
885    pending_emits: Vec<usize>,
886
887    /// Seconds elapsed since creation (advanced by `begin_tick`).
888    ///
889    /// `f64` rather than `f32`: an `f32` accumulator quantizes `Held`
890    /// durations after a few hours of uptime and stops advancing entirely
891    /// after about nineteen days, which is well inside a dedicated server's
892    /// lifetime.
893    elapsed: f64,
894}
895
896impl ActionMap {
897    /// Create an empty action map.
898    #[must_use]
899    pub fn new() -> Self {
900        Self {
901            slots: Vec::new(),
902            name_to_idx: HashMap::new(),
903            contexts: vec![GAMEPLAY_CONTEXT.to_owned(), GLOBAL_CONTEXT.to_owned()],
904            stack: vec![0],
905            modal: Vec::new(),
906            routes: Routes::default(),
907            suppressed: Suppressed::default(),
908            devices: Vec::new(),
909            last_pad_kind: None,
910            device_changed: false,
911            trace: None,
912            held_keys: HeldKeys::new(),
913            key_presses: 0,
914            held_buttons: HashSet::new(),
915            held_controls: HashSet::new(),
916            control_sticks: HashMap::new(),
917            pads: Pads::new(),
918            held_pad_buttons: PadButtons::EMPTY,
919            mouse_delta: (0.0, 0.0),
920            scroll_delta: (0.0, 0.0),
921            pointer: None,
922            pending_emits: Vec::new(),
923            elapsed: 0.0,
924        }
925    }
926
927    /// Register an action in [`GAMEPLAY_CONTEXT`].  Actions are enabled by
928    /// default.
929    ///
930    /// # Panics
931    /// Panics if an action with the same name has already been declared. Use
932    /// [`ActionMap::try_declare`] when the declaration comes from a user config
933    /// file, where a duplicate is a message rather than a crash.
934    pub fn declare(&mut self, decl: ActionDecl) {
935        if let Err(error) = self.try_declare(decl) {
936            panic!("{error}");
937        }
938    }
939
940    /// Register an action, reporting a duplicate name rather than panicking.
941    ///
942    /// # Errors
943    /// [`ActionMapError::DuplicateName`] if the name is already declared.
944    pub fn try_declare(&mut self, decl: ActionDecl) -> Result<(), ActionMapError> {
945        self.try_declare_in(GAMEPLAY_CONTEXT, decl)
946    }
947
948    /// Register an action in the named context, declaring the context on first
949    /// use.
950    ///
951    /// # Panics
952    /// Panics on a duplicate action name, as [`ActionMap::declare`] does.
953    pub fn declare_in(&mut self, context: &str, decl: ActionDecl) {
954        if let Err(error) = self.try_declare_in(context, decl) {
955            panic!("{error}");
956        }
957    }
958
959    /// Register an action in the named context, reporting a duplicate name
960    /// rather than panicking. Action names are unique across every context.
961    ///
962    /// An action declared into an active context with its input already held
963    /// reports it at once, as [`ActionMap::rebind`] does.
964    ///
965    /// # Errors
966    /// [`ActionMapError::DuplicateName`] if the name is already declared,
967    /// [`ActionMapError::InvalidDeadzone`] if a pad binding's dead zone or
968    /// threshold is unusable.
969    pub fn try_declare_in(
970        &mut self,
971        context: &str,
972        decl: ActionDecl,
973    ) -> Result<(), ActionMapError> {
974        if self.name_to_idx.contains_key(&decl.name) {
975            return Err(ActionMapError::DuplicateName(decl.name));
976        }
977        if !decl.bindings.iter().all(Binding::deadzone_is_valid) {
978            return Err(ActionMapError::InvalidDeadzone(decl.name));
979        }
980        let context = match self.contexts.iter().position(|name| name == context) {
981            Some(index) => index,
982            None => {
983                self.contexts.push(context.to_owned());
984                self.contexts.len() - 1
985            }
986        };
987        let idx = self.slots.len();
988        self.name_to_idx.insert(decl.name.clone(), idx);
989        self.slots.push(ActionSlot::new(decl, context));
990        self.reroute();
991        Ok(())
992    }
993
994    /// Replace an action's bindings in place — the rebind path.
995    ///
996    /// Keeps the action's registration order and enabled flag, and resets its
997    /// value and hold state (the old bindings' "is it down" answer says nothing
998    /// about the new ones). Rebuilding the whole map instead, which was the
999    /// only option before, lost both.
1000    ///
1001    /// # Errors
1002    /// [`ActionMapError::UnknownAction`] if nothing with that name is declared,
1003    /// [`ActionMapError::InvalidDeadzone`] if a pad binding's dead zone or
1004    /// threshold is unusable — and then the old bindings stay.
1005    pub fn rebind(&mut self, name: &str, bindings: Vec<Binding>) -> Result<(), ActionMapError> {
1006        let Some(&idx) = self.name_to_idx.get(name) else {
1007            return Err(ActionMapError::UnknownAction(name.to_owned()));
1008        };
1009        if !bindings.iter().all(Binding::deadzone_is_valid) {
1010            return Err(ActionMapError::InvalidDeadzone(name.to_owned()));
1011        }
1012        let slot = &mut self.slots[idx];
1013        slot.decl.bindings = bindings;
1014        slot.reset();
1015        // Every live action, not only this one: the new bindings can take an
1016        // input from a context beneath, or give one back.
1017        self.reroute();
1018        Ok(())
1019    }
1020
1021    /// The bindings currently driving an action, or `None` if it is not
1022    /// declared.
1023    #[must_use]
1024    pub fn bindings(&self, name: &str) -> Option<&[Binding]> {
1025        let &idx = self.name_to_idx.get(name)?;
1026        Some(&self.slots[idx].decl.bindings)
1027    }
1028
1029    /// Whether an action is enabled. `None` if it is not declared.
1030    #[must_use]
1031    pub fn is_enabled(&self, name: &str) -> Option<bool> {
1032        let &idx = self.name_to_idx.get(name)?;
1033        Some(self.slots[idx].enabled)
1034    }
1035
1036    /// Enable or disable an action by name.  Disabled actions always return
1037    /// their default (idle) value and do not react to input — including across
1038    /// [`ActionMap::begin_tick`], which used to re-read the held-key set and
1039    /// re-press a disabled action on the very next tick.
1040    ///
1041    /// An action with a [`Binding::ScrollChord`] also leaves or rejoins the
1042    /// wheel's competition, and the wheel is re-read at once.
1043    ///
1044    /// Has no effect if the named action does not exist.
1045    pub fn set_enabled(&mut self, name: &str, enabled: bool) {
1046        let Some(&idx) = self.name_to_idx.get(name) else {
1047            return;
1048        };
1049        let slot = &mut self.slots[idx];
1050        let changed = slot.enabled != enabled;
1051        slot.enabled = enabled;
1052        if !enabled {
1053            // Reset the action to idle so it doesn't stick.
1054            slot.reset();
1055        }
1056        // Only an enabled scroll chord competes for the wheel, so turning one
1057        // on or off can hand the wheel to another binding.
1058        let scroll_chord = slot
1059            .decl
1060            .bindings
1061            .iter()
1062            .any(|binding| matches!(binding, Binding::ScrollChord { .. }));
1063        if changed && scroll_chord {
1064            self.reroute();
1065        }
1066    }
1067
1068    // -- feeding raw events --------------------------------------------------
1069
1070    /// Feed a key event. `pressed` → key-down, `!pressed` → key-up.
1071    ///
1072    /// A press makes [`Device::Keyboard`] the last device; a release does not,
1073    /// since letting go of a key after reaching for the mouse is not the
1074    /// keyboard speaking.
1075    pub fn key_event(&mut self, key: KeyCode, pressed: bool) {
1076        // An OS auto-repeat of a key already down keeps its first serial: it
1077        // is not a new press, and the trace does not record it again.
1078        let fresh = pressed && !self.held_keys.contains_key(&key);
1079        if pressed {
1080            if fresh {
1081                self.key_presses += 1;
1082                self.held_keys.insert(key, self.key_presses);
1083            }
1084            self.spoke(Device::Keyboard);
1085        } else {
1086            self.held_keys.remove(&key);
1087            self.suppressed.keys.remove(&key);
1088        }
1089        // A scroll chord's key can hand the wheel from one binding to another.
1090        let wheel = self.routes.is_scroll_chord_key(key);
1091        if Modifier::of(key).is_some() {
1092            // A modifier can press or release any chord, and shadow or unshadow
1093            // any plain key or mouse button a chord shares.
1094            self.resolve_matching(|b| {
1095                b.reads_keyboard() || b.mouse_button().is_some() || (wheel && b.reads_wheel())
1096            });
1097        } else {
1098            self.resolve_matching(|b| b.owns_key(key) || (wheel && b.reads_wheel()));
1099        }
1100        if fresh {
1101            self.traced(|| TracedInput::Key(key));
1102        }
1103    }
1104
1105    /// Feed a mouse-button event.
1106    pub fn mouse_button(&mut self, button: PointerButton, pressed: bool) {
1107        let mut fresh = false;
1108        if pressed {
1109            fresh = self.held_buttons.insert(button);
1110            self.spoke(Device::Pointer);
1111        } else {
1112            self.held_buttons.remove(&button);
1113            self.suppressed.buttons.remove(&button);
1114        }
1115        self.resolve_matching(|b| b.mouse_button() == Some(button));
1116        if fresh {
1117            self.traced(|| TracedInput::MouseButton(button));
1118        }
1119    }
1120
1121    /// Feed mouse motion (delta in pixels since the last event).
1122    ///
1123    /// Non-finite deltas are dropped: a `NaN` from a driver or a divide-by-zero
1124    /// sensitivity would otherwise propagate straight into an action value and
1125    /// poison every comparison downstream.
1126    pub fn mouse_motion(&mut self, dx: f32, dy: f32) {
1127        if !dx.is_finite() || !dy.is_finite() {
1128            return;
1129        }
1130        if dx != 0.0 || dy != 0.0 {
1131            self.spoke(Device::Pointer);
1132        }
1133        self.mouse_delta.0 += dx;
1134        self.mouse_delta.1 += dy;
1135        self.resolve_matching(|b| matches!(b, Binding::MouseMotion));
1136    }
1137
1138    /// Feed scroll input (delta in detents or pixels — caller normalises).
1139    ///
1140    /// Non-finite deltas are dropped, as for [`ActionMap::mouse_motion`];
1141    /// `f32::clamp` returns `NaN` for a `NaN` input, so the clamp downstream is
1142    /// no defence.
1143    pub fn mouse_scroll(&mut self, dx: f32, dy: f32) {
1144        if !dx.is_finite() || !dy.is_finite() {
1145            return;
1146        }
1147        let turned = dx != 0.0 || dy != 0.0;
1148        if turned {
1149            self.spoke(Device::Pointer);
1150        }
1151        self.scroll_delta.0 += dx;
1152        self.scroll_delta.1 += dy;
1153        self.resolve_matching(Binding::reads_wheel);
1154        if turned {
1155            self.traced(|| TracedInput::Wheel);
1156        }
1157    }
1158
1159    /// Feed the pointer's position, normalised to the surface: −1.0 at one edge
1160    /// and +1.0 at the other, +X right and +Y up.
1161    ///
1162    /// The caller does the conversion because the caller is the only thing that
1163    /// knows the surface — see [`Binding::PointerPosition`], which also says why
1164    /// a pointer that *leaves* is not reported here at all.
1165    ///
1166    /// A coordinate equal to the one already held is dropped, so
1167    /// [`Axis1Action::pointer_moved`] means the pointer moved rather than that
1168    /// an event arrived. Non-finite coordinates are dropped like
1169    /// [`ActionMap::mouse_motion`]'s deltas: a `NaN` would poison every
1170    /// comparison downstream of the axis.
1171    pub fn pointer_position(&mut self, x: f32, y: f32) {
1172        if !x.is_finite() || !y.is_finite() {
1173            return;
1174        }
1175        let moved = Some((x, y));
1176        if self.pointer == moved {
1177            return;
1178        }
1179        self.pointer = moved;
1180        self.spoke(Device::Pointer);
1181
1182        for i in 0..self.slots.len() {
1183            let slot = &self.slots[i];
1184            let drives = slot
1185                .decl
1186                .bindings
1187                .iter()
1188                .any(|b| matches!(b, Binding::PointerPosition { .. }));
1189            // The owner only: a consumed pointer has not moved for anyone else.
1190            if !self.is_live(i) || !drives || self.routes.pointer != Some(slot.context) {
1191                continue;
1192            }
1193            self.resolve_one(i);
1194            // Set here rather than in `resolve_one`, which also runs from
1195            // `begin_tick`: an edge raised by re-resolving an unchanged value
1196            // would be raised on every tick forever.
1197            if let ActionValue::Axis1(axis) = &mut self.slots[i].value {
1198                axis.pointer_moved = true;
1199            }
1200        }
1201    }
1202
1203    /// Feed an on-screen **button**: the control named by `control` is held, or
1204    /// it is not.
1205    ///
1206    /// The widget decides what "held" means for its own geometry — which
1207    /// contact owns it, and whether a lift over it commits or cancels. What
1208    /// arrives here is the answer, and it drives every
1209    /// [`Binding::Virtual`] with this id on an [`ActionKind::Button`] exactly as
1210    /// a key event drives a [`Binding::Key`], edges included.
1211    pub fn virtual_button(&mut self, control: &str, pressed: bool) {
1212        // `contains` first so a control that is already held costs no
1213        // allocation: this runs once per frame per held control.
1214        let fresh = pressed && !self.held_controls.contains(control);
1215        if pressed {
1216            if fresh {
1217                self.held_controls.insert(control.to_owned());
1218            }
1219            self.spoke(Device::Touch);
1220        } else {
1221            self.held_controls.remove(control);
1222            self.suppressed.controls.remove(control);
1223        }
1224        self.resolve_matching(|b| matches!(b, Binding::Virtual(id) if id == control));
1225        if fresh {
1226            self.traced(|| TracedInput::Control(control.to_owned()));
1227        }
1228    }
1229
1230    /// Feed an on-screen **stick**: where the control named by `control` is
1231    /// deflected to, with +X right and +Y up.
1232    ///
1233    /// A *level*, not a delta — a stick held still reports the same value every
1234    /// frame, and a stick nobody is touching reports `(0.0, 0.0)`. The magnitude
1235    /// belongs to the widget's own radius, so a control at its edge reports 1.0
1236    /// and the value is clamped to the unit disc when it is resolved.
1237    ///
1238    /// Non-finite coordinates are dropped like [`ActionMap::mouse_motion`]'s
1239    /// deltas: a `NaN` would poison every comparison downstream of the axis.
1240    pub fn virtual_stick(&mut self, control: &str, x: f32, y: f32) {
1241        if !x.is_finite() || !y.is_finite() {
1242            return;
1243        }
1244        let deflected = x != 0.0 || y != 0.0;
1245        if deflected {
1246            self.spoke(Device::Touch);
1247        }
1248        let before = if let Some(held) = self.control_sticks.get_mut(control) {
1249            std::mem::replace(held, (x, y))
1250        } else {
1251            self.control_sticks.insert(control.to_owned(), (x, y));
1252            (0.0, 0.0)
1253        };
1254        // Centred is a stick's release: what lifts a suppress.
1255        if !deflected {
1256            self.suppressed.control_sticks.remove(control);
1257        }
1258        self.resolve_matching(|b| matches!(b, Binding::Virtual(id) if id == control));
1259        if deflected && before == (0.0, 0.0) {
1260            self.traced(|| TracedInput::ControlStick(control.to_owned()));
1261        }
1262    }
1263
1264    /// Called at the start of each server tick.
1265    ///
1266    /// - Resets per-frame edge flags (`just_pressed`, `just_released` on every
1267    ///   button action, `pointer_moved` on every 1-D axis, what
1268    ///   [`ActionMap::repeated`], [`ActionMap::tapped`],
1269    ///   [`ActionMap::hold_fired`] and [`ActionMap::double_tapped`] read, and
1270    ///   [`ActionMap::last_device_changed`]).
1271    /// - Zeroes accumulated mouse-motion and scroll deltas.
1272    /// - Releases every action a pattern emitted since the last tick, and
1273    ///   presses the ones a pattern emits as this tick begins — see `emit.rs`.
1274    /// - Advances the internal clock by `dt` seconds so that [`ButtonState::Held`]
1275    ///   durations are up-to-date next time a button action is resolved.
1276    pub fn begin_tick(&mut self, dt: f32) {
1277        // A negative dt would move the clock backwards and report negative
1278        // `Held` durations; only forward time is meaningful here.
1279        if dt.is_finite() && dt >= 0.0 {
1280            self.elapsed += f64::from(dt);
1281        }
1282        self.mouse_delta = (0.0, 0.0);
1283        self.scroll_delta = (0.0, 0.0);
1284        self.device_changed = false;
1285        // Every slot, live or not: one taken off the stack while pressed must
1286        // not come back pressed.
1287        for slot in &mut self.slots {
1288            slot.emitted = false;
1289        }
1290
1291        // Reset edge flags and re-resolve every action so that Held durations
1292        // and axis values reflect the new elapsed time and cleared deltas.
1293        //
1294        // Disabled actions are skipped entirely. Resolving them here re-read
1295        // `held_keys` and re-pressed them — so disabling "jump" while Space was
1296        // held cleared it, and the next tick emitted `just_pressed` again.
1297        for i in 0..self.slots.len() {
1298            if !self.is_live(i) {
1299                continue;
1300            }
1301            if let Some(repeat) = &mut self.slots[i].repeat {
1302                repeat.fired = false;
1303            }
1304            self.slots[i].patterns.clear_fired();
1305            match &mut self.slots[i].value {
1306                ActionValue::Button(a) => a.reset_edges(),
1307                ActionValue::Axis1(a) => a.pointer_moved = false,
1308                ActionValue::Axis2(_) => {}
1309            }
1310            // Re-resolve to update Held duration and zero out axis deltas.
1311            // Not `resolve_one`: what a pattern emits now waits for the loop
1312            // to finish, so the edges do not depend on declaration order.
1313            self.resolve_slot(i);
1314        }
1315        self.drain_emits();
1316    }
1317
1318    // -- reading resolved state ----------------------------------------------
1319
1320    /// Get the current value of an action by name.
1321    ///
1322    /// Returns `None` if no action with that name was declared.
1323    #[must_use]
1324    pub fn action(&self, name: &str) -> Option<&ActionValue> {
1325        let &idx = self.name_to_idx.get(name)?;
1326        Some(&self.slots[idx].value)
1327    }
1328
1329    /// Get a mutable reference to the current value of an action.
1330    ///
1331    /// The caller can use this to clear edge flags after consuming them
1332    /// (e.g. setting `just_pressed = false` after a jump was processed).
1333    #[must_use]
1334    pub fn action_mut(&mut self, name: &str) -> Option<&mut ActionValue> {
1335        let &idx = self.name_to_idx.get(name)?;
1336        Some(&mut self.slots[idx].value)
1337    }
1338
1339    /// All declared action names, in registration order.
1340    pub fn action_names(&self) -> impl Iterator<Item = &str> {
1341        self.slots.iter().map(|s| s.decl.name.as_str())
1342    }
1343
1344    /// Whether a button action is down — pressed this tick or still held.
1345    ///
1346    /// An action that was never declared, or that is not a button, is `false`
1347    /// rather than an error: a caller asking whether a button is down has no
1348    /// better answer to give, and every game was already collapsing both cases
1349    /// that way.
1350    ///
1351    /// The state is matched positively rather than as "not
1352    /// [`Released`](ButtonState::Released)". The two agree while
1353    /// [`ButtonState`] has three variants, and would stop agreeing the day it
1354    /// gains a fourth — silently, and only for whoever wrote the negation.
1355    #[must_use]
1356    pub fn button_held(&self, name: &str) -> bool {
1357        self.button(name).is_some_and(|button| {
1358            matches!(
1359                button.state,
1360                ButtonState::Pressed | ButtonState::Held { .. }
1361            )
1362        })
1363    }
1364
1365    /// Whether a button action went down on this tick.
1366    ///
1367    /// The edge, not the level: true only for the tick of the transition, and
1368    /// cleared by [`ActionMap::begin_tick`]. This is what a jump or a flap
1369    /// reads, so that holding the key does not repeat it.
1370    #[must_use]
1371    pub fn just_pressed(&self, name: &str) -> bool {
1372        self.button(name).is_some_and(|button| button.just_pressed)
1373    }
1374
1375    /// Whether a button action came up on this tick.
1376    #[must_use]
1377    pub fn just_released(&self, name: &str) -> bool {
1378        self.button(name).is_some_and(|button| button.just_released)
1379    }
1380
1381    /// A 1-D axis action's value, or `0.0` if it is absent or another kind.
1382    #[must_use]
1383    pub fn axis1(&self, name: &str) -> f32 {
1384        match self.action(name) {
1385            Some(ActionValue::Axis1(axis)) => axis.value,
1386            _ => 0.0,
1387        }
1388    }
1389
1390    /// A 2-D axis action's value, or `(0.0, 0.0)` if it is absent or another
1391    /// kind.
1392    ///
1393    /// Key-driven composites arrive normalised to at most unit length, so a
1394    /// diagonal is not faster than a cardinal; mouse motion is a raw pixel
1395    /// delta and is not normalised.
1396    #[must_use]
1397    pub fn axis2(&self, name: &str) -> (f32, f32) {
1398        match self.action(name) {
1399            Some(ActionValue::Axis2(axis)) => (axis.x, axis.y),
1400            _ => (0.0, 0.0),
1401        }
1402    }
1403
1404    // -- reading raw input --------------------------------------------------
1405
1406    /// Every key held down, in the order they were pressed — what an input
1407    /// inspector shows beside the actions they drive. Raw: a key no context
1408    /// binds, or one withheld, is listed as much as any other.
1409    #[must_use]
1410    pub fn held_keys(&self) -> Vec<KeyCode> {
1411        let mut held: Vec<(u64, KeyCode)> = self
1412            .held_keys
1413            .iter()
1414            .map(|(&key, &serial)| (serial, key))
1415            .collect();
1416        held.sort_unstable();
1417        held.into_iter().map(|(_, key)| key).collect()
1418    }
1419
1420    /// Every pointer button held down, in [`PointerButton`]'s order. Raw, as
1421    /// [`ActionMap::held_keys`] is.
1422    #[must_use]
1423    pub fn held_mouse_buttons(&self) -> Vec<PointerButton> {
1424        let mut held: Vec<PointerButton> = self.held_buttons.iter().copied().collect();
1425        held.sort_unstable();
1426        held
1427    }
1428
1429    /// Where the pointer last was, normalised to the surface as
1430    /// [`ActionMap::pointer_position`] takes it, or `None` before it reported
1431    /// a position.
1432    #[must_use]
1433    pub const fn pointer(&self) -> Option<(f32, f32)> {
1434        self.pointer
1435    }
1436
1437    /// The button behind `name`, if it is declared and is one.
1438    fn button(&self, name: &str) -> Option<&ButtonAction> {
1439        match self.action(name)? {
1440            ActionValue::Button(button) => Some(button),
1441            _ => None,
1442        }
1443    }
1444
1445    // -- internal resolution -------------------------------------------------
1446
1447    /// Re-resolve every enabled action with a binding `matches` accepts.
1448    ///
1449    /// One helper rather than four near-identical bodies, and no `Vec<usize>`
1450    /// per event: the enabled flag lives on the slot, so nothing here allocates
1451    /// or hashes a `String`.
1452    fn resolve_matching(&mut self, matches: impl Fn(&Binding) -> bool) {
1453        for i in 0..self.slots.len() {
1454            let slot = &self.slots[i];
1455            if self.is_live(i) && slot.decl.bindings.iter().any(&matches) {
1456                self.resolve_one(i);
1457            }
1458        }
1459    }
1460
1461    /// Compute the current value of a single action slot from raw state, and
1462    /// press whatever its patterns emitted on the way.
1463    fn resolve_one(&mut self, idx: usize) {
1464        self.resolve_slot(idx);
1465        self.drain_emits();
1466    }
1467
1468    /// Compute the current value of a single action slot from raw state,
1469    /// queueing what its patterns emit for [`ActionMap::drain_emits`].
1470    ///
1471    /// Reads raw input only through the slot's context's [`View`], which is
1472    /// where consumption happens.
1473    fn resolve_slot(&mut self, idx: usize) {
1474        let control_sticks = &self.control_sticks;
1475        let pads = &self.pads;
1476        let mouse_delta = self.mouse_delta;
1477        let scroll_delta = self.scroll_delta;
1478        let pointer = self.pointer;
1479        let elapsed = self.elapsed;
1480
1481        let slot = &mut self.slots[idx];
1482        let view = View {
1483            context: slot.context,
1484            routes: &self.routes,
1485            suppressed: &self.suppressed,
1486            held_keys: &self.held_keys,
1487            held_buttons: &self.held_buttons,
1488            held_controls: &self.held_controls,
1489            held_pad_buttons: self.held_pad_buttons,
1490        };
1491        let kind = slot.kind();
1492        let bindings = &slot.decl.bindings;
1493
1494        match kind {
1495            ActionKind::Button => {
1496                let down = slot.emitted
1497                    || bindings.iter().any(|b| match b {
1498                        Binding::Key(k) => view.key(*k),
1499                        Binding::Chord { modifier, key } => view.chord(*modifier, *key),
1500                        Binding::MouseButton(b) => view.button(*b),
1501                        Binding::ButtonChord { modifier, button } => {
1502                            view.button_chord(*modifier, *button)
1503                        }
1504                        Binding::Virtual(id) => view.control(id),
1505                        Binding::PadButton(button) => view.pad_button(*button),
1506                        Binding::PadChord { modifier, button } => {
1507                            view.pad_chord(*modifier, *button)
1508                        }
1509                        Binding::PadDpad => PadButton::DPAD.iter().any(|&b| view.pad_button(b)),
1510                        Binding::PadTrigger { trigger, threshold } => {
1511                            view.trigger(*trigger)
1512                                && gamepad::pad_trigger(pads, *trigger) > *threshold
1513                        }
1514                        Binding::MouseMotion
1515                        | Binding::MouseScroll
1516                        | Binding::ScrollChord { .. }
1517                        | Binding::PointerPosition { .. }
1518                        | Binding::PadStick { .. } => false,
1519                        Binding::KeyAxis { negative, positive } => {
1520                            view.key(*negative) || view.key(*positive)
1521                        }
1522                        Binding::Wasd {
1523                            up,
1524                            down: w_down,
1525                            left,
1526                            right,
1527                        } => {
1528                            view.key(*up)
1529                                || view.key(*w_down)
1530                                || view.key(*left)
1531                                || view.key(*right)
1532                        }
1533                    });
1534
1535                let was_active = slot.active;
1536                slot.active = down;
1537
1538                let action = match &mut slot.value {
1539                    ActionValue::Button(a) => a,
1540                    _ => unreachable!(),
1541                };
1542
1543                match (was_active, down) {
1544                    (false, true) => {
1545                        action.state = ButtonState::Pressed;
1546                        action.just_pressed = true;
1547                        slot.hold_start = Some(elapsed);
1548                    }
1549                    (true, true) => {
1550                        let duration = (elapsed - slot.hold_start.unwrap_or(elapsed)) as f32;
1551                        action.state = ButtonState::Held { duration };
1552                    }
1553                    (true, false) => {
1554                        action.state = ButtonState::Released;
1555                        action.just_released = true;
1556                        slot.hold_start = None;
1557                    }
1558                    (false, false) => {
1559                        action.state = ButtonState::Released;
1560                    }
1561                }
1562            }
1563            ActionKind::Axis1 => {
1564                // The absolute source, if this action has one and a pointer has
1565                // ever reported a position. It **replaces** the relative
1566                // contributions below rather than adding to them: a place and a
1567                // rate do not sum. See [`Binding::PointerPosition`].
1568                let absolute = pointer.filter(|_| view.pointer()).and_then(|(px, py)| {
1569                    bindings.iter().find_map(|binding| match binding {
1570                        Binding::PointerPosition { axis } => Some(match axis {
1571                            PointerAxis::X => px,
1572                            PointerAxis::Y => py,
1573                        }),
1574                        _ => None,
1575                    })
1576                });
1577
1578                let mut value: f32 = absolute.unwrap_or(0.0);
1579
1580                if absolute.is_none() {
1581                    for binding in bindings {
1582                        match binding {
1583                            Binding::MouseScroll if view.scroll() => {
1584                                value += scroll_delta.1;
1585                            }
1586                            Binding::ScrollChord { held } if view.scroll_chord(*held) => {
1587                                value += scroll_delta.1;
1588                            }
1589                            Binding::Key(k) if view.key(*k) => {
1590                                value += 1.0;
1591                            }
1592                            Binding::Chord { modifier, key } if view.chord(*modifier, *key) => {
1593                                value += 1.0;
1594                            }
1595                            Binding::PadButton(button) if view.pad_button(*button) => {
1596                                value += 1.0;
1597                            }
1598                            Binding::PadChord { modifier, button }
1599                                if view.pad_chord(*modifier, *button) =>
1600                            {
1601                                value += 1.0;
1602                            }
1603                            Binding::PadTrigger { trigger, threshold }
1604                                if view.trigger(*trigger) =>
1605                            {
1606                                value += gamepad::trigger_past(
1607                                    gamepad::pad_trigger(pads, *trigger),
1608                                    *threshold,
1609                                );
1610                            }
1611                            Binding::KeyAxis { negative, positive } => {
1612                                if view.key(*negative) {
1613                                    value -= 1.0;
1614                                }
1615                                if view.key(*positive) {
1616                                    value += 1.0;
1617                                }
1618                            }
1619                            _ => {}
1620                        }
1621                    }
1622                }
1623
1624                // The surface's edge is the axis's end, whatever the caller
1625                // passed: a coordinate outside it is a pointer outside the
1626                // window, not a stronger command.
1627                value = value.clamp(-1.0, 1.0);
1628
1629                let action = match &mut slot.value {
1630                    ActionValue::Axis1(a) => a,
1631                    _ => unreachable!(),
1632                };
1633                action.value = value;
1634            }
1635            ActionKind::Axis2 => {
1636                // The *direction* sources and the pointer delta are accumulated
1637                // separately: a direction is bounded by the unit disc, so a
1638                // diagonal is a unit vector rather than 1.414 long and a stick
1639                // pushed past its own radius is still all the way over. A
1640                // pointer delta is a pixel count, and normalizing it would
1641                // throw away the speed.
1642                let mut dir_x: f32 = 0.0;
1643                let mut dir_y: f32 = 0.0;
1644                let mut x: f32 = 0.0;
1645                let mut y: f32 = 0.0;
1646
1647                for binding in bindings {
1648                    match binding {
1649                        Binding::Wasd {
1650                            up,
1651                            down: w_down,
1652                            left,
1653                            right,
1654                        } => {
1655                            if view.key(*up) {
1656                                dir_y += 1.0;
1657                            }
1658                            if view.key(*w_down) {
1659                                dir_y -= 1.0;
1660                            }
1661                            if view.key(*left) {
1662                                dir_x -= 1.0;
1663                            }
1664                            if view.key(*right) {
1665                                dir_x += 1.0;
1666                            }
1667                        }
1668                        Binding::PadDpad => {
1669                            if view.pad_button(PadButton::DpadUp) {
1670                                dir_y += 1.0;
1671                            }
1672                            if view.pad_button(PadButton::DpadDown) {
1673                                dir_y -= 1.0;
1674                            }
1675                            if view.pad_button(PadButton::DpadLeft) {
1676                                dir_x -= 1.0;
1677                            }
1678                            if view.pad_button(PadButton::DpadRight) {
1679                                dir_x += 1.0;
1680                            }
1681                        }
1682                        // Into the same accumulator as the keys, so a player
1683                        // pushing a stick *and* holding a key asks for one
1684                        // direction rather than for twice the speed.
1685                        Binding::Virtual(id) if view.stick(id) => {
1686                            if let Some((sx, sy)) = control_sticks.get(id.as_str()) {
1687                                dir_x += sx;
1688                                dir_y += sy;
1689                            }
1690                        }
1691                        // Each pad through its own dead zone, then into the
1692                        // same accumulator as the keys.
1693                        Binding::PadStick { stick, deadzone } if view.pad_stick(*stick) => {
1694                            for pad in pads.values() {
1695                                let (sx, sy) =
1696                                    gamepad::radial_deadzone(pad.stick(*stick), *deadzone);
1697                                dir_x += sx;
1698                                dir_y += sy;
1699                            }
1700                        }
1701                        Binding::MouseMotion if view.motion() => {
1702                            x += mouse_delta.0;
1703                            y += mouse_delta.1;
1704                        }
1705                        _ => {}
1706                    }
1707                }
1708
1709                let len = (dir_x * dir_x + dir_y * dir_y).sqrt();
1710                if len > 1.0 {
1711                    dir_x /= len;
1712                    dir_y /= len;
1713                }
1714                x += dir_x;
1715                y += dir_y;
1716
1717                let action = match &mut slot.value {
1718                    ActionValue::Axis2(a) => a,
1719                    _ => unreachable!(),
1720                };
1721                action.x = x;
1722                action.y = y;
1723            }
1724        }
1725
1726        if let Some(repeat) = &mut slot.repeat {
1727            repeat.update(&slot.value, elapsed);
1728        }
1729        let before = slot.patterns.fired();
1730        slot.patterns.update(&slot.value, elapsed);
1731        slot.patterns.queue_emits(before, &mut self.pending_emits);
1732    }
1733}
1734
1735impl Default for ActionMap {
1736    fn default() -> Self {
1737        Self::new()
1738    }
1739}
1740
1741impl std::fmt::Debug for ActionMap {
1742    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1743        f.debug_struct("ActionMap")
1744            .field(
1745                "actions",
1746                &self.slots.iter().map(|s| &s.decl.name).collect::<Vec<_>>(),
1747            )
1748            .field("held_keys", &self.held_keys.len())
1749            .field("held_buttons", &self.held_buttons.len())
1750            .field("held_controls", &self.held_controls)
1751            .field("control_sticks", &self.control_sticks)
1752            .field("pads", &self.pads)
1753            .field("contexts", &self.active_contexts().collect::<Vec<_>>())
1754            .field("devices", &self.devices)
1755            .field("last_pad_kind", &self.last_pad_kind)
1756            .field("pointer", &self.pointer)
1757            .field("elapsed", &self.elapsed)
1758            .finish_non_exhaustive()
1759    }
1760}
1761
1762// ---------------------------------------------------------------------------
1763// InputTickState
1764// ---------------------------------------------------------------------------
1765
1766/// The input state for one server tick.
1767///
1768/// The client builds one per tick with [`InputTickState::capture`], sends it to
1769/// the server, which applies it before running the tick.
1770#[derive(Debug, Clone, Default)]
1771pub struct InputTickState {
1772    /// Active actions and their values at this tick.
1773    pub actions: Vec<(String, ActionValue)>,
1774}
1775
1776impl InputTickState {
1777    /// Create an empty tick state.
1778    #[must_use]
1779    pub fn new() -> Self {
1780        Self {
1781            actions: Vec::new(),
1782        }
1783    }
1784
1785    /// Capture the current state of every declared action from `map`.
1786    ///
1787    /// This snapshots enabled and disabled actions alike; disabled actions will
1788    /// be in their idle state, and so are the actions of a context that is not
1789    /// on the stack. An action whose input a context above it consumed is
1790    /// captured as that input left it — which is exactly what the active
1791    /// contexts let through, and all the simulation should see.
1792    pub fn capture(map: &ActionMap) -> Self {
1793        let actions = map
1794            .slots
1795            .iter()
1796            .map(|slot| (slot.decl.name.clone(), slot.value.clone()))
1797            .collect();
1798        Self { actions }
1799    }
1800
1801    /// Look up an action value by name.
1802    #[must_use]
1803    pub fn get(&self, name: &str) -> Option<&ActionValue> {
1804        self.actions.iter().find(|(n, _)| n == name).map(|(_, v)| v)
1805    }
1806}
1807
1808// ---------------------------------------------------------------------------
1809// Tests
1810// ---------------------------------------------------------------------------
1811
1812#[cfg(test)]
1813mod tests {
1814    use super::*;
1815
1816    // -- helpers ------------------------------------------------------------
1817
1818    fn decl_button(name: &str, binding: Binding) -> ActionDecl {
1819        ActionDecl {
1820            name: name.to_owned(),
1821            kind: ActionKind::Button,
1822            bindings: vec![binding],
1823        }
1824    }
1825
1826    fn decl_axis2_wasd(
1827        name: &str,
1828        up: KeyCode,
1829        down: KeyCode,
1830        left: KeyCode,
1831        right: KeyCode,
1832    ) -> ActionDecl {
1833        ActionDecl {
1834            name: name.to_owned(),
1835            kind: ActionKind::Axis2,
1836            bindings: vec![Binding::Wasd {
1837                up,
1838                down,
1839                left,
1840                right,
1841            }],
1842        }
1843    }
1844
1845    fn decl_axis2_mouse(name: &str) -> ActionDecl {
1846        ActionDecl {
1847            name: name.to_owned(),
1848            kind: ActionKind::Axis2,
1849            bindings: vec![Binding::MouseMotion],
1850        }
1851    }
1852
1853    fn decl_axis1_scroll(name: &str) -> ActionDecl {
1854        ActionDecl {
1855            name: name.to_owned(),
1856            kind: ActionKind::Axis1,
1857            bindings: vec![Binding::MouseScroll],
1858        }
1859    }
1860
1861    // -- typed accessors ----------------------------------------------------
1862
1863    /// One tick at sixty hertz, which is what `begin_tick` wants — a delta,
1864    /// not a timestamp.
1865    const TICK: f32 = 1.0 / 60.0;
1866
1867    /// Held is down, and so is the tick the key went down on.
1868    ///
1869    /// Both, not just the second: a game reading "is the thrust on" during the
1870    /// very tick the key arrived would otherwise see it off for one tick and
1871    /// stutter.
1872    #[test]
1873    fn a_button_is_held_while_it_is_down_and_not_after() {
1874        let mut map = ActionMap::new();
1875        map.declare(decl_button("thrust", Binding::Key(KeyCode::KeyW)));
1876
1877        assert!(!map.button_held("thrust"), "nothing is down yet");
1878
1879        map.begin_tick(TICK);
1880        map.key_event(KeyCode::KeyW, true);
1881        assert!(map.button_held("thrust"), "the tick the key arrives counts");
1882
1883        map.begin_tick(TICK);
1884        assert!(
1885            matches!(
1886                map.action("thrust"),
1887                Some(ActionValue::Button(ButtonAction {
1888                    state: ButtonState::Held { .. },
1889                    ..
1890                }))
1891            ),
1892            "a second tick with the key still down is Held, which is the other \
1893             state that must read as down"
1894        );
1895        assert!(map.button_held("thrust"));
1896
1897        map.key_event(KeyCode::KeyW, false);
1898        assert!(!map.button_held("thrust"), "released is not down");
1899    }
1900
1901    /// The edges are one tick wide, and they are not the level.
1902    #[test]
1903    fn the_edges_fire_once_each() {
1904        let mut map = ActionMap::new();
1905        map.declare(decl_button("flap", Binding::Key(KeyCode::Space)));
1906
1907        map.begin_tick(TICK);
1908        map.key_event(KeyCode::Space, true);
1909        assert!(map.just_pressed("flap"));
1910        assert!(!map.just_released("flap"));
1911
1912        map.begin_tick(TICK);
1913        assert!(
1914            !map.just_pressed("flap"),
1915            "holding the key does not press it again — this is the whole \
1916             reason a flap reads the edge and not the level"
1917        );
1918        assert!(map.button_held("flap"), "still down, though");
1919
1920        map.key_event(KeyCode::Space, false);
1921        assert!(map.just_released("flap"));
1922        map.begin_tick(TICK);
1923        assert!(
1924            !map.just_released("flap"),
1925            "the release edge is one tick too"
1926        );
1927    }
1928
1929    /// An axis answers with its value, and a composite is not longer than one.
1930    #[test]
1931    fn the_axis_accessors_read_the_value() {
1932        let mut map = ActionMap::new();
1933        map.declare(decl_axis2_wasd(
1934            "move",
1935            KeyCode::KeyW,
1936            KeyCode::KeyS,
1937            KeyCode::KeyA,
1938            KeyCode::KeyD,
1939        ));
1940        map.declare(decl_axis1_scroll("zoom"));
1941
1942        assert_eq!(map.axis2("move"), (0.0, 0.0));
1943        assert_eq!(map.axis1("zoom"), 0.0);
1944
1945        map.begin_tick(TICK);
1946        map.key_event(KeyCode::KeyD, true);
1947        let (x, y) = map.axis2("move");
1948        assert!((x - 1.0).abs() < 1e-6, "right alone is +1 on x, got {x}");
1949        assert!(y.abs() < 1e-6, "and nothing on y, got {y}");
1950
1951        map.key_event(KeyCode::KeyW, true);
1952        let (x, y) = map.axis2("move");
1953        let length = x.hypot(y);
1954        assert!(
1955            (length - 1.0).abs() < 1e-6,
1956            "a diagonal is normalised, or it is 41% faster than a cardinal: \
1957             got length {length}"
1958        );
1959
1960        map.mouse_scroll(0.0, 3.0);
1961        assert_ne!(map.axis1("zoom"), 0.0, "the scroll reached the axis");
1962    }
1963
1964    /// Asking the wrong kind, or a name nobody declared, is the idle value.
1965    ///
1966    /// Every game collapsed both cases this way already. Worth pinning because
1967    /// the alternative — a panic, or `true` — would turn a typo into a stuck
1968    /// input rather than a dead one.
1969    #[test]
1970    fn the_wrong_kind_and_the_wrong_name_are_idle() {
1971        let mut map = ActionMap::new();
1972        map.declare(decl_button("fire", Binding::Key(KeyCode::Space)));
1973        map.declare(decl_axis1_scroll("zoom"));
1974
1975        map.begin_tick(TICK);
1976        map.key_event(KeyCode::Space, true);
1977
1978        assert!(!map.button_held("zoom"), "an axis is not a held button");
1979        assert!(!map.just_pressed("zoom"));
1980        assert!(!map.just_released("zoom"));
1981        assert_eq!(map.axis1("fire"), 0.0, "a button is not an axis");
1982        assert_eq!(map.axis2("fire"), (0.0, 0.0));
1983
1984        assert!(!map.button_held("nonesuch"));
1985        assert!(!map.just_pressed("nonesuch"));
1986        assert_eq!(map.axis1("nonesuch"), 0.0);
1987        assert_eq!(map.axis2("nonesuch"), (0.0, 0.0));
1988    }
1989
1990    // -- ButtonState / ButtonAction -----------------------------------------
1991
1992    #[test]
1993    fn button_action_default_is_released() {
1994        let a = ButtonAction::default();
1995        assert!(matches!(a.state, ButtonState::Released));
1996        assert!(!a.just_pressed);
1997        assert!(!a.just_released);
1998    }
1999
2000    #[test]
2001    fn reset_edges_clears_flags() {
2002        let mut a = ButtonAction {
2003            state: ButtonState::Pressed,
2004            just_pressed: true,
2005            just_released: true,
2006        };
2007        a.reset_edges();
2008        assert!(!a.just_pressed);
2009        assert!(!a.just_released);
2010        // state is untouched by reset_edges.
2011        assert!(matches!(a.state, ButtonState::Pressed));
2012    }
2013
2014    // -- ActionMap: key binding ---------------------------------------------
2015
2016    #[test]
2017    fn key_press_and_release() {
2018        let mut map = ActionMap::new();
2019        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2020
2021        // Initially released.
2022        let v = map.action("jump").unwrap();
2023        assert!(matches!(
2024            v,
2025            ActionValue::Button(ButtonAction {
2026                state: ButtonState::Released,
2027                just_pressed: false,
2028                just_released: false,
2029            })
2030        ));
2031
2032        // Press space.
2033        map.key_event(KeyCode::Space, true);
2034        let v = map.action("jump").unwrap();
2035        assert!(matches!(
2036            v,
2037            ActionValue::Button(ButtonAction {
2038                state: ButtonState::Pressed,
2039                just_pressed: true,
2040                just_released: false,
2041            })
2042        ));
2043
2044        // Next tick: edges cleared, still held → Held { duration }.
2045        map.begin_tick(1.0 / 60.0);
2046        let v = map.action("jump").unwrap();
2047        assert!(matches!(
2048            v,
2049            ActionValue::Button(ButtonAction {
2050                state: ButtonState::Held { .. },
2051                just_pressed: false,
2052                just_released: false,
2053            })
2054        ));
2055
2056        // Release space.
2057        map.key_event(KeyCode::Space, false);
2058        let v = map.action("jump").unwrap();
2059        assert!(matches!(
2060            v,
2061            ActionValue::Button(ButtonAction {
2062                state: ButtonState::Released,
2063                just_pressed: false,
2064                just_released: true,
2065            })
2066        ));
2067    }
2068
2069    #[test]
2070    fn a_key_still_down_after_a_tick_reports_held_with_how_long_it_has_been() {
2071        let mut map = ActionMap::new();
2072        map.declare(decl_button("fire", Binding::Key(KeyCode::KeyF)));
2073
2074        map.key_event(KeyCode::KeyF, true);
2075        // Advance 2 seconds.
2076        map.begin_tick(2.0);
2077
2078        let v = map.action("fire").unwrap();
2079        if let ActionValue::Button(a) = v {
2080            if let ButtonState::Held { duration } = a.state {
2081                assert!(
2082                    (duration - 2.0).abs() < 0.001,
2083                    "expected ~2s, got {duration}"
2084                );
2085            } else {
2086                panic!("expected Held, got {:?}", a.state);
2087            }
2088        } else {
2089            panic!("expected Button, got {:?}", v);
2090        }
2091    }
2092
2093    // -- ActionMap: mouse-button binding ------------------------------------
2094
2095    #[test]
2096    fn mouse_button_press_and_release() {
2097        let mut map = ActionMap::new();
2098        map.declare(decl_button(
2099            "shoot",
2100            Binding::MouseButton(PointerButton::Left),
2101        ));
2102
2103        map.mouse_button(PointerButton::Left, true);
2104        let v = map.action("shoot").unwrap();
2105        assert!(matches!(
2106            v,
2107            ActionValue::Button(ButtonAction {
2108                state: ButtonState::Pressed,
2109                just_pressed: true,
2110                just_released: false,
2111            })
2112        ));
2113
2114        map.begin_tick(0.016);
2115        map.mouse_button(PointerButton::Left, false);
2116        let v = map.action("shoot").unwrap();
2117        assert!(matches!(
2118            v,
2119            ActionValue::Button(ButtonAction {
2120                state: ButtonState::Released,
2121                just_pressed: false,
2122                just_released: true,
2123            })
2124        ));
2125    }
2126
2127    // -- ActionMap: WASD composite -------------------------------------------
2128
2129    #[test]
2130    fn a_wasd_composite_stays_a_unit_vector_from_idle_through_every_diagonal() {
2131        let mut map = ActionMap::new();
2132        map.declare(decl_axis2_wasd(
2133            "move",
2134            KeyCode::KeyW,
2135            KeyCode::KeyS,
2136            KeyCode::KeyA,
2137            KeyCode::KeyD,
2138        ));
2139
2140        // Idle.
2141        let v = map.action("move").unwrap();
2142        assert_eq_axis2(v, 0.0, 0.0);
2143
2144        // Press W → (0, 1).
2145        map.key_event(KeyCode::KeyW, true);
2146        let v = map.action("move").unwrap();
2147        assert_eq_axis2(v, 0.0, 1.0);
2148
2149        // Press D as well → a *unit* diagonal, not (1, 1).
2150        map.key_event(KeyCode::KeyD, true);
2151        let v = map.action("move").unwrap();
2152        let diag = std::f32::consts::FRAC_1_SQRT_2;
2153        assert_eq_axis2(v, diag, diag);
2154
2155        // Release W and D, then press S and A → the opposite unit diagonal.
2156        map.key_event(KeyCode::KeyW, false);
2157        map.key_event(KeyCode::KeyD, false);
2158        map.key_event(KeyCode::KeyA, true);
2159        map.key_event(KeyCode::KeyS, true);
2160        let v = map.action("move").unwrap();
2161        assert_eq_axis2(v, -diag, -diag);
2162
2163        // Release all.
2164        map.key_event(KeyCode::KeyD, false);
2165        map.key_event(KeyCode::KeyA, false);
2166        map.key_event(KeyCode::KeyS, false);
2167        let v = map.action("move").unwrap();
2168        assert_eq_axis2(v, 0.0, 0.0);
2169    }
2170
2171    #[test]
2172    fn wasd_opposing_keys_cancel() {
2173        let mut map = ActionMap::new();
2174        map.declare(decl_axis2_wasd(
2175            "move",
2176            KeyCode::KeyW,
2177            KeyCode::KeyS,
2178            KeyCode::KeyA,
2179            KeyCode::KeyD,
2180        ));
2181
2182        // W + S simultaneously → (0, 0).
2183        map.key_event(KeyCode::KeyW, true);
2184        map.key_event(KeyCode::KeyS, true);
2185        let v = map.action("move").unwrap();
2186        assert_eq_axis2(v, 0.0, 0.0);
2187
2188        // A + D simultaneously → (0, 0).
2189        map.key_event(KeyCode::KeyA, true);
2190        map.key_event(KeyCode::KeyD, true);
2191        let v = map.action("move").unwrap();
2192        assert_eq_axis2(v, 0.0, 0.0);
2193    }
2194
2195    // -- ActionMap: mouse motion --------------------------------------------
2196
2197    #[test]
2198    fn mouse_motion_accumulates_within_a_tick_and_begin_tick_clears_it() {
2199        let mut map = ActionMap::new();
2200        map.declare(decl_axis2_mouse("look"));
2201
2202        // Accumulate delta over several events in one tick.
2203        map.mouse_motion(5.0, -3.0);
2204        map.mouse_motion(2.0, 1.0);
2205        let v = map.action("look").unwrap();
2206        assert_eq_axis2(v, 7.0, -2.0);
2207
2208        // begin_tick resets deltas.
2209        map.begin_tick(0.016);
2210        let v = map.action("look").unwrap();
2211        assert_eq_axis2(v, 0.0, 0.0);
2212    }
2213
2214    // -- ActionMap: mouse scroll --------------------------------------------
2215
2216    #[test]
2217    fn scroll_sums_positive_and_negative_notches_and_resets_on_the_next_tick() {
2218        let mut map = ActionMap::new();
2219        map.declare(decl_axis1_scroll("zoom"));
2220
2221        map.mouse_scroll(0.0, 1.0);
2222        let v = map.action("zoom").unwrap();
2223        assert_eq_axis1(v, 1.0);
2224
2225        map.mouse_scroll(0.0, -0.5);
2226        let v = map.action("zoom").unwrap();
2227        assert_eq_axis1(v, 0.5);
2228
2229        // begin_tick resets.
2230        map.begin_tick(0.016);
2231        let v = map.action("zoom").unwrap();
2232        assert_eq_axis1(v, 0.0);
2233    }
2234
2235    #[test]
2236    fn scroll_clamped_to_range() {
2237        let mut map = ActionMap::new();
2238        map.declare(decl_axis1_scroll("zoom"));
2239
2240        map.mouse_scroll(0.0, 5.0);
2241        let v = map.action("zoom").unwrap();
2242        assert_eq_axis1(v, 1.0);
2243
2244        map.begin_tick(0.016);
2245        map.mouse_scroll(0.0, -5.0);
2246        let v = map.action("zoom").unwrap();
2247        assert_eq_axis1(v, -1.0);
2248    }
2249
2250    // -- ActionMap: absolute pointer position --------------------------------
2251
2252    fn decl_axis1_pointer(name: &str, axis: PointerAxis) -> ActionDecl {
2253        ActionDecl {
2254            name: name.to_owned(),
2255            kind: ActionKind::Axis1,
2256            bindings: vec![Binding::PointerPosition { axis }],
2257        }
2258    }
2259
2260    /// **Each axis reads its own coordinate, at the value it was given.**
2261    ///
2262    /// Two actions on one map and asymmetric coordinates, because that is what
2263    /// a swapped axis fails: a pointer at `(0.5, 0.5)` puts the right answer on
2264    /// both of them.
2265    #[test]
2266    fn a_pointer_position_lands_on_the_axis_it_is_bound_to() {
2267        let mut map = ActionMap::new();
2268        map.declare(decl_axis1_pointer("aim_x", PointerAxis::X));
2269        map.declare(decl_axis1_pointer("aim_y", PointerAxis::Y));
2270
2271        assert_eq!(
2272            map.axis1("aim_x"),
2273            0.0,
2274            "an axis nothing has pointed at yet is idle, not at the left edge",
2275        );
2276
2277        map.begin_tick(TICK);
2278        map.pointer_position(-0.75, 0.25);
2279
2280        assert_eq_axis1(map.action("aim_x").unwrap(), -0.75);
2281        assert_eq_axis1(map.action("aim_y").unwrap(), 0.25);
2282    }
2283
2284    /// **The position is a level: it survives ticks, unlike a scroll delta.**
2285    ///
2286    /// This is also what "a pointer that leaves keeps its last position" means
2287    /// mechanically — leaving reports no coordinate, so nothing here is fed and
2288    /// the axis is exactly where the last real position put it.
2289    #[test]
2290    fn an_absolute_axis_holds_its_position_across_ticks() {
2291        let mut map = ActionMap::new();
2292        map.declare(decl_axis1_pointer("aim", PointerAxis::X));
2293        map.declare(decl_axis1_scroll("zoom"));
2294
2295        map.begin_tick(TICK);
2296        map.pointer_position(0.6, 0.0);
2297        map.mouse_scroll(0.0, 1.0);
2298        assert_eq_axis1(map.action("aim").unwrap(), 0.6);
2299        assert_eq_axis1(map.action("zoom").unwrap(), 1.0);
2300
2301        map.begin_tick(TICK);
2302        assert_eq_axis1(map.action("zoom").unwrap(), 0.0);
2303        assert_eq_axis1(map.action("aim").unwrap(), 0.6);
2304    }
2305
2306    /// **`pointer_moved` is an edge, and it is about the pointer moving.**
2307    ///
2308    /// A tick that re-sends the coordinate the axis already holds raises
2309    /// nothing: a consumer arbitrating between the pointer and the keyboard
2310    /// reads this as "the player just asked for this", and a browser that
2311    /// re-reports a resting cursor would otherwise hold the keyboard off
2312    /// forever.
2313    #[test]
2314    fn the_pointer_edge_is_one_tick_wide_and_only_for_a_new_position() {
2315        let mut map = ActionMap::new();
2316        map.declare(decl_axis1_pointer("aim", PointerAxis::X));
2317
2318        let moved = |map: &ActionMap| match map.action("aim") {
2319            Some(ActionValue::Axis1(axis)) => axis.pointer_moved,
2320            other => panic!("expected Axis1, got {other:?}"),
2321        };
2322
2323        map.begin_tick(TICK);
2324        assert!(!moved(&map), "nothing has pointed anywhere yet");
2325        map.pointer_position(0.2, 0.0);
2326        assert!(moved(&map), "the pointer moved on this tick");
2327
2328        map.begin_tick(TICK);
2329        assert!(!moved(&map), "the edge is one tick wide");
2330        assert_eq_axis1(map.action("aim").unwrap(), 0.2);
2331
2332        map.pointer_position(0.2, 0.0);
2333        assert!(
2334            !moved(&map),
2335            "the same coordinate again is not the player asking for anything",
2336        );
2337        map.pointer_position(0.2, 0.9);
2338        assert!(
2339            moved(&map),
2340            "the other coordinate changing is still the pointer moving",
2341        );
2342    }
2343
2344    /// **An absolute source replaces the relative ones on the same axis.**
2345    ///
2346    /// Summing them would add a place to a rate. The keyboard drives the axis
2347    /// while no pointer has ever reported a position, and stops the moment one
2348    /// has — which is the documented reason a game whose keys mean something
2349    /// *different* from its pointer declares two actions instead of one.
2350    #[test]
2351    fn an_absolute_binding_replaces_the_relative_ones_on_the_same_axis() {
2352        let mut map = ActionMap::new();
2353        map.declare(ActionDecl {
2354            name: "slide".to_owned(),
2355            kind: ActionKind::Axis1,
2356            bindings: vec![
2357                Binding::KeyAxis {
2358                    negative: KeyCode::ArrowLeft,
2359                    positive: KeyCode::ArrowRight,
2360                },
2361                Binding::PointerPosition {
2362                    axis: PointerAxis::X,
2363                },
2364            ],
2365        });
2366
2367        map.begin_tick(TICK);
2368        map.key_event(KeyCode::ArrowRight, true);
2369        assert_eq_axis1(map.action("slide").unwrap(), 1.0);
2370
2371        map.pointer_position(-0.5, 0.0);
2372        assert_eq_axis1(map.action("slide").unwrap(), -0.5);
2373    }
2374
2375    /// **The surface's edge is the end of the axis.**
2376    #[test]
2377    fn a_pointer_outside_the_surface_is_clamped_to_its_edge() {
2378        let mut map = ActionMap::new();
2379        map.declare(decl_axis1_pointer("aim", PointerAxis::X));
2380
2381        map.pointer_position(4.0, 0.0);
2382        assert_eq_axis1(map.action("aim").unwrap(), 1.0);
2383
2384        map.pointer_position(-4.0, 0.0);
2385        assert_eq_axis1(map.action("aim").unwrap(), -1.0);
2386    }
2387
2388    /// **A pointer binding joins the keys on a button, it does not replace
2389    /// them.**
2390    ///
2391    /// The flap that a phone taps and a keyboard presses is one action with
2392    /// both bindings on it, and each still fires on its own. A binding list
2393    /// that had been overwritten rather than appended to passes every test that
2394    /// only ever presses the pointer.
2395    #[test]
2396    fn a_pointer_button_joins_the_keys_bound_to_the_same_action() {
2397        let mut map = ActionMap::new();
2398        map.declare(ActionDecl {
2399            name: "flap".to_owned(),
2400            kind: ActionKind::Button,
2401            bindings: vec![
2402                Binding::Key(KeyCode::Space),
2403                Binding::MouseButton(PointerButton::Left),
2404            ],
2405        });
2406
2407        map.begin_tick(TICK);
2408        map.key_event(KeyCode::Space, true);
2409        assert!(map.just_pressed("flap"), "the key still flaps");
2410        map.key_event(KeyCode::Space, false);
2411
2412        map.begin_tick(TICK);
2413        map.mouse_button(PointerButton::Left, true);
2414        assert!(map.just_pressed("flap"), "and so does the tap");
2415        map.mouse_button(PointerButton::Left, false);
2416        assert!(map.just_released("flap"));
2417    }
2418
2419    // -- begin_tick resets edges and deltas ----------------------------------
2420
2421    #[test]
2422    fn begin_tick_resets_just_pressed_and_just_released() {
2423        let mut map = ActionMap::new();
2424        map.declare(decl_button("a", Binding::Key(KeyCode::KeyA)));
2425
2426        map.key_event(KeyCode::KeyA, true);
2427        assert!(matches!(
2428            map.action("a").unwrap(),
2429            ActionValue::Button(ButtonAction {
2430                just_pressed: true,
2431                ..
2432            })
2433        ));
2434
2435        map.begin_tick(0.016);
2436        assert!(matches!(
2437            map.action("a").unwrap(),
2438            ActionValue::Button(ButtonAction {
2439                just_pressed: false,
2440                just_released: false,
2441                ..
2442            })
2443        ));
2444
2445        map.key_event(KeyCode::KeyA, false);
2446        assert!(matches!(
2447            map.action("a").unwrap(),
2448            ActionValue::Button(ButtonAction {
2449                just_released: true,
2450                ..
2451            })
2452        ));
2453
2454        map.begin_tick(0.016);
2455        assert!(matches!(
2456            map.action("a").unwrap(),
2457            ActionValue::Button(ButtonAction {
2458                just_released: false,
2459                ..
2460            })
2461        ));
2462    }
2463
2464    // -- enable / disable ---------------------------------------------------
2465
2466    #[test]
2467    fn disabled_action_returns_idle() {
2468        let mut map = ActionMap::new();
2469        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2470
2471        map.key_event(KeyCode::Space, true);
2472        assert!(matches!(
2473            map.action("jump").unwrap(),
2474            ActionValue::Button(ButtonAction {
2475                state: ButtonState::Pressed,
2476                ..
2477            })
2478        ));
2479
2480        map.set_enabled("jump", false);
2481        // Disabled → Released and edges cleared.
2482        let v = map.action("jump").unwrap();
2483        assert!(matches!(
2484            v,
2485            ActionValue::Button(ButtonAction {
2486                state: ButtonState::Released,
2487                just_pressed: false,
2488                just_released: false,
2489            })
2490        ));
2491
2492        // Re-enable: key is still held, so it becomes pressed again.
2493        map.set_enabled("jump", true);
2494        map.key_event(KeyCode::Space, true); // need to re-trigger resolution
2495        let v = map.action("jump").unwrap();
2496        assert!(matches!(
2497            v,
2498            ActionValue::Button(ButtonAction {
2499                state: ButtonState::Pressed,
2500                just_pressed: true,
2501                ..
2502            })
2503        ));
2504    }
2505
2506    #[test]
2507    fn disabled_wasd_returns_zero() {
2508        let mut map = ActionMap::new();
2509        map.declare(decl_axis2_wasd(
2510            "move",
2511            KeyCode::KeyW,
2512            KeyCode::KeyS,
2513            KeyCode::KeyA,
2514            KeyCode::KeyD,
2515        ));
2516        map.key_event(KeyCode::KeyW, true);
2517        assert_eq_axis2(map.action("move").unwrap(), 0.0, 1.0);
2518
2519        map.set_enabled("move", false);
2520        assert_eq_axis2(map.action("move").unwrap(), 0.0, 0.0);
2521
2522        // …and it must stay zero across ticks. `begin_tick` used to re-resolve
2523        // every slot with no enabled check, so the still-held W came straight
2524        // back on the next tick.
2525        for tick in 0..3 {
2526            map.begin_tick(1.0 / 60.0);
2527            assert_eq_axis2(map.action("move").unwrap(), 0.0, 0.0);
2528            assert!(tick < 3);
2529        }
2530    }
2531
2532    /// The same bug for a Button action: disabling "jump" while Space is held
2533    /// cleared it, and the very next `begin_tick` re-emitted
2534    /// `state: Pressed, just_pressed: true` — a menu opened with gameplay
2535    /// disabled still jumped.
2536    #[test]
2537    fn a_disabled_button_does_not_re_press_itself_on_the_next_tick() {
2538        let mut map = ActionMap::new();
2539        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2540
2541        map.key_event(KeyCode::Space, true);
2542        map.set_enabled("jump", false);
2543
2544        for _ in 0..3 {
2545            map.begin_tick(1.0 / 60.0);
2546            let v = map.action("jump").unwrap();
2547            assert!(
2548                matches!(
2549                    v,
2550                    ActionValue::Button(ButtonAction {
2551                        state: ButtonState::Released,
2552                        just_pressed: false,
2553                        just_released: false,
2554                    })
2555                ),
2556                "a disabled action fired anyway: {v:?}"
2557            );
2558        }
2559
2560        // Re-enabling with the key still held presses it again, which is the
2561        // intended behaviour.
2562        map.set_enabled("jump", true);
2563        map.begin_tick(1.0 / 60.0);
2564        assert!(matches!(
2565            map.action("jump").unwrap(),
2566            ActionValue::Button(ButtonAction {
2567                state: ButtonState::Pressed,
2568                just_pressed: true,
2569                ..
2570            })
2571        ));
2572    }
2573
2574    /// `set_enabled` on a name that was never declared used to insert into a
2575    /// `HashSet<String>` that nothing ever pruned.
2576    #[test]
2577    fn set_enabled_on_an_unknown_action_is_a_no_op() {
2578        let mut map = ActionMap::new();
2579        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2580        map.set_enabled("typo-in-the-profile", true);
2581        map.set_enabled("typo-in-the-profile", false);
2582        assert_eq!(map.is_enabled("typo-in-the-profile"), None);
2583        assert_eq!(map.is_enabled("jump"), Some(true));
2584        assert_eq!(map.action_names().count(), 1);
2585    }
2586
2587    // -- declare / rebind ---------------------------------------------------
2588
2589    #[test]
2590    fn try_declare_reports_a_duplicate_rather_than_aborting() {
2591        let mut map = ActionMap::new();
2592        assert!(
2593            map.try_declare(decl_button("jump", Binding::Key(KeyCode::Space)))
2594                .is_ok()
2595        );
2596        assert_eq!(
2597            map.try_declare(decl_button("jump", Binding::Key(KeyCode::KeyJ))),
2598            Err(ActionMapError::DuplicateName("jump".to_owned()))
2599        );
2600        // The first declaration is untouched.
2601        assert_eq!(
2602            map.bindings("jump"),
2603            Some(&[Binding::Key(KeyCode::Space)][..])
2604        );
2605    }
2606
2607    #[test]
2608    fn rebind_replaces_bindings_and_keeps_the_enabled_flag() {
2609        let mut map = ActionMap::new();
2610        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2611        map.set_enabled("jump", false);
2612
2613        map.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
2614            .expect("declared");
2615        assert_eq!(
2616            map.bindings("jump"),
2617            Some(&[Binding::Key(KeyCode::KeyJ)][..])
2618        );
2619        assert_eq!(
2620            map.is_enabled("jump"),
2621            Some(false),
2622            "the rebind must not silently re-enable the action"
2623        );
2624
2625        map.set_enabled("jump", true);
2626        map.key_event(KeyCode::KeyJ, true);
2627        assert!(matches!(
2628            map.action("jump").unwrap(),
2629            ActionValue::Button(ButtonAction {
2630                state: ButtonState::Pressed,
2631                ..
2632            })
2633        ));
2634        // The old key no longer does anything.
2635        map.begin_tick(0.016);
2636        map.key_event(KeyCode::KeyJ, false);
2637        map.begin_tick(0.016);
2638        map.key_event(KeyCode::Space, true);
2639        assert!(matches!(
2640            map.action("jump").unwrap(),
2641            ActionValue::Button(ButtonAction {
2642                state: ButtonState::Released,
2643                ..
2644            })
2645        ));
2646    }
2647
2648    #[test]
2649    fn rebind_an_unknown_action_errors() {
2650        let mut map = ActionMap::new();
2651        assert_eq!(
2652            map.rebind("nope", vec![]),
2653            Err(ActionMapError::UnknownAction("nope".to_owned()))
2654        );
2655    }
2656
2657    /// A rebind while the new key is already held resolves immediately.
2658    #[test]
2659    fn rebind_resolves_against_the_current_held_keys() {
2660        let mut map = ActionMap::new();
2661        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2662        map.key_event(KeyCode::KeyJ, true);
2663        map.rebind("jump", vec![Binding::Key(KeyCode::KeyJ)])
2664            .expect("declared");
2665        assert!(matches!(
2666            map.action("jump").unwrap(),
2667            ActionValue::Button(ButtonAction {
2668                state: ButtonState::Pressed,
2669                just_pressed: true,
2670                ..
2671            })
2672        ));
2673    }
2674
2675    // -- Axis1 key pairs ----------------------------------------------------
2676
2677    /// A single `Key` binding can only ever add +1.0, so a keyboard axis could
2678    /// not go negative at all.
2679    #[test]
2680    fn key_axis_reaches_both_ends_of_the_range() {
2681        let mut map = ActionMap::new();
2682        map.declare(ActionDecl {
2683            name: "throttle".into(),
2684            kind: ActionKind::Axis1,
2685            bindings: vec![Binding::KeyAxis {
2686                negative: KeyCode::KeyS,
2687                positive: KeyCode::KeyW,
2688            }],
2689        });
2690
2691        assert_eq_axis1(map.action("throttle").unwrap(), 0.0);
2692
2693        map.key_event(KeyCode::KeyW, true);
2694        assert_eq_axis1(map.action("throttle").unwrap(), 1.0);
2695
2696        // Both held → cancel.
2697        map.key_event(KeyCode::KeyS, true);
2698        assert_eq_axis1(map.action("throttle").unwrap(), 0.0);
2699
2700        map.key_event(KeyCode::KeyW, false);
2701        assert_eq_axis1(map.action("throttle").unwrap(), -1.0);
2702    }
2703
2704    /// A `KeyAxis` on a `Button` action is "either key is down".
2705    #[test]
2706    fn key_axis_drives_a_button_from_either_key() {
2707        let mut map = ActionMap::new();
2708        map.declare(decl_button(
2709            "any",
2710            Binding::KeyAxis {
2711                negative: KeyCode::KeyS,
2712                positive: KeyCode::KeyW,
2713            },
2714        ));
2715        map.key_event(KeyCode::KeyS, true);
2716        assert!(matches!(
2717            map.action("any").unwrap(),
2718            ActionValue::Button(ButtonAction {
2719                state: ButtonState::Pressed,
2720                ..
2721            })
2722        ));
2723    }
2724
2725    // -- non-finite input ---------------------------------------------------
2726
2727    /// `mouse_motion(NAN, 1.0)` used to write `Axis2 { x: NaN, y: 1.0 }`
2728    /// straight through, and `f32::clamp` returns `NaN` for a `NaN` scroll, so
2729    /// the clamp downstream was no defence either.
2730    #[test]
2731    fn non_finite_deltas_are_dropped_at_the_ingest_boundary() {
2732        let mut map = ActionMap::new();
2733        map.declare(decl_axis2_mouse("look"));
2734        map.declare(decl_axis1_scroll("zoom"));
2735
2736        for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2737            map.mouse_motion(bad, 1.0);
2738            map.mouse_motion(1.0, bad);
2739            map.mouse_scroll(0.0, bad);
2740        }
2741
2742        match map.action("look").unwrap() {
2743            ActionValue::Axis2(a) => assert!(
2744                a.x.is_finite() && a.y.is_finite(),
2745                "look went non-finite: {a:?}"
2746            ),
2747            other => panic!("expected Axis2, got {other:?}"),
2748        }
2749        match map.action("zoom").unwrap() {
2750            ActionValue::Axis1(a) => {
2751                assert!(a.value.is_finite(), "zoom went non-finite: {a:?}");
2752            }
2753            other => panic!("expected Axis1, got {other:?}"),
2754        }
2755
2756        // Good deltas still land.
2757        map.begin_tick(0.016);
2758        map.mouse_motion(3.0, -4.0);
2759        assert_eq_axis2(map.action("look").unwrap(), 3.0, -4.0);
2760    }
2761
2762    /// A non-finite pointer coordinate must not become the axis a paddle is
2763    /// positioned from — and must not evict the last good position either.
2764    #[test]
2765    fn a_non_finite_pointer_position_is_dropped_and_the_last_one_survives() {
2766        let mut map = ActionMap::new();
2767        map.declare(decl_axis1_pointer("aim", PointerAxis::X));
2768
2769        map.pointer_position(0.4, 0.0);
2770        for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2771            map.pointer_position(bad, 0.0);
2772            map.pointer_position(0.0, bad);
2773        }
2774
2775        assert_eq_axis1(map.action("aim").unwrap(), 0.4);
2776    }
2777
2778    /// A non-finite `dt` must not poison the clock every `Held` duration is
2779    /// measured against.
2780    #[test]
2781    fn a_non_finite_dt_does_not_poison_hold_durations() {
2782        let mut map = ActionMap::new();
2783        map.declare(decl_button("fire", Binding::Key(KeyCode::KeyF)));
2784        map.key_event(KeyCode::KeyF, true);
2785        map.begin_tick(f32::NAN);
2786        map.begin_tick(2.0);
2787
2788        let ActionValue::Button(a) = map.action("fire").unwrap() else {
2789            panic!("expected Button");
2790        };
2791        let ButtonState::Held { duration } = a.state else {
2792            panic!("expected Held, got {:?}", a.state);
2793        };
2794        assert!((duration - 2.0).abs() < 0.001, "got {duration}");
2795    }
2796
2797    /// A negative `dt` used to move the clock backwards, so a key held across
2798    /// it reported a negative `Held` duration — and the lost time then made the
2799    /// next tick measure the hold as though it had never been held.
2800    #[test]
2801    fn a_negative_dt_does_not_report_a_negative_hold_duration() {
2802        let mut map = ActionMap::new();
2803        map.declare(decl_button("fire", Binding::Key(KeyCode::KeyF)));
2804        map.key_event(KeyCode::KeyF, true);
2805
2806        map.begin_tick(-1.0);
2807        let ActionValue::Button(a) = map.action("fire").unwrap() else {
2808            panic!("expected Button");
2809        };
2810        let ButtonState::Held { duration } = a.state else {
2811            panic!("expected Held, got {:?}", a.state);
2812        };
2813        assert!(
2814            duration >= 0.0,
2815            "a negative dt must not report a negative hold duration, got {duration}"
2816        );
2817
2818        // The negative tick was dropped rather than absorbed: a real 1s tick
2819        // measures a full second, not the second minus the backwards one.
2820        map.begin_tick(1.0);
2821        let ActionValue::Button(a) = map.action("fire").unwrap() else {
2822            panic!("expected Button");
2823        };
2824        let ButtonState::Held { duration } = a.state else {
2825            panic!("expected Held, got {:?}", a.state);
2826        };
2827        assert!(
2828            (duration - 1.0).abs() < 0.001,
2829            "with the negative tick dropped the hold measures normally, got {duration}"
2830        );
2831    }
2832
2833    // -- action_mut ---------------------------------------------------------
2834
2835    #[test]
2836    fn action_mut_lets_caller_clear_edges() {
2837        let mut map = ActionMap::new();
2838        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2839
2840        map.key_event(KeyCode::Space, true);
2841        assert!(matches!(
2842            map.action("jump").unwrap(),
2843            ActionValue::Button(ButtonAction {
2844                just_pressed: true,
2845                ..
2846            })
2847        ));
2848
2849        // Consume the edge mid-tick.
2850        if let Some(ActionValue::Button(a)) = map.action_mut("jump") {
2851            a.just_pressed = false;
2852        }
2853
2854        assert!(matches!(
2855            map.action("jump").unwrap(),
2856            ActionValue::Button(ButtonAction {
2857                just_pressed: false,
2858                ..
2859            })
2860        ));
2861    }
2862
2863    // -- multiple bindings per action ---------------------------------------
2864
2865    #[test]
2866    fn multiple_bindings_any_triggers() {
2867        let mut map = ActionMap::new();
2868        map.declare(ActionDecl {
2869            name: "jump".into(),
2870            kind: ActionKind::Button,
2871            bindings: vec![
2872                Binding::Key(KeyCode::Space),
2873                Binding::MouseButton(PointerButton::Left),
2874            ],
2875        });
2876
2877        // Either binding triggers.
2878        map.key_event(KeyCode::Space, true);
2879        assert!(matches!(
2880            map.action("jump").unwrap(),
2881            ActionValue::Button(ButtonAction {
2882                state: ButtonState::Pressed,
2883                ..
2884            })
2885        ));
2886
2887        map.begin_tick(0.016);
2888        map.key_event(KeyCode::Space, false);
2889        assert!(matches!(
2890            map.action("jump").unwrap(),
2891            ActionValue::Button(ButtonAction {
2892                state: ButtonState::Released,
2893                ..
2894            })
2895        ));
2896
2897        map.begin_tick(0.016);
2898        map.mouse_button(PointerButton::Left, true);
2899        assert!(matches!(
2900            map.action("jump").unwrap(),
2901            ActionValue::Button(ButtonAction {
2902                state: ButtonState::Pressed,
2903                ..
2904            })
2905        ));
2906    }
2907
2908    #[test]
2909    fn multiple_bindings_stays_held_until_all_released() {
2910        let mut map = ActionMap::new();
2911        map.declare(ActionDecl {
2912            name: "crouch".into(),
2913            kind: ActionKind::Button,
2914            bindings: vec![
2915                Binding::Key(KeyCode::ControlLeft),
2916                Binding::Key(KeyCode::KeyC),
2917            ],
2918        });
2919
2920        map.key_event(KeyCode::ControlLeft, true);
2921        assert!(matches!(
2922            map.action("crouch").unwrap(),
2923            ActionValue::Button(ButtonAction {
2924                state: ButtonState::Pressed,
2925                ..
2926            })
2927        ));
2928
2929        map.begin_tick(0.016);
2930        map.key_event(KeyCode::KeyC, true);
2931        // Still held (via ControlLeft).
2932        assert!(matches!(
2933            map.action("crouch").unwrap(),
2934            ActionValue::Button(ButtonAction {
2935                state: ButtonState::Held { .. },
2936                ..
2937            })
2938        ));
2939
2940        // Release one, still held via the other.
2941        map.key_event(KeyCode::ControlLeft, false);
2942        assert!(matches!(
2943            map.action("crouch").unwrap(),
2944            ActionValue::Button(ButtonAction {
2945                state: ButtonState::Held { .. },
2946                ..
2947            })
2948        ));
2949
2950        // Release both → released.
2951        map.key_event(KeyCode::KeyC, false);
2952        assert!(matches!(
2953            map.action("crouch").unwrap(),
2954            ActionValue::Button(ButtonAction {
2955                state: ButtonState::Released,
2956                just_released: true,
2957                ..
2958            })
2959        ));
2960    }
2961
2962    // -- action_names -------------------------------------------------------
2963
2964    #[test]
2965    fn action_names_returns_in_order() {
2966        let mut map = ActionMap::new();
2967        map.declare(decl_button("a", Binding::Key(KeyCode::KeyA)));
2968        map.declare(decl_button("b", Binding::Key(KeyCode::KeyB)));
2969        map.declare(decl_button("c", Binding::Key(KeyCode::KeyC)));
2970
2971        let names: Vec<&str> = map.action_names().collect();
2972        assert_eq!(names, vec!["a", "b", "c"]);
2973    }
2974
2975    // -- InputTickState -----------------------------------------------------
2976
2977    #[test]
2978    fn tick_state_capture_and_get() {
2979        let mut map = ActionMap::new();
2980        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
2981        map.declare(decl_axis2_wasd(
2982            "move",
2983            KeyCode::KeyW,
2984            KeyCode::KeyS,
2985            KeyCode::KeyA,
2986            KeyCode::KeyD,
2987        ));
2988
2989        map.key_event(KeyCode::Space, true);
2990        map.key_event(KeyCode::KeyW, true);
2991
2992        let tick = InputTickState::capture(&map);
2993        assert_eq!(tick.actions.len(), 2);
2994
2995        // jump should be pressed.
2996        let jump = tick.get("jump").unwrap();
2997        assert!(matches!(
2998            jump,
2999            ActionValue::Button(ButtonAction {
3000                state: ButtonState::Pressed,
3001                just_pressed: true,
3002                just_released: false,
3003            })
3004        ));
3005
3006        // move should be (0, 1).
3007        let mv = tick.get("move").unwrap();
3008        assert_eq_axis2(mv, 0.0, 1.0);
3009
3010        // Unknown name → None.
3011        assert!(tick.get("nope").is_none());
3012    }
3013
3014    #[test]
3015    fn tick_state_new_is_empty() {
3016        let tick = InputTickState::new();
3017        assert!(tick.actions.is_empty());
3018        assert!(tick.get("anything").is_none());
3019    }
3020
3021    // -- edge case: multiple actions with overlapping bindings ---------------
3022
3023    #[test]
3024    fn two_actions_bound_to_one_key_both_report_pressed() {
3025        let mut map = ActionMap::new();
3026        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
3027        map.declare(decl_button("fire", Binding::Key(KeyCode::Space)));
3028
3029        map.key_event(KeyCode::Space, true);
3030
3031        // Both should be pressed independently.
3032        let jump = map.action("jump").unwrap();
3033        let fire = map.action("fire").unwrap();
3034        assert!(matches!(
3035            jump,
3036            ActionValue::Button(ButtonAction {
3037                state: ButtonState::Pressed,
3038                just_pressed: true,
3039                ..
3040            })
3041        ));
3042        assert!(matches!(
3043            fire,
3044            ActionValue::Button(ButtonAction {
3045                state: ButtonState::Pressed,
3046                just_pressed: true,
3047                ..
3048            })
3049        ));
3050    }
3051
3052    // -- edge case: non-existent action -------------------------------------
3053
3054    #[test]
3055    fn unknown_action_returns_none() {
3056        let mut map = ActionMap::new();
3057        assert!(map.action("nope").is_none());
3058        assert!(map.action_mut("nope").is_none());
3059    }
3060
3061    // -- Debug implementations ----------------------------------------------
3062
3063    #[test]
3064    fn every_input_type_debug_prints_without_panicking() {
3065        // Just make sure they don't panic.
3066        let mut map = ActionMap::new();
3067        map.declare(decl_button("jump", Binding::Key(KeyCode::Space)));
3068        let _ = format!("{map:?}");
3069
3070        let tick = InputTickState::capture(&map);
3071        let _ = format!("{tick:?}");
3072
3073        let _ = format!("{:?}", ButtonAction::default());
3074        let _ = format!("{:?}", ButtonState::Pressed);
3075        let _ = format!("{:?}", ActionKind::Button);
3076        let _ = format!(
3077            "{:?}",
3078            Binding::Wasd {
3079                up: KeyCode::KeyW,
3080                down: KeyCode::KeyS,
3081                left: KeyCode::KeyA,
3082                right: KeyCode::KeyD,
3083            }
3084        );
3085    }
3086
3087    // -- Default impls ------------------------------------------------------
3088
3089    #[test]
3090    fn axis_defaults_are_zero() {
3091        assert_eq!(
3092            Axis1Action::default(),
3093            Axis1Action {
3094                value: 0.0,
3095                pointer_moved: false,
3096            }
3097        );
3098        assert_eq!(Axis2Action::default(), Axis2Action { x: 0.0, y: 0.0 });
3099    }
3100
3101    #[test]
3102    fn input_tick_state_default_is_empty() {
3103        let tick = InputTickState::default();
3104        assert!(tick.actions.is_empty());
3105    }
3106
3107    // -- WASD with non-standard keys ----------------------------------------
3108
3109    #[test]
3110    fn wasd_with_arrow_keys() {
3111        let mut map = ActionMap::new();
3112        map.declare(decl_axis2_wasd(
3113            "move",
3114            KeyCode::ArrowUp,
3115            KeyCode::ArrowDown,
3116            KeyCode::ArrowLeft,
3117            KeyCode::ArrowRight,
3118        ));
3119
3120        map.key_event(KeyCode::ArrowRight, true);
3121        map.key_event(KeyCode::ArrowUp, true);
3122        let diag = std::f32::consts::FRAC_1_SQRT_2;
3123        assert_eq_axis2(map.action("move").unwrap(), diag, diag);
3124    }
3125
3126    /// The docs promised a unit vector and the code never normalized, so a
3127    /// diagonal was 1.414 long — 41% faster than a cardinal direction.
3128    #[test]
3129    fn wasd_diagonal_is_not_faster_than_a_cardinal_direction() {
3130        let mut map = ActionMap::new();
3131        map.declare(decl_axis2_wasd(
3132            "move",
3133            KeyCode::KeyW,
3134            KeyCode::KeyS,
3135            KeyCode::KeyA,
3136            KeyCode::KeyD,
3137        ));
3138
3139        map.key_event(KeyCode::KeyW, true);
3140        let cardinal = axis2_len(map.action("move").unwrap());
3141        assert!((cardinal - 1.0).abs() < 0.001, "got {cardinal}");
3142
3143        map.key_event(KeyCode::KeyD, true);
3144        let diagonal = axis2_len(map.action("move").unwrap());
3145        assert!(
3146            (diagonal - 1.0).abs() < 0.001,
3147            "diagonal magnitude {diagonal} should be 1.0"
3148        );
3149    }
3150
3151    /// Mouse motion is a pixel delta, not a direction: normalizing it would
3152    /// throw away how far the pointer actually moved.
3153    #[test]
3154    fn mouse_motion_is_not_normalized() {
3155        let mut map = ActionMap::new();
3156        map.declare(decl_axis2_mouse("look"));
3157        map.mouse_motion(30.0, 40.0);
3158        assert_eq_axis2(map.action("look").unwrap(), 30.0, 40.0);
3159    }
3160
3161    // -- on-screen controls --------------------------------------------------
3162
3163    /// An on-screen stick and the keyboard drive the **same** action, and the
3164    /// action cannot tell which of them spoke.
3165    ///
3166    /// The whole claim of [`Binding::Virtual`]: one `move`, two devices, and a
3167    /// game that reads `axis2` and never asks where the value came from.
3168    #[test]
3169    fn a_stick_and_a_keyboard_drive_one_action() {
3170        let mut map = ActionMap::new();
3171        map.declare(ActionDecl {
3172            name: "move".to_owned(),
3173            kind: ActionKind::Axis2,
3174            bindings: vec![
3175                Binding::Wasd {
3176                    up: KeyCode::KeyW,
3177                    down: KeyCode::KeyS,
3178                    left: KeyCode::KeyA,
3179                    right: KeyCode::KeyD,
3180                },
3181                Binding::Virtual("stick_move".to_owned()),
3182            ],
3183        });
3184
3185        assert_eq!(map.axis2("move"), (0.0, 0.0), "nothing is touching it yet");
3186
3187        map.virtual_stick("stick_move", -0.5, 0.25);
3188        assert_eq_axis2(map.action("move").unwrap(), -0.5, 0.25);
3189
3190        // A partial deflection is a partial value, which is the thing four
3191        // buttons could not say — and it survives the tick boundary, because a
3192        // finger resting on the glass reports nothing.
3193        map.begin_tick(TICK);
3194        assert_eq_axis2(map.action("move").unwrap(), -0.5, 0.25);
3195
3196        // A key *and* a stick is one direction, not twice the speed: the two
3197        // sum to (-1.5, 0.25) and the sum is scaled back onto the disc, so the
3198        // heading survives and the magnitude does not exceed one.
3199        map.key_event(KeyCode::KeyA, true);
3200        let (x, y) = map.axis2("move");
3201        let len = x.hypot(y);
3202        assert!((len - 1.0).abs() < 0.001, "magnitude {len} left the disc");
3203        assert!(
3204            (y / x - 0.25 / -1.5).abs() < 0.001,
3205            "the summed heading was not kept, got ({x}, {y})",
3206        );
3207
3208        map.virtual_stick("stick_move", 0.0, 0.0);
3209        map.key_event(KeyCode::KeyA, false);
3210        assert_eq!(map.axis2("move"), (0.0, 0.0), "both let go");
3211    }
3212
3213    /// A stick reported past its own radius is still only all the way over,
3214    /// **and keeps its direction**.
3215    #[test]
3216    fn a_stick_past_its_radius_is_clamped_to_the_disc() {
3217        let mut map = ActionMap::new();
3218        map.declare(ActionDecl {
3219            name: "move".to_owned(),
3220            kind: ActionKind::Axis2,
3221            bindings: vec![Binding::Virtual("stick_move".to_owned())],
3222        });
3223
3224        // 3-4-5: a length of 5, so the clamped value is the exact (0.6, 0.8)
3225        // rather than a number that only looks about right.
3226        map.virtual_stick("stick_move", 3.0, 4.0);
3227        assert_eq_axis2(map.action("move").unwrap(), 0.6, 0.8);
3228        let len = axis2_len(map.action("move").unwrap());
3229        assert!((len - 1.0).abs() < 0.001, "got {len}");
3230    }
3231
3232    /// A control drives a button action with the same edges a key does.
3233    #[test]
3234    fn a_virtual_button_has_the_edges_a_key_has() {
3235        let mut map = ActionMap::new();
3236        map.declare(decl_button(
3237            "pause",
3238            Binding::Virtual("btn_pause".to_owned()),
3239        ));
3240
3241        map.begin_tick(TICK);
3242        map.virtual_button("btn_pause", true);
3243        assert!(map.just_pressed("pause"));
3244        assert!(map.button_held("pause"));
3245
3246        map.begin_tick(TICK);
3247        assert!(
3248            !map.just_pressed("pause"),
3249            "holding the control does not press it again",
3250        );
3251        assert!(map.button_held("pause"));
3252
3253        map.virtual_button("btn_pause", false);
3254        assert!(map.just_released("pause"));
3255        assert!(!map.button_held("pause"));
3256    }
3257
3258    /// Ids name different controls: a second control does not drive the first
3259    /// one's action.
3260    #[test]
3261    fn a_control_drives_only_the_action_that_names_it() {
3262        let mut map = ActionMap::new();
3263        map.declare(decl_button("pause", Binding::Virtual("btn_pause".into())));
3264        map.declare(ActionDecl {
3265            name: "move".to_owned(),
3266            kind: ActionKind::Axis2,
3267            bindings: vec![Binding::Virtual("stick_move".to_owned())],
3268        });
3269
3270        map.virtual_button("btn_other", true);
3271        map.virtual_stick("stick_other", 1.0, 0.0);
3272        assert!(!map.button_held("pause"), "another control's press");
3273        assert_eq!(map.axis2("move"), (0.0, 0.0), "another control's stick");
3274
3275        map.virtual_button("btn_pause", true);
3276        map.virtual_stick("stick_move", 1.0, 0.0);
3277        assert!(map.button_held("pause"));
3278        assert_eq!(map.axis2("move"), (1.0, 0.0));
3279    }
3280
3281    /// A stick fed to a 1-D axis reads nothing, and the docs say so: reducing a
3282    /// vector to one axis is a choice no control makes.
3283    ///
3284    /// The inert case pinned deliberately — a `Virtual` that silently drove
3285    /// `Axis1` off its `x` would be a value nobody chose.
3286    #[test]
3287    fn a_stick_on_a_one_dimensional_axis_reads_nothing() {
3288        let mut map = ActionMap::new();
3289        map.declare(ActionDecl {
3290            name: "throttle".to_owned(),
3291            kind: ActionKind::Axis1,
3292            bindings: vec![Binding::Virtual("stick_move".to_owned())],
3293        });
3294
3295        map.virtual_stick("stick_move", 1.0, 1.0);
3296        assert_eq!(map.axis1("throttle"), 0.0);
3297    }
3298
3299    /// A non-finite deflection is dropped rather than stored, like a `NaN`
3300    /// mouse delta: one would poison every comparison downstream of the axis.
3301    #[test]
3302    fn a_non_finite_deflection_is_dropped() {
3303        let mut map = ActionMap::new();
3304        map.declare(ActionDecl {
3305            name: "move".to_owned(),
3306            kind: ActionKind::Axis2,
3307            bindings: vec![Binding::Virtual("stick_move".to_owned())],
3308        });
3309
3310        map.virtual_stick("stick_move", 0.5, 0.5);
3311        map.virtual_stick("stick_move", f32::NAN, 0.0);
3312        assert_eq_axis2(map.action("move").unwrap(), 0.5, 0.5);
3313        map.virtual_stick("stick_move", 0.0, f32::INFINITY);
3314        assert_eq_axis2(map.action("move").unwrap(), 0.5, 0.5);
3315    }
3316
3317    // -- assert helpers -----------------------------------------------------
3318
3319    fn assert_eq_axis2(v: &ActionValue, x: f32, y: f32) {
3320        match v {
3321            ActionValue::Axis2(a) => {
3322                assert!(
3323                    (a.x - x).abs() < 0.001 && (a.y - y).abs() < 0.001,
3324                    "expected Axis2({x}, {y}), got Axis2({}, {})",
3325                    a.x,
3326                    a.y
3327                );
3328            }
3329            other => panic!("expected Axis2, got {other:?}"),
3330        }
3331    }
3332
3333    fn axis2_len(v: &ActionValue) -> f32 {
3334        match v {
3335            ActionValue::Axis2(a) => (a.x * a.x + a.y * a.y).sqrt(),
3336            other => panic!("expected Axis2, got {other:?}"),
3337        }
3338    }
3339
3340    fn assert_eq_axis1(v: &ActionValue, val: f32) {
3341        match v {
3342            ActionValue::Axis1(a) => {
3343                assert!(
3344                    (a.value - val).abs() < 0.001,
3345                    "expected Axis1({val}), got Axis1({})",
3346                    a.value
3347                );
3348            }
3349            other => panic!("expected Axis1, got {other:?}"),
3350        }
3351    }
3352}