1use std::collections::{HashMap, HashSet};
4use std::hash::Hash;
5
6use super::binding::Binding;
7
8#[derive(Clone, Debug, Default)]
11pub struct ActionMap<A> {
12 bindings: HashMap<A, Vec<Binding>>,
13 contexts: HashMap<String, HashSet<A>>,
14}
15
16impl<A: Clone + Eq + Hash> ActionMap<A> {
17 pub fn new() -> Self {
18 Self {
19 bindings: HashMap::new(),
20 contexts: HashMap::new(),
21 }
22 }
23
24 pub fn bind(&mut self, action: A, binding: Binding) -> &mut Self {
26 self.bindings.entry(action).or_default().push(binding);
27 self
28 }
29
30 pub fn in_context(&mut self, context: &str, action: A) -> &mut Self {
32 self.contexts
33 .entry(context.to_string())
34 .or_default()
35 .insert(action);
36 self
37 }
38
39 pub fn bindings_for(&self, action: &A) -> &[Binding] {
41 self.bindings.get(action).map(Vec::as_slice).unwrap_or(&[])
42 }
43
44 pub fn actions(&self) -> impl Iterator<Item = &A> {
46 self.bindings.keys()
47 }
48
49 pub fn has_contexts(&self) -> bool {
51 !self.contexts.is_empty()
52 }
53
54 pub fn live_in(&self, action: &A, active: &HashSet<String>) -> bool {
57 if !self.has_contexts() {
58 return true;
59 }
60 if !self.contexts.values().any(|set| set.contains(action)) {
61 return true;
62 }
63 self.contexts
64 .iter()
65 .any(|(name, set)| active.contains(name) && set.contains(action))
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use repose_core::input::{Key, Modifiers};
73
74 fn chord() -> Binding {
75 Binding::Key(repose_core::shortcuts::KeyChord::new(
76 Key::Space,
77 Modifiers::default(),
78 ))
79 }
80
81 #[test]
82 fn unbound_action_has_no_bindings() {
83 let map: ActionMap<&str> = ActionMap::new();
84 assert!(map.bindings_for(&"jump").is_empty());
85 }
86
87 #[test]
88 fn context_gating() {
89 let mut map = ActionMap::new();
90 map.bind("jump", chord());
91 map.bind("pause", chord());
92 map.in_context("gameplay", "jump");
93 let active: HashSet<String> = ["gameplay".to_string()].into_iter().collect();
94 assert!(map.live_in(&"jump", &active));
95 assert!(map.live_in(&"pause", &active));
96 let menu: HashSet<String> = ["menu".to_string()].into_iter().collect();
97 assert!(!map.live_in(&"jump", &menu));
98 assert!(map.live_in(&"pause", &menu));
99 }
100}