1use std::collections::{HashMap, HashSet};
110
111use crcbl_core::input::{KeyCode, PointerButton};
112
113use super::{
114 ActionMap, ActionMapError, Binding, HeldKeys, Modifier, PadButton, PadButtons, Stick, Trigger,
115};
116use crate::trace::TracedInput;
117
118pub const GAMEPLAY_CONTEXT: &str = "gameplay";
121
122pub const GLOBAL_CONTEXT: &str = "global";
129
130const GLOBAL_INDEX: usize = 1;
133
134#[derive(Debug, Default, Clone, PartialEq, Eq)]
137pub(crate) struct Routes {
138 keys: HashMap<KeyCode, usize>,
139 chords: HashMap<KeyCode, Vec<Modifier>>,
142 pad_chords: HashMap<PadButton, Vec<PadButton>>,
145 outranking_pad_chords: HashMap<PadButton, Vec<(PadButton, usize)>>,
149 button_chords: HashMap<PointerButton, Vec<Modifier>>,
152 buttons: HashMap<PointerButton, usize>,
153 controls: HashMap<String, usize>,
154 motion: Option<usize>,
155 scroll: Option<usize>,
156 scroll_chords: Vec<KeyCode>,
159 pub(crate) pointer: Option<usize>,
160 pad_buttons: HashMap<PadButton, usize>,
161 pad_sticks: HashMap<Stick, usize>,
162 pad_triggers: HashMap<Trigger, usize>,
163}
164
165#[derive(Debug, Default, Clone)]
167pub(crate) struct Suppressed {
168 pub(crate) keys: HashSet<KeyCode>,
169 pub(crate) buttons: HashSet<PointerButton>,
170 pub(crate) controls: HashSet<String>,
171 pub(crate) pad_buttons: PadButtons,
174 pub(crate) control_sticks: HashSet<String>,
177 pub(crate) pad_sticks: HashSet<Stick>,
178 pub(crate) pad_triggers: HashSet<Trigger>,
179}
180
181impl Suppressed {
182 fn extend(&mut self, other: Self) {
184 self.keys.extend(other.keys);
185 self.buttons.extend(other.buttons);
186 self.controls.extend(other.controls);
187 self.pad_buttons = self.pad_buttons.union(other.pad_buttons);
188 self.control_sticks.extend(other.control_sticks);
189 self.pad_sticks.extend(other.pad_sticks);
190 self.pad_triggers.extend(other.pad_triggers);
191 }
192
193 fn withholds(&self, binding: &Binding) -> bool {
195 let mut key = false;
196 binding.visit_keys(|owned| key |= self.keys.contains(&owned));
197 key || match binding {
198 Binding::ScrollChord { held } => self.keys.contains(held),
199 Binding::MouseButton(button) | Binding::ButtonChord { button, .. } => {
200 self.buttons.contains(button)
201 }
202 Binding::Virtual(id) => {
203 self.controls.contains(id.as_str()) || self.control_sticks.contains(id.as_str())
204 }
205 Binding::PadButton(button) | Binding::PadChord { button, .. } => {
206 self.pad_buttons.contains(*button)
207 }
208 Binding::PadDpad => PadButton::DPAD
209 .iter()
210 .any(|&button| self.pad_buttons.contains(button)),
211 Binding::PadStick { stick, .. } => self.pad_sticks.contains(stick),
212 Binding::PadTrigger { trigger, .. } => self.pad_triggers.contains(trigger),
213 Binding::Key(_)
214 | Binding::KeyAxis { .. }
215 | Binding::Chord { .. }
216 | Binding::Wasd { .. }
217 | Binding::MouseMotion
218 | Binding::MouseScroll
219 | Binding::PointerPosition { .. } => false,
220 }
221 }
222
223 pub(crate) fn holds(&self, input: &TracedInput) -> bool {
227 match input {
228 TracedInput::Key(key) => self.keys.contains(key),
229 TracedInput::MouseButton(button) => self.buttons.contains(button),
230 TracedInput::Wheel => false,
231 TracedInput::Control(id) => self.controls.contains(id.as_str()),
232 TracedInput::ControlStick(id) => self.control_sticks.contains(id.as_str()),
233 TracedInput::PadButton(button) => self.pad_buttons.contains(*button),
234 TracedInput::PadStick(stick) => self.pad_sticks.contains(stick),
235 TracedInput::PadTrigger(trigger) => self.pad_triggers.contains(trigger),
236 }
237 }
238}
239
240pub(crate) struct View<'a> {
242 pub(crate) context: usize,
243 pub(crate) routes: &'a Routes,
244 pub(crate) suppressed: &'a Suppressed,
245 pub(crate) held_keys: &'a HeldKeys,
246 pub(crate) held_buttons: &'a HashSet<PointerButton>,
247 pub(crate) held_controls: &'a HashSet<String>,
248 pub(crate) held_pad_buttons: PadButtons,
250}
251
252impl View<'_> {
253 fn owns_held(&self, key: KeyCode) -> bool {
255 self.held_keys.contains_key(&key)
256 && !self.suppressed.keys.contains(&key)
257 && self.routes.keys.get(&key) == Some(&self.context)
258 }
259
260 pub(crate) fn key(&self, key: KeyCode) -> bool {
262 self.owns_held(key) && !self.any_held(self.routes.chords.get(&key))
263 }
264
265 fn any_held(&self, modifiers: Option<&Vec<Modifier>>) -> bool {
270 modifiers.is_some_and(|modifiers| modifiers.iter().any(|m| m.held(self.held_keys)))
271 }
272
273 pub(crate) fn chord(&self, modifier: Modifier, key: KeyCode) -> bool {
275 self.owns_held(key) && modifier.held(self.held_keys)
276 }
277
278 fn owns_held_button(&self, button: PointerButton) -> bool {
281 self.held_buttons.contains(&button)
282 && !self.suppressed.buttons.contains(&button)
283 && self.routes.buttons.get(&button) == Some(&self.context)
284 }
285
286 pub(crate) fn button(&self, button: PointerButton) -> bool {
289 self.owns_held_button(button) && !self.any_held(self.routes.button_chords.get(&button))
290 }
291
292 pub(crate) fn button_chord(&self, modifier: Modifier, button: PointerButton) -> bool {
295 self.owns_held_button(button) && modifier.held(self.held_keys)
296 }
297
298 pub(crate) fn control(&self, id: &str) -> bool {
299 self.held_controls.contains(id)
300 && !self.suppressed.controls.contains(id)
301 && self.owns_control(id)
302 }
303
304 pub(crate) fn stick(&self, id: &str) -> bool {
307 self.owns_control(id) && !self.suppressed.control_sticks.contains(id)
308 }
309
310 fn owns_control(&self, id: &str) -> bool {
311 self.routes.controls.get(id) == Some(&self.context)
312 }
313
314 pub(crate) fn motion(&self) -> bool {
315 self.routes.motion == Some(self.context)
316 }
317
318 pub(crate) fn scroll(&self) -> bool {
321 self.routes.scroll == Some(self.context) && self.wheel_key().is_none()
322 }
323
324 pub(crate) fn scroll_chord(&self, held: KeyCode) -> bool {
327 self.routes.scroll == Some(self.context) && self.wheel_key() == Some(held)
328 }
329
330 fn wheel_key(&self) -> Option<KeyCode> {
333 self.routes
334 .scroll_chords
335 .iter()
336 .filter(|key| !self.suppressed.keys.contains(key))
337 .filter_map(|&key| self.held_keys.get(&key).map(|&serial| (serial, key)))
338 .max()
339 .map(|(_, key)| key)
340 }
341
342 pub(crate) fn pointer(&self) -> bool {
343 self.routes.pointer == Some(self.context)
344 }
345
346 fn owns_held_pad_button(&self, button: PadButton) -> bool {
349 self.held_pad_buttons.contains(button)
350 && !self.suppressed.pad_buttons.contains(button)
351 && self.routes.pad_reader(button, self.held_pad_buttons) == Some(self.context)
352 }
353
354 pub(crate) fn pad_button(&self, button: PadButton) -> bool {
357 self.owns_held_pad_button(button)
358 && !self
359 .routes
360 .pad_chords
361 .get(&button)
362 .is_some_and(|modifiers| {
363 modifiers
364 .iter()
365 .any(|&modifier| self.held_pad_buttons.contains(modifier))
366 })
367 }
368
369 pub(crate) fn pad_chord(&self, modifier: PadButton, button: PadButton) -> bool {
372 self.owns_held_pad_button(button)
373 && self.held_pad_buttons.contains(modifier)
374 && (self.routes.pad_buttons.get(&button) == Some(&self.context)
375 || self.routes.outranks(button, modifier, self.context))
376 }
377
378 pub(crate) fn pad_stick(&self, stick: Stick) -> bool {
381 self.routes.pad_sticks.get(&stick) == Some(&self.context)
382 && !self.suppressed.pad_sticks.contains(&stick)
383 }
384
385 pub(crate) fn trigger(&self, trigger: Trigger) -> bool {
388 self.routes.pad_triggers.get(&trigger) == Some(&self.context)
389 && !self.suppressed.pad_triggers.contains(&trigger)
390 }
391}
392
393impl Routes {
394 pub(crate) fn is_scroll_chord_key(&self, key: KeyCode) -> bool {
396 self.scroll_chords.contains(&key)
397 }
398
399 pub(crate) fn owner(&self, input: &TracedInput, held: PadButtons) -> Option<usize> {
403 match input {
404 TracedInput::Key(key) => self.keys.get(key).copied(),
405 TracedInput::MouseButton(button) => self.buttons.get(button).copied(),
406 TracedInput::Wheel => self.scroll,
407 TracedInput::Control(id) | TracedInput::ControlStick(id) => {
408 self.controls.get(id.as_str()).copied()
409 }
410 TracedInput::PadButton(button) => self.pad_reader(*button, held),
411 TracedInput::PadStick(stick) => self.pad_sticks.get(stick).copied(),
412 TracedInput::PadTrigger(trigger) => self.pad_triggers.get(trigger).copied(),
413 }
414 }
415
416 pub(crate) fn pad_reader(&self, button: PadButton, held: PadButtons) -> Option<usize> {
422 self.outranking_pad_chords
423 .get(&button)
424 .and_then(|chords| {
425 chords
426 .iter()
427 .find(|&&(modifier, _)| held.contains(modifier))
428 })
429 .map(|&(_, context)| context)
430 .or_else(|| self.pad_buttons.get(&button).copied())
431 }
432
433 fn outranks(&self, button: PadButton, modifier: PadButton, context: usize) -> bool {
436 self.outranking_pad_chords
437 .get(&button)
438 .is_some_and(|chords| chords.contains(&(modifier, context)))
439 }
440
441 fn build(map: &ActionMap) -> Self {
444 let mut routes = Self::default();
445 let mut below_modal = false;
448 let mut pad_chords_above = HashSet::new();
451 for context in map.route_order() {
452 let bindings = || {
453 map.slots
454 .iter()
455 .filter(move |slot| slot.context == context)
456 .flat_map(|slot| slot.decl.bindings.iter())
457 };
458 if below_modal {
459 for binding in bindings() {
460 match binding {
461 Binding::MouseMotion => {
462 routes.motion.get_or_insert(context);
463 }
464 Binding::PointerPosition { .. } => {
465 routes.pointer.get_or_insert(context);
466 }
467 _ => {}
468 }
469 }
470 continue;
471 }
472 for binding in bindings() {
473 binding.visit_keys(|key| {
474 routes.keys.entry(key).or_insert(context);
475 });
476 match binding {
477 Binding::MouseButton(button) | Binding::ButtonChord { button, .. } => {
478 routes.buttons.entry(*button).or_insert(context);
479 }
480 Binding::Virtual(id) => {
481 if !routes.controls.contains_key(id.as_str()) {
482 routes.controls.insert(id.clone(), context);
483 }
484 }
485 Binding::MouseMotion => {
486 routes.motion.get_or_insert(context);
487 }
488 Binding::MouseScroll | Binding::ScrollChord { .. } => {
489 routes.scroll.get_or_insert(context);
490 }
491 Binding::PointerPosition { .. } => {
492 routes.pointer.get_or_insert(context);
493 }
494 Binding::PadButton(button) | Binding::PadChord { button, .. } => {
495 routes.pad_buttons.entry(*button).or_insert(context);
496 }
497 Binding::PadDpad => {
498 for button in PadButton::DPAD {
499 routes.pad_buttons.entry(button).or_insert(context);
500 }
501 }
502 Binding::PadStick { stick, .. } => {
503 routes.pad_sticks.entry(*stick).or_insert(context);
504 }
505 Binding::PadTrigger { trigger, .. } => {
506 routes.pad_triggers.entry(*trigger).or_insert(context);
507 }
508 Binding::Key(_)
509 | Binding::KeyAxis { .. }
510 | Binding::Chord { .. }
511 | Binding::Wasd { .. } => {}
512 }
513 }
514 let slots = map.slots.iter().filter(|slot| slot.context == context);
526 for slot in slots {
527 for binding in &slot.decl.bindings {
528 match binding {
529 Binding::Chord { modifier, key }
530 if routes.keys.get(key) == Some(&context) =>
531 {
532 add_chord(&mut routes.chords, *key, *modifier);
533 }
534 Binding::ButtonChord { modifier, button }
535 if routes.buttons.get(button) == Some(&context) =>
536 {
537 add_chord(&mut routes.button_chords, *button, *modifier);
538 }
539 Binding::PadChord { modifier, button }
540 if routes.pad_buttons.get(button) == Some(&context) =>
541 {
542 add_chord(&mut routes.pad_chords, *button, *modifier);
543 }
544 Binding::PadChord { modifier, button }
545 if slot.pad_chords_outrank
546 && !pad_chords_above.contains(&(*modifier, *button)) =>
547 {
548 add_chord(
549 &mut routes.outranking_pad_chords,
550 *button,
551 (*modifier, context),
552 );
553 }
554 Binding::ScrollChord { held }
555 if slot.enabled
556 && routes.scroll == Some(context)
557 && !routes.scroll_chords.contains(held) =>
558 {
559 routes.scroll_chords.push(*held);
560 }
561 _ => {}
562 }
563 }
564 }
565 for binding in bindings() {
566 if let Binding::PadChord { modifier, button } = binding {
567 pad_chords_above.insert((*modifier, *button));
568 }
569 }
570 below_modal = map.modal.contains(&context);
571 }
572 routes
573 }
574}
575
576fn add_chord<I: Eq + std::hash::Hash, M: PartialEq>(
582 chords: &mut HashMap<I, Vec<M>>,
583 input: I,
584 modifier: M,
585) {
586 let modifiers = chords.entry(input).or_default();
587 if !modifiers.contains(&modifier) {
588 modifiers.push(modifier);
589 }
590}
591
592impl ActionMap {
593 pub fn push_context(&mut self, context: &str) -> Result<(), ActionMapError> {
603 self.push(context, false)
604 }
605
606 pub fn push_context_modal(&mut self, context: &str) -> Result<(), ActionMapError> {
620 self.push(context, true)
621 }
622
623 fn push(&mut self, context: &str, modal: bool) -> Result<(), ActionMapError> {
624 let index = self.context_index(context)?;
625 if self.is_active(index) {
626 return Err(ActionMapError::ContextAlreadyActive(context.to_owned()));
627 }
628 self.stack.push(index);
629 if modal {
630 self.modal.push(index);
631 }
632 self.restack();
633 Ok(())
634 }
635
636 pub fn pop_context(&mut self, context: &str) -> Result<(), ActionMapError> {
647 let index = self.context_index(context)?;
648 if self.stack.len() == 1 || self.stack.last() != Some(&index) {
649 return Err(ActionMapError::ContextNotOnTop(context.to_owned()));
650 }
651 self.stack.pop();
652 self.modal.retain(|&modal| modal != index);
653 self.restack();
654 Ok(())
655 }
656
657 pub fn active_contexts(&self) -> impl Iterator<Item = &str> {
661 self.stack
662 .iter()
663 .map(|&index| self.contexts[index].as_str())
664 }
665
666 #[must_use]
668 pub fn is_context_active(&self, context: &str) -> bool {
669 self.contexts
670 .iter()
671 .position(|name| name == context)
672 .is_some_and(|index| self.is_active(index))
673 }
674
675 fn route_order(&self) -> impl Iterator<Item = usize> + '_ {
678 std::iter::once(GLOBAL_INDEX).chain(self.stack.iter().rev().copied())
679 }
680
681 pub(crate) fn blocking_modal(&self, claims: impl Fn(&Binding) -> bool) -> Option<usize> {
686 let mut modal = None;
687 for context in self.route_order() {
688 let binds = || {
689 self.slots
690 .iter()
691 .filter(|slot| slot.context == context)
692 .any(|slot| slot.decl.bindings.iter().any(&claims))
693 };
694 if modal.is_some() && binds() {
695 return modal;
696 }
697 if modal.is_none() && self.modal.contains(&context) {
698 modal = Some(context);
699 }
700 }
701 None
702 }
703
704 #[must_use]
707 pub fn is_context_modal(&self, context: &str) -> bool {
708 self.contexts
709 .iter()
710 .position(|name| name == context)
711 .is_some_and(|index| self.modal.contains(&index))
712 }
713
714 fn is_active(&self, index: usize) -> bool {
717 index == GLOBAL_INDEX || self.stack.contains(&index)
718 }
719
720 #[must_use]
722 pub fn context_of(&self, name: &str) -> Option<&str> {
723 let &idx = self.name_to_idx.get(name)?;
724 Some(self.contexts[self.slots[idx].context].as_str())
725 }
726
727 pub fn set_pad_chords_outrank(
746 &mut self,
747 name: &str,
748 outrank: bool,
749 ) -> Result<(), ActionMapError> {
750 let Some(&idx) = self.name_to_idx.get(name) else {
751 return Err(ActionMapError::UnknownAction(name.to_owned()));
752 };
753 if self.slots[idx].pad_chords_outrank != outrank {
754 self.slots[idx].pad_chords_outrank = outrank;
755 self.reroute();
756 }
757 Ok(())
758 }
759
760 #[must_use]
763 pub fn pad_chords_outrank(&self, name: &str) -> Option<bool> {
764 let &idx = self.name_to_idx.get(name)?;
765 Some(self.slots[idx].pad_chords_outrank)
766 }
767
768 fn context_index(&self, context: &str) -> Result<usize, ActionMapError> {
769 self.contexts
770 .iter()
771 .position(|name| name == context)
772 .ok_or_else(|| ActionMapError::UnknownContext(context.to_owned()))
773 }
774
775 pub(crate) fn is_live(&self, idx: usize) -> bool {
777 let slot = &self.slots[idx];
778 slot.enabled && self.is_active(slot.context)
779 }
780
781 pub(crate) fn reroute(&mut self) {
784 self.routes = Routes::build(self);
785 self.resolve_live();
786 }
787
788 fn resolve_live(&mut self) {
790 for idx in 0..self.slots.len() {
791 if self.is_live(idx) {
792 self.resolve_one(idx);
793 }
794 }
795 }
796
797 pub fn suppress_held(&mut self) {
806 let withheld = Suppressed {
807 keys: self.held_keys.keys().copied().collect(),
808 buttons: self.held_buttons.clone(),
809 controls: self.held_controls.clone(),
810 pad_buttons: self.held_pad_buttons,
811 control_sticks: self
812 .control_sticks
813 .iter()
814 .filter(|(_, deflection)| **deflection != (0.0, 0.0))
815 .map(|(id, _)| id.clone())
816 .collect(),
817 pad_sticks: [Stick::Left, Stick::Right]
818 .into_iter()
819 .filter(|&stick| !self.stick_rests(stick))
820 .collect(),
821 pad_triggers: [Trigger::Left, Trigger::Right]
822 .into_iter()
823 .filter(|&trigger| !self.trigger_rests(trigger))
824 .collect(),
825 };
826 for slot in &mut self.slots {
827 slot.patterns.cancel();
828 }
829 self.withhold(withheld);
830 }
831
832 pub fn suppress_held_action(&mut self, name: &str) -> Result<(), ActionMapError> {
845 let Some(&idx) = self.name_to_idx.get(name) else {
846 return Err(ActionMapError::UnknownAction(name.to_owned()));
847 };
848 let mut withheld = Suppressed::default();
849 for binding in &self.slots[idx].decl.bindings {
850 self.held_by(binding, &mut withheld);
851 }
852 self.slots[idx].patterns.cancel();
853 for slot in &mut self.slots {
854 if slot.decl.bindings.iter().any(|b| withheld.withholds(b)) {
855 slot.patterns.cancel();
856 }
857 }
858 self.withhold(withheld);
859 Ok(())
860 }
861
862 fn held_by(&self, binding: &Binding, withheld: &mut Suppressed) {
865 binding.visit_keys(|key| {
866 if self.held_keys.contains_key(&key) {
867 withheld.keys.insert(key);
868 }
869 });
870 match binding {
871 Binding::ScrollChord { held } if self.held_keys.contains_key(held) => {
872 withheld.keys.insert(*held);
873 }
874 Binding::MouseButton(button) | Binding::ButtonChord { button, .. }
875 if self.held_buttons.contains(button) =>
876 {
877 withheld.buttons.insert(*button);
878 }
879 Binding::Virtual(id) => {
880 if self.held_controls.contains(id.as_str()) {
881 withheld.controls.insert(id.clone());
882 }
883 if self
884 .control_sticks
885 .get(id.as_str())
886 .is_some_and(|&deflection| deflection != (0.0, 0.0))
887 {
888 withheld.control_sticks.insert(id.clone());
889 }
890 }
891 Binding::PadButton(button) | Binding::PadChord { button, .. }
892 if self.held_pad_buttons.contains(*button) =>
893 {
894 withheld.pad_buttons.insert(*button);
895 }
896 Binding::PadDpad => {
897 for button in PadButton::DPAD {
898 if self.held_pad_buttons.contains(button) {
899 withheld.pad_buttons.insert(button);
900 }
901 }
902 }
903 Binding::PadStick { stick, .. } if !self.stick_rests(*stick) => {
904 withheld.pad_sticks.insert(*stick);
905 }
906 Binding::PadTrigger { trigger, .. } if !self.trigger_rests(*trigger) => {
907 withheld.pad_triggers.insert(*trigger);
908 }
909 _ => {}
910 }
911 }
912
913 fn withhold(&mut self, withheld: Suppressed) {
917 self.suppressed.extend(withheld);
918 self.resolve_live();
919 }
920
921 fn restack(&mut self) {
925 let routes = Routes::build(self);
926 let old = std::mem::replace(&mut self.routes, routes);
927 let new = &self.routes;
928 for key in self.held_keys.keys() {
929 if old.keys.get(key) != new.keys.get(key) {
930 self.suppressed.keys.insert(*key);
931 }
932 }
933 for button in &self.held_buttons {
934 if old.buttons.get(button) != new.buttons.get(button) {
935 self.suppressed.buttons.insert(*button);
936 }
937 }
938 for control in &self.held_controls {
939 if old.controls.get(control) != new.controls.get(control) {
940 self.suppressed.controls.insert(control.clone());
941 }
942 }
943 let held = self.held_pad_buttons;
944 for button in PadButton::ALL {
945 if held.contains(button) && old.pad_reader(button, held) != new.pad_reader(button, held)
946 {
947 self.suppressed.pad_buttons.insert(button);
948 }
949 }
950 for idx in 0..self.slots.len() {
951 self.slots[idx].patterns.cancel();
955 if self.is_active(self.slots[idx].context) {
956 if self.slots[idx].enabled {
957 self.resolve_one(idx);
958 }
959 } else {
960 self.slots[idx].reset();
961 }
962 }
963 }
964}
965
966#[cfg(test)]
967mod outrank_tests;
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use crate::{ActionDecl, ActionKind, ActionValue, InputTickState, Repeat};
973
974 const TICK: f32 = 1.0 / 60.0;
975
976 fn button(name: &str, bindings: Vec<Binding>) -> ActionDecl {
977 ActionDecl {
978 name: name.to_owned(),
979 kind: ActionKind::Button,
980 bindings,
981 }
982 }
983
984 fn menu_over_gameplay() -> ActionMap {
986 let mut map = ActionMap::new();
987 map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
988 map.declare(button("crouch", vec![Binding::Key(KeyCode::KeyC)]));
989 map.declare_in(
990 "menu",
991 button(
992 "accept",
993 vec![Binding::Key(KeyCode::Space), Binding::Key(KeyCode::Enter)],
994 ),
995 );
996 map
997 }
998
999 #[test]
1002 fn a_key_bound_in_both_reaches_only_the_pushed_context_then_gameplay_after_pop() {
1003 let mut map = menu_over_gameplay();
1004 assert_eq!(map.context_of("accept"), Some("menu"));
1005 assert_eq!(map.context_of("jump"), Some(GAMEPLAY_CONTEXT));
1006
1007 map.begin_tick(TICK);
1008 map.key_event(KeyCode::Space, true);
1009 assert!(
1010 map.just_pressed("jump"),
1011 "nothing is pushed: gameplay has it"
1012 );
1013 assert!(
1014 !map.button_held("accept"),
1015 "and a context off the stack is idle"
1016 );
1017 map.key_event(KeyCode::Space, false);
1018
1019 map.push_context("menu").expect("declared, not active");
1020 map.begin_tick(TICK);
1021 map.key_event(KeyCode::Space, true);
1022 assert!(map.just_pressed("accept"), "the pushed context owns Space");
1023 assert!(
1024 !map.button_held("jump"),
1025 "and gameplay beneath it does not see it"
1026 );
1027 map.key_event(KeyCode::Space, false);
1028
1029 map.pop_context("menu").expect("menu is on top");
1030 map.begin_tick(TICK);
1031 map.key_event(KeyCode::Space, true);
1032 assert!(
1033 map.just_pressed("jump"),
1034 "popped: Space is gameplay's again"
1035 );
1036 assert!(!map.button_held("accept"));
1037 }
1038
1039 fn inventory_over_gameplay() -> ActionMap {
1043 let mut map = ActionMap::new();
1044 map.declare(button("walk", vec![Binding::Key(KeyCode::KeyW)]));
1045 map.declare(button(
1046 "shoot",
1047 vec![Binding::MouseButton(PointerButton::Left)],
1048 ));
1049 map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
1050 map.declare(axis(
1051 "aim_x",
1052 ActionKind::Axis1,
1053 vec![Binding::PointerPosition {
1054 axis: crate::PointerAxis::X,
1055 }],
1056 ));
1057 map.declare_in(
1058 "inventory",
1059 button("close", vec![Binding::Key(KeyCode::Escape)]),
1060 );
1061 map.declare_in(
1062 GLOBAL_CONTEXT,
1063 button("map", vec![Binding::Key(KeyCode::KeyM)]),
1064 );
1065 map
1066 }
1067
1068 #[test]
1074 fn a_modal_context_stops_what_it_does_not_bind() {
1075 let mut map = inventory_over_gameplay();
1076 map.push_context("inventory").expect("declared");
1077 map.begin_tick(TICK);
1078 map.key_event(KeyCode::KeyW, true);
1079 assert!(map.button_held("walk"), "a plain context lets W through");
1080 map.key_event(KeyCode::KeyW, false);
1081 map.pop_context("inventory").expect("on top");
1082
1083 map.push_context_modal("inventory").expect("declared");
1084 map.begin_tick(TICK);
1085 map.key_event(KeyCode::KeyW, true);
1086 map.mouse_button(PointerButton::Left, true);
1087 map.mouse_scroll(0.0, 1.0);
1088 map.pointer_position(0.5, 0.25);
1089 map.key_event(KeyCode::Escape, true);
1090 assert!(!map.button_held("walk"), "W reached gameplay");
1091 assert!(
1092 !map.button_held("shoot"),
1093 "the left button reached gameplay"
1094 );
1095 assert_eq!(map.axis1("zoom"), 0.0, "the wheel reached gameplay");
1096 assert!(map.just_pressed("close"), "the modal context's own key");
1097 assert_eq!(map.axis1("aim_x"), 0.5, "the cursor is still read beneath");
1098 }
1099
1100 #[test]
1104 fn a_key_held_into_a_modal_context_waits_for_a_new_press() {
1105 let mut map = inventory_over_gameplay();
1106 map.begin_tick(TICK);
1107 map.key_event(KeyCode::KeyW, true);
1108 assert!(map.button_held("walk"));
1109
1110 map.push_context_modal("inventory").expect("declared");
1111 assert!(!map.button_held("walk"), "the walk stops as it opens");
1112 map.pop_context("inventory").expect("on top");
1113 map.begin_tick(TICK);
1114 assert!(!map.button_held("walk"), "and does not resume as it closes");
1115
1116 map.key_event(KeyCode::KeyW, false);
1117 map.key_event(KeyCode::KeyW, true);
1118 assert!(map.button_held("walk"), "a new press walks");
1119 }
1120
1121 #[test]
1125 fn the_global_context_stays_above_a_modal_one() {
1126 let mut map = inventory_over_gameplay();
1127 map.push_context_modal("inventory").expect("declared");
1128 map.begin_tick(TICK);
1129 map.key_event(KeyCode::KeyM, true);
1130 assert!(
1131 map.just_pressed("map"),
1132 "M is global's over the modal screen"
1133 );
1134
1135 assert_eq!(
1136 map.push_context(GLOBAL_CONTEXT),
1137 Err(ActionMapError::ContextAlreadyActive(
1138 GLOBAL_CONTEXT.to_owned()
1139 ))
1140 );
1141 assert_eq!(
1142 map.pop_context(GLOBAL_CONTEXT),
1143 Err(ActionMapError::ContextNotOnTop(GLOBAL_CONTEXT.to_owned()))
1144 );
1145 assert!(map.is_context_active(GLOBAL_CONTEXT));
1146 assert_eq!(
1147 map.active_contexts().collect::<Vec<_>>(),
1148 [GAMEPLAY_CONTEXT, "inventory"]
1149 );
1150 }
1151
1152 #[test]
1155 fn an_input_the_top_context_does_not_bind_falls_through() {
1156 let mut map = menu_over_gameplay();
1157 map.push_context("menu").expect("declared");
1158
1159 map.begin_tick(TICK);
1160 map.key_event(KeyCode::KeyC, true);
1161 assert!(
1162 map.just_pressed("crouch"),
1163 "menu binds no C, so it falls through"
1164 );
1165 map.key_event(KeyCode::KeyQ, true);
1166 assert!(!map.button_held("accept") && !map.button_held("jump"));
1167 }
1168
1169 #[test]
1173 fn nested_pushes_route_each_input_to_the_topmost_binder() {
1174 let mut map = menu_over_gameplay();
1175 map.declare_in(
1176 "editor",
1177 button("select", vec![Binding::Key(KeyCode::Enter)]),
1178 );
1179
1180 map.push_context("menu").expect("declared");
1181 map.push_context("editor").expect("declared");
1182 assert_eq!(
1183 map.active_contexts().collect::<Vec<_>>(),
1184 [GAMEPLAY_CONTEXT, "menu", "editor"],
1185 );
1186 assert_eq!(
1187 map.push_context("menu"),
1188 Err(ActionMapError::ContextAlreadyActive("menu".to_owned())),
1189 );
1190 assert_eq!(
1191 map.pop_context("menu"),
1192 Err(ActionMapError::ContextNotOnTop("menu".to_owned())),
1193 "menu is under editor, and popping it would pop editor's",
1194 );
1195
1196 map.begin_tick(TICK);
1197 map.key_event(KeyCode::Enter, true);
1198 map.key_event(KeyCode::Space, true);
1199 map.key_event(KeyCode::KeyC, true);
1200 assert!(
1201 map.button_held("select"),
1202 "Enter: editor is topmost and binds it"
1203 );
1204 assert!(
1205 map.button_held("accept"),
1206 "Space falls through editor to menu"
1207 );
1208 assert!(
1209 !map.button_held("jump"),
1210 "and menu consumes it before gameplay"
1211 );
1212 assert!(map.button_held("crouch"), "C falls through both");
1213 for key in [KeyCode::Enter, KeyCode::Space, KeyCode::KeyC] {
1214 map.key_event(key, false);
1215 }
1216
1217 map.pop_context("editor").expect("editor is on top");
1218 map.begin_tick(TICK);
1219 map.key_event(KeyCode::Enter, true);
1220 assert!(map.just_pressed("accept"), "editor popped: menu owns Enter");
1221 assert!(!map.button_held("select"), "and a popped context is idle");
1222 }
1223
1224 #[test]
1227 fn a_key_held_across_a_push_or_pop_waits_for_its_release() {
1228 let mut map = menu_over_gameplay();
1229
1230 map.begin_tick(TICK);
1233 map.key_event(KeyCode::Space, true);
1234 map.begin_tick(TICK);
1235 map.push_context("menu").expect("declared");
1236 assert!(
1237 map.just_released("jump"),
1238 "the walk stops when a menu opens over it"
1239 );
1240 assert!(
1241 !map.button_held("accept"),
1242 "and the menu is not pressed by it"
1243 );
1244 map.begin_tick(TICK);
1245 map.key_event(KeyCode::Space, true); assert!(!map.button_held("accept"));
1247 map.key_event(KeyCode::Space, false);
1248 map.key_event(KeyCode::Space, true);
1249 assert!(
1250 map.just_pressed("accept"),
1251 "a fresh press after the lift is menu's"
1252 );
1253
1254 map.pop_context("menu").expect("menu is on top");
1256 assert!(
1257 !map.button_held("jump"),
1258 "the key that closed the menu does not jump"
1259 );
1260 map.begin_tick(TICK);
1261 assert!(!map.button_held("jump"));
1262 map.key_event(KeyCode::Space, false);
1263 map.begin_tick(TICK);
1264 map.key_event(KeyCode::Space, true);
1265 assert!(map.just_pressed("jump"));
1266 }
1267
1268 #[test]
1271 fn a_held_key_whose_owner_does_not_change_is_not_withheld() {
1272 let mut map = menu_over_gameplay();
1273 map.key_event(KeyCode::KeyC, true);
1274 map.push_context("menu").expect("declared");
1275 map.begin_tick(TICK);
1276 assert!(map.button_held("crouch"));
1277 assert!(!map.just_released("crouch"));
1278 }
1279
1280 #[test]
1282 fn the_stack_refuses_unknown_contexts_and_the_base() {
1283 let mut map = menu_over_gameplay();
1284 assert_eq!(
1285 map.push_context("nope"),
1286 Err(ActionMapError::UnknownContext("nope".to_owned())),
1287 );
1288 assert_eq!(
1289 map.push_context(GAMEPLAY_CONTEXT),
1290 Err(ActionMapError::ContextAlreadyActive(
1291 GAMEPLAY_CONTEXT.to_owned()
1292 )),
1293 );
1294 assert_eq!(
1295 map.pop_context(GAMEPLAY_CONTEXT),
1296 Err(ActionMapError::ContextNotOnTop(GAMEPLAY_CONTEXT.to_owned())),
1297 );
1298 assert!(map.is_context_active(GAMEPLAY_CONTEXT));
1299 assert!(!map.is_context_active("menu"));
1300 assert!(!map.is_context_active("nope"));
1301 }
1302
1303 #[test]
1305 fn pointer_inputs_and_controls_are_consumed_too() {
1306 let mut map = ActionMap::new();
1307 map.declare(button(
1308 "fire",
1309 vec![
1310 Binding::MouseButton(PointerButton::Left),
1311 Binding::Virtual("btn".to_owned()),
1312 ],
1313 ));
1314 map.declare(ActionDecl {
1315 name: "aim".to_owned(),
1316 kind: ActionKind::Axis1,
1317 bindings: vec![Binding::PointerPosition {
1318 axis: crate::PointerAxis::X,
1319 }],
1320 });
1321 map.declare_in(
1322 "menu",
1323 ActionDecl {
1324 name: "cursor".to_owned(),
1325 kind: ActionKind::Axis1,
1326 bindings: vec![Binding::PointerPosition {
1327 axis: crate::PointerAxis::X,
1328 }],
1329 },
1330 );
1331 map.declare_in(
1332 "menu",
1333 button(
1334 "click",
1335 vec![
1336 Binding::MouseButton(PointerButton::Left),
1337 Binding::Virtual("btn".to_owned()),
1338 ],
1339 ),
1340 );
1341 map.push_context("menu").expect("declared");
1342
1343 map.begin_tick(TICK);
1344 map.pointer_position(0.5, 0.0);
1345 map.mouse_button(PointerButton::Left, true);
1346 map.virtual_button("btn", true);
1347 assert!(map.just_pressed("click"));
1348 assert!(!map.button_held("fire"));
1349 assert_eq!(map.axis1("cursor"), 0.5);
1350 assert_eq!(
1351 map.axis1("aim"),
1352 0.0,
1353 "the menu's pointer is not the game's"
1354 );
1355 let Some(ActionValue::Axis1(aim)) = map.action("aim") else {
1356 panic!("aim is an Axis1");
1357 };
1358 assert!(!aim.pointer_moved, "nor did it move for the game");
1359
1360 map.begin_tick(TICK);
1362 assert_eq!(map.axis1("aim"), 0.0, "a consumed pointer stays consumed");
1363 assert!(!map.button_held("fire"));
1364 }
1365
1366 #[test]
1369 fn a_rebind_moves_ownership_and_re_resolves_the_loser() {
1370 let mut map = menu_over_gameplay();
1371 map.push_context("menu").expect("declared");
1372 map.key_event(KeyCode::KeyC, true);
1373 assert!(map.button_held("crouch"));
1374
1375 map.rebind("accept", vec![Binding::Key(KeyCode::KeyC)])
1376 .expect("declared");
1377 assert!(
1378 !map.button_held("crouch"),
1379 "menu took C, so gameplay lost it"
1380 );
1381 assert!(
1382 map.button_held("accept"),
1383 "and a rebind resolves against held keys"
1384 );
1385 }
1386
1387 #[test]
1390 fn a_chord_shadows_the_plain_key_and_leaves_its_modifier_alone() {
1391 let mut map = ActionMap::new();
1392 map.declare(button("sprint", vec![Binding::Key(KeyCode::ShiftLeft)]));
1393 map.declare_in("menu", button("next", vec![Binding::Key(KeyCode::Tab)]));
1394 map.declare_in(
1395 "menu",
1396 button(
1397 "prev",
1398 vec![Binding::Chord {
1399 modifier: Modifier::Shift,
1400 key: KeyCode::Tab,
1401 }],
1402 ),
1403 );
1404 map.push_context("menu").expect("declared");
1405
1406 map.begin_tick(TICK);
1407 map.key_event(KeyCode::Tab, true);
1408 assert!(map.button_held("next") && !map.button_held("prev"));
1409
1410 map.key_event(KeyCode::ShiftLeft, true);
1411 assert!(
1412 map.just_released("next"),
1413 "Shift turns the held Tab into Shift+Tab"
1414 );
1415 assert!(map.just_pressed("prev"));
1416 assert!(
1417 map.button_held("sprint"),
1418 "the modifier still reaches gameplay"
1419 );
1420
1421 map.key_event(KeyCode::ShiftLeft, false);
1422 assert!(!map.button_held("prev") && map.button_held("next"));
1423 }
1424
1425 fn alt_right() -> Binding {
1426 Binding::ButtonChord {
1427 modifier: Modifier::Alt,
1428 button: PointerButton::Right,
1429 }
1430 }
1431
1432 fn sights() -> ActionMap {
1435 let mut map = ActionMap::new();
1436 map.declare(button(
1437 "aim",
1438 vec![Binding::MouseButton(PointerButton::Right)],
1439 ));
1440 map.declare(button("zoom", vec![alt_right()]));
1441 map.declare(button("lean", vec![Binding::Key(KeyCode::AltLeft)]));
1442 map
1443 }
1444
1445 #[test]
1449 fn a_button_chord_shadows_the_plain_button_and_leaves_its_modifier_alone() {
1450 let mut map = sights();
1451 map.begin_tick(TICK);
1452 map.mouse_button(PointerButton::Right, true);
1453 assert!(map.button_held("aim") && !map.button_held("zoom"));
1454
1455 map.mouse_button(PointerButton::Right, false);
1456 map.begin_tick(TICK);
1457 map.key_event(KeyCode::AltLeft, true);
1458 map.mouse_button(PointerButton::Right, true);
1459 assert!(map.just_pressed("zoom"));
1460 assert!(!map.button_held("aim"), "the plain binding fired too");
1461 assert!(map.button_held("lean"), "the modifier was consumed");
1462 }
1463
1464 #[test]
1468 fn a_modifier_hands_a_held_mouse_button_to_the_chord_and_back() {
1469 let mut map = sights();
1470 map.begin_tick(TICK);
1471 map.mouse_button(PointerButton::Right, true);
1472 assert!(map.just_pressed("aim"));
1473
1474 map.begin_tick(TICK);
1475 map.key_event(KeyCode::AltRight, true);
1476 assert!(
1477 map.just_released("aim"),
1478 "Alt turns the held right button into Alt+right"
1479 );
1480 assert!(map.just_pressed("zoom"));
1481
1482 map.begin_tick(TICK);
1483 map.key_event(KeyCode::AltRight, false);
1484 assert!(map.just_released("zoom"));
1485 assert!(map.just_pressed("aim"), "the button went back to the aim");
1486 }
1487
1488 #[test]
1492 fn a_button_chord_shadows_only_in_the_context_that_owns_its_button() {
1493 let mut map = ActionMap::new();
1494 map.declare(button(
1495 "aim",
1496 vec![Binding::MouseButton(PointerButton::Right)],
1497 ));
1498 map.declare_in("optic", button("magnify", vec![alt_right()]));
1499 map.key_event(KeyCode::AltLeft, true);
1500 map.mouse_button(PointerButton::Right, true);
1501 assert!(
1502 map.button_held("aim"),
1503 "a chord off the stack shadowed the button"
1504 );
1505 assert!(!map.button_held("magnify"));
1506
1507 map.mouse_button(PointerButton::Right, false);
1508 map.push_context("optic").expect("declared");
1509 map.mouse_button(PointerButton::Right, true);
1510 assert!(map.button_held("magnify"));
1511 assert!(!map.button_held("aim"));
1512 }
1513
1514 fn axis(name: &str, kind: ActionKind, bindings: Vec<Binding>) -> ActionDecl {
1515 ActionDecl {
1516 name: name.to_owned(),
1517 kind,
1518 bindings,
1519 }
1520 }
1521
1522 fn wasd() -> Binding {
1523 Binding::Wasd {
1524 up: KeyCode::KeyW,
1525 down: KeyCode::KeyS,
1526 left: KeyCode::KeyA,
1527 right: KeyCode::KeyD,
1528 }
1529 }
1530
1531 #[test]
1535 fn suppress_held_withholds_a_key_until_it_is_pressed_again() {
1536 let mut map = menu_over_gameplay();
1537 map.begin_tick(TICK);
1538 map.key_event(KeyCode::Space, true);
1539 map.begin_tick(TICK);
1540 map.suppress_held();
1541 assert!(map.just_released("jump"), "the walk stops on the spot");
1542 map.begin_tick(TICK);
1543 map.key_event(KeyCode::Space, true); assert!(!map.button_held("jump"));
1545 map.key_event(KeyCode::Space, false);
1546 map.key_event(KeyCode::Space, true);
1547 assert!(map.just_pressed("jump"), "pressed again");
1548 }
1549
1550 #[test]
1553 fn suppress_held_mid_press_fires_no_pattern_and_the_next_press_does() {
1554 let mut map = menu_over_gameplay();
1555 map.set_tap("jump", Some(crate::Tap::default()))
1556 .expect("declared");
1557 map.set_hold("jump", Some(crate::Hold::default()))
1558 .expect("declared");
1559 map.begin_tick(TICK);
1560 map.key_event(KeyCode::Space, true);
1561 map.begin_tick(TICK);
1562 map.suppress_held();
1563 let mut fired = map.tapped("jump");
1564 for _ in 0..60 {
1565 map.begin_tick(TICK);
1566 fired |= map.hold_fired("jump");
1567 }
1568 map.key_event(KeyCode::Space, false);
1569 fired |= map.tapped("jump");
1570 assert!(!fired, "neither a hold nor a tap");
1571
1572 map.begin_tick(TICK);
1573 map.key_event(KeyCode::Space, true);
1574 map.begin_tick(TICK);
1575 map.key_event(KeyCode::Space, false);
1576 assert!(map.tapped("jump"), "suppress, release, press: it taps");
1577 }
1578
1579 #[test]
1582 fn suppress_held_drops_a_waiting_first_tap() {
1583 let mut map = menu_over_gameplay();
1584 map.set_double_tap("jump", Some(crate::DoubleTap::default()))
1585 .expect("declared");
1586 map.begin_tick(TICK);
1587 map.key_event(KeyCode::Space, true);
1588 map.key_event(KeyCode::Space, false);
1589 map.suppress_held();
1590 map.begin_tick(TICK);
1591 map.key_event(KeyCode::Space, true);
1592 assert!(!map.double_tapped("jump"));
1593 }
1594
1595 #[test]
1599 fn suppress_held_withholds_an_axis_per_key() {
1600 let mut map = ActionMap::new();
1601 map.declare(axis("move", ActionKind::Axis2, vec![wasd()]));
1602 map.declare(axis(
1603 "zoom",
1604 ActionKind::Axis1,
1605 vec![Binding::KeyAxis {
1606 negative: KeyCode::KeyQ,
1607 positive: KeyCode::KeyE,
1608 }],
1609 ));
1610 map.key_event(KeyCode::KeyW, true);
1611 map.key_event(KeyCode::KeyE, true);
1612 map.suppress_held();
1613 assert_eq!(map.axis2("move"), (0.0, 0.0));
1614 assert_eq!(map.axis1("zoom"), 0.0);
1615
1616 map.begin_tick(TICK);
1617 assert_eq!(map.axis2("move"), (0.0, 0.0), "still held, still withheld");
1618 map.key_event(KeyCode::KeyD, true);
1619 assert_eq!(map.axis2("move"), (1.0, 0.0), "a fresh key moves");
1620 map.key_event(KeyCode::KeyQ, true);
1621 assert_eq!(map.axis1("zoom"), -1.0);
1622
1623 map.key_event(KeyCode::KeyW, false);
1624 map.key_event(KeyCode::KeyW, true);
1625 let (x, y) = map.axis2("move");
1626 assert!(
1627 x > 0.0 && y > 0.0,
1628 "W pressed again: diagonal, got {x}, {y}"
1629 );
1630 }
1631
1632 #[test]
1635 fn suppress_held_withholds_a_chord_until_its_key_is_pressed_again() {
1636 let mut map = ActionMap::new();
1637 map.declare(button(
1638 "save",
1639 vec![Binding::Chord {
1640 modifier: Modifier::Control,
1641 key: KeyCode::KeyS,
1642 }],
1643 ));
1644 map.key_event(KeyCode::ControlLeft, true);
1645 map.key_event(KeyCode::KeyS, true);
1646 assert!(map.button_held("save"));
1647 map.suppress_held();
1648 assert!(!map.button_held("save"));
1649 map.key_event(KeyCode::KeyS, false);
1650 map.key_event(KeyCode::KeyS, true);
1651 assert!(map.just_pressed("save"), "Ctrl never let go");
1652 }
1653
1654 #[test]
1658 fn suppress_held_withholds_a_button_chord_until_its_button_is_pressed_again() {
1659 let mut map = sights();
1660 map.key_event(KeyCode::AltLeft, true);
1661 map.mouse_button(PointerButton::Right, true);
1662 assert!(map.button_held("zoom"));
1663 map.suppress_held();
1664 assert!(!map.button_held("zoom"));
1665 map.mouse_button(PointerButton::Right, false);
1666 map.mouse_button(PointerButton::Right, true);
1667 assert!(map.just_pressed("zoom"), "Alt never let go");
1668 }
1669
1670 #[test]
1673 fn suppress_held_withholds_pointer_buttons_and_on_screen_controls() {
1674 let mut map = ActionMap::new();
1675 map.declare(button(
1676 "fire",
1677 vec![
1678 Binding::MouseButton(PointerButton::Left),
1679 Binding::Virtual("btn".to_owned()),
1680 ],
1681 ));
1682 map.declare(axis(
1683 "move",
1684 ActionKind::Axis2,
1685 vec![Binding::Virtual("stick".to_owned())],
1686 ));
1687 map.mouse_button(PointerButton::Left, true);
1688 map.virtual_button("btn", true);
1689 map.virtual_stick("stick", 0.0, 1.0);
1690 map.suppress_held();
1691 assert!(!map.button_held("fire"));
1692 assert_eq!(map.axis2("move"), (0.0, 0.0));
1693
1694 map.virtual_stick("stick", 0.5, 0.5);
1695 assert_eq!(map.axis2("move"), (0.0, 0.0), "moved, never centred");
1696 map.virtual_stick("stick", 0.0, 0.0);
1697 map.virtual_stick("stick", 0.0, 1.0);
1698 assert_eq!(map.axis2("move"), (0.0, 1.0), "centred, then pushed");
1699
1700 map.mouse_button(PointerButton::Left, false);
1701 assert!(
1702 !map.button_held("fire"),
1703 "the on-screen button is still held"
1704 );
1705 map.virtual_button("btn", false);
1706 map.mouse_button(PointerButton::Left, true);
1707 assert!(map.just_pressed("fire"));
1708 }
1709
1710 fn pad(snapshot: impl FnOnce(&mut crate::GamepadSnapshot)) -> crate::GamepadEvent {
1711 let mut state = crate::GamepadSnapshot::neutral(crate::PadKind::Xbox);
1712 snapshot(&mut state);
1713 crate::GamepadEvent::State {
1714 id: crate::GamepadId(1),
1715 snapshot: state,
1716 }
1717 }
1718
1719 fn stick_y(y: f32) -> crate::GamepadEvent {
1720 pad(|state| state.axes[crate::PadAxis::LeftY as usize] = y)
1721 }
1722
1723 #[test]
1727 fn suppress_held_withholds_pad_buttons_sticks_and_triggers() {
1728 let mut map = ActionMap::new();
1729 map.declare(axis(
1730 "move",
1731 ActionKind::Axis2,
1732 vec![Binding::PadStick {
1733 stick: Stick::Left,
1734 deadzone: 0.2,
1735 }],
1736 ));
1737 map.declare(button(
1738 "fire",
1739 vec![Binding::PadTrigger {
1740 trigger: Trigger::Right,
1741 threshold: 0.5,
1742 }],
1743 ));
1744 map.declare(button("jump", vec![Binding::PadButton(PadButton::South)]));
1745 map.gamepad_event(&pad(|state| {
1746 state.axes[crate::PadAxis::LeftY as usize] = 1.0;
1747 state.axes[crate::PadAxis::RightTrigger as usize] = 1.0;
1748 state.buttons.insert(PadButton::South);
1749 }));
1750 assert_eq!(map.axis2("move"), (0.0, 1.0));
1751 assert!(map.button_held("fire") && map.button_held("jump"));
1752 map.suppress_held();
1753 assert_eq!(map.axis2("move"), (0.0, 0.0));
1754 assert!(!map.button_held("fire") && !map.button_held("jump"));
1755
1756 map.gamepad_event(&stick_y(0.8));
1757 assert_eq!(map.axis2("move"), (0.0, 0.0), "eased off, never rested");
1758 assert!(!map.button_held("jump"), "and South was let go of");
1759 map.gamepad_event(&stick_y(0.1));
1760 map.gamepad_event(&stick_y(1.0));
1761 assert_eq!(map.axis2("move"), (0.0, 1.0), "rested, then pushed");
1762 map.gamepad_event(&pad(|state| {
1763 state.axes[crate::PadAxis::RightTrigger as usize] = 1.0;
1764 state.buttons.insert(PadButton::South);
1765 }));
1766 assert!(map.button_held("fire") && map.button_held("jump"));
1767 }
1768
1769 #[test]
1773 fn suppress_held_action_withholds_only_that_actions_inputs() {
1774 let mut map = ActionMap::new();
1775 map.declare(axis("move", ActionKind::Axis2, vec![wasd()]));
1776 map.declare(button("forward", vec![Binding::Key(KeyCode::KeyW)]));
1777 map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
1778 map.key_event(KeyCode::KeyW, true);
1779 map.key_event(KeyCode::Space, true);
1780 map.suppress_held_action("move").expect("declared");
1781 assert_eq!(map.axis2("move"), (0.0, 0.0));
1782 assert!(!map.button_held("forward"), "W itself is withheld");
1783 assert!(map.button_held("jump"), "Space is not move's");
1784 map.key_event(KeyCode::KeyW, false);
1785 map.key_event(KeyCode::KeyW, true);
1786 assert_eq!(map.axis2("move"), (0.0, 1.0));
1787 assert_eq!(
1788 map.suppress_held_action("nope"),
1789 Err(ActionMapError::UnknownAction("nope".to_owned())),
1790 );
1791 }
1792
1793 fn scroll_chords() -> ActionMap {
1796 let mut map = ActionMap::new();
1797 map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
1798 for (name, held) in [
1799 ("zoom_z", KeyCode::KeyZ),
1800 ("zoom_ctrl", KeyCode::ControlLeft),
1801 ] {
1802 map.declare(axis(
1803 name,
1804 ActionKind::Axis1,
1805 vec![Binding::ScrollChord { held }],
1806 ));
1807 }
1808 map.declare(button("z", vec![Binding::Key(KeyCode::KeyZ)]));
1809 map
1810 }
1811
1812 fn wheel(map: &mut ActionMap) -> [f32; 3] {
1814 map.begin_tick(TICK);
1815 map.mouse_scroll(0.0, 1.0);
1816 ["zoom", "zoom_z", "zoom_ctrl"].map(|name| map.axis1(name))
1817 }
1818
1819 #[test]
1825 fn a_disabled_scroll_chord_takes_the_wheel_from_nothing() {
1826 let mut map = scroll_chords();
1827 map.key_event(KeyCode::ControlLeft, true);
1828 map.set_enabled("zoom_z", false);
1829 map.key_event(KeyCode::KeyZ, true);
1830 assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "Ctrl keeps the wheel");
1831
1832 map.key_event(KeyCode::ControlLeft, false);
1833 assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0], "Z silences nothing");
1834
1835 map.key_event(KeyCode::ControlLeft, true);
1836 map.set_enabled("zoom_z", true);
1837 assert_eq!(
1838 wheel(&mut map),
1839 [0.0, 0.0, 1.0],
1840 "Ctrl went down after Z, so it is the newer press"
1841 );
1842 map.key_event(KeyCode::ControlLeft, false);
1843 map.key_event(KeyCode::ControlLeft, true);
1844 map.key_event(KeyCode::KeyZ, false);
1845 map.key_event(KeyCode::KeyZ, true);
1846 map.set_enabled("zoom_z", false);
1847 map.set_enabled("zoom_z", true);
1848 assert_eq!(
1849 wheel(&mut map),
1850 [0.0, 1.0, 0.0],
1851 "Z pressed after Ctrl takes it back once enabled"
1852 );
1853 }
1854
1855 #[test]
1859 fn a_scroll_chord_takes_the_wheel_while_its_key_is_down() {
1860 let mut map = scroll_chords();
1861 assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
1862 map.key_event(KeyCode::KeyZ, true);
1863 assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0]);
1864 assert!(map.button_held("z"), "Z is read, not consumed");
1865 map.key_event(KeyCode::KeyZ, false);
1866 assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0], "the wheel is back");
1867 }
1868
1869 #[test]
1873 fn the_most_recently_pressed_scroll_chord_key_takes_the_wheel() {
1874 let mut map = scroll_chords();
1875 map.key_event(KeyCode::ControlLeft, true);
1876 map.key_event(KeyCode::KeyZ, true);
1877 assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "Ctrl then Z: Z's");
1878 map.key_event(KeyCode::ControlLeft, true); assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "a repeat is no press");
1880 map.key_event(KeyCode::KeyZ, false);
1881 assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "back to Ctrl");
1882 map.key_event(KeyCode::ControlLeft, false);
1883
1884 map.key_event(KeyCode::KeyZ, true);
1885 map.key_event(KeyCode::ControlLeft, true);
1886 assert_eq!(wheel(&mut map), [0.0, 0.0, 1.0], "Z then Ctrl: Ctrl's");
1887 map.key_event(KeyCode::ControlLeft, false);
1888 assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0], "back to Z");
1889 map.key_event(KeyCode::KeyZ, false);
1890 assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
1891 }
1892
1893 #[test]
1896 fn a_scroll_chord_key_pressed_mid_tick_takes_that_ticks_scroll() {
1897 let mut map = scroll_chords();
1898 map.begin_tick(TICK);
1899 map.mouse_scroll(0.0, 1.0);
1900 map.key_event(KeyCode::KeyZ, true);
1901 assert_eq!(["zoom", "zoom_z"].map(|name| map.axis1(name)), [0.0, 1.0]);
1902 }
1903
1904 #[test]
1908 fn a_scroll_chord_claims_the_wheel_for_its_context() {
1909 let mut map = ActionMap::new();
1910 map.declare(axis("zoom", ActionKind::Axis1, vec![Binding::MouseScroll]));
1911 map.declare_in(
1912 "map",
1913 axis(
1914 "pan",
1915 ActionKind::Axis1,
1916 vec![Binding::ScrollChord {
1917 held: KeyCode::AltLeft,
1918 }],
1919 ),
1920 );
1921 map.push_context("map").expect("declared");
1922 map.begin_tick(TICK);
1923 map.mouse_scroll(0.0, 1.0);
1924 assert_eq!(map.axis1("zoom"), 0.0, "the pushed context owns the wheel");
1925 assert_eq!(map.axis1("pan"), 0.0, "and Alt is not down");
1926 map.key_event(KeyCode::AltLeft, true);
1927 assert_eq!(map.axis1("pan"), 1.0);
1928 }
1929
1930 #[test]
1933 fn a_suppressed_scroll_chord_key_leaves_the_wheel_plain() {
1934 let mut map = scroll_chords();
1935 map.key_event(KeyCode::KeyZ, true);
1936 map.suppress_held();
1937 assert_eq!(wheel(&mut map), [1.0, 0.0, 0.0]);
1938 map.key_event(KeyCode::KeyZ, false);
1939 map.key_event(KeyCode::KeyZ, true);
1940 assert_eq!(wheel(&mut map), [0.0, 1.0, 0.0]);
1941 }
1942
1943 #[test]
1947 fn a_scripted_sequence_yields_the_same_action_stream_twice() {
1948 fn run() -> Vec<String> {
1949 let mut map = ActionMap::new();
1950 map.declare(ActionDecl {
1951 name: "move".to_owned(),
1952 kind: ActionKind::Axis2,
1953 bindings: vec![Binding::Wasd {
1954 up: KeyCode::KeyW,
1955 down: KeyCode::KeyS,
1956 left: KeyCode::KeyA,
1957 right: KeyCode::KeyD,
1958 }],
1959 });
1960 map.declare(button("jump", vec![Binding::Key(KeyCode::Space)]));
1961 crate::ui::declare(&mut map).expect("no clash with move or jump");
1962 map.set_repeat("jump", Some(Repeat::UI)).expect("declared");
1963
1964 let script: &[(u32, KeyCode, bool)] = &[
1965 (0, KeyCode::KeyD, true),
1966 (1, KeyCode::Space, true),
1967 (3, KeyCode::KeyW, true),
1968 (5, KeyCode::Tab, true),
1969 (6, KeyCode::ShiftRight, true),
1970 (9, KeyCode::Space, false),
1971 (12, KeyCode::KeyD, false),
1972 (14, KeyCode::Space, true),
1973 (20, KeyCode::KeyW, false),
1974 (40, KeyCode::Tab, false),
1975 (41, KeyCode::ShiftRight, false),
1976 (44, KeyCode::Space, false),
1977 ];
1978 let mut stream = Vec::new();
1979 for tick in 0..60 {
1980 map.begin_tick(TICK);
1981 match tick {
1982 4 | 30 => map.push_context(crate::ui::CONTEXT).expect("off the stack"),
1983 18 | 50 => map.pop_context(crate::ui::CONTEXT).expect("on top"),
1984 _ => {}
1985 }
1986 for &(at, key, pressed) in script {
1987 if at == tick {
1988 map.key_event(key, pressed);
1989 }
1990 }
1991 let repeats: Vec<bool> =
1992 map.action_names().map(|name| map.repeated(name)).collect();
1993 stream.push(format!(
1994 "{tick} {:?} {repeats:?} {:?}",
1995 InputTickState::capture(&map),
1996 map.last_device(),
1997 ));
1998 }
1999 stream
2000 }
2001
2002 let first = run();
2003 assert_eq!(first, run());
2004 assert!(
2005 first.iter().any(|line| line.contains("just_pressed: true")),
2006 "the script pressed something, or equality says nothing",
2007 );
2008 }
2009}