gpui_component/
form.rs

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
11/// Create a new form with a vertical layout.
12pub fn v_form() -> Form {
13    Form::vertical()
14}
15
16/// Create a new form with a horizontal layout.
17pub fn h_form() -> Form {
18    Form::horizontal()
19}
20
21/// Create a new form field.
22pub fn form_field() -> FormField {
23    FormField::new()
24}
25
26/// A form element that contains multiple form fields.
27#[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    /// Field gap
40    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    /// Creates a new form with a horizontal layout.
66    pub fn horizontal() -> Self {
67        Self::new().layout(Axis::Horizontal)
68    }
69
70    /// Creates a new form with a vertical layout.
71    pub fn vertical() -> Self {
72        Self::new().layout(Axis::Vertical)
73    }
74
75    /// Set the layout for the form, default is `Axis::Vertical`.
76    pub fn layout(mut self, layout: Axis) -> Self {
77        self.props.layout = layout;
78        self
79    }
80
81    /// Set the width of the labels in the form. Default is `px(100.)`.
82    pub fn label_width(mut self, width: Pixels) -> Self {
83        self.props.label_width = Some(width);
84        self
85    }
86
87    /// Set the text size of the labels in the form. Default is `None`.
88    pub fn label_text_size(mut self, size: Rems) -> Self {
89        self.props.label_text_size = Some(size);
90        self
91    }
92
93    /// Set the gap between the form fields.
94    pub fn gap(mut self, gap: Pixels) -> Self {
95        self.props.gap = Some(gap);
96        self
97    }
98
99    /// Add a child to the form.
100    pub fn child(mut self, field: impl Into<FormField>) -> Self {
101        self.fields.push(field.into());
102        self
103    }
104
105    /// Add multiple children to the form.
106    pub fn children(mut self, fields: impl IntoIterator<Item = FormField>) -> Self {
107        self.fields.extend(fields);
108        self
109    }
110
111    /// Set the column count for the form.
112    ///
113    /// Default is 1.
114    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    /// Used to render the actual form field, e.g.: Input, Switch...
182    child: Div,
183    visible: bool,
184    required: bool,
185    /// Alignment of the form field.
186    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    /// Sets the label for the form field.
214    pub fn label(mut self, label: impl Into<FieldBuilder>) -> Self {
215        self.label = Some(label.into());
216        self
217    }
218
219    /// Sets not indent with the label width (in Horizontal layout).
220    ///
221    /// Sometimes you want to align the input form left (Default is align after the label width in Horizontal layout).
222    ///
223    /// This is only work when the `label` is not set.
224    pub fn no_label_indent(mut self) -> Self {
225        self.no_label_indent = true;
226        self
227    }
228
229    /// Sets the label for the form field using a function.
230    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    /// Sets the description for the form field.
242    pub fn description(mut self, description: impl Into<FieldBuilder>) -> Self {
243        self.description = Some(description.into());
244        self
245    }
246
247    /// Sets the description for the form field using a function.
248    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    /// Set the visibility of the form field, default is `true`.
260    pub fn visible(mut self, visible: bool) -> Self {
261        self.visible = visible;
262        self
263    }
264
265    /// Set the required status of the form field, default is `false`.
266    pub fn required(mut self, required: bool) -> Self {
267        self.required = required;
268        self
269    }
270
271    /// Set the focus handle for the form field.
272    ///
273    /// If not set, the form field will not be focusable.
274    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    /// Set the properties for the form field.
285    ///
286    /// This is internal API for sync props from From.
287    fn props(mut self, ix: usize, props: FieldProps) -> Self {
288        self.id = ix.into();
289        self.props = props;
290        self
291    }
292
293    /// Align the form field items to the start, this is the default.
294    pub fn items_start(mut self) -> Self {
295        self.align_items = Some(AlignItems::Start);
296        self
297    }
298
299    /// Align the form field items to the end.
300    pub fn items_end(mut self) -> Self {
301        self.align_items = Some(AlignItems::End);
302        self
303    }
304
305    /// Align the form field items to the center.
306    pub fn items_center(mut self) -> Self {
307        self.align_items = Some(AlignItems::Center);
308        self
309    }
310
311    /// Sets the column span for the form field.
312    ///
313    /// Default is 1.
314    pub fn col_span(mut self, col_span: u16) -> Self {
315        self.col_span = col_span;
316        self
317    }
318
319    /// Sets the column start of this form field.
320    pub fn col_start(mut self, col_start: i16) -> Self {
321        self.col_start = Some(col_start);
322        self
323    }
324
325    /// Sets the column end of this form field.
326    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                // This warp for aligning the Label + Input
384                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                        // Label
398                        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                // Other
437                wrap_div(layout)
438                    .gap(inner_gap)
439                    .when(has_label && layout.is_horizontal(), |this| {
440                        this.child(
441                            // Empty for spacing to align with the input
442                            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}