Skip to main content

gpui_component/form/
form.rs

1use gpui::{
2    AnyElement, App, Axis, IntoElement, ParentElement, Pixels, Rems, RenderOnce, StyleRefinement,
3    Styled, Window, prelude::FluentBuilder as _, px,
4};
5
6use crate::{
7    Sizable, Size,
8    form::{Field, FieldProps},
9    h_flex, v_flex,
10};
11
12/// A form element that contains multiple form fields.
13#[derive(IntoElement)]
14pub struct Form {
15    style: StyleRefinement,
16    fields: Vec<Field>,
17    footer: Option<AnyElement>,
18    props: FieldProps,
19}
20
21impl Default for Form {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl Form {
28    /// Creates a single-column form with labels above their controls.
29    pub fn new() -> Self {
30        Self {
31            style: StyleRefinement::default(),
32            props: FieldProps::default(),
33            fields: Vec::new(),
34            footer: None,
35        }
36    }
37
38    /// Creates a new form with labels beside their controls.
39    pub fn horizontal() -> Self {
40        Self::new().layout(Axis::Horizontal)
41    }
42
43    /// Creates a new form with labels above their controls.
44    pub fn vertical() -> Self {
45        Self::new().layout(Axis::Vertical)
46    }
47
48    /// Sets label/control orientation within each field, default is `Axis::Vertical`.
49    ///
50    /// This is an alias for [`Self::label_layout`]. Use [`Self::columns`] to arrange fields.
51    pub fn layout(self, layout: Axis) -> Self {
52        self.label_layout(layout)
53    }
54
55    /// Sets label/control orientation within each field.
56    ///
57    /// `Axis::Vertical` (default) places labels above controls; `Axis::Horizontal`
58    /// places labels beside controls. This does not change the field grid columns.
59    pub fn label_layout(mut self, layout: Axis) -> Self {
60        self.props.layout = layout;
61        self
62    }
63
64    /// Set the width of the labels in the form. Default is `px(140.)`.
65    pub fn label_width(mut self, width: Pixels) -> Self {
66        self.props.label_width = Some(width);
67        self
68    }
69
70    /// Set the text size of the labels in the form. Default is `None`.
71    pub fn label_text_size(mut self, size: Rems) -> Self {
72        self.props.label_text_size = Some(size);
73        self
74    }
75
76    /// Add a child to the form.
77    pub fn child(mut self, field: impl Into<Field>) -> Self {
78        self.fields.push(field.into());
79        self
80    }
81
82    /// Add multiple children to the form.
83    pub fn children(mut self, fields: impl IntoIterator<Item = Field>) -> Self {
84        self.fields.extend(fields);
85        self
86    }
87
88    /// Sets content in a full-width footer after all fields, aligned to the trailing edge.
89    ///
90    /// The caller owns action composition, state, and callbacks. Calling this again
91    /// replaces the footer. No footer row is rendered unless content is supplied.
92    pub fn footer(mut self, footer: impl IntoElement) -> Self {
93        self.footer = Some(footer.into_any_element());
94        self
95    }
96
97    /// Set the column count for the field grid, independently of label orientation.
98    ///
99    /// Default is 1.
100    pub fn columns(mut self, columns: usize) -> Self {
101        self.props.columns = columns;
102        self
103    }
104}
105
106impl Styled for Form {
107    fn style(&mut self) -> &mut StyleRefinement {
108        &mut self.style
109    }
110}
111
112impl Sizable for Form {
113    fn with_size(mut self, size: impl Into<Size>) -> Self {
114        self.props.size = size.into();
115        self
116    }
117}
118
119impl RenderOnce for Form {
120    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
121        let props = self.props;
122
123        let gap = match props.size {
124            Size::XSmall | Size::Small => px(6.),
125            Size::Large => px(12.),
126            _ => px(8.),
127        };
128
129        v_flex()
130            .w_full()
131            .gap_x(gap * 3.)
132            .gap_y(gap)
133            .grid()
134            .grid_cols(props.columns as u16)
135            .children(
136                self.fields
137                    .into_iter()
138                    .enumerate()
139                    .map(|(ix, field)| field.props(ix, props)),
140            )
141            .when_some(self.footer, |this, footer| {
142                this.child(
143                    h_flex()
144                        .col_span_full()
145                        .min_w_0()
146                        .justify_end()
147                        .child(footer),
148                )
149            })
150    }
151}
152
153#[cfg(test)]
154#[path = "tests.rs"]
155mod tests;