gpui_kit/controls/
form_field.rs1use gpui::{
8 AnyElement, App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
9 prelude::FluentBuilder, px,
10};
11use gpui_kit_semantics::{NodeSpec, Role, Semantic};
12use gpui_kit_theme::{ActiveTheme, Space, TypeScale};
13
14use crate::foundation::{Ident, StyledExt, text as foundation_text};
15use crate::overlay::Kbd;
16
17#[derive(IntoElement)]
26pub struct FormField {
27 ident: Ident,
28 label: SharedString,
29 control: Option<SharedString>,
30 description: Option<SharedString>,
31 error: Option<SharedString>,
32 hint: Option<SharedString>,
33 required: bool,
34 children: Vec<AnyElement>,
35}
36
37impl std::fmt::Debug for FormField {
38 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 formatter
40 .debug_struct("FormField")
41 .field("ident", &self.ident)
42 .field("label", &self.label)
43 .field("control", &self.control)
44 .field("required", &self.required)
45 .field("invalid", &self.error.is_some())
46 .finish()
47 }
48}
49
50impl FormField {
51 pub fn new(ident: impl Into<Ident>, label: impl Into<SharedString>) -> Self {
52 Self {
53 ident: ident.into(),
54 label: label.into(),
55 control: None,
56 description: None,
57 error: None,
58 hint: None,
59 required: false,
60 children: Vec::new(),
61 }
62 }
63
64 pub fn control(mut self, control: impl Into<SharedString>) -> Self {
69 self.control = Some(control.into());
70 self
71 }
72
73 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
75 self.description = Some(description.into());
76 self
77 }
78
79 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
81 self.error = Some(error.into());
82 self
83 }
84
85 pub fn hint(mut self, keystroke: impl Into<SharedString>) -> Self {
87 self.hint = Some(keystroke.into());
88 self
89 }
90
91 pub fn required(mut self, required: bool) -> Self {
92 self.required = required;
93 self
94 }
95
96 pub fn is_invalid(&self) -> bool {
97 self.error.is_some()
98 }
99
100 fn shown_description(&self) -> Option<SharedString> {
102 match (&self.description, &self.error) {
103 (Some(description), Some(error)) if description == error => None,
104 (description, _) => description.clone(),
105 }
106 }
107}
108
109impl ParentElement for FormField {
110 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
111 self.children.extend(elements);
112 }
113}
114
115impl RenderOnce for FormField {
116 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
117 let theme = cx.theme().clone();
118 let invalid = self.error.is_some();
119 let field_id = self.ident.semantic_id();
120 let label_ident = self.ident.child("label");
121 let description = self.shown_description();
122
123 let mut label_spec = NodeSpec::new(label_ident.semantic_id(), Role::Text)
124 .parent(field_id.clone())
125 .text(self.label.clone());
126 if let Some(control) = self.control.clone() {
127 label_spec = label_spec.labels(control);
128 }
129
130 let label_element = foundation_text(&theme, TypeScale::Label, self.label.clone())
131 .row()
132 .gap_token(&theme, Space::Xs)
133 .when(self.required, |element| {
134 element.child(
135 foundation_text(&theme, TypeScale::Label, SharedString::from("*"))
136 .text_color(theme.colors.danger),
137 )
138 })
139 .when_some(self.hint.clone(), |element, keystroke| {
140 element.child(
141 div()
142 .ml_auto()
143 .child(Kbd::new(keystroke).id(self.ident.child("hint"))),
144 )
145 })
146 .semantic_in(cx, label_spec);
147
148 let description = description.map(|text| {
149 foundation_text(&theme, TypeScale::Caption, text.clone())
150 .text_tone(&theme, gpui_kit_theme::TextTone::Muted)
151 .semantic_in(
152 cx,
153 NodeSpec::new(self.ident.child("description").semantic_id(), Role::Text)
154 .parent(field_id.clone())
155 .text(text),
156 )
157 });
158
159 let error = self.error.clone().map(|text| {
160 foundation_text(&theme, TypeScale::Caption, text.clone())
161 .text_color(theme.colors.danger)
162 .semantic_in(
163 cx,
164 NodeSpec::new(self.ident.child("error").semantic_id(), Role::Status)
165 .parent(field_id.clone())
166 .invalid(true)
167 .text(text),
168 )
169 });
170
171 div()
172 .column()
173 .w_full()
174 .gap(px(theme.space(Space::Xs)))
175 .child(label_element)
176 .children(self.children)
177 .children(description)
178 .children(error)
179 .semantic_in(
180 cx,
181 NodeSpec::new(field_id, Role::Field)
182 .text(self.label.clone())
183 .required(self.required)
184 .invalid(invalid),
185 )
186 }
187}