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