GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/scene/layout.rs (31.9K)
1 //! Hand-rolled two-phase layout engine — Phase 2 of the core rebuild.
2 //!
3 //! The legacy toolkit computes layout *inline during paint*, smeared across `LayoutStrategy`, a
4 //! child-driven `allocate` bump-cursor, and hand-written `set_rect` calls with absolute
5 //! coordinates — with `SectionContext` literally rendering twice to measure. There is no layout
6 //! pass that is independent of paint, which is why animated/relayout-able UI is hard and why a
7 //! widget can end up sized by two different owners (the breadcrumb bug).
8 //!
9 //! This module replaces that with a real, self-contained solver that runs over the [`Arena`] and
10 //! is independent of paint:
11 //!
12 //! * **measure** (bottom-up): each node reports an intrinsic [`Size`] from its children (or, for
13 //! a leaf, its content size). Written into `LayoutBox::measured`.
14 //! * **arrange** (top-down): each node is given a final [`Rect`] and positions its children
15 //! within it. Written into `LayoutBox::rect`.
16 //!
17 //! Because it operates on `Style` + `Size` and writes plain rects, it is fully unit-testable
18 //! without a GPU or a Wayland surface (see the tests below).
19 //!
20 //! Layout modes ([`LayoutMode`]): **Flex** (row/column with grow/shrink, gap, padding, main/cross
21 //! alignment incl. stretch), **Stack** (Z-overlay with per-axis alignment), and **Grid** (fixed
22 //! column count with uniform column width and per-row heights). Deferred to later increments:
23 //! wrapping, percentage lengths, width-dependent adaptive grids, and the cosmic-text text-measure
24 //! hook for real leaf widgets (that lands with Phase 2b integration).
25
26 use crate::scene::arena::{Arena, NodeId};
27
28 /// A width/height pair in logical pixels.
29 #[derive(Debug, Clone, Copy, PartialEq)]
30 pub struct Size {
31 pub width: f32,
32 pub height: f32,
33 }
34
35 impl Size {
36 pub const ZERO: Size = Size { width: 0.0, height: 0.0 };
37 pub fn new(width: f32, height: f32) -> Self {
38 Size { width, height }
39 }
40 }
41
42 /// A positioned box in logical pixels (absolute coordinates after arrange).
43 #[derive(Debug, Clone, Copy, PartialEq)]
44 pub struct Rect {
45 pub x: f32,
46 pub y: f32,
47 pub width: f32,
48 pub height: f32,
49 }
50
51 impl Rect {
52 pub const ZERO: Rect = Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 };
53 }
54
55 /// How an image maps into a bounding box — see [`fit_rect`].
56 #[derive(Debug, Clone, Copy, PartialEq)]
57 pub enum FitMode {
58 /// Aspect-preserving: the image fills the box on its long axis and
59 /// letterboxes on the other, never scaling up past `max_upscale`
60 /// (1.0 = never enlarge; f32::INFINITY = always fill).
61 Contain { max_upscale: f32 },
62 /// The full box, aspect ignored.
63 Stretch,
64 }
65
66 /// The rect an `img_w` × `img_h` image occupies inside `bounds` under `mode`,
67 /// centered on both axes. Zero-sized images yield a zero rect at the box
68 /// center rather than a division blow-up.
69 pub fn fit_rect(img_w: u32, img_h: u32, bounds: Rect, mode: FitMode) -> Rect {
70 match mode {
71 FitMode::Stretch => bounds,
72 FitMode::Contain { max_upscale } => {
73 if img_w == 0 || img_h == 0 {
74 return Rect {
75 x: bounds.x + bounds.width * 0.5,
76 y: bounds.y + bounds.height * 0.5,
77 width: 0.0,
78 height: 0.0,
79 };
80 }
81 let (iw, ih) = (img_w as f32, img_h as f32);
82 let scale = (bounds.width / iw).min(bounds.height / ih).min(max_upscale).max(0.0);
83 let (w, h) = (iw * scale, ih * scale);
84 Rect {
85 x: bounds.x + (bounds.width - w) * 0.5,
86 y: bounds.y + (bounds.height - h) * 0.5,
87 width: w,
88 height: h,
89 }
90 }
91 }
92 }
93
94 /// Per-side spacing (padding).
95 #[derive(Debug, Clone, Copy, PartialEq)]
96 pub struct Edges {
97 pub left: f32,
98 pub right: f32,
99 pub top: f32,
100 pub bottom: f32,
101 }
102
103 impl Edges {
104 pub const ZERO: Edges = Edges { left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 };
105 pub fn all(v: f32) -> Self {
106 Edges { left: v, right: v, top: v, bottom: v }
107 }
108 }
109
110 /// The main-axis direction a flex container lays its children along.
111 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
112 pub enum Axis {
113 Row,
114 Column,
115 }
116
117 /// How a node arranges its children.
118 #[derive(Debug, Clone, Copy, PartialEq)]
119 pub enum LayoutMode {
120 /// Row/column flex (uses `Style::axis`).
121 Flex,
122 /// All children overlaid in the same box (Z-stack), aligned per axis.
123 Stack,
124 /// Fixed-column grid, filling left-to-right then top-to-bottom.
125 Grid(GridSpec),
126 }
127
128 /// A fixed-column grid: uniform column width (max child width), per-row heights.
129 #[derive(Debug, Clone, Copy, PartialEq)]
130 pub struct GridSpec {
131 pub columns: usize,
132 pub col_gap: f32,
133 pub row_gap: f32,
134 }
135
136 /// A length along one axis.
137 #[derive(Debug, Clone, Copy, PartialEq)]
138 pub enum Length {
139 /// Size to content (children/intrinsic), plus padding.
140 Auto,
141 /// Fixed logical pixels, overriding content size.
142 Fixed(f32),
143 }
144
145 /// Distribution of free space along the main axis (when no child grows/shrinks). For [`Stack`]
146 /// this selects horizontal placement.
147 ///
148 /// [`Stack`]: LayoutMode::Stack
149 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
150 pub enum MainAlign {
151 Start,
152 Center,
153 End,
154 SpaceBetween,
155 }
156
157 /// Placement of each child across the cross axis. For [`Stack`] this selects vertical placement.
158 ///
159 /// [`Stack`]: LayoutMode::Stack
160 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
161 pub enum CrossAlign {
162 Start,
163 Center,
164 End,
165 /// Fill the container's cross-axis content extent.
166 Stretch,
167 }
168
169 /// Layout inputs for a node.
170 #[derive(Debug, Clone, Copy, PartialEq)]
171 pub struct Style {
172 pub mode: LayoutMode,
173 pub axis: Axis,
174 pub padding: Edges,
175 pub gap: f32,
176 pub main_align: MainAlign,
177 pub cross_align: CrossAlign,
178 pub width: Length,
179 pub height: Length,
180 /// Flex grow weight: share of leftover main-axis space this node claims.
181 pub grow: f32,
182 /// Flex shrink weight: share of a main-axis overflow this node gives up.
183 pub shrink: f32,
184 pub min_width: f32,
185 pub min_height: f32,
186 pub max_width: f32,
187 pub max_height: f32,
188 }
189
190 impl Default for Style {
191 fn default() -> Self {
192 Style {
193 mode: LayoutMode::Flex,
194 axis: Axis::Column,
195 padding: Edges::ZERO,
196 gap: 0.0,
197 main_align: MainAlign::Start,
198 cross_align: CrossAlign::Start,
199 width: Length::Auto,
200 height: Length::Auto,
201 grow: 0.0,
202 shrink: 0.0,
203 min_width: 0.0,
204 min_height: 0.0,
205 max_width: f32::INFINITY,
206 max_height: f32::INFINITY,
207 }
208 }
209 }
210
211 impl Style {
212 pub fn row() -> Self {
213 Style { mode: LayoutMode::Flex, axis: Axis::Row, ..Default::default() }
214 }
215 pub fn column() -> Self {
216 Style { mode: LayoutMode::Flex, axis: Axis::Column, ..Default::default() }
217 }
218 pub fn stack() -> Self {
219 Style { mode: LayoutMode::Stack, ..Default::default() }
220 }
221 pub fn grid(columns: usize, col_gap: f32, row_gap: f32) -> Self {
222 Style { mode: LayoutMode::Grid(GridSpec { columns: columns.max(1), col_gap, row_gap }), ..Default::default() }
223 }
224 // ── The spacing ladder as presets ─────────────────────────────────
225 // An app on the standard root plate never names a padding or gap: it
226 // picks the rung. Root presets inset by `root_plate_inset` (the plate's
227 // roll plus one padding) and space siblings by `root_plate_gap`; pane
228 // presets by `plate_padding` / `plate_gap`; the controls presets space
229 // a form's controls by `control_gap` with no inset of their own, since
230 // they sit inside a pane or root preset that already has one.
231
232 /// A column of siblings standing on the root plate, stretched across it.
233 pub fn root_column() -> Self {
234 Self::column()
235 .padding(crate::layout::root_plate_inset())
236 .gap(crate::layout::root_plate_gap())
237 .cross_align(CrossAlign::Stretch)
238 }
239 /// A row of siblings standing on the root plate.
240 pub fn root_row() -> Self {
241 Self::row()
242 .padding(crate::layout::root_plate_inset())
243 .gap(crate::layout::root_plate_gap())
244 .cross_align(CrossAlign::Stretch)
245 }
246 /// A column inside a pane plate, inset from its rim.
247 pub fn pane_column() -> Self {
248 Self::column()
249 .padding(crate::layout::plate_padding())
250 .gap(crate::layout::plate_gap())
251 .cross_align(CrossAlign::Stretch)
252 }
253 /// A row inside a pane plate.
254 pub fn pane_row() -> Self {
255 Self::row()
256 .padding(crate::layout::plate_padding())
257 .gap(crate::layout::plate_gap())
258 .cross_align(CrossAlign::Stretch)
259 }
260 /// A column of controls: the control gap between them, no inset.
261 pub fn controls_column() -> Self {
262 Self::column().gap(crate::layout::control_gap())
263 }
264 /// A row of controls: the control gap between them, no inset.
265 pub fn controls_row() -> Self {
266 Self::row().gap(crate::layout::control_gap())
267 }
268
269 pub fn gap(mut self, v: f32) -> Self {
270 self.gap = v;
271 self
272 }
273 pub fn padding(mut self, v: f32) -> Self {
274 self.padding = Edges::all(v);
275 self
276 }
277 pub fn grow(mut self, v: f32) -> Self {
278 self.grow = v;
279 self
280 }
281 pub fn shrink(mut self, v: f32) -> Self {
282 self.shrink = v;
283 self
284 }
285 pub fn main_align(mut self, a: MainAlign) -> Self {
286 self.main_align = a;
287 self
288 }
289 pub fn cross_align(mut self, a: CrossAlign) -> Self {
290 self.cross_align = a;
291 self
292 }
293 pub fn width(mut self, w: Length) -> Self {
294 self.width = w;
295 self
296 }
297 pub fn height(mut self, h: Length) -> Self {
298 self.height = h;
299 self
300 }
301 }
302
303 /// A node's layout state: inputs (`style`, optional `intrinsic` content size for leaves) and the
304 /// two computed outputs (`measured`, then `rect`).
305 #[derive(Debug, Clone, Copy)]
306 pub struct LayoutBox {
307 pub style: Style,
308 /// Content size for a leaf (e.g. measured text). Ignored when the node has children.
309 pub intrinsic: Option<Size>,
310 pub measured: Size,
311 pub rect: Rect,
312 }
313
314 impl LayoutBox {
315 /// A container node laid out from its children.
316 pub fn container(style: Style) -> Self {
317 LayoutBox { style, intrinsic: None, measured: Size::ZERO, rect: Rect::ZERO }
318 }
319
320 /// A leaf node with a fixed content size.
321 pub fn leaf(style: Style, content: Size) -> Self {
322 LayoutBox { style, intrinsic: Some(content), measured: Size::ZERO, rect: Rect::ZERO }
323 }
324 }
325
326 // --- axis helpers: project/unproject a Size onto the (main, cross) frame of an axis ---
327
328 #[inline]
329 fn main_of(axis: Axis, s: Size) -> f32 {
330 match axis {
331 Axis::Row => s.width,
332 Axis::Column => s.height,
333 }
334 }
335
336 #[inline]
337 fn cross_of(axis: Axis, s: Size) -> f32 {
338 match axis {
339 Axis::Row => s.height,
340 Axis::Column => s.width,
341 }
342 }
343
344 #[inline]
345 fn make_size(axis: Axis, main: f32, cross: f32) -> Size {
346 match axis {
347 Axis::Row => Size::new(main, cross),
348 Axis::Column => Size::new(cross, main),
349 }
350 }
351
352 /// Offset that places an item of extent `item` within `container` per a start/center/end rule.
353 #[inline]
354 fn align_offset(start: bool, center: bool, container: f32, item: f32) -> f32 {
355 if center {
356 (container - item) / 2.0
357 } else if start {
358 0.0
359 } else {
360 container - item // end
361 }
362 }
363
364 /// Build a child rect from axis-relative main/cross offsets and extents (offsets are relative to
365 /// the parent's content origin `content_x`/`content_y`).
366 #[inline]
367 fn child_rect(
368 axis: Axis,
369 content_x: f32,
370 content_y: f32,
371 main_pos: f32,
372 cross_pos: f32,
373 main_size: f32,
374 cross_size: f32,
375 ) -> Rect {
376 match axis {
377 Axis::Row => Rect {
378 x: content_x + main_pos,
379 y: content_y + cross_pos,
380 width: main_size,
381 height: cross_size,
382 },
383 Axis::Column => Rect {
384 x: content_x + cross_pos,
385 y: content_y + main_pos,
386 width: cross_size,
387 height: main_size,
388 },
389 }
390 }
391
392 #[inline]
393 fn content_box(rect: Rect, p: Edges) -> (f32, f32, f32, f32) {
394 (
395 rect.x + p.left,
396 rect.y + p.top,
397 (rect.width - p.left - p.right).max(0.0),
398 (rect.height - p.top - p.bottom).max(0.0),
399 )
400 }
401
402 /// Resolve the node's own size from its content box: apply explicit width/height, add padding for
403 /// `Auto`, then clamp to min/max.
404 fn finalize_size(style: &Style, content: Size) -> Size {
405 let padded = Size::new(
406 content.width + style.padding.left + style.padding.right,
407 content.height + style.padding.top + style.padding.bottom,
408 );
409 let mut size = Size {
410 width: match style.width {
411 Length::Fixed(v) => v,
412 Length::Auto => padded.width,
413 },
414 height: match style.height {
415 Length::Fixed(v) => v,
416 Length::Auto => padded.height,
417 },
418 };
419 size.width = size.width.clamp(style.min_width, style.max_width);
420 size.height = size.height.clamp(style.min_height, style.max_height);
421 size
422 }
423
424 /// Run both passes over the subtree rooted at `root`, laying it out into `available` space at the
425 /// origin. Writes `measured` and `rect` into every node.
426 pub fn compute_layout(arena: &mut Arena<LayoutBox>, root: NodeId, available: Size) {
427 measure(arena, root);
428 let root_rect = Rect { x: 0.0, y: 0.0, width: available.width, height: available.height };
429 arrange(arena, root, root_rect);
430 }
431
432 /// Bottom-up intrinsic sizing. Returns and records the node's `measured` size.
433 pub fn measure(arena: &mut Arena<LayoutBox>, id: NodeId) -> Size {
434 let (style, intrinsic) = {
435 let b = arena.value(id).expect("measure: stale node");
436 (b.style, b.intrinsic)
437 };
438 let children = arena.children(id).to_vec();
439
440 let content = if children.is_empty() {
441 intrinsic.unwrap_or(Size::ZERO)
442 } else {
443 match style.mode {
444 LayoutMode::Flex => measure_flex(arena, &style, &children),
445 LayoutMode::Stack => measure_stack(arena, &children),
446 LayoutMode::Grid(spec) => measure_grid(arena, spec, &children),
447 }
448 };
449
450 let size = finalize_size(&style, content);
451 arena.value_mut(id).expect("measure: stale node").measured = size;
452 size
453 }
454
455 fn measure_flex(arena: &mut Arena<LayoutBox>, style: &Style, children: &[NodeId]) -> Size {
456 let mut main = 0.0f32;
457 let mut cross = 0.0f32;
458 for (i, &child) in children.iter().enumerate() {
459 let cs = measure(arena, child);
460 if i > 0 {
461 main += style.gap;
462 }
463 main += main_of(style.axis, cs);
464 cross = cross.max(cross_of(style.axis, cs));
465 }
466 make_size(style.axis, main, cross)
467 }
468
469 fn measure_stack(arena: &mut Arena<LayoutBox>, children: &[NodeId]) -> Size {
470 let mut w = 0.0f32;
471 let mut h = 0.0f32;
472 for &child in children {
473 let cs = measure(arena, child);
474 w = w.max(cs.width);
475 h = h.max(cs.height);
476 }
477 Size::new(w, h)
478 }
479
480 fn measure_grid(arena: &mut Arena<LayoutBox>, spec: GridSpec, children: &[NodeId]) -> Size {
481 let cols = spec.columns.max(1);
482 let sizes: Vec<Size> = children.iter().map(|&c| measure(arena, c)).collect();
483 let cell_w = sizes.iter().fold(0.0f32, |m, s| m.max(s.width));
484 let rows = sizes.len().div_ceil(cols);
485 let mut row_heights = vec![0.0f32; rows];
486 for (i, s) in sizes.iter().enumerate() {
487 let r = i / cols;
488 row_heights[r] = row_heights[r].max(s.height);
489 }
490 let content_w = cols as f32 * cell_w + (cols as f32 - 1.0) * spec.col_gap;
491 let content_h =
492 row_heights.iter().sum::<f32>() + (rows as f32 - 1.0).max(0.0) * spec.row_gap;
493 Size::new(content_w, content_h)
494 }
495
496 /// Top-down placement. Assigns `rect` to `id`, then positions its children within it.
497 pub fn arrange(arena: &mut Arena<LayoutBox>, id: NodeId, rect: Rect) {
498 arena.value_mut(id).expect("arrange: stale node").rect = rect;
499
500 let style = arena.value(id).unwrap().style;
501 let children = arena.children(id).to_vec();
502 if children.is_empty() {
503 return;
504 }
505 let (cx, cy, cw, ch) = content_box(rect, style.padding);
506
507 let placements = match style.mode {
508 LayoutMode::Flex => arrange_flex(arena, &style, &children, cx, cy, cw, ch),
509 LayoutMode::Stack => arrange_stack(arena, &style, &children, cx, cy, cw, ch),
510 LayoutMode::Grid(spec) => arrange_grid(arena, spec, &children, cx, cy),
511 };
512
513 for (child, r) in placements {
514 arrange(arena, child, r);
515 }
516 }
517
518 fn arrange_flex(
519 arena: &Arena<LayoutBox>,
520 style: &Style,
521 children: &[NodeId],
522 cx: f32,
523 cy: f32,
524 cw: f32,
525 ch: f32,
526 ) -> Vec<(NodeId, Rect)> {
527 let axis = style.axis;
528 let content = Size::new(cw, ch);
529 let content_main = main_of(axis, content);
530 let content_cross = cross_of(axis, content);
531
532 let mut child_main = Vec::with_capacity(children.len());
533 let mut child_cross = Vec::with_capacity(children.len());
534 let mut grows = Vec::with_capacity(children.len());
535 let mut shrinks = Vec::with_capacity(children.len());
536 for &child in children {
537 let b = arena.value(child).unwrap();
538 child_main.push(main_of(axis, b.measured));
539 child_cross.push(cross_of(axis, b.measured));
540 grows.push(b.style.grow);
541 shrinks.push(b.style.shrink);
542 }
543
544 let n = children.len();
545 let total_main: f32 = child_main.iter().sum::<f32>() + style.gap * (n as f32 - 1.0);
546 let free = content_main - total_main;
547 let total_grow: f32 = grows.iter().sum();
548 let total_shrink: f32 = shrinks.iter().sum();
549
550 // Resolve each child's main extent: grow to fill, or shrink to fit, else keep measured.
551 let mut sizes = child_main.clone();
552 let distributed = if free > 0.0 && total_grow > 0.0 {
553 for i in 0..n {
554 sizes[i] += grows[i] / total_grow * free;
555 }
556 true
557 } else if free < 0.0 && total_shrink > 0.0 {
558 let deficit = -free;
559 for i in 0..n {
560 sizes[i] = (child_main[i] - shrinks[i] / total_shrink * deficit).max(0.0);
561 }
562 true
563 } else {
564 false
565 };
566
567 // Alignment only distributes leftover space when grow/shrink didn't consume it.
568 let (start_offset, spacing_extra) = if distributed {
569 (0.0, 0.0)
570 } else {
571 match style.main_align {
572 MainAlign::Start => (0.0, 0.0),
573 MainAlign::Center => (free.max(0.0) / 2.0, 0.0),
574 MainAlign::End => (free.max(0.0), 0.0),
575 MainAlign::SpaceBetween => {
576 (0.0, if n > 1 { free.max(0.0) / (n as f32 - 1.0) } else { 0.0 })
577 }
578 }
579 };
580
581 let mut out = Vec::with_capacity(n);
582 let mut main_pos = start_offset;
583 for i in 0..n {
584 let main_size = sizes[i];
585 let cross_size = match style.cross_align {
586 CrossAlign::Stretch => content_cross,
587 _ => child_cross[i],
588 };
589 let cross_pos = match style.cross_align {
590 CrossAlign::Start | CrossAlign::Stretch => 0.0,
591 CrossAlign::Center => (content_cross - cross_size) / 2.0,
592 CrossAlign::End => content_cross - cross_size,
593 };
594 out.push((children[i], child_rect(axis, cx, cy, main_pos, cross_pos, main_size, cross_size)));
595 main_pos += main_size + style.gap + spacing_extra;
596 }
597 out
598 }
599
600 fn arrange_stack(
601 arena: &Arena<LayoutBox>,
602 style: &Style,
603 children: &[NodeId],
604 cx: f32,
605 cy: f32,
606 cw: f32,
607 ch: f32,
608 ) -> Vec<(NodeId, Rect)> {
609 // Stack has no axis: `main_align` places children horizontally, `cross_align` vertically.
610 let (h_start, h_center) =
611 (style.main_align == MainAlign::Start || style.main_align == MainAlign::SpaceBetween,
612 style.main_align == MainAlign::Center);
613 let (v_start, v_center) =
614 (style.cross_align == CrossAlign::Start, style.cross_align == CrossAlign::Center);
615 let stretch_v = style.cross_align == CrossAlign::Stretch;
616
617 let mut out = Vec::with_capacity(children.len());
618 for &child in children {
619 let m = arena.value(child).unwrap().measured;
620 let w = m.width;
621 let h = if stretch_v { ch } else { m.height };
622 let x = cx + align_offset(h_start, h_center, cw, w);
623 let y = cy + if stretch_v { 0.0 } else { align_offset(v_start, v_center, ch, h) };
624 out.push((child, Rect { x, y, width: w, height: h }));
625 }
626 out
627 }
628
629 fn arrange_grid(
630 arena: &Arena<LayoutBox>,
631 spec: GridSpec,
632 children: &[NodeId],
633 cx: f32,
634 cy: f32,
635 ) -> Vec<(NodeId, Rect)> {
636 let cols = spec.columns.max(1);
637 let sizes: Vec<Size> = children.iter().map(|&c| arena.value(c).unwrap().measured).collect();
638 let cell_w = sizes.iter().fold(0.0f32, |m, s| m.max(s.width));
639 let rows = sizes.len().div_ceil(cols);
640
641 let mut row_heights = vec![0.0f32; rows];
642 for (i, s) in sizes.iter().enumerate() {
643 row_heights[i / cols] = row_heights[i / cols].max(s.height);
644 }
645 // y offset of each row's top.
646 let mut row_y = vec![0.0f32; rows];
647 let mut acc = 0.0;
648 for r in 0..rows {
649 row_y[r] = acc;
650 acc += row_heights[r] + spec.row_gap;
651 }
652
653 let mut out = Vec::with_capacity(children.len());
654 for (i, &child) in children.iter().enumerate() {
655 let col = i % cols;
656 let row = i / cols;
657 let x = cx + col as f32 * (cell_w + spec.col_gap);
658 let y = cy + row_y[row];
659 out.push((child, Rect { x, y, width: sizes[i].width, height: sizes[i].height }));
660 }
661 out
662 }
663
664 #[cfg(test)]
665 mod tests {
666 use super::*;
667
668 #[test]
669 fn fit_contain_letterboxes_and_centers() {
670 let b = Rect { x: 10.0, y: 20.0, width: 100.0, height: 50.0 };
671 // 200x100 source, scale limited by both axes equally -> 100x50 fill
672 let r = fit_rect(200, 100, b, FitMode::Contain { max_upscale: 4.0 });
673 assert_eq!((r.x, r.y, r.width, r.height), (10.0, 20.0, 100.0, 50.0));
674 // tall source letterboxes horizontally: scale = 50/200 -> 25x50
675 let r = fit_rect(100, 200, b, FitMode::Contain { max_upscale: 4.0 });
676 assert_eq!((r.width, r.height), (25.0, 50.0));
677 assert_eq!(r.x, 10.0 + (100.0 - 25.0) * 0.5);
678 assert_eq!(r.y, 20.0);
679 }
680
681 #[test]
682 fn fit_contain_caps_upscale_but_downscales_freely() {
683 let b = Rect { x: 0.0, y: 0.0, width: 400.0, height: 400.0 };
684 // small source: would need 8x, capped at 4x, centered
685 let r = fit_rect(50, 50, b, FitMode::Contain { max_upscale: 4.0 });
686 assert_eq!((r.width, r.height), (200.0, 200.0));
687 assert_eq!((r.x, r.y), (100.0, 100.0));
688 // large source downscales with no floor (the old .max(1.0) bug)
689 let r = fit_rect(800, 800, b, FitMode::Contain { max_upscale: 4.0 });
690 assert_eq!((r.width, r.height), (400.0, 400.0));
691 }
692
693 #[test]
694 fn fit_degenerate_inputs() {
695 let b = Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 };
696 let r = fit_rect(0, 50, b, FitMode::Contain { max_upscale: 4.0 });
697 assert_eq!((r.width, r.height), (0.0, 0.0));
698 let r = fit_rect(10, 10, b, FitMode::Stretch);
699 assert_eq!((r.width, r.height), (100.0, 100.0));
700 }
701
702 fn leaf(arena: &mut Arena<LayoutBox>, w: f32, h: f32) -> NodeId {
703 arena.insert(LayoutBox::leaf(Style::default(), Size::new(w, h)))
704 }
705
706 fn rect_of(arena: &Arena<LayoutBox>, id: NodeId) -> Rect {
707 arena.value(id).unwrap().rect
708 }
709
710 #[test]
711 fn row_places_children_left_to_right_with_gap() {
712 let mut arena = Arena::new();
713 let root = arena.insert(LayoutBox::container(Style::row().gap(5.0)));
714 let a = leaf(&mut arena, 10.0, 10.0);
715 let b = leaf(&mut arena, 20.0, 10.0);
716 arena.append_child(root, a);
717 arena.append_child(root, b);
718
719 compute_layout(&mut arena, root, Size::new(100.0, 50.0));
720 assert_eq!(rect_of(&arena, a), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
721 assert_eq!(rect_of(&arena, b), Rect { x: 15.0, y: 0.0, width: 20.0, height: 10.0 });
722 }
723
724 #[test]
725 fn padding_offsets_content() {
726 let mut arena = Arena::new();
727 let mut s = Style::row();
728 s.padding = Edges { left: 5.0, right: 0.0, top: 7.0, bottom: 0.0 };
729 let root = arena.insert(LayoutBox::container(s));
730 let a = leaf(&mut arena, 10.0, 10.0);
731 arena.append_child(root, a);
732
733 compute_layout(&mut arena, root, Size::new(100.0, 50.0));
734 assert_eq!(rect_of(&arena, a), Rect { x: 5.0, y: 7.0, width: 10.0, height: 10.0 });
735 }
736
737 #[test]
738 fn grow_distributes_free_space_by_weight() {
739 let mut arena = Arena::new();
740 let root = arena.insert(LayoutBox::container(Style::row()));
741 let a = arena.insert(LayoutBox::leaf(Style::default().grow(1.0), Size::new(10.0, 10.0)));
742 let b = arena.insert(LayoutBox::leaf(Style::default().grow(3.0), Size::new(10.0, 10.0)));
743 arena.append_child(root, a);
744 arena.append_child(root, b);
745
746 compute_layout(&mut arena, root, Size::new(100.0, 50.0));
747 assert_eq!(rect_of(&arena, a).width, 30.0);
748 assert_eq!(rect_of(&arena, b).width, 70.0);
749 assert_eq!(rect_of(&arena, b).x, 30.0);
750 }
751
752 #[test]
753 fn shrink_absorbs_overflow_by_weight() {
754 // Two 60-wide leaves in 100px, both shrink 1 => 20px deficit split evenly => 50 each.
755 let mut arena = Arena::new();
756 let root = arena.insert(LayoutBox::container(Style::row()));
757 let a = arena.insert(LayoutBox::leaf(Style::default().shrink(1.0), Size::new(60.0, 10.0)));
758 let b = arena.insert(LayoutBox::leaf(Style::default().shrink(1.0), Size::new(60.0, 10.0)));
759 arena.append_child(root, a);
760 arena.append_child(root, b);
761
762 compute_layout(&mut arena, root, Size::new(100.0, 50.0));
763 assert_eq!(rect_of(&arena, a).width, 50.0);
764 assert_eq!(rect_of(&arena, b).width, 50.0);
765 assert_eq!(rect_of(&arena, b).x, 50.0);
766 }
767
768 #[test]
769 fn main_align_center_and_end() {
770 let mut arena = Arena::new();
771 let root_c = arena.insert(LayoutBox::container(Style::row().main_align(MainAlign::Center)));
772 let a = leaf(&mut arena, 20.0, 10.0);
773 arena.append_child(root_c, a);
774 compute_layout(&mut arena, root_c, Size::new(100.0, 50.0));
775 assert_eq!(rect_of(&arena, a).x, 40.0);
776
777 let mut arena2 = Arena::new();
778 let root_e = arena2.insert(LayoutBox::container(Style::row().main_align(MainAlign::End)));
779 let b = arena2.insert(LayoutBox::leaf(Style::default(), Size::new(20.0, 10.0)));
780 arena2.append_child(root_e, b);
781 compute_layout(&mut arena2, root_e, Size::new(100.0, 50.0));
782 assert_eq!(rect_of(&arena2, b).x, 80.0);
783 }
784
785 #[test]
786 fn space_between_pushes_children_to_edges() {
787 let mut arena = Arena::new();
788 let root = arena.insert(LayoutBox::container(Style::row().main_align(MainAlign::SpaceBetween)));
789 let a = leaf(&mut arena, 10.0, 10.0);
790 let b = leaf(&mut arena, 10.0, 10.0);
791 arena.append_child(root, a);
792 arena.append_child(root, b);
793
794 compute_layout(&mut arena, root, Size::new(100.0, 50.0));
795 assert_eq!(rect_of(&arena, a).x, 0.0);
796 assert_eq!(rect_of(&arena, b).x, 90.0);
797 }
798
799 #[test]
800 fn cross_align_center_and_stretch() {
801 let mut arena = Arena::new();
802 let root = arena.insert(LayoutBox::container(Style::row().cross_align(CrossAlign::Center)));
803 let a = leaf(&mut arena, 10.0, 10.0);
804 arena.append_child(root, a);
805 compute_layout(&mut arena, root, Size::new(100.0, 50.0));
806 assert_eq!(rect_of(&arena, a).y, 20.0);
807
808 let mut arena2 = Arena::new();
809 let root2 = arena2.insert(LayoutBox::container(Style::row().cross_align(CrossAlign::Stretch)));
810 let b = arena2.insert(LayoutBox::leaf(Style::default(), Size::new(10.0, 10.0)));
811 arena2.append_child(root2, b);
812 compute_layout(&mut arena2, root2, Size::new(100.0, 50.0));
813 assert_eq!(rect_of(&arena2, b).height, 50.0);
814 assert_eq!(rect_of(&arena2, b).y, 0.0);
815 }
816
817 #[test]
818 fn auto_container_measures_to_content() {
819 let mut arena = Arena::new();
820 let root = arena.insert(LayoutBox::container(Style::column().gap(4.0)));
821 let a = leaf(&mut arena, 10.0, 10.0);
822 let b = leaf(&mut arena, 10.0, 10.0);
823 arena.append_child(root, a);
824 arena.append_child(root, b);
825
826 let m = measure(&mut arena, root);
827 assert_eq!(m, Size::new(10.0, 24.0));
828 }
829
830 #[test]
831 fn fixed_length_overrides_content_and_clamps() {
832 let mut arena = Arena::new();
833 let s = Style::column().width(Length::Fixed(200.0));
834 let root = arena.insert(LayoutBox::container(s));
835 let a = leaf(&mut arena, 10.0, 10.0);
836 arena.append_child(root, a);
837
838 let m = measure(&mut arena, root);
839 assert_eq!(m.width, 200.0);
840 assert_eq!(m.height, 10.0);
841 }
842
843 #[test]
844 fn nested_containers_lay_out_recursively() {
845 let mut arena = Arena::new();
846 let root = arena.insert(LayoutBox::container(Style::row().gap(0.0)));
847 let inner = arena.insert(LayoutBox::container(Style::column().gap(2.0)));
848 let c1 = leaf(&mut arena, 10.0, 10.0);
849 let c2 = leaf(&mut arena, 10.0, 10.0);
850 let sibling = leaf(&mut arena, 5.0, 5.0);
851 arena.append_child(root, inner);
852 arena.append_child(root, sibling);
853 arena.append_child(inner, c1);
854 arena.append_child(inner, c2);
855
856 compute_layout(&mut arena, root, Size::new(100.0, 100.0));
857 assert_eq!(rect_of(&arena, inner), Rect { x: 0.0, y: 0.0, width: 10.0, height: 22.0 });
858 assert_eq!(rect_of(&arena, c1), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
859 assert_eq!(rect_of(&arena, c2), Rect { x: 0.0, y: 12.0, width: 10.0, height: 10.0 });
860 assert_eq!(rect_of(&arena, sibling).x, 10.0);
861 }
862
863 #[test]
864 fn stack_overlays_children_and_aligns_per_axis() {
865 let mut arena = Arena::new();
866 let root = arena.insert(LayoutBox::container(Style::stack()));
867 let a = leaf(&mut arena, 10.0, 10.0);
868 let b = leaf(&mut arena, 30.0, 20.0);
869 arena.append_child(root, a);
870 arena.append_child(root, b);
871 compute_layout(&mut arena, root, Size::new(100.0, 100.0));
872 // Start/Start: both at the content origin, at their own sizes.
873 assert_eq!(rect_of(&arena, a), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
874 assert_eq!(rect_of(&arena, b), Rect { x: 0.0, y: 0.0, width: 30.0, height: 20.0 });
875
876 // Centered on both axes.
877 let mut arena2 = Arena::new();
878 let root2 = arena2.insert(LayoutBox::container(
879 Style::stack().main_align(MainAlign::Center).cross_align(CrossAlign::Center),
880 ));
881 let c = arena2.insert(LayoutBox::leaf(Style::default(), Size::new(10.0, 10.0)));
882 arena2.append_child(root2, c);
883 compute_layout(&mut arena2, root2, Size::new(100.0, 100.0));
884 assert_eq!(rect_of(&arena2, c), Rect { x: 45.0, y: 45.0, width: 10.0, height: 10.0 });
885 }
886
887 #[test]
888 fn grid_flows_children_by_columns() {
889 // 3 leaves (10x10) in a 2-col grid, gaps 5/5.
890 let mut arena = Arena::new();
891 let root = arena.insert(LayoutBox::container(Style::grid(2, 5.0, 5.0)));
892 let a = leaf(&mut arena, 10.0, 10.0);
893 let b = leaf(&mut arena, 10.0, 10.0);
894 let c = leaf(&mut arena, 10.0, 10.0);
895 arena.append_child(root, a);
896 arena.append_child(root, b);
897 arena.append_child(root, c);
898
899 // measured: 2 cols * 10 + 5 = 25 wide; 2 rows * 10 + 5 = 25 tall.
900 let m = measure(&mut arena, root);
901 assert_eq!(m, Size::new(25.0, 25.0));
902
903 compute_layout(&mut arena, root, Size::new(200.0, 200.0));
904 assert_eq!(rect_of(&arena, a), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
905 assert_eq!(rect_of(&arena, b), Rect { x: 15.0, y: 0.0, width: 10.0, height: 10.0 });
906 assert_eq!(rect_of(&arena, c), Rect { x: 0.0, y: 15.0, width: 10.0, height: 10.0 });
907 }
908 }