Skip to main content

gpui_component/form/
field.rs

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