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}