fresh/view/controls/number_input/
input.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
4
5use super::{FocusState, NumberInputLayout, NumberInputState};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum NumberInputEvent {
10 Incremented(i64),
12 Decremented(i64),
14 Changed(i64),
16 StartedEditing,
18 CancelledEditing,
20 Hovered,
22 Left,
24}
25
26impl NumberInputState {
27 pub fn handle_mouse(
37 &mut self,
38 event: MouseEvent,
39 layout: &NumberInputLayout,
40 ) -> Option<NumberInputEvent> {
41 if !self.is_enabled() {
42 return None;
43 }
44
45 match event.kind {
46 MouseEventKind::Down(MouseButton::Left) => {
47 if layout.is_increment(event.column, event.row) {
48 self.increment();
49 Some(NumberInputEvent::Incremented(self.value))
50 } else if layout.is_decrement(event.column, event.row) {
51 self.decrement();
52 Some(NumberInputEvent::Decremented(self.value))
53 } else if layout.is_value(event.column, event.row) {
54 if !self.editing() {
55 self.start_editing();
56 Some(NumberInputEvent::StartedEditing)
57 } else {
58 None
59 }
60 } else {
61 None
62 }
63 }
64 MouseEventKind::Moved => {
65 let inside = layout.contains(event.column, event.row);
66 if inside {
67 if self.focus != FocusState::Focused {
68 self.focus = FocusState::Hovered;
69 }
70 Some(NumberInputEvent::Hovered)
71 } else if self.focus == FocusState::Hovered {
72 self.focus = FocusState::Normal;
73 Some(NumberInputEvent::Left)
74 } else {
75 None
76 }
77 }
78 _ => None,
79 }
80 }
81
82 pub fn handle_key(&mut self, key: KeyEvent) -> Option<NumberInputEvent> {
88 if !self.is_enabled() {
89 return None;
90 }
91
92 if self.editing() {
93 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
94 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
95
96 match key.code {
97 KeyCode::Enter => {
98 let old_value = self.value;
99 self.confirm_editing();
100 if self.value != old_value {
101 Some(NumberInputEvent::Changed(self.value))
102 } else {
103 Some(NumberInputEvent::CancelledEditing)
104 }
105 }
106 KeyCode::Esc => {
107 self.cancel_editing();
108 Some(NumberInputEvent::CancelledEditing)
109 }
110 KeyCode::Backspace if ctrl => {
111 self.delete_word_backward();
112 None
113 }
114 KeyCode::Backspace => {
115 self.backspace();
116 None
117 }
118 KeyCode::Delete if ctrl => {
119 self.delete_word_forward();
120 None
121 }
122 KeyCode::Delete => {
123 self.delete();
124 None
125 }
126 KeyCode::Left if ctrl && shift => {
127 self.move_word_left_selecting();
128 None
129 }
130 KeyCode::Left if ctrl => {
131 self.move_word_left();
132 None
133 }
134 KeyCode::Left if shift => {
135 self.move_left_selecting();
136 None
137 }
138 KeyCode::Left => {
139 self.move_left();
140 None
141 }
142 KeyCode::Right if ctrl && shift => {
143 self.move_word_right_selecting();
144 None
145 }
146 KeyCode::Right if ctrl => {
147 self.move_word_right();
148 None
149 }
150 KeyCode::Right if shift => {
151 self.move_right_selecting();
152 None
153 }
154 KeyCode::Right => {
155 self.move_right();
156 None
157 }
158 KeyCode::Home if shift => {
159 self.move_home_selecting();
160 None
161 }
162 KeyCode::Home => {
163 self.move_home();
164 None
165 }
166 KeyCode::End if shift => {
167 self.move_end_selecting();
168 None
169 }
170 KeyCode::End => {
171 self.move_end();
172 None
173 }
174 KeyCode::Char('a') if ctrl => {
175 self.select_all();
176 None
177 }
178 KeyCode::Char(c) => {
179 self.insert_char(c);
180 None
181 }
182 _ => None,
183 }
184 } else if self.focus == FocusState::Focused {
185 match key.code {
186 KeyCode::Up | KeyCode::Char('+') | KeyCode::Char('=') => {
187 self.increment();
188 Some(NumberInputEvent::Incremented(self.value))
189 }
190 KeyCode::Down | KeyCode::Char('-') => {
191 self.decrement();
192 Some(NumberInputEvent::Decremented(self.value))
193 }
194 KeyCode::Enter => {
195 self.start_editing();
196 Some(NumberInputEvent::StartedEditing)
197 }
198 _ => None,
199 }
200 } else {
201 None
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crossterm::event::KeyModifiers;
210 use ratatui::layout::Rect;
211
212 fn make_layout() -> NumberInputLayout {
213 NumberInputLayout {
214 value_area: Rect::new(8, 0, 7, 1),
215 decrement_area: Rect::new(16, 0, 3, 1),
216 increment_area: Rect::new(20, 0, 3, 1),
217 full_area: Rect::new(0, 0, 23, 1),
218 }
219 }
220
221 fn mouse_down(x: u16, y: u16) -> MouseEvent {
222 MouseEvent {
223 kind: MouseEventKind::Down(MouseButton::Left),
224 column: x,
225 row: y,
226 modifiers: KeyModifiers::empty(),
227 }
228 }
229
230 fn mouse_move(x: u16, y: u16) -> MouseEvent {
231 MouseEvent {
232 kind: MouseEventKind::Moved,
233 column: x,
234 row: y,
235 modifiers: KeyModifiers::empty(),
236 }
237 }
238
239 #[test]
240 fn test_click_increment() {
241 let mut state = NumberInputState::new(5, "Value");
242 let layout = make_layout();
243
244 let result = state.handle_mouse(mouse_down(20, 0), &layout);
245 assert_eq!(result, Some(NumberInputEvent::Incremented(6)));
246 assert_eq!(state.value, 6);
247 }
248
249 #[test]
250 fn test_click_decrement() {
251 let mut state = NumberInputState::new(5, "Value");
252 let layout = make_layout();
253
254 let result = state.handle_mouse(mouse_down(16, 0), &layout);
255 assert_eq!(result, Some(NumberInputEvent::Decremented(4)));
256 assert_eq!(state.value, 4);
257 }
258
259 #[test]
260 fn test_click_value_starts_editing() {
261 let mut state = NumberInputState::new(42, "Value");
262 let layout = make_layout();
263
264 let result = state.handle_mouse(mouse_down(10, 0), &layout);
265 assert_eq!(result, Some(NumberInputEvent::StartedEditing));
266 assert!(state.editing());
267 }
268
269 #[test]
270 fn test_hover() {
271 let mut state = NumberInputState::new(42, "Value");
272 let layout = make_layout();
273
274 let result = state.handle_mouse(mouse_move(10, 0), &layout);
275 assert_eq!(result, Some(NumberInputEvent::Hovered));
276 assert_eq!(state.focus, FocusState::Hovered);
277
278 let result = state.handle_mouse(mouse_move(30, 0), &layout);
279 assert_eq!(result, Some(NumberInputEvent::Left));
280 assert_eq!(state.focus, FocusState::Normal);
281 }
282
283 #[test]
284 fn test_keyboard_increment() {
285 let mut state = NumberInputState::new(5, "Value").with_focus(FocusState::Focused);
286
287 let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
288 let result = state.handle_key(up);
289 assert_eq!(result, Some(NumberInputEvent::Incremented(6)));
290 }
291
292 #[test]
293 fn test_keyboard_decrement() {
294 let mut state = NumberInputState::new(5, "Value").with_focus(FocusState::Focused);
295
296 let down = KeyEvent::new(KeyCode::Down, KeyModifiers::empty());
297 let result = state.handle_key(down);
298 assert_eq!(result, Some(NumberInputEvent::Decremented(4)));
299 }
300
301 #[test]
302 fn test_editing_confirm() {
303 let mut state = NumberInputState::new(42, "Value");
304 state.start_editing();
305 state.select_all();
307 state.insert_str("100");
308
309 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
310 let result = state.handle_key(enter);
311 assert_eq!(result, Some(NumberInputEvent::Changed(100)));
312 assert!(!state.editing());
313 }
314
315 #[test]
316 fn test_editing_cancel() {
317 let mut state = NumberInputState::new(42, "Value");
318 state.start_editing();
319 state.select_all();
321 state.insert_str("100");
322
323 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
324 let result = state.handle_key(esc);
325 assert_eq!(result, Some(NumberInputEvent::CancelledEditing));
326 assert!(!state.editing());
327 assert_eq!(state.value, 42);
328 }
329
330 #[test]
331 fn test_editing_cursor_navigation() {
332 let mut state = NumberInputState::new(12345, "Value");
333 state.start_editing();
334 assert_eq!(state.cursor_col(), 5); let left = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
337 state.handle_key(left);
338 assert_eq!(state.cursor_col(), 4);
339
340 let home = KeyEvent::new(KeyCode::Home, KeyModifiers::empty());
341 state.handle_key(home);
342 assert_eq!(state.cursor_col(), 0);
343
344 let end = KeyEvent::new(KeyCode::End, KeyModifiers::empty());
345 state.handle_key(end);
346 assert_eq!(state.cursor_col(), 5);
347 }
348
349 #[test]
350 fn test_editing_selection() {
351 let mut state = NumberInputState::new(123, "Value");
352 state.start_editing();
353
354 let ctrl_a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
356 state.handle_key(ctrl_a);
357 assert!(state.has_selection());
358
359 let key_9 = KeyEvent::new(KeyCode::Char('9'), KeyModifiers::empty());
361 state.handle_key(key_9);
362 assert_eq!(state.display_text(), "9");
363 }
364
365 #[test]
366 fn test_disabled_ignores_input() {
367 let mut state = NumberInputState::new(5, "Value").with_focus(FocusState::Disabled);
368 let layout = make_layout();
369
370 let result = state.handle_mouse(mouse_down(20, 0), &layout);
371 assert!(result.is_none());
372 assert_eq!(state.value, 5);
373 }
374}