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 }