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