1use gpui::AnyElement;
2
3use crate::prelude::*;
4
5#[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 pub fn touched(mut self, touched: bool) -> Self {
63 self.touched = touched;
64 self
65 }
66
67 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}