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::DeleteButton(index) => {
43                        self.remove_binding(index);
44                        return Some(KeybindingListEvent::BindingRemoved(index));
45                    }
46                    KeybindingListHit::Entry(index) => {
47                        self.focus_entry(index);
48                        return Some(KeybindingListEvent::FocusChanged(Some(index)));
49                    }
50                    KeybindingListHit::AddRow => {
51                        self.focus_add_row();
52                        return Some(KeybindingListEvent::FocusChanged(None));
53                    }
54                }
55            }
56        }
57        None
58    }
59
60    /// Handle a keyboard event for this keybinding list
61    ///
62    /// # Returns
63    /// * `Some(KeybindingListEvent)` if the event was consumed
64    /// * `None` if the event was not relevant
65    pub fn handle_key(&mut self, key: KeyEvent) -> Option<KeybindingListEvent> {
66        if !self.is_enabled() {
67            return None;
68        }
69
70        match key.code {
71            KeyCode::Enter => match self.focused_index {
72                // On add row
73                None => Some(KeybindingListEvent::AddRequested),
74                // On an entry - request edit
75                Some(index) => Some(KeybindingListEvent::EditRequested(index)),
76            },
77            KeyCode::Delete | KeyCode::Backspace => {
78                if let Some(index) = self.focused_index {
79                    self.remove_binding(index);
80                    Some(KeybindingListEvent::BindingRemoved(index))
81                } else {
82                    None
83                }
84            }
85            KeyCode::Up | KeyCode::Char('k') => {
86                self.focus_prev();
87                Some(KeybindingListEvent::FocusChanged(self.focused_index))
88            }
89            KeyCode::Down | KeyCode::Char('j') => {
90                self.focus_next();
91                Some(KeybindingListEvent::FocusChanged(self.focused_index))
92            }
93            _ => None,
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crossterm::event::KeyModifiers;
102    use ratatui::layout::Rect;
103
104    fn make_layout() -> KeybindingListLayout {
105        KeybindingListLayout {
106            entry_rects: vec![(0, Rect::new(2, 1, 40, 1)), (1, Rect::new(2, 2, 40, 1))],
107            delete_rects: vec![Rect::new(38, 1, 3, 1), Rect::new(38, 2, 3, 1)],
108            add_rect: Some(Rect::new(2, 3, 40, 1)),
109        }
110    }
111
112    fn mouse_down(x: u16, y: u16) -> MouseEvent {
113        MouseEvent {
114            kind: MouseEventKind::Down(MouseButton::Left),
115            column: x,
116            row: y,
117            modifiers: KeyModifiers::empty(),
118        }
119    }
120
121    #[test]
122    fn test_click_delete_button() {
123        let mut state = KeybindingListState::new("Test");
124        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
125        let layout = make_layout();
126
127        let result = state.handle_mouse(mouse_down(38, 1), &layout);
128        assert_eq!(result, Some(KeybindingListEvent::BindingRemoved(0)));
129        assert!(state.bindings.is_empty());
130    }
131
132    #[test]
133    fn test_click_entry() {
134        let mut state = KeybindingListState::new("Test");
135        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
136        let layout = make_layout();
137
138        let result = state.handle_mouse(mouse_down(10, 1), &layout);
139        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(0))));
140        assert_eq!(state.focused_index, Some(0));
141    }
142
143    #[test]
144    fn test_click_add_row() {
145        let mut state = KeybindingListState::new("Test");
146        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
147        state.focus_entry(0);
148        let layout = make_layout();
149
150        let result = state.handle_mouse(mouse_down(10, 3), &layout);
151        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(None)));
152        assert!(state.focused_index.is_none());
153    }
154
155    #[test]
156    fn test_keyboard_navigation() {
157        let mut state = KeybindingListState::new("Test");
158        state.add_binding(serde_json::json!({"key": "a", "action": "test1"}));
159        state.add_binding(serde_json::json!({"key": "b", "action": "test2"}));
160
161        let down = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
162        let result = state.handle_key(down);
163        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(0))));
164
165        let result = state.handle_key(down);
166        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(1))));
167
168        let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
169        let result = state.handle_key(up);
170        assert_eq!(result, Some(KeybindingListEvent::FocusChanged(Some(0))));
171    }
172
173    #[test]
174    fn test_enter_on_add_row() {
175        let mut state = KeybindingListState::new("Test");
176
177        let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
178        let result = state.handle_key(enter);
179        assert_eq!(result, Some(KeybindingListEvent::AddRequested));
180    }
181
182    #[test]
183    fn test_enter_on_entry() {
184        let mut state = KeybindingListState::new("Test");
185        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
186        state.focus_entry(0);
187
188        let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
189        let result = state.handle_key(enter);
190        assert_eq!(result, Some(KeybindingListEvent::EditRequested(0)));
191    }
192
193    #[test]
194    fn test_delete_removes_focused() {
195        let mut state = KeybindingListState::new("Test");
196        state.add_binding(serde_json::json!({"key": "a", "action": "test"}));
197        state.focus_entry(0);
198
199        let delete = KeyEvent::new(KeyCode::Delete, KeyModifiers::empty());
200        let result = state.handle_key(delete);
201        assert_eq!(result, Some(KeybindingListEvent::BindingRemoved(0)));
202        assert!(state.bindings.is_empty());
203    }
204}