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 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 #[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 let mut updated_actions = UpdatedActions::default();
683 updated_actions.insert(CtrlUp, UpdatedValue::Button(true));
684 updated_actions.insert(MoveDPad, UpdatedValue::Button(true));
685
686 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 input_map
700 .possible_clash(&CtrlUp, &MoveDPad)
701 .expect("Clash not detected");
702
703 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 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}