1use 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;
16const MIN_COLUMN_WIDTH: f32 = 180.0;
18
19enum Cell {
20 Field(AnyView),
21 Wide(AnyView),
22}
23
24#[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 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn titled(title: impl Into<String>) -> Self {
48 Self { title: Some(title.into()), ..Self::default() }
49 }
50
51 pub fn columns(mut self, columns: usize) -> Self {
53 self.columns = Some(columns.max(1));
54 self
55 }
56
57 pub fn field(mut self, field: impl View + 'static) -> Self {
59 self.cells.push(Cell::Field(Box::new(field)));
60 self
61 }
62
63 pub fn wide(mut self, field: impl View + 'static) -> Self {
65 self.cells.push(Cell::Wide(Box::new(field)));
66 self
67 }
68}
69
70fn 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
88struct 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#[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 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn columns(mut self, columns: usize) -> Self {
137 self.columns = Some(columns.max(1));
138 self
139 }
140
141 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 pub fn group(mut self, group: FormGroup) -> Self {
149 self.groups.push(group);
150 self
151 }
152
153 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 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}