1use crate::field::Field;
5use crate::form::{Form, FormEvent, FormMode};
6use crate::validate::validate_field;
7use hjkl_engine::{CoarseMode, Input, Key};
8
9impl Form {
10 pub fn handle_input(&mut self, input: Input) -> Option<FormEvent> {
14 if self.fields.is_empty() {
15 return None;
16 }
17 match self.mode {
18 FormMode::Normal => self.handle_normal(input),
19 FormMode::Insert => self.handle_insert(input),
20 }
21 }
22
23 fn handle_normal(&mut self, input: Input) -> Option<FormEvent> {
24 let was_pending_g = self.pending_g;
27 if input.key != Key::Char('g') || input.ctrl || input.alt {
28 self.pending_g = false;
29 }
30
31 if input.key == Key::Esc {
33 return Some(FormEvent::Cancelled);
34 }
35
36 if let Some(ev) = self.try_navigate(input, was_pending_g) {
39 return Some(ev);
40 }
41
42 let field_kind = field_kind(&self.fields[self.focused]);
44 match field_kind {
45 FieldKind::Checkbox => self.handle_normal_checkbox(input),
46 FieldKind::Select => self.handle_normal_select(input),
47 FieldKind::Submit => self.handle_normal_submit(input),
48 FieldKind::SingleLineText | FieldKind::MultiLineText => {
49 self.handle_normal_text(input, field_kind == FieldKind::SingleLineText)
50 }
51 }
52 }
53
54 fn try_navigate(&mut self, input: Input, was_pending_g: bool) -> Option<FormEvent> {
55 let len = self.fields.len();
56 let mut moved = false;
57 let prev = self.focused;
58 match input.key {
59 Key::Tab if input.shift => {
64 self.focused = self.focused.saturating_sub(1);
65 moved = true;
66 }
67 Key::Char('j') | Key::Down | Key::Tab if !input.ctrl && !input.alt => {
68 self.focused = (self.focused + 1) % len;
69 moved = true;
70 }
71 Key::Char('k') | Key::Up if !input.ctrl && !input.alt => {
72 self.focused = self.focused.saturating_sub(1);
73 moved = true;
74 }
75 Key::Char('g') if !input.ctrl && !input.alt && !input.shift => {
76 if was_pending_g {
77 self.focused = 0;
78 moved = true;
79 } else {
80 self.pending_g = true;
81 return None;
82 }
83 }
84 Key::Char('G') if !input.ctrl && !input.alt => {
85 self.focused = len - 1;
86 moved = true;
87 }
88 _ => {}
89 }
90 if moved {
91 if prev != self.focused {
92 if let Field::SingleLineText(f) | Field::MultiLineText(f) = &mut self.fields[prev] {
96 f.enter_normal();
97 }
98 validate_field(&mut self.fields[prev]);
100 self.bump_dirty();
101 }
102 return Some(FormEvent::Changed);
103 }
104 None
105 }
106
107 fn handle_normal_checkbox(&mut self, input: Input) -> Option<FormEvent> {
108 match input.key {
109 Key::Char(' ') | Key::Enter => {
110 if let Field::Checkbox(c) = &mut self.fields[self.focused] {
111 c.value = !c.value;
112 }
113 self.bump_dirty();
114 Some(FormEvent::Changed)
115 }
116 _ => None,
117 }
118 }
119
120 fn handle_normal_select(&mut self, input: Input) -> Option<FormEvent> {
121 match input.key {
122 Key::Char('l') | Key::Right if !input.ctrl && !input.alt => {
123 if let Field::Select(s) = &mut self.fields[self.focused]
124 && !s.options.is_empty()
125 {
126 s.index = (s.index + 1) % s.options.len();
127 }
128 self.bump_dirty();
129 Some(FormEvent::Changed)
130 }
131 Key::Char('h') | Key::Left if !input.ctrl && !input.alt => {
132 if let Field::Select(s) = &mut self.fields[self.focused]
133 && !s.options.is_empty()
134 {
135 s.index = if s.index == 0 {
136 s.options.len() - 1
137 } else {
138 s.index - 1
139 };
140 }
141 self.bump_dirty();
142 Some(FormEvent::Changed)
143 }
144 _ => None,
145 }
146 }
147
148 fn handle_normal_submit(&mut self, input: Input) -> Option<FormEvent> {
149 if input.key == Key::Enter || (input.key == Key::Char(' ') && !input.ctrl && !input.alt) {
150 return Some(self.try_submit_event());
151 }
152 None
153 }
154
155 fn handle_normal_text(&mut self, input: Input, _single_line: bool) -> Option<FormEvent> {
156 if let Field::SingleLineText(f) | Field::MultiLineText(f) = &mut self.fields[self.focused] {
161 let prev_gen_before = f.editor.buffer().dirty_gen();
162 hjkl_vim::dispatch_input(&mut f.editor, input);
163 if f.editor.coarse_mode() == CoarseMode::Insert {
164 f.enter_gen = prev_gen_before;
165 self.mode = FormMode::Insert;
166 return Some(FormEvent::Changed);
167 }
168 }
169 Some(FormEvent::Changed)
172 }
173
174 fn handle_insert(&mut self, input: Input) -> Option<FormEvent> {
175 if !self.fields[self.focused].is_text() {
179 self.mode = FormMode::Normal;
180 return self.handle_normal(input);
181 }
182
183 let single = matches!(self.fields[self.focused], Field::SingleLineText(_));
186 if single && input.key == Key::Enter {
187 let len = self.fields.len();
188 if self.focused + 1 < len {
189 let prev = self.focused;
190 self.focused += 1;
191 if let Field::SingleLineText(f) | Field::MultiLineText(f) = &mut self.fields[prev] {
194 f.enter_normal();
195 }
196 validate_field(&mut self.fields[prev]);
197 match &mut self.fields[self.focused] {
202 Field::SingleLineText(f) | Field::MultiLineText(f) => {
203 f.enter_gen = f.editor.buffer().dirty_gen();
204 f.enter_insert_at_end();
205 }
206 _ => self.mode = FormMode::Normal,
207 }
208 self.bump_dirty();
209 }
210 return Some(FormEvent::Changed);
211 }
212
213 if let Field::SingleLineText(f) | Field::MultiLineText(f) = &mut self.fields[self.focused] {
215 let before_gen = f.editor.buffer().dirty_gen();
216 hjkl_vim::dispatch_input(&mut f.editor, input);
217 let after_mode = f.editor.coarse_mode();
218 let after_gen = f.editor.buffer().dirty_gen();
219 if after_mode == CoarseMode::Normal {
220 self.mode = FormMode::Normal;
222 let prev_focus = self.focused;
223 validate_field(&mut self.fields[prev_focus]);
224 self.bump_dirty();
225 return Some(FormEvent::Changed);
226 }
227 if after_gen != before_gen {
228 self.bump_dirty();
229 return Some(FormEvent::Changed);
230 }
231 }
232 None
233 }
234
235 fn try_submit_event(&mut self) -> FormEvent {
238 match self.try_submit() {
239 Some(outcome) => FormEvent::Submitted(outcome),
240 None => FormEvent::ValidationFailed,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246enum FieldKind {
247 SingleLineText,
248 MultiLineText,
249 Select,
250 Checkbox,
251 Submit,
252}
253
254fn field_kind(field: &Field) -> FieldKind {
255 match field {
256 Field::SingleLineText(_) => FieldKind::SingleLineText,
257 Field::MultiLineText(_) => FieldKind::MultiLineText,
258 Field::Select(_) => FieldKind::Select,
259 Field::Checkbox(_) => FieldKind::Checkbox,
260 Field::Submit(_) => FieldKind::Submit,
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::field::{CheckboxField, FieldMeta, SelectField, SubmitField, TextFieldEditor};
268 use crate::submit::SubmitOutcome;
269 use std::sync::Arc;
270 use std::sync::atomic::{AtomicBool, Ordering};
271
272 fn key(c: char) -> Input {
273 Input {
274 key: Key::Char(c),
275 ..Input::default()
276 }
277 }
278
279 fn special(k: Key) -> Input {
280 Input {
281 key: k,
282 ..Input::default()
283 }
284 }
285
286 fn make_form() -> Form {
287 Form::new()
288 .with_field(Field::SingleLineText(TextFieldEditor::with_meta(
289 FieldMeta::new("Name"),
290 1,
291 )))
292 .with_field(Field::SingleLineText(TextFieldEditor::with_meta(
293 FieldMeta::new("Email"),
294 1,
295 )))
296 .with_field(Field::Checkbox(CheckboxField::new(FieldMeta::new("Save"))))
297 .with_field(Field::Select(SelectField::new(
298 FieldMeta::new("Format"),
299 vec!["json".into(), "yaml".into(), "toml".into()],
300 )))
301 .with_field(Field::Submit(SubmitField::new(FieldMeta::new("Submit"))))
302 }
303
304 #[test]
305 fn j_advances_focus() {
306 let mut form = make_form();
307 form.handle_input(key('j'));
308 assert_eq!(form.focused(), 1);
309 }
310
311 #[test]
312 fn blur_resets_field_from_visual_to_normal() {
313 let mut form = make_form();
314 form.handle_input(key('v'));
316 let in_visual = matches!(
317 &form.fields[0],
318 Field::SingleLineText(f) | Field::MultiLineText(f) if f.coarse_mode() != CoarseMode::Normal
319 );
320 assert!(
321 in_visual,
322 "`v` must put the field editor in a non-Normal mode"
323 );
324
325 form.handle_input(key('j'));
327 assert_eq!(form.focused(), 1);
328
329 match &form.fields[0] {
331 Field::SingleLineText(f) | Field::MultiLineText(f) => {
332 assert_eq!(
333 f.coarse_mode(),
334 CoarseMode::Normal,
335 "blurred field must reset to Normal"
336 );
337 }
338 _ => panic!("field 0 must be a text field"),
339 }
340 }
341
342 #[test]
343 fn j_past_end_wraps_to_zero() {
344 let mut form = make_form();
345 for _ in 0..form.fields.len() {
346 form.handle_input(key('j'));
347 }
348 assert_eq!(form.focused(), 0);
349 }
350
351 #[test]
352 fn k_past_zero_saturates() {
353 let mut form = make_form();
354 form.handle_input(key('k'));
355 assert_eq!(form.focused(), 0);
356 }
357
358 #[test]
359 fn gg_jumps_to_zero() {
360 let mut form = make_form();
361 form.handle_input(key('j'));
362 form.handle_input(key('j'));
363 assert_eq!(form.focused(), 2);
364 form.handle_input(key('g'));
365 form.handle_input(key('g'));
366 assert_eq!(form.focused(), 0);
367 }
368
369 #[test]
370 fn capital_g_jumps_to_last() {
371 let mut form = make_form();
372 let last = form.fields.len() - 1;
373 form.handle_input(Input {
374 key: Key::Char('G'),
375 shift: true,
376 ..Input::default()
377 });
378 assert_eq!(form.focused(), last);
379 }
380
381 #[test]
382 fn i_on_text_enters_insert() {
383 let mut form = make_form();
384 form.handle_input(key('i'));
385 assert_eq!(form.mode, FormMode::Insert);
386 }
387
388 #[test]
389 fn esc_after_insert_returns_to_normal() {
390 let mut form =
391 make_form().with_field(Field::Submit(SubmitField::new(FieldMeta::new("Sub"))));
392 form.handle_input(key('i'));
394 assert_eq!(form.mode, FormMode::Insert);
395 form.handle_input(key('x'));
396 form.handle_input(special(Key::Esc));
397 assert_eq!(form.mode, FormMode::Normal);
398 }
399
400 #[test]
401 fn enter_on_submit_fires_submit_fn() {
402 let fired = Arc::new(AtomicBool::new(false));
403 let f2 = fired.clone();
404 let mut form = make_form().with_submit(Box::new(move || {
405 f2.store(true, Ordering::SeqCst);
406 SubmitOutcome::Ok
407 }));
408 form.handle_input(Input {
410 key: Key::Char('G'),
411 shift: true,
412 ..Input::default()
413 });
414 let ev = form.handle_input(special(Key::Enter));
415 assert!(matches!(ev, Some(FormEvent::Submitted(SubmitOutcome::Ok))));
416 assert!(fired.load(Ordering::SeqCst));
417 }
418
419 #[test]
420 fn submit_with_failing_validator_does_not_fire() {
421 let fired = Arc::new(AtomicBool::new(false));
422 let f2 = fired.clone();
423 let mut name = TextFieldEditor::with_meta(FieldMeta::new("Name").required(true), 1);
424 name.validator = Some(Box::new(|s: &str| {
425 if s.is_empty() {
426 Err("required".into())
427 } else {
428 Ok(())
429 }
430 }));
431 let mut form = Form::new()
432 .with_field(Field::SingleLineText(name))
433 .with_field(Field::Submit(SubmitField::new(FieldMeta::new("Submit"))))
434 .with_submit(Box::new(move || {
435 f2.store(true, Ordering::SeqCst);
436 SubmitOutcome::Ok
437 }));
438 form.handle_input(key('j'));
439 let ev = form.handle_input(special(Key::Enter));
440 assert!(matches!(ev, Some(FormEvent::ValidationFailed)));
441 assert!(!fired.load(Ordering::SeqCst));
442 }
443
444 #[test]
445 fn enter_in_insert_jumps_focus_to_next() {
446 let mut form = make_form();
447 form.handle_input(key('i'));
448 assert_eq!(form.mode, FormMode::Insert);
449 form.handle_input(special(Key::Enter));
450 assert_eq!(form.focused(), 1);
451 assert_eq!(form.mode, FormMode::Insert);
453 }
454
455 #[test]
456 fn shift_tab_moves_focus_backwards() {
457 let mut form = make_form();
458 form.handle_input(key('j'));
459 form.handle_input(key('j'));
460 assert_eq!(form.focused(), 2);
461 form.handle_input(Input {
462 key: Key::Tab,
463 shift: true,
464 ..Input::default()
465 });
466 assert_eq!(form.focused(), 1, "Shift-Tab must move focus backwards");
467 }
468
469 #[test]
470 fn enter_in_insert_puts_next_text_field_in_insert() {
471 let mut form = make_form();
472 form.handle_input(key('i'));
473 form.handle_input(key('a')); form.handle_input(special(Key::Enter)); assert_eq!(form.focused(), 1);
476 assert_eq!(form.mode, FormMode::Insert);
477 form.handle_input(key('x'));
479 if let Field::SingleLineText(f) = &form.fields[1] {
480 assert_eq!(f.text(), "x");
481 } else {
482 panic!("expected text field");
483 }
484 if let Field::SingleLineText(f) = &form.fields[0] {
486 assert_eq!(f.coarse_mode(), CoarseMode::Normal);
487 } else {
488 panic!("expected text field");
489 }
490 }
491
492 #[test]
493 fn enter_in_insert_onto_non_text_field_returns_to_normal() {
494 let mut form = make_form();
495 form.handle_input(key('j'));
497 form.handle_input(key('i'));
498 assert_eq!(form.mode, FormMode::Insert);
499 form.handle_input(special(Key::Enter)); assert_eq!(form.focused(), 2);
501 assert_eq!(
502 form.mode,
503 FormMode::Normal,
504 "form must not stay in Insert on a non-text field"
505 );
506 let ev = form.handle_input(special(Key::Esc));
508 assert!(matches!(ev, Some(FormEvent::Cancelled)));
509 }
510
511 #[test]
512 fn o_on_text_field_enters_insert_mode() {
513 let mut form = make_form();
514 form.handle_input(key('o'));
515 assert_eq!(
516 form.mode,
517 FormMode::Insert,
518 "any Normal command landing in editor-Insert must switch the form to Insert"
519 );
520 }
521
522 #[test]
523 fn checkbox_toggles_on_space() {
524 let mut form = make_form();
525 form.handle_input(key('j'));
527 form.handle_input(key('j'));
528 assert_eq!(form.focused(), 2);
529 form.handle_input(key(' '));
530 if let Field::Checkbox(c) = &form.fields[2] {
531 assert!(c.value);
532 } else {
533 panic!("expected checkbox");
534 }
535 }
536
537 #[test]
538 fn select_cycles_on_h_l() {
539 let mut form = make_form();
540 for _ in 0..3 {
542 form.handle_input(key('j'));
543 }
544 assert_eq!(form.focused(), 3);
545 form.handle_input(key('l'));
546 if let Field::Select(s) = &form.fields[3] {
547 assert_eq!(s.index, 1);
548 } else {
549 panic!("expected select");
550 }
551 form.handle_input(key('h'));
552 form.handle_input(key('h'));
553 if let Field::Select(s) = &form.fields[3] {
554 assert_eq!(s.index, 2); } else {
556 panic!("expected select");
557 }
558 }
559}