1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use gpui::{AnyElement, FontWeight, IntoElement, ParentElement, Styled};
use crate::prelude::*;
/// A label + input + help/error/success/warning wrapper for form layouts.
///
/// Note: `FormField` renders label/help/validation text around an opaque
/// `AnyElement` child; it cannot retroactively restyle that child's internal
/// focus ring. When `.error(...)`/`.success(...)`/`.warning(...)` is set,
/// pass a child that already reflects that state itself (e.g.
/// `TextInput::invalid(true)`, which already routes to `focus_ring_error`
/// colors) — this is the caller's responsibility, matching how `TextInput`'s
/// own validation flags work.
#[derive(IntoElement, RegisterComponent)]
pub struct FormField {
label: Option<SharedString>,
content: AnyElement,
help: Option<SharedString>,
error: Option<SharedString>,
success: Option<SharedString>,
warning: Option<SharedString>,
}
impl FormField {
pub fn new(content: impl IntoElement) -> Self {
Self {
label: None,
content: content.into_any_element(),
help: None,
error: None,
success: None,
warning: None,
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn help(mut self, help: impl Into<SharedString>) -> Self {
self.help = Some(help.into());
self
}
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
self.error = Some(error.into());
self
}
/// Shows a success message with a check-circle icon below the field.
/// Takes precedence over `.help(...)` but not `.error(...)`/`.warning(...)`.
pub fn success(mut self, success: impl Into<SharedString>) -> Self {
self.success = Some(success.into());
self
}
/// Shows a warning message with a warning icon below the field.
/// Takes precedence over `.help(...)`/`.success(...)` but not `.error(...)`.
pub fn warning(mut self, warning: impl Into<SharedString>) -> Self {
self.warning = Some(warning.into());
self
}
}
impl RenderOnce for FormField {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
// Precedence: error > warning > success > help.
let validation_message = self
.error
.as_ref()
.map(|msg| (msg.clone(), IconName::XCircle, palette::danger(600)))
.or_else(|| {
self.warning
.as_ref()
.map(|msg| (msg.clone(), IconName::Warning, palette::warning(600)))
})
.or_else(|| {
self.success
.as_ref()
.map(|msg| (msg.clone(), IconName::CheckCircle, palette::success(600)))
});
let has_validation_message = validation_message.is_some();
v_flex()
.gap_1()
.when_some(self.label, |this, label| {
this.child(
Label::new(label)
.size(LabelSize::Small)
.weight(FontWeight::MEDIUM),
)
})
.child(self.content)
.when_some(validation_message, |this, (message, icon, color)| {
this.child(
h_flex()
.gap_1()
.items_center()
.child(
Icon::new(icon)
.size(IconSize::XSmall)
.color(Color::Custom(color)),
)
.child(
Label::new(message)
.size(LabelSize::XSmall)
.color(Color::Custom(color)),
),
)
})
.when(!has_validation_message, |this| {
this.when_some(self.help, |this, help| {
this.child(Label::new(help).size(LabelSize::XSmall).color(Color::Muted))
})
})
}
}
impl Component for FormField {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn description() -> Option<&'static str> {
Some("Label + input + optional help/error/success/warning text for form layouts.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
.child(
FormField::new(Label::new("you@example.com").color(Color::Placeholder))
.label("Email")
.help("We'll never share your email.")
.into_any_element(),
)
.child(
FormField::new(Label::new("").color(Color::Placeholder))
.label("Password")
.error("Password must be at least 8 characters.")
.into_any_element(),
)
.child(
FormField::new(Label::new("acme-corp").color(Color::Default))
.label("Workspace slug")
.success("This slug is available.")
.into_any_element(),
)
.child(
FormField::new(Label::new("admin@example.com").color(Color::Default))
.label("Recovery email")
.warning("This email is not yet verified.")
.into_any_element(),
)
.into_any_element(),
)
}
}