tui_form_widget/
form.rs

1use crossterm::event::KeyCode;
2use ratatui::{prelude::*, widgets::*};
3
4use crate::widget::Renderer;
5
6pub enum FieldStatus {
7    Valid,
8    Invalid,
9}
10
11impl Into<String> for Field<'_> {
12    fn into(self) -> String {
13        self.fd.val.to_string()
14    }
15}
16
17/// A reference to a specific field's data in a form that also indicates whether or not it's valid.
18pub struct Field<'a> {
19    fd: FieldData<'a>,
20    status: FieldStatus,
21}
22
23impl<'a> Field<'a> {
24    pub fn valid(name: &'a str, val: &'a str) -> Field<'a> {
25        Self {
26            fd: FieldData { name, val },
27            status: FieldStatus::Valid,
28        }
29    }
30
31    pub fn invalid(name: &'a str, val: &'a str) -> Field<'a> {
32        Self {
33            fd: FieldData { name, val },
34            status: FieldStatus::Invalid,
35        }
36    }
37
38    pub fn name(&self) -> &str {
39        self.fd.name
40    }
41
42    pub fn value(&self) -> &str {
43        self.fd.val
44    }
45
46    fn inner(&self) -> &FieldData<'_> {
47        &self.fd
48    }
49
50    pub fn is_valid(&self) -> bool {
51        match self.status {
52            FieldStatus::Valid => true,
53            FieldStatus::Invalid => false,
54        }
55    }
56}
57
58#[derive(Clone)]
59struct FieldData<'a> {
60    name: &'a str,
61    val: &'a str,
62}
63
64type FormFieldStatus<'a> = Vec<Field<'a>>;
65#[derive(PartialEq)]
66pub enum FormSelection {
67    NoSelection,
68    Hovered(usize),
69    Active(usize),
70}
71
72pub(crate) struct FieldBuffer {
73    name: String,
74    val: String,
75}
76
77impl From<Vec<(&str, &str)>> for Form {
78    fn from(value: Vec<(&str, &str)>) -> Self {
79        Self {
80            fields: value
81                .into_iter()
82                .map(|(d_name, d_val)| FieldBuffer {
83                    name: d_name.to_string(),
84                    val: d_val.to_string(),
85                })
86                .collect(),
87            ..Default::default()
88        }
89    }
90}
91
92impl From<Vec<&str>> for Form {
93    fn from(value: Vec<&str>) -> Self {
94        Self {
95            fields: value
96                .into_iter()
97                .map(|d_name| FieldBuffer {
98                    name: d_name.to_string(),
99                    val: String::new(),
100                })
101                .collect(),
102            ..Default::default()
103        }
104    }
105}
106
107impl From<Vec<FieldBuffer>> for Form {
108    fn from(value: Vec<FieldBuffer>) -> Self {
109        Self {
110            fields: value,
111            ..Default::default()
112        }
113    }
114}
115
116/// A widget to display data in a collection of fields
117///
118/// # Example
119///
120/// ```rust
121/// let form = Form::new(&["A", "B", "C"], |field| !field.is_empty());
122///
123/// // all fields remain valid until form is submitted.
124/// assert_eq!(form.status().iter().all(|field| field.is_valid()), true);
125///
126/// // fields will now be invalid after submitting.
127/// form.submit();
128/// assert_eq!(form.status().iter().all(|field| field.is_valid()), false);
129///
130/// form.select(FormSelection::Active(0));
131/// form.append_selection('a');
132/// assert!(form.status[0].is_valid());
133/// ```
134pub struct Form {
135    selected: FormSelection,
136    pub(crate) fields: Vec<FieldBuffer>,
137    pub(crate) submitted: bool,
138    validation_fn: Box<dyn Fn(&str) -> bool + 'static>,
139    pub(crate) default_field_style: Style,
140    pub(crate) invalid_field_style: Style,
141    pub(crate) hovered_field_style: Style,
142    pub(crate) active_field_style: Style,
143}
144
145impl Default for Form {
146    fn default() -> Self {
147        Self {
148            selected: FormSelection::NoSelection,
149            fields: Vec::new(),
150            submitted: false,
151            validation_fn: Box::new(|f| !f.is_empty()),
152            default_field_style: Style::default(),
153            invalid_field_style: Style::default().red().bold(),
154            hovered_field_style: Style::default().cyan(),
155            active_field_style: Style::default().cyan().bold(),
156        }
157    }
158}
159
160impl Form {
161    /// Create a new [`Form`] from a slice of field titles and a validator function.
162    /// `validation_fn` is used to mark fields as either valid or invalid when `.status()` is called.
163    pub fn new(fields: &[&str], validation_fn: impl Fn(&str) -> bool + 'static) -> Self {
164        let fields = fields
165            .iter()
166            .map(|&title| FieldBuffer {
167                name: title.to_string(),
168                val: String::new(),
169            })
170            .collect();
171
172        Self {
173            fields,
174            validation_fn: Box::new(validation_fn),
175            ..Default::default()
176        }
177    }
178
179    pub fn widget(&self) -> impl Widget + '_ {
180        Renderer::new(self)
181    }
182
183    pub fn select(&mut self, s: FormSelection) {
184        self.selected = s;
185    }
186
187    pub fn selected(&self) -> &FormSelection {
188        &self.selected
189    }
190
191    /// Submits form and returns status of fields.
192    pub fn submit(&mut self) -> FormFieldStatus {
193        self.submitted = true;
194        self.status()
195    }
196
197    /// Returns the state of all fields in the form. Uses a [`Field`] struct to indicate whether or
198    /// not each field's buffer is valid.
199    pub fn status(&self) -> FormFieldStatus {
200        if self.submitted {
201            self.fields
202                .iter()
203                .map(|fb| {
204                    if (self.validation_fn)(&fb.val) {
205                        Field::valid(&fb.name, &fb.val)
206                    } else {
207                        Field::invalid(&fb.name, &fb.val)
208                    }
209                })
210                .collect()
211        } else {
212            self.fields
213                .iter()
214                .map(|fb| Field::valid(&fb.name, &fb.val))
215                .collect()
216        }
217    }
218
219    pub fn input(&mut self, key: KeyCode) {
220        if let FormSelection::Active(i) = self.selected {
221            match key {
222                KeyCode::Enter => self.next_field(),
223                KeyCode::Esc => self.select(FormSelection::Hovered(i)),
224                KeyCode::Backspace => self.pop_field(i),
225                KeyCode::Char(ch) => self.append_field(ch, i),
226                _ => {}
227            }
228        } else {
229            match key {
230                KeyCode::Esc => self.select(FormSelection::NoSelection),
231                KeyCode::Char('j') => self.next_field(),
232                KeyCode::Char('k') => self.prev_field(),
233                KeyCode::Enter => {
234                    if let FormSelection::Hovered(i) = self.selected {
235                        self.selected = FormSelection::Active(i)
236                    } else {
237                        self.selected = FormSelection::Active(0)
238                    }
239                }
240                _ => {}
241            }
242        }
243    }
244
245    fn pop_field(&mut self, field: usize) {
246        self.fields[field].val.pop();
247    }
248
249    fn append_field(&mut self, ch: char, field: usize) {
250        self.fields[field].val.push(ch)
251    }
252
253    pub fn append_selection(&mut self, ch: char) {
254        match self.selected() {
255            FormSelection::NoSelection => {}
256            FormSelection::Hovered(_) => {}
257            FormSelection::Active(i) => self.append_field(ch, *i),
258        }
259    }
260
261    pub fn pop_selection(&mut self) {
262        match self.selected() {
263            FormSelection::NoSelection => {}
264            FormSelection::Hovered(_) => {}
265            FormSelection::Active(i) => self.pop_field(*i),
266        }
267    }
268
269    pub fn deselect(&mut self) {
270        self.selected = FormSelection::NoSelection
271    }
272
273    pub fn next_field(&mut self) {
274        self.selected = match self.selected {
275            FormSelection::NoSelection => FormSelection::Hovered(0),
276            FormSelection::Hovered(i) => {
277                FormSelection::Hovered((i + 1).rem_euclid(self.fields.len()))
278            }
279            FormSelection::Active(i) => {
280                FormSelection::Active((i + 1).rem_euclid(self.fields.len()))
281            }
282        }
283    }
284
285    pub fn prev_field(&mut self) {
286        self.selected = match self.selected {
287            FormSelection::NoSelection => FormSelection::Hovered(0),
288            FormSelection::Hovered(i) => {
289                let i = if i == 0 { self.fields.len() - 1 } else { i - 1 };
290                FormSelection::Hovered(i)
291            }
292            FormSelection::Active(i) => {
293                let i = if i == 0 { self.fields.len() - 1 } else { i - 1 };
294                FormSelection::Active(i)
295            }
296        }
297    }
298
299    /// Set whether the Form has been submitted
300    pub fn submitted(&mut self, submitted: bool) {
301        self.submitted = submitted;
302    }
303
304    pub fn active_field_style(&mut self, style: Style) {
305        self.active_field_style = style;
306    }
307
308    pub fn invalid_field_style(&mut self, style: Style) {
309        self.invalid_field_style = style;
310    }
311
312    pub fn hovered_field_style(&mut self, style: Style) {
313        self.hovered_field_style = style;
314    }
315
316    pub fn default_field_style(&mut self, style: Style) {
317        self.default_field_style = style;
318    }
319}