1use std::rc::{Rc, Weak};
2
3use gpui::{
4 div, prelude::FluentBuilder as _, px, AlignItems, AnyElement, AnyView, App, Axis, Div, Element,
5 ElementId, FocusHandle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Rems,
6 RenderOnce, SharedString, Styled, Window,
7};
8
9use crate::{h_flex, v_flex, ActiveTheme as _, AxisExt, Sizable, Size, StyledExt};
10
11pub fn v_form() -> Form {
13 Form::vertical()
14}
15
16pub fn h_form() -> Form {
18 Form::horizontal()
19}
20
21pub fn form_field() -> FormField {
23 FormField::new()
24}
25
26#[derive(IntoElement)]
27pub struct Form {
28 fields: Vec<FormField>,
29 props: FieldProps,
30}
31
32#[derive(Clone, Copy)]
33struct FieldProps {
34 size: Size,
35 label_width: Option<Pixels>,
36 label_text_size: Option<Rems>,
37 layout: Axis,
38 gap: Option<Pixels>,
40 column: u16,
41}
42
43impl Default for FieldProps {
44 fn default() -> Self {
45 Self {
46 label_width: Some(px(140.)),
47 label_text_size: None,
48 layout: Axis::Vertical,
49 size: Size::default(),
50 gap: None,
51 column: 1,
52 }
53 }
54}
55
56impl Form {
57 fn new() -> Self {
58 Self {
59 props: FieldProps::default(),
60 fields: Vec::new(),
61 }
62 }
63
64 pub fn horizontal() -> Self {
66 Self::new().layout(Axis::Horizontal)
67 }
68
69 pub fn vertical() -> Self {
71 Self::new().layout(Axis::Vertical)
72 }
73
74 pub fn layout(mut self, layout: Axis) -> Self {
76 self.props.layout = layout;
77 self
78 }
79
80 pub fn label_width(mut self, width: Pixels) -> Self {
82 self.props.label_width = Some(width);
83 self
84 }
85
86 pub fn label_text_size(mut self, size: Rems) -> Self {
88 self.props.label_text_size = Some(size);
89 self
90 }
91
92 pub fn gap(mut self, gap: Pixels) -> Self {
94 self.props.gap = Some(gap);
95 self
96 }
97
98 pub fn child(mut self, field: impl Into<FormField>) -> Self {
100 self.fields.push(field.into());
101 self
102 }
103
104 pub fn children(mut self, fields: impl IntoIterator<Item = FormField>) -> Self {
106 self.fields.extend(fields);
107 self
108 }
109
110 pub fn column(mut self, column: u16) -> Self {
114 self.props.column = column;
115 self
116 }
117}
118
119impl Sizable for Form {
120 fn with_size(mut self, size: impl Into<Size>) -> Self {
121 self.props.size = size.into();
122 self
123 }
124}
125
126pub enum FieldBuilder {
127 String(SharedString),
128 Element(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
129 View(AnyView),
130}
131
132impl Default for FieldBuilder {
133 fn default() -> Self {
134 Self::String(SharedString::default())
135 }
136}
137
138impl From<AnyView> for FieldBuilder {
139 fn from(view: AnyView) -> Self {
140 Self::View(view)
141 }
142}
143
144impl RenderOnce for FieldBuilder {
145 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
146 match self {
147 FieldBuilder::String(value) => value.into_any_element(),
148 FieldBuilder::Element(builder) => builder(window, cx),
149 FieldBuilder::View(view) => view.into_any(),
150 }
151 }
152}
153
154impl From<&'static str> for FieldBuilder {
155 fn from(value: &'static str) -> Self {
156 Self::String(value.into())
157 }
158}
159
160impl From<String> for FieldBuilder {
161 fn from(value: String) -> Self {
162 Self::String(value.into())
163 }
164}
165
166impl From<SharedString> for FieldBuilder {
167 fn from(value: SharedString) -> Self {
168 Self::String(value)
169 }
170}
171
172#[derive(IntoElement)]
173pub struct FormField {
174 id: ElementId,
175 form: Weak<Form>,
176 label: Option<FieldBuilder>,
177 no_label_indent: bool,
178 focus_handle: Option<FocusHandle>,
179 description: Option<FieldBuilder>,
180 child: Div,
182 visible: bool,
183 required: bool,
184 align_items: Option<AlignItems>,
186 props: FieldProps,
187 col_span: u16,
188 col_start: Option<i16>,
189 col_end: Option<i16>,
190}
191
192impl FormField {
193 pub fn new() -> Self {
194 Self {
195 id: 0.into(),
196 form: Weak::new(),
197 label: None,
198 description: None,
199 child: div(),
200 visible: true,
201 required: false,
202 no_label_indent: false,
203 focus_handle: None,
204 align_items: None,
205 props: FieldProps::default(),
206 col_span: 1,
207 col_start: None,
208 col_end: None,
209 }
210 }
211
212 pub fn label(mut self, label: impl Into<FieldBuilder>) -> Self {
214 self.label = Some(label.into());
215 self
216 }
217
218 pub fn no_label_indent(mut self) -> Self {
224 self.no_label_indent = true;
225 self
226 }
227
228 pub fn label_fn<F, E>(mut self, label: F) -> Self
230 where
231 E: IntoElement,
232 F: Fn(&mut Window, &mut App) -> E + 'static,
233 {
234 self.label = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
235 label(window, cx).into_any_element()
236 })));
237 self
238 }
239
240 pub fn description(mut self, description: impl Into<FieldBuilder>) -> Self {
242 self.description = Some(description.into());
243 self
244 }
245
246 pub fn description_fn<F, E>(mut self, description: F) -> Self
248 where
249 E: IntoElement,
250 F: Fn(&mut Window, &mut App) -> E + 'static,
251 {
252 self.description = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
253 description(window, cx).into_any_element()
254 })));
255 self
256 }
257
258 pub fn visible(mut self, visible: bool) -> Self {
260 self.visible = visible;
261 self
262 }
263
264 pub fn required(mut self, required: bool) -> Self {
266 self.required = required;
267 self
268 }
269
270 pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
274 self.focus_handle = Some(focus_handle.clone());
275 self
276 }
277
278 pub fn parent(mut self, form: &Rc<Form>) -> Self {
279 self.form = Rc::downgrade(form);
280 self
281 }
282
283 fn props(mut self, ix: usize, props: FieldProps) -> Self {
287 self.id = ix.into();
288 self.props = props;
289 self
290 }
291
292 pub fn items_start(mut self) -> Self {
294 self.align_items = Some(AlignItems::Start);
295 self
296 }
297
298 pub fn items_end(mut self) -> Self {
300 self.align_items = Some(AlignItems::End);
301 self
302 }
303
304 pub fn items_center(mut self) -> Self {
306 self.align_items = Some(AlignItems::Center);
307 self
308 }
309
310 pub fn col_span(mut self, col_span: u16) -> Self {
314 self.col_span = col_span;
315 self
316 }
317
318 pub fn col_start(mut self, col_start: i16) -> Self {
320 self.col_start = Some(col_start);
321 self
322 }
323
324 pub fn col_end(mut self, col_end: i16) -> Self {
326 self.col_end = Some(col_end);
327 self
328 }
329}
330impl ParentElement for FormField {
331 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
332 self.child.extend(elements);
333 }
334}
335
336impl RenderOnce for FormField {
337 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
338 let layout = self.props.layout;
339
340 let label_width = if layout.is_vertical() {
341 None
342 } else {
343 self.props.label_width
344 };
345 let has_label = !self.no_label_indent;
346
347 #[inline]
348 fn wrap_div(layout: Axis) -> Div {
349 if layout.is_vertical() {
350 v_flex()
351 } else {
352 h_flex()
353 }
354 }
355
356 #[inline]
357 fn wrap_label(label_width: Option<Pixels>) -> Div {
358 div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
359 }
360
361 let gap = match self.props.gap {
362 Some(v) => v,
363 None => match self.props.size {
364 Size::Large => px(8.),
365 Size::XSmall | Size::Small => px(4.),
366 _ => px(4.),
367 },
368 };
369 let inner_gap = if layout.is_horizontal() {
370 gap
371 } else {
372 gap / 2.
373 };
374
375 v_flex()
376 .flex_1()
377 .gap(gap / 2.)
378 .col_span(self.col_span)
379 .when_some(self.col_start, |this, start| this.col_start(start))
380 .when_some(self.col_end, |this, end| this.col_end(end))
381 .child(
382 wrap_div(layout)
384 .id(self.id)
385 .gap(inner_gap)
386 .when_some(self.align_items, |this, align| {
387 this.map(|this| match align {
388 AlignItems::Start => this.items_start(),
389 AlignItems::End => this.items_end(),
390 AlignItems::Center => this.items_center(),
391 AlignItems::Baseline => this.items_baseline(),
392 _ => this,
393 })
394 })
395 .when(has_label, |this| {
396 this.child(
398 wrap_label(label_width)
399 .text_sm()
400 .when_some(self.props.label_text_size, |this, size| {
401 this.text_size(size)
402 })
403 .font_medium()
404 .flex()
405 .flex_row()
406 .gap_1()
407 .items_center()
408 .when_some(self.label, |this, builder| {
409 this.child(
410 h_flex()
411 .gap_1()
412 .child(
413 div()
414 .overflow_x_hidden()
415 .child(builder.render(window, cx)),
416 )
417 .when(self.required, |this| {
418 this.child(
419 div().text_color(cx.theme().danger).child("*"),
420 )
421 }),
422 )
423 }),
424 )
425 })
426 .child(
427 div()
428 .w_full()
429 .flex_1()
430 .overflow_x_hidden()
431 .child(self.child),
432 ),
433 )
434 .child(
435 wrap_div(layout)
437 .gap(inner_gap)
438 .when(has_label && layout.is_horizontal(), |this| {
439 this.child(
440 wrap_label(label_width),
442 )
443 })
444 .when_some(self.description, |this, builder| {
445 this.child(
446 div()
447 .text_xs()
448 .text_color(cx.theme().muted_foreground)
449 .child(builder.render(window, cx)),
450 )
451 }),
452 )
453 }
454}
455impl RenderOnce for Form {
456 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
457 let props = self.props;
458
459 let gap = match props.size {
460 Size::XSmall | Size::Small => px(6.),
461 Size::Large => px(12.),
462 _ => px(8.),
463 };
464
465 v_flex()
466 .w_full()
467 .gap_x(gap * 3.)
468 .gap_y(gap)
469 .grid()
470 .grid_cols(props.column)
471 .children(
472 self.fields
473 .into_iter()
474 .enumerate()
475 .map(|(ix, field)| field.props(ix, props)),
476 )
477 }
478}