1use crate::text;
2use crate::widget::{Action, Dock, HAlign, Units, VAlign, Widget, WidgetKind};
3
4mod grid;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum FlexDir {
8 Row,
9 Column,
10}
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Align {
13 Start,
14 Center,
15 End,
16}
17
18#[derive(Debug, Clone, Copy, Default)]
19pub struct Rect {
20 pub x: f32,
21 pub y: f32,
22 pub w: f32,
23 pub h: f32,
24}
25
26pub const SCROLLBAR_W: f32 = 6.0;
29pub const MIN_KNOB_H: f32 = 16.0;
31
32impl Rect {
33 pub fn contains(&self, px: f32, py: f32) -> bool {
35 px >= self.x && py >= self.y && px <= self.x + self.w && py <= self.y + self.h
36 }
37
38 pub fn intersect(&self, o: &Rect) -> Option<Rect> {
40 let x0 = self.x.max(o.x);
41 let y0 = self.y.max(o.y);
42 let x1 = (self.x + self.w).min(o.x + o.w);
43 let y1 = (self.y + self.h).min(o.y + o.h);
44 (x1 > x0 && y1 > y0).then_some(Rect {
45 x: x0,
46 y: y0,
47 w: x1 - x0,
48 h: y1 - y0,
49 })
50 }
51}
52
53#[derive(Debug, Clone)]
54pub struct LayoutNode {
55 pub rect: Rect,
56 pub id: Option<String>,
57 pub on_click: Option<String>,
58 pub children: Vec<LayoutNode>,
59 pub enabled: bool,
60 pub focused: bool,
61 pub content_h: f32,
64 pub scroll_y: f32,
68 pub clip: bool,
70 pub scrollbar: bool,
72 pub action: Option<Action>,
74 pub visible: bool,
77 pub grid: Option<Box<GridLines>>,
79}
80
81#[derive(Debug, Clone, Default, PartialEq)]
84pub struct GridLines {
85 pub rows: Vec<(f32, f32)>,
87 pub cols: Vec<(f32, f32)>,
89 pub(crate) floor_rows: Vec<f32>,
93 pub(crate) floor_cols: Vec<f32>,
94}
95
96impl LayoutNode {
97 pub fn max_scroll(&self) -> f32 {
99 (self.content_h - self.rect.h).max(0.0)
100 }
101
102 pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
105 let max = self.max_scroll();
106 if !self.scrollbar || max <= 0.0 {
107 return None;
108 }
109 let r = self.rect;
110 let track = Rect {
111 x: r.x + r.w - SCROLLBAR_W,
112 y: r.y,
113 w: SCROLLBAR_W,
114 h: r.h,
115 };
116 let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
117 let knob = Rect {
118 y: r.y + (r.h - knob_h) * (self.scroll_y / max),
119 h: knob_h,
120 ..track
121 };
122 Some((track, knob))
123 }
124
125 fn translate_y(&mut self, dy: f32) {
126 self.rect.y += dy;
127 if let Some(g) = &mut self.grid {
128 for r in &mut g.rows {
129 r.0 += dy;
130 }
131 }
132 for c in &mut self.children {
133 c.translate_y(dy);
134 }
135 }
136}
137
138impl Default for LayoutNode {
139 fn default() -> Self {
140 Self {
141 rect: Rect::default(),
142 id: None,
143 on_click: None,
144 children: Vec::new(),
145 enabled: true,
146 focused: false,
147 content_h: 0.0,
148 scroll_y: 0.0,
149 clip: false,
150 scrollbar: false,
151 action: None,
152 visible: true,
153 grid: None,
154 }
155 }
156}
157
158pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
175 let mut node = new_node(widget);
176 if widget.visible {
177 layout_widget(
178 widget, &mut node, 0.0, 0.0, avail_w, avail_h, avail_w, avail_h,
179 );
180 }
181 node
182}
183
184const UNBOUNDED: f32 = f32::MAX;
187
188fn bounded(v: f32) -> bool {
189 v < UNBOUNDED / 2.0
190}
191
192fn resolve_size(u: Units, basis: f32) -> Option<f32> {
194 match u {
195 Units::Percent(_) if !bounded(basis) => None,
196 _ => u.resolve(basis),
197 }
198}
199
200fn new_node(w: &Widget) -> LayoutNode {
201 LayoutNode {
202 id: w.id.clone(),
203 on_click: w.on_click.clone(),
204 enabled: w.enabled,
205 focused: w.focused,
206 action: w.action,
207 visible: w.visible,
208 ..Default::default()
209 }
210}
211
212#[allow(clippy::too_many_arguments)]
216fn layout_widget(
217 w: &Widget,
218 node: &mut LayoutNode,
219 x: f32,
220 y: f32,
221 max_w: f32,
222 max_h: f32,
223 basis_w: f32,
224 basis_h: f32,
225) {
226 let s = &w.style;
227 let has_children = is_container(w);
228 let explicit_w = resolve_size(s.w, basis_w).map(|v| v.min(max_w));
229 let explicit_h = resolve_size(s.h, basis_h).map(|v| v.min(max_h));
230
231 let mut ww = explicit_w.unwrap_or(max_w);
233 let mut hh = explicit_h.unwrap_or(max_h);
234
235 if !has_children {
236 match &w.kind {
238 WidgetKind::Label(t) | WidgetKind::Button(t) => {
239 let box_w = explicit_w.unwrap_or(max_w);
241 let avail_w = (box_w - s.pad[1] - s.pad[3]).max(0.0);
242 if avail_w < 4096.0 && !s.no_wrap {
245 ww = (avail_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
246 hh = (text::text_height_in(t, avail_w, s.font_scale, &s.font)
247 + s.pad[0]
248 + s.pad[2])
249 .max(s.min_h)
250 .min(max_h);
251 } else {
252 let tw = text::str_width_in(t, s.font_scale, &s.font);
253 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
254 hh = (text::LINE_H * s.font_scale + s.pad[0] + s.pad[2])
255 .max(s.min_h)
256 .min(max_h);
257 }
258 }
259 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
260 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
261 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
262 }
263 WidgetKind::Spacer | WidgetKind::Surface(_) => {
264 ww = s.min_w.max(1.0).min(max_w);
265 hh = s.min_h.max(1.0).min(max_h);
266 }
267 WidgetKind::Panel(_) | WidgetKind::Grid(_) => {
270 if !bounded(ww) {
271 ww = s.min_w;
272 }
273 if !bounded(hh) {
274 hh = s.min_h;
275 }
276 }
277 WidgetKind::TextBox(_) => {
280 if !bounded(ww) {
281 ww = 100.0;
282 }
283 ww = ww.max(s.min_w).min(max_w);
284 hh = (text::LINE_H * s.font_scale + 7.0 + s.pad[0] + s.pad[2])
285 .max(s.min_h)
286 .min(max_h);
287 }
288 WidgetKind::Image(image) => {
289 let (nw, nh) = crate::image::natural_size(image);
290 ww = (nw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
291 hh = (nh + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
292 }
293 }
294 if let Some(v) = explicit_w {
296 ww = v;
297 }
298 if let Some(v) = explicit_h {
299 hh = v;
300 }
301 }
302
303 node.rect = Rect { x, y, w: ww, h: hh };
304 node.grid = None;
305 node.content_h = hh;
306 node.scroll_y = 0.0;
307 node.clip = s.clip;
308 node.scrollbar = s.scrollbar;
309
310 if has_children {
311 let shrink = Shrink {
312 w: explicit_w.is_none(),
313 h: explicit_h.is_none(),
314 max_w,
315 max_h,
316 };
317 layout_children(w, node, Some(shrink));
318 }
319}
320
321fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
324 node.rect.x = x;
325 node.rect.y = y;
326 node.content_h = node.rect.h;
327 if is_container(w) {
328 layout_children(w, node, None);
329 }
330}
331
332fn is_container(w: &Widget) -> bool {
334 !w.children.is_empty() || matches!(w.kind, WidgetKind::Grid(_))
335}
336
337struct Shrink {
340 w: bool,
341 h: bool,
342 max_w: f32,
343 max_h: f32,
344}
345
346fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>) {
349 if let WidgetKind::Grid(spec) = &w.kind {
350 return grid::layout(w, spec, node, shrink);
351 }
352 let s = &w.style;
353 let Rect { x, y, w: ww, h: hh } = node.rect;
354
355 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
356 FlexDir::Row
357 } else {
358 FlexDir::Column
359 };
360 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
361 let content_w = if bounded(ww) {
362 (ww - s.pad[1] - s.pad[3] - bar_w).max(0.0)
363 } else {
364 UNBOUNDED
365 };
366 let content_h = if bounded(hh) {
367 (hh - s.pad[0] - s.pad[2]).max(0.0)
368 } else {
369 UNBOUNDED
370 };
371 let (main_bounded, cross_bounded) = match dir {
372 FlexDir::Row => (bounded(content_w), bounded(content_h)),
373 FlexDir::Column => (bounded(content_h), bounded(content_w)),
374 };
375 let cross_shrinks = shrink.as_ref().is_some_and(|f| match dir {
378 FlexDir::Row => f.h,
379 FlexDir::Column => f.w,
380 });
381 let self_place = |child: &Widget| -> Option<Option<Align>> {
385 match dir {
386 FlexDir::Column => child.style.halign.map(|a| match a {
387 HAlign::Left => Some(Align::Start),
388 HAlign::Center => Some(Align::Center),
389 HAlign::Right => Some(Align::End),
390 HAlign::Stretch => None,
391 }),
392 FlexDir::Row => child.style.valign.map(|a| match a {
393 VAlign::Top => Some(Align::Start),
394 VAlign::Center => Some(Align::Center),
395 VAlign::Bottom => Some(Align::End),
396 VAlign::Stretch => None,
397 }),
398 }
399 };
400 let cross_offset = |child: &Widget, content: f32, size: f32| -> f32 {
402 if !cross_bounded || cross_shrinks {
403 return 0.0;
404 }
405 match self_place(child).flatten().unwrap_or(s.align) {
406 Align::Center => (content - size) / 2.0,
407 Align::End => content - size,
408 Align::Start => 0.0,
409 }
410 };
411 let main_m = |c: &Widget| -> (f32, f32) {
413 let m = c.style.margin;
414 match dir {
415 FlexDir::Row => (m[3], m[1] + m[3]),
416 FlexDir::Column => (m[0], m[0] + m[2]),
417 }
418 };
419 let cross_m = |c: &Widget| -> (f32, f32) {
420 let m = c.style.margin;
421 match dir {
422 FlexDir::Row => (m[0], m[0] + m[2]),
423 FlexDir::Column => (m[3], m[1] + m[3]),
424 }
425 };
426
427 let effective_flex = |child: &Widget| -> f32 {
430 if child.style.dock == Dock::Fill {
431 child.style.flex.max(1.0)
432 } else {
433 child.style.flex
434 }
435 };
436 let in_flow = |child: &Widget| -> bool {
438 child.visible
439 && child.style.pos.is_none()
440 && !matches!(
441 (dir, child.style.dock),
442 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
443 )
444 };
445 let stretches = |child: &Widget| -> bool {
446 let dock = child.style.dock;
447 let auto_cross = match dir {
448 FlexDir::Row => child.style.h.is_auto(),
449 FlexDir::Column => child.style.w.is_auto(),
450 };
451 cross_bounded
452 && (match dir {
453 FlexDir::Row => matches!(dock, Dock::Fill | Dock::Left | Dock::Right),
454 FlexDir::Column => matches!(dock, Dock::Fill | Dock::Top | Dock::Bottom),
455 } || (auto_cross && self_place(child) == Some(None)))
456 };
457
458 let mut child_nodes: Vec<LayoutNode> = Vec::with_capacity(w.children.len());
460 let mut total_flex: f32 = 0.0;
461 let mut used_main: f32 = 0.0;
462
463 for child in &w.children {
464 let mut cn = new_node(child);
465 if !child.visible {
467 cn.rect = Rect {
468 x: x + s.pad[3],
469 y: y + s.pad[0],
470 w: 0.0,
471 h: 0.0,
472 };
473 child_nodes.push(cn);
474 continue;
475 }
476 let m = child.style.margin;
477 if let Some((px, py)) = child.style.pos {
481 let mw = less(content_w, px + m[1] + m[3]);
482 let mh = less(content_h, py + m[0] + m[2]);
483 layout_widget(child, &mut cn, 0.0, 0.0, mw, mh, content_w, content_h);
484 child_nodes.push(cn);
485 continue;
486 }
487 let (cmw, cmh) = match (dir, child.style.dock) {
490 (_, Dock::Fill) => (content_w, content_h),
491 (FlexDir::Row, _) => (UNBOUNDED, content_h),
492 (FlexDir::Column, _) => (content_w, UNBOUNDED),
493 };
494 let (cmw, cmh) = (less(cmw, m[1] + m[3]), less(cmh, m[0] + m[2]));
495 if dir == FlexDir::Column
496 && child.style.dock != Dock::Fill
497 && matches!(self_place(child), Some(Some(_)))
498 {
499 measure_aligned(child, &mut cn, cmw, cmh, content_w, content_h);
500 } else {
501 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh, content_w, content_h);
502 }
503 let ef = effective_flex(child);
504 if ef > 0.0 && main_bounded && in_flow(child) {
509 match dir {
510 FlexDir::Row if child.style.w.is_auto() => cn.rect.w = child.style.min_w,
511 FlexDir::Column if child.style.h.is_auto() => cn.rect.h = child.style.min_h,
512 _ => {}
513 }
514 }
515 used_main += main_m(child).1
518 + match dir {
519 FlexDir::Row => cn.rect.w,
520 FlexDir::Column => cn.rect.h,
521 };
522 if in_flow(child) {
523 total_flex += ef;
524 }
525 child_nodes.push(cn);
526 }
527 let placed = w
528 .children
529 .iter()
530 .filter(|c| c.visible && c.style.pos.is_none())
531 .count();
532 used_main += s.gap * (placed.saturating_sub(1) as f32);
533
534 let available = if main_bounded {
536 (match dir {
537 FlexDir::Row => content_w,
538 FlexDir::Column => content_h,
539 }) - used_main
540 } else {
541 0.0
542 };
543 let mut pos = match dir {
544 FlexDir::Row => s.pad[3],
545 FlexDir::Column => s.pad[0],
546 };
547
548 for (i, child) in w.children.iter().enumerate() {
550 if !in_flow(child) {
551 continue;
552 }
553 let cn = &mut child_nodes[i];
554 let ef = effective_flex(child);
555 let grow = if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
556 available * ef / total_flex
557 } else {
558 0.0
559 };
560 let stretch = stretches(child);
561 let (ml, mt) = main_m(child);
562 let (cl, ct) = cross_m(child);
563 let (cx, cy) = if dir == FlexDir::Row {
564 cn.rect.w += grow;
565 if stretch {
566 cn.rect.h = (content_h - ct).max(0.0);
567 }
568 let p = (
569 x + pos + ml,
570 y + s.pad[0] + cl + cross_offset(child, content_h, cn.rect.h + ct),
571 );
572 pos += cn.rect.w + mt + s.gap;
573 p
574 } else {
575 cn.rect.h += grow;
576 if stretch {
577 cn.rect.w = (content_w - ct).max(0.0);
578 }
579 let p = (
580 x + s.pad[3] + cl + cross_offset(child, content_w, cn.rect.w + ct),
581 y + pos + ml,
582 );
583 pos += cn.rect.h + mt + s.gap;
584 p
585 };
586 arrange(child, cn, cx, cy);
587 }
588
589 let mut rpos = if main_bounded {
593 match dir {
594 FlexDir::Row => x + s.pad[3] + content_w,
595 FlexDir::Column => y + s.pad[0] + content_h,
596 }
597 } else {
598 let docked: Vec<f32> = w
599 .children
600 .iter()
601 .zip(&child_nodes)
602 .filter(|(c, _)| c.visible && !in_flow(c) && c.style.pos.is_none())
603 .map(|(c, n)| {
604 main_m(c).1
605 + match dir {
606 FlexDir::Row => n.rect.w,
607 FlexDir::Column => n.rect.h,
608 }
609 })
610 .collect();
611 let span: f32 =
612 docked.iter().sum::<f32>() + s.gap * (docked.len().saturating_sub(1) as f32);
613 (match dir {
614 FlexDir::Row => x,
615 FlexDir::Column => y,
616 }) + pos
617 + span
618 };
619 for (i, child) in w.children.iter().enumerate() {
620 if !child.visible || in_flow(child) || child.style.pos.is_some() {
621 continue;
622 }
623 let cn = &mut child_nodes[i];
624 let stretch = stretches(child);
625 let (ml, mt) = main_m(child);
626 let (cl, ct) = cross_m(child);
627 rpos -= mt - ml;
629 let (cx, cy) = if dir == FlexDir::Row {
630 if stretch {
631 cn.rect.h = (content_h - ct).max(0.0);
632 }
633 rpos -= cn.rect.w;
634 (
635 rpos,
636 y + s.pad[0] + cl + cross_offset(child, content_h, cn.rect.h + ct),
637 )
638 } else {
639 if stretch {
640 cn.rect.w = (content_w - ct).max(0.0);
641 }
642 rpos -= cn.rect.h;
643 (
644 x + s.pad[3] + cl + cross_offset(child, content_w, cn.rect.w + ct),
645 rpos,
646 )
647 };
648 rpos -= ml + s.gap;
649 arrange(child, cn, cx, cy);
650 }
651
652 for (i, child) in w.children.iter().enumerate() {
654 let Some((px, py)) = child.style.pos else {
655 continue;
656 };
657 if !child.visible {
658 continue;
659 }
660 let m = child.style.margin;
661 let cn = &mut child_nodes[i];
662 arrange(
663 child,
664 cn,
665 x + s.pad[3] + px + m[3],
666 y + s.pad[0] + py + m[0],
667 );
668 }
669
670 finish(w, node, child_nodes, shrink, bar_w);
671}
672
673fn measure_aligned(
677 w: &Widget,
678 node: &mut LayoutNode,
679 max_w: f32,
680 max_h: f32,
681 basis_w: f32,
682 basis_h: f32,
683) {
684 if bounded(max_w) && w.style.w.is_auto() {
685 layout_widget(w, node, 0.0, 0.0, UNBOUNDED, max_h, basis_w, basis_h);
686 if node.rect.w <= max_w {
687 return;
688 }
689 }
690 layout_widget(w, node, 0.0, 0.0, max_w, max_h, basis_w, basis_h);
691}
692
693fn vis_margin(c: &Widget, side: usize) -> f32 {
695 if c.visible {
696 c.style.margin[side]
697 } else {
698 0.0
699 }
700}
701
702fn less(v: f32, by: f32) -> f32 {
704 if bounded(v) {
705 (v - by).max(0.0)
706 } else {
707 UNBOUNDED
708 }
709}
710
711fn finish(
714 w: &Widget,
715 node: &mut LayoutNode,
716 mut child_nodes: Vec<LayoutNode>,
717 shrink: Option<Shrink>,
718 bar_w: f32,
719) {
720 let s = &w.style;
721 let Rect { x, y, .. } = node.rect;
722 let extent = |f: &dyn Fn(&Widget, &LayoutNode) -> f32| -> f32 {
723 w.children
724 .iter()
725 .zip(&child_nodes)
726 .map(|(c, n)| f(c, n))
727 .fold(0.0f32, f32::max)
728 };
729 let ch = extent(&|c, n| n.rect.y - y + n.rect.h + vis_margin(c, 2));
730 if let Some(fit) = shrink {
731 if fit.w {
732 let cw = extent(&|c, n| n.rect.x - x + n.rect.w + vis_margin(c, 1));
733 node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
734 }
735 if fit.h {
736 node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
737 }
738 }
739
740 node.content_h = ch + s.pad[2];
743 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
744 if node.scroll_y > 0.0 {
745 for c in &mut child_nodes {
746 c.translate_y(-node.scroll_y);
747 }
748 }
749
750 node.children = child_nodes;
751}
752
753pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
757 if !node.visible || !node.rect.contains(mx, my) {
758 return None;
759 }
760 for child in node.children.iter().rev() {
761 if let Some(hit) = hit_test(child, mx, my) {
762 return Some(hit);
763 }
764 }
765 if node.is_clickable() {
766 Some(node)
767 } else {
768 None
769 }
770}
771
772pub fn hit_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
775 fn walk(node: &LayoutNode, mx: f32, my: f32, path: &mut Vec<u16>) -> bool {
776 if !node.visible || !node.rect.contains(mx, my) {
777 return false;
778 }
779 for (i, child) in node.children.iter().enumerate().rev() {
780 path.push(i as u16);
781 if walk(child, mx, my, path) {
782 return true;
783 }
784 path.pop();
785 }
786 node.is_clickable()
787 }
788 let mut path = Vec::new();
789 walk(node, mx, my, &mut path).then_some(path)
790}
791
792pub fn deepest_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
795 if !node.visible || !node.rect.contains(mx, my) {
796 return None;
797 }
798 let mut path = Vec::new();
799 let mut n = node;
800 'outer: loop {
801 for (i, child) in n.children.iter().enumerate().rev() {
802 if child.visible && child.rect.contains(mx, my) {
803 path.push(i as u16);
804 n = child;
805 continue 'outer;
806 }
807 }
808 return Some(path);
809 }
810}
811
812pub fn node_at<'a>(node: &'a LayoutNode, path: &[u16]) -> Option<&'a LayoutNode> {
814 let mut n = node;
815 for &i in path {
816 n = n.children.get(i as usize)?;
817 }
818 Some(n)
819}
820
821pub fn node_at_mut<'a>(node: &'a mut LayoutNode, path: &[u16]) -> Option<&'a mut LayoutNode> {
823 let mut n = node;
824 for &i in path {
825 n = n.children.get_mut(i as usize)?;
826 }
827 Some(n)
828}
829
830impl LayoutNode {
831 pub fn is_clickable(&self) -> bool {
834 self.visible && self.enabled && (self.action.is_some() || self.on_click.is_some())
835 }
836}
837
838pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
843 if node.id.as_deref() == Some(id) {
844 return Some(node.rect);
845 }
846 node.children.iter().find_map(|c| find_by_id(c, id))
847}
848
849pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
852 if node.id.as_deref() == Some(id) {
853 return Some(node);
854 }
855 node.children.iter().find_map(|c| find_node(c, id))
856}
857
858pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
860 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
861 for child in &mut node.children {
862 set_focus(child, focused_id);
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869 use crate::widget::{self, Units};
870
871 #[test]
872 fn positioned_children_leave_the_flow() {
873 let root = widget::panel(FlexDir::Column)
874 .w(200.0)
875 .h(100.0)
876 .padding(4.0, 4.0, 4.0, 4.0)
877 .child(widget::spacer().min_w(10.0).min_h(10.0))
878 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
879 .child(widget::spacer().min_w(10.0).min_h(10.0));
880 let node = compute(&root, 400.0, 400.0);
881 let r = node.children[1].rect;
882 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
883 assert_eq!(node.children[0].rect.y, 4.0);
885 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
886 }
887
888 #[test]
889 fn positioned_container_lays_out_its_children_in_place() {
890 let root = widget::panel(FlexDir::Column)
891 .w(Units::Percent(100.0))
892 .h(Units::Percent(100.0))
893 .child(
894 widget::panel(FlexDir::Row)
895 .w(100.0)
896 .h(20.0)
897 .gap(0.0)
898 .at(10.0, 20.0)
899 .child(widget::spacer().min_w(30.0).min_h(5.0))
900 .child(widget::spacer().min_w(30.0).min_h(5.0)),
901 );
902 let node = compute(&root, 320.0, 240.0);
903 let panel = &node.children[0];
904 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
905 assert_eq!(panel.children[1].rect.x, 40.0);
906 assert_eq!(panel.children[1].rect.y, 20.0);
907 }
908
909 #[test]
910 fn auto_sized_parent_grows_to_fit_positioned_children() {
911 let root = widget::panel(FlexDir::Column)
912 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
913 let node = compute(&root, 300.0, 300.0);
914 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
915 }
916
917 fn pct(v: f32) -> Units {
918 Units::Percent(v)
919 }
920
921 fn r(n: &LayoutNode) -> (f32, f32, f32, f32) {
922 (n.rect.x, n.rect.y, n.rect.w, n.rect.h)
923 }
924
925 #[test]
926 fn percent_width_in_a_row_resolves_once_against_the_content_box() {
927 let root = widget::panel(FlexDir::Row)
928 .w(400.0)
929 .h(100.0)
930 .padding(5.0, 10.0, 5.0, 10.0)
931 .child(
932 widget::panel(FlexDir::Column)
933 .w(pct(50.0))
934 .child(widget::spacer().min_w(10.0).min_h(10.0)),
935 )
936 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)));
937 let node = compute(&root, 800.0, 600.0);
938 assert_eq!(r(&node.children[0]).2, 190.0);
940 assert_eq!(r(&node.children[1]), (10.0 + 190.0 + 2.0, 5.0, 95.0, 45.0));
941 }
942
943 #[test]
944 fn percent_sizes_in_a_column() {
945 let root = widget::panel(FlexDir::Column)
946 .w(200.0)
947 .h(300.0)
948 .padding(10.0, 0.0, 10.0, 0.0)
949 .gap(0.0)
950 .child(
951 widget::panel(FlexDir::Row)
952 .w(pct(50.0))
953 .h(pct(50.0))
954 .child(widget::spacer()),
955 )
956 .child(widget::spacer().w(pct(100.0)).h(pct(10.0)));
957 let node = compute(&root, 800.0, 600.0);
958 assert_eq!(r(&node.children[0]), (0.0, 10.0, 100.0, 140.0));
959 assert_eq!(r(&node.children[1]), (0.0, 150.0, 200.0, 28.0));
960 }
961
962 #[test]
963 fn nested_percents_compound_per_level_only() {
964 let root = widget::panel(FlexDir::Row).w(400.0).h(200.0).child(
965 widget::panel(FlexDir::Column)
966 .w(pct(50.0))
967 .h(pct(100.0))
968 .child(
969 widget::panel(FlexDir::Row)
970 .w(pct(50.0))
971 .h(pct(50.0))
972 .child(widget::spacer().w(pct(50.0)).h(pct(50.0))),
973 ),
974 );
975 let node = compute(&root, 400.0, 200.0);
976 let a = &node.children[0];
977 let b = &a.children[0];
978 let c = &b.children[0];
979 assert_eq!(r(a), (0.0, 0.0, 200.0, 200.0));
980 assert_eq!(r(b), (0.0, 0.0, 100.0, 100.0));
981 assert_eq!(r(c), (0.0, 0.0, 50.0, 50.0));
982 }
983
984 #[test]
985 fn percent_resolves_against_the_scrollbar_free_box() {
986 let root = widget::scroll_view()
987 .w(106.0)
988 .h(50.0)
989 .child(widget::spacer().w(pct(50.0)).h(10.0));
990 let node = compute(&root, 400.0, 400.0);
991 assert_eq!(node.children[0].rect.w, 50.0);
992 }
993
994 #[test]
995 fn grow_shares_free_space_with_percent_siblings() {
996 let root = widget::panel(FlexDir::Row)
997 .w(300.0)
998 .h(50.0)
999 .gap(4.0)
1000 .child(widget::spacer().w(pct(50.0)))
1001 .child(
1002 widget::panel(FlexDir::Column)
1003 .flex(1.0)
1004 .child(widget::spacer()),
1005 )
1006 .child(widget::spacer().w(pct(10.0)));
1007 let node = compute(&root, 300.0, 50.0);
1008 assert_eq!(r(&node.children[0]).2, 150.0);
1010 assert_eq!(r(&node.children[1]), (154.0, 0.0, 112.0, 4.0));
1011 assert_eq!(r(&node.children[2]), (270.0, 0.0, 30.0, 4.0));
1012 }
1013
1014 #[test]
1015 fn grown_panel_keeps_its_size_and_lays_children_out_against_it() {
1016 let root = widget::panel(FlexDir::Column)
1017 .w(200.0)
1018 .h(200.0)
1019 .gap(0.0)
1020 .child(widget::spacer().min_h(20.0))
1021 .child(
1022 widget::panel(FlexDir::Row)
1023 .flex(1.0)
1024 .w(pct(100.0))
1025 .gap(0.0)
1026 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)))
1027 .child(
1028 widget::panel(FlexDir::Column)
1029 .flex(1.0)
1030 .child(widget::spacer()),
1031 )
1032 .child(widget::spacer().dock(Dock::Right).min_w(10.0)),
1033 );
1034 let node = compute(&root, 400.0, 400.0);
1035 let grown = &node.children[1];
1036 assert_eq!(r(grown), (0.0, 20.0, 200.0, 180.0));
1037 assert_eq!(r(&grown.children[0]), (0.0, 20.0, 50.0, 90.0));
1038 assert_eq!(r(&grown.children[1]).2, 200.0 - 50.0 - 10.0);
1040 assert_eq!(r(&grown.children[2]), (190.0, 20.0, 10.0, 180.0));
1042 }
1043
1044 #[test]
1045 fn long_list_deep_inside_a_grown_row_does_not_push_siblings_out() {
1046 let mut list = widget::scroll_view().flex(1.0).w(pct(100.0));
1049 for _ in 0..60 {
1050 list = list.child(widget::spacer().h(25.0));
1051 }
1052 let root = widget::panel(FlexDir::Column)
1053 .w(400.0)
1054 .h(252.0)
1055 .gap(0.0)
1056 .child(widget::spacer().h(20.0))
1057 .child(
1058 widget::panel(FlexDir::Row).flex(1.0).w(pct(100.0)).child(
1059 widget::panel(FlexDir::Column)
1060 .w(pct(50.0))
1061 .h(pct(100.0))
1062 .child(widget::spacer().h(16.0))
1063 .child(list),
1064 ),
1065 )
1066 .child(widget::spacer().h(26.0));
1067 let node = compute(&root, 400.0, 252.0);
1068 assert_eq!(r(&node.children[1]), (0.0, 20.0, 400.0, 206.0));
1069 let footer = r(&node.children[2]);
1070 assert_eq!((footer.1, footer.3), (226.0, 26.0));
1071 let column = &node.children[1].children[0];
1072 assert_eq!(column.rect.h, 206.0);
1073 let list = &column.children[1];
1074 assert!(list.rect.y + list.rect.h <= 226.0, "{:?}", list.rect);
1075 assert!(list.content_h > list.rect.h);
1076 }
1077
1078 #[test]
1079 fn grown_column_panel_fills_its_parent_and_children_grow_too() {
1080 let root = widget::panel(FlexDir::Column)
1081 .w(100.0)
1082 .h(100.0)
1083 .gap(0.0)
1084 .child(
1085 widget::panel(FlexDir::Column)
1086 .flex(1.0)
1087 .gap(0.0)
1088 .child(widget::spacer().flex(1.0)),
1089 );
1090 let node = compute(&root, 100.0, 100.0);
1091 assert_eq!(node.children[0].rect.h, 100.0);
1092 assert_eq!(node.children[0].children[0].rect.h, 100.0);
1093 }
1094
1095 #[test]
1096 fn empty_auto_panel_in_a_row_takes_its_min_width() {
1097 let root = widget::panel(FlexDir::Row)
1098 .w(100.0)
1099 .h(20.0)
1100 .child(widget::panel(FlexDir::Column).min_w(7.0))
1101 .child(widget::spacer());
1102 let node = compute(&root, 100.0, 20.0);
1103 assert_eq!(node.children[0].rect.w, 7.0);
1104 assert_eq!(node.children[1].rect.x, 9.0);
1105 }
1106
1107 #[test]
1108 fn padding_counts_once_in_auto_size() {
1109 let root = widget::panel(FlexDir::Column)
1110 .padding(3.0, 4.0, 5.0, 6.0)
1111 .child(widget::spacer().min_w(10.0).min_h(10.0));
1112 let node = compute(&root, 300.0, 300.0);
1113 assert_eq!((node.rect.w, node.rect.h), (20.0, 18.0));
1114 assert_eq!(
1115 (node.children[0].rect.x, node.children[0].rect.y),
1116 (6.0, 3.0)
1117 );
1118 }
1119
1120 #[test]
1121 fn auto_row_in_a_column_measures_to_content() {
1122 let root = widget::panel(FlexDir::Column).w(200.0).h(200.0).child(
1124 widget::panel(FlexDir::Row)
1125 .align(Align::Center)
1126 .gap(0.0)
1127 .child(widget::spacer().min_w(10.0).min_h(10.0))
1128 .child(widget::spacer().min_w(10.0).min_h(20.0))
1129 .child(widget::spacer().min_w(5.0).dock(Dock::Right)),
1130 );
1131 let node = compute(&root, 200.0, 200.0);
1132 let row = &node.children[0];
1133 assert_eq!(row.rect.h, 20.0);
1134 assert_eq!(row.children[0].rect.y, 5.0);
1135 assert_eq!(row.children[1].rect.y, 0.0);
1136 }
1137
1138 #[test]
1139 fn positioned_auto_row_measures_to_content() {
1140 let pill = widget::panel(FlexDir::Row)
1143 .padding(1.0, 1.0, 1.0, 1.0)
1144 .child(
1145 widget::panel(FlexDir::Row)
1146 .align(Align::Center)
1147 .child(widget::spacer().min_w(10.0).min_h(9.0)),
1148 );
1149 let root = widget::panel(FlexDir::Column)
1150 .w(500.0)
1151 .h(500.0)
1152 .child(pill.at(3.0, 4.0));
1153 let node = compute(&root, 500.0, 500.0);
1154 let r = node.children[0].rect;
1155 assert_eq!((r.x, r.y, r.w, r.h), (3.0, 4.0, 12.0, 11.0));
1156 assert_eq!(node.children[0].children[0].children[0].rect.y, 5.0);
1157 }
1158
1159 #[test]
1160 fn row_with_right_dock_measured_on_unbounded_axis() {
1161 let root = widget::panel(FlexDir::Row).w(300.0).h(20.0).child(
1164 widget::panel(FlexDir::Row)
1165 .gap(1.0)
1166 .child(widget::spacer().min_w(10.0))
1167 .child(widget::spacer().min_w(5.0).dock(Dock::Right))
1168 .child(widget::spacer().min_w(7.0).dock(Dock::Right)),
1169 );
1170 let node = compute(&root, 300.0, 20.0);
1171 let inner = &node.children[0];
1172 assert_eq!(inner.rect.w, 10.0 + 1.0 + 7.0 + 1.0 + 5.0);
1173 assert_eq!(inner.children[1].rect.x, 19.0);
1174 assert_eq!(inner.children[2].rect.x, 11.0);
1175 }
1176
1177 #[test]
1178 fn align_center_and_end_in_both_directions() {
1179 let col = widget::panel(FlexDir::Column)
1180 .w(100.0)
1181 .h(50.0)
1182 .align(Align::Center)
1183 .child(widget::spacer().min_w(20.0).min_h(5.0));
1184 assert_eq!(compute(&col, 100.0, 50.0).children[0].rect.x, 40.0);
1185 let row = widget::panel(FlexDir::Row)
1186 .w(100.0)
1187 .h(50.0)
1188 .padding(10.0, 0.0, 0.0, 0.0)
1189 .align(Align::End)
1190 .child(widget::spacer().min_w(20.0).h(pct(20.0)));
1191 assert_eq!(compute(&row, 100.0, 50.0).children[0].rect.y, 10.0 + 32.0);
1193 }
1194
1195 #[test]
1196 fn positioned_children_with_percent_sizes() {
1197 let root = widget::panel(FlexDir::Column)
1198 .w(200.0)
1199 .h(100.0)
1200 .padding(10.0, 10.0, 10.0, 10.0)
1201 .child(widget::spacer().w(pct(50.0)).h(pct(50.0)).at(20.0, 10.0))
1202 .child(
1203 widget::panel(FlexDir::Row)
1204 .w(pct(100.0))
1205 .h(pct(25.0))
1206 .at(0.0, 60.0)
1207 .gap(0.0)
1208 .child(widget::spacer().w(pct(50.0)))
1209 .child(widget::spacer().flex(1.0)),
1210 );
1211 let node = compute(&root, 400.0, 400.0);
1212 assert_eq!(r(&node.children[0]), (30.0, 20.0, 90.0, 40.0));
1214 let p = &node.children[1];
1217 assert_eq!(r(p), (10.0, 70.0, 180.0, 20.0));
1218 assert_eq!(r(&p.children[0]), (10.0, 70.0, 90.0, 4.0));
1219 assert_eq!(r(&p.children[1]), (100.0, 70.0, 90.0, 4.0));
1220 }
1221
1222 #[test]
1223 fn growing_scroll_view_fills_free_space_and_scrolls_the_rest() {
1224 let list = widget::scroll_view()
1225 .id("list")
1226 .flex(1.0)
1227 .w(pct(100.0))
1228 .gap(0.0)
1229 .padding(2.0, 2.0, 2.0, 2.0)
1230 .scroll_y(1000.0)
1231 .children((0..10).map(|_| widget::spacer().w(pct(100.0)).min_h(20.0)));
1232 let root = widget::panel(FlexDir::Column)
1233 .w(100.0)
1234 .h(120.0)
1235 .gap(0.0)
1236 .child(widget::spacer().min_h(20.0))
1237 .child(list);
1238 let node = compute(&root, 100.0, 120.0);
1239 let v = find_node(&node, "list").unwrap();
1240 assert_eq!(r(v), (0.0, 20.0, 100.0, 100.0));
1241 assert_eq!(v.content_h, 204.0);
1242 assert_eq!(v.scroll_y, 104.0);
1243 assert_eq!(v.children[0].rect.w, 100.0 - 4.0 - SCROLLBAR_W);
1245 let last = v.children[9].rect;
1247 assert_eq!(last.y + last.h, 20.0 + 100.0 - 2.0);
1248 assert!(v.scrollbar_rects().is_some());
1249 }
1250
1251 #[test]
1252 fn label_wraps_at_its_explicit_width() {
1253 let root = widget::panel(FlexDir::Column)
1254 .w(300.0)
1255 .child(widget::label("aaaa bbbb cccc").w(40.0));
1256 let node = compute(&root, 300.0, 300.0);
1257 let l = node.children[0].rect;
1258 assert_eq!(l.w, 40.0);
1259 assert!(l.h > text::LINE_H, "wrapped onto several lines: {l:?}");
1260 }
1261
1262 #[test]
1263 fn percent_caps_at_the_cross_axis_and_ignores_unbounded_basis() {
1264 let root = widget::panel(FlexDir::Column)
1266 .w(100.0)
1267 .h(100.0)
1268 .child(widget::spacer().w(pct(150.0)));
1269 assert_eq!(compute(&root, 100.0, 100.0).children[0].rect.w, 100.0);
1270 let root = widget::panel(FlexDir::Row).w(200.0).h(20.0).child(
1273 widget::panel(FlexDir::Row)
1274 .gap(0.0)
1275 .child(widget::spacer().min_w(40.0))
1276 .child(widget::spacer().w(pct(50.0))),
1277 );
1278 let node = compute(&root, 200.0, 20.0);
1279 let inner = &node.children[0];
1280 assert_eq!(inner.rect.w, 44.0);
1281 assert_eq!(inner.children[1].rect.w, 22.0);
1282 }
1283
1284 #[test]
1285 fn find_mut_and_set_text() {
1286 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
1287 assert!(root.find_mut("title").unwrap().set_text("b"));
1288 match &root.children[0].kind {
1289 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
1290 _ => unreachable!(),
1291 }
1292 assert!(root.find_mut("missing").is_none());
1293 }
1294}