Skip to main content

fresh/view/controls/keybinding_list/
input.rs

1//! Keybinding list input handling
2
3use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{KeybindingListHit, KeybindingListLayout, KeybindingListState};
6
7/// Events that can be returned from keybinding list input handling
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum KeybindingListEvent {
10    /// A binding was removed
11    BindingRemoved(usize),
12    /// Focus moved to a different entry
13    FocusChanged(Option<usize>),
14    /// Add new binding requested (user clicked add row or pressed Enter on it)
15    AddRequested,
16    /// Edit binding requested (user pressed Enter on a binding)
17    EditRequested(usize),
18}
19
20impl KeybindingListState {
21    /// Handle a mouse event for this keybinding list
22    ///
23    /// # Arguments
24    /// * `event` - The mouse event to handle
25    /// * `layout` - The control's rendered layout for hit testing
26    ///
27    /// # Returns
28    /// * `Some(KeybindingListEvent)` if the event was consumed
29    /// * `None` if the event was not relevant
30    pub fn handle_mouse(
31        &mut self,
32        event: MouseEvent,
33        layout: &KeybindingListLayout,
34    ) -> Option<KeybindingListEvent> {
35        if !self.is_enabled() {
36            return None;
37        }
38
39        if let MouseEventKind::Down(MouseButton::Left) = event.kind {
40            if let Some(hit) = layout.hit_test(event.column, event.row) {
41                match hit {
42                    KeybindingListHit::Entry(index) => {
43                        self.focus_entry(index);
44                        return Some(KeybindingListEvent::FocusChanged(Some(index)));
45                    }
46                    KeybindingListHit::AddRow => {
47                        self.focus_add_row();
48                        return Some(KeybindingListEvent::FocusChanged(None));
49                    }
50                }
51            }
52        }
53        None
54    }
55
56    /// Handle a keyboard event for this keybinding list
57    ///
58    /// # Returns
59    /// * `Some(KeybindingListEvent)` if the event was consumed
60    /// * `None` if the event was not relevant
61    pub fn handle_key(&mut self, key: KeyEvent) -> Option<KeybindingListEvent> {
62        if !self.is_enabled() {
63            return None;
64        }
65
66        match key.code {
67            KeyCode::Enter => match self.focused_index {
68                // On add row
69                None => Some(KeybindingListEvent::AddRequested),
70                // On an entry - request edit
71                Some(index) => Some(KeybindingListEvent::EditRequested(index)),
72            },
73            KeyCode::Delete | KeyCode::Backspace => {
74                if let Some(index) = self.focused_index {
75                    self.remove_binding(index);
76                    Some(KeybindingListEvent::BindingRemoved(index))
77                } else {
78                    None
79                }
80            }
81            KeyCode::Up | KeyCode::Char('k') => {
82                self.focus_prev();
83                Some(KeybindingListEvent::FocusChanged(self.focused_index))
84            }
85            KeyCode::Down | KeyCode::Char('j') => {
86                self.focus_next();
87                Some(KeybindingListEvent::FocusChanged(self.focused_index))
88            }
89            _ => None,
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crossterm::event::KeyModifiers;
98    use ratatui::layout::Rect;
99
100    fn make_layout() -> KeybindingListLayout {
101        KeybindingListLayout {
102            entry_rects: vec![(0, Rect::new(2, 1, 40, 1)), (1, Rect::new(2, 2, 40, 1))],
103            add_rect: Some(Rect::new(2, 3, 40, 1)),
104        }
105    }
106
107    fn mouse_down(x: u16, y: u16) -> MouseEvent {
108        MouseEvent {
109            kind: MouseEventKind::Down(MouseButton::Left),
110            column: x,
111            row: y,
112            modifiers: KeyModifiers::empty(),
113        }
114    }
115
116    #[test]
117    fn test_click_entry() {
118        let mut state = KeybindingListState::new("Test");
119        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
120        let layout = make_layout();
121
122        let result = state.handle_mouse(mouse_down(10, 1), &layout);
123        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(0))));
124        assert_eq!(state.focused_index, Some(0));
125    }
126
127    #[test]
128    fn test_click_add_row() {
129        let mut state = KeybindingListState::new("Test");
130        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
131        state.focus_entry(0);
132        let layout = make_layout();
133
134        let result = state.handle_mouse(mouse_down(10, 3), &layout);
135        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(None)));
136        assert!(state.focused_index.is_none());
137    }
138
139    #[test]
140    fn test_keyboard_navigation() {
141        let mut state = KeybindingListState::new("Test");
142        state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
143        state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
144
145        let down = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
146        let result = state.handle_key(down);
147        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(0))));
148
149        let result = state.handle_key(down);
150        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(1))));
151
152        let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
153        let result = state.handle_key(up);
154        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(0))));
155    }
156
157    #[test]
158    fn test_enter_on_add_row() {
159        let mut state = KeybindingListState::new("Test");
160
161        let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
162        let result = state.handle_key(enter);
163        assert_eq!(result, Some(KeybindingListEvent::AddRequested));
164    }
165
166    #[test]
167    fn test_enter_on_entry() {
168        let mut state = KeybindingListState::new("Test");
169        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
170        state.focus_entry(0);
171
172        let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
173        let result = state.handle_key(enter);
174        assert_eq!(result, Some(KeybindingListEvent::EditRequested(0)));
175    }
176
177    #[test]
178    fn test_delete_removes_focused() {
179        let mut state = KeybindingListState::new("Test");
180        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
181        state.focus_entry(0);
182
183        let delete = KeyEvent::new(KeyCode::Delete, KeyModifiers::empty());
184        let result = state.handle_key(delete);
185        assert_eq!(result, Some(KeybindingListEvent::BindingRemoved(0)));
186        assert!(state.bindings.is_empty());
187    }
188}