1use 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#[non_exhaustive]
31#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default)]
32pub enum ClashStrategy {
33 PressAll,
35 #[default]
39 PrioritizeLongest,
40}
41
42impl ClashStrategy {
43 pub fn variants() -> &'static [ClashStrategy] {
45 use ClashStrategy::*;
46
47 &[PressAll, PrioritizeLongest]
48 }
49}
50
51#[derive(Debug, Clone)]
56#[must_use]
57pub enum BasicInputs {
58 None,
62
63 Simple(Box<dyn Buttonlike>),
67
68 Composite(Vec<Box<dyn Buttonlike>>),
73
74 Chord(Vec<Box<dyn Buttonlike>>),
78}
79
80impl BasicInputs {
81 #[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 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 #[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 #[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 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 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 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 #[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 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 if pressed_a && pressed_b {
224 if let Some(clash) = check_clash(&clash, input_store, gamepad) {
226 clashes.push(clash)
227 }
228 }
229 }
230
231 clashes
232 }
233
234 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 #[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#[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 #[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#[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 input_a in clash
330 .inputs_a
331 .iter()
332 .filter(|&input| input.pressed(input_store, gamepad))
333 {
334 for input_b in clash
336 .inputs_b
337 .iter()
338 .filter(|&input| input.pressed(input_store, gamepad))
339 {
340 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#[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 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 for reason_a in reasons_a_is_pressed.iter() {
377 for reason_b in reasons_b_is_pressed.iter() {
378 if !reason_a.decompose().clashes_with(&reason_b.decompose()) {
381 return None;
382 }
383 }
384 }
385
386 match clash_strategy {
388 ClashStrategy::PressAll => None,
390 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 #[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 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 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 input_map
737 .possible_clash(&CtrlUp, &MoveDPad)
738 .expect("Clash not detected");
739
740 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 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}