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_value(event.column, event.row) {
48 if !self.editing() {
49 self.start_editing();
50 Some(NumberInputEvent::StartedEditing)
51 } else {
52 None
53 }
54 } else {
55 None
56 }
57 }
58 MouseEventKind::Moved => {
59 let inside = layout.contains(event.column, event.row);
60 if inside {
61 if self.focus != FocusState::Focused {
62 self.focus = FocusState::Hovered;
63 }
64 Some(NumberInputEvent::Hovered)
65 } else if self.focus == FocusState::Hovered {
66 self.focus = FocusState::Normal;
67 Some(NumberInputEvent::Left)
68 } else {
69 None
70 }
71 }
72 _ => None,
73 }
74 }
75
76 pub fn handle_key(&mut self, key: KeyEvent) -> Option<NumberInputEvent> {
82 if !self.is_enabled() {
83 return None;
84 }
85
86 if self.editing() {
87 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
88 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
89
90 match key.code {
91 KeyCode::Enter => {
92 let old_value = self.value;
93 self.confirm_editing();
94 if self.value != old_value {
95 Some(NumberInputEvent::Changed(self.value))
96 } else {
97 Some(NumberInputEvent::CancelledEditing)
98 }
99 }
100 KeyCode::Esc => {
101 self.cancel_editing();
102 Some(NumberInputEvent::CancelledEditing)
103 }
104 KeyCode::Backspace if ctrl => {
105 self.delete_word_backward();
106 None
107 }
108 KeyCode::Backspace => {
109 self.backspace();
110 None
111 }
112 KeyCode::Delete if ctrl => {
113 self.delete_word_forward();
114 None
115 }
116 KeyCode::Delete => {
117 self.delete();
118 None
119 }
120 KeyCode::Left if ctrl && shift => {
121 self.move_word_left_selecting();
122 None
123 }
124 KeyCode::Left if ctrl => {
125 self.move_word_left();
126 None
127 }
128 KeyCode::Left if shift => {
129 self.move_left_selecting();
130 None
131 }
132 KeyCode::Left => {
133 self.move_left();
134 None
135 }
136 KeyCode::Right if ctrl && shift => {
137 self.move_word_right_selecting();
138 None
139 }
140 KeyCode::Right if ctrl => {
141 self.move_word_right();
142 None
143 }
144 KeyCode::Right if shift => {
145 self.move_right_selecting();
146 None
147 }
148 KeyCode::Right => {
149 self.move_right();
150 None
151 }
152 KeyCode::Home if shift => {
153 self.move_home_selecting();
154 None
155 }
156 KeyCode::Home => {
157 self.move_home();
158 None
159 }
160 KeyCode::End if shift => {
161 self.move_end_selecting();
162 None
163 }
164 KeyCode::End => {
165 self.move_end();
166 None
167 }
168 KeyCode::Char('a') if ctrl => {
169 self.select_all();
170 None
171 }
172 KeyCode::Char(c) => {
173 self.insert_char(c);
174 None
175 }
176 _ => None,
177 }
178 } else if self.focus == FocusState::Focused {
179 match key.code {
180 KeyCode::Enter => {
181 self.start_editing();
182 Some(NumberInputEvent::StartedEditing)
183 }
184 KeyCode::Char(c) if c.is_ascii_digit() || c == '-' || c == '.' => {
189 self.start_editing();
190 self.insert_char(c);
191 Some(NumberInputEvent::StartedEditing)
192 }
193 _ => None,
194 }
195 } else {
196 None
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crossterm::event::KeyModifiers;
205 use ratatui::layout::Rect;
206
207 fn make_layout() -> NumberInputLayout {
208 NumberInputLayout {
209 value_area: Rect::new(8, 0, 7, 1),
210 decrement_area: Rect::default(),
211 increment_area: Rect::default(),
212 full_area: Rect::new(0, 0, 15, 1),
213 }
214 }
215
216 fn mouse_down(x: u16, y: u16) -> MouseEvent {
217 MouseEvent {
218 kind: MouseEventKind::Down(MouseButton::Left),
219 column: x,
220 row: y,
221 modifiers: KeyModifiers::empty(),
222 }
223 }
224
225 fn mouse_move(x: u16, y: u16) -> MouseEvent {
226 MouseEvent {
227 kind: MouseEventKind::Moved,
228 column: x,
229 row: y,
230 modifiers: KeyModifiers::empty(),
231 }
232 }
233
234 #[test]
235 fn test_click_value_starts_editing() {
236 let mut state = NumberInputState::new(42, "Value");
237 let layout = make_layout();
238
239 let result = state.handle_mouse(mouse_down(10, 0), &layout);
240 assert_eq!(result, Some(NumberInputEvent::StartedEditing));
241 assert!(state.editing());
242 }
243
244 #[test]
245 fn test_hover() {
246 let mut state = NumberInputState::new(42, "Value");
247 let layout = make_layout();
248
249 let result = state.handle_mouse(mouse_move(10, 0), &layout);
250 assert_eq!(result, Some(NumberInputEvent::Hovered));
251 assert_eq!(state.focus, FocusState::Hovered);
252
253 let result = state.handle_mouse(mouse_move(30, 0), &layout);
254 assert_eq!(result, Some(NumberInputEvent::Left));
255 assert_eq!(state.focus, FocusState::Normal);
256 }
257
258 #[test]
259 fn test_keyboard_digit_starts_editing_and_replaces_value() {
260 let mut state = NumberInputState::new(42, "Value").with_focus(FocusState::Focused);
261
262 let key = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::empty());
263 let result = state.handle_key(key);
264 assert_eq!(result, Some(NumberInputEvent::StartedEditing));
265 assert!(state.editing());
266 assert_eq!(state.display_text(), "7");
268 }
269
270 #[test]
271 fn test_editing_confirm() {
272 let mut state = NumberInputState::new(42, "Value");
273 state.start_editing();
274 state.select_all();
276 state.insert_str("100");
277
278 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
279 let result = state.handle_key(enter);
280 assert_eq!(result, Some(NumberInputEvent::Changed(100)));
281 assert!(!state.editing());
282 }
283
284 #[test]
285 fn test_editing_cancel() {
286 let mut state = NumberInputState::new(42, "Value");
287 state.start_editing();
288 state.select_all();
290 state.insert_str("100");
291
292 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
293 let result = state.handle_key(esc);
294 assert_eq!(result, Some(NumberInputEvent::CancelledEditing));
295 assert!(!state.editing());
296 assert_eq!(state.value, 42);
297 }
298
299 #[test]
300 fn test_editing_cursor_navigation() {
301 let mut state = NumberInputState::new(12345, "Value");
302 state.start_editing();
303 assert_eq!(state.cursor_col(), 5); let left = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
306 state.handle_key(left);
307 assert_eq!(state.cursor_col(), 4);
308
309 let home = KeyEvent::new(KeyCode::Home, KeyModifiers::empty());
310 state.handle_key(home);
311 assert_eq!(state.cursor_col(), 0);
312
313 let end = KeyEvent::new(KeyCode::End, KeyModifiers::empty());
314 state.handle_key(end);
315 assert_eq!(state.cursor_col(), 5);
316 }
317
318 #[test]
319 fn test_editing_selection() {
320 let mut state = NumberInputState::new(123, "Value");
321 state.start_editing();
322
323 let ctrl_a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
325 state.handle_key(ctrl_a);
326 assert!(state.has_selection());
327
328 let key_9 = KeyEvent::new(KeyCode::Char('9'), KeyModifiers::empty());
330 state.handle_key(key_9);
331 assert_eq!(state.display_text(), "9");
332 }
333
334 #[test]
335 fn test_disabled_ignores_input() {
336 let mut state = NumberInputState::new(5, "Value").with_focus(FocusState::Disabled);
337 let layout = make_layout();
338
339 let result = state.handle_mouse(mouse_down(10, 0), &layout);
340 assert!(result.is_none());
341 assert_eq!(state.value, 5);
342 }
343}