ui/components/
form_field.rs1use gpui::{AnyElement, FontWeight, IntoElement, ParentElement, Styled};
2
3use crate::prelude::*;
4
5#[derive(IntoElement, RegisterComponent)]
15pub struct FormField {
16 label: Option<SharedString>,
17 content: AnyElement,
18 help: Option<SharedString>,
19 error: Option<SharedString>,
20 success: Option<SharedString>,
21 warning: Option<SharedString>,
22}
23
24impl FormField {
25 pub fn new(content: impl IntoElement) -> Self {
26 Self {
27 label: None,
28 content: content.into_any_element(),
29 help: None,
30 error: None,
31 success: None,
32 warning: None,
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 {
54 self.success = Some(success.into());
55 self
56 }
57
58 pub fn warning(mut self, warning: impl Into<SharedString>) -> Self {
61 self.warning = Some(warning.into());
62 self
63 }
64}
65
66impl RenderOnce for FormField {
67 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
68 let validation_message = self
70 .error
71 .as_ref()
72 .map(|msg| (msg.clone(), IconName::XCircle, palette::danger(600)))
73 .or_else(|| {
74 self.warning
75 .as_ref()
76 .map(|msg| (msg.clone(), IconName::Warning, palette::warning(600)))
77 })
78 .or_else(|| {
79 self.success
80 .as_ref()
81 .map(|msg| (msg.clone(), IconName::CheckCircle, palette::success(600)))
82 });
83 let has_validation_message = validation_message.is_some();
84
85 v_flex()
86 .gap_1()
87 .when_some(self.label, |this, label| {
88 this.child(
89 Label::new(label)
90 .size(LabelSize::Small)
91 .weight(FontWeight::MEDIUM),
92 )
93 })
94 .child(self.content)
95 .when_some(validation_message, |this, (message, icon, color)| {
96 this.child(
97 h_flex()
98 .gap_1()
99 .items_center()
100 .child(
101 Icon::new(icon)
102 .size(IconSize::XSmall)
103 .color(Color::Custom(color)),
104 )
105 .child(
106 Label::new(message)
107 .size(LabelSize::XSmall)
108 .color(Color::Custom(color)),
109 ),
110 )
111 })
112 .when(!has_validation_message, |this| {
113 this.when_some(self.help, |this, help| {
114 this.child(Label::new(help).size(LabelSize::XSmall).color(Color::Muted))
115 })
116 })
117 }
118}
119
120impl Component for FormField {
121 fn scope() -> ComponentScope {
122 ComponentScope::Input
123 }
124
125 fn description() -> Option<&'static str> {
126 Some("Label + input + optional help/error/success/warning text for form layouts.")
127 }
128
129 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
130 Some(
131 v_flex()
132 .gap_6()
133 .child(
134 FormField::new(Label::new("you@example.com").color(Color::Placeholder))
135 .label("Email")
136 .help("We'll never share your email.")
137 .into_any_element(),
138 )
139 .child(
140 FormField::new(Label::new("").color(Color::Placeholder))
141 .label("Password")
142 .error("Password must be at least 8 characters.")
143 .into_any_element(),
144 )
145 .child(
146 FormField::new(Label::new("acme-corp").color(Color::Default))
147 .label("Workspace slug")
148 .success("This slug is available.")
149 .into_any_element(),
150 )
151 .child(
152 FormField::new(Label::new("admin@example.com").color(Color::Default))
153 .label("Recovery email")
154 .warning("This email is not yet verified.")
155 .into_any_element(),
156 )
157 .into_any_element(),
158 )
159 }
160}