use crate::event::{KeyEvent, KeyEventKind};
use crate::{KeyCode, KeyModifiers, ModifierKey};
#[derive(Debug, Clone)]
pub struct Binding {
pub key: KeyCode,
pub modifiers: Option<KeyModifiers>,
pub display: String,
pub description: String,
pub visible: bool,
}
impl Binding {
pub fn matches(&self, key: &KeyEvent) -> bool {
if key.kind != KeyEventKind::Press {
return false;
}
if key.code != self.key {
return false;
}
match self.modifiers {
None => key.modifiers == KeyModifiers::NONE,
Some(mods) => key.modifiers.contains(mods),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct KeyMap {
pub bindings: Vec<Binding>,
}
impl KeyMap {
pub fn new() -> Self {
Self::default()
}
pub fn bind(mut self, key: char, description: &str) -> Self {
self.bindings.push(Binding {
key: KeyCode::Char(key),
modifiers: None,
display: key.to_string(),
description: description.to_string(),
visible: true,
});
self
}
pub fn bind_code(mut self, key: KeyCode, description: &str) -> Self {
self.bindings.push(Binding {
display: display_for_key_code(&key),
key,
modifiers: None,
description: description.to_string(),
visible: true,
});
self
}
pub fn bind_mod(mut self, key: char, mods: KeyModifiers, description: &str) -> Self {
self.bindings.push(Binding {
key: KeyCode::Char(key),
modifiers: Some(mods),
display: display_for_mod_char(mods, key),
description: description.to_string(),
visible: true,
});
self
}
pub fn bind_code_mod(mut self, key: KeyCode, mods: KeyModifiers, description: &str) -> Self {
let display = display_for_code_mod(&key, mods);
self.bindings.push(Binding {
key,
modifiers: Some(mods),
display,
description: description.to_string(),
visible: true,
});
self
}
pub fn bind_hidden(mut self, key: char, description: &str) -> Self {
self.bindings.push(Binding {
key: KeyCode::Char(key),
modifiers: None,
display: key.to_string(),
description: description.to_string(),
visible: false,
});
self
}
pub fn visible_bindings(&self) -> impl Iterator<Item = &Binding> {
self.bindings.iter().filter(|binding| binding.visible)
}
pub fn matched(&self, key: &KeyEvent) -> Option<&Binding> {
self.bindings.iter().find(|binding| binding.matches(key))
}
}
fn display_for_key_code(key: &KeyCode) -> String {
match key {
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::BackTab => "Shift+Tab".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Up => "↑".to_string(),
KeyCode::Down => "↓".to_string(),
KeyCode::Left => "←".to_string(),
KeyCode::Right => "→".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PgUp".to_string(),
KeyCode::PageDown => "PgDn".to_string(),
KeyCode::Delete => "Del".to_string(),
KeyCode::Insert => "Ins".to_string(),
KeyCode::Null => "Null".to_string(),
KeyCode::CapsLock => "CapsLock".to_string(),
KeyCode::ScrollLock => "ScrollLock".to_string(),
KeyCode::NumLock => "NumLock".to_string(),
KeyCode::PrintScreen => "PrtSc".to_string(),
KeyCode::Pause => "Pause".to_string(),
KeyCode::Menu => "Menu".to_string(),
KeyCode::KeypadBegin => "KP5".to_string(),
KeyCode::F(n) => format!("F{n}"),
KeyCode::Modifier(m) => display_for_modifier_key(*m).to_string(),
}
}
fn display_for_modifier_key(m: ModifierKey) -> &'static str {
match m {
ModifierKey::LeftShift => "LShift",
ModifierKey::LeftCtrl => "LCtrl",
ModifierKey::LeftAlt => "LAlt",
ModifierKey::LeftSuper => "LSuper",
ModifierKey::RightShift => "RShift",
ModifierKey::RightCtrl => "RCtrl",
ModifierKey::RightAlt => "RAlt",
ModifierKey::RightSuper => "RSuper",
ModifierKey::LeftHyper => "LHyper",
ModifierKey::LeftMeta => "LMeta",
ModifierKey::RightHyper => "RHyper",
ModifierKey::RightMeta => "RMeta",
ModifierKey::IsoLevel3Shift => "ISO3",
ModifierKey::IsoLevel5Shift => "ISO5",
}
}
fn display_for_code_mod(key: &KeyCode, mods: KeyModifiers) -> String {
let mut parts: Vec<&str> = Vec::new();
if mods.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl");
}
if mods.contains(KeyModifiers::ALT) {
parts.push("Alt");
}
if mods.contains(KeyModifiers::SHIFT) {
parts.push("Shift");
}
if mods.contains(KeyModifiers::SUPER) {
parts.push("Super");
}
if mods.contains(KeyModifiers::HYPER) {
parts.push("Hyper");
}
if mods.contains(KeyModifiers::META) {
parts.push("Meta");
}
let key_label = display_for_key_code(key);
if parts.is_empty() {
key_label
} else {
format!("{}+{}", parts.join("+"), key_label)
}
}
fn display_for_mod_char(mods: KeyModifiers, key: char) -> String {
let mut parts: Vec<&str> = Vec::new();
if mods.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl");
}
if mods.contains(KeyModifiers::ALT) {
parts.push("Alt");
}
if mods.contains(KeyModifiers::SHIFT) {
parts.push("Shift");
}
if mods.contains(KeyModifiers::SUPER) {
parts.push("Super");
}
if mods.contains(KeyModifiers::HYPER) {
parts.push("Hyper");
}
if mods.contains(KeyModifiers::META) {
parts.push("Meta");
}
if parts.is_empty() {
key.to_string()
} else {
format!("{}+{}", parts.join("+"), key.to_ascii_uppercase())
}
}
pub trait WidgetKeyHelp {
fn key_help(&self) -> &'static [(&'static str, &'static str)];
}
#[derive(Debug, Clone)]
pub struct PublishedKeymap {
pub name: &'static str,
pub bindings: &'static [(&'static str, &'static str)],
}
impl PublishedKeymap {
pub const fn new(
name: &'static str,
bindings: &'static [(&'static str, &'static str)],
) -> Self {
Self { name, bindings }
}
}
impl crate::Context {
pub fn keymap_match<'m>(&self, map: &'m KeyMap) -> Option<&'m Binding> {
if (self.rollback.modal_active || self.prev_modal_active)
&& self.rollback.overlay_depth == 0
{
return None;
}
self.available_key_presses()
.find_map(|(_, key)| map.matched(key))
}
}
#[cfg(test)]
mod dispatch_tests {
use super::*;
use crate::TestBackend;
use crate::event::Event;
fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
match Event::key_mod(code, modifiers) {
Event::Key(k) => k,
_ => unreachable!("key_mod always builds a Key event"),
}
}
fn release_event(c: char) -> KeyEvent {
match Event::key_release(c) {
Event::Key(k) => k,
_ => unreachable!("key_release always builds a Key event"),
}
}
#[test]
fn binding_matches_plain_char() {
let km = KeyMap::new().bind('q', "Quit");
let binding = &km.bindings[0];
assert!(binding.matches(&key_event(KeyCode::Char('q'), KeyModifiers::NONE)));
assert!(!binding.matches(&key_event(KeyCode::Char('x'), KeyModifiers::NONE)));
assert!(!binding.matches(&key_event(KeyCode::Char('q'), KeyModifiers::CONTROL)));
}
#[test]
fn binding_matches_modifier_chord_contains() {
let km = KeyMap::new().bind_mod('s', KeyModifiers::CONTROL, "Save");
let binding = &km.bindings[0];
assert!(binding.matches(&key_event(KeyCode::Char('s'), KeyModifiers::CONTROL)));
let ctrl_shift = KeyModifiers(KeyModifiers::CONTROL.0 | KeyModifiers::SHIFT.0);
assert!(binding.matches(&key_event(KeyCode::Char('s'), ctrl_shift)));
assert!(!binding.matches(&key_event(KeyCode::Char('s'), KeyModifiers::NONE)));
}
#[test]
fn binding_rejects_release_events() {
let km = KeyMap::new().bind('q', "Quit");
assert!(!km.bindings[0].matches(&release_event('q')));
}
#[test]
fn matched_returns_first_registered_binding() {
let km = KeyMap::new()
.bind('q', "Quit")
.bind_code(KeyCode::Up, "Move up")
.bind_mod('s', KeyModifiers::CONTROL, "Save");
let up = km
.matched(&key_event(KeyCode::Up, KeyModifiers::NONE))
.expect("Up should match");
assert_eq!(up.description, "Move up");
let save = km
.matched(&key_event(KeyCode::Char('s'), KeyModifiers::CONTROL))
.expect("Ctrl+S should match");
assert_eq!(save.description, "Save");
assert!(
km.matched(&key_event(KeyCode::Char('z'), KeyModifiers::NONE))
.is_none()
);
}
#[test]
fn matched_first_registration_wins_on_overlap() {
let km = KeyMap::new().bind('a', "First").bind('a', "Second");
let hit = km
.matched(&key_event(KeyCode::Char('a'), KeyModifiers::NONE))
.expect("'a' should match");
assert_eq!(hit.description, "First");
}
#[test]
fn context_keymap_match_returns_binding_for_frame_press() {
let km = KeyMap::new()
.bind('q', "Quit")
.bind_code(KeyCode::Up, "Move up");
let mut tb = TestBackend::new(20, 3);
tb.run_with_events(vec![Event::key(KeyCode::Up)], |ui| {
let hit = ui.keymap_match(&km);
ui.text(hit.map(|b| b.description.as_str()).unwrap_or("none"));
});
tb.assert_contains("Move up");
}
#[test]
fn context_keymap_match_none_when_no_press_matches() {
let km = KeyMap::new().bind('q', "Quit");
let mut tb = TestBackend::new(20, 3);
tb.run_with_events(vec![Event::key_char('z')], |ui| {
let hit = ui.keymap_match(&km);
ui.text(hit.map(|b| b.description.as_str()).unwrap_or("none"));
});
tb.assert_contains("none");
}
}