1use alloc::vec::Vec;
4
5use retroglyph_core::event::{Event, KeyCode, KeyModifiers};
6
7#[derive(Debug, Clone, Copy)]
9struct Binding<Id, Action> {
10 scope: Option<Id>,
13 code: KeyCode,
14 modifiers: KeyModifiers,
15 action: Action,
16}
17
18#[derive(Debug, Clone)]
71pub struct Shortcuts<Id, Action> {
72 bindings: Vec<Binding<Id, Action>>,
73}
74
75impl<Id, Action> Shortcuts<Id, Action> {
76 #[must_use]
78 pub const fn new() -> Self {
79 Self {
80 bindings: Vec::new(),
81 }
82 }
83}
84
85impl<Id: Copy + PartialEq, Action: Copy> Shortcuts<Id, Action> {
86 pub fn bind_global(&mut self, code: KeyCode, modifiers: KeyModifiers, action: Action) {
88 self.bindings.push(Binding {
89 scope: None,
90 code,
91 modifiers,
92 action,
93 });
94 }
95
96 pub fn bind_scoped(&mut self, id: Id, code: KeyCode, modifiers: KeyModifiers, action: Action) {
98 self.bindings.push(Binding {
99 scope: Some(id),
100 code,
101 modifiers,
102 action,
103 });
104 }
105
106 #[must_use]
115 pub fn resolve(&self, event: &Event, focused: Option<Id>) -> Option<Action> {
116 let Event::Key(key) = event else {
117 return None;
118 };
119 if !key.is_down() {
120 return None;
121 }
122 let matches = |b: &&Binding<Id, Action>| b.code == key.code && b.modifiers == key.modifiers;
123
124 if let Some(focused) = focused
125 && let Some(binding) = self
126 .bindings
127 .iter()
128 .find(|b| b.scope == Some(focused) && matches(b))
129 {
130 return Some(binding.action);
131 }
132 self.bindings
133 .iter()
134 .find(|b| b.scope.is_none() && matches(b))
135 .map(|b| b.action)
136 }
137}
138
139impl<Id, Action> Default for Shortcuts<Id, Action> {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use retroglyph_core::event::KeyEvent;
151
152 use super::*;
153
154 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
155 enum Id {
156 List,
157 Search,
158 }
159
160 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
161 enum Action {
162 ToggleTheme,
163 ClearSearch,
164 DeleteSelected,
165 }
166
167 fn key(code: KeyCode) -> Event {
168 Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
169 }
170
171 #[test]
172 fn global_binding_fires_regardless_of_focus() {
173 let mut shortcuts = Shortcuts::new();
174 shortcuts.bind_global(KeyCode::Char('t'), KeyModifiers::NONE, Action::ToggleTheme);
175
176 assert_eq!(
177 shortcuts.resolve(&key(KeyCode::Char('t')), None),
178 Some(Action::ToggleTheme)
179 );
180 assert_eq!(
181 shortcuts.resolve(&key(KeyCode::Char('t')), Some(Id::List)),
182 Some(Action::ToggleTheme)
183 );
184 }
185
186 #[test]
187 fn scoped_binding_only_fires_while_its_id_is_focused() {
188 let mut shortcuts = Shortcuts::new();
189 shortcuts.bind_scoped(
190 Id::Search,
191 KeyCode::Escape,
192 KeyModifiers::NONE,
193 Action::ClearSearch,
194 );
195
196 assert_eq!(
197 shortcuts.resolve(&key(KeyCode::Escape), Some(Id::Search)),
198 Some(Action::ClearSearch)
199 );
200 assert_eq!(
201 shortcuts.resolve(&key(KeyCode::Escape), Some(Id::List)),
202 None
203 );
204 assert_eq!(shortcuts.resolve(&key(KeyCode::Escape), None), None);
205 }
206
207 #[test]
208 fn scoped_binding_takes_priority_over_a_global_one_for_the_same_key() {
209 let mut shortcuts = Shortcuts::new();
210 shortcuts.bind_global(KeyCode::Delete, KeyModifiers::NONE, Action::ToggleTheme);
211 shortcuts.bind_scoped(
212 Id::List,
213 KeyCode::Delete,
214 KeyModifiers::NONE,
215 Action::DeleteSelected,
216 );
217
218 assert_eq!(
219 shortcuts.resolve(&key(KeyCode::Delete), Some(Id::List)),
220 Some(Action::DeleteSelected)
221 );
222 assert_eq!(
224 shortcuts.resolve(&key(KeyCode::Delete), Some(Id::Search)),
225 Some(Action::ToggleTheme)
226 );
227 assert_eq!(
228 shortcuts.resolve(&key(KeyCode::Delete), None),
229 Some(Action::ToggleTheme)
230 );
231 }
232
233 #[test]
234 fn modifiers_must_match_exactly() {
235 let mut shortcuts = Shortcuts::<Id, Action>::new();
236 shortcuts.bind_global(
237 KeyCode::Char('s'),
238 KeyModifiers::CONTROL,
239 Action::ToggleTheme,
240 );
241
242 let ctrl_s = Event::Key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL));
243 assert_eq!(shortcuts.resolve(&ctrl_s, None), Some(Action::ToggleTheme));
244 assert_eq!(shortcuts.resolve(&key(KeyCode::Char('s')), None), None);
245 }
246
247 #[test]
248 fn ignores_non_key_and_key_up_events() {
249 let mut shortcuts = Shortcuts::<Id, Action>::new();
250 shortcuts.bind_global(KeyCode::Char('t'), KeyModifiers::NONE, Action::ToggleTheme);
251
252 assert_eq!(shortcuts.resolve(&Event::Close, None), None);
253
254 let released = Event::Key(KeyEvent::with_kind(
255 KeyCode::Char('t'),
256 KeyModifiers::NONE,
257 retroglyph_core::event::KeyEventKind::Release,
258 ));
259 assert_eq!(shortcuts.resolve(&released, None), None);
260 }
261
262 #[test]
263 fn empty_registry_resolves_nothing() {
264 let shortcuts = Shortcuts::<Id, Action>::new();
265 assert_eq!(shortcuts.resolve(&key(KeyCode::Char('t')), None), None);
266 }
267}