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            buttonlike::ButtonValue,
479            input_map::UpdatedValue,
480            plugin::CentralInputStorePlugin,
481            prelude::{ModifierKey, VirtualDPad},
482        };
483        use bevy::{input::InputPlugin, prelude::*};
484        use Action::*;
485
486        #[test]
487        #[ignore = "Figuring out how to handle the length of chords with group inputs is out of scope."]
488        fn input_types_have_right_length() {
489            let simple = KeyA.decompose();
490            assert_eq!(simple.len(), 1);
491
492            let empty_chord = ButtonlikeChord::default().decompose();
493            assert_eq!(empty_chord.len(), 0);
494
495            let chord = ButtonlikeChord::new([KeyA, KeyB, KeyC]).decompose();
496            assert_eq!(chord.len(), 3);
497
498            let modifier = ModifierKey::Control.decompose();
499            assert_eq!(modifier.len(), 1);
500
501            let modified_chord = ButtonlikeChord::modified(ModifierKey::Control, KeyA).decompose();
502            assert_eq!(modified_chord.len(), 2);
503
504            let group = VirtualDPad::wasd().decompose();
505            assert_eq!(group.len(), 1);
506        }
507
508        #[test]
509        fn clash_detection() {
510            let a = KeyA;
511            let b = KeyB;
512            let c = KeyC;
513            let ab = ButtonlikeChord::new([KeyA, KeyB]);
514            let bc = ButtonlikeChord::new([KeyB, KeyC]);
515            let abc = ButtonlikeChord::new([KeyA, KeyB, KeyC]);
516            let axyz_dpad = VirtualDPad::new(KeyA, KeyX, KeyY, KeyZ);
517            let abcd_dpad = VirtualDPad::wasd();
518
519            let ctrl_up = ButtonlikeChord::new([ArrowUp, ControlLeft]);
520            let directions_dpad = VirtualDPad::arrow_keys();
521
522            assert!(!inputs_clash(a, b));
523            assert!(inputs_clash(a, ab.clone()));
524            assert!(!inputs_clash(c, ab.clone()));
525            assert!(!inputs_clash(ab.clone(), bc.clone()));
526            assert!(inputs_clash(ab.clone(), abc.clone()));
527            assert!(inputs_clash(axyz_dpad.clone(), a));
528            assert!(inputs_clash(axyz_dpad.clone(), ab.clone()));
529            assert!(!inputs_clash(axyz_dpad.clone(), bc.clone()));
530            assert!(inputs_clash(axyz_dpad.clone(), abcd_dpad.clone()));
531            assert!(inputs_clash(ctrl_up.clone(), directions_dpad.clone()));
532        }
533
534        #[test]
535        fn button_chord_clash_construction() {
536            let input_map = test_input_map();
537
538            let observed_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
539
540            let correct_clash = Clash {
541                action_a: One,
542                action_b: OneAndTwo,
543                inputs_a: vec![Box::new(Digit1)],
544                inputs_b: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2]))],
545            };
546
547            assert_eq!(observed_clash, correct_clash);
548        }
549
550        #[test]
551        fn chord_chord_clash_construction() {
552            let input_map = test_input_map();
553
554            let observed_clash = input_map
555                .possible_clash(&OneAndTwoAndThree, &OneAndTwo)
556                .unwrap();
557            let correct_clash = Clash {
558                action_a: OneAndTwoAndThree,
559                action_b: OneAndTwo,
560                inputs_a: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2, Digit3]))],
561                inputs_b: vec![Box::new(ButtonlikeChord::new([Digit1, Digit2]))],
562            };
563
564            assert_eq!(observed_clash, correct_clash);
565        }
566
567        #[test]
568        fn can_clash() {
569            let input_map = test_input_map();
570
571            assert!(input_map.possible_clash(&One, &Two).is_none());
572            assert!(input_map.possible_clash(&One, &OneAndTwo).is_some());
573            assert!(input_map.possible_clash(&One, &OneAndTwoAndThree).is_some());
574            assert!(input_map.possible_clash(&One, &TwoAndThree).is_none());
575            assert!(input_map
576                .possible_clash(&OneAndTwo, &OneAndTwoAndThree)
577                .is_some());
578        }
579
580        #[test]
581        fn resolve_prioritize_longest() {
582            let mut app = App::new();
583            app.add_plugins((InputPlugin, CentralInputStorePlugin));
584
585            let input_map = test_input_map();
586            let simple_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
587            Digit1.press(app.world_mut());
588            Digit2.press(app.world_mut());
589            app.update();
590
591            let gamepad = app.world_mut().spawn(()).id();
592            let input_store = app.world().resource::<CentralInputStore>();
593
594            assert_eq!(
595                resolve_clash(
596                    &simple_clash,
597                    ClashStrategy::PrioritizeLongest,
598                    input_store,
599                    gamepad,
600                ),
601                Some(One)
602            );
603
604            let reversed_clash = input_map.possible_clash(&OneAndTwo, &One).unwrap();
605            let input_store = app.world().resource::<CentralInputStore>();
606
607            assert_eq!(
608                resolve_clash(
609                    &reversed_clash,
610                    ClashStrategy::PrioritizeLongest,
611                    input_store,
612                    gamepad,
613                ),
614                Some(One)
615            );
616
617            let chord_clash = input_map
618                .possible_clash(&OneAndTwo, &OneAndTwoAndThree)
619                .unwrap();
620            Digit3.press(app.world_mut());
621            app.update();
622
623            let input_store = app.world().resource::<CentralInputStore>();
624
625            assert_eq!(
626                resolve_clash(
627                    &chord_clash,
628                    ClashStrategy::PrioritizeLongest,
629                    input_store,
630                    gamepad,
631                ),
632                Some(OneAndTwo)
633            );
634        }
635
636        #[test]
637        fn handle_simple_clash() {
638            let mut app = App::new();
639            app.add_plugins((InputPlugin, CentralInputStorePlugin));
640            let input_map = test_input_map();
641            let gamepad = app.world_mut().spawn(()).id();
642
643            Digit1.press(app.world_mut());
644            Digit2.press(app.world_mut());
645            app.update();
646
647            let mut updated_actions = UpdatedActions::default();
648
649            updated_actions.insert(
650                One,
651                UpdatedValue::Button(ButtonValue {
652                    pressed: true,
653                    value: 1.0,
654                }),
655            );
656            updated_actions.insert(
657                Two,
658                UpdatedValue::Button(ButtonValue {
659                    pressed: true,
660                    value: 1.0,
661                }),
662            );
663            updated_actions.insert(
664                OneAndTwo,
665                UpdatedValue::Button(ButtonValue {
666                    pressed: true,
667                    value: 1.0,
668                }),
669            );
670
671            let input_store = app.world().resource::<CentralInputStore>();
672
673            input_map.handle_clashes(
674                &mut updated_actions,
675                input_store,
676                ClashStrategy::PrioritizeLongest,
677                gamepad,
678            );
679
680            let mut expected = UpdatedActions::default();
681            expected.insert(
682                OneAndTwo,
683                UpdatedValue::Button(ButtonValue {
684                    pressed: true,
685                    value: 1.0,
686                }),
687            );
688
689            assert_eq!(updated_actions, expected);
690        }
691
692        // Checks that a clash between a VirtualDPad and a chord chooses the chord
693        #[test]
694        #[ignore = "Clashing inputs for non-buttonlike inputs is broken."]
695        fn handle_clashes_dpad_chord() {
696            let mut app = App::new();
697            app.add_plugins(InputPlugin);
698            let input_map = test_input_map();
699            let gamepad = app.world_mut().spawn(()).id();
700
701            ControlLeft.press(app.world_mut());
702            ArrowUp.press(app.world_mut());
703            app.update();
704
705            // Both the DPad and the chord are pressed,
706            // because we've sent the inputs for both
707            let mut updated_actions = UpdatedActions::default();
708            updated_actions.insert(
709                CtrlUp,
710                UpdatedValue::Button(ButtonValue {
711                    pressed: true,
712                    value: 1.0,
713                }),
714            );
715            updated_actions.insert(
716                MoveDPad,
717                UpdatedValue::Button(ButtonValue {
718                    pressed: true,
719                    value: 1.0,
720                }),
721            );
722
723            // Double-check that the two input bindings clash
724            let chord_input = input_map.get_buttonlike(&CtrlUp).unwrap().first().unwrap();
725            let dpad_input = input_map
726                .get_dual_axislike(&MoveDPad)
727                .unwrap()
728                .first()
729                .unwrap();
730
731            assert!(chord_input
732                .decompose()
733                .clashes_with(&dpad_input.decompose()));
734
735            // Triple check that the inputs are clashing
736            input_map
737                .possible_clash(&CtrlUp, &MoveDPad)
738                .expect("Clash not detected");
739
740            // Double check that the chord is longer than the DPad
741            assert!(chord_input.decompose().len() > dpad_input.decompose().len());
742
743            let input_store = app.world().resource::<CentralInputStore>();
744
745            input_map.handle_clashes(
746                &mut updated_actions,
747                input_store,
748                ClashStrategy::PrioritizeLongest,
749                gamepad,
750            );
751
752            // Only the chord should be pressed,
753            // because it is longer than the DPad
754            let mut expected = UpdatedActions::default();
755            expected.insert(
756                CtrlUp,
757                UpdatedValue::Button(ButtonValue {
758                    pressed: true,
759                    value: 1.0,
760                }),
761            );
762
763            assert_eq!(updated_actions, expected);
764        }
765
766        #[test]
767        fn check_which_pressed() {
768            let mut app = App::new();
769            app.add_plugins((InputPlugin, CentralInputStorePlugin));
770            let input_map = test_input_map();
771
772            Digit1.press(app.world_mut());
773            Digit2.press(app.world_mut());
774            ControlLeft.press(app.world_mut());
775            app.update();
776
777            let input_store = app.world().resource::<CentralInputStore>();
778
779            let action_data =
780                input_map.process_actions(None, input_store, ClashStrategy::PrioritizeLongest);
781
782            for (action, &updated_value) in action_data.iter() {
783                if *action == CtrlOne || *action == OneAndTwo {
784                    assert_eq!(
785                        updated_value,
786                        UpdatedValue::Button(ButtonValue {
787                            pressed: true,
788                            value: 1.0,
789                        })
790                    );
791                } else {
792                    match updated_value {
793                        UpdatedValue::Button(value) => assert_eq!(
794                            value,
795                            ButtonValue {
796                                pressed: false,
797                                value: 0.0,
798                            }
799                        ),
800                        UpdatedValue::Axis(value) => assert_eq!(value, 0.0),
801                        UpdatedValue::DualAxis(pair) => assert_eq!(pair, Vec2::ZERO),
802                        UpdatedValue::TripleAxis(triple) => assert_eq!(triple, Vec3::ZERO),
803                    }
804                }
805            }
806        }
807    }
808}