use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::focus::FocusHandle;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum InputEvent {
Key(KeyEvent),
Paste(String),
Mouse(crossterm::event::MouseEvent),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Key {
pub code: KeyCode,
pub mods: KeyModifiers,
}
pub fn key(code: KeyCode) -> Key {
Key {
code,
mods: KeyModifiers::NONE,
}
}
impl Key {
pub fn ctrl(mut self) -> Self {
self.mods |= KeyModifiers::CONTROL;
self
}
pub fn shift(mut self) -> Self {
self.mods |= KeyModifiers::SHIFT;
self
}
pub fn alt(mut self) -> Self {
self.mods |= KeyModifiers::ALT;
self
}
fn matches(&self, event: &KeyEvent) -> bool {
self.code == event.code && self.mods == event.modifiers
}
}
enum Scope {
Override,
Focus(FocusHandle),
Global,
}
type FallthroughFn<Msg> = Box<dyn Fn(InputEvent) -> Msg + Send>;
pub struct Keymap<Msg> {
bindings: Vec<(Scope, Key, Msg)>,
fallthrough: Vec<(FocusHandle, FallthroughFn<Msg>)>,
}
impl<Msg> Default for Keymap<Msg> {
fn default() -> Self {
Self::new()
}
}
pub fn keymap<Msg>() -> Keymap<Msg> {
Keymap::new()
}
impl<Msg> Keymap<Msg> {
pub fn new() -> Self {
Self {
bindings: Vec::new(),
fallthrough: Vec::new(),
}
}
pub fn on(mut self, key: Key, msg: Msg) -> Self {
self.bindings.push((Scope::Global, key, msg));
self
}
pub fn on_override(mut self, key: Key, msg: Msg) -> Self {
self.bindings.push((Scope::Override, key, msg));
self
}
pub fn in_scope(mut self, focus: &FocusHandle, key: Key, msg: Msg) -> Self {
self.bindings.push((Scope::Focus(focus.clone()), key, msg));
self
}
pub fn fallthrough(
mut self,
focus: &FocusHandle,
map: impl Fn(InputEvent) -> Msg + Send + 'static,
) -> Self {
self.fallthrough.push((focus.clone(), Box::new(map)));
self
}
pub fn merge(mut self, other: Keymap<Msg>) -> Self {
self.bindings.extend(other.bindings);
self.fallthrough.extend(other.fallthrough);
self
}
pub fn map<M2>(self, f: impl Fn(Msg) -> M2 + Clone + Send + 'static) -> Keymap<M2>
where
Msg: 'static,
{
Keymap {
bindings: self
.bindings
.into_iter()
.map(|(scope, key, msg)| (scope, key, f(msg)))
.collect(),
fallthrough: self
.fallthrough
.into_iter()
.map(|(focus, g)| {
let f = f.clone();
let mapped: FallthroughFn<M2> = Box::new(move |ev| f(g(ev)));
(focus, mapped)
})
.collect(),
}
}
}
impl<Msg: Clone> Keymap<Msg> {
pub fn dispatch(&self, event: &InputEvent) -> Option<Msg> {
if let InputEvent::Key(k) = event {
for (scope, key, msg) in &self.bindings {
if matches!(scope, Scope::Override) && key.matches(k) {
return Some(msg.clone());
}
}
for (scope, key, msg) in &self.bindings {
if let Scope::Focus(handle) = scope
&& handle.is_focused()
&& key.matches(k)
{
return Some(msg.clone());
}
}
for (scope, key, msg) in &self.bindings {
if matches!(scope, Scope::Global) && key.matches(k) {
return Some(msg.clone());
}
}
}
for (handle, map) in &self.fallthrough {
if handle.is_focused() {
return Some(map(event.clone()));
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::focus::Focus;
fn press(code: KeyCode) -> InputEvent {
InputEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
fn press_mod(code: KeyCode, mods: KeyModifiers) -> InputEvent {
InputEvent::Key(KeyEvent::new(code, mods))
}
#[derive(Clone, Debug, PartialEq)]
enum Msg {
Interrupt,
Submit,
GlobalHelp,
Edit(char),
}
fn edit_msg(ev: InputEvent) -> Msg {
match ev {
InputEvent::Key(k) => {
if let KeyCode::Char(c) = k.code {
Msg::Edit(c)
} else {
Msg::Edit('?')
}
}
InputEvent::Paste(_) => Msg::Edit('P'),
_ => Msg::Edit('?'),
}
}
#[test]
fn merge_appends_and_earlier_declarations_win() {
let child = keymap()
.on(key(KeyCode::Up), Msg::Edit('u'))
.on(key(KeyCode::Enter), Msg::Edit('!'));
let km = keymap().on(key(KeyCode::Enter), Msg::Submit).merge(child);
assert_eq!(km.dispatch(&press(KeyCode::Up)), Some(Msg::Edit('u')));
assert_eq!(km.dispatch(&press(KeyCode::Enter)), Some(Msg::Submit));
}
#[test]
fn dispatch_order_override_scoped_global_fallthrough() {
let focus = Focus::new();
let input = focus.handle();
input.focus();
let km = keymap()
.on_override(key(KeyCode::Char('c')).ctrl(), Msg::Interrupt)
.in_scope(&input, key(KeyCode::Enter), Msg::Submit)
.on(key(KeyCode::Char('h')), Msg::GlobalHelp)
.fallthrough(&input, edit_msg);
assert_eq!(
km.dispatch(&press_mod(KeyCode::Char('c'), KeyModifiers::CONTROL)),
Some(Msg::Interrupt)
);
assert_eq!(km.dispatch(&press(KeyCode::Enter)), Some(Msg::Submit));
assert_eq!(
km.dispatch(&press(KeyCode::Char('h'))),
Some(Msg::GlobalHelp)
);
assert_eq!(
km.dispatch(&press(KeyCode::Char('x'))),
Some(Msg::Edit('x'))
);
assert_eq!(
km.dispatch(&InputEvent::Paste("hi".into())),
Some(Msg::Edit('P'))
);
}
#[test]
fn mouse_events_reach_the_fallthrough() {
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let focus = Focus::new();
let input = focus.handle();
input.focus();
let km: Keymap<&'static str> = keymap().fallthrough(&input, |ev| match ev {
InputEvent::Mouse(m) => match m.kind {
MouseEventKind::ScrollUp => "scroll-up",
MouseEventKind::ScrollDown => "scroll-down",
_ => "other-mouse",
},
_ => "not-mouse",
});
let scroll = |kind| {
InputEvent::Mouse(MouseEvent {
kind,
column: 3,
row: 5,
modifiers: KeyModifiers::NONE,
})
};
assert_eq!(
km.dispatch(&scroll(MouseEventKind::ScrollUp)),
Some("scroll-up")
);
assert_eq!(
km.dispatch(&scroll(MouseEventKind::ScrollDown)),
Some("scroll-down")
);
assert_eq!(
km.dispatch(&scroll(MouseEventKind::Down(MouseButton::Left))),
Some("other-mouse")
);
}
#[test]
fn scoped_bindings_inactive_without_focus() {
let focus = Focus::new();
let input = focus.handle();
let km = keymap()
.in_scope(&input, key(KeyCode::Enter), Msg::Submit)
.fallthrough(&input, edit_msg);
assert_eq!(km.dispatch(&press(KeyCode::Enter)), None);
assert_eq!(km.dispatch(&press(KeyCode::Char('x'))), None);
}
#[test]
fn first_match_in_declaration_order_wins() {
let focus = Focus::new();
let input = focus.handle();
input.focus();
let km = keymap()
.in_scope(&input, key(KeyCode::Tab), Msg::Submit)
.in_scope(&input, key(KeyCode::Tab), Msg::GlobalHelp);
assert_eq!(km.dispatch(&press(KeyCode::Tab)), Some(Msg::Submit));
}
#[test]
fn map_retargets_bindings_and_fallthrough() {
#[derive(Clone, Debug, PartialEq)]
enum Outer {
Inner(Msg),
}
let focus = Focus::new();
let input = focus.handle();
input.focus();
let km = keymap()
.in_scope(&input, key(KeyCode::Enter), Msg::Submit)
.fallthrough(&input, edit_msg)
.map(Outer::Inner);
assert_eq!(
km.dispatch(&press(KeyCode::Enter)),
Some(Outer::Inner(Msg::Submit))
);
assert_eq!(
km.dispatch(&press(KeyCode::Char('z'))),
Some(Outer::Inner(Msg::Edit('z')))
);
}
#[test]
fn modifiers_must_match_exactly() {
let km: Keymap<Msg> = keymap().on(key(KeyCode::Enter), Msg::Submit);
assert_eq!(
km.dispatch(&press_mod(KeyCode::Enter, KeyModifiers::SHIFT)),
None
);
}
}