Skip to main content

ui/components/
form.rs

1use gpui::AnyElement;
2
3use crate::prelude::*;
4
5/// Layout + validation-state wrapper composing [`FormField`].
6///
7/// GPUI has no React-Hook-Form equivalent — this struct carries
8/// `error`/`touched`/`dirty` flags and feeds them into `FormField`'s existing
9/// error slot. Schema validation libraries are explicitly out of scope.
10#[derive(IntoElement, RegisterComponent)]
11pub struct Form {
12    label: Option<SharedString>,
13    content: AnyElement,
14    help: Option<SharedString>,
15    error: Option<SharedString>,
16    success: Option<SharedString>,
17    warning: Option<SharedString>,
18    touched: bool,
19    dirty: bool,
20}
21
22impl Form {
23    pub fn new(content: impl IntoElement) -> Self {
24        Self {
25            label: None,
26            content: content.into_any_element(),
27            help: None,
28            error: None,
29            success: None,
30            warning: None,
31            touched: false,
32            dirty: false,
33        }
34    }
35
36    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
37        self.label = Some(label.into());
38        self
39    }
40
41    pub fn help(mut self, help: impl Into<SharedString>) -> Self {
42        self.help = Some(help.into());
43        self
44    }
45
46    pub fn error(mut self, error: impl Into<SharedString>) -> Self {
47        self.error = Some(error.into());
48        self
49    }
50
51    pub fn success(mut self, success: impl Into<SharedString>) -> Self {
52        self.success = Some(success.into());
53        self
54    }
55
56    pub fn warning(mut self, warning: impl Into<SharedString>) -> Self {
57        self.warning = Some(warning.into());
58        self
59    }
60
61    /// Marks the field as touched — errors are only shown once touched.
62    pub fn touched(mut self, touched: bool) -> Self {
63        self.touched = touched;
64        self
65    }
66
67    /// Marks the field as dirty (modified from its initial value).
68    pub fn dirty(mut self, dirty: bool) -> Self {
69        self.dirty = dirty;
70        self
71    }
72
73    pub fn is_touched(&self) -> bool {
74        self.touched
75    }
76
77    pub fn is_dirty(&self) -> bool {
78        self.dirty
79    }
80}
81
82impl RenderOnce for Form {
83    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
84        let mut field = FormField::new(self.content);
85
86        if let Some(label) = self.label {
87            field = field.label(label);
88        }
89        if let Some(help) = self.help {
90            field = field.help(help);
91        }
92        if let Some(success) = self.success {
93            field = field.success(success);
94        }
95        if let Some(warning) = self.warning {
96            field = field.warning(warning);
97        }
98        if self.touched {
99            if let Some(error) = self.error {
100                field = field.error(error);
101            }
102        }
103
104        v_flex().gap_1().child(field).when(self.dirty, |this| {
105            this.child(
106                Label::new("Modified")
107                    .size(LabelSize::XSmall)
108                    .color(Color::Muted),
109            )
110        })
111    }
112}
113
114impl Component for Form {
115    fn scope() -> ComponentScope {
116        ComponentScope::Input
117    }
118
119    fn description() -> Option<&'static str> {
120        Some(
121            "A form field wrapper with touched/dirty validation state composing FormField. Not a schema-validation library.",
122        )
123    }
124
125    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
126        Some(
127            v_flex()
128                .gap_6()
129                .child(
130                    Form::new(Label::new("you@example.com").color(Color::Placeholder))
131                        .label("Email")
132                        .help("We'll never share your email.")
133                        .touched(false),
134                )
135                .child(
136                    Form::new(Label::new("").color(Color::Placeholder))
137                        .label("Password")
138                        .error("Password must be at least 8 characters.")
139                        .touched(true)
140                        .dirty(true),
141                )
142                .into_any_element(),
143        )
144    }
145}