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 Action::*;
484 use bevy::{input::InputPlugin, prelude::*};
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!(
576 input_map
577 .possible_clash(&OneAndTwo, &OneAndTwoAndThree)
578 .is_some()
579 );
580 }
581
582 #[test]
583 fn resolve_prioritize_longest() {
584 let mut app = App::new();
585 app.add_plugins((InputPlugin, CentralInputStorePlugin));
586
587 let input_map = test_input_map();
588 let simple_clash = input_map.possible_clash(&One, &OneAndTwo).unwrap();
589 Digit1.press(app.world_mut());
590 Digit2.press(app.world_mut());
591 app.update();
592
593 let gamepad = app.world_mut().spawn(()).id();
594 let input_store = app.world().resource::<CentralInputStore>();
595
596 assert_eq!(
597 resolve_clash(
598 &simple_clash,
599 ClashStrategy::PrioritizeLongest,
600 input_store,
601 gamepad,
602 ),
603 Some(One)
604 );
605
606 let reversed_clash = input_map.possible_clash(&OneAndTwo, &One).unwrap();
607 let input_store = app.world().resource::<CentralInputStore>();
608
609 assert_eq!(
610 resolve_clash(
611 &reversed_clash,
612 ClashStrategy::PrioritizeLongest,
613 input_store,
614 gamepad,
615 ),
616 Some(One)
617 );
618
619 let chord_clash = input_map
620 .possible_clash(&OneAndTwo, &OneAndTwoAndThree)
621 .unwrap();
622 Digit3.press(app.world_mut());
623 app.update();
624
625 let input_store = app.world().resource::<CentralInputStore>();
626
627 assert_eq!(
628 resolve_clash(
629 &chord_clash,
630 ClashStrategy::PrioritizeLongest,
631 input_store,
632 gamepad,
633 ),
634 Some(OneAndTwo)
635 );
636 }
637
638 #[test]
639 fn handle_simple_clash() {
640 let mut app = App::new();
641 app.add_plugins((InputPlugin, CentralInputStorePlugin));
642 let input_map = test_input_map();
643 let gamepad = app.world_mut().spawn(()).id();
644
645 Digit1.press(app.world_mut());
646 Digit2.press(app.world_mut());
647 app.update();
648
649 let mut updated_actions = UpdatedActions::default();
650
651 updated_actions.insert(
652 One,
653 UpdatedValue::Button(ButtonValue {
654 pressed: true,
655 value: 1.0,
656 }),
657 );
658 updated_actions.insert(
659 Two,
660 UpdatedValue::Button(ButtonValue {
661 pressed: true,
662 value: 1.0,
663 }),
664 );
665 updated_actions.insert(
666 OneAndTwo,
667 UpdatedValue::Button(ButtonValue {
668 pressed: true,
669 value: 1.0,
670 }),
671 );
672
673 let input_store = app.world().resource::<CentralInputStore>();
674
675 input_map.handle_clashes(
676 &mut updated_actions,
677 input_store,
678 ClashStrategy::PrioritizeLongest,
679 gamepad,
680 );
681
682 let mut expected = UpdatedActions::default();
683 expected.insert(
684 OneAndTwo,
685 UpdatedValue::Button(ButtonValue {
686 pressed: true,
687 value: 1.0,
688 }),
689 );
690
691 assert_eq!(updated_actions, expected);
692 }
693
694 #[test]
696 #[ignore = "Clashing inputs for non-buttonlike inputs is broken."]
697 fn handle_clashes_dpad_chord() {
698 let mut app = App::new();
699 app.add_plugins(InputPlugin);
700 let input_map = test_input_map();
701 let gamepad = app.world_mut().spawn(()).id();
702
703 ControlLeft.press(app.world_mut());
704 ArrowUp.press(app.world_mut());
705 app.update();
706
707 let mut updated_actions = UpdatedActions::default();
710 updated_actions.insert(
711 CtrlUp,
712 UpdatedValue::Button(ButtonValue {
713 pressed: true,
714 value: 1.0,
715 }),
716 );
717 updated_actions.insert(
718 MoveDPad,
719 UpdatedValue::Button(ButtonValue {
720 pressed: true,
721 value: 1.0,
722 }),
723 );
724
725 let chord_input = input_map.get_buttonlike(&CtrlUp).unwrap().first().unwrap();
727 let dpad_input = input_map
728 .get_dual_axislike(&MoveDPad)
729 .unwrap()
730 .first()
731 .unwrap();
732
733 assert!(
734 chord_input
735 .decompose()
736 .clashes_with(&dpad_input.decompose())
737 );
738
739 input_map
741 .possible_clash(&CtrlUp, &MoveDPad)
742 .expect("Clash not detected");
743
744 assert!(chord_input.decompose().len() > dpad_input.decompose().len());
746
747 let input_store = app.world().resource::<CentralInputStore>();
748
749 input_map.handle_clashes(
750 &mut updated_actions,
751 input_store,
752 ClashStrategy::PrioritizeLongest,
753 gamepad,
754 );
755
756 let mut expected = UpdatedActions::default();
759 expected.insert(
760 CtrlUp,
761 UpdatedValue::Button(ButtonValue {
762 pressed: true,
763 value: 1.0,
764 }),
765 );
766
767 assert_eq!(updated_actions, expected);
768 }
769
770 #[test]
771 fn check_which_pressed() {
772 let mut app = App::new();
773 app.add_plugins((InputPlugin, CentralInputStorePlugin));
774 let input_map = test_input_map();
775
776 Digit1.press(app.world_mut());
777 Digit2.press(app.world_mut());
778 ControlLeft.press(app.world_mut());
779 app.update();
780
781 let input_store = app.world().resource::<CentralInputStore>();
782
783 let action_data =
784 input_map.process_actions(None, input_store, ClashStrategy::PrioritizeLongest);
785
786 for (action, &updated_value) in action_data.iter() {
787 if *action == CtrlOne || *action == OneAndTwo {
788 assert_eq!(
789 updated_value,
790 UpdatedValue::Button(ButtonValue {
791 pressed: true,
792 value: 1.0,
793 })
794 );
795 } else {
796 match updated_value {
797 UpdatedValue::Button(value) => assert_eq!(
798 value,
799 ButtonValue {
800 pressed: false,
801 value: 0.0,
802 }
803 ),
804 UpdatedValue::Axis(value) => assert_eq!(value, 0.0),
805 UpdatedValue::DualAxis(pair) => assert_eq!(pair, Vec2::ZERO),
806 UpdatedValue::TripleAxis(triple) => assert_eq!(triple, Vec3::ZERO),
807 }
808 }
809 }
810 }
811 }
812}