#![allow(dead_code)]
use quadraui::accelerator::{key_to_binding_name, parse_binding};
use quadraui::{
Accelerator, AcceleratorId, AcceleratorScope, Key, KeyBinding, Modifiers, ParsedBinding,
WidgetId,
};
#[derive(Debug, Clone)]
struct KeyMapEntry {
scope: AcceleratorScope,
binding: ParsedBinding,
action: AcceleratorId,
}
#[derive(Debug, Clone, Default)]
pub struct KeyMap {
entries: Vec<KeyMapEntry>,
}
impl KeyMap {
pub fn new() -> Self {
Self::default()
}
pub fn bind(
&mut self,
scope: AcceleratorScope,
binding: KeyBinding,
action: impl Into<AcceleratorId>,
) -> &mut Self {
if let Some(parsed) = parse_binding(&binding) {
self.entries.push(KeyMapEntry {
scope,
binding: parsed,
action: action.into(),
});
}
self
}
pub fn bind_accelerator(&mut self, acc: &Accelerator) -> &mut Self {
if let Some(parsed) = parse_binding(&acc.binding) {
self.entries.push(KeyMapEntry {
scope: acc.scope.clone(),
binding: parsed,
action: acc.id.clone(),
});
}
self
}
pub fn unbind(&mut self, action: &AcceleratorId) {
self.entries.retain(|e| &e.action != action);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn resolve(
&self,
key: &Key,
modifiers: Modifiers,
ctx: &KeyContext,
) -> Option<AcceleratorId> {
let key_name = key_to_binding_name(key);
self.entries
.iter()
.find(|e| {
e.binding.modifiers == modifiers
&& e.binding.key == key_name
&& ctx.scope_active(&e.scope)
})
.map(|e| e.action.clone())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct KeyContext<'a> {
pub focus_chain: &'a [WidgetId],
pub mode: Option<&'a str>,
pub global_blocked: bool,
}
impl<'a> KeyContext<'a> {
pub fn new() -> Self {
Self {
focus_chain: &[],
mode: None,
global_blocked: false,
}
}
pub fn with_focus_chain(mut self, chain: &'a [WidgetId]) -> Self {
self.focus_chain = chain;
self
}
pub fn with_mode(mut self, mode: &'a str) -> Self {
self.mode = Some(mode);
self
}
pub fn blocked(mut self) -> Self {
self.global_blocked = true;
self
}
fn scope_active(&self, scope: &AcceleratorScope) -> bool {
match scope {
AcceleratorScope::Global => !self.global_blocked,
AcceleratorScope::Widget(id) => self.focus_chain.contains(id),
AcceleratorScope::Mode(m) => self.mode == Some(m.as_str()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mods_none() -> Modifiers {
Modifiers::default()
}
fn ctrl() -> Modifiers {
Modifiers {
ctrl: true,
..Modifiers::default()
}
}
#[test]
fn empty_map_resolves_nothing() {
let map = KeyMap::new();
assert!(map.is_empty());
assert_eq!(
map.resolve(&Key::Char('p'), ctrl(), &KeyContext::new()),
None
);
}
#[test]
fn global_scope_matches_by_default() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("Ctrl+P".into()),
"palette.open",
);
assert_eq!(map.len(), 1);
let id = map
.resolve(&Key::Char('p'), ctrl(), &KeyContext::new())
.unwrap();
assert_eq!(id.as_str(), "palette.open");
}
#[test]
fn global_scope_blocked_does_not_match() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("Ctrl+P".into()),
"palette.open",
);
let ctx = KeyContext::new().blocked();
assert_eq!(map.resolve(&Key::Char('p'), ctrl(), &ctx), None);
}
#[test]
fn widget_scope_matches_when_in_focus_chain() {
let mut map = KeyMap::new();
let modal_id = WidgetId::new("modal:confirm");
map.bind(
AcceleratorScope::Widget(modal_id.clone()),
KeyBinding::Literal("Escape".into()),
"modal.close",
);
let chain = [WidgetId::new("modal:confirm:input"), modal_id.clone()];
let ctx = KeyContext::new().with_focus_chain(&chain);
let id = map
.resolve(&Key::Named(quadraui::NamedKey::Escape), mods_none(), &ctx)
.unwrap();
assert_eq!(id.as_str(), "modal.close");
}
#[test]
fn widget_scope_does_not_match_when_absent_from_focus_chain() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Widget(WidgetId::new("modal:confirm")),
KeyBinding::Literal("Escape".into()),
"modal.close",
);
let chain = [WidgetId::new("editor:main")];
let ctx = KeyContext::new().with_focus_chain(&chain);
assert_eq!(
map.resolve(&Key::Named(quadraui::NamedKey::Escape), mods_none(), &ctx),
None
);
}
#[test]
fn mode_scope_matches_active_mode_only() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Mode("n".into()),
KeyBinding::Literal("d".into()),
"vim.delete_line",
);
let ctx_normal = KeyContext::new().with_mode("n");
assert_eq!(
map.resolve(&Key::Char('d'), mods_none(), &ctx_normal)
.as_ref()
.map(AcceleratorId::as_str),
Some("vim.delete_line")
);
let ctx_insert = KeyContext::new().with_mode("i");
assert_eq!(map.resolve(&Key::Char('d'), mods_none(), &ctx_insert), None);
}
#[test]
fn earlier_binding_wins_when_both_scopes_active() {
let mut map = KeyMap::new();
let palette_id = WidgetId::new("palette");
map.bind(
AcceleratorScope::Widget(palette_id.clone()),
KeyBinding::Literal("Escape".into()),
"palette.close",
);
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("Escape".into()),
"app.blur",
);
let chain = [palette_id.clone()];
let ctx_focused = KeyContext::new().with_focus_chain(&chain);
assert_eq!(
map.resolve(
&Key::Named(quadraui::NamedKey::Escape),
mods_none(),
&ctx_focused
)
.as_ref()
.map(AcceleratorId::as_str),
Some("palette.close")
);
}
#[test]
fn falls_through_to_next_entry_when_first_scope_inactive() {
let mut map = KeyMap::new();
let palette_id = WidgetId::new("palette");
map.bind(
AcceleratorScope::Widget(palette_id),
KeyBinding::Literal("Escape".into()),
"palette.close",
);
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("Escape".into()),
"app.blur",
);
let ctx = KeyContext::new();
assert_eq!(
map.resolve(&Key::Named(quadraui::NamedKey::Escape), mods_none(), &ctx)
.as_ref()
.map(AcceleratorId::as_str),
Some("app.blur")
);
}
#[test]
fn bind_accelerator_shares_scope_and_id() {
let mut map = KeyMap::new();
let acc = Accelerator {
id: AcceleratorId::new("editor.save"),
binding: KeyBinding::Save,
scope: AcceleratorScope::Global,
label: None,
};
map.bind_accelerator(&acc);
let id = map
.resolve(&Key::Char('s'), ctrl(), &KeyContext::new())
.unwrap();
assert_eq!(id, acc.id);
}
#[test]
fn unparseable_literal_is_skipped_silently() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("".into()),
"noop",
);
assert!(map.is_empty());
}
#[test]
fn unbind_removes_all_entries_for_action() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("Ctrl+P".into()),
"palette.open",
);
map.bind(
AcceleratorScope::Mode("n".into()),
KeyBinding::Literal("Ctrl+P".into()),
"palette.open",
);
assert_eq!(map.len(), 2);
map.unbind(&AcceleratorId::new("palette.open"));
assert!(map.is_empty());
}
#[test]
fn modifier_mismatch_does_not_match() {
let mut map = KeyMap::new();
map.bind(
AcceleratorScope::Global,
KeyBinding::Literal("Ctrl+P".into()),
"palette.open",
);
assert_eq!(
map.resolve(&Key::Char('p'), mods_none(), &KeyContext::new()),
None
);
}
}