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