Skip to main content

revue/widget/form/
form.rs

1//! Form and FormField widgets for automated form rendering
2//!
3//! These widgets provide automatic two-way binding with FormState,
4//! reducing form creation boilerplate by 50-70%.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use revue::prelude::*;
10//! use revue::patterns::form::FormState;
11//! use revue::widget::{Form, FormField};
12//!
13//! let form_state = FormState::new()
14//!     .field("email", |f| f.label("Email").required().email())
15//!     .field("password", |f| f.label("Password").required().min_length(8))
16//!     .build();
17//!
18//! Form::new(form_state.clone())
19//!     .on_submit(|data| {
20//!         println!("Form submitted: {:?}", data);
21//!     })
22//!     .child(FormField::new("email").placeholder("Enter email"))
23//!     .child(FormField::new("password").input_type(InputType::Password))
24//!     .child(Button::new("Submit").submit());
25//! ```
26
27use crate::patterns::form::FormState;
28use crate::render::{Cell, Modifier};
29use crate::style::Color;
30use crate::widget::traits::{RenderContext, View, WidgetProps};
31use std::collections::HashMap;
32use std::sync::Arc;
33
34/// Type alias for form submit callback
35type SubmitCallback = Arc<dyn Fn(HashMap<String, String>)>;
36
37/// Input type for FormField
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub enum InputType {
40    /// Standard text input
41    #[default]
42    Text,
43    /// Password input (masked)
44    Password,
45    /// Email input
46    Email,
47    /// Number input
48    Number,
49}
50
51/// Form widget for automated form rendering with FormState binding
52pub struct Form {
53    /// Form state for two-way binding
54    form_state: FormState,
55    /// Submit callback
56    on_submit: Option<SubmitCallback>,
57    /// Form widget properties
58    props: WidgetProps,
59    /// Custom submit button text (None = default)
60    submit_text: Option<String>,
61    /// Whether to show validation errors inline
62    show_errors: bool,
63    /// Error display style
64    error_style: ErrorDisplayStyle,
65}
66
67/// How to display validation errors
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub enum ErrorDisplayStyle {
70    /// Show errors below each field
71    #[default]
72    Inline,
73    /// Show errors at the bottom of the form
74    Summary,
75    /// Show both inline and summary
76    Both,
77}
78
79impl Form {
80    /// Create a new Form with FormState binding
81    pub fn new(form_state: FormState) -> Self {
82        Self {
83            form_state,
84            on_submit: None,
85            props: WidgetProps::default(),
86            submit_text: None,
87            show_errors: true,
88            error_style: ErrorDisplayStyle::default(),
89        }
90    }
91
92    /// Set submit callback
93    pub fn on_submit(mut self, callback: SubmitCallback) -> Self {
94        self.on_submit = Some(callback);
95        self
96    }
97
98    /// Set custom submit button text
99    pub fn submit_text(mut self, text: impl Into<String>) -> Self {
100        self.submit_text = Some(text.into());
101        self
102    }
103
104    /// Set whether to show validation errors inline
105    pub fn show_errors(mut self, show: bool) -> Self {
106        self.show_errors = show;
107        self
108    }
109
110    /// Set error display style
111    pub fn error_style(mut self, style: ErrorDisplayStyle) -> Self {
112        self.error_style = style;
113        self
114    }
115
116    /// Get the form state
117    pub fn form_state(&self) -> &FormState {
118        &self.form_state
119    }
120
121    /// Check if form is valid
122    pub fn is_valid(&self) -> bool {
123        self.form_state.is_valid()
124    }
125
126    /// Get the number of errors in the form
127    pub fn error_count(&self) -> usize {
128        self.form_state.errors().len()
129    }
130
131    /// Submit the form (triggers callback if valid)
132    pub fn submit(&self) {
133        if self.is_valid() {
134            if let Some(ref callback) = self.on_submit {
135                let data = self.form_state.values();
136                callback(data);
137            }
138        }
139    }
140
141    /// Render form border
142    fn render_border(&self, ctx: &mut RenderContext) {
143        let area = ctx.area;
144        if area.width < 2 || area.height < 2 {
145            return;
146        }
147
148        let border_color = if self.is_valid() {
149            Color::rgb(100, 100, 100)
150        } else {
151            Color::rgb(200, 80, 80) // Red for invalid
152        };
153
154        // Draw horizontal borders
155        for x in area.x..area.x + area.width {
156            let mut top_cell = Cell::new('─');
157            top_cell.fg = Some(border_color);
158            ctx.buffer.set(x, area.y, top_cell);
159
160            let mut bottom_cell = Cell::new('─');
161            bottom_cell.fg = Some(border_color);
162            ctx.buffer.set(x, area.y + area.height - 1, bottom_cell);
163        }
164
165        // Draw vertical borders
166        for y in area.y..area.y + area.height {
167            let mut left_cell = Cell::new('│');
168            left_cell.fg = Some(border_color);
169            ctx.buffer.set(area.x, y, left_cell);
170
171            let mut right_cell = Cell::new('│');
172            right_cell.fg = Some(border_color);
173            ctx.buffer.set(area.x + area.width - 1, y, right_cell);
174        }
175
176        // Draw corners
177        let corners = [
178            ('┌', area.x, area.y),
179            ('┐', area.x + area.width - 1, area.y),
180            ('└', area.x, area.y + area.height - 1),
181            ('┘', area.x + area.width - 1, area.y + area.height - 1),
182        ];
183
184        for &(ch, x, y) in &corners {
185            let mut cell = Cell::new(ch);
186            cell.fg = Some(border_color);
187            ctx.buffer.set(x, y, cell);
188        }
189    }
190
191    /// Render form title
192    fn render_title(&self, ctx: &mut RenderContext) {
193        let area = ctx.area;
194        if area.width < 4 {
195            return;
196        }
197
198        let title = "Form";
199        let title_x = area.x + 2;
200
201        for (i, ch) in title.chars().enumerate() {
202            if title_x + (i as u16) < area.x + area.width - 1 {
203                let mut cell = Cell::new(ch);
204                cell.fg = Some(Color::WHITE);
205                cell.bg = Some(Color::BLACK);
206                ctx.buffer.set(title_x + i as u16, area.y, cell);
207            }
208        }
209    }
210}
211
212impl Default for Form {
213    fn default() -> Self {
214        Self::new(FormState::new().build())
215    }
216}
217
218impl View for Form {
219    crate::impl_view_meta!("Form");
220
221    fn render(&self, ctx: &mut RenderContext) {
222        let area = ctx.area;
223
224        // Render border
225        self.render_border(ctx);
226
227        // Render title
228        self.render_title(ctx);
229
230        // Render validation status at bottom
231        let status_y = area.y + area.height - 2;
232        if status_y > area.y && area.width > 4 {
233            let status_text = if self.is_valid() {
234                "✓ Valid".to_string()
235            } else {
236                let error_count = self.error_count();
237                format!("✗ {} error(s)", error_count)
238            };
239
240            let status_color = if self.is_valid() {
241                Color::rgb(80, 200, 80)
242            } else {
243                Color::rgb(200, 80, 80)
244            };
245
246            for (i, ch) in status_text.chars().enumerate() {
247                let x = area.x + 2 + i as u16;
248                if x < area.x + area.width - 2 {
249                    let mut cell = Cell::new(ch);
250                    cell.fg = Some(status_color);
251                    ctx.buffer.set(x, status_y, cell);
252                }
253            }
254        }
255    }
256}
257
258/// FormField widget for individual form field rendering
259pub struct FormFieldWidget {
260    /// Field name (key in FormState)
261    name: String,
262    /// Placeholder text
263    placeholder: String,
264    /// Input type
265    input_type: InputType,
266    /// Widget properties
267    props: WidgetProps,
268    /// Whether to show label
269    show_label: bool,
270    /// Whether to show errors inline
271    show_errors: bool,
272}
273
274impl FormFieldWidget {
275    /// Create a new FormField widget
276    pub fn new(name: impl Into<String>) -> Self {
277        Self {
278            name: name.into(),
279            placeholder: String::new(),
280            input_type: InputType::Text,
281            props: WidgetProps::default(),
282            show_label: true,
283            show_errors: true,
284        }
285    }
286
287    /// Set placeholder text
288    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
289        self.placeholder = text.into();
290        self
291    }
292
293    /// Set input type
294    pub fn input_type(mut self, input_type: InputType) -> Self {
295        self.input_type = input_type;
296        self
297    }
298
299    /// Set whether to show label
300    pub fn show_label(mut self, show: bool) -> Self {
301        self.show_label = show;
302        self
303    }
304
305    /// Set whether to show errors inline
306    pub fn show_errors(mut self, show: bool) -> Self {
307        self.show_errors = show;
308        self
309    }
310
311    /// Get the field name
312    pub fn name(&self) -> &str {
313        &self.name
314    }
315
316    /// Render the field label
317    #[allow(dead_code)]
318    fn render_label(&self, form_state: &FormState, ctx: &mut RenderContext) {
319        let area = ctx.area;
320        if let Some(field) = form_state.get(&self.name) {
321            let label = &field.label;
322
323            for (i, ch) in label.chars().enumerate() {
324                if area.x + (i as u16) < area.x + area.width {
325                    let mut cell = Cell::new(ch);
326                    cell.fg = Some(Color::rgb(200, 200, 200));
327                    ctx.buffer.set(area.x + i as u16, area.y, cell);
328                }
329            }
330        }
331    }
332
333    /// Render the field value
334    #[allow(dead_code)]
335    fn render_value(&self, form_state: &FormState, ctx: &mut RenderContext) {
336        let area = ctx.area;
337        let value = form_state.value(&self.name).unwrap_or_default();
338
339        // Display value or placeholder
340        let display_text = if value.is_empty() {
341            self.placeholder.clone()
342        } else {
343            match self.input_type {
344                InputType::Password => "•".repeat(value.len().min(20)),
345                _ => value.clone(),
346            }
347        };
348
349        let text_color = if value.is_empty() {
350            Color::rgb(120, 120, 120) // Gray for placeholder
351        } else {
352            Color::WHITE
353        };
354
355        for (i, ch) in display_text.chars().enumerate() {
356            let x = area.x + i as u16;
357            if x < area.x + area.width {
358                let mut cell = Cell::new(ch);
359                cell.fg = Some(text_color);
360                ctx.buffer.set(x, area.y, cell);
361            }
362        }
363    }
364
365    /// Render validation errors
366    #[allow(dead_code)]
367    fn render_errors(&self, form_state: &FormState, ctx: &mut RenderContext) {
368        if !self.show_errors {
369            return;
370        }
371
372        let field = match form_state.get(&self.name) {
373            Some(f) => f,
374            None => return,
375        };
376
377        let error_msg = match field.first_error() {
378            Some(err) => err,
379            None => return,
380        };
381
382        let area = ctx.area;
383        let error_color = Color::rgb(200, 80, 80);
384
385        for (i, ch) in error_msg.chars().enumerate() {
386            let x = area.x + i as u16;
387            if x < area.x + area.width {
388                let mut cell = Cell::new(ch);
389                cell.fg = Some(error_color);
390                cell.modifier |= Modifier::DIM;
391                ctx.buffer.set(x, area.y, cell);
392            }
393        }
394    }
395}
396
397impl Default for FormFieldWidget {
398    fn default() -> Self {
399        Self::new("")
400    }
401}
402
403impl View for FormFieldWidget {
404    crate::impl_view_meta!("FormField");
405
406    fn render(&self, ctx: &mut RenderContext) {
407        // Note: Actual rendering happens through Form which has access to FormState
408        // This is a placeholder render - in practice, Form renders its children
409        let area = ctx.area;
410
411        if area.width > self.name.len() as u16 {
412            for (i, ch) in self.name.chars().enumerate() {
413                let mut cell = Cell::new(ch);
414                cell.fg = Some(Color::rgb(150, 150, 150));
415                ctx.buffer.set(area.x + i as u16, area.y, cell);
416            }
417        }
418    }
419}
420
421/// Convenience function to create a Form
422pub fn form(form_state: FormState) -> Form {
423    Form::new(form_state)
424}
425
426/// Convenience function to create a FormField
427pub fn form_field(name: impl Into<String>) -> FormFieldWidget {
428    FormFieldWidget::new(name)
429}
430
431// Re-export FormField from patterns module for convenience
432pub use crate::patterns::form::FormField;
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_form_new() {
440        let form_state = FormState::new().build();
441        let form = Form::new(form_state);
442        assert!(form.is_valid());
443    }
444
445    #[test]
446    fn test_form_builder() {
447        let form_state = FormState::new().build();
448        let form = Form::new(form_state)
449            .submit_text("Send")
450            .show_errors(true)
451            .error_style(ErrorDisplayStyle::Summary);
452
453        assert_eq!(form.submit_text, Some("Send".to_string()));
454        assert!(form.show_errors);
455        assert_eq!(form.error_style, ErrorDisplayStyle::Summary);
456    }
457
458    #[test]
459    fn test_input_type_default() {
460        let input_type = InputType::default();
461        assert_eq!(input_type, InputType::Text);
462    }
463
464    #[test]
465    fn test_form_field_new() {
466        let field = FormFieldWidget::new("username");
467        assert_eq!(field.name, "username");
468        assert_eq!(field.input_type, InputType::Text);
469    }
470
471    #[test]
472    fn test_form_field_builder() {
473        let field = FormFieldWidget::new("email")
474            .placeholder("Enter email")
475            .input_type(InputType::Email)
476            .show_label(false)
477            .show_errors(false);
478
479        assert_eq!(field.placeholder, "Enter email");
480        assert_eq!(field.input_type, InputType::Email);
481        assert!(!field.show_label);
482        assert!(!field.show_errors);
483    }
484
485    #[test]
486    fn test_error_display_style_default() {
487        let style = ErrorDisplayStyle::default();
488        assert_eq!(style, ErrorDisplayStyle::Inline);
489    }
490
491    #[test]
492    fn test_convenience_functions() {
493        let form_state = FormState::new().build();
494        let form = form(form_state);
495        assert_eq!(form.submit_text, None);
496
497        let field = form_field("password");
498        assert_eq!(field.name, "password");
499    }
500}