use retroglyph_core::{Event, KeyCode, KeyModifiers};
#[derive(Debug, Clone, Copy)]
struct Binding<Id, Action> {
scope: Option<Id>,
code: KeyCode,
modifiers: KeyModifiers,
action: Action,
}
#[derive(Debug, Clone)]
pub struct Shortcuts<Id, Action> {
bindings: Vec<Binding<Id, Action>>,
}
impl<Id, Action> Shortcuts<Id, Action> {
#[must_use]
pub const fn new() -> Self {
Self {
bindings: Vec::new(),
}
}
}
impl<Id: Copy + PartialEq, Action: Copy> Shortcuts<Id, Action> {
pub fn bind_global(&mut self, code: KeyCode, modifiers: KeyModifiers, action: Action) {
self.bindings.push(Binding {
scope: None,
code,
modifiers,
action,
});
}
pub fn bind_scoped(&mut self, id: Id, code: KeyCode, modifiers: KeyModifiers, action: Action) {
self.bindings.push(Binding {
scope: Some(id),
code,
modifiers,
action,
});
}
#[must_use]
pub fn resolve(&self, event: &Event, focused: Option<Id>) -> Option<Action> {
let Event::Key(key) = event else {
return None;
};
if !key.is_down() {
return None;
}
let matches = |b: &&Binding<Id, Action>| b.code == key.code && b.modifiers == key.modifiers;
if let Some(focused) = focused
&& let Some(binding) = self
.bindings
.iter()
.find(|b| b.scope == Some(focused) && matches(b))
{
return Some(binding.action);
}
self.bindings
.iter()
.find(|b| b.scope.is_none() && matches(b))
.map(|b| b.action)
}
}
impl<Id, Action> Default for Shortcuts<Id, Action> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::KeyEvent;
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Id {
List,
Search,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Action {
ToggleTheme,
ClearSearch,
DeleteSelected,
}
fn key(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
#[test]
fn global_binding_fires_regardless_of_focus() {
let mut shortcuts = Shortcuts::new();
shortcuts.bind_global(KeyCode::Char('t'), KeyModifiers::NONE, Action::ToggleTheme);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Char('t')), None),
Some(Action::ToggleTheme)
);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Char('t')), Some(Id::List)),
Some(Action::ToggleTheme)
);
}
#[test]
fn scoped_binding_only_fires_while_its_id_is_focused() {
let mut shortcuts = Shortcuts::new();
shortcuts.bind_scoped(
Id::Search,
KeyCode::Escape,
KeyModifiers::NONE,
Action::ClearSearch,
);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Escape), Some(Id::Search)),
Some(Action::ClearSearch)
);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Escape), Some(Id::List)),
None
);
assert_eq!(shortcuts.resolve(&key(KeyCode::Escape), None), None);
}
#[test]
fn scoped_binding_takes_priority_over_a_global_one_for_the_same_key() {
let mut shortcuts = Shortcuts::new();
shortcuts.bind_global(KeyCode::Delete, KeyModifiers::NONE, Action::ToggleTheme);
shortcuts.bind_scoped(
Id::List,
KeyCode::Delete,
KeyModifiers::NONE,
Action::DeleteSelected,
);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Delete), Some(Id::List)),
Some(Action::DeleteSelected)
);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Delete), Some(Id::Search)),
Some(Action::ToggleTheme)
);
assert_eq!(
shortcuts.resolve(&key(KeyCode::Delete), None),
Some(Action::ToggleTheme)
);
}
#[test]
fn modifiers_must_match_exactly() {
let mut shortcuts = Shortcuts::<Id, Action>::new();
shortcuts.bind_global(
KeyCode::Char('s'),
KeyModifiers::CONTROL,
Action::ToggleTheme,
);
let ctrl_s = Event::Key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL));
assert_eq!(shortcuts.resolve(&ctrl_s, None), Some(Action::ToggleTheme));
assert_eq!(shortcuts.resolve(&key(KeyCode::Char('s')), None), None);
}
#[test]
fn ignores_non_key_and_key_up_events() {
let mut shortcuts = Shortcuts::<Id, Action>::new();
shortcuts.bind_global(KeyCode::Char('t'), KeyModifiers::NONE, Action::ToggleTheme);
assert_eq!(shortcuts.resolve(&Event::Close, None), None);
let released = Event::Key(KeyEvent::with_kind(
KeyCode::Char('t'),
KeyModifiers::NONE,
retroglyph_core::KeyEventKind::Release,
));
assert_eq!(shortcuts.resolve(&released, None), None);
}
#[test]
fn empty_registry_resolves_nothing() {
let shortcuts = Shortcuts::<Id, Action>::new();
assert_eq!(shortcuts.resolve(&key(KeyCode::Char('t')), None), None);
}
}