fresh/view/controls/text_input/
input.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{FocusState, TextInputLayout, TextInputState};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum TextInputEvent {
10 Changed(String),
12 Submitted(String),
14 Cancelled,
16 Focused,
18 Hovered,
20 Left,
22}
23
24impl TextInputState {
25 pub fn handle_mouse(
35 &mut self,
36 event: MouseEvent,
37 layout: &TextInputLayout,
38 ) -> Option<TextInputEvent> {
39 if !self.is_enabled() {
40 return None;
41 }
42
43 match event.kind {
44 MouseEventKind::Down(MouseButton::Left) => {
45 if layout.is_input(event.column, event.row) {
46 if self.focus != FocusState::Focused {
48 self.focus = FocusState::Focused;
49 Some(TextInputEvent::Focused)
50 } else {
51 None
52 }
53 } else {
54 None
55 }
56 }
57 MouseEventKind::Moved => {
58 let inside = layout.contains(event.column, event.row);
59 if inside {
60 if self.focus != FocusState::Focused && self.focus != FocusState::Hovered {
61 self.focus = FocusState::Hovered;
62 }
63 Some(TextInputEvent::Hovered)
64 } else if self.focus == FocusState::Hovered {
65 self.focus = FocusState::Normal;
66 Some(TextInputEvent::Left)
67 } else {
68 None
69 }
70 }
71 _ => None,
72 }
73 }
74
75 pub fn handle_key(&mut self, key: KeyEvent) -> Option<TextInputEvent> {
81 if !self.is_enabled() || self.focus != FocusState::Focused {
82 return None;
83 }
84
85 match key.code {
86 KeyCode::Enter => Some(TextInputEvent::Submitted(self.value.clone())),
87 KeyCode::Esc => Some(TextInputEvent::Cancelled),
88 KeyCode::Backspace => {
89 if !self.value.is_empty() && self.cursor > 0 {
90 self.backspace();
91 Some(TextInputEvent::Changed(self.value.clone()))
92 } else {
93 None
94 }
95 }
96 KeyCode::Delete => {
97 if self.cursor < self.value.len() {
98 self.delete();
99 Some(TextInputEvent::Changed(self.value.clone()))
100 } else {
101 None
102 }
103 }
104 KeyCode::Left => {
105 if key.modifiers.contains(KeyModifiers::CONTROL) {
106 self.move_home();
107 } else {
108 self.move_left();
109 }
110 None
111 }
112 KeyCode::Right => {
113 if key.modifiers.contains(KeyModifiers::CONTROL) {
114 self.move_end();
115 } else {
116 self.move_right();
117 }
118 None
119 }
120 KeyCode::Home => {
121 self.move_home();
122 None
123 }
124 KeyCode::End => {
125 self.move_end();
126 None
127 }
128 KeyCode::Char(c) => {
129 self.insert(c);
130 Some(TextInputEvent::Changed(self.value.clone()))
131 }
132 _ => None,
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use ratatui::layout::Rect;
141
142 fn make_layout() -> TextInputLayout {
143 TextInputLayout {
144 input_area: Rect::new(8, 0, 20, 1),
145 full_area: Rect::new(0, 0, 28, 1),
146 cursor_pos: None,
147 }
148 }
149
150 fn mouse_down(x: u16, y: u16) -> MouseEvent {
151 MouseEvent {
152 kind: MouseEventKind::Down(MouseButton::Left),
153 column: x,
154 row: y,
155 modifiers: KeyModifiers::empty(),
156 }
157 }
158
159 fn mouse_move(x: u16, y: u16) -> MouseEvent {
160 MouseEvent {
161 kind: MouseEventKind::Moved,
162 column: x,
163 row: y,
164 modifiers: KeyModifiers::empty(),
165 }
166 }
167
168 #[test]
169 fn test_click_focuses() {
170 let mut state = TextInputState::new("Name");
171 let layout = make_layout();
172
173 let result = state.handle_mouse(mouse_down(10, 0), &layout);
174 assert_eq!(result, Some(TextInputEvent::Focused));
175 assert_eq!(state.focus, FocusState::Focused);
176 }
177
178 #[test]
179 fn test_hover() {
180 let mut state = TextInputState::new("Name");
181 let layout = make_layout();
182
183 let result = state.handle_mouse(mouse_move(10, 0), &layout);
184 assert_eq!(result, Some(TextInputEvent::Hovered));
185
186 let result = state.handle_mouse(mouse_move(30, 0), &layout);
187 assert_eq!(result, Some(TextInputEvent::Left));
188 }
189
190 #[test]
191 fn test_typing() {
192 let mut state = TextInputState::new("Name").with_focus(FocusState::Focused);
193
194 let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
195 let result = state.handle_key(a);
196 assert_eq!(result, Some(TextInputEvent::Changed("a".to_string())));
197
198 let b = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::empty());
199 state.handle_key(b);
200 assert_eq!(state.value, "ab");
201 }
202
203 #[test]
204 fn test_backspace() {
205 let mut state = TextInputState::new("Name")
206 .with_value("abc")
207 .with_focus(FocusState::Focused);
208
209 let bs = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
210 let result = state.handle_key(bs);
211 assert_eq!(result, Some(TextInputEvent::Changed("ab".to_string())));
212 }
213
214 #[test]
215 fn test_submit() {
216 let mut state = TextInputState::new("Name")
217 .with_value("John")
218 .with_focus(FocusState::Focused);
219
220 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
221 let result = state.handle_key(enter);
222 assert_eq!(result, Some(TextInputEvent::Submitted("John".to_string())));
223 }
224
225 #[test]
226 fn test_cancel() {
227 let mut state = TextInputState::new("Name")
228 .with_value("John")
229 .with_focus(FocusState::Focused);
230
231 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
232 let result = state.handle_key(esc);
233 assert_eq!(result, Some(TextInputEvent::Cancelled));
234 }
235
236 #[test]
237 fn test_cursor_movement() {
238 let mut state = TextInputState::new("Name")
239 .with_value("hello")
240 .with_focus(FocusState::Focused);
241
242 let left = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
243 state.handle_key(left);
244 assert_eq!(state.cursor, 4);
245
246 let home = KeyEvent::new(KeyCode::Home, KeyModifiers::empty());
247 state.handle_key(home);
248 assert_eq!(state.cursor, 0);
249
250 let end = KeyEvent::new(KeyCode::End, KeyModifiers::empty());
251 state.handle_key(end);
252 assert_eq!(state.cursor, 5);
253 }
254
255 #[test]
256 fn test_unfocused_ignores_keyboard() {
257 let mut state = TextInputState::new("Name"); let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
260 let result = state.handle_key(a);
261 assert!(result.is_none());
262 assert!(state.value.is_empty());
263 }
264}