use abstracttui::base::{Rect, Size};
use abstracttui::layout::{solve, Align, Dimension, Edges, LayoutId, LayoutTree, Style, Track};
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"
);
}
fn wrapped_rows(chars: i32, width: i32) -> i32 {
let w = width.max(1);
((chars + w - 1) / w).max(1)
}
fn text_leaf(tree: &mut LayoutTree, style: Style, chars: i32) -> LayoutId {
tree.add_leaf(
style,
Box::new(move |inner: Size| Size::new(inner.w, wrapped_rows(chars, inner.w))),
)
}
#[test]
fn content_sized_children_are_solved_big_enough_for_their_own_content() {
let mut rng = Rng::new(0x00C0_17E5);
let mut load_bearing = 0usize;
for case in 0..400 {
let n = 1 + rng.below(4);
let w = 8 + rng.below(60) as i32;
let gap = rng.below(3) as i32;
let pad = rng.below(3) as i32;
let wrap = rng.below(2) == 0;
let align = match rng.below(3) {
0 => Align::Start,
1 => Align::Center,
_ => Align::Stretch,
};
let mut root_style = Style::column()
.gap(gap)
.padding(Edges::all(pad))
.align_items(align);
if wrap {
root_style = root_style.wrap();
}
let mut tree = LayoutTree::new();
let root = tree.add(root_style);
let mut kids: Vec<(LayoutId, i32, i32)> = Vec::new();
for _ in 0..n {
let chars = 1 + rng.below(200) as i32;
let mx = rng.below(4) as i32;
let my = rng.below(3) as i32;
let id = text_leaf(&mut tree, Style::default().margin(Edges::hv(mx, my)), chars);
tree.add_child(root, id);
kids.push((id, chars, mx));
}
let container = Rect::new(0, 0, w, 4000);
solve(&mut tree, root, container);
let ctx =
format!("case {case}: w={w} n={n} gap={gap} pad={pad} wrap={wrap} align={align:?}");
for (id, chars, mx) in &kids {
let r = tree.rect(*id);
let needs = wrapped_rows(*chars, r.w);
if *mx > 0 && needs > 1 {
load_bearing += 1;
}
assert!(
r.h >= needs,
"{ctx}: leaf of {chars} chars solved to {:?} — {} columns \
wraps to {needs} rows, but it was given {}",
r,
r.w,
r.h
);
}
}
assert!(
load_bearing >= 200,
"population went vacuous: only {load_bearing} leaves both carried \
a side margin and wrapped past one row"
);
}
#[test]
fn content_sized_row_children_are_solved_big_enough_for_their_own_content() {
let mut rng = Rng::new(0x00B0_5EED);
let mut load_bearing = 0usize;
for case in 0..400 {
let n = 1 + rng.below(4);
let w = 8 + rng.below(80) as i32;
let gap = rng.below(3) as i32;
let pad = rng.below(3) as i32;
let align = match rng.below(3) {
0 => Align::Start,
1 => Align::Center,
_ => Align::End,
};
let root_style = Style::row()
.gap(gap)
.padding(Edges::all(pad))
.align_items(align);
let mut tree = LayoutTree::new();
let root = tree.add(root_style);
let mut kids: Vec<(LayoutId, i32, i32, i32)> = Vec::new();
for _ in 0..n {
let chars = 1 + rng.below(200) as i32;
let mx = rng.below(4) as i32;
let my = rng.below(3) as i32;
let mut s = Style::default().margin(Edges::hv(mx, my));
match rng.below(3) {
0 => s = s.grow(1.0),
1 => s = s.shrink(1.0),
_ => {}
}
let id = text_leaf(&mut tree, s, chars);
tree.add_child(root, id);
kids.push((id, chars, mx, my));
}
let container = Rect::new(0, 0, w, 4000);
solve(&mut tree, root, container);
let ctx = format!("case {case}: w={w} n={n} gap={gap} pad={pad}");
let offered = w - 2 * pad; for (id, chars, mx, my) in &kids {
let r = tree.rect(*id);
let needs = wrapped_rows(*chars, r.w);
let cross_avail = container.h - 2 * pad - 2 * my;
let stretched = r.h >= cross_avail;
if needs > 1 && r.w != (offered - 2 * *mx).max(0) && !stretched {
load_bearing += 1;
}
assert!(
r.h >= needs,
"{ctx}: row leaf of {chars} chars solved to {:?} — {} \
columns wraps to {needs} rows, but it was given {}",
r,
r.w,
r.h
);
}
}
assert!(
load_bearing >= 400,
"population went vacuous: only {load_bearing} row leaves wrapped \
past one row AND were moved off the estimated width by flex \
distribution — without those the ordering cycle is never exercised"
);
}
fn para_leaf(tree: &mut LayoutTree, style: Style, chars: i32, pref: i32) -> LayoutId {
tree.add_leaf(
style,
Box::new(move |inner: Size| {
let w = pref.min(inner.w).max(1);
Size::new(w, wrapped_rows(chars, w))
}),
)
}
#[test]
fn content_sized_wrap_children_are_solved_big_enough_for_their_own_content() {
let mut rng = Rng::new(0x00C5_1A7E);
let mut load_bearing = 0usize;
for case in 0..400 {
let n = 2 + rng.below(5);
let w = 12 + rng.below(50) as i32;
let gap = rng.below(3) as i32;
let cross_gap = rng.below(3) as i32;
let pad = rng.below(3) as i32;
let align = match rng.below(4) {
0 => Align::Start,
1 => Align::Center,
2 => Align::End,
_ => Align::Stretch,
};
let root_style = Style::row()
.wrap()
.gap(gap)
.cross_gap(cross_gap)
.padding(Edges::all(pad))
.align_items(align);
let mut tree = LayoutTree::new();
let root = tree.add(root_style);
let mut kids: Vec<(LayoutId, i32)> = Vec::new();
for _ in 0..n {
let mx = rng.below(3) as i32;
let my = rng.below(2) as i32;
let style = Style::default().margin(Edges::hv(mx, my));
if rng.below(3) == 0 {
let id = tree.add(style.w(1 + rng.below(6) as i32).h(1));
tree.add_child(root, id);
} else {
let chars = 1 + rng.below(200) as i32;
let pref = 4 + rng.below(30) as i32;
let id = para_leaf(&mut tree, style, chars, pref);
tree.add_child(root, id);
kids.push((id, chars));
}
}
let container = Rect::new(0, 0, w, 4000);
solve(&mut tree, root, container);
let ctx = format!(
"case {case}: w={w} n={n} gap={gap} cross_gap={cross_gap} \
pad={pad} align={align:?}"
);
for (id, chars) in &kids {
let r = tree.rect(*id);
let needs = wrapped_rows(*chars, r.w);
if needs > 1 && align == Align::Stretch {
load_bearing += 1;
}
assert!(
r.h >= needs,
"{ctx}: wrapped leaf of {chars} chars solved to {:?} — {} \
columns wraps to {needs} rows, but its line gave it {}",
r,
r.w,
r.h
);
}
}
assert!(
load_bearing >= 200,
"population went vacuous: only {load_bearing} wrapped leaves both \
stretched into their line and wrapped past one row — without \
those the line-extent negotiation is never exercised"
);
}
#[test]
fn content_sized_grid_children_are_solved_big_enough_for_their_own_content() {
let mut rng = Rng::new(0x0061_1D00);
let mut load_bearing = 0usize;
for case in 0..400 {
let ncols = 1 + rng.below(4);
let cols: Vec<Track> = (0..ncols)
.map(|_| match rng.below(4) {
0 => Track::Cells(2 + rng.below(12) as i32),
1 => Track::Percent((10 + rng.below(40)) as f32 / 100.0),
2 => Track::Auto,
_ => Track::Fr(1.0 + rng.below(3) as f32),
})
.collect();
let w = 12 + rng.below(60) as i32;
let col_gap = rng.below(3) as i32;
let row_gap = rng.below(3) as i32;
let pad = rng.below(3) as i32;
let align = match rng.below(4) {
0 => Align::Start,
1 => Align::Center,
2 => Align::End,
_ => Align::Stretch,
};
let mut tree = LayoutTree::new();
let root = tree.add(
Style::default()
.grid(cols, vec![])
.gap(col_gap)
.cross_gap(row_gap)
.padding(Edges::all(pad))
.align_items(align),
);
let n = 1 + rng.below(5);
let mut kids: Vec<(LayoutId, i32)> = Vec::new();
for _ in 0..n {
let chars = 1 + rng.below(200) as i32;
let pref = 4 + rng.below(30) as i32;
let mx = rng.below(3) as i32;
let my = rng.below(2) as i32;
let mut style = Style::default().margin(Edges::hv(mx, my));
if rng.below(4) == 0 {
style = style.col_span(2);
}
let id = para_leaf(&mut tree, style, chars, pref);
tree.add_child(root, id);
kids.push((id, chars));
}
let container = Rect::new(0, 0, w, 4000);
solve(&mut tree, root, container);
let ctx = format!(
"case {case}: w={w} n={n} ncols={ncols} col_gap={col_gap} \
row_gap={row_gap} pad={pad} align={align:?}"
);
for (id, chars) in &kids {
let r = tree.rect(*id);
let needs = wrapped_rows(*chars, r.w);
if needs > 1 && r.w < w - 2 * pad {
load_bearing += 1;
}
assert!(
r.h >= needs,
"{ctx}: grid leaf of {chars} chars solved to {:?} — {} \
columns wraps to {needs} rows, but its cell gave it {}",
r,
r.w,
r.h
);
}
}
assert!(
load_bearing >= 200,
"population went vacuous: only {load_bearing} grid leaves both \
wrapped past one row and sat in a cell narrower than the \
container — without those the cell width never matters"
);
}
#[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);
}