gpui_component/form/
field.rs

1use std::rc::Rc;
2
3use gpui::{
4    div, prelude::FluentBuilder as _, px, AlignItems, AnyElement, AnyView, App, Axis, Div, Element,
5    ElementId, InteractiveElement as _, IntoElement, ParentElement, Pixels, Rems, RenderOnce,
6    SharedString, Styled, Window,
7};
8
9use crate::{h_flex, v_flex, ActiveTheme as _, AxisExt, Size, StyledExt};
10
11#[derive(Clone, Copy)]
12pub(super) struct FieldProps {
13    pub(super) size: Size,
14    pub(super) layout: Axis,
15    pub(super) columns: usize,
16
17    pub(super) label_width: Option<Pixels>,
18    pub(super) label_text_size: Option<Rems>,
19}
20
21impl Default for FieldProps {
22    fn default() -> Self {
23        Self {
24            layout: Axis::Vertical,
25            size: Size::default(),
26            columns: 1,
27            label_width: Some(px(140.)),
28            label_text_size: None,
29        }
30    }
31}
32
33pub enum FieldBuilder {
34    String(SharedString),
35    Element(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
36    View(AnyView),
37}
38
39impl Default for FieldBuilder {
40    fn default() -> Self {
41        Self::String(SharedString::default())
42    }
43}
44
45impl From<AnyView> for FieldBuilder {
46    fn from(view: AnyView) -> Self {
47        Self::View(view)
48    }
49}
50
51impl RenderOnce for FieldBuilder {
52    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
53        match self {
54            FieldBuilder::String(value) => value.into_any_element(),
55            FieldBuilder::Element(builder) => builder(window, cx),
56            FieldBuilder::View(view) => view.into_any(),
57        }
58    }
59}
60
61impl From<&'static str> for FieldBuilder {
62    fn from(value: &'static str) -> Self {
63        Self::String(value.into())
64    }
65}
66
67impl From<String> for FieldBuilder {
68    fn from(value: String) -> Self {
69        Self::String(value.into())
70    }
71}
72
73impl From<SharedString> for FieldBuilder {
74    fn from(value: SharedString) -> Self {
75        Self::String(value)
76    }
77}
78
79/// Form field element.
80#[derive(IntoElement)]
81pub struct Field {
82    id: ElementId,
83    props: FieldProps,
84    label: Option<FieldBuilder>,
85    label_indent: bool,
86    description: Option<FieldBuilder>,
87    /// Used to render the actual form field, e.g.: Input, Switch...
88    children: Vec<AnyElement>,
89    visible: bool,
90    required: bool,
91    /// Alignment of the form field.
92    align_items: Option<AlignItems>,
93    col_span: u16,
94    col_start: Option<i16>,
95    col_end: Option<i16>,
96}
97
98impl Field {
99    pub fn new() -> Self {
100        Self {
101            id: 0.into(),
102            label: None,
103            description: None,
104            children: Vec::new(),
105            visible: true,
106            required: false,
107            label_indent: true,
108            align_items: None,
109            props: FieldProps::default(),
110            col_span: 1,
111            col_start: None,
112            col_end: None,
113        }
114    }
115
116    /// Sets the label for the form field.
117    pub fn label(mut self, label: impl Into<FieldBuilder>) -> Self {
118        self.label = Some(label.into());
119        self
120    }
121
122    /// Sets indent with the label width (in Horizontal layout), default is `true`.
123    ///
124    /// Sometimes you want to align the input form left (Default is align after the label width in Horizontal layout).
125    ///
126    /// This is only work when the `label` is not set.
127    pub fn label_indent(mut self, indent: bool) -> Self {
128        self.label_indent = indent;
129        self
130    }
131
132    /// Sets the label for the form field using a function.
133    pub fn label_fn<F, E>(mut self, label: F) -> Self
134    where
135        E: IntoElement,
136        F: Fn(&mut Window, &mut App) -> E + 'static,
137    {
138        self.label = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
139            label(window, cx).into_any_element()
140        })));
141        self
142    }
143
144    /// Sets the description for the form field.
145    pub fn description(mut self, description: impl Into<FieldBuilder>) -> Self {
146        self.description = Some(description.into());
147        self
148    }
149
150    /// Sets the description for the form field using a function.
151    pub fn description_fn<F, E>(mut self, description: F) -> Self
152    where
153        E: IntoElement,
154        F: Fn(&mut Window, &mut App) -> E + 'static,
155    {
156        self.description = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
157            description(window, cx).into_any_element()
158        })));
159        self
160    }
161
162    /// Set the visibility of the form field, default is `true`.
163    pub fn visible(mut self, visible: bool) -> Self {
164        self.visible = visible;
165        self
166    }
167
168    /// Set the required status of the form field, default is `false`.
169    pub fn required(mut self, required: bool) -> Self {
170        self.required = required;
171        self
172    }
173
174    /// Set the properties for the form field.
175    ///
176    /// This is internal API for sync props from From.
177    pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self {
178        self.id = ix.into();
179        self.props = props;
180        self
181    }
182
183    /// Align the form field items to the start, this is the default.
184    pub fn items_start(mut self) -> Self {
185        self.align_items = Some(AlignItems::Start);
186        self
187    }
188
189    /// Align the form field items to the end.
190    pub fn items_end(mut self) -> Self {
191        self.align_items = Some(AlignItems::End);
192        self
193    }
194
195    /// Align the form field items to the center.
196    pub fn items_center(mut self) -> Self {
197        self.align_items = Some(AlignItems::Center);
198        self
199    }
200
201    /// Sets the column span for the form field.
202    ///
203    /// Default is 1.
204    pub fn col_span(mut self, col_span: u16) -> Self {
205        self.col_span = col_span;
206        self
207    }
208
209    /// Sets the column start of this form field.
210    pub fn col_start(mut self, col_start: i16) -> Self {
211        self.col_start = Some(col_start);
212        self
213    }
214
215    /// Sets the column end of this form field.
216    pub fn col_end(mut self, col_end: i16) -> Self {
217        self.col_end = Some(col_end);
218        self
219    }
220}
221
222impl ParentElement for Field {
223    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
224        self.children.extend(elements);
225    }
226}
227
228impl RenderOnce for Field {
229    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
230        let layout = self.props.layout;
231
232        let label_width = if layout.is_vertical() {
233            None
234        } else {
235            self.props.label_width
236        };
237        let has_label = self.label_indent;
238
239        #[inline]
240        fn wrap_div(layout: Axis) -> Div {
241            if layout.is_vertical() {
242                v_flex()
243            } else {
244                h_flex()
245            }
246        }
247
248        #[inline]
249        fn wrap_label(label_width: Option<Pixels>) -> Div {
250            div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
251        }
252
253        let gap = match self.props.size {
254            Size::Large => px(8.),
255            Size::XSmall | Size::Small => px(4.),
256            _ => px(4.),
257        };
258        let inner_gap = if layout.is_horizontal() {
259            gap
260        } else {
261            gap / 2.
262        };
263
264        v_flex()
265            .flex_1()
266            .gap(gap / 2.)
267            .col_span(self.col_span)
268            .when_some(self.col_start, |this, start| this.col_start(start))
269            .when_some(self.col_end, |this, end| this.col_end(end))
270            .child(
271                // This warp for aligning the Label + Input
272                wrap_div(layout)
273                    .id(self.id)
274                    .gap(inner_gap)
275                    .when_some(self.align_items, |this, align| {
276                        this.map(|this| match align {
277                            AlignItems::Start => this.items_start(),
278                            AlignItems::End => this.items_end(),
279                            AlignItems::Center => this.items_center(),
280                            AlignItems::Baseline => this.items_baseline(),
281                            _ => this,
282                        })
283                    })
284                    .when(has_label, |this| {
285                        // Label
286                        this.child(
287                            wrap_label(label_width)
288                                .text_sm()
289                                .when_some(self.props.label_text_size, |this, size| {
290                                    this.text_size(size)
291                                })
292                                .font_medium()
293                                .gap_1()
294                                .items_center()
295                                .when_some(self.label, |this, builder| {
296                                    this.child(
297                                        h_flex()
298                                            .gap_1()
299                                            .child(
300                                                div()
301                                                    .overflow_x_hidden()
302                                                    .child(builder.render(window, cx)),
303                                            )
304                                            .when(self.required, |this| {
305                                                this.child(
306                                                    div().text_color(cx.theme().danger).child("*"),
307                                                )
308                                            }),
309                                    )
310                                }),
311                        )
312                    })
313                    .child(
314                        div()
315                            .w_full()
316                            .flex_1()
317                            .overflow_x_hidden()
318                            .children(self.children),
319                    ),
320            )
321            .child(
322                // Other
323                wrap_div(layout)
324                    .gap(inner_gap)
325                    .when(has_label && layout.is_horizontal(), |this| {
326                        this.child(
327                            // Empty for spacing to align with the input
328                            wrap_label(label_width),
329                        )
330                    })
331                    .when_some(self.description, |this, builder| {
332                        this.child(
333                            div()
334                                .text_xs()
335                                .text_color(cx.theme().muted_foreground)
336                                .child(builder.render(window, cx)),
337                        )
338                    }),
339            )
340    }
341}