Skip to main content

ravenrs_widgets/
form_layout.rs

1//! Arranging the fields of a form: groups, columns, tabs and what is wrong.
2
3use std::fmt;
4use std::rc::Rc;
5
6use ravenrs_layout::{Align, Axis, LayoutStyle};
7use ravenrs_reactive::State;
8use ravenrs_style::{Semantic, TextStyle};
9
10use crate::context::{BuildCx, NodeId, Paint};
11use crate::{Alert, AnyView, Tabs, Tone, View, util};
12
13const FIELD_GAP: f32 = 12.0;
14const COLUMN_GAP: f32 = 16.0;
15const GROUP_GAP: f32 = 20.0;
16/// A field narrower than this moves to a line of its own, wherever the form is.
17const MIN_COLUMN_WIDTH: f32 = 180.0;
18
19enum Cell {
20    Field(AnyView),
21    Wide(AnyView),
22}
23
24/// A titled set of fields in a [`FormLayout`], filling its columns row by row.
25#[derive(Default)]
26#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
27pub struct FormGroup {
28    title: Option<String>,
29    columns: Option<usize>,
30    cells: Vec<Cell>,
31}
32
33impl fmt::Debug for FormGroup {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("FormGroup").field("title", &self.title).finish_non_exhaustive()
36    }
37}
38
39#[cfg_attr(feature = "live", ravenrs_live::expose)]
40impl FormGroup {
41    /// A group with no title.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// A group under this title.
47    pub fn titled(title: impl Into<String>) -> Self {
48        Self { title: Some(title.into()), ..Self::default() }
49    }
50
51    /// How many columns this group uses, instead of the form's.
52    pub fn columns(mut self, columns: usize) -> Self {
53        self.columns = Some(columns.max(1));
54        self
55    }
56
57    /// Appends a field, in the next free column.
58    pub fn field(mut self, field: impl View + 'static) -> Self {
59        self.cells.push(Cell::Field(Box::new(field)));
60        self
61    }
62
63    /// Appends a field on a row of its own, as wide as the group: a long text, a list.
64    pub fn wide(mut self, field: impl View + 'static) -> Self {
65        self.cells.push(Cell::Wide(Box::new(field)));
66        self
67    }
68}
69
70/// Lays out `cells` in rows of `columns`: the index of the first cell of each
71/// row, with how many it holds. A wide cell has a row of its own.
72fn form_rows(wide: &[bool], columns: usize) -> Vec<(usize, usize)> {
73    let mut rows = Vec::new();
74    let mut start = 0;
75    while start < wide.len() {
76        let mut count = 1;
77        if !wide[start] {
78            while count < columns && start + count < wide.len() && !wide[start + count] {
79                count += 1;
80            }
81        }
82        rows.push((start, count));
83        start += count;
84    }
85    rows
86}
87
88/// A view shown in more than one build, by a parent that takes ownership.
89struct Shared(Rc<AnyView>);
90
91impl View for Shared {
92    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
93        cx.child(self.0.as_ref().as_ref())
94    }
95}
96
97/// The body of a form: groups of fields in columns, tabs below them, and a list of
98/// what keeps it from being saved at the top. Columns collapse when the form is narrow.
99///
100/// ```
101/// use ravenrs_reactive::State;
102/// use ravenrs_widgets::{FormField, FormGroup, FormLayout, TextField, Ui};
103///
104/// let (name, city) = (State::new(String::new()), State::new(String::new()));
105/// let form = FormLayout::new().columns(2).group(
106///     FormGroup::titled("Contact")
107///         .field(FormField::new("Name", TextField::new(name.binding())))
108///         .field(FormField::new("City", TextField::new(city.binding()))),
109/// );
110/// let mut ui = Ui::new(form, ravenrs_style::Theme::raven_dark());
111/// ui.update();
112/// ```
113#[derive(Default)]
114#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
115pub struct FormLayout {
116    columns: Option<usize>,
117    errors: Vec<String>,
118    groups: Vec<FormGroup>,
119    tabs: Vec<(String, Rc<AnyView>)>,
120}
121
122impl fmt::Debug for FormLayout {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("FormLayout").field("groups", &self.groups).finish_non_exhaustive()
125    }
126}
127
128#[cfg_attr(feature = "live", ravenrs_live::expose)]
129impl FormLayout {
130    /// An empty form with one column.
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// How many columns the groups use, when the window has room for them.
136    pub fn columns(mut self, columns: usize) -> Self {
137        self.columns = Some(columns.max(1));
138        self
139    }
140
141    /// What keeps the form from being saved, listed at the top. Empty shows nothing.
142    pub fn errors(mut self, errors: impl IntoIterator<Item = impl Into<String>>) -> Self {
143        self.errors = errors.into_iter().map(Into::into).collect();
144        self
145    }
146
147    /// Appends a group of fields.
148    pub fn group(mut self, group: FormGroup) -> Self {
149        self.groups.push(group);
150        self
151    }
152
153    /// Appends a tab under the groups, for long parts such as lists of related records.
154    pub fn tab(mut self, title: impl Into<String>, content: impl View + 'static) -> Self {
155        self.tabs.push((title.into(), Rc::new(Box::new(content))));
156        self
157    }
158}
159
160impl View for FormLayout {
161    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
162        let theme = *cx.theme();
163        let mut parts = Vec::new();
164        if !self.errors.is_empty() {
165            let title = cx.labels().form_errors.clone();
166            parts.push(cx.child(&Alert::new(title, self.errors.join("\n")).tone(Tone::Danger)));
167        }
168        for (g, group) in self.groups.iter().enumerate() {
169            let mut lines = Vec::new();
170            if let Some(title) = &group.title {
171                lines.push(util::label(cx, title, TextStyle::Headline, None, theme.color(Semantic::Ink)));
172            }
173            let wide: Vec<bool> = group.cells.iter().map(|c| matches!(c, Cell::Wide(_))).collect();
174            let columns = group.columns.or(self.columns).unwrap_or(1);
175            for (start, count) in form_rows(&wide, columns) {
176                let mut cells = Vec::with_capacity(columns);
177                for (i, cell) in group.cells[start..start + count].iter().enumerate() {
178                    let (Cell::Field(view) | Cell::Wide(view)) = cell;
179                    let key = (g as u64) << 32 | (start + i) as u64;
180                    let node = cx.keyed_child(key, view.as_ref());
181                    cells.push(cx.container(
182                        LayoutStyle {
183                            grow: 1.0,
184                            width: Some(MIN_COLUMN_WIDTH),
185                            align: Align::Stretch,
186                            ..LayoutStyle::stack(Axis::Vertical)
187                        },
188                        &[node],
189                        Paint::None,
190                    ));
191                }
192                // Keeps a short last row's fields as wide as the rows above.
193                if !wide[start] {
194                    while cells.len() < columns {
195                        cells.push(cx.container(
196                            LayoutStyle { grow: 1.0, width: Some(MIN_COLUMN_WIDTH), ..LayoutStyle::leaf() },
197                            &[],
198                            Paint::None,
199                        ));
200                    }
201                }
202                lines.push(cx.container(
203                    LayoutStyle {
204                        spacing: COLUMN_GAP,
205                        align: Align::Start,
206                        align_self: Some(Align::Stretch),
207                        wrap: true,
208                        ..LayoutStyle::stack(Axis::Horizontal)
209                    },
210                    &cells,
211                    Paint::None,
212                ));
213            }
214            parts.push(cx.container(
215                LayoutStyle {
216                    spacing: FIELD_GAP,
217                    align: Align::Stretch,
218                    ..LayoutStyle::stack(Axis::Vertical)
219                },
220                &lines,
221                Paint::None,
222            ));
223        }
224        if !self.tabs.is_empty() {
225            let selected: State<usize> = cx.use_state(|| 0);
226            let mut tabs = Tabs::new(selected.binding());
227            for (title, content) in &self.tabs {
228                tabs = tabs.tab(title.clone(), Shared(content.clone()));
229            }
230            parts.push(cx.child(&tabs));
231        }
232        cx.container(
233            LayoutStyle {
234                spacing: GROUP_GAP,
235                align: Align::Stretch,
236                align_self: Some(Align::Stretch),
237                ..LayoutStyle::stack(Axis::Vertical)
238            },
239            &parts,
240            Paint::None,
241        )
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::form_rows;
248
249    #[test]
250    fn fields_fill_rows_and_wide_ones_take_their_own() {
251        assert_eq!(form_rows(&[], 2), []);
252        assert_eq!(form_rows(&[false, false, false], 2), [(0, 2), (2, 1)]);
253        assert_eq!(form_rows(&[false, true, false, false], 2), [(0, 1), (1, 1), (2, 2)]);
254        assert_eq!(form_rows(&[false, false], 1), [(0, 1), (1, 1)]);
255    }
256}