use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use super::state::{HotkeyDialogState, HotkeyFocus};
use super::traits::HotkeyCategory;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HotkeyDialogAction {
None,
Close,
EntrySelected {
key_combination: String,
action: String,
context: String,
},
ScrollUp(usize),
ScrollDown(usize),
}
pub fn handle_hotkey_dialog_key<C: HotkeyCategory>(
state: &mut HotkeyDialogState<C>,
key: KeyEvent,
) -> HotkeyDialogAction {
if key.code == KeyCode::Esc {
if state.focus == HotkeyFocus::SearchInput && !state.search_query.is_empty() {
state.clear_search();
return HotkeyDialogAction::None;
}
return HotkeyDialogAction::Close;
}
if key.code == KeyCode::Tab {
if key.modifiers.contains(KeyModifiers::SHIFT) {
state.focus_prev();
} else {
state.focus_next();
}
return HotkeyDialogAction::None;
}
if key.code == KeyCode::BackTab {
state.focus_prev();
return HotkeyDialogAction::None;
}
match state.focus {
HotkeyFocus::SearchInput => handle_search_input_key(state, key),
HotkeyFocus::CategoryList => handle_category_list_key(state, key),
HotkeyFocus::HotkeyList => handle_hotkey_list_key(state, key),
}
}
fn handle_search_input_key<C: HotkeyCategory>(
state: &mut HotkeyDialogState<C>,
key: KeyEvent,
) -> HotkeyDialogAction {
match key.code {
KeyCode::Char(c) => {
state.insert_char(c);
}
KeyCode::Backspace => {
state.delete_char_backward();
}
KeyCode::Delete => {
state.delete_char_forward();
}
KeyCode::Left => {
state.move_cursor_left();
}
KeyCode::Right => {
state.move_cursor_right();
}
KeyCode::Home => {
state.move_cursor_home();
}
KeyCode::End => {
state.move_cursor_end();
}
KeyCode::Enter => {
state.focus = HotkeyFocus::HotkeyList;
}
_ => {}
}
HotkeyDialogAction::None
}
fn handle_category_list_key<C: HotkeyCategory>(
state: &mut HotkeyDialogState<C>,
key: KeyEvent,
) -> HotkeyDialogAction {
match key.code {
KeyCode::Up => {
state.prev_category();
}
KeyCode::Down => {
state.next_category();
}
KeyCode::Enter | KeyCode::Right => {
state.focus = HotkeyFocus::HotkeyList;
}
_ => {}
}
HotkeyDialogAction::None
}
fn handle_hotkey_list_key<C: HotkeyCategory>(
state: &mut HotkeyDialogState<C>,
key: KeyEvent,
) -> HotkeyDialogAction {
match key.code {
KeyCode::Up => {
state.prev_hotkey();
}
KeyCode::Down => {
state.next_hotkey();
}
KeyCode::PageUp => {
state.page_up();
}
KeyCode::PageDown => {
state.page_down();
}
KeyCode::Left => {
state.focus = HotkeyFocus::CategoryList;
}
KeyCode::Enter => {
return HotkeyDialogAction::EntrySelected {
key_combination: String::new(), action: String::new(),
context: String::new(),
};
}
_ => {}
}
HotkeyDialogAction::None
}
pub fn handle_hotkey_dialog_mouse<C: HotkeyCategory>(
state: &mut HotkeyDialogState<C>,
mouse: MouseEvent,
) -> HotkeyDialogAction {
match mouse.kind {
MouseEventKind::ScrollUp => {
state.scroll_hotkeys_up(3);
HotkeyDialogAction::ScrollUp(3)
}
MouseEventKind::ScrollDown => {
state.scroll_hotkeys_down(3);
HotkeyDialogAction::ScrollDown(3)
}
MouseEventKind::Down(MouseButton::Left) => {
state.handle_click(mouse.column, mouse.row);
HotkeyDialogAction::None
}
_ => HotkeyDialogAction::None,
}
}
pub fn is_close_key(key: &KeyEvent) -> bool {
key.code == KeyCode::Esc
}
pub fn is_navigation_key(key: &KeyEvent) -> bool {
matches!(key.code, KeyCode::Tab | KeyCode::BackTab)
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
enum TestCategory {
#[default]
First,
Second,
}
impl HotkeyCategory for TestCategory {
fn all() -> &'static [Self] {
&[Self::First, Self::Second]
}
fn display_name(&self) -> &str {
match self {
Self::First => "First",
Self::Second => "Second",
}
}
fn next(&self) -> Self {
match self {
Self::First => Self::Second,
Self::Second => Self::First,
}
}
fn prev(&self) -> Self {
match self {
Self::First => Self::Second,
Self::Second => Self::First,
}
}
}
fn key_event(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::empty())
}
fn key_event_shift(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::SHIFT)
}
#[test]
fn test_escape_closes_dialog() {
let mut state: HotkeyDialogState<TestCategory> = HotkeyDialogState::new();
let action = handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Esc));
assert_eq!(action, HotkeyDialogAction::Close);
}
#[test]
fn test_escape_clears_search_first() {
let mut state: HotkeyDialogState<TestCategory> = HotkeyDialogState::new();
state.focus = HotkeyFocus::SearchInput;
state.insert_char('a');
let action = handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Esc));
assert_eq!(action, HotkeyDialogAction::None);
assert!(state.search_query.is_empty());
let action = handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Esc));
assert_eq!(action, HotkeyDialogAction::Close);
}
#[test]
fn test_tab_cycles_focus() {
let mut state: HotkeyDialogState<TestCategory> = HotkeyDialogState::new();
state.focus = HotkeyFocus::CategoryList;
handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Tab));
assert_eq!(state.focus, HotkeyFocus::HotkeyList);
handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Tab));
assert_eq!(state.focus, HotkeyFocus::SearchInput);
handle_hotkey_dialog_key(&mut state, key_event_shift(KeyCode::Tab));
assert_eq!(state.focus, HotkeyFocus::HotkeyList);
}
#[test]
fn test_category_navigation() {
let mut state: HotkeyDialogState<TestCategory> = HotkeyDialogState::new();
state.focus = HotkeyFocus::CategoryList;
handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Down));
assert_eq!(state.selected_category, TestCategory::Second);
handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Up));
assert_eq!(state.selected_category, TestCategory::First);
}
#[test]
fn test_search_input() {
let mut state: HotkeyDialogState<TestCategory> = HotkeyDialogState::new();
state.focus = HotkeyFocus::SearchInput;
handle_hotkey_dialog_key(
&mut state,
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()),
);
handle_hotkey_dialog_key(
&mut state,
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty()),
);
assert_eq!(state.search_query, "ab");
handle_hotkey_dialog_key(&mut state, key_event(KeyCode::Backspace));
assert_eq!(state.search_query, "a");
}
}