use abstracttui::base::{Rect, Size};
use abstracttui::layout::{solve, Dimension, LayoutId, LayoutTree, Style};
use abstracttui::testing::Rng;
fn overlaps(a: Rect, b: Rect) -> bool {
let ix = a.x.max(b.x);
let iy = a.y.max(b.y);
let ir = a.right().min(b.right());
let ib = a.bottom().min(b.bottom());
ix < ir && iy < ib
}
fn assert_within(child: Rect, parent: Rect, ctx: &str) {
assert!(
child.x >= parent.x
&& child.y >= parent.y
&& child.right() <= parent.right()
&& child.bottom() <= parent.bottom(),
"{ctx}: child {child:?} escapes parent {parent:?}"
);
}
#[test]
fn flex_grow_tiles_main_axis_exactly_for_random_rows() {
let mut rng = Rng::new(0x001A_7007);
for _ in 0..400 {
let n = 1 + rng.below(6);
let w = 1 + rng.below(120) as i32;
let h = 1 + rng.below(20) as i32;
let gap = rng.below(4) as i32;
let mut tree = LayoutTree::new();
let root = tree.add(Style::row().gap(gap));
let mut ids = Vec::new();
for _ in 0..n {
let style = if rng.below(4) == 0 {
Style::default().w(1 + rng.below(8) as i32)
} else {
Style::default().grow(1.0 + rng.below(3) as f32)
};
let id = tree.add(style);
tree.add_child(root, id);
ids.push(id);
}
let container = Rect::new(0, 0, w, h);
solve(&mut tree, root, container);
let rects: Vec<Rect> = ids.iter().map(|&id| tree.rect(id)).collect();
for i in 0..rects.len() {
for j in i + 1..rects.len() {
assert!(
!overlaps(rects[i], rects[j]),
"overlap {:?} vs {:?} (w={w} gap={gap})",
rects[i],
rects[j]
);
}
assert!(
rects[i].y >= container.y && rects[i].bottom() <= container.bottom(),
"child escapes on the cross axis: {:?} in {container:?}",
rects[i]
);
}
let gaps_total = gap * (n as i32 - 1).max(0);
let widths: i32 = rects.iter().map(|r| r.w).sum();
if widths + gaps_total <= w {
for r in &rects {
assert_within(*r, container, "flex row (fits)");
}
}
}
}
#[test]
fn pure_grow_fills_container_to_the_last_cell() {
for (vertical, w, h) in [
(false, 100, 3),
(true, 4, 100),
(false, 37, 5),
(true, 6, 41),
] {
for n in 1..=7usize {
let mut tree = LayoutTree::new();
let root = tree.add(if vertical {
Style::column()
} else {
Style::row()
});
let mut ids = Vec::new();
for _ in 0..n {
let id = tree.add(Style::default().grow(1.0));
tree.add_child(root, id);
ids.push(id);
}
let container = Rect::new(0, 0, w, h);
solve(&mut tree, root, container);
let rects: Vec<Rect> = ids.iter().map(|&id| tree.rect(id)).collect();
let extent: i32 = rects.iter().map(|r| if vertical { r.h } else { r.w }).sum();
let target = if vertical { h } else { w };
assert_eq!(
extent, target,
"n={n} vertical={vertical}: not tiled ({rects:?})"
);
for pair in rects.windows(2) {
let (a, b) = (pair[0], pair[1]);
if vertical {
assert_eq!(a.bottom(), b.y, "gap in column");
} else {
assert_eq!(a.right(), b.x, "gap in row");
}
}
}
}
}
#[test]
fn wrap_breaks_lines_without_overlap_or_escape() {
let mut rng = Rng::new(0x005E_ED0F);
for _ in 0..400 {
let n = 1 + rng.below(12);
let w = 4 + rng.below(60) as i32;
let h = 4 + rng.below(30) as i32;
let gap = rng.below(3) as i32;
let cross_gap = rng.below(3) as i32;
let mut tree = LayoutTree::new();
let root = tree.add(Style::row().wrap().gap(gap).cross_gap(cross_gap));
let mut ids = Vec::new();
for _ in 0..n {
let cw = 1 + rng.below(20) as i32;
let ch = 1 + rng.below(4) as i32;
let id = tree.add(Style::default().w(cw).h(ch));
tree.add_child(root, id);
ids.push(id);
}
let container = Rect::new(0, 0, w, h);
solve(&mut tree, root, container);
let rects: Vec<(LayoutId, Rect)> = ids.iter().map(|&id| (id, tree.rect(id))).collect();
for i in 0..rects.len() {
for j in i + 1..rects.len() {
assert!(
!overlaps(rects[i].1, rects[j].1),
"wrap overlap {:?} vs {:?} (w={w} gap={gap})",
rects[i].1,
rects[j].1
);
}
assert!(rects[i].1.x >= 0, "child left of container");
assert!(
rects[i].1.right() <= w,
"child {i} exceeds width {w}: {:?}",
rects[i].1
);
}
}
}
#[test]
fn wrap_single_line_matches_unwrapped_row() {
let build = |wrap: bool| {
let mut tree = LayoutTree::new();
let root = if wrap {
Style::row().wrap().gap(1)
} else {
Style::row().gap(1)
};
let root = tree.add(root);
let mut ids = Vec::new();
for _ in 0..3 {
let id = tree.add(Style::default().w(5).h(2));
tree.add_child(root, id);
ids.push(id);
}
solve(&mut tree, root, Rect::new(0, 0, 40, 4));
ids.iter().map(|&id| tree.rect(id)).collect::<Vec<_>>()
};
assert_eq!(
build(true),
build(false),
"one-line wrap must match a plain row"
);
}
#[test]
fn percent_dimension_resolves_against_parent() {
let mut tree = LayoutTree::new();
let root = tree.add(Style::row());
let half = tree.add(
Style::default()
.width(Dimension::Percent(0.5))
.height(Dimension::Percent(1.0)),
);
tree.add_child(root, half);
solve(&mut tree, root, Rect::new(0, 0, 20, 10));
let r = tree.rect(half);
assert_eq!(r.w, 10, "50% of 20");
assert_eq!(r.h, 10, "100% of 10");
}
#[test]
fn solve_is_deterministic() {
let build = || {
let mut tree = LayoutTree::new();
let root = tree.add(Style::row().gap(2));
let ids: Vec<LayoutId> = (0..5)
.map(|i| {
let s = if i % 2 == 0 {
Style::default().grow(1.0)
} else {
Style::default().w(3)
};
let id = tree.add(s);
tree.add_child(root, id);
id
})
.collect();
solve(&mut tree, root, Rect::new(0, 0, 53, 7));
ids.iter().map(|&id| tree.rect(id)).collect::<Vec<_>>()
};
assert_eq!(build(), build(), "layout must be deterministic");
let _ = Size::new(1, 1);
}