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