Skip to main content

fresh/view/controls/map_input/
input.rs

1//! Map input handling
2
3use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{MapHit, MapLayout, MapState};
6
7/// Events that can be returned from map input handling
8#[derive(Debug, Clone, PartialEq)]
9pub enum MapEvent {
10    /// An entry was added with the given key
11    EntryAdded(String),
12    /// An entry was removed
13    EntryRemoved(usize),
14    /// An entry was expanded or collapsed
15    EntryToggled(usize, bool),
16    /// Focus moved to a different entry
17    FocusChanged(Option<usize>),
18    /// Text in the new key field changed
19    NewKeyChanged(String),
20}
21
22impl MapState {
23    /// Handle a mouse event for this map control
24    ///
25    /// # Arguments
26    /// * `event` - The mouse event to handle
27    /// * `layout` - The control's rendered layout for hit testing
28    ///
29    /// # Returns
30    /// * `Some(MapEvent)` if the event was consumed
31    /// * `None` if the event was not relevant
32    pub fn handle_mouse(&mut self, event: MouseEvent, layout: &MapLayout) -> Option<MapEvent> {
33        if !self.is_enabled() {
34            return None;
35        }
36
37        if let MouseEventKind::Down(MouseButton::Left) = event.kind {
38            if let Some(hit) = layout.hit_test(event.column, event.row) {
39                match hit {
40                    MapHit::ExpandArrow(index) => {
41                        self.toggle_expand(index);
42                        return Some(MapEvent::EntryToggled(index, self.is_expanded(index)));
43                    }
44                    MapHit::EntryKey(index) => {
45                        self.focus_entry(index);
46                        return Some(MapEvent::FocusChanged(Some(index)));
47                    }
48                    MapHit::RemoveButton(index) => {
49                        self.remove_entry(index);
50                        return Some(MapEvent::EntryRemoved(index));
51                    }
52                    MapHit::AddRow => {
53                        self.focus_new_entry();
54                        return Some(MapEvent::FocusChanged(None));
55                    }
56                }
57            }
58        }
59        None
60    }
61
62    /// Handle a keyboard event for this map control
63    ///
64    /// # Returns
65    /// * `Some(MapEvent)` if the event was consumed
66    /// * `None` if the event was not relevant
67    pub fn handle_key(&mut self, key: KeyEvent) -> Option<MapEvent> {
68        if !self.is_enabled() {
69            return None;
70        }
71
72        match key.code {
73            KeyCode::Enter => {
74                if self.focused_entry.is_none() && !self.new_key_text.is_empty() {
75                    let key = self.new_key_text.clone();
76                    self.add_entry_from_input();
77                    Some(MapEvent::EntryAdded(key))
78                } else if let Some(index) = self.focused_entry {
79                    // Toggle expand on Enter when focused on entry
80                    self.toggle_expand(index);
81                    Some(MapEvent::EntryToggled(index, self.is_expanded(index)))
82                } else {
83                    None
84                }
85            }
86            KeyCode::Delete => {
87                if let Some(index) = self.focused_entry {
88                    self.remove_entry(index);
89                    Some(MapEvent::EntryRemoved(index))
90                } else {
91                    None
92                }
93            }
94            KeyCode::Backspace => {
95                if self.focused_entry.is_none() && self.cursor > 0 {
96                    self.backspace();
97                    Some(MapEvent::NewKeyChanged(self.new_key_text.clone()))
98                } else {
99                    None
100                }
101            }
102            KeyCode::Left => {
103                self.move_left();
104                None
105            }
106            KeyCode::Right => {
107                self.move_right();
108                None
109            }
110            KeyCode::Up => {
111                self.focus_prev();
112                Some(MapEvent::FocusChanged(self.focused_entry))
113            }
114            KeyCode::Down => {
115                self.focus_next();
116                Some(MapEvent::FocusChanged(self.focused_entry))
117            }
118            KeyCode::Char(' ') if self.focused_entry.is_some() => {
119                // Space toggles expand when focused on entry
120                if let Some(index) = self.focused_entry {
121                    self.toggle_expand(index);
122                    Some(MapEvent::EntryToggled(index, self.is_expanded(index)))
123                } else {
124                    None
125                }
126            }
127            KeyCode::Char(c) => {
128                if self.focused_entry.is_none() {
129                    self.insert(c);
130                    Some(MapEvent::NewKeyChanged(self.new_key_text.clone()))
131                } else {
132                    None
133                }
134            }
135            _ => None,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crossterm::event::KeyModifiers;
144    use ratatui::layout::Rect;
145
146    use crate::view::controls::map_input::MapEntryLayout;
147
148    fn make_layout() -> MapLayout {
149        MapLayout {
150            full_area: Rect::new(0, 0, 50, 5),
151            entry_areas: vec![MapEntryLayout {
152                index: 0,
153                row_area: Rect::new(0, 1, 50, 1),
154                expand_area: Rect::new(2, 1, 1, 1),
155                key_area: Rect::new(4, 1, 10, 1),
156                remove_area: Rect::new(40, 1, 3, 1),
157            }],
158            add_row_area: Some(Rect::new(0, 2, 50, 1)),
159        }
160    }
161
162    fn mouse_down(x: u16, y: u16) -> MouseEvent {
163        MouseEvent {
164            kind: MouseEventKind::Down(MouseButton::Left),
165            column: x,
166            row: y,
167            modifiers: KeyModifiers::empty(),
168        }
169    }
170
171    #[test]
172    fn test_click_expand_arrow() {
173        let mut state = MapState::new("Test");
174        state.add_entry("key1".to_string(), serde_json::json!({"foo": "bar"}));
175        let layout = make_layout();
176
177        let result = state.handle_mouse(mouse_down(2, 1), &layout);
178        assert_eq!(result, Some(MapEvent::EntryToggled(0, true)));
179        assert!(state.is_expanded(0));
180    }
181
182    #[test]
183    fn test_click_remove_button() {
184        let mut state = MapState::new("Test");
185        state.add_entry("key1".to_string(), serde_json::json!({}));
186        let layout = make_layout();
187
188        let result = state.handle_mouse(mouse_down(40, 1), &layout);
189        assert_eq!(result, Some(MapEvent::EntryRemoved(0)));
190        assert!(state.entries.is_empty());
191    }
192
193    #[test]
194    fn test_click_add_row() {
195        let mut state = MapState::new("Test");
196        state.new_key_text = "newkey".to_string();
197        let layout = make_layout();
198
199        // Clicking on the add row focuses it
200        let result = state.handle_mouse(mouse_down(13, 2), &layout);
201        assert_eq!(result, Some(MapEvent::FocusChanged(None)));
202        assert!(state.focused_entry.is_none());
203    }
204
205    #[test]
206    fn test_keyboard_navigation() {
207        let mut state = MapState::new("Test");
208        state.add_entry("a".to_string(), serde_json::json!({}));
209        state.add_entry("b".to_string(), serde_json::json!({}));
210        state.focus_new_entry();
211
212        let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
213        let result = state.handle_key(up);
214        assert_eq!(result, Some(MapEvent::FocusChanged(Some(1))));
215
216        let result = state.handle_key(up);
217        assert_eq!(result, Some(MapEvent::FocusChanged(Some(0))));
218    }
219
220    #[test]
221    fn test_enter_adds_entry() {
222        let mut state = MapState::new("Test");
223        state.new_key_text = "newkey".to_string();
224
225        let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
226        let result = state.handle_key(enter);
227        assert_eq!(result, Some(MapEvent::EntryAdded("newkey".to_string())));
228        assert_eq!(state.entries.len(), 1);
229    }
230
231    #[test]
232    fn test_delete_removes_focused_entry() {
233        let mut state = MapState::new("Test");
234        state.add_entry("a".to_string(), serde_json::json!({}));
235        state.focus_entry(0);
236
237        let delete = KeyEvent::new(KeyCode::Delete, KeyModifiers::empty());
238        let result = state.handle_key(delete);
239        assert_eq!(result, Some(MapEvent::EntryRemoved(0)));
240        assert!(state.entries.is_empty());
241    }
242
243    #[test]
244    fn test_typing_in_new_key_field() {
245        let mut state = MapState::new("Test");
246        state.focus_new_entry();
247
248        let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
249        let result = state.handle_key(key);
250        assert_eq!(result, Some(MapEvent::NewKeyChanged("a".to_string())));
251        assert_eq!(state.new_key_text, "a");
252    }
253
254    #[test]
255    fn test_space_toggles_expansion() {
256        let mut state = MapState::new("Test");
257        state.add_entry("key1".to_string(), serde_json::json!({"foo": "bar"}));
258        state.focus_entry(0);
259
260        let space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty());
261        let result = state.handle_key(space);
262        assert_eq!(result, Some(MapEvent::EntryToggled(0, true)));
263        assert!(state.is_expanded(0));
264    }
265}