Skip to main content

hjkl_form/
form.rs

1//! `Form` — the top-level container.
2
3use crate::field::Field;
4use crate::submit::{SubmitFn, SubmitOutcome};
5
6/// Form-level mode. Insert delegates to the focused field's `Editor`.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FormMode {
9    Normal,
10    Insert,
11}
12
13/// Events emitted by [`crate::Form::handle_input`] back to the host.
14#[derive(Debug)]
15pub enum FormEvent {
16    /// Focus moved, value mutated, or any user-visible state changed.
17    Changed,
18    /// User pressed `Esc` outside Insert mode.
19    Cancelled,
20    /// Submit fired (validators ran). Outcome is the `SubmitFn` return
21    /// or `Err(...)` if validation failed.
22    Submitted(SubmitOutcome),
23    /// Submit was attempted but blocked by validation. Field errors
24    /// are populated; the host should re-render.
25    ValidationFailed,
26}
27
28/// A vim-modal form. Holds an ordered list of fields, the focused
29/// index, the current `FormMode`, an optional title, and a `submit`
30/// closure consumed on first call.
31pub struct Form {
32    pub title: Option<String>,
33    pub fields: Vec<Field>,
34    pub mode: FormMode,
35    pub(crate) focused: usize,
36    pub(crate) submit: Option<SubmitFn>,
37    pub(crate) dirty_gen: u64,
38    pub(crate) pending_g: bool,
39}
40
41impl Form {
42    /// Build an empty form.
43    pub fn new() -> Self {
44        Self {
45            title: None,
46            fields: Vec::new(),
47            mode: FormMode::Normal,
48            focused: 0,
49            submit: None,
50            dirty_gen: 0,
51            pending_g: false,
52        }
53    }
54
55    /// Set an optional title row rendered above the fields.
56    pub fn with_title(mut self, title: impl Into<String>) -> Self {
57        self.title = Some(title.into());
58        self
59    }
60
61    /// Append a field to the form. Order matters — first field is
62    /// focused by default and `j`/`k` walks the list in order.
63    pub fn with_field(mut self, field: Field) -> Self {
64        self.fields.push(field);
65        self
66    }
67
68    /// Register the submit closure. Consumed via `Option::take` on
69    /// the first successful submit.
70    pub fn with_submit(mut self, submit: SubmitFn) -> Self {
71        self.submit = Some(submit);
72        self
73    }
74
75    /// Index of the currently-focused field.
76    pub fn focused(&self) -> usize {
77        self.focused
78    }
79
80    /// Borrow the focused field, if any.
81    pub fn focused_field(&self) -> Option<&Field> {
82        self.fields.get(self.focused)
83    }
84
85    /// Mutably borrow the focused field, if any.
86    pub fn focused_field_mut(&mut self) -> Option<&mut Field> {
87        self.fields.get_mut(self.focused)
88    }
89
90    /// Programmatically set the focus index. Out-of-range indices are
91    /// ignored.
92    pub fn set_focus(&mut self, index: usize) {
93        if index < self.fields.len() {
94            self.focused = index;
95            self.dirty_gen = self.dirty_gen.wrapping_add(1);
96        }
97    }
98
99    /// Sum of the form-level dirty counter and every text field's
100    /// buffer `dirty_gen`. Renderers can cheap-check this to skip
101    /// redraws.
102    pub fn dirty_gen(&self) -> u64 {
103        let mut sum = self.dirty_gen;
104        for field in &self.fields {
105            match field {
106                Field::SingleLineText(f) | Field::MultiLineText(f) => {
107                    sum = sum.wrapping_add(f.editor.buffer().dirty_gen());
108                }
109                _ => {}
110            }
111        }
112        sum
113    }
114
115    pub(crate) fn bump_dirty(&mut self) {
116        self.dirty_gen = self.dirty_gen.wrapping_add(1);
117    }
118}
119
120impl Default for Form {
121    fn default() -> Self {
122        Self::new()
123    }
124}