Skip to main content

fresh/view/controls/text_input/
input.rs

1//! Text input handling
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{FocusState, TextInputLayout, TextInputState};
6
7/// Events that can be returned from text input handling
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum TextInputEvent {
10    /// Text was changed
11    Changed(String),
12    /// Input was submitted (Enter pressed)
13    Submitted(String),
14    /// Input was cancelled (Escape pressed)
15    Cancelled,
16    /// Input gained focus
17    Focused,
18    /// Mouse is hovering
19    Hovered,
20    /// Mouse left the area
21    Left,
22}
23
24impl TextInputState {
25    /// Handle a mouse event for this text input
26    ///
27    /// # Arguments
28    /// * `event` - The mouse event to handle
29    /// * `layout` - The control's rendered layout for hit testing
30    ///
31    /// # Returns
32    /// * `Some(TextInputEvent)` if the event was consumed
33    /// * `None` if the event was not relevant
34    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                    // Click in input area - could set cursor position based on click
47                    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    /// Handle a keyboard event for this text input
76    ///
77    /// # Returns
78    /// * `Some(TextInputEvent)` if the event was consumed
79    /// * `None` if the event was not relevant
80    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"); // Normal focus
258
259        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}