leafwing_input_manager/
clashing_inputs.rs

1//! Handles clashing inputs into a [`InputMap`] in a configurable fashion.
2//!
3//! [`Buttonlike`] actions can clash, if one is a strict subset of the other.
4//! For example, the user might have bound `Ctrl + S` to save, and `S` to move down.
5//! If the user presses `Ctrl + S`, the input manager should not also trigger the `S` action.
6
7use std::cmp::Ordering;
8
9use bevy::prelude::{Entity, Resource};
10use serde::{Deserialize, Serialize};
11
12use crate::input_map::{InputMap, UpdatedActions};
13use crate::prelude::updating::CentralInputStore;
14use crate::user_input::Buttonlike;
15use crate::{Actionlike, InputControlKind};
16
17/// How should clashing inputs by handled by an [`InputMap`]?
18///
19/// Inputs "clash" if and only if the [`Buttonlike`] components of one user input is a strict subset of the other.
20/// For example:
21///
22/// - `S` and `W`: does not clash
23/// - `ControlLeft + S` and `S`: clashes
24/// - `S` and `S`: does not clash
25/// - `ControlLeft + S` and ` AltLeft + S`: does not clash
26/// - `ControlLeft + S`, `AltLeft + S` and `ControlLeft + AltLeft + S`: clashes
27///
28/// This strategy is only used when assessing the actions and input holistically,
29/// in [`InputMap::process_actions`], using [`InputMap::handle_clashes`].
30#[non_exhaustive]
31#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default)]
32pub enum ClashStrategy {
33    /// All matching inputs will always be pressed
34    PressAll,
35    /// Only press the action that corresponds to the longest chord
36    ///
37    /// This is the default strategy.
38    #[default]
39    PrioritizeLongest,
40}
41
42impl ClashStrategy {
43    /// Returns the list of all possible clash strategies.
44    pub fn variants() -> &'static [ClashStrategy] {
45        use ClashStrategy::*;
46
47        &[PressAll, PrioritizeLongest]
48    }
49}
50
51/// A flat list of the [`Buttonlike`] inputs that make up a [`UserInput`](crate::user_input::UserInput).
52///
53/// This is used to check for potential clashes between actions,
54/// where one action is a strict subset of another.
55#[derive(Debug, Clone)]
56#[must_use]
57pub enum BasicInputs {
58    /// No buttonlike inputs are involved.
59    ///
60    /// This might be used for things like a joystick axis.
61    None,
62
63    /// The input consists of a single, fundamental [`Buttonlike`] [`UserInput`](crate::user_input::UserInput).
64    ///
65    /// For example, a single key press.
66    Simple(Box<dyn Buttonlike>),
67
68    /// The input can be triggered by multiple independent [`Buttonlike`] [`UserInput`](crate::user_input::UserInput)s,
69    /// but is still fundamentally considered a single input.
70    ///
71    /// For example, a virtual D-Pad is only one input, but can be triggered by multiple keys.
72    Composite(Vec<Box<dyn Buttonlike>>),
73
74    /// The input represents one or more independent [`Buttonlike`] [`UserInput`](crate::user_input::UserInput) types.
75    ///
76    /// For example, a chorded input is a group of multiple keys that must be pressed together.
77    Chord(Vec<Box<dyn Buttonlike>>),
78}
79
80impl BasicInputs {
81    /// Returns a list of the underlying [`Buttonlike`] [`UserInput`](crate::user_input::UserInput)s.
82    ///
83    /// # Warning
84    ///
85    /// When checking for clashes, do not use this method to compute the length of the input.
86    /// Instead, use [`BasicInputs::len`], as these do not always agree.
87    #[inline]
88    pub fn inputs(&self) -> Vec<Box<dyn Buttonlike>> {
89        match self.clone() {
90            Self::None => Vec::default(),
91            Self::Simple(input) => vec![input],
92            Self::Composite(inputs) => inputs,
93            Self::Chord(inputs) => inputs,
94        }
95    }
96
97    ///  Create a [`BasicInputs::Composite`] from two existing [`BasicInputs`].
98    pub fn compose(self, other: BasicInputs) -> Self {
99        let combined_inputs = self.inputs().into_iter().chain(other.inputs()).collect();
100
101        BasicInputs::Composite(combined_inputs)
102    }
103
104    /// Returns the number of the logical [`Buttonlike`] [`UserInput`](crate::user_input::UserInput)s that make up the input.
105    ///
106    /// A single key press is one input, while a chorded input is multiple inputs.
107    /// A composite input is still considered one input, even if it can be triggered by multiple keys,
108    /// as only one input need actually be pressed.
109    #[allow(clippy::len_without_is_empty)]
110    #[inline]
111    pub fn len(&self) -> usize {
112        match self {
113            Self::None => 0,
114            Self::Simple(_) => 1,
115            Self::Composite(_) => 1,
116            Self::Chord(inputs) => inputs.len(),
117        }
118    }
119
120    /// Checks if the given two [`BasicInputs`] clash with each other.
121    #[inline]
122    pub fn clashes_with(&self, other: &BasicInputs) -> bool {
123        match (self, other) {
124            (Self::None, _) | (_, Self::None) => false,
125            (Self::Simple(_), Self::Simple(_)) => false,
126            (Self::Simple(self_single), Self::Chord(other_group)) => {
127                other_group.len() > 1 && other_group.contains(self_single)
128            }
129            (Self::Chord(self_group), Self::Simple(other_single)) => {
130                self_group.len() > 1 && self_group.contains(other_single)
131            }
132            (Self::Simple(self_single), Self::Composite(other_composite)) => {
133                other_composite.contains(self_single)
134            }
135            (Self::Composite(self_composite), Self::Simple(other_single)) => {
136                self_composite.contains(other_single)
137            }
138            (Self::Composite(self_composite), Self::Chord(other_group)) => {
139                other_group.len() > 1
140                    && other_group
141                        .iter()
142                        .any(|input| self_composite.contains(input))
143            }
144            (Self::Chord(self_group), Self::Composite(other_composite)) => {
145                self_group.len() > 1
146                    && self_group
147                        .iter()
148                        .any(|input| other_composite.contains(input))
149            }
150            (Self::Chord(self_group), Self::Chord(other_group)) => {
151                self_group.len() > 1
152                    && other_group.len() > 1
153                    && self_group != other_group
154                    && (self_group.iter().all(|input| other_group.contains(input))
155                        || other_group.iter().all(|input| self_group.contains(input)))
156            }
157            (Self::Composite(self_composite), Self::Composite(other_composite)) => {
158                other_composite
159                    .iter()
160                    .any(|input| self_composite.contains(input))
161                    || self_composite
162                        .iter()
163                        .any(|input| other_composite.contains(input))
164            }
165        }
166    }
167}
168
169impl<A: Actionlike> InputMap<A> {
170    /// Resolve clashing button-like inputs, removing action presses that have been overruled
171    ///
172    /// The `usize` stored in `pressed_actions` corresponds to `Actionlike::index`
173    pub fn handle_clashes(
174        &self,
175        updated_actions: &mut UpdatedActions<A>,
176        input_store: &CentralInputStore,
177        clash_strategy: ClashStrategy,
178        gamepad: Entity,
179    ) {
180        for clash in self.get_clashes(updated_actions, input_store, gamepad) {
181            // Remove the action in the pair that was overruled, if any
182            if let Some(culled_action) = resolve_clash(&clash, clash_strategy, input_store, gamepad)
183            {
184                updated_actions.remove(&culled_action);
185            }
186        }
187    }
188
189    /// Updates the cache of possible input clashes
190    pub(crate) fn possible_clashes(&self) -> Vec<Clash<A>> {
191        let mut clashes = Vec::default();
192
193        for action_a in self.buttonlike_actions() {
194            for action_b in self.buttonlike_actions() {
195                if let Some(clash) = self.possible_clash(action_a, action_b) {
196                    clashes.push(clash);
197                }
198            }
199        }
200
201        clashes
202    }
203
204    /// Gets the set of clashing action-input pairs
205    ///
206    /// Returns both the action and [`UserInput`](crate::user_input::UserInput)s for each clashing set
207    #[must_use]
208    fn get_clashes(
209        &self,
210        updated_actions: &UpdatedActions<A>,
211        input_store: &CentralInputStore,
212        gamepad: Entity,
213    ) -> Vec<Clash<A>> {
214        let mut clashes = Vec::default();
215
216        // We can limit our search to the cached set of possibly clashing actions
217        for clash in self.possible_clashes() {
218            let pressed_a = updated_actions.pressed(&clash.action_a);
219            let pressed_b = updated_actions.pressed(&clash.action_b);
220
221            // Clashes can only occur if both actions were triggered
222            // This is not strictly necessary, but saves work
223            if pressed_a && pressed_b {
224                // Check if the potential clash occurred based on the pressed inputs
225                if let Some(clash) = check_clash(&clash, input_store, gamepad) {
226                    clashes.push(clash)
227                }
228            }
229        }
230
231        clashes
232    }
233
234    /// Gets the decomposed [`BasicInputs`] for each binding mapped to the given action.
235    pub fn decomposed(&self, action: &A) -> Vec<BasicInputs> {
236        match action.input_control_kind() {
237            InputControlKind::Button => {
238                let Some(buttonlike) = self.get_buttonlike(action) else {
239                    return Vec::new();
240                };
241
242                buttonlike.iter().map(|input| input.decompose()).collect()
243            }
244            InputControlKind::Axis => {
245                let Some(axislike) = self.get_axislike(action) else {
246                    return Vec::new();
247                };
248
249                axislike.iter().map(|input| input.decompose()).collect()
250            }
251            InputControlKind::DualAxis => {
252                let Some(dual_axislike) = self.get_dual_axislike(action) else {
253                    return Vec::new();
254                };
255
256                dual_axislike
257                    .iter()
258                    .map(|input| input.decompose())
259                    .collect()
260            }
261            InputControlKind::TripleAxis => {
262                let Some(triple_axislike) = self.get_triple_axislike(action) else {
263                    return Vec::new();
264                };
265
266                triple_axislike
267                    .iter()
268                    .map(|input| input.decompose())
269                    .collect()
270            }
271        }
272    }
273
274    /// If the pair of actions could clash, how?
275    // FIXME: does not handle axis inputs. Should use the `decomposed` method instead of `get_buttonlike`
276    #[must_use]
277    fn possible_clash(&self, action_a: &A, action_b: &A) -> Option<Clash<A>> {
278        let mut clash = Clash::new(action_a.clone(), action_b.clone());
279
280        for input_a in self.get_buttonlike(action_a)? {
281            for input_b in self.get_buttonlike(action_b)? {
282                if input_a.decompose().clashes_with(&input_b.decompose()) {
283                    clash.inputs_a.push(input_a.clone());
284                    clash.inputs_b.push(input_b.clone());
285                }
286            }
287        }
288
289        let clashed = !clash.inputs_a.is_empty();
290        clashed.then_some(clash)
291    }
292}
293
294/// A user-input clash, which stores the actions that are being clashed on,
295/// as well as the corresponding user inputs
296#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
297pub(crate) struct Clash<A: Actionlike> {
298    action_a: A,
299    action_b: A,
300    inputs_a: Vec<Box<dyn Buttonlike>>,
301    inputs_b: Vec<Box<dyn Buttonlike>>,
302}
303
304impl<A: Actionlike> Clash<A> {
305    /// Creates a new clash between the two actions
306    #[must_use]
307    fn new(action_a: A, action_b: A) -> Self {
308        Self {
309            action_a,
310            action_b,
311            inputs_a: Vec::default(),
312            inputs_b: Vec::default(),
313        }
314    }
315}
316
317/// Given the `input_store`, does the provided clash actually occur?
318///
319/// Returns `Some(clash)` if they are clashing, and `None` if they are not.
320#[must_use]
321fn check_clash<A: Actionlike>(
322    clash: &Clash<A>,
323    input_store: &CentralInputStore,
324    gamepad: Entity,
325) -> Option<Clash<A>> {
326    let mut actual_clash: Clash<A> = clash.clone();
327
328    // For all inputs actually pressed that match action A
329    for input_a in clash
330        .inputs_a
331        .iter()
332        .filter(|&input| input.pressed(input_store, gamepad))
333    {
334        // For all inputs actually pressed that match action B
335        for input_b in clash
336            .inputs_b
337            .iter()
338            .filter(|&input| input.pressed(input_store, gamepad))
339        {
340            // If a clash was detected
341            if input_a.decompose().clashes_with(&input_b.decompose()) {
342                actual_clash.inputs_a.push(input_a.clone());
343                actual_clash.inputs_b.push(input_b.clone());
344            }
345        }
346    }
347
348    let clashed = !clash.inputs_a.is_empty();
349    clashed.then_some(actual_clash)
350}
351
352/// Which (if any) of the actions in the [`Clash`] should be discarded?
353#[must_use]
354fn resolve_clash<A: Actionlike>(
355    clash: &Clash<A>,
356    clash_strategy: ClashStrategy,
357    input_store: &CentralInputStore,
358    gamepad: Entity,
359) -> Option<A> {
360    // Figure out why the actions are pressed
361    let reasons_a_is_pressed: Vec<&dyn Buttonlike> = clash
362        .inputs_a
363        .iter()
364        .filter(|input| input.pressed(input_store, gamepad))
365        .map(|input| input.as_ref())
366        .collect();
367
368    let reasons_b_is_pressed: Vec<&dyn Buttonlike> = clash
369        .inputs_b
370        .iter()
371        .filter(|input| input.pressed(input_store, gamepad))
372        .map(|input| input.as_ref())
373        .collect();
374
375    // Clashes are spurious if the actions are pressed for any non-clashing reason
376    for reason_a in reasons_a_is_pressed.iter() {
377        for reason_b in reasons_b_is_pressed.iter() {
378            // If there is at least one non-clashing reason why these buttons should both be pressed,
379            // we can avoid resolving the clash completely
380            if !reason_a.decompose().clashes_with(&reason_b.decompose()) {
381                return None;
382            }
383        }
384    }
385
386    // There's a real clash; resolve it according to the `clash_strategy`
387    match clash_strategy {
388        // Do nothing
389        ClashStrategy::PressAll => None,
390        // Remove the clashing action with the shorter chord
391        ClashStrategy::PrioritizeLongest => {
392            let longest_a: usize = reasons_a_is_pressed
393                .iter()
394                .map(|input| input.decompose().len())
395                .reduce(|a, b| a.max(b))
396                .unwrap_or_default();
397
398            let longest_b: usize = reasons_b_is_pressed
399                .iter()
400                .map(|input| input.decompose().len())
401                .reduce(|a, b| a.max(b))
402                .unwrap_or_default();
403
404            match longest_a.cmp(&longest_b) {
405                Ordering::Greater => Some(clash.action_b.clone()),
406                Ordering::Less => Some(clash.action_a.clone()),
407                Ordering::Equal => None,
408            }
409        }
410    }
411}
412
413#[cfg(feature = "keyboard")]
414#[cfg(test)]
415mod tests {
416    use bevy::input::keyboard::KeyCode::*;
417    use bevy::prelude::Reflect;
418
419    use super::*;
420    use crate::prelude::{UserInput, VirtualDPad};
421    use crate::user_input::ButtonlikeChord;
422
423    use crate as leafwing_input_manager;
424
425    #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Hash, Debug, Reflect)]
426    enum Action {
427        One,
428        Two,
429        OneAndTwo,
430        TwoAndThree,
431        OneAndTwoAndThree,
432        CtrlOne,
433        AltOne,
434        CtrlAltOne,
435        CtrlUp,
436        #[actionlike(DualAxis)]
437        MoveDPad,
438    }
439
440    fn test_input_map() -> InputMap<Action> {
441        use Action::*;
442
443        let mut input_map = InputMap::default();
444
445        input_map.insert(One, Digit1);
446        input_map.insert(Two, Digit2);
447        input_map.insert(OneAndTwo, ButtonlikeChord::new([Digit1, Digit2]));
448        input_map.insert(TwoAndThree, ButtonlikeChord::new([Digit2, Digit3]));
449        input_map.insert(
450            OneAndTwoAndThree,
451            ButtonlikeChord::new([Digit1, Digit2, Digit3]),
452        );
453        input_map.insert(CtrlOne, ButtonlikeChord::new([ControlLeft, Digit1]));
454        input_map.insert(AltOne, ButtonlikeChord::new([AltLeft, Digit1]));
455        input_map.insert(
456            CtrlAltOne,
457            ButtonlikeChord::new([ControlLeft, AltLeft, Digit1]),
458        );
459        input_map.insert_dual_axis(MoveDPad, VirtualDPad::arrow_keys());
460        input_map.insert(CtrlUp, ButtonlikeChord::new([ControlLeft, ArrowUp]));
461
462        input_map
463    }
464
465    fn inputs_clash(input_a: impl UserInput, input_b: impl UserInput) -> bool {
466        let decomposed_a = input_a.decompose();
467        println!("{decomposed_a:?}");
468        let decomposed_b = input_b.decompose();
469        println!("{decomposed_b:?}");
470        let do_inputs_clash = decomposed_a.clashes_with(&decomposed_b);
471        println!("Clash: {do_inputs_clash}");
472        do_inputs_clash
473    }
474
475    mod basic_functionality {
476        use super::*;
477        use crate::{
478            input_map::UpdatedValue,
479            plugin::CentralInputStorePlugin,
480            prelude::{ModifierKey, VirtualDPad},
481        };
482        use bevy::{input::InputPlugin, prelude::*};
483        use Action::*;
484
485        #[test]
486        #[ignore = "Figuring out how to handle the length of chords with group inputs is out of scope."]
487        fn input_types_have_right_length() {
488            let simple = KeyA.decompose();
489            assert_eq!(simple.len(), 1);
490
491            let empty_chord = ButtonlikeChord::default().decompose();
492            assert_eq!(empty_chord.len(), 0);
493
494            let chord = ButtonlikeChord::new([KeyA, KeyB, KeyC]).decompose();
495            assert_eq!(chord.len(), 3);
496
497            let modifier = ModifierKey::Control.decompose();
498            assert_eq!(modifier.len(), 1);
499
500            let modified_chord = ButtonlikeChord::modified(ModifierKey::Control, KeyA).decompose();
501            assert_eq!(modified_chord.len(), 2);
502
503            let group = VirtualDPad::wasd().decompose();
504            assert_eq!(group.len(), 1);
505        }
506
507        #[test]
508        fn clash_detection() {
509            let a = KeyA;
510            let b = KeyB;
511            let c = KeyC;
512            let ab = ButtonlikeChord::new([KeyA, KeyB]);
513            let bc = ButtonlikeChord::new([KeyB, KeyC]);
514            let abc = ButtonlikeChord::new([KeyA, KeyB, KeyC]);
515            let axyz_dpad = VirtualDPad::new(KeyA, KeyX, KeyY, KeyZ);
516            let abcd_dpad = VirtualDPad::wasd();
517
518            let ctrl_up = ButtonlikeChord::new([ArrowUp, ControlLeft]);
519            let directions_dpad = VirtualDPad::arrow_keys();
520
521            assert!(!inputs_clash(a, b));
522            assert!(inputs_clash(a, ab.clone()));
523            assert!(!inputs_clash(c, ab.clone()));
524            assert!(!inputs_clash(ab.clone(), bc.clone()));
525            assert!(inputs_clash(ab.clone(), abc.clone()));
526            assert!(inputs_clash(axyz_dpad.clone(), a));
527            assert!(inputs_clash(axyz_dpad.clone(), ab.clone()));
528            assert!(!inputs_clash(axyz_dpad.clone(), bc.clone()));
529            assert!(inputs_clash(axyz_dpad.clone(), abcd_dpad.clone()));
530            assert!(inputs_clash(ctrl_up.clone(), directions_dpad.clone()));
531        }
532
533        #[test]
534        fn button_chord_clash_construction() {
535            let input_map = test_input_map();
536
537            let observed_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
538
539            let correct_clash = Clash {
540                action_a: One,
541                action_b: OneAndTwo,
542                inputs_a: vec![Box::new(Digit1)],
543                inputs_b: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2]))],
544            };
545
546            assert_eq!(observed_clash, correct_clash);
547        }
548
549        #[test]
550        fn chord_chord_clash_construction() {
551            let input_map = test_input_map();
552
553            let observed_clash = input_map
554                .possible_clash(&OneAndTwoAndThree, &OneAndTwo)
555                .unwrap();
556            let correct_clash = Clash {
557                action_a: OneAndTwoAndThree,
558                action_b: OneAndTwo,
559                inputs_a: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2, Digit3]))],
560                inputs_b: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2]))],
561            };
562
563            assert_eq!(observed_clash, correct_clash);
564        }
565
566        #[test]
567        fn can_clash() {
568            let input_map = test_input_map();
569
570            assert!(input_map.possible_clash(&One, &Two).is_none());
571            assert!(input_map.possible_clash(&One, &OneAndTwo).is_some());
572            assert!(input_map.possible_clash(&One, &OneAndTwoAndThree).is_some());
573            assert!(input_map.possible_clash(&One, &TwoAndThree).is_none());
574            assert!(input_map
575                .possible_clash(&OneAndTwo, &OneAndTwoAndThree)
576                .is_some());
577        }
578
579        #[test]
580        fn resolve_prioritize_longest() {
581            let mut app = App::new();
582            app.add_plugins((InputPlugin, CentralInputStorePlugin));
583
584            let input_map = test_input_map();
585            let simple_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
586            Digit1.press(app.world_mut());
587            Digit2.press(app.world_mut());
588            app.update();
589
590            let gamepad = app.world_mut().spawn(()).id();
591            let input_store = app.world().resource::<CentralInputStore>();
592
593            assert_eq!(
594                resolve_clash(
595                    &simple_clash,
596                    ClashStrategy::PrioritizeLongest,
597                    input_store,
598                    gamepad,
599                ),
600                Some(One)
601            );
602
603            let reversed_clash = input_map.possible_clash(&OneAndTwo, &One).unwrap();
604            let input_store = app.world().resource::<CentralInputStore>();
605
606            assert_eq!(
607                resolve_clash(
608                    &reversed_clash,
609                    ClashStrategy::PrioritizeLongest,
610                    input_store,
611                    gamepad,
612                ),
613                Some(One)
614            );
615
616            let chord_clash = input_map
617                .possible_clash(&OneAndTwo, &OneAndTwoAndThree)
618                .unwrap();
619            Digit3.press(app.world_mut());
620            app.update();
621
622            let input_store = app.world().resource::<CentralInputStore>();
623
624            assert_eq!(
625                resolve_clash(
626                    &chord_clash,
627                    ClashStrategy::PrioritizeLongest,
628                    input_store,
629                    gamepad,
630                ),
631                Some(OneAndTwo)
632            );
633        }
634
635        #[test]
636        fn handle_simple_clash() {
637            let mut app = App::new();
638            app.add_plugins((InputPlugin, CentralInputStorePlugin));
639            let input_map = test_input_map();
640            let gamepad = app.world_mut().spawn(()).id();
641
642            Digit1.press(app.world_mut());
643            Digit2.press(app.world_mut());
644            app.update();
645
646            let mut updated_actions = UpdatedActions::default();
647
648            updated_actions.insert(One, UpdatedValue::Button(true));
649            updated_actions.insert(Two, UpdatedValue::Button(true));
650            updated_actions.insert(OneAndTwo, UpdatedValue::Button(true));
651
652            let input_store = app.world().resource::<CentralInputStore>();
653
654            input_map.handle_clashes(
655                &mut updated_actions,
656                input_store,
657                ClashStrategy::PrioritizeLongest,
658                gamepad,
659            );
660
661            let mut expected = UpdatedActions::default();
662            expected.insert(OneAndTwo, UpdatedValue::Button(true));
663
664            assert_eq!(updated_actions, expected);
665        }
666
667        // Checks that a clash between a VirtualDPad and a chord chooses the chord
668        #[test]
669        #[ignore = "Clashing inputs for non-buttonlike inputs is broken."]
670        fn handle_clashes_dpad_chord() {
671            let mut app = App::new();
672            app.add_plugins(InputPlugin);
673            let input_map = test_input_map();
674            let gamepad = app.world_mut().spawn(()).id();
675
676            ControlLeft.press(app.world_mut());
677            ArrowUp.press(app.world_mut());
678            app.update();
679
680            // Both the DPad and the chord are pressed,
681            // because we've sent the inputs for both
682            let mut updated_actions = UpdatedActions::default();
683            updated_actions.insert(CtrlUp, UpdatedValue::Button(true));
684            updated_actions.insert(MoveDPad, UpdatedValue::Button(true));
685
686            // Double-check that the two input bindings clash
687            let chord_input = input_map.get_buttonlike(&CtrlUp).unwrap().first().unwrap();
688            let dpad_input = input_map
689                .get_dual_axislike(&MoveDPad)
690                .unwrap()
691                .first()
692                .unwrap();
693
694            assert!(chord_input
695                .decompose()
696                .clashes_with(&dpad_input.decompose()));
697
698            // Triple check that the inputs are clashing
699            input_map
700                .possible_clash(&CtrlUp, &MoveDPad)
701                .expect("Clash not detected");
702
703            // Double check that the chord is longer than the DPad
704            assert!(chord_input.decompose().len() > dpad_input.decompose().len());
705
706            let input_store = app.world().resource::<CentralInputStore>();
707
708            input_map.handle_clashes(
709                &mut updated_actions,
710                input_store,
711                ClashStrategy::PrioritizeLongest,
712                gamepad,
713            );
714
715            // Only the chord should be pressed,
716            // because it is longer than the DPad
717            let mut expected = UpdatedActions::default();
718            expected.insert(CtrlUp, UpdatedValue::Button(true));
719
720            assert_eq!(updated_actions, expected);
721        }
722
723        #[test]
724        fn check_which_pressed() {
725            let mut app = App::new();
726            app.add_plugins((InputPlugin, CentralInputStorePlugin));
727            let input_map = test_input_map();
728
729            Digit1.press(app.world_mut());
730            Digit2.press(app.world_mut());
731            ControlLeft.press(app.world_mut());
732            app.update();
733
734            let input_store = app.world().resource::<CentralInputStore>();
735
736            let action_data =
737                input_map.process_actions(None, input_store, ClashStrategy::PrioritizeLongest);
738
739            for (action, &updated_value) in action_data.iter() {
740                if *action == CtrlOne || *action == OneAndTwo {
741                    assert_eq!(updated_value, UpdatedValue::Button(true));
742                } else {
743                    match updated_value {
744                        UpdatedValue::Button(pressed) => assert!(!pressed),
745                        UpdatedValue::Axis(value) => assert_eq!(value, 0.0),
746                        UpdatedValue::DualAxis(pair) => assert_eq!(pair, Vec2::ZERO),
747                        UpdatedValue::TripleAxis(triple) => assert_eq!(triple, Vec3::ZERO),
748                    }
749                }
750            }
751        }
752    }
753}