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    /// Returns whether the form field is visible.
171    pub(super) fn is_visible(&self) -> bool {
172        self.visible
173    }
174
175    /// Set the required status of the form field, default is `false`.
176    pub fn required(mut self, required: bool) -> Self {
177        self.required = required;
178        self
179    }
180
181    /// Set the properties for the form field.
182    ///
183    /// This is internal API for sync props from From.
184    pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self {
185        self.id = ix.into();
186        self.props = props;
187        self
188    }
189
190    /// Align the form field items to the start, this is the default.
191    pub fn items_start(mut self) -> Self {
192        self.align_items = Some(AlignItems::Start);
193        self
194    }
195
196    /// Align the form field items to the end.
197    pub fn items_end(mut self) -> Self {
198        self.align_items = Some(AlignItems::End);
199        self
200    }
201
202    /// Align the form field items to the center.
203    pub fn items_center(mut self) -> Self {
204        self.align_items = Some(AlignItems::Center);
205        self
206    }
207
208    /// Sets the column span for the form field.
209    ///
210    /// Default is 1.
211    pub fn col_span(mut self, col_span: u16) -> Self {
212        self.col_span = col_span;
213        self
214    }
215
216    /// Sets the column start of this form field.
217    pub fn col_start(mut self, col_start: i16) -> Self {
218        self.col_start = Some(col_start);
219        self
220    }
221
222    /// Sets the column end of this form field.
223    pub fn col_end(mut self, col_end: i16) -> Self {
224        self.col_end = Some(col_end);
225        self
226    }
227}
228
229impl ParentElement for Field {
230    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
231        self.children.extend(elements);
232    }
233}
234
235impl Styled for Field {
236    fn style(&mut self) -> &mut StyleRefinement {
237        &mut self.style
238    }
239}
240
241impl RenderOnce for Field {
242    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
243        let layout = self.props.layout;
244
245        let label_width = if layout.is_vertical() {
246            None
247        } else {
248            self.props.label_width
249        };
250        let has_label = self.label_indent;
251
252        #[inline]
253        fn wrap_div(layout: Axis) -> Div {
254            if layout.is_vertical() {
255                v_flex()
256            } else {
257                h_flex()
258            }
259        }
260
261        #[inline]
262        fn wrap_label(label_width: Option<Pixels>) -> Div {
263            div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
264        }
265
266        let gap = match self.props.size {
267            Size::Large => px(8.),
268            Size::XSmall | Size::Small => px(4.),
269            _ => px(4.),
270        };
271        let inner_gap = if layout.is_horizontal() {
272            gap
273        } else {
274            gap / 2.
275        };
276
277        v_flex()
278            .flex_1()
279            .gap(gap / 2.)
280            .col_span(self.col_span)
281            .when_some(self.col_start, |this, start| this.col_start(start))
282            .when_some(self.col_end, |this, end| this.col_end(end))
283            .refine_style(&self.style)
284            .when(!self.visible, |this| this.hidden())
285            .child(
286                // This warp for aligning the Label + Input
287                wrap_div(layout)
288                    .id(self.id)
289                    .gap(inner_gap)
290                    .when_some(self.align_items, |this, align| {
291                        this.map(|this| match align {
292                            AlignItems::Start => this.items_start(),
293                            AlignItems::End => this.items_end(),
294                            AlignItems::Center => this.items_center(),
295                            AlignItems::Baseline => this.items_baseline(),
296                            _ => this,
297                        })
298                    })
299                    .when(has_label, |this| {
300                        // Label
301                        this.child(
302                            wrap_label(label_width)
303                                .text_sm()
304                                .when_some(self.props.label_text_size, |this, size| {
305                                    this.text_size(size)
306                                })
307                                .font_medium()
308                                .gap_1()
309                                .items_center()
310                                .when_some(self.label, |this, builder| {
311                                    this.child(
312                                        h_flex()
313                                            .gap_1()
314                                            .child(
315                                                div()
316                                                    .overflow_x_hidden()
317                                                    .child(builder.render(window, cx)),
318                                            )
319                                            .when(self.required, |this| {
320                                                this.child(
321                                                    div().text_color(cx.theme().danger).child("*"),
322                                                )
323                                            }),
324                                    )
325                                }),
326                        )
327                    })
328                    .child(div().w_full().flex_1().children(self.children)),
329            )
330            .child(
331                // Other
332                wrap_div(layout)
333                    .gap(inner_gap)
334                    .when(has_label && layout.is_horizontal(), |this| {
335                        this.child(
336                            // Empty for spacing to align with the input
337                            wrap_label(label_width),
338                        )
339                    })
340                    .when_some(self.description, |this, builder| {
341                        this.child(
342                            div()
343                                .text_xs()
344                                .text_color(cx.theme().muted_foreground)
345                                .child(builder.render(window, cx)),
346                        )
347                    }),
348            )
349    }
350}