Skip to main content

crcbl_input/
context.rs

1//! The context stack: which active context an input reaches.
2//!
3//! # The stack
4//!
5//! Every action is in one context. [`GAMEPLAY_CONTEXT`] is the base: it is
6//! always on the stack, is never pushed or popped, and is where
7//! [`ActionMap::declare`] puts an action. Any other context is on the stack
8//! only between [`ActionMap::push_context`] and [`ActionMap::pop_context`], and
9//! the stack is a stack — a pop names the context it expects on top and is
10//! refused otherwise, so two owners pushing and popping out of order fail
11//! loudly instead of popping each other's context. An action in a context that
12//! is not on the stack is idle, as a disabled one is.
13//!
14//! [`GLOBAL_CONTEXT`] sits above the stack: always active, never pushed or
15//! popped, and routed before every pushed context — the place for a toggle
16//! that must work over any screen.
17//!
18//! # Consumption
19//!
20//! **An input belongs to the topmost active context that binds it**, and only
21//! that context's actions read it; an input no active context binds reaches
22//! nothing, and one the top context does not bind falls through to the first
23//! context beneath that does — unless the context between was pushed with
24//! [`ActionMap::push_context_modal`], which stops every input it does not bind
25//! but the pointer's position and motion. "Binds" is by binding, not by enabled flag: a
26//! disabled action is silenced, and its keys stay its context's until
27//! [`ActionMap::rebind`] moves them. Inputs are keys (the keys a
28//! [`Binding::Chord`] owns, not its modifier), pointer buttons (the button a
29//! [`Binding::ButtonChord`] owns, not its modifier), on-screen
30//! controls by id, the pointer's position, motion and wheel each as one input
31//! (the wheel taken by a [`Binding::ScrollChord`] as by a
32//! [`Binding::MouseScroll`], and not its held key), and pad buttons, sticks
33//! and triggers — every pad's South is one input, as every pad drives every
34//! pad binding.
35//!
36//! # A pad chord that outranks a context above
37//!
38//! A chord takes its button only in the context that owns the button, so a
39//! [`Binding::PadChord`] beneath a context binding its button plainly reads
40//! nothing: LB+Select rebound to a gameplay reload under a global map on
41//! Select still opens the map. **An action marked with
42//! [`ActionMap::set_pad_chords_outrank`] lifts that for its own pad chords**:
43//! while a chord's modifier is held, the chord takes its button from every
44//! context above it, and the plain bindings there read the button as up — the
45//! chord's rule within one context, stretched across the contexts between.
46//! Nothing else changes. A context that binds the same chord itself keeps it,
47//! a chord beneath a modal context stays blocked as every other input does,
48//! the button without the modifier still goes to its owner, and an action not
49//! marked keeps the ordinary precedence.
50//!
51//! The modifier hands a held button between two contexts, so the handover is
52//! a change of owner (below): **pressing or letting go of the modifier while
53//! the button is held withholds the button from its new reader until it is
54//! released.** Letting go of LB before Select releases the reload without
55//! opening the map, and LB pressed over a held Select releases the map
56//! without reloading.
57//!
58//! # A key held while the stack changes
59//!
60//! **A held input whose owner a push or a pop changes is withheld from its new
61//! owner until it is released.** Its old owner sees it come up — a walk stops
62//! when a menu opens over it — and its new owner does not see it go down: the
63//! Enter that accepted a menu item and popped the `ui` context does not then
64//! fire the game's Enter binding, and the Escape that opened a menu does not
65//! reach the `ui_back` that would close it. Unreal's Enhanced Input makes the
66//! same call with `FModifyContextOptions::bIgnoreAllPressedKeysUntilRelease`,
67//! on by default; this is narrower, withholding only inputs whose owner
68//! actually changed, so a key the new context does not bind keeps working
69//! without a lift. Levels are not withheld — a pointer position or a stick
70//! deflection is a place, not a press — only routed; a pad button is a press
71//! and is withheld like a key, but a pad trigger is a level even when a button
72//! action reads it. A rebind moves ownership
73//! without withholding, because a rebind already resolves against the keys
74//! held at the time.
75//!
76//! # Withholding on request
77//!
78//! [`ActionMap::suppress_held`] withholds **every** held input, whoever owns
79//! it, and [`ActionMap::suppress_held_action`] every held input one action's
80//! bindings read — the walk held into a revival, a raid resolving or a menu
81//! that is not a context. It is the same withholding a stack change does, and
82//! it too cancels the patterns in flight, but it is wider in two ways:
83//!
84//! - **It withholds the input, not the action.** Every action reading a
85//!   withheld input reads it as up, not only the one named; an action sharing
86//!   none of them is untouched.
87//! - **Levels are withheld too**, because the request is "nothing held moves
88//!   the player": a pad stick, a pad trigger and an on-screen stick.
89//!
90//! What lifts it — what "pressed again" means — is per input:
91//!
92//! - **A key, pointer button, on-screen button or pad button**: released, as
93//!   for a stack change. Its next press reads. A [`Binding::Wasd`] or
94//!   [`Binding::KeyAxis`] withholds per key, so a key still held reads zero
95//!   while a key pressed afresh moves the axis; a [`Binding::Chord`] is
96//!   withheld through its key, and reads again when the key is pressed again
97//!   with the modifier still down, since the modifier is read raw.
98//! - **A pad stick**: back at rest on every pad — inside the dead zone of every
99//!   [`Binding::PadStick`] on it, or exactly centred if none reads it. Once
100//!   lifted, deflecting it reads at once: returning to rest is its release.
101//! - **A pad trigger**: back at or below the threshold of every
102//!   [`Binding::PadTrigger`] on it, on every pad, or fully out if none reads it.
103//! - **An on-screen stick**: reporting exactly `(0.0, 0.0)`, which is what a
104//!   stick nobody touches reports.
105//!
106//! The pointer's position is a place and is never withheld, and its motion and
107//! wheel are deltas that nothing holds.
108
109use std::collections::{HashMap, HashSet};
110
111use crcbl_core::input::{KeyCode, PointerButton};
112
113use super::{
114    ActionMap, ActionMapError, Binding, HeldKeys, Modifier, PadButton, PadButtons, Stick, Trigger,
115};
116use crate::trace::TracedInput;
117
118/// The base context: always active, and where [`ActionMap::declare`] puts an
119/// action.
120pub const GAMEPLAY_CONTEXT: &str = "gameplay";
121
122/// The context above every other: always active, never pushed or popped, and
123/// routed ahead of the whole stack, a modal context included — where a toggle
124/// that must work over any screen goes (a debug camera, the map key, the key
125/// that opens and closes an inventory). Declare into it with
126/// [`ActionMap::declare_in`]. It is not on the stack, so
127/// [`ActionMap::active_contexts`] does not list it.
128pub const GLOBAL_CONTEXT: &str = "global";
129
130/// [`GLOBAL_CONTEXT`]'s index in [`ActionMap::contexts`]: registered second, at
131/// construction.
132const GLOBAL_INDEX: usize = 1;
133
134/// The owner of each input some active context binds, as an index into
135/// [`ActionMap::contexts`].
136#[derive(Debug, Default, Clone, PartialEq, Eq)]
137pub(crate) struct Routes {
138    keys: HashMap<KeyCode, usize>,
139    /// For each key, the modifiers of the chords its **owner** binds on it:
140    /// what shadows that owner's plain bindings on the key.
141    chords: HashMap<KeyCode, Vec<Modifier>>,
142    /// For each pad button, the modifiers of the pad chords its **owner**
143    /// binds on it — [`Self::chords`] for the pad.
144    pad_chords: HashMap<PadButton, Vec<PadButton>>,
145    /// For each pad button, the outranking pad chords on it from contexts
146    /// beneath its owner, as `(modifier, context)`, topmost context first —
147    /// see [`ActionMap::set_pad_chords_outrank`].
148    outranking_pad_chords: HashMap<PadButton, Vec<(PadButton, usize)>>,
149    /// For each pointer button, the modifiers of the button chords its
150    /// **owner** binds on it — [`Self::chords`] for the mouse.
151    button_chords: HashMap<PointerButton, Vec<Modifier>>,
152    buttons: HashMap<PointerButton, usize>,
153    controls: HashMap<String, usize>,
154    motion: Option<usize>,
155    scroll: Option<usize>,
156    /// The held keys of the scroll chords the wheel's **owner** binds: what
157    /// shadows that owner's plain wheel bindings.
158    scroll_chords: Vec<KeyCode>,
159    pub(crate) pointer: Option<usize>,
160    pad_buttons: HashMap<PadButton, usize>,
161    pad_sticks: HashMap<Stick, usize>,
162    pad_triggers: HashMap<Trigger, usize>,
163}
164
165/// Held inputs withheld from their owner until released — see the module docs.
166#[derive(Debug, Default, Clone)]
167pub(crate) struct Suppressed {
168    pub(crate) keys: HashSet<KeyCode>,
169    pub(crate) buttons: HashSet<PointerButton>,
170    pub(crate) controls: HashSet<String>,
171    /// Pad buttons withheld — by a stack change, or by
172    /// [`ActionMap::release_gamepads`].
173    pub(crate) pad_buttons: PadButtons,
174    /// The levels only [`ActionMap::suppress_held`] and
175    /// [`ActionMap::suppress_held_action`] withhold, each until it rests.
176    pub(crate) control_sticks: HashSet<String>,
177    pub(crate) pad_sticks: HashSet<Stick>,
178    pub(crate) pad_triggers: HashSet<Trigger>,
179}
180
181impl Suppressed {
182    /// Withhold everything `other` does as well.
183    fn extend(&mut self, other: Self) {
184        self.keys.extend(other.keys);
185        self.buttons.extend(other.buttons);
186        self.controls.extend(other.controls);
187        self.pad_buttons = self.pad_buttons.union(other.pad_buttons);
188        self.control_sticks.extend(other.control_sticks);
189        self.pad_sticks.extend(other.pad_sticks);
190        self.pad_triggers.extend(other.pad_triggers);
191    }
192
193    /// Whether `binding` reads any input this withholds.
194    fn withholds(&self, binding: &Binding) -> bool {
195        let mut key = false;
196        binding.visit_keys(|owned| key |= self.keys.contains(&owned));
197        key || match binding {
198            Binding::ScrollChord { held } => self.keys.contains(held),
199            Binding::MouseButton(button) | Binding::ButtonChord { button, .. } => {
200                self.buttons.contains(button)
201            }
202            Binding::Virtual(id) => {
203                self.controls.contains(id.as_str()) || self.control_sticks.contains(id.as_str())
204            }
205            Binding::PadButton(button) | Binding::PadChord { button, .. } => {
206                self.pad_buttons.contains(*button)
207            }
208            Binding::PadDpad => PadButton::DPAD
209                .iter()
210                .any(|&button| self.pad_buttons.contains(button)),
211            Binding::PadStick { stick, .. } => self.pad_sticks.contains(stick),
212            Binding::PadTrigger { trigger, .. } => self.pad_triggers.contains(trigger),
213            Binding::Key(_)
214            | Binding::KeyAxis { .. }
215            | Binding::Chord { .. }
216            | Binding::Wasd { .. }
217            | Binding::MouseMotion
218            | Binding::MouseScroll
219            | Binding::PointerPosition { .. } => false,
220        }
221    }
222
223    /// Whether this withholds `input` itself — the trace's
224    /// [`Outcome::Withheld`](crate::trace::Outcome::Withheld). The wheel is a
225    /// delta, and nothing withholds it.
226    pub(crate) fn holds(&self, input: &TracedInput) -> bool {
227        match input {
228            TracedInput::Key(key) => self.keys.contains(key),
229            TracedInput::MouseButton(button) => self.buttons.contains(button),
230            TracedInput::Wheel => false,
231            TracedInput::Control(id) => self.controls.contains(id.as_str()),
232            TracedInput::ControlStick(id) => self.control_sticks.contains(id.as_str()),
233            TracedInput::PadButton(button) => self.pad_buttons.contains(*button),
234            TracedInput::PadStick(stick) => self.pad_sticks.contains(stick),
235            TracedInput::PadTrigger(trigger) => self.pad_triggers.contains(trigger),
236        }
237    }
238}
239
240/// What one context can see of the raw input.
241pub(crate) struct View<'a> {
242    pub(crate) context: usize,
243    pub(crate) routes: &'a Routes,
244    pub(crate) suppressed: &'a Suppressed,
245    pub(crate) held_keys: &'a HeldKeys,
246    pub(crate) held_buttons: &'a HashSet<PointerButton>,
247    pub(crate) held_controls: &'a HashSet<String>,
248    /// Every button some pad holds.
249    pub(crate) held_pad_buttons: PadButtons,
250}
251
252impl View<'_> {
253    /// The key is held, this context owns it, and it is not withheld.
254    fn owns_held(&self, key: KeyCode) -> bool {
255        self.held_keys.contains_key(&key)
256            && !self.suppressed.keys.contains(&key)
257            && self.routes.keys.get(&key) == Some(&self.context)
258    }
259
260    /// A plain binding's read of `key`: down unless a chord on it is satisfied.
261    pub(crate) fn key(&self, key: KeyCode) -> bool {
262        self.owns_held(key) && !self.any_held(self.routes.chords.get(&key))
263    }
264
265    /// Whether any of `modifiers` — the modifiers of the chords an input's
266    /// owner binds on it — is held, which shadows that owner's plain bindings
267    /// on the input. What [`Self::key`] and [`Self::button`] both ask, so a
268    /// key chord and a button chord hand their input over on one rule.
269    fn any_held(&self, modifiers: Option<&Vec<Modifier>>) -> bool {
270        modifiers.is_some_and(|modifiers| modifiers.iter().any(|m| m.held(self.held_keys)))
271    }
272
273    /// A [`Binding::Chord`]'s read: its key, and its modifier read raw.
274    pub(crate) fn chord(&self, modifier: Modifier, key: KeyCode) -> bool {
275        self.owns_held(key) && modifier.held(self.held_keys)
276    }
277
278    /// The pointer button is held, this context owns it, and it is not
279    /// withheld.
280    fn owns_held_button(&self, button: PointerButton) -> bool {
281        self.held_buttons.contains(&button)
282            && !self.suppressed.buttons.contains(&button)
283            && self.routes.buttons.get(&button) == Some(&self.context)
284    }
285
286    /// A plain [`Binding::MouseButton`]'s read: down unless a button chord on
287    /// it is satisfied.
288    pub(crate) fn button(&self, button: PointerButton) -> bool {
289        self.owns_held_button(button) && !self.any_held(self.routes.button_chords.get(&button))
290    }
291
292    /// A [`Binding::ButtonChord`]'s read: its button, and its modifier read
293    /// raw.
294    pub(crate) fn button_chord(&self, modifier: Modifier, button: PointerButton) -> bool {
295        self.owns_held_button(button) && modifier.held(self.held_keys)
296    }
297
298    pub(crate) fn control(&self, id: &str) -> bool {
299        self.held_controls.contains(id)
300            && !self.suppressed.controls.contains(id)
301            && self.owns_control(id)
302    }
303
304    /// A stick's deflection is a level: a stack change never withholds it, and
305    /// only a suppress does.
306    pub(crate) fn stick(&self, id: &str) -> bool {
307        self.owns_control(id) && !self.suppressed.control_sticks.contains(id)
308    }
309
310    fn owns_control(&self, id: &str) -> bool {
311        self.routes.controls.get(id) == Some(&self.context)
312    }
313
314    pub(crate) fn motion(&self) -> bool {
315        self.routes.motion == Some(self.context)
316    }
317
318    /// A plain [`Binding::MouseScroll`]'s read: this context owns the wheel,
319    /// and none of its scroll chords' keys is down.
320    pub(crate) fn scroll(&self) -> bool {
321        self.routes.scroll == Some(self.context) && self.wheel_key().is_none()
322    }
323
324    /// A [`Binding::ScrollChord`]'s read: this context owns the wheel, and
325    /// `held` is the most recently pressed of its scroll chords' keys down.
326    pub(crate) fn scroll_chord(&self, held: KeyCode) -> bool {
327        self.routes.scroll == Some(self.context) && self.wheel_key() == Some(held)
328    }
329
330    /// The most recently pressed scroll-chord key that is down and not
331    /// withheld, in the context that owns the wheel.
332    fn wheel_key(&self) -> Option<KeyCode> {
333        self.routes
334            .scroll_chords
335            .iter()
336            .filter(|key| !self.suppressed.keys.contains(key))
337            .filter_map(|&key| self.held_keys.get(&key).map(|&serial| (serial, key)))
338            .max()
339            .map(|(_, key)| key)
340    }
341
342    pub(crate) fn pointer(&self) -> bool {
343        self.routes.pointer == Some(self.context)
344    }
345
346    /// The pad button is held, this context reads it with the modifiers held
347    /// now ([`Routes::pad_reader`]), and it is not withheld.
348    fn owns_held_pad_button(&self, button: PadButton) -> bool {
349        self.held_pad_buttons.contains(button)
350            && !self.suppressed.pad_buttons.contains(button)
351            && self.routes.pad_reader(button, self.held_pad_buttons) == Some(self.context)
352    }
353
354    /// A plain pad binding's read of `button`: down unless a pad chord on it
355    /// is satisfied.
356    pub(crate) fn pad_button(&self, button: PadButton) -> bool {
357        self.owns_held_pad_button(button)
358            && !self
359                .routes
360                .pad_chords
361                .get(&button)
362                .is_some_and(|modifiers| {
363                    modifiers
364                        .iter()
365                        .any(|&modifier| self.held_pad_buttons.contains(modifier))
366                })
367    }
368
369    /// A [`Binding::PadChord`]'s read: its button, and its modifier read raw.
370    /// Beneath the button's owner, only an outranking chord reads it.
371    pub(crate) fn pad_chord(&self, modifier: PadButton, button: PadButton) -> bool {
372        self.owns_held_pad_button(button)
373            && self.held_pad_buttons.contains(modifier)
374            && (self.routes.pad_buttons.get(&button) == Some(&self.context)
375                || self.routes.outranks(button, modifier, self.context))
376    }
377
378    /// A stick is a level: a stack change never withholds it, and only a
379    /// suppress does.
380    pub(crate) fn pad_stick(&self, stick: Stick) -> bool {
381        self.routes.pad_sticks.get(&stick) == Some(&self.context)
382            && !self.suppressed.pad_sticks.contains(&stick)
383    }
384
385    /// A trigger is a level: a stack change never withholds it, and only a
386    /// suppress does.
387    pub(crate) fn trigger(&self, trigger: Trigger) -> bool {
388        self.routes.pad_triggers.get(&trigger) == Some(&self.context)
389            && !self.suppressed.pad_triggers.contains(&trigger)
390    }
391}
392
393impl Routes {
394    /// Whether `key` is the held key of a scroll chord the wheel's owner binds.
395    pub(crate) fn is_scroll_chord_key(&self, key: KeyCode) -> bool {
396        self.scroll_chords.contains(&key)
397    }
398
399    /// The context that reads `input` while `held` is every pad button down,
400    /// if an active one binds it: the owner, or for a pad button the context
401    /// [`Self::pad_reader`] hands it to.
402    pub(crate) fn owner(&self, input: &TracedInput, held: PadButtons) -> Option<usize> {
403        match input {
404            TracedInput::Key(key) => self.keys.get(key).copied(),
405            TracedInput::MouseButton(button) => self.buttons.get(button).copied(),
406            TracedInput::Wheel => self.scroll,
407            TracedInput::Control(id) | TracedInput::ControlStick(id) => {
408                self.controls.get(id.as_str()).copied()
409            }
410            TracedInput::PadButton(button) => self.pad_reader(*button, held),
411            TracedInput::PadStick(stick) => self.pad_sticks.get(stick).copied(),
412            TracedInput::PadTrigger(trigger) => self.pad_triggers.get(trigger).copied(),
413        }
414    }
415
416    /// The context that reads `button` while `held` is down: the topmost one
417    /// with an outranking chord on it whose modifier is held, or else its
418    /// owner. What a change of owner compares for a pad button, so the
419    /// modifier of an outranking chord hands a held button over the way a
420    /// push does.
421    pub(crate) fn pad_reader(&self, button: PadButton, held: PadButtons) -> Option<usize> {
422        self.outranking_pad_chords
423            .get(&button)
424            .and_then(|chords| {
425                chords
426                    .iter()
427                    .find(|&&(modifier, _)| held.contains(modifier))
428            })
429            .map(|&(_, context)| context)
430            .or_else(|| self.pad_buttons.get(&button).copied())
431    }
432
433    /// Whether `context` has an outranking chord on `button` held by
434    /// `modifier`.
435    fn outranks(&self, button: PadButton, modifier: PadButton, context: usize) -> bool {
436        self.outranking_pad_chords
437            .get(&button)
438            .is_some_and(|chords| chords.contains(&(modifier, context)))
439    }
440
441    /// Owners for the stack as it stands: each context from the top down
442    /// claims what nothing above it already has.
443    fn build(map: &ActionMap) -> Self {
444        let mut routes = Self::default();
445        // Below a modal context only the pointer's position and motion still
446        // route: a screen reads the cursor, and nothing else passes it.
447        let mut below_modal = false;
448        // Every pad chord a context above binds, outranking or not: a context
449        // that binds a chord itself keeps it from an outranking one beneath.
450        let mut pad_chords_above = HashSet::new();
451        for context in map.route_order() {
452            let bindings = || {
453                map.slots
454                    .iter()
455                    .filter(move |slot| slot.context == context)
456                    .flat_map(|slot| slot.decl.bindings.iter())
457            };
458            if below_modal {
459                for binding in bindings() {
460                    match binding {
461                        Binding::MouseMotion => {
462                            routes.motion.get_or_insert(context);
463                        }
464                        Binding::PointerPosition { .. } => {
465                            routes.pointer.get_or_insert(context);
466                        }
467                        _ => {}
468                    }
469                }
470                continue;
471            }
472            for binding in bindings() {
473                binding.visit_keys(|key| {
474                    routes.keys.entry(key).or_insert(context);
475                });
476                match binding {
477                    Binding::MouseButton(button) | Binding::ButtonChord { button, .. } => {
478                        routes.buttons.entry(*button).or_insert(context);
479                    }
480                    Binding::Virtual(id) => {
481                        if !routes.controls.contains_key(id.as_str()) {
482                            routes.controls.insert(id.clone(), context);
483                        }
484                    }
485                    Binding::MouseMotion => {
486                        routes.motion.get_or_insert(context);
487                    }
488                    Binding::MouseScroll | Binding::ScrollChord { .. } => {
489                        routes.scroll.get_or_insert(context);
490                    }
491                    Binding::PointerPosition { .. } => {
492                        routes.pointer.get_or_insert(context);
493                    }
494                    Binding::PadButton(button) | Binding::PadChord { button, .. } => {
495                        routes.pad_buttons.entry(*button).or_insert(context);
496                    }
497                    Binding::PadDpad => {
498                        for button in PadButton::DPAD {
499                            routes.pad_buttons.entry(button).or_insert(context);
500                        }
501                    }
502                    Binding::PadStick { stick, .. } => {
503                        routes.pad_sticks.entry(*stick).or_insert(context);
504                    }
505                    Binding::PadTrigger { trigger, .. } => {
506                        routes.pad_triggers.entry(*trigger).or_insert(context);
507                    }
508                    Binding::Key(_)
509                    | Binding::KeyAxis { .. }
510                    | Binding::Chord { .. }
511                    | Binding::Wasd { .. } => {}
512                }
513            }
514            // After this context's keys and wheel are claimed, so a chord
515            // registers only where its context is the one reading the key, and
516            // a scroll chord only where its context is the one reading the
517            // wheel. An outranking pad chord whose button a context above owns
518            // registers beneath that owner instead, unless a context above
519            // binds the same chord.
520            //
521            // A scroll chord competes for the wheel only while its action is
522            // enabled: a newer key whose action cannot use the wheel right now
523            // must not take it from an older one that can, nor silence the
524            // plain wheel. The wheel itself stays this context's either way.
525            let slots = map.slots.iter().filter(|slot| slot.context == context);
526            for slot in slots {
527                for binding in &slot.decl.bindings {
528                    match binding {
529                        Binding::Chord { modifier, key }
530                            if routes.keys.get(key) == Some(&context) =>
531                        {
532                            add_chord(&mut routes.chords, *key, *modifier);
533                        }
534                        Binding::ButtonChord { modifier, button }
535                            if routes.buttons.get(button) == Some(&context) =>
536                        {
537                            add_chord(&mut routes.button_chords, *button, *modifier);
538                        }
539                        Binding::PadChord { modifier, button }
540                            if routes.pad_buttons.get(button) == Some(&context) =>
541                        {
542                            add_chord(&mut routes.pad_chords, *button, *modifier);
543                        }
544                        Binding::PadChord { modifier, button }
545                            if slot.pad_chords_outrank
546                                && !pad_chords_above.contains(&(*modifier, *button)) =>
547                        {
548                            add_chord(
549                                &mut routes.outranking_pad_chords,
550                                *button,
551                                (*modifier, context),
552                            );
553                        }
554                        Binding::ScrollChord { held }
555                            if slot.enabled
556                                && routes.scroll == Some(context)
557                                && !routes.scroll_chords.contains(held) =>
558                        {
559                            routes.scroll_chords.push(*held);
560                        }
561                        _ => {}
562                    }
563                }
564            }
565            for binding in bindings() {
566                if let Binding::PadChord { modifier, button } = binding {
567                    pad_chords_above.insert((*modifier, *button));
568                }
569            }
570            below_modal = map.modal.contains(&context);
571        }
572        routes
573    }
574}
575
576/// Record that `input`'s owner binds a chord on it held by `modifier`, once
577/// per modifier: the table [`View`] reads to shadow that owner's plain
578/// bindings on the input, for a key, a mouse button and a pad button alike.
579/// [`Routes::outranking_pad_chords`] keeps each chord's context beside its
580/// modifier, so a chord is recorded once per modifier and context there.
581fn add_chord<I: Eq + std::hash::Hash, M: PartialEq>(
582    chords: &mut HashMap<I, Vec<M>>,
583    input: I,
584    modifier: M,
585) {
586    let modifiers = chords.entry(input).or_default();
587    if !modifiers.contains(&modifier) {
588        modifiers.push(modifier);
589    }
590}
591
592impl ActionMap {
593    /// Push a declared context on top of the stack.
594    ///
595    /// Every held input the context takes from a context beneath is withheld
596    /// until released — see the module docs — and every action re-resolves, so
597    /// what the context beneath lost reads as released on this tick.
598    ///
599    /// # Errors
600    /// [`ActionMapError::UnknownContext`] if no action was ever declared in it,
601    /// [`ActionMapError::ContextAlreadyActive`] if it is already on the stack.
602    pub fn push_context(&mut self, context: &str) -> Result<(), ActionMapError> {
603        self.push(context, false)
604    }
605
606    /// Push a declared context on top of the stack as **modal**: every key,
607    /// pointer button, on-screen control, wheel and pad input it does not bind
608    /// reaches nothing beneath it, where [`ActionMap::push_context`] lets it
609    /// fall through. The pointer's position and motion still route beneath,
610    /// and [`GLOBAL_CONTEXT`] stays above it.
611    ///
612    /// Everything else is [`ActionMap::push_context`]'s: an input held when it
613    /// opens loses its owner and is withheld until released, so a walk held
614    /// into an inventory stops, and does not resume when it closes until the
615    /// key is pressed again.
616    ///
617    /// # Errors
618    /// As [`ActionMap::push_context`].
619    pub fn push_context_modal(&mut self, context: &str) -> Result<(), ActionMapError> {
620        self.push(context, true)
621    }
622
623    fn push(&mut self, context: &str, modal: bool) -> Result<(), ActionMapError> {
624        let index = self.context_index(context)?;
625        if self.is_active(index) {
626            return Err(ActionMapError::ContextAlreadyActive(context.to_owned()));
627        }
628        self.stack.push(index);
629        if modal {
630            self.modal.push(index);
631        }
632        self.restack();
633        Ok(())
634    }
635
636    /// Pop the topmost pushed context, which must be `context`.
637    ///
638    /// Its actions go idle without a release edge, as a disabled action does,
639    /// and inputs it held fall through to the contexts beneath only once they
640    /// have been released.
641    ///
642    /// # Errors
643    /// [`ActionMapError::UnknownContext`] if it was never declared,
644    /// [`ActionMapError::ContextNotOnTop`] if it is not the topmost pushed
645    /// context — including [`GAMEPLAY_CONTEXT`], which is never pushed.
646    pub fn pop_context(&mut self, context: &str) -> Result<(), ActionMapError> {
647        let index = self.context_index(context)?;
648        if self.stack.len() == 1 || self.stack.last() != Some(&index) {
649            return Err(ActionMapError::ContextNotOnTop(context.to_owned()));
650        }
651        self.stack.pop();
652        self.modal.retain(|&modal| modal != index);
653        self.restack();
654        Ok(())
655    }
656
657    /// The stack's contexts' names, bottom first: [`GAMEPLAY_CONTEXT`], then
658    /// each pushed context in the order it was pushed. [`GLOBAL_CONTEXT`] is
659    /// active too, above them all, and is not listed.
660    pub fn active_contexts(&self) -> impl Iterator<Item = &str> {
661        self.stack
662            .iter()
663            .map(|&index| self.contexts[index].as_str())
664    }
665
666    /// Whether `context` is on the stack. `false` for one never declared.
667    #[must_use]
668    pub fn is_context_active(&self, context: &str) -> bool {
669        self.contexts
670            .iter()
671            .position(|name| name == context)
672            .is_some_and(|index| self.is_active(index))
673    }
674
675    /// Every active context in the order routing claims inputs for them:
676    /// [`GLOBAL_CONTEXT`], then the stack from the top down.
677    fn route_order(&self) -> impl Iterator<Item = usize> + '_ {
678        std::iter::once(GLOBAL_INDEX).chain(self.stack.iter().rev().copied())
679    }
680
681    /// The modal context that stops an input from reaching a context beneath
682    /// it with a binding `claims` accepts, if one does — what routing does to
683    /// everything a modal context does not bind but the pointer's position and
684    /// motion, which the trace never asks about.
685    pub(crate) fn blocking_modal(&self, claims: impl Fn(&Binding) -> bool) -> Option<usize> {
686        let mut modal = None;
687        for context in self.route_order() {
688            let binds = || {
689                self.slots
690                    .iter()
691                    .filter(|slot| slot.context == context)
692                    .any(|slot| slot.decl.bindings.iter().any(&claims))
693            };
694            if modal.is_some() && binds() {
695                return modal;
696            }
697            if modal.is_none() && self.modal.contains(&context) {
698                modal = Some(context);
699            }
700        }
701        None
702    }
703
704    /// Whether `context` was pushed with [`ActionMap::push_context_modal`].
705    /// `false` for one not on the stack, or never declared.
706    #[must_use]
707    pub fn is_context_modal(&self, context: &str) -> bool {
708        self.contexts
709            .iter()
710            .position(|name| name == context)
711            .is_some_and(|index| self.modal.contains(&index))
712    }
713
714    /// Whether the context at `index` routes input: [`GLOBAL_CONTEXT`] always,
715    /// any other while it is on the stack.
716    fn is_active(&self, index: usize) -> bool {
717        index == GLOBAL_INDEX || self.stack.contains(&index)
718    }
719
720    /// The context an action was declared in, or `None` if it is not declared.
721    #[must_use]
722    pub fn context_of(&self, name: &str) -> Option<&str> {
723        let &idx = self.name_to_idx.get(name)?;
724        Some(self.contexts[self.slots[idx].context].as_str())
725    }
726
727    /// Let an action's [`Binding::PadChord`]s take their button from the
728    /// contexts above it while their modifier is held, or put them back under
729    /// the ordinary precedence — see the module docs.
730    ///
731    /// For a pad chord a player can give a gameplay action when the plain
732    /// button is a global one: LB+Select rebound from a global free look to a
733    /// gameplay reload reloads, and Select alone still opens the global map.
734    /// The mark is the action's, not a binding's, so it holds whatever the
735    /// action is rebound to — [`ActionMap::rebind`] and
736    /// [`ActionMap::apply_overrides`] leave it alone, restoring the defaults
737    /// included — and a binding asset declares it with
738    /// `pad_chords_outrank: true` (`binding_asset.rs`). Off for every action
739    /// until set.
740    ///
741    /// Every action re-resolves, as after a rebind, and nothing is withheld.
742    ///
743    /// # Errors
744    /// [`ActionMapError::UnknownAction`] if nothing with that name is declared.
745    pub fn set_pad_chords_outrank(
746        &mut self,
747        name: &str,
748        outrank: bool,
749    ) -> Result<(), ActionMapError> {
750        let Some(&idx) = self.name_to_idx.get(name) else {
751            return Err(ActionMapError::UnknownAction(name.to_owned()));
752        };
753        if self.slots[idx].pad_chords_outrank != outrank {
754            self.slots[idx].pad_chords_outrank = outrank;
755            self.reroute();
756        }
757        Ok(())
758    }
759
760    /// Whether an action's pad chords outrank the contexts above it — see
761    /// [`ActionMap::set_pad_chords_outrank`]. `None` if it is not declared.
762    #[must_use]
763    pub fn pad_chords_outrank(&self, name: &str) -> Option<bool> {
764        let &idx = self.name_to_idx.get(name)?;
765        Some(self.slots[idx].pad_chords_outrank)
766    }
767
768    fn context_index(&self, context: &str) -> Result<usize, ActionMapError> {
769        self.contexts
770            .iter()
771            .position(|name| name == context)
772            .ok_or_else(|| ActionMapError::UnknownContext(context.to_owned()))
773    }
774
775    /// Whether the slot at `idx` reacts to input: enabled, in an active context.
776    pub(crate) fn is_live(&self, idx: usize) -> bool {
777        let slot = &self.slots[idx];
778        slot.enabled && self.is_active(slot.context)
779    }
780
781    /// Rebuild the routes after a binding changed, and re-resolve every live
782    /// action against them. Nothing is withheld.
783    pub(crate) fn reroute(&mut self) {
784        self.routes = Routes::build(self);
785        self.resolve_live();
786    }
787
788    /// Re-resolve every live action.
789    fn resolve_live(&mut self) {
790        for idx in 0..self.slots.len() {
791            if self.is_live(idx) {
792                self.resolve_one(idx);
793            }
794        }
795    }
796
797    /// Withhold every input held right now, whoever owns it, until it is
798    /// released — or for a level, until it rests — and cancel every pattern in
799    /// flight. See the module docs for what lifts each input.
800    ///
801    /// For a state change that is not a context: movement keys held when a
802    /// revival starts or a menu opens stop moving the player, and move it again
803    /// only once pressed again. Every action re-resolves, so one that was down
804    /// reads as released on this tick.
805    pub fn suppress_held(&mut self) {
806        let withheld = Suppressed {
807            keys: self.held_keys.keys().copied().collect(),
808            buttons: self.held_buttons.clone(),
809            controls: self.held_controls.clone(),
810            pad_buttons: self.held_pad_buttons,
811            control_sticks: self
812                .control_sticks
813                .iter()
814                .filter(|(_, deflection)| **deflection != (0.0, 0.0))
815                .map(|(id, _)| id.clone())
816                .collect(),
817            pad_sticks: [Stick::Left, Stick::Right]
818                .into_iter()
819                .filter(|&stick| !self.stick_rests(stick))
820                .collect(),
821            pad_triggers: [Trigger::Left, Trigger::Right]
822                .into_iter()
823                .filter(|&trigger| !self.trigger_rests(trigger))
824                .collect(),
825        };
826        for slot in &mut self.slots {
827            slot.patterns.cancel();
828        }
829        self.withhold(withheld);
830    }
831
832    /// Withhold every input held right now that one action's bindings read,
833    /// as [`ActionMap::suppress_held`] does for all of them, and cancel the
834    /// patterns in flight on that action and on every action reading one of
835    /// those inputs.
836    ///
837    /// The input is withheld, not the action: another action bound to the same
838    /// key reads it as up too — see the module docs. A [`Binding::Chord`]
839    /// withholds its key and not its modifier, and a [`Binding::ScrollChord`]
840    /// its held key.
841    ///
842    /// # Errors
843    /// [`ActionMapError::UnknownAction`] if nothing with that name is declared.
844    pub fn suppress_held_action(&mut self, name: &str) -> Result<(), ActionMapError> {
845        let Some(&idx) = self.name_to_idx.get(name) else {
846            return Err(ActionMapError::UnknownAction(name.to_owned()));
847        };
848        let mut withheld = Suppressed::default();
849        for binding in &self.slots[idx].decl.bindings {
850            self.held_by(binding, &mut withheld);
851        }
852        self.slots[idx].patterns.cancel();
853        for slot in &mut self.slots {
854            if slot.decl.bindings.iter().any(|b| withheld.withholds(b)) {
855                slot.patterns.cancel();
856            }
857        }
858        self.withhold(withheld);
859        Ok(())
860    }
861
862    /// Add to `withheld` every input `binding` reads that is held right now:
863    /// a key down, a stick or trigger off rest, an on-screen stick off centre.
864    fn held_by(&self, binding: &Binding, withheld: &mut Suppressed) {
865        binding.visit_keys(|key| {
866            if self.held_keys.contains_key(&key) {
867                withheld.keys.insert(key);
868            }
869        });
870        match binding {
871            Binding::ScrollChord { held } if self.held_keys.contains_key(held) => {
872                withheld.keys.insert(*held);
873            }
874            Binding::MouseButton(button) | Binding::ButtonChord { button, .. }
875                if self.held_buttons.contains(button) =>
876            {
877                withheld.buttons.insert(*button);
878            }
879            Binding::Virtual(id) => {
880                if self.held_controls.contains(id.as_str()) {
881                    withheld.controls.insert(id.clone());
882                }
883                if self
884                    .control_sticks
885                    .get(id.as_str())
886                    .is_some_and(|&deflection| deflection != (0.0, 0.0))
887                {
888                    withheld.control_sticks.insert(id.clone());
889                }
890            }
891            Binding::PadButton(button) | Binding::PadChord { button, .. }
892                if self.held_pad_buttons.contains(*button) =>
893            {
894                withheld.pad_buttons.insert(*button);
895            }
896            Binding::PadDpad => {
897                for button in PadButton::DPAD {
898                    if self.held_pad_buttons.contains(button) {
899                        withheld.pad_buttons.insert(button);
900                    }
901                }
902            }
903            Binding::PadStick { stick, .. } if !self.stick_rests(*stick) => {
904                withheld.pad_sticks.insert(*stick);
905            }
906            Binding::PadTrigger { trigger, .. } if !self.trigger_rests(*trigger) => {
907                withheld.pad_triggers.insert(*trigger);
908            }
909            _ => {}
910        }
911    }
912
913    /// Add `withheld` to what is withheld, and re-resolve every live action
914    /// against it. The caller cancels the patterns first, so a press taken
915    /// away is not read as a release that taps.
916    fn withhold(&mut self, withheld: Suppressed) {
917        self.suppressed.extend(withheld);
918        self.resolve_live();
919    }
920
921    /// Rebuild the routes after the stack changed: withhold every held input
922    /// whose owner moved, idle the actions of contexts that left, and
923    /// re-resolve the rest.
924    fn restack(&mut self) {
925        let routes = Routes::build(self);
926        let old = std::mem::replace(&mut self.routes, routes);
927        let new = &self.routes;
928        for key in self.held_keys.keys() {
929            if old.keys.get(key) != new.keys.get(key) {
930                self.suppressed.keys.insert(*key);
931            }
932        }
933        for button in &self.held_buttons {
934            if old.buttons.get(button) != new.buttons.get(button) {
935                self.suppressed.buttons.insert(*button);
936            }
937        }
938        for control in &self.held_controls {
939            if old.controls.get(control) != new.controls.get(control) {
940                self.suppressed.controls.insert(control.clone());
941            }
942        }
943        let held = self.held_pad_buttons;
944        for button in PadButton::ALL {
945            if held.contains(button) && old.pad_reader(button, held) != new.pad_reader(button, held)
946            {
947                self.suppressed.pad_buttons.insert(button);
948            }
949        }
950        for idx in 0..self.slots.len() {
951            // Before resolving, so a press the new stack takes away is not
952            // read as a release that taps: a stack change cancels every
953            // pattern in flight — see `patterns.rs`.
954            self.slots[idx].patterns.cancel();
955            if self.is_active(self.slots[idx].context) {
956                if self.slots[idx].enabled {
957                    self.resolve_one(idx);
958                }
959            } else {
960                self.slots[idx].reset();
961            }
962        }
963    }
964}
965
966#[cfg(test)]
967mod outrank_tests;
968
969#[cfg(test)]
970mod tests {
971    use super::*;
972    use crate::{ActionDecl, ActionKind, ActionValue, InputTickState, Repeat};
973
974    const TICK: f32 = 1.0 / 60.0;
975
976    fn button(name: &str, bindings: Vec<Binding>) -> ActionDecl {
977        ActionDecl {
978            name: name.to_owned(),
979            kind: ActionKind::Button,
980            bindings,
981        }
982    }
983
984    /// `jump` on Space in gameplay, `accept` on Space and Enter in `menu`.
985    fn menu_over_gameplay() -> ActionMap {
986        let mut map = ActionMap::new();
987        map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
988        map.declare(button("crouch", vec![Binding::Key(KeyCode::KeyC)]));
989        map.declare_in(
990            "menu",
991            button(
992                "accept",
993                vec![Binding::Key(KeyCode::Space), Binding::Key(KeyCode::Enter)],
994            ),
995        );
996        map
997    }
998
999    /// **A key bound in two contexts reaches only the topmost one while it is
1000    /// pushed, and the one beneath once it is popped.**
1001    #[test]
1002    fn a_key_bound_in_both_reaches_only_the_pushed_context_then_gameplay_after_pop() {
1003        let mut map = menu_over_gameplay();
1004        assert_eq!(map.context_of("accept"), Some("menu"));
1005        assert_eq!(map.context_of("jump"), Some(GAMEPLAY_CONTEXT));
1006
1007        map.begin_tick(TICK);
1008        map.key_event(KeyCode::Space, true);
1009        assert!(
1010            map.just_pressed("jump"),
1011            "nothing is pushed: gameplay has it"
1012        );
1013        assert!(
1014            !map.button_held("accept"),
1015            "and a context off the stack is idle"
1016        );
1017        map.key_event(KeyCode::Space, false);
1018
1019        map.push_context("menu").expect("declared, not active");
1020        map.begin_tick(TICK);
1021        map.key_event(KeyCode::Space, true);
1022        assert!(map.just_pressed("accept"), "the pushed context owns Space");
1023        assert!(
1024            !map.button_held("jump"),
1025            "and gameplay beneath it does not see it"
1026        );
1027        map.key_event(KeyCode::Space, false);
1028
1029        map.pop_context("menu").expect("menu is on top");
1030        map.begin_tick(TICK);
1031        map.key_event(KeyCode::Space, true);
1032        assert!(
1033            map.just_pressed("jump"),
1034            "popped: Space is gameplay's again"
1035        );
1036        assert!(!map.button_held("accept"));
1037    }
1038
1039    /// Gameplay's walk on W, shoot on the left button, zoom on the wheel and
1040    /// an aim that reads the pointer's X; an inventory that binds only Escape;
1041    /// and the map toggle on M in [`GLOBAL_CONTEXT`].
1042    fn inventory_over_gameplay() -> ActionMap {
1043        let mut map = ActionMap::new();
1044        map.declare(button("walk", vec![Binding::Key(KeyCode::KeyW)]));
1045        map.declare(button(
1046            "shoot",
1047            vec![Binding::MouseButton(PointerButton::Left)],
1048        ));
1049        map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
1050        map.declare(axis(
1051            "aim_x",
1052            ActionKind::Axis1,
1053            vec![Binding::PointerPosition {
1054                axis: crate::PointerAxis::X,
1055            }],
1056        ));
1057        map.declare_in(
1058            "inventory",
1059            button("close", vec![Binding::Key(KeyCode::Escape)]),
1060        );
1061        map.declare_in(
1062            GLOBAL_CONTEXT,
1063            button("map", vec![Binding::Key(KeyCode::KeyM)]),
1064        );
1065        map
1066    }
1067
1068    /// **A modal context stops every input it does not bind; a plain one lets
1069    /// it fall through.** Over a plain inventory, W still walks; over a modal
1070    /// one, W, the left button and the wheel reach nothing while Escape, which
1071    /// it binds, closes it — and the pointer's position still reaches the aim
1072    /// beneath, because a screen reads the cursor.
1073    #[test]
1074    fn a_modal_context_stops_what_it_does_not_bind() {
1075        let mut map = inventory_over_gameplay();
1076        map.push_context("inventory").expect("declared");
1077        map.begin_tick(TICK);
1078        map.key_event(KeyCode::KeyW, true);
1079        assert!(map.button_held("walk"), "a plain context lets W through");
1080        map.key_event(KeyCode::KeyW, false);
1081        map.pop_context("inventory").expect("on top");
1082
1083        map.push_context_modal("inventory").expect("declared");
1084        map.begin_tick(TICK);
1085        map.key_event(KeyCode::KeyW, true);
1086        map.mouse_button(PointerButton::Left, true);
1087        map.mouse_scroll(0.0, 1.0);
1088        map.pointer_position(0.5, 0.25);
1089        map.key_event(KeyCode::Escape, true);
1090        assert!(!map.button_held("walk"), "W reached gameplay");
1091        assert!(
1092            !map.button_held("shoot"),
1093            "the left button reached gameplay"
1094        );
1095        assert_eq!(map.axis1("zoom"), 0.0, "the wheel reached gameplay");
1096        assert!(map.just_pressed("close"), "the modal context's own key");
1097        assert_eq!(map.axis1("aim_x"), 0.5, "the cursor is still read beneath");
1098    }
1099
1100    /// **A key held into a modal context is dead until pressed again**, after
1101    /// it closes too: the walk held when the inventory opened does not resume
1102    /// when it closes.
1103    #[test]
1104    fn a_key_held_into_a_modal_context_waits_for_a_new_press() {
1105        let mut map = inventory_over_gameplay();
1106        map.begin_tick(TICK);
1107        map.key_event(KeyCode::KeyW, true);
1108        assert!(map.button_held("walk"));
1109
1110        map.push_context_modal("inventory").expect("declared");
1111        assert!(!map.button_held("walk"), "the walk stops as it opens");
1112        map.pop_context("inventory").expect("on top");
1113        map.begin_tick(TICK);
1114        assert!(!map.button_held("walk"), "and does not resume as it closes");
1115
1116        map.key_event(KeyCode::KeyW, false);
1117        map.key_event(KeyCode::KeyW, true);
1118        assert!(map.button_held("walk"), "a new press walks");
1119    }
1120
1121    /// **The global context works over anything, and is never pushed or
1122    /// popped.** M toggles the map over a modal inventory; pushing or popping
1123    /// it is refused; it is active and not listed among the stack's contexts.
1124    #[test]
1125    fn the_global_context_stays_above_a_modal_one() {
1126        let mut map = inventory_over_gameplay();
1127        map.push_context_modal("inventory").expect("declared");
1128        map.begin_tick(TICK);
1129        map.key_event(KeyCode::KeyM, true);
1130        assert!(
1131            map.just_pressed("map"),
1132            "M is global's over the modal screen"
1133        );
1134
1135        assert_eq!(
1136            map.push_context(GLOBAL_CONTEXT),
1137            Err(ActionMapError::ContextAlreadyActive(
1138                GLOBAL_CONTEXT.to_owned()
1139            ))
1140        );
1141        assert_eq!(
1142            map.pop_context(GLOBAL_CONTEXT),
1143            Err(ActionMapError::ContextNotOnTop(GLOBAL_CONTEXT.to_owned()))
1144        );
1145        assert!(map.is_context_active(GLOBAL_CONTEXT));
1146        assert_eq!(
1147            map.active_contexts().collect::<Vec<_>>(),
1148            [GAMEPLAY_CONTEXT, "inventory"]
1149        );
1150    }
1151
1152    /// **An input the top context does not bind falls through**, and one no
1153    /// active context binds reaches nothing.
1154    #[test]
1155    fn an_input_the_top_context_does_not_bind_falls_through() {
1156        let mut map = menu_over_gameplay();
1157        map.push_context("menu").expect("declared");
1158
1159        map.begin_tick(TICK);
1160        map.key_event(KeyCode::KeyC, true);
1161        assert!(
1162            map.just_pressed("crouch"),
1163            "menu binds no C, so it falls through"
1164        );
1165        map.key_event(KeyCode::KeyQ, true);
1166        assert!(!map.button_held("accept") && !map.button_held("jump"));
1167    }
1168
1169    /// **Nested pushes**: each input goes to the topmost context binding it,
1170    /// the stack is enforced, and popping the top hands its inputs back to the
1171    /// next one down.
1172    #[test]
1173    fn nested_pushes_route_each_input_to_the_topmost_binder() {
1174        let mut map = menu_over_gameplay();
1175        map.declare_in(
1176            "editor",
1177            button("select", vec![Binding::Key(KeyCode::Enter)]),
1178        );
1179
1180        map.push_context("menu").expect("declared");
1181        map.push_context("editor").expect("declared");
1182        assert_eq!(
1183            map.active_contexts().collect::<Vec<_>>(),
1184            [GAMEPLAY_CONTEXT, "menu", "editor"],
1185        );
1186        assert_eq!(
1187            map.push_context("menu"),
1188            Err(ActionMapError::ContextAlreadyActive("menu".to_owned())),
1189        );
1190        assert_eq!(
1191            map.pop_context("menu"),
1192            Err(ActionMapError::ContextNotOnTop("menu".to_owned())),
1193            "menu is under editor, and popping it would pop editor's",
1194        );
1195
1196        map.begin_tick(TICK);
1197        map.key_event(KeyCode::Enter, true);
1198        map.key_event(KeyCode::Space, true);
1199        map.key_event(KeyCode::KeyC, true);
1200        assert!(
1201            map.button_held("select"),
1202            "Enter: editor is topmost and binds it"
1203        );
1204        assert!(
1205            map.button_held("accept"),
1206            "Space falls through editor to menu"
1207        );
1208        assert!(
1209            !map.button_held("jump"),
1210            "and menu consumes it before gameplay"
1211        );
1212        assert!(map.button_held("crouch"), "C falls through both");
1213        for key in [KeyCode::Enter, KeyCode::Space, KeyCode::KeyC] {
1214            map.key_event(key, false);
1215        }
1216
1217        map.pop_context("editor").expect("editor is on top");
1218        map.begin_tick(TICK);
1219        map.key_event(KeyCode::Enter, true);
1220        assert!(map.just_pressed("accept"), "editor popped: menu owns Enter");
1221        assert!(!map.button_held("select"), "and a popped context is idle");
1222    }
1223
1224    /// **A key held while the stack changes is withheld from its new owner
1225    /// until it is released** — in both directions.
1226    #[test]
1227    fn a_key_held_across_a_push_or_pop_waits_for_its_release() {
1228        let mut map = menu_over_gameplay();
1229
1230        // Held into a push: gameplay sees it come up, menu does not see it go
1231        // down.
1232        map.begin_tick(TICK);
1233        map.key_event(KeyCode::Space, true);
1234        map.begin_tick(TICK);
1235        map.push_context("menu").expect("declared");
1236        assert!(
1237            map.just_released("jump"),
1238            "the walk stops when a menu opens over it"
1239        );
1240        assert!(
1241            !map.button_held("accept"),
1242            "and the menu is not pressed by it"
1243        );
1244        map.begin_tick(TICK);
1245        map.key_event(KeyCode::Space, true); // an OS auto-repeat is not a new press
1246        assert!(!map.button_held("accept"));
1247        map.key_event(KeyCode::Space, false);
1248        map.key_event(KeyCode::Space, true);
1249        assert!(
1250            map.just_pressed("accept"),
1251            "a fresh press after the lift is menu's"
1252        );
1253
1254        // Held out through a pop: the accept that closed the menu does not jump.
1255        map.pop_context("menu").expect("menu is on top");
1256        assert!(
1257            !map.button_held("jump"),
1258            "the key that closed the menu does not jump"
1259        );
1260        map.begin_tick(TICK);
1261        assert!(!map.button_held("jump"));
1262        map.key_event(KeyCode::Space, false);
1263        map.begin_tick(TICK);
1264        map.key_event(KeyCode::Space, true);
1265        assert!(map.just_pressed("jump"));
1266    }
1267
1268    /// A held key the pushed context does not bind keeps its owner, and so
1269    /// keeps working with no lift.
1270    #[test]
1271    fn a_held_key_whose_owner_does_not_change_is_not_withheld() {
1272        let mut map = menu_over_gameplay();
1273        map.key_event(KeyCode::KeyC, true);
1274        map.push_context("menu").expect("declared");
1275        map.begin_tick(TICK);
1276        assert!(map.button_held("crouch"));
1277        assert!(!map.just_released("crouch"));
1278    }
1279
1280    /// Unknown contexts and the base are refused rather than ignored.
1281    #[test]
1282    fn the_stack_refuses_unknown_contexts_and_the_base() {
1283        let mut map = menu_over_gameplay();
1284        assert_eq!(
1285            map.push_context("nope"),
1286            Err(ActionMapError::UnknownContext("nope".to_owned())),
1287        );
1288        assert_eq!(
1289            map.push_context(GAMEPLAY_CONTEXT),
1290            Err(ActionMapError::ContextAlreadyActive(
1291                GAMEPLAY_CONTEXT.to_owned()
1292            )),
1293        );
1294        assert_eq!(
1295            map.pop_context(GAMEPLAY_CONTEXT),
1296            Err(ActionMapError::ContextNotOnTop(GAMEPLAY_CONTEXT.to_owned())),
1297        );
1298        assert!(map.is_context_active(GAMEPLAY_CONTEXT));
1299        assert!(!map.is_context_active("menu"));
1300        assert!(!map.is_context_active("nope"));
1301    }
1302
1303    /// The pointer, a button and an on-screen control are consumed like keys.
1304    #[test]
1305    fn pointer_inputs_and_controls_are_consumed_too() {
1306        let mut map = ActionMap::new();
1307        map.declare(button(
1308            "fire",
1309            vec![
1310                Binding::MouseButton(PointerButton::Left),
1311                Binding::Virtual("btn".to_owned()),
1312            ],
1313        ));
1314        map.declare(ActionDecl {
1315            name: "aim".to_owned(),
1316            kind: ActionKind::Axis1,
1317            bindings: vec![Binding::PointerPosition {
1318                axis: crate::PointerAxis::X,
1319            }],
1320        });
1321        map.declare_in(
1322            "menu",
1323            ActionDecl {
1324                name: "cursor".to_owned(),
1325                kind: ActionKind::Axis1,
1326                bindings: vec![Binding::PointerPosition {
1327                    axis: crate::PointerAxis::X,
1328                }],
1329            },
1330        );
1331        map.declare_in(
1332            "menu",
1333            button(
1334                "click",
1335                vec![
1336                    Binding::MouseButton(PointerButton::Left),
1337                    Binding::Virtual("btn".to_owned()),
1338                ],
1339            ),
1340        );
1341        map.push_context("menu").expect("declared");
1342
1343        map.begin_tick(TICK);
1344        map.pointer_position(0.5, 0.0);
1345        map.mouse_button(PointerButton::Left, true);
1346        map.virtual_button("btn", true);
1347        assert!(map.just_pressed("click"));
1348        assert!(!map.button_held("fire"));
1349        assert_eq!(map.axis1("cursor"), 0.5);
1350        assert_eq!(
1351            map.axis1("aim"),
1352            0.0,
1353            "the menu's pointer is not the game's"
1354        );
1355        let Some(ActionValue::Axis1(aim)) = map.action("aim") else {
1356            panic!("aim is an Axis1");
1357        };
1358        assert!(!aim.pointer_moved, "nor did it move for the game");
1359
1360        // And not on the next tick either, which re-resolves every action.
1361        map.begin_tick(TICK);
1362        assert_eq!(map.axis1("aim"), 0.0, "a consumed pointer stays consumed");
1363        assert!(!map.button_held("fire"));
1364    }
1365
1366    /// A rebind can take a key from a context beneath, and the context beneath
1367    /// releases it on the spot.
1368    #[test]
1369    fn a_rebind_moves_ownership_and_re_resolves_the_loser() {
1370        let mut map = menu_over_gameplay();
1371        map.push_context("menu").expect("declared");
1372        map.key_event(KeyCode::KeyC, true);
1373        assert!(map.button_held("crouch"));
1374
1375        map.rebind("accept", vec![Binding::Key(KeyCode::KeyC)])
1376            .expect("declared");
1377        assert!(
1378            !map.button_held("crouch"),
1379            "menu took C, so gameplay lost it"
1380        );
1381        assert!(
1382            map.button_held("accept"),
1383            "and a rebind resolves against held keys"
1384        );
1385    }
1386
1387    /// **Chords take the key from plain bindings on it**: Tab and Shift+Tab in
1388    /// one context never both fire, and the modifier is not consumed.
1389    #[test]
1390    fn a_chord_shadows_the_plain_key_and_leaves_its_modifier_alone() {
1391        let mut map = ActionMap::new();
1392        map.declare(button("sprint", vec![Binding::Key(KeyCode::ShiftLeft)]));
1393        map.declare_in("menu", button("next", vec![Binding::Key(KeyCode::Tab)]));
1394        map.declare_in(
1395            "menu",
1396            button(
1397                "prev",
1398                vec![Binding::Chord {
1399                    modifier: Modifier::Shift,
1400                    key: KeyCode::Tab,
1401                }],
1402            ),
1403        );
1404        map.push_context("menu").expect("declared");
1405
1406        map.begin_tick(TICK);
1407        map.key_event(KeyCode::Tab, true);
1408        assert!(map.button_held("next") && !map.button_held("prev"));
1409
1410        map.key_event(KeyCode::ShiftLeft, true);
1411        assert!(
1412            map.just_released("next"),
1413            "Shift turns the held Tab into Shift+Tab"
1414        );
1415        assert!(map.just_pressed("prev"));
1416        assert!(
1417            map.button_held("sprint"),
1418            "the modifier still reaches gameplay"
1419        );
1420
1421        map.key_event(KeyCode::ShiftLeft, false);
1422        assert!(!map.button_held("prev") && map.button_held("next"));
1423    }
1424
1425    fn alt_right() -> Binding {
1426        Binding::ButtonChord {
1427            modifier: Modifier::Alt,
1428            button: PointerButton::Right,
1429        }
1430    }
1431
1432    /// `aim` on the right button, `zoom` on Alt+right and `lean` on Alt alone,
1433    /// all in gameplay.
1434    fn sights() -> ActionMap {
1435        let mut map = ActionMap::new();
1436        map.declare(button(
1437            "aim",
1438            vec![Binding::MouseButton(PointerButton::Right)],
1439        ));
1440        map.declare(button("zoom", vec![alt_right()]));
1441        map.declare(button("lean", vec![Binding::Key(KeyCode::AltLeft)]));
1442        map
1443    }
1444
1445    /// **A button chord takes the button from the plain binding on it**, and
1446    /// leaves its modifier alone: a right click aims, Alt+right click zooms
1447    /// and does not aim, and Alt's own binding sees Alt either way.
1448    #[test]
1449    fn a_button_chord_shadows_the_plain_button_and_leaves_its_modifier_alone() {
1450        let mut map = sights();
1451        map.begin_tick(TICK);
1452        map.mouse_button(PointerButton::Right, true);
1453        assert!(map.button_held("aim") && !map.button_held("zoom"));
1454
1455        map.mouse_button(PointerButton::Right, false);
1456        map.begin_tick(TICK);
1457        map.key_event(KeyCode::AltLeft, true);
1458        map.mouse_button(PointerButton::Right, true);
1459        assert!(map.just_pressed("zoom"));
1460        assert!(!map.button_held("aim"), "the plain binding fired too");
1461        assert!(map.button_held("lean"), "the modifier was consumed");
1462    }
1463
1464    /// **The modifier hands the held button over both ways**, as a key
1465    /// chord's does: pressed over a held right button it releases the aim and
1466    /// presses the zoom, and let go first it hands the button back.
1467    #[test]
1468    fn a_modifier_hands_a_held_mouse_button_to_the_chord_and_back() {
1469        let mut map = sights();
1470        map.begin_tick(TICK);
1471        map.mouse_button(PointerButton::Right, true);
1472        assert!(map.just_pressed("aim"));
1473
1474        map.begin_tick(TICK);
1475        map.key_event(KeyCode::AltRight, true);
1476        assert!(
1477            map.just_released("aim"),
1478            "Alt turns the held right button into Alt+right"
1479        );
1480        assert!(map.just_pressed("zoom"));
1481
1482        map.begin_tick(TICK);
1483        map.key_event(KeyCode::AltRight, false);
1484        assert!(map.just_released("zoom"));
1485        assert!(map.just_pressed("aim"), "the button went back to the aim");
1486    }
1487
1488    /// **A button chord shadows plain bindings only in the context that owns
1489    /// the button**: declared in a context off the stack, it leaves the right
1490    /// button with gameplay; pushed, it takes it.
1491    #[test]
1492    fn a_button_chord_shadows_only_in_the_context_that_owns_its_button() {
1493        let mut map = ActionMap::new();
1494        map.declare(button(
1495            "aim",
1496            vec![Binding::MouseButton(PointerButton::Right)],
1497        ));
1498        map.declare_in("optic", button("magnify", vec![alt_right()]));
1499        map.key_event(KeyCode::AltLeft, true);
1500        map.mouse_button(PointerButton::Right, true);
1501        assert!(
1502            map.button_held("aim"),
1503            "a chord off the stack shadowed the button"
1504        );
1505        assert!(!map.button_held("magnify"));
1506
1507        map.mouse_button(PointerButton::Right, false);
1508        map.push_context("optic").expect("declared");
1509        map.mouse_button(PointerButton::Right, true);
1510        assert!(map.button_held("magnify"));
1511        assert!(!map.button_held("aim"));
1512    }
1513
1514    fn axis(name: &str, kind: ActionKind, bindings: Vec<Binding>) -> ActionDecl {
1515        ActionDecl {
1516            name: name.to_owned(),
1517            kind,
1518            bindings,
1519        }
1520    }
1521
1522    fn wasd() -> Binding {
1523        Binding::Wasd {
1524            up: KeyCode::KeyW,
1525            down: KeyCode::KeyS,
1526            left: KeyCode::KeyA,
1527            right: KeyCode::KeyD,
1528        }
1529    }
1530
1531    /// **`suppress_held` withholds a held key until it is pressed again**: the
1532    /// action reads released at once, an OS auto-repeat does not bring it back,
1533    /// and a fresh press after the lift does.
1534    #[test]
1535    fn suppress_held_withholds_a_key_until_it_is_pressed_again() {
1536        let mut map = menu_over_gameplay();
1537        map.begin_tick(TICK);
1538        map.key_event(KeyCode::Space, true);
1539        map.begin_tick(TICK);
1540        map.suppress_held();
1541        assert!(map.just_released("jump"), "the walk stops on the spot");
1542        map.begin_tick(TICK);
1543        map.key_event(KeyCode::Space, true); // an OS auto-repeat
1544        assert!(!map.button_held("jump"));
1545        map.key_event(KeyCode::Space, false);
1546        map.key_event(KeyCode::Space, true);
1547        assert!(map.just_pressed("jump"), "pressed again");
1548    }
1549
1550    /// **Suppressing mid-press fires neither the hold nor the tap**, and the
1551    /// press after the release fires as usual.
1552    #[test]
1553    fn suppress_held_mid_press_fires_no_pattern_and_the_next_press_does() {
1554        let mut map = menu_over_gameplay();
1555        map.set_tap("jump", Some(crate::Tap::default()))
1556            .expect("declared");
1557        map.set_hold("jump", Some(crate::Hold::default()))
1558            .expect("declared");
1559        map.begin_tick(TICK);
1560        map.key_event(KeyCode::Space, true);
1561        map.begin_tick(TICK);
1562        map.suppress_held();
1563        let mut fired = map.tapped("jump");
1564        for _ in 0..60 {
1565            map.begin_tick(TICK);
1566            fired |= map.hold_fired("jump");
1567        }
1568        map.key_event(KeyCode::Space, false);
1569        fired |= map.tapped("jump");
1570        assert!(!fired, "neither a hold nor a tap");
1571
1572        map.begin_tick(TICK);
1573        map.key_event(KeyCode::Space, true);
1574        map.begin_tick(TICK);
1575        map.key_event(KeyCode::Space, false);
1576        assert!(map.tapped("jump"), "suppress, release, press: it taps");
1577    }
1578
1579    /// A first tap waiting for its second is dropped by `suppress_held`, with
1580    /// nothing held at the time.
1581    #[test]
1582    fn suppress_held_drops_a_waiting_first_tap() {
1583        let mut map = menu_over_gameplay();
1584        map.set_double_tap("jump", Some(crate::DoubleTap::default()))
1585            .expect("declared");
1586        map.begin_tick(TICK);
1587        map.key_event(KeyCode::Space, true);
1588        map.key_event(KeyCode::Space, false);
1589        map.suppress_held();
1590        map.begin_tick(TICK);
1591        map.key_event(KeyCode::Space, true);
1592        assert!(!map.double_tapped("jump"));
1593    }
1594
1595    /// **A `Wasd` axis is withheld per key**: a key held through the suppress
1596    /// reads zero until pressed again, and a key pressed afresh moves the axis
1597    /// meanwhile. A `KeyAxis` likewise.
1598    #[test]
1599    fn suppress_held_withholds_an_axis_per_key() {
1600        let mut map = ActionMap::new();
1601        map.declare(axis("move", ActionKind::Axis2, vec![wasd()]));
1602        map.declare(axis(
1603            "zoom",
1604            ActionKind::Axis1,
1605            vec![Binding::KeyAxis {
1606                negative: KeyCode::KeyQ,
1607                positive: KeyCode::KeyE,
1608            }],
1609        ));
1610        map.key_event(KeyCode::KeyW, true);
1611        map.key_event(KeyCode::KeyE, true);
1612        map.suppress_held();
1613        assert_eq!(map.axis2("move"), (0.0, 0.0));
1614        assert_eq!(map.axis1("zoom"), 0.0);
1615
1616        map.begin_tick(TICK);
1617        assert_eq!(map.axis2("move"), (0.0, 0.0), "still held, still withheld");
1618        map.key_event(KeyCode::KeyD, true);
1619        assert_eq!(map.axis2("move"), (1.0, 0.0), "a fresh key moves");
1620        map.key_event(KeyCode::KeyQ, true);
1621        assert_eq!(map.axis1("zoom"), -1.0);
1622
1623        map.key_event(KeyCode::KeyW, false);
1624        map.key_event(KeyCode::KeyW, true);
1625        let (x, y) = map.axis2("move");
1626        assert!(
1627            x > 0.0 && y > 0.0,
1628            "W pressed again: diagonal, got {x}, {y}"
1629        );
1630    }
1631
1632    /// A chord is withheld through its key: pressed again with the modifier
1633    /// still down, it reads again.
1634    #[test]
1635    fn suppress_held_withholds_a_chord_until_its_key_is_pressed_again() {
1636        let mut map = ActionMap::new();
1637        map.declare(button(
1638            "save",
1639            vec![Binding::Chord {
1640                modifier: Modifier::Control,
1641                key: KeyCode::KeyS,
1642            }],
1643        ));
1644        map.key_event(KeyCode::ControlLeft, true);
1645        map.key_event(KeyCode::KeyS, true);
1646        assert!(map.button_held("save"));
1647        map.suppress_held();
1648        assert!(!map.button_held("save"));
1649        map.key_event(KeyCode::KeyS, false);
1650        map.key_event(KeyCode::KeyS, true);
1651        assert!(map.just_pressed("save"), "Ctrl never let go");
1652    }
1653
1654    /// A button chord is withheld through its button, as a key chord is
1655    /// through its key: clicked again with the modifier still down, it reads
1656    /// again.
1657    #[test]
1658    fn suppress_held_withholds_a_button_chord_until_its_button_is_pressed_again() {
1659        let mut map = sights();
1660        map.key_event(KeyCode::AltLeft, true);
1661        map.mouse_button(PointerButton::Right, true);
1662        assert!(map.button_held("zoom"));
1663        map.suppress_held();
1664        assert!(!map.button_held("zoom"));
1665        map.mouse_button(PointerButton::Right, false);
1666        map.mouse_button(PointerButton::Right, true);
1667        assert!(map.just_pressed("zoom"), "Alt never let go");
1668    }
1669
1670    /// Pointer buttons, on-screen buttons and on-screen sticks are withheld
1671    /// too, a stick until it reports centre.
1672    #[test]
1673    fn suppress_held_withholds_pointer_buttons_and_on_screen_controls() {
1674        let mut map = ActionMap::new();
1675        map.declare(button(
1676            "fire",
1677            vec![
1678                Binding::MouseButton(PointerButton::Left),
1679                Binding::Virtual("btn".to_owned()),
1680            ],
1681        ));
1682        map.declare(axis(
1683            "move",
1684            ActionKind::Axis2,
1685            vec![Binding::Virtual("stick".to_owned())],
1686        ));
1687        map.mouse_button(PointerButton::Left, true);
1688        map.virtual_button("btn", true);
1689        map.virtual_stick("stick", 0.0, 1.0);
1690        map.suppress_held();
1691        assert!(!map.button_held("fire"));
1692        assert_eq!(map.axis2("move"), (0.0, 0.0));
1693
1694        map.virtual_stick("stick", 0.5, 0.5);
1695        assert_eq!(map.axis2("move"), (0.0, 0.0), "moved, never centred");
1696        map.virtual_stick("stick", 0.0, 0.0);
1697        map.virtual_stick("stick", 0.0, 1.0);
1698        assert_eq!(map.axis2("move"), (0.0, 1.0), "centred, then pushed");
1699
1700        map.mouse_button(PointerButton::Left, false);
1701        assert!(
1702            !map.button_held("fire"),
1703            "the on-screen button is still held"
1704        );
1705        map.virtual_button("btn", false);
1706        map.mouse_button(PointerButton::Left, true);
1707        assert!(map.just_pressed("fire"));
1708    }
1709
1710    fn pad(snapshot: impl FnOnce(&mut crate::GamepadSnapshot)) -> crate::GamepadEvent {
1711        let mut state = crate::GamepadSnapshot::neutral(crate::PadKind::Xbox);
1712        snapshot(&mut state);
1713        crate::GamepadEvent::State {
1714            id: crate::GamepadId(1),
1715            snapshot: state,
1716        }
1717    }
1718
1719    fn stick_y(y: f32) -> crate::GamepadEvent {
1720        pad(|state| state.axes[crate::PadAxis::LeftY as usize] = y)
1721    }
1722
1723    /// **A pad stick is withheld until it rests inside its dead zone**, and a
1724    /// trigger until it is back under its threshold; a pad button until it is
1725    /// released.
1726    #[test]
1727    fn suppress_held_withholds_pad_buttons_sticks_and_triggers() {
1728        let mut map = ActionMap::new();
1729        map.declare(axis(
1730            "move",
1731            ActionKind::Axis2,
1732            vec![Binding::PadStick {
1733                stick: Stick::Left,
1734                deadzone: 0.2,
1735            }],
1736        ));
1737        map.declare(button(
1738            "fire",
1739            vec![Binding::PadTrigger {
1740                trigger: Trigger::Right,
1741                threshold: 0.5,
1742            }],
1743        ));
1744        map.declare(button("jump", vec![Binding::PadButton(PadButton::South)]));
1745        map.gamepad_event(&pad(|state| {
1746            state.axes[crate::PadAxis::LeftY as usize] = 1.0;
1747            state.axes[crate::PadAxis::RightTrigger as usize] = 1.0;
1748            state.buttons.insert(PadButton::South);
1749        }));
1750        assert_eq!(map.axis2("move"), (0.0, 1.0));
1751        assert!(map.button_held("fire") && map.button_held("jump"));
1752        map.suppress_held();
1753        assert_eq!(map.axis2("move"), (0.0, 0.0));
1754        assert!(!map.button_held("fire") && !map.button_held("jump"));
1755
1756        map.gamepad_event(&stick_y(0.8));
1757        assert_eq!(map.axis2("move"), (0.0, 0.0), "eased off, never rested");
1758        assert!(!map.button_held("jump"), "and South was let go of");
1759        map.gamepad_event(&stick_y(0.1));
1760        map.gamepad_event(&stick_y(1.0));
1761        assert_eq!(map.axis2("move"), (0.0, 1.0), "rested, then pushed");
1762        map.gamepad_event(&pad(|state| {
1763            state.axes[crate::PadAxis::RightTrigger as usize] = 1.0;
1764            state.buttons.insert(PadButton::South);
1765        }));
1766        assert!(map.button_held("fire") && map.button_held("jump"));
1767    }
1768
1769    /// **`suppress_held_action` withholds only what that action reads**: an
1770    /// action on another held key keeps it, and an action sharing the key
1771    /// loses it too.
1772    #[test]
1773    fn suppress_held_action_withholds_only_that_actions_inputs() {
1774        let mut map = ActionMap::new();
1775        map.declare(axis("move", ActionKind::Axis2, vec![wasd()]));
1776        map.declare(button("forward", vec![Binding::Key(KeyCode::KeyW)]));
1777        map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
1778        map.key_event(KeyCode::KeyW, true);
1779        map.key_event(KeyCode::Space, true);
1780        map.suppress_held_action("move").expect("declared");
1781        assert_eq!(map.axis2("move"), (0.0, 0.0));
1782        assert!(!map.button_held("forward"), "W itself is withheld");
1783        assert!(map.button_held("jump"), "Space is not move's");
1784        map.key_event(KeyCode::KeyW, false);
1785        map.key_event(KeyCode::KeyW, true);
1786        assert_eq!(map.axis2("move"), (0.0, 1.0));
1787        assert_eq!(
1788            map.suppress_held_action("nope"),
1789            Err(ActionMapError::UnknownAction("nope".to_owned())),
1790        );
1791    }
1792
1793    /// `zoom` on the plain wheel, `zoom_z` on Z+wheel, `zoom_ctrl` on
1794    /// Ctrl+wheel, and `z` on Z itself, all in gameplay.
1795    fn scroll_chords() -> ActionMap {
1796        let mut map = ActionMap::new();
1797        map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
1798        for (name, held) in [
1799            ("zoom_z", KeyCode::KeyZ),
1800            ("zoom_ctrl", KeyCode::ControlLeft),
1801        ] {
1802            map.declare(axis(
1803                name,
1804                ActionKind::Axis1,
1805                vec![Binding::ScrollChord { held }],
1806            ));
1807        }
1808        map.declare(button("z", vec![Binding::Key(KeyCode::KeyZ)]));
1809        map
1810    }
1811
1812    /// Scroll one detent on a fresh tick and read who got it.
1813    fn wheel(map: &mut ActionMap) -> [f32; 3] {
1814        map.begin_tick(TICK);
1815        map.mouse_scroll(0.0, 1.0);
1816        ["zoom", "zoom_z", "zoom_ctrl"].map(|name| map.axis1(name))
1817    }
1818
1819    /// **A disabled action's scroll chord takes the wheel from nothing.** Ctrl
1820    /// goes down for `zoom_ctrl`, then Z for `zoom_z` while `zoom_z` is
1821    /// disabled: the wheel stays Ctrl's. With Ctrl up too, the plain wheel is
1822    /// not silenced by Z. Enabled again, Z takes the wheel back from Ctrl by
1823    /// when it went down, which was after Ctrl, not by when it was enabled.
1824    #[test]
1825    fn a_disabled_scroll_chord_takes_the_wheel_from_nothing() {
1826        let mut map = scroll_chords();
1827        map.key_event(KeyCode::ControlLeft, true);
1828        map.set_enabled("zoom_z", false);
1829        map.key_event(KeyCode::KeyZ, true);
1830        assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "Ctrl keeps the wheel");
1831
1832        map.key_event(KeyCode::ControlLeft, false);
1833        assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0], "Z silences nothing");
1834
1835        map.key_event(KeyCode::ControlLeft, true);
1836        map.set_enabled("zoom_z", true);
1837        assert_eq!(
1838            wheel(&mut map),
1839            [0.0, 0.0, 1.0],
1840            "Ctrl went down after Z, so it is the newer press"
1841        );
1842        map.key_event(KeyCode::ControlLeft, false);
1843        map.key_event(KeyCode::ControlLeft, true);
1844        map.key_event(KeyCode::KeyZ, false);
1845        map.key_event(KeyCode::KeyZ, true);
1846        map.set_enabled("zoom_z", false);
1847        map.set_enabled("zoom_z", true);
1848        assert_eq!(
1849            wheel(&mut map),
1850            [0.0, 1.0, 0.0],
1851            "Z pressed after Ctrl takes it back once enabled"
1852        );
1853    }
1854
1855    /// **A scroll chord takes the wheel from the plain binding while its key is
1856    /// down**, gives it back on release, and reads the key without consuming
1857    /// it.
1858    #[test]
1859    fn a_scroll_chord_takes_the_wheel_while_its_key_is_down() {
1860        let mut map = scroll_chords();
1861        assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
1862        map.key_event(KeyCode::KeyZ, true);
1863        assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0]);
1864        assert!(map.button_held("z"), "Z is read, not consumed");
1865        map.key_event(KeyCode::KeyZ, false);
1866        assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0], "the wheel is back");
1867    }
1868
1869    /// **With two scroll-chord keys down, the most recently pressed one takes
1870    /// the wheel**, in either order, and releasing it hands the wheel back to
1871    /// the other; an auto-repeat of the earlier key takes nothing.
1872    #[test]
1873    fn the_most_recently_pressed_scroll_chord_key_takes_the_wheel() {
1874        let mut map = scroll_chords();
1875        map.key_event(KeyCode::ControlLeft, true);
1876        map.key_event(KeyCode::KeyZ, true);
1877        assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "Ctrl then Z: Z's");
1878        map.key_event(KeyCode::ControlLeft, true); // an OS auto-repeat
1879        assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "a repeat is no press");
1880        map.key_event(KeyCode::KeyZ, false);
1881        assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "back to Ctrl");
1882        map.key_event(KeyCode::ControlLeft, false);
1883
1884        map.key_event(KeyCode::KeyZ, true);
1885        map.key_event(KeyCode::ControlLeft, true);
1886        assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "Z then Ctrl: Ctrl's");
1887        map.key_event(KeyCode::ControlLeft, false);
1888        assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "back to Z");
1889        map.key_event(KeyCode::KeyZ, false);
1890        assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
1891    }
1892
1893    /// A key pressed after the wheel turned, in the same tick, moves that
1894    /// tick's scroll to the chord: the key re-resolves the wheel's bindings.
1895    #[test]
1896    fn a_scroll_chord_key_pressed_mid_tick_takes_that_ticks_scroll() {
1897        let mut map = scroll_chords();
1898        map.begin_tick(TICK);
1899        map.mouse_scroll(0.0, 1.0);
1900        map.key_event(KeyCode::KeyZ, true);
1901        assert_eq!(["zoom", "zoom_z"].map(|name| map.axis1(name)), [0.0, 1.0]);
1902    }
1903
1904    /// **A scroll chord in a pushed context takes the wheel for it**, and its
1905    /// key shadows only that context's plain wheel: the context beneath gets
1906    /// no wheel at all, as with any consumed input.
1907    #[test]
1908    fn a_scroll_chord_claims_the_wheel_for_its_context() {
1909        let mut map = ActionMap::new();
1910        map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
1911        map.declare_in(
1912            "map",
1913            axis(
1914                "pan",
1915                ActionKind::Axis1,
1916                vec![Binding::ScrollChord {
1917                    held: KeyCode::AltLeft,
1918                }],
1919            ),
1920        );
1921        map.push_context("map").expect("declared");
1922        map.begin_tick(TICK);
1923        map.mouse_scroll(0.0, 1.0);
1924        assert_eq!(map.axis1("zoom"), 0.0, "the pushed context owns the wheel");
1925        assert_eq!(map.axis1("pan"), 0.0, "and Alt is not down");
1926        map.key_event(KeyCode::AltLeft, true);
1927        assert_eq!(map.axis1("pan"), 1.0);
1928    }
1929
1930    /// A scroll chord's key withheld by `suppress_held` reads as up: the plain
1931    /// wheel has it until the key is pressed again.
1932    #[test]
1933    fn a_suppressed_scroll_chord_key_leaves_the_wheel_plain() {
1934        let mut map = scroll_chords();
1935        map.key_event(KeyCode::KeyZ, true);
1936        map.suppress_held();
1937        assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
1938        map.key_event(KeyCode::KeyZ, false);
1939        map.key_event(KeyCode::KeyZ, true);
1940        assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0]);
1941    }
1942
1943    /// **The same scripted input yields the same action stream twice**, from
1944    /// two maps built independently — each with its own hash seeds, so a rule
1945    /// that leaned on a `HashMap`'s iteration order would split them.
1946    #[test]
1947    fn a_scripted_sequence_yields_the_same_action_stream_twice() {
1948        fn run() -> Vec<String> {
1949            let mut map = ActionMap::new();
1950            map.declare(ActionDecl {
1951                name: "move".to_owned(),
1952                kind: ActionKind::Axis2,
1953                bindings: vec![Binding::Wasd {
1954                    up: KeyCode::KeyW,
1955                    down: KeyCode::KeyS,
1956                    left: KeyCode::KeyA,
1957                    right: KeyCode::KeyD,
1958                }],
1959            });
1960            map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
1961            crate::ui::declare(&mut map).expect("no clash with move or jump");
1962            map.set_repeat("jump", Some(Repeat::UI)).expect("declared");
1963
1964            let script: &[(u32, KeyCode, bool)] = &[
1965                (0, KeyCode::KeyD, true),
1966                (1, KeyCode::Space, true),
1967                (3, KeyCode::KeyW, true),
1968                (5, KeyCode::Tab, true),
1969                (6, KeyCode::ShiftRight, true),
1970                (9, KeyCode::Space, false),
1971                (12, KeyCode::KeyD, false),
1972                (14, KeyCode::Space, true),
1973                (20, KeyCode::KeyW, false),
1974                (40, KeyCode::Tab, false),
1975                (41, KeyCode::ShiftRight, false),
1976                (44, KeyCode::Space, false),
1977            ];
1978            let mut stream = Vec::new();
1979            for tick in 0..60 {
1980                map.begin_tick(TICK);
1981                match tick {
1982                    4 | 30 => map.push_context(crate::ui::CONTEXT).expect("off the stack"),
1983                    18 | 50 => map.pop_context(crate::ui::CONTEXT).expect("on top"),
1984                    _ => {}
1985                }
1986                for &(at, key, pressed) in script {
1987                    if at == tick {
1988                        map.key_event(key, pressed);
1989                    }
1990                }
1991                let repeats: Vec<bool> =
1992                    map.action_names().map(|name| map.repeated(name)).collect();
1993                stream.push(format!(
1994                    "{tick} {:?} {repeats:?} {:?}",
1995                    InputTickState::capture(&map),
1996                    map.last_device(),
1997                ));
1998            }
1999            stream
2000        }
2001
2002        let first = run();
2003        assert_eq!(first, run());
2004        assert!(
2005            first.iter().any(|line| line.contains("just_pressed: true")),
2006            "the script pressed something, or equality says nothing",
2007        );
2008    }
2009}