fresh/view/controls/text_list/
input.rs1use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{TextListHit, TextListLayout, TextListState};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum TextListEvent {
10 ItemAdded(String),
12 ItemRemoved(usize),
14 ItemChanged(usize, String),
16 FocusChanged(Option<usize>),
18}
19
20impl TextListState {
21 pub fn handle_mouse(
31 &mut self,
32 event: MouseEvent,
33 layout: &TextListLayout,
34 ) -> Option<TextListEvent> {
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 TextListHit::Button(Some(index)) => {
43 self.remove_item(index);
45 return Some(TextListEvent::ItemRemoved(index));
46 }
47 TextListHit::Button(None) => {
48 if !self.new_item_text.is_empty() {
50 let item = self.new_item_text.clone();
51 self.add_item();
52 return Some(TextListEvent::ItemAdded(item));
53 }
54 }
55 TextListHit::TextField(Some(index)) => {
56 self.focus_item(index);
58 return Some(TextListEvent::FocusChanged(Some(index)));
59 }
60 TextListHit::TextField(None) => {
61 self.focus_new_item();
63 return Some(TextListEvent::FocusChanged(None));
64 }
65 }
66 }
67 }
68 None
69 }
70
71 pub fn handle_key(&mut self, key: KeyEvent) -> Option<TextListEvent> {
77 if !self.is_enabled() {
78 return None;
79 }
80
81 match key.code {
82 KeyCode::Enter => {
83 if self.focused_item.is_none() && !self.new_item_text.is_empty() {
84 let item = self.new_item_text.clone();
85 self.add_item();
86 Some(TextListEvent::ItemAdded(item))
87 } else {
88 None
89 }
90 }
91 KeyCode::Backspace => {
92 if self.cursor > 0 {
93 self.backspace();
94 if let Some(idx) = self.focused_item {
95 Some(TextListEvent::ItemChanged(idx, self.items[idx].clone()))
96 } else {
97 None
98 }
99 } else {
100 None
101 }
102 }
103 KeyCode::Delete => {
104 if let Some(idx) = self.focused_item {
105 if idx < self.items.len() {
106 self.remove_item(idx);
107 return Some(TextListEvent::ItemRemoved(idx));
108 }
109 }
110 None
111 }
112 KeyCode::Left => {
113 self.move_left();
114 None
115 }
116 KeyCode::Right => {
117 self.move_right();
118 None
119 }
120 KeyCode::Up => {
121 self.focus_prev();
122 Some(TextListEvent::FocusChanged(self.focused_item))
123 }
124 KeyCode::Down => {
125 self.focus_next();
126 Some(TextListEvent::FocusChanged(self.focused_item))
127 }
128 KeyCode::Char(c) => {
129 self.insert(c);
130 if let Some(idx) = self.focused_item {
131 Some(TextListEvent::ItemChanged(idx, self.items[idx].clone()))
132 } else {
133 None
134 }
135 }
136 _ => None,
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crossterm::event::KeyModifiers;
145 use ratatui::layout::Rect;
146
147 fn make_layout() -> TextListLayout {
148 TextListLayout {
149 rows: vec![
150 super::super::TextListRowLayout {
151 text_area: Rect::new(2, 1, 22, 1),
152 button_area: Rect::new(25, 1, 3, 1),
153 index: Some(0),
154 },
155 super::super::TextListRowLayout {
156 text_area: Rect::new(2, 2, 22, 1),
157 button_area: Rect::new(25, 2, 3, 1),
158 index: None,
159 },
160 ],
161 full_area: Rect::new(0, 0, 30, 3),
162 }
163 }
164
165 fn mouse_down(x: u16, y: u16) -> MouseEvent {
166 MouseEvent {
167 kind: MouseEventKind::Down(MouseButton::Left),
168 column: x,
169 row: y,
170 modifiers: KeyModifiers::empty(),
171 }
172 }
173
174 #[test]
175 fn test_click_remove_button() {
176 let mut state = TextListState::new("Items").with_items(vec!["item".to_string()]);
177 let layout = make_layout();
178
179 let result = state.handle_mouse(mouse_down(25, 1), &layout);
180 assert_eq!(result, Some(TextListEvent::ItemRemoved(0)));
181 assert!(state.items.is_empty());
182 }
183
184 #[test]
185 fn test_click_add_button() {
186 let mut state = TextListState::new("Items");
187 state.new_item_text = "new".to_string();
188 let layout = make_layout();
189
190 let result = state.handle_mouse(mouse_down(25, 2), &layout);
191 assert_eq!(result, Some(TextListEvent::ItemAdded("new".to_string())));
192 assert_eq!(state.items, vec!["new"]);
193 }
194
195 #[test]
196 fn test_click_text_field() {
197 let mut state = TextListState::new("Items").with_items(vec!["item".to_string()]);
198 let layout = make_layout();
199
200 let result = state.handle_mouse(mouse_down(10, 1), &layout);
201 assert_eq!(result, Some(TextListEvent::FocusChanged(Some(0))));
202 assert_eq!(state.focused_item, Some(0));
203 }
204
205 #[test]
206 fn test_keyboard_navigation() {
207 let mut state =
208 TextListState::new("Items").with_items(vec!["a".to_string(), "b".to_string()]);
209 state.focus_new_item();
210
211 let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
212 let result = state.handle_key(up);
213 assert_eq!(result, Some(TextListEvent::FocusChanged(Some(1))));
214
215 let result = state.handle_key(up);
216 assert_eq!(result, Some(TextListEvent::FocusChanged(Some(0))));
217 }
218
219 #[test]
220 fn test_enter_adds_item() {
221 let mut state = TextListState::new("Items");
222 state.new_item_text = "test".to_string();
223
224 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
225 let result = state.handle_key(enter);
226 assert_eq!(result, Some(TextListEvent::ItemAdded("test".to_string())));
227 assert_eq!(state.items, vec!["test"]);
228 }
229
230 #[test]
231 fn test_delete_removes_focused_item() {
232 let mut state =
233 TextListState::new("Items").with_items(vec!["a".to_string(), "b".to_string()]);
234 state.focus_item(0);
235
236 let delete = KeyEvent::new(KeyCode::Delete, KeyModifiers::empty());
237 let result = state.handle_key(delete);
238 assert_eq!(result, Some(TextListEvent::ItemRemoved(0)));
239 assert_eq!(state.items, vec!["b"]);
240 }
241
242 #[test]
243 fn test_typing_in_item() {
244 let mut state = TextListState::new("Items").with_items(vec!["hello".to_string()]);
245 state.focus_item(0);
246
247 let key = KeyEvent::new(KeyCode::Char('!'), KeyModifiers::empty());
248 let result = state.handle_key(key);
249 assert_eq!(
250 result,
251 Some(TextListEvent::ItemChanged(0, "hello!".to_string()))
252 );
253 }
254}