git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/layout/form.rs (19.9K)

  1 //! A section's contents as a box-model tree: declared first, solved by `scene::layout`, then
  2 //! painted where the solver put each piece.
  3 //!
  4 //! [`SectionContext`](super::SectionContext) frames a section (its title tab, its well) and
  5 //! used to place what goes in it with a cursor: every row was handed its y and width, and a
  6 //! page added insets of its own on top. A [`Form`] replaces the cursor. A page asks the section
  7 //! for one ([`SectionContext::form`]), declares its contents into it — retained widgets, text
  8 //! lines, rows of cells, blocks it paints itself — and hands it back
  9 //! ([`SectionContext::place`]), which lays the tree out across the section's content box,
 10 //! paints each piece in the order it was declared, and moves the section past it.
 11 //!
 12 //! The spacing is the ladder's, never the page's: a form is a [`Style::controls_column`],
 13 //! a row a [`Style::controls_row`], both spaced by `control_gap`. A page states sizes only
 14 //! where a piece has a size of its own (a list's height, a button's width).
 15 
 16 use crate::scene::arena::{Arena, NodeId};
 17 use crate::scene::layout::{arrange, measure, CrossAlign, LayoutBox, Length, Rect, Size, Style};
 18 use crate::widget::{UiContext, WidgetHost, WidgetHostExt};
 19 
 20 use super::{render_widget, RenderTarget};
 21 
 22 /// What a piece paints once it is placed: the target, its rect, the widget context.
 23 pub type Draw<'w, P> = Box<dyn FnOnce(&mut P, Rect, &mut UiContext) + 'w>;
 24 
 25 /// The tree a page declares a section's contents into. See the module docs.
 26 pub struct Form<'w, P> {
 27     arena: Arena<LayoutBox>,
 28     root: NodeId,
 29     /// Where the content box starts and how wide it is.
 30     x: f32,
 31     y: f32,
 32     width: f32,
 33     /// A height to fill: set by [`Form::fill_height`], so a growing piece (a list that takes
 34     /// the rest of the page) has room to grow into.
 35     height: Option<f32>,
 36     /// The row a widget lights when hovered or focused: the section's span, not the cell's.
 37     row_span: (f32, f32),
 38     pieces: Vec<(NodeId, Draw<'w, P>)>,
 39 }
 40 
 41 impl<'w, P: RenderTarget + 'w> Form<'w, P> {
 42     /// A form across a content box at `(x, y)`, `width` wide; widgets light `row_span`.
 43     pub fn new(x: f32, y: f32, width: f32, row_span: (f32, f32)) -> Self {
 44         let mut arena = Arena::new();
 45         let root = arena.insert(LayoutBox::container(
 46             Style::controls_column().cross_align(CrossAlign::Stretch).width(Length::Fixed(width)),
 47         ));
 48         Form { arena, root, x, y, width, height: None, row_span, pieces: Vec::new() }
 49     }
 50 
 51     /// The content box's width — what a page wraps text to.
 52     pub fn width(&self) -> f32 {
 53         self.width
 54     }
 55 
 56     /// Where the form starts.
 57     pub fn top(&self) -> f32 {
 58         self.y
 59     }
 60 
 61     /// Give the form this height to fill, so a piece declared with
 62     /// [`Group::fill`] takes what the rest leave of it.
 63     pub fn fill_height(&mut self, height: f32) {
 64         self.height = Some(height.max(0.0));
 65     }
 66 
 67     /// The form's top-level column.
 68     pub fn column(&mut self) -> Group<'_, 'w, P> {
 69         let node = self.root;
 70         Group { form: self, node, axis_row: false }
 71     }
 72 
 73     /// Lay the tree out and paint every piece in the order it was declared. Returns the
 74     /// bottom of what was placed.
 75     pub fn paint(mut self, pc: &mut P, ctx: &mut UiContext) -> f32 {
 76         let measured = measure(&mut self.arena, self.root);
 77         let height = self.height.unwrap_or(measured.height);
 78         arrange(&mut self.arena, self.root, Rect { x: self.x, y: self.y, width: self.width, height });
 79         for (node, draw) in self.pieces {
 80             let rect = self.arena.value(node).map(|b| b.rect).unwrap_or(Rect::ZERO);
 81             draw(pc, rect, ctx);
 82         }
 83         self.y + height
 84     }
 85 
 86     fn add(&mut self, parent: NodeId, style: Style, size: Size, draw: Option<Draw<'w, P>>) -> NodeId {
 87         let node = self.arena.insert(LayoutBox::leaf(style, size));
 88         self.arena.append_child(parent, node);
 89         if let Some(draw) = draw {
 90             self.pieces.push((node, draw));
 91         }
 92         node
 93     }
 94 }
 95 
 96 /// A column or a row of a [`Form`], what pieces are declared into.
 97 pub struct Group<'f, 'w, P> {
 98     form: &'f mut Form<'w, P>,
 99     node: NodeId,
100     axis_row: bool,
101 }
102 
103 impl<'f, 'w, P: RenderTarget + 'w> Group<'f, 'w, P> {
104     /// The leaf style for a piece of this group: across a column it stretches to the width;
105     /// along a row it takes its own width and grows only when asked.
106     fn leaf_style(&self, grow: bool) -> Style {
107         if grow { Style::default().grow(1.0) } else { Style::default() }
108     }
109 
110     /// The span a widget lights when hovered or focused: down a column, the section's row;
111     /// in a row, its own cell (`None`).
112     fn row_span(&self) -> Option<(f32, f32)> {
113         if self.axis_row { None } else { Some(self.form.row_span) }
114     }
115 
116     /// A retained widget at its own height (its preferred height, or `fallback`, plus its
117     /// detached label). In a row it shares the row's width with the other growing cells.
118     pub fn widget<T: WidgetHost + 'static>(&mut self, w: &'w mut T, fallback: f32) -> &mut Self {
119         let h = w.preferred_height().unwrap_or(fallback) + w.label_strip();
120         let style = self.leaf_style(self.axis_row);
121         let span = self.row_span();
122         let draw: Draw<'w, P> = Box::new(move |pc, r, ctx| {
123             let (x, width) = span.unwrap_or((r.x, r.width));
124             w.set_row_rect(x, width);
125             render_widget(pc, w, r.x, r.y, r.width, r.height, ctx);
126         });
127         self.form.add(self.node, style, Size::new(0.0, h), Some(draw));
128         self
129     }
130 
131     /// [`widget`](Self::widget) for a widget the context owns, named by its handle: its height
132     /// read from `ctx` now, and the widget lent for its placement and paint. A handle that no
133     /// longer names a widget takes `fallback` and draws nothing.
134     pub fn widget_h<T: WidgetHost + 'static>(&mut self, ctx: &UiContext, h: crate::widget::Handle<T>, fallback: f32) -> &mut Self {
135         let h_px = ctx.get(h).map_or(fallback, |w| w.preferred_height().unwrap_or(fallback) + w.label_strip());
136         let style = self.leaf_style(self.axis_row);
137         let span = self.row_span();
138         let draw: Draw<'w, P> = Box::new(move |pc, r, ctx| {
139             ctx.lend_h(h, |w, ctx| {
140                 let (x, width) = span.unwrap_or((r.x, r.width));
141                 w.set_row_rect(x, width);
142                 render_widget(pc, w, r.x, r.y, r.width, r.height, ctx);
143             });
144         });
145         self.form.add(self.node, style, Size::new(0.0, h_px), Some(draw));
146         self
147     }
148 
149     /// A retained widget at a width of its own (a toggle that is not as wide as its row), not
150     /// growing. Its height is as for [`widget`](Self::widget).
151     pub fn widget_w<T: WidgetHost + 'static>(&mut self, w: &'w mut T, width: f32, fallback: f32) -> &mut Self {
152         let h = w.preferred_height().unwrap_or(fallback) + w.label_strip();
153         let span = self.row_span();
154         let draw: Draw<'w, P> = Box::new(move |pc, r, ctx| {
155             let (x, width) = span.unwrap_or((r.x, r.width));
156             w.set_row_rect(x, width);
157             render_widget(pc, w, r.x, r.y, r.width, r.height, ctx);
158         });
159         self.form.add(self.node, Style::default(), Size::new(width, h), Some(draw));
160         self
161     }
162 
163     /// [`widget_w`](Self::widget_w) for a widget the context owns, as
164     /// [`widget_h`](Self::widget_h) is [`widget`](Self::widget)'s.
165     pub fn widget_w_h<T: WidgetHost + 'static>(&mut self, ctx: &UiContext, h: crate::widget::Handle<T>, width: f32, fallback: f32) -> &mut Self {
166         let h_px = ctx.get(h).map_or(fallback, |w| w.preferred_height().unwrap_or(fallback) + w.label_strip());
167         let span = self.row_span();
168         let draw: Draw<'w, P> = Box::new(move |pc, r, ctx| {
169             ctx.lend_h(h, |w, ctx| {
170                 let (x, width) = span.unwrap_or((r.x, r.width));
171                 w.set_row_rect(x, width);
172                 render_widget(pc, w, r.x, r.y, r.width, r.height, ctx);
173             });
174         });
175         self.form.add(self.node, Style::default(), Size::new(width, h_px), Some(draw));
176         self
177     }
178 
179     /// A piece the page paints itself, `w` × `h` (in a column the width is the column's;
180     /// in a row `w` is the cell's width, and it grows into the row's slack when `grow`).
181     pub fn draw(&mut self, w: f32, h: f32, grow: bool, draw: impl FnOnce(&mut P, Rect, &mut UiContext) + 'w) -> &mut Self {
182         let style = self.leaf_style(grow);
183         self.form.add(self.node, style, Size::new(w, h), Some(Box::new(draw)));
184         self
185     }
186 
187     /// A piece that takes the height the rest of the form leaves (see
188     /// [`Form::fill_height`]), at least `min_h`.
189     pub fn fill(&mut self, min_h: f32, draw: impl FnOnce(&mut P, Rect, &mut UiContext) + 'w) -> &mut Self {
190         let style = Style { min_height: min_h, ..Style::default() }.grow(1.0);
191         self.form.add(self.node, style, Size::new(0.0, min_h), Some(Box::new(draw)));
192         self
193     }
194 
195     /// A line of text in the default face, as tall as a section's text line.
196     pub fn text(&mut self, text: impl Into<String>, size: f32, color: [f32; 4]) -> &mut Self {
197         let text = text.into();
198         let w = text_width(&text, size);
199         self.draw(w, line_height(size), false, move |pc, r, _| {
200             pc.text_with_bounds(&text, r.x, r.y, size, color, Some([r.x, r.y - size, r.x + r.width, r.y + 2.0 * size]));
201         })
202     }
203 
204     /// A line of text that takes the slack of its row (or the width of its column) and is cut
205     /// at the edge of what it was given — a value beside a label that may run long.
206     pub fn text_fill(&mut self, text: impl Into<String>, size: f32, color: [f32; 4]) -> &mut Self {
207         let text = text.into();
208         self.draw(0.0, line_height(size), true, move |pc, r, _| {
209             pc.text_with_bounds(&text, r.x, r.y, size, color, Some([r.x, r.y - size, r.x + r.width, r.y + 2.0 * size]));
210         })
211     }
212 
213     /// Lines of text, one under the next, as one piece.
214     pub fn lines(&mut self, lines: Vec<String>, size: f32, color: [f32; 4]) -> &mut Self {
215         let w = lines.iter().map(|l| text_width(l, size)).fold(0.0, f32::max);
216         let h = line_height(size) * lines.len() as f32;
217         self.draw(w, h, false, move |pc, r, _| {
218             for (i, line) in lines.iter().enumerate() {
219                 let y = r.y + i as f32 * line_height(size);
220                 pc.text_with_bounds(line, r.x, y, size, color, Some([r.x, y - size, r.x + r.width, y + 2.0 * size]));
221             }
222         })
223     }
224 
225     /// A one-pixel rule across the group, parting its zones.
226     pub fn rule(&mut self, color: [f32; 4]) -> &mut Self {
227         self.draw(0.0, 1.0, false, move |pc, r, _| pc.rect(color, r.x, r.y, r.width, 1.0))
228     }
229 
230     /// Empty room of the given extent (along a row, a width; down a column, a height),
231     /// growing into the slack when `grow` — what pushes a row's later cells to its end.
232     pub fn space(&mut self, extent: f32, grow: bool) -> &mut Self {
233         let size = if self.axis_row { Size::new(extent, 0.0) } else { Size::new(0.0, extent) };
234         let style = self.leaf_style(grow);
235         self.form.add(self.node, style, size, None);
236         self
237     }
238 
239     /// A row of cells, the control gap between them, centred on each other.
240     pub fn row(&mut self, build: impl FnOnce(&mut Group<'_, 'w, P>)) -> &mut Self {
241         self.group(Style::controls_row().cross_align(CrossAlign::Center), true, build)
242     }
243 
244     /// Lines of text one under the next with no gap between them, stretched across: a block
245     /// of text, not a column of controls.
246     pub fn block(&mut self, build: impl FnOnce(&mut Group<'_, 'w, P>)) -> &mut Self {
247         self.group(Style::column().cross_align(CrossAlign::Stretch), false, build)
248     }
249 
250     /// A column of pieces, the control gap between them, stretched across.
251     pub fn column(&mut self, build: impl FnOnce(&mut Group<'_, 'w, P>)) -> &mut Self {
252         self.group(Style::controls_column().cross_align(CrossAlign::Stretch), false, build)
253     }
254 
255     /// A nested group with a style of its own (a grid, a tighter gap).
256     pub fn group(&mut self, style: Style, row: bool, build: impl FnOnce(&mut Group<'_, 'w, P>)) -> &mut Self {
257         let node = self.form.arena.insert(LayoutBox::container(style));
258         self.form.arena.append_child(self.node, node);
259         let mut g = Group { form: self.form, node, axis_row: row };
260         build(&mut g);
261         self
262     }
263 
264     /// The width of the form this group is in.
265     pub fn form_width(&self) -> f32 {
266         self.form.width
267     }
268 }
269 
270 /// One cell of [`lay_row`]: its width (its own, or the least it takes when it grows), its
271 /// height, and whether it grows into the row's slack.
272 #[derive(Debug, Clone, Copy)]
273 pub struct Cell {
274     pub width: f32,
275     pub height: f32,
276     pub grow: bool,
277 }
278 
279 impl Cell {
280     /// A cell of its own width.
281     pub fn fixed(width: f32, height: f32) -> Self {
282         Cell { width, height, grow: false }
283     }
284     /// A cell that takes the row's slack.
285     pub fn grow(height: f32) -> Self {
286         Cell { width: 0.0, height, grow: true }
287     }
288 }
289 
290 /// A list row's cells laid across `rect` by `scene::layout`: `list_gap` in from the row's
291 /// ends and between cells, each centred on the row's height. For what a scrolling list draws
292 /// per visible row — its buttons, its glyph, its name — where a `Form` (one tree for a whole
293 /// section) does not reach.
294 pub fn lay_row(rect: Rect, cells: &[Cell]) -> Vec<Rect> {
295     let gap = crate::layout::list_gap();
296     let mut arena = Arena::new();
297     let style = Style::row().gap(gap).cross_align(CrossAlign::Center).width(Length::Fixed(rect.width)).height(Length::Fixed(rect.height));
298     let style = Style { padding: crate::scene::layout::Edges { left: gap, right: gap, top: 0.0, bottom: 0.0 }, ..style };
299     let row = arena.insert(LayoutBox::container(style));
300     let ids: Vec<NodeId> = cells
301         .iter()
302         .map(|c| {
303             let st = if c.grow { Style::default().grow(1.0) } else { Style::default() };
304             let id = arena.insert(LayoutBox::leaf(st, Size::new(c.width, c.height)));
305             arena.append_child(row, id);
306             id
307         })
308         .collect();
309     measure(&mut arena, row);
310     arrange(&mut arena, row, rect);
311     ids.into_iter().map(|id| arena.value(id).map(|b| b.rect).unwrap_or(Rect::ZERO)).collect()
312 }
313 
314 /// How tall a section's text line is at `size`: the size and the 4 px a line has always had
315 /// below it.
316 pub fn line_height(size: f32) -> f32 {
317     size + 4.0
318 }
319 
320 /// The width `text` takes at `size` in the default face, as the renderer shapes it.
321 pub fn text_width(text: &str, size: f32) -> f32 {
322     crate::geometry_font_system()
323         .lock()
324         .ok()
325         .and_then(|mut fs| crate::backend::text::shaped_cluster_offsets(&mut fs, text, size, None).last().map(|&(_, total)| total))
326         .unwrap_or(0.0)
327 }
328 
329 #[cfg(test)]
330 mod tests {
331     use super::*;
332     use std::cell::RefCell;
333     use std::rc::Rc;
334 
335     /// A target that keeps the bounds every text was given.
336     #[derive(Default)]
337     struct Texts {
338         bounds: Vec<(String, Option<[f32; 4]>)>,
339     }
340     impl RenderTarget for Texts {
341         fn rect(&mut self, _: [f32; 4], _: f32, _: f32, _: f32, _: f32) {}
342         fn text(&mut self, content: &str, _: f32, _: f32, _: f32, _: [f32; 4]) {
343             self.bounds.push((content.to_string(), None));
344         }
345         fn text_with_bounds(&mut self, content: &str, _: f32, _: f32, _: f32, _: [f32; 4], bounds: Option<[f32; 4]>) {
346             self.bounds.push((content.to_string(), bounds));
347         }
348     }
349 
350     type Seen = Rc<RefCell<Vec<Rect>>>;
351 
352     /// A piece of the given size that records where it was put.
353     fn piece<'w>(g: &mut Group<'_, 'w, Texts>, seen: &Seen, w: f32, h: f32, grow: bool) {
354         let seen = seen.clone();
355         g.draw(w, h, grow, move |_, r, _| seen.borrow_mut().push(r));
356     }
357 
358     /// A form spans its content box, and its pieces stand one control gap apart, in the
359     /// order they were declared; the section moves past the last.
360     #[test]
361     fn a_form_stacks_its_pieces_a_control_gap_apart_across_the_box() {
362         let gap = crate::layout::control_gap();
363         let seen: Seen = Rc::default();
364         let mut form: Form<'_, Texts> = Form::new(10.0, 20.0, 300.0, (0.0, 320.0));
365         {
366             let mut col = form.column();
367             piece(&mut col, &seen, 0.0, 30.0, false);
368             piece(&mut col, &seen, 0.0, 12.0, false);
369         }
370         let bottom = form.paint(&mut Texts::default(), &mut UiContext::new());
371         let r = seen.borrow();
372         assert_eq!((r[0].x, r[0].y, r[0].width, r[0].height), (10.0, 20.0, 300.0, 30.0));
373         assert_eq!((r[1].x, r[1].y, r[1].width), (10.0, 20.0 + 30.0 + gap, 300.0));
374         assert_eq!(bottom, 20.0 + 30.0 + gap + 12.0);
375     }
376 
377     /// Cells that grow share a row's slack equally on top of what each asked for — a row of
378     /// buttons sized to their labels — and the row is the form's width, gaps included.
379     #[test]
380     fn a_rows_growing_cells_share_its_slack() {
381         let gap = crate::layout::control_gap();
382         let seen: Seen = Rc::default();
383         let mut form: Form<'_, Texts> = Form::new(0.0, 0.0, 400.0, (0.0, 400.0));
384         form.column().row(|r| {
385             piece(r, &seen, 50.0, 20.0, true);
386             piece(r, &seen, 100.0, 20.0, true);
387         });
388         form.paint(&mut Texts::default(), &mut UiContext::new());
389         let r = seen.borrow();
390         let extra = (400.0 - gap - 150.0) / 2.0;
391         assert!((r[0].width - (50.0 + extra)).abs() < 1e-3, "{r:?}");
392         assert!((r[1].width - (100.0 + extra)).abs() < 1e-3, "{r:?}");
393         assert!((r[1].x + r[1].width - 400.0).abs() < 1e-3, "the row ends at the form's edge");
394     }
395 
396     /// Given a height to fill, a fill piece takes what the other pieces leave of it.
397     #[test]
398     fn a_fill_piece_takes_the_rest_of_the_height() {
399         let gap = crate::layout::control_gap();
400         let seen: Seen = Rc::default();
401         let mut form: Form<'_, Texts> = Form::new(0.0, 0.0, 200.0, (0.0, 200.0));
402         form.fill_height(300.0);
403         {
404             let mut col = form.column();
405             piece(&mut col, &seen, 0.0, 40.0, false);
406             let s = seen.clone();
407             col.fill(10.0, move |_, r, _| s.borrow_mut().push(r));
408         }
409         let bottom = form.paint(&mut Texts::default(), &mut UiContext::new());
410         let r = seen.borrow();
411         assert!((r[1].height - (300.0 - 40.0 - gap)).abs() < 1e-3, "{r:?}");
412         assert_eq!(bottom, 300.0);
413     }
414 
415     /// A row's cells stand `list_gap` apart and in from its ends, each centred on its height,
416     /// and a growing cell takes what the fixed ones leave.
417     #[test]
418     fn a_list_rows_cells_are_a_list_gap_apart() {
419         let g = crate::layout::list_gap();
420         let rect = Rect { x: 10.0, y: 100.0, width: 300.0, height: 30.0 };
421         let r = lay_row(rect, &[Cell::fixed(24.0, 24.0), Cell::fixed(24.0, 24.0), Cell::grow(16.0), Cell::fixed(50.0, 24.0)]);
422         assert_eq!(r[0].x, 10.0 + g);
423         assert_eq!(r[1].x, r[0].x + 24.0 + g);
424         assert_eq!(r[2].x, r[1].x + 24.0 + g);
425         assert!((r[3].x + 50.0 - (310.0 - g)).abs() < 1e-3, "{r:?}");
426         assert!((r[2].width - (300.0 - 2.0 * g - 3.0 * g - 98.0)).abs() < 1e-3, "{r:?}");
427         assert_eq!((r[0].y, r[2].y), (103.0, 107.0), "centred on the row");
428     }
429 
430     /// Text is cut where its piece ends: a line in a column at the content box's edge, a value
431     /// at its cell's.
432     #[test]
433     fn text_is_cut_at_the_edge_of_its_piece() {
434         let mut form: Form<'_, Texts> = Form::new(10.0, 0.0, 200.0, (0.0, 200.0));
435         {
436             let mut col = form.column();
437             col.text("a line far wider than the box it was put in", 12.0, [1.0; 4]);
438             col.row(|r| {
439                 r.draw(60.0, 16.0, false, |_, _, _| {});
440                 r.text_fill("a value", 12.0, [1.0; 4]);
441             });
442         }
443         let mut out = Texts::default();
444         form.paint(&mut out, &mut UiContext::new());
445         for (text, bounds) in &out.bounds {
446             let right = bounds.expect("bounded")[2];
447             assert!((right - 210.0).abs() < 1e-3, "{text}: {bounds:?}");
448         }
449     }
450 }