use super::*;
use crate::prelude::*;
fn tui_dom() -> TuiDom {
TuiDom::new()
}
fn cascade(dom: &mut TuiDom, sheet: &Stylesheet) {
dom.cascade(sheet);
}
fn layout_rect_of(dom: &TuiDom, id: NodeId) -> LayoutRect {
dom.node(id).ext().unwrap().layout
}
fn content_rect_of(dom: &TuiDom, id: NodeId) -> LayoutRect {
dom.node(id).ext().unwrap().content_layout
}
#[test]
fn scroll_offset_clamps_when_content_shrinks() {
let mut dom = tui_dom();
let root = dom.root();
let boxed = dom.create_element("div");
dom.set_attribute(boxed, "class", "box").unwrap();
dom.append_child(root, boxed).unwrap();
for _ in 0..20 {
let r = dom.create_element("div");
dom.set_attribute(r, "class", "row").unwrap();
dom.append_child(boxed, r).unwrap();
}
let sheet = rdom_css::from_css(".box { height: 5; overflow-y: auto; } .row { height: 1; }");
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 20, 30));
if let Some(ext) = dom.node_mut(boxed).ext_mut() {
ext.scroll_y = 15;
}
dom.clear_children(boxed).unwrap();
for _ in 0..2 {
let r = dom.create_element("div");
dom.set_attribute(r, "class", "row").unwrap();
dom.append_child(boxed, r).unwrap();
}
dom.layout_dom(Rect::new(0, 0, 20, 30));
assert_eq!(
dom.node(boxed).ext().unwrap().scroll_y,
0,
"scroll offset must clamp to 0 once the content fits the viewport"
);
}
#[test]
fn percent_height_resolves_against_flex_sized_ancestor() {
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("div");
dom.set_attribute(outer, "class", "outer").unwrap();
dom.append_child(root, outer).unwrap();
let mid = dom.create_element("div");
dom.set_attribute(mid, "class", "mid").unwrap();
dom.append_child(outer, mid).unwrap();
let inner = dom.create_element("div");
dom.set_attribute(inner, "class", "inner").unwrap();
dom.append_child(mid, inner).unwrap();
let sheet = rdom_css::from_css(
".outer { height: 10; display: flex; flex-direction: column; } \
.mid { flex: 1; } \
.inner { height: 100%; }",
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 20, 12));
assert_eq!(
layout_rect_of(&dom, mid).height,
10,
"flex:1 item fills the fixed-height column container"
);
assert_eq!(
layout_rect_of(&dom, inner).height,
10,
"height:100% must resolve against the flex-sized parent (definite per Flexbox §9.8)"
);
}
#[test]
fn root_fragment_distributes_to_children() {
let mut dom = tui_dom();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
cascade(&mut dom, &Stylesheet::bare());
dom.layout_dom(Rect::new(0, 0, 20, 10));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.x, 0);
assert_eq!(la.y, 0);
assert_eq!(la.width, 20);
assert_eq!(lb.y, la.y + la.height as i32); }
#[test]
fn row_fixed_children() {
let mut dom = tui_dom();
let root = dom.root();
let container = dom.create_element("div");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(container, a).unwrap();
dom.append_child(container, b).unwrap();
dom.append_child(root, container).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.width(Size::Fixed(20))
.height(Size::Fixed(5)),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(5)))
.rule_unchecked("b", TuiStyle::new().width(Size::Fixed(7)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 20));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.width, 5);
assert_eq!(lb.width, 7);
assert_eq!(lb.x, la.x + 5); }
#[test]
fn row_with_gap() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.gap(2),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(3)))
.rule_unchecked("b", TuiStyle::new().width(Size::Fixed(4)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 10));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.width, 3);
assert_eq!(lb.x, la.x + 3 + 2); }
#[test]
fn row_flex_distributes_remaining() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
let fx = dom.create_element("fx");
dom.append_child(c, a).unwrap();
dom.append_child(c, fx).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(5)))
.rule_unchecked("fx", TuiStyle::new().width(Size::Flex(1)))
.rule_unchecked("b", TuiStyle::new().width(Size::Fixed(5)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 5));
assert_eq!(layout_rect_of(&dom, a).width, 5);
assert_eq!(layout_rect_of(&dom, fx).width, 40);
assert_eq!(layout_rect_of(&dom, b).width, 5);
}
#[test]
fn row_flex_weights_distribute_proportionally() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Flex(1)))
.rule_unchecked("b", TuiStyle::new().width(Size::Flex(3)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 5));
assert_eq!(layout_rect_of(&dom, a).width, 10);
assert_eq!(layout_rect_of(&dom, b).width, 30);
}
#[test]
fn column_stacks_vertically() {
let mut dom = tui_dom();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(3)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(2)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 20, 10));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.y, 0);
assert_eq!(la.height, 3);
assert_eq!(lb.y, la.y + 3);
assert_eq!(lb.height, 2);
}
#[test]
fn content_layout_insets_by_padding() {
let mut dom = tui_dom();
let root = dom.root();
let box_ = dom.create_element("box");
dom.append_child(root, box_).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"box",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(10))
.padding(Padding::symmetric(2, 1)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 20));
let outer = layout_rect_of(&dom, box_);
let inner = content_rect_of(&dom, box_);
assert_eq!(outer.width, 20);
assert_eq!(outer.height, 10);
assert_eq!(inner.x, outer.x + 2);
assert_eq!(inner.y, outer.y + 1);
assert_eq!(inner.width, 20 - 4);
assert_eq!(inner.height, 10 - 2);
}
#[test]
fn content_layout_insets_by_border() {
let mut dom = tui_dom();
let root = dom.root();
let box_ = dom.create_element("box");
dom.append_child(root, box_).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"box",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.border(Border::single()),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 20, 10));
let outer = layout_rect_of(&dom, box_);
let inner = content_rect_of(&dom, box_);
assert_eq!(inner.x, outer.x + 1);
assert_eq!(inner.y, outer.y + 1);
assert_eq!(inner.width, 10 - 2);
assert_eq!(inner.height, 5 - 2);
}
#[test]
fn auto_text_uses_unicode_width() {
let mut dom = tui_dom();
let root = dom.root();
let span = dom.create_element("span");
let t = dom.create_text_node("hello");
dom.append_child(span, t).unwrap();
dom.append_child(root, span).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"span",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row), );
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 5));
let _ = layout_rect_of(&dom, span);
}
#[test]
fn auto_text_in_row_parent() {
let mut dom = tui_dom();
let root = dom.root();
let r = dom.create_element("r");
let span = dom.create_element("span");
let t = dom.create_text_node("hello");
dom.append_child(span, t).unwrap();
dom.append_child(r, span).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"r",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 5));
assert_eq!(layout_rect_of(&dom, span).width, 5);
}
#[test]
fn auto_text_cjk_is_two_cells_each() {
let mut dom = tui_dom();
let root = dom.root();
let r = dom.create_element("r");
let span = dom.create_element("span");
let t = dom.create_text_node("中国"); dom.append_child(span, t).unwrap();
dom.append_child(r, span).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"r",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 5));
assert_eq!(layout_rect_of(&dom, span).width, 4);
}
#[test]
fn auto_text_ignores_out_of_flow_child_text_for_intrinsic_width() {
let mut dom = tui_dom();
let root = dom.root();
let r = dom.create_element("r");
let chip = dom.create_element("span");
let label = dom.create_text_node("…"); dom.append_child(chip, label).unwrap();
let drop = dom.create_element("div");
let long = dom.create_text_node("a very long dropdown panel of choices");
dom.append_child(drop, long).unwrap();
dom.append_child(chip, drop).unwrap();
dom.append_child(r, chip).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"r",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"div",
TuiStyle::new().position(crate::layout::Position::Absolute),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 5));
assert_eq!(layout_rect_of(&dom, chip).width, 1);
}
#[test]
fn auto_nested_element_recursive_fit() {
let mut dom = tui_dom();
let root = dom.root();
let r = dom.create_element("r");
let outer = dom.create_element("outer");
let inner = dom.create_element("inner");
let t = dom.create_text_node("hi"); dom.append_child(inner, t).unwrap();
dom.append_child(outer, inner).unwrap();
dom.append_child(r, outer).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"r",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"outer",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.padding(Padding::all(1)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 5));
assert_eq!(layout_rect_of(&dom, outer).width, 4);
}
#[test]
fn max_width_clamps_flex() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
dom.append_child(c, a).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Flex(1)).max_width(20));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 5));
assert_eq!(layout_rect_of(&dom, a).width, 20);
}
#[test]
fn min_width_lifts_fixed() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
dom.append_child(c, a).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(3)).min_width(10));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 5));
assert_eq!(layout_rect_of(&dom, a).width, 10);
}
#[test]
fn min_max_width_height_clamp_via_css_strings() {
use rdom_css::parse_inline;
let css_a = "width: 100; max-width: 30";
let css_b = "width: 5; min-width: 15";
let style_a = parse_inline(css_a).style;
let style_b = parse_inline(css_b).style;
use rdom_style::layout::MinSize;
assert_eq!(style_a.max_width, Some(Value::Specified(30)));
assert_eq!(
style_b.min_width,
Some(Value::Specified(MinSize::Cells(15)))
);
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("a", style_a)
.rule_unchecked("b", style_b);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 200, 5));
assert_eq!(
layout_rect_of(&dom, a).width,
30,
"max-width clamps width down"
);
assert_eq!(
layout_rect_of(&dom, b).width,
15,
"min-width lifts width up"
);
}
#[test]
fn auto_basis_flex_item_keeps_intrinsic_under_pressure() {
use rdom_style::layout::MinSize;
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let protected = dom.create_element("p");
let greedy = dom.create_element("g");
let text = dom.create_text_node("hello world");
dom.append_child(protected, text).unwrap();
dom.append_child(c, protected).unwrap();
dom.append_child(c, greedy).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("p", TuiStyle::new().min_width(MinSize::Auto))
.rule_unchecked("g", TuiStyle::new().width(Size::Flex(99)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 5));
let pw = layout_rect_of(&dom, protected).width;
assert!(
pw >= 11,
"auto-basis item with min-width: auto must protect intrinsic content, got {pw}"
);
}
#[test]
fn flex_basis_zero_shrinks_freely_per_css_strict() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let p = dom.create_element("p");
let g = dom.create_element("g");
let text = dom.create_text_node("very long text that would intrinsically need many cells");
dom.append_child(p, text).unwrap();
dom.append_child(c, p).unwrap();
dom.append_child(c, g).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("p", TuiStyle::new().width(Size::Flex(1)))
.rule_unchecked("g", TuiStyle::new().width(Size::Flex(99)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 5));
let pw = layout_rect_of(&dom, p).width;
let gw = layout_rect_of(&dom, g).width;
assert!(
pw <= 1,
"flex: 1 with no min must shrink to its flex share (~0–1), got {pw}"
);
assert!(
gw >= 78,
"flex: 99 sibling must take ~99/100 of the row, got {gw}"
);
}
#[test]
fn auto_min_drops_to_zero_when_overflow_non_visible_per_css_4_5() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let p = dom.create_element("p");
let g = dom.create_element("g");
let tp = dom.create_text_node("hello world"); let tg = dom.create_text_node("x");
dom.append_child(p, tp).unwrap();
dom.append_child(g, tg).unwrap();
dom.append_child(c, p).unwrap();
dom.append_child(c, g).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.width(Size::Fixed(20)),
)
.rule_unchecked(
"p",
TuiStyle::new()
.width(Size::Flex(1))
.overflow_x(crate::layout::Overflow::Hidden),
)
.rule_unchecked("g", TuiStyle::new().width(Size::Flex(1)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 5));
let pw = layout_rect_of(&dom, p).width;
assert!(
pw < 11,
"overflow:hidden must drop the content-min floor — p should shrink to its flex share, got {pw}"
);
}
#[test]
fn fixed_width_caps_auto_min_per_css_4_5() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let p = dom.create_element("p");
let text = dom.create_text_node("a string that would intrinsically take many cells");
dom.append_child(p, text).unwrap();
dom.append_child(c, p).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked("p", TuiStyle::new().width(Size::Fixed(30)).max_width(30));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 200, 5));
let pw = layout_rect_of(&dom, p).width;
assert_eq!(
pw, 30,
"max-width must clamp even when intrinsic content is larger (got {pw})"
);
}
#[test]
fn calc_specified_caps_auto_min_per_css_4_5() {
use rdom_style::calc::CalcExpr;
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let p = dom.create_element("p");
let text = dom.create_text_node("long string of content larger than 30 cells of intrinsic");
dom.append_child(p, text).unwrap();
dom.append_child(c, p).unwrap();
dom.append_child(root, c).unwrap();
let basis_50pct = CalcExpr::Percent(50.0);
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.width(Size::Fixed(80)),
)
.rule_unchecked(
"p",
TuiStyle::new().width(Size::Calc(Box::new(basis_50pct))),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 200, 5));
let pw = layout_rect_of(&dom, p).width;
assert_eq!(
pw, 40,
"calc(50%) must produce a definite specified suggestion that bounds auto-min; got {pw}"
);
}
#[test]
fn aspect_ratio_computes_height_from_explicit_width_in_row_flex() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
dom.append_child(c, a).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(32)).aspect_ratio(16, 9),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 30));
assert_eq!(layout_rect_of(&dom, a).height, 18);
}
#[test]
fn aspect_ratio_computes_width_from_explicit_height_in_column_flex() {
let mut dom = tui_dom();
let root = dom.root();
let a = dom.create_element("a");
dom.append_child(root, a).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Auto)
.height(Size::Fixed(9))
.aspect_ratio(16, 9),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 30));
assert_eq!(layout_rect_of(&dom, a).width, 16);
}
#[test]
fn aspect_ratio_ignored_when_both_axes_explicit() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
dom.append_child(c, a).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(32))
.height(Size::Fixed(7))
.aspect_ratio(16, 9),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 30));
assert_eq!(layout_rect_of(&dom, a).height, 7);
}
#[test]
fn aspect_ratio_rounds_half_to_even() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(30)).aspect_ratio(16, 9),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(5)).aspect_ratio(2, 1),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 30));
assert_eq!(layout_rect_of(&dom, a).height, 17);
assert_eq!(layout_rect_of(&dom, b).height, 2);
}
#[test]
fn aspect_ratio_round_trip_via_css() {
use rdom_css::parse_inline;
let style = parse_inline("aspect-ratio: 16/9").style;
use rdom_style::layout::AspectRatio;
assert_eq!(
style.aspect_ratio,
Some(Value::Specified(AspectRatio::new(16, 9).unwrap()))
);
}
#[test]
fn flex_row_centers_child_with_margin_auto_horizontal() {
use rdom_style::layout::{Margin, MarginValue};
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
dom.append_child(c, a).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(10)).margin(Margin {
top: MarginValue::Cells(0),
right: MarginValue::Auto,
bottom: MarginValue::Cells(0),
left: MarginValue::Auto,
}),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 5));
let la = layout_rect_of(&dom, a);
assert_eq!(la.width, 10);
assert_eq!(la.x, 15);
}
#[test]
fn flex_row_multi_auto_margin_distributes_equally() {
use rdom_style::layout::{Margin, MarginValue};
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
let d = dom.create_element("d");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(c, d).unwrap();
dom.append_child(root, c).unwrap();
let auto_left = Margin {
top: MarginValue::Cells(0),
right: MarginValue::Cells(0),
bottom: MarginValue::Cells(0),
left: MarginValue::Auto,
};
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(5))
.margin(auto_left.clone()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(5))
.margin(auto_left.clone()),
)
.rule_unchecked("d", TuiStyle::new().width(Size::Fixed(5)).margin(auto_left));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 30, 5));
assert_eq!(layout_rect_of(&dom, a).x, 5);
assert_eq!(layout_rect_of(&dom, b).x, 15);
assert_eq!(layout_rect_of(&dom, d).x, 25);
}
#[test]
fn flex_main_axis_cells_margin_offsets_child() {
use rdom_style::layout::{Margin, MarginValue};
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
dom.append_child(c, a).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(10)).margin(Margin {
top: MarginValue::Cells(0),
right: MarginValue::Cells(0),
bottom: MarginValue::Cells(0),
left: MarginValue::Cells(5),
}),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 5));
assert_eq!(layout_rect_of(&dom, a).x, 5);
assert_eq!(layout_rect_of(&dom, a).width, 10);
}
#[test]
fn flex_auto_margin_starves_flex_grow() {
use rdom_style::layout::{Margin, MarginValue};
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Flex(1)).margin(Margin {
top: MarginValue::Cells(0),
right: MarginValue::Auto,
bottom: MarginValue::Cells(0),
left: MarginValue::Auto,
}),
)
.rule_unchecked("b", TuiStyle::new().width(Size::Fixed(10)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 5));
assert_eq!(layout_rect_of(&dom, a).width, 0);
assert_eq!(layout_rect_of(&dom, a).x, 15);
assert_eq!(layout_rect_of(&dom, b).x, 30);
}
#[test]
fn absolute_modal_centers_with_inset_zero_and_margin_auto() {
use rdom_style::layout::{Length, Margin, Position};
let mut dom = tui_dom();
let root = dom.root();
let modal = dom.create_element("modal");
dom.append_child(root, modal).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"modal",
TuiStyle::new()
.position(Position::Absolute)
.top(Length::Cells(0))
.right(Length::Cells(0))
.bottom(Length::Cells(0))
.left(Length::Cells(0))
.width(Size::Fixed(20))
.height(Size::Fixed(5))
.margin(Margin::all_auto()),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 30));
let r = layout_rect_of(&dom, modal);
assert_eq!(r.width, 20);
assert_eq!(r.height, 5);
assert_eq!(r.x, 30);
assert_eq!(r.y, 12);
}
#[test]
fn sticky_top_pinned_when_scrollport_scrolls_past_threshold() {
use rdom_style::layout::{Length, Position};
let mut dom = tui_dom();
let root = dom.root();
let scrollport = dom.create_element("scrollport");
let banner = dom.create_element("banner");
let body = dom.create_element("body");
dom.append_child(scrollport, banner).unwrap();
dom.append_child(scrollport, body).unwrap();
dom.append_child(root, scrollport).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"scrollport",
TuiStyle::new()
.height(Size::Fixed(20))
.overflow(Overflow::Hidden),
)
.rule_unchecked(
"banner",
TuiStyle::new()
.height(Size::Fixed(1))
.position(Position::Sticky)
.top(Length::Cells(0)),
)
.rule_unchecked("body", TuiStyle::new().height(Size::Fixed(100)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 20));
assert_eq!(layout_rect_of(&dom, banner).y, 0);
if let Some(ext) = dom.node_mut(scrollport).ext_mut() {
ext.scroll_y = 5;
}
dom.layout_dom(Rect::new(0, 0, 40, 20));
assert_eq!(layout_rect_of(&dom, banner).y, 0);
assert_eq!(layout_rect_of(&dom, body).y, -4);
}
#[test]
fn sticky_top_pre_stick_renders_in_normal_flow() {
use rdom_style::layout::{Length, Position};
let mut dom = tui_dom();
let root = dom.root();
let scrollport = dom.create_element("scrollport");
let spacer = dom.create_element("spacer");
let banner = dom.create_element("banner");
dom.append_child(scrollport, spacer).unwrap();
dom.append_child(scrollport, banner).unwrap();
dom.append_child(root, scrollport).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"scrollport",
TuiStyle::new()
.height(Size::Fixed(20))
.overflow(Overflow::Hidden),
)
.rule_unchecked("spacer", TuiStyle::new().height(Size::Fixed(5)))
.rule_unchecked(
"banner",
TuiStyle::new()
.height(Size::Fixed(1))
.position(Position::Sticky)
.top(Length::Cells(0)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 20));
assert_eq!(layout_rect_of(&dom, banner).y, 5);
}
#[test]
fn sticky_no_scrollport_ancestor_behaves_as_relative() {
use rdom_style::layout::{Length, Position};
let mut dom = tui_dom();
let root = dom.root();
let banner = dom.create_element("banner");
dom.append_child(root, banner).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"banner",
TuiStyle::new()
.height(Size::Fixed(3))
.position(Position::Sticky)
.top(Length::Cells(10)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 20));
assert_eq!(layout_rect_of(&dom, banner).y, 0);
}
#[test]
fn sticky_keyword_parses_via_css() {
use rdom_css::parse_inline;
use rdom_style::layout::Position;
let s = parse_inline("position: sticky").style;
assert_eq!(s.position, Some(Value::Specified(Position::Sticky)));
}
#[test]
fn flex_row_with_collapse_shared_edge_is_one_cell() {
use rdom_style::layout::{Border, BorderCollapse};
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(outer, a).unwrap();
dom.append_child(outer, b).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(5))
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single())
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(8))
.border(Border::single()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(8))
.border(Border::single()),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.x, 0, "child A's outer extends into parent border");
assert_eq!(la.width, 8);
assert_eq!(lb.x, 7, "sibling overlap: B starts 1 cell before A's end");
assert_eq!(lb.width, 8);
}
#[test]
fn flex_row_collapse_inactive_keeps_separate_borders() {
use rdom_style::layout::Border;
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(outer, a).unwrap();
dom.append_child(outer, b).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(5))
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single()),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(8))
.border(Border::single()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(8))
.border(Border::single()),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.x, 1);
assert_eq!(lb.x, 9);
}
#[test]
fn collapse_without_border_does_not_expand_content_area() {
use rdom_style::layout::BorderCollapse;
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
dom.append_child(outer, a).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(20))
.padding(Padding::all(1))
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(10)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
assert_eq!(layout_rect_of(&dom, a).x, 1);
}
#[test]
fn flex_distribution_redistributes_integer_remainder() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(31))
.flow(Flow::Flex)
.direction(Direction::Row),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Flex(1)))
.rule_unchecked("b", TuiStyle::new().width(Size::Flex(1)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 31, 5));
let aw = layout_rect_of(&dom, a).width;
let bw = layout_rect_of(&dom, b).width;
assert_eq!(aw + bw, 31, "flex distribution must consume all 31 cells");
assert_eq!(
layout_rect_of(&dom, b).x + bw as i32,
31,
"rightmost edge reaches the parent's right"
);
}
#[test]
fn hit_test_at_shared_edge_returns_deeper_element() {
use crate::runtime::hit_test::HitTestExt;
use rdom_style::layout::{Border, BorderCollapse};
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(outer, a).unwrap();
dom.append_child(outer, b).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(5))
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single())
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(8))
.border(Border::single()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(8))
.border(Border::single()),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
let hit = dom.hit_test(7, 2);
assert_eq!(hit, Some(b), "shared-edge click goes to later sibling");
let hit_a = dom.hit_test(3, 2);
assert_eq!(hit_a, Some(a));
}
#[test]
fn collapse_three_bordered_siblings_share_two_junctions() {
use rdom_style::layout::{Border, BorderCollapse};
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(outer, a).unwrap();
dom.append_child(outer, b).unwrap();
dom.append_child(outer, c).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.width(Size::Fixed(20))
.flow(Flow::Flex)
.direction(Direction::Row)
.border(Border::single())
.border_collapse(BorderCollapse::Collapse),
)
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(6))
.border(Border::single()),
)
.rule_unchecked(
"b",
TuiStyle::new()
.width(Size::Fixed(6))
.border(Border::single()),
)
.rule_unchecked(
"c",
TuiStyle::new()
.width(Size::Fixed(6))
.border(Border::single()),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
assert_eq!(layout_rect_of(&dom, a).x, 0);
assert_eq!(layout_rect_of(&dom, b).x, 5);
assert_eq!(layout_rect_of(&dom, c).x, 10);
}
#[test]
fn row_cross_axis_stretches() {
let mut dom = tui_dom();
let root = dom.root();
let r = dom.create_element("r");
let a = dom.create_element("a");
dom.append_child(r, a).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"r",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.height(Size::Fixed(8)),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(5)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 20));
assert_eq!(layout_rect_of(&dom, a).height, 8);
}
#[test]
fn row_cross_axis_fixed_not_stretched() {
let mut dom = tui_dom();
let root = dom.root();
let r = dom.create_element("r");
let a = dom.create_element("a");
dom.append_child(r, a).unwrap();
dom.append_child(root, r).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"r",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.height(Size::Fixed(10)),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 20));
assert_eq!(layout_rect_of(&dom, a).height, 3);
}
#[test]
fn scroll_y_offsets_children_negative() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", TuiStyle::new().height(Size::Fixed(3)))
.rule_unchecked("b", TuiStyle::new().height(Size::Fixed(3)));
cascade(&mut dom, &sheet);
dom.node_mut(c).ext_mut().unwrap().scroll_y = 2;
dom.layout_dom(Rect::new(0, 0, 20, 10));
let la = layout_rect_of(&dom, a);
let lb = layout_rect_of(&dom, b);
assert_eq!(la.y, -2);
assert_eq!(lb.y, la.y + 3);
}
#[test]
fn nested_containers_lay_out_independently() {
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let inner = dom.create_element("inner");
let leaf = dom.create_element("leaf");
dom.append_child(inner, leaf).unwrap();
dom.append_child(outer, inner).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.padding(Padding::all(1))
.border(Border::single())
.width(Size::Fixed(20))
.height(Size::Fixed(10)),
)
.rule_unchecked("inner", TuiStyle::new().height(Size::Fixed(4)))
.rule_unchecked("leaf", TuiStyle::new().height(Size::Fixed(2)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 50, 20));
let oc = content_rect_of(&dom, outer);
assert_eq!(oc.x, 2);
assert_eq!(oc.y, 2);
assert_eq!(oc.width, 16);
assert_eq!(oc.height, 6);
let il = layout_rect_of(&dom, inner);
assert_eq!(il.x, 2);
assert_eq!(il.y, 2);
assert_eq!(il.height, 4);
assert_eq!(il.width, 16);
let leaf_rect = layout_rect_of(&dom, leaf);
assert_eq!(leaf_rect.y, 2);
assert_eq!(leaf_rect.height, 2);
}
#[test]
fn layout_pass_clears_layout_dirty() {
let mut dom = tui_dom();
let root = dom.root();
let a = dom.create_element("a");
dom.append_child(root, a).unwrap();
dom.cascade(&Stylesheet::bare()); assert!(dom.node(a).is_layout_dirty());
dom.layout_dom(Rect::new(0, 0, 20, 10));
assert!(!dom.node(a).is_layout_dirty());
}
#[test]
fn empty_root_no_children_no_panic() {
let mut dom = tui_dom();
dom.cascade(&Stylesheet::bare());
dom.layout_dom(Rect::new(0, 0, 20, 10));
}
#[test]
fn gap_only_between_not_after_last() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(c, a).unwrap();
dom.append_child(c, b).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.gap(3),
)
.rule_unchecked("a", TuiStyle::new().width(Size::Fixed(5)))
.rule_unchecked("b", TuiStyle::new().width(Size::Fixed(5)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 30, 5));
assert_eq!(layout_rect_of(&dom, a).x, 0);
assert_eq!(layout_rect_of(&dom, b).x, 8);
}
#[test]
fn flex_phase1_skips_absolute_child() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let abs = dom.create_element("abs");
let flex = dom.create_element("flex");
dom.append_child(c, abs).unwrap();
dom.append_child(c, flex).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.width(Size::Fixed(20)),
)
.rule_unchecked("flex", TuiStyle::new().width(Size::Flex(1)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 30, 5));
assert_eq!(layout_rect_of(&dom, flex).width, 30);
}
#[test]
fn flex_phase1_skips_fixed_child() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let fixed = dom.create_element("fixed");
let flex = dom.create_element("flex");
dom.append_child(c, fixed).unwrap();
dom.append_child(c, flex).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"fixed",
TuiStyle::new()
.position(crate::layout::Position::Fixed)
.width(Size::Fixed(20)),
)
.rule_unchecked("flex", TuiStyle::new().width(Size::Flex(1)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 30, 5));
assert_eq!(layout_rect_of(&dom, flex).width, 30);
}
#[test]
fn relative_shift_moves_layout_rect() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let rel = dom.create_element("rel");
dom.append_child(c, rel).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"rel",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.top(crate::layout::Length::Cells(2))
.left(crate::layout::Length::Cells(3))
.width(Size::Fixed(10))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 30, 10));
let r = layout_rect_of(&dom, rel);
assert_eq!(r.x, 3);
assert_eq!(r.y, 2);
}
#[test]
fn relative_shift_does_not_affect_sibling_layout() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let rel = dom.create_element("rel");
let after = dom.create_element("after");
dom.append_child(c, rel).unwrap();
dom.append_child(c, after).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"rel",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.left(crate::layout::Length::Cells(50))
.width(Size::Fixed(5))
.height(Size::Fixed(2)),
)
.rule_unchecked("after", TuiStyle::new().width(Size::Fixed(10)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 10));
let after_r = layout_rect_of(&dom, after);
assert_eq!(after_r.x, 5);
}
#[test]
fn relative_negative_offset_shifts_left() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let rel = dom.create_element("rel");
dom.append_child(c, rel).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"rel",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.top(crate::layout::Length::Cells(-1))
.width(Size::Fixed(5))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 5, 30, 10));
let r = layout_rect_of(&dom, rel);
assert_eq!(r.y, 4);
}
#[test]
fn absolute_placed_against_viewport() {
let mut dom = tui_dom();
let root = dom.root();
let abs = dom.create_element("abs");
dom.append_child(root, abs).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(5))
.left(crate::layout::Length::Cells(3))
.width(Size::Fixed(10))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let r = layout_rect_of(&dom, abs);
assert_eq!(r.x, 3);
assert_eq!(r.y, 5);
assert_eq!(r.width, 10);
assert_eq!(r.height, 2);
}
#[test]
fn absolute_placed_against_relative_parent() {
let mut dom = tui_dom();
let root = dom.root();
let parent = dom.create_element("parent");
let abs = dom.create_element("abs");
dom.append_child(parent, abs).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"parent",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(30))
.height(Size::Fixed(10)),
)
.rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(1))
.left(crate::layout::Length::Cells(2))
.width(Size::Fixed(5))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let parent_rect = layout_rect_of(&dom, parent);
let abs_rect = layout_rect_of(&dom, abs);
assert_eq!(abs_rect.x, parent_rect.x + 2);
assert_eq!(abs_rect.y, parent_rect.y + 1);
}
#[test]
fn absolute_inset_zero_fills_containing_block() {
let mut dom = tui_dom();
let root = dom.root();
let parent = dom.create_element("parent");
let abs = dom.create_element("abs");
dom.append_child(parent, abs).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"parent",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(20))
.height(Size::Fixed(8)),
)
.rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.right(crate::layout::Length::Cells(0))
.bottom(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let parent_rect = layout_rect_of(&dom, parent);
let abs_rect = layout_rect_of(&dom, abs);
assert_eq!(abs_rect.x, parent_rect.x);
assert_eq!(abs_rect.y, parent_rect.y);
assert_eq!(abs_rect.width, parent_rect.width);
assert_eq!(abs_rect.height, parent_rect.height);
}
#[test]
fn fixed_placed_against_viewport_even_with_relative_parent() {
let mut dom = tui_dom();
let root = dom.root();
let parent = dom.create_element("parent");
let f = dom.create_element("f");
dom.append_child(parent, f).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"parent",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(30))
.height(Size::Fixed(10)),
)
.rule_unchecked(
"f",
TuiStyle::new()
.position(crate::layout::Position::Fixed)
.top(crate::layout::Length::Cells(2))
.left(crate::layout::Length::Cells(4))
.width(Size::Fixed(8))
.height(Size::Fixed(3)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let r = layout_rect_of(&dom, f);
assert_eq!(r.x, 4);
assert_eq!(r.y, 2);
}
#[test]
fn nested_absolute_uses_outer_absolute_as_containing_block() {
let mut dom = tui_dom();
let root = dom.root();
let outer = dom.create_element("outer");
let inner = dom.create_element("inner");
dom.append_child(outer, inner).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"outer",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(5))
.left(crate::layout::Length::Cells(5))
.width(Size::Fixed(30))
.height(Size::Fixed(10)),
)
.rule_unchecked(
"inner",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(1))
.left(crate::layout::Length::Cells(2))
.width(Size::Fixed(5))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let inner_r = layout_rect_of(&dom, inner);
assert_eq!(inner_r.x, 7);
assert_eq!(inner_r.y, 6);
}
#[test]
fn absolute_with_right_and_no_left_aligns_to_right_edge() {
let mut dom = tui_dom();
let root = dom.root();
let abs = dom.create_element("abs");
dom.append_child(root, abs).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.right(crate::layout::Length::Cells(2))
.width(Size::Fixed(10))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let r = layout_rect_of(&dom, abs);
assert_eq!(r.x, 88);
}
#[test]
fn absolute_with_top_and_bottom_stretches_height() {
let mut dom = tui_dom();
let root = dom.root();
let abs = dom.create_element("abs");
dom.append_child(root, abs).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.bottom(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(20)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let r = layout_rect_of(&dom, abs);
assert_eq!(r.height, 50);
}
#[test]
fn fixed_inside_overflow_scroll_uses_viewport() {
let mut dom = tui_dom();
let root = dom.root();
let scroller = dom.create_element("scroll");
let f = dom.create_element("f");
dom.append_child(scroller, f).unwrap();
dom.append_child(root, scroller).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"scroll",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(10))
.overflow(Overflow::Scroll),
)
.rule_unchecked(
"f",
TuiStyle::new()
.position(crate::layout::Position::Fixed)
.top(crate::layout::Length::Cells(2))
.left(crate::layout::Length::Cells(4))
.width(Size::Fixed(5))
.height(Size::Fixed(2)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 100, 50));
let r = layout_rect_of(&dom, f);
assert_eq!(r.x, 4);
assert_eq!(r.y, 2);
}
#[test]
fn flex_phase1_keeps_relative_child_in_flow() {
let mut dom = tui_dom();
let root = dom.root();
let c = dom.create_element("c");
let rel = dom.create_element("rel");
let flex = dom.create_element("flex");
dom.append_child(c, rel).unwrap();
dom.append_child(c, flex).unwrap();
dom.append_child(root, c).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"c",
TuiStyle::new().flow(Flow::Flex).direction(Direction::Row),
)
.rule_unchecked(
"rel",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.width(Size::Fixed(10)),
)
.rule_unchecked("flex", TuiStyle::new().width(Size::Flex(1)));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 30, 5));
assert_eq!(layout_rect_of(&dom, rel).width, 10);
assert_eq!(layout_rect_of(&dom, flex).width, 20);
}
#[test]
fn debug_inline_block_cascade() {
let mut dom = tui_dom();
let root = dom.root();
let btn = dom.create_element("btn");
let t = dom.create_text_node("Submit");
dom.append_child(btn, t).unwrap();
dom.append_child(root, btn).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("btn", TuiStyle::new().display(Display::InlineBlock));
cascade(&mut dom, &sheet);
let display = dom.node(btn).computed().unwrap().display;
assert_eq!(
display,
Display::InlineBlock,
"cascade did not produce InlineBlock"
);
}
#[test]
fn inline_block_hugs_content_in_column_parent() {
let mut dom = tui_dom();
let root = dom.root();
let screen = dom.create_element("screen");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Submit");
dom.append_child(btn, label).unwrap();
dom.append_child(screen, btn).unwrap();
dom.append_child(root, screen).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"screen",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.width(Size::Flex(1))
.height(Size::Flex(1)),
)
.rule_unchecked("btn", TuiStyle::new().display(Display::InlineBlock));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(
lb.width, 6,
"InlineBlock child must size to intrinsic content width (got {lb:?})"
);
}
#[test]
fn inline_block_with_pseudo_chrome_hugs_content_plus_pseudos() {
let mut dom = tui_dom();
let root = dom.root();
let screen = dom.create_element("screen");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Submit");
dom.append_child(btn, label).unwrap();
dom.append_child(screen, btn).unwrap();
dom.append_child(root, screen).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"screen",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.width(Size::Flex(1)),
)
.rule_unchecked("btn", TuiStyle::new().display(Display::InlineBlock))
.rule_unchecked(
"btn::before",
TuiStyle::new().content(crate::style::Content::Str("[ ".into())),
)
.rule_unchecked(
"btn::after",
TuiStyle::new().content(crate::style::Content::Str(" ]".into())),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(
lb.width, 10,
"InlineBlock width = text(6) + ::before(2) + ::after(2) = 10 (got {lb:?})"
);
}
#[test]
fn inline_block_hugs_content_in_row_parent() {
let mut dom = tui_dom();
let root = dom.root();
let screen = dom.create_element("screen");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Go");
dom.append_child(btn, label).unwrap();
dom.append_child(screen, btn).unwrap();
dom.append_child(root, screen).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"screen",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.width(Size::Flex(1))
.height(Size::Flex(1)),
)
.rule_unchecked("btn", TuiStyle::new().display(Display::InlineBlock));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(lb.width, 2, "InlineBlock width hugs 'Go' (2 cells)");
assert_eq!(
lb.height, 1,
"InlineBlock height hugs content — does NOT stretch to row cross"
);
}
#[test]
fn inline_block_fixed_width_wins_over_intrinsic() {
let mut dom = tui_dom();
let root = dom.root();
let screen = dom.create_element("screen");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Go");
dom.append_child(btn, label).unwrap();
dom.append_child(screen, btn).unwrap();
dom.append_child(root, screen).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"screen",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.width(Size::Flex(1)),
)
.rule_unchecked(
"btn",
TuiStyle::new()
.display(Display::InlineBlock)
.width(Size::Fixed(20)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(lb.width, 20, "Fixed width overrides InlineBlock intrinsic");
}
#[test]
fn block_still_stretches_cross_axially() {
let mut dom = tui_dom();
let root = dom.root();
let screen = dom.create_element("screen");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Submit");
dom.append_child(btn, label).unwrap();
dom.append_child(screen, btn).unwrap();
dom.append_child(root, screen).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"screen",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.width(Size::Flex(1)),
)
.rule_unchecked("btn", TuiStyle::new().display(Display::Block));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(lb.width, 80, "Block + Auto still stretches");
}
#[test]
fn inline_block_with_position_relative_shifts_in_flex_parent() {
let mut dom = tui_dom();
let root = dom.root();
let screen = dom.create_element("screen");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Hi");
dom.append_child(btn, label).unwrap();
dom.append_child(screen, btn).unwrap();
dom.append_child(root, screen).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"screen",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Column)
.width(Size::Flex(1)),
)
.rule_unchecked(
"btn",
TuiStyle::new()
.display(Display::InlineBlock)
.position(crate::layout::Position::Relative)
.left(crate::layout::Length::Cells(5)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(
lb.width, 2,
"intrinsic width preserved through relative shift"
);
assert_eq!(lb.x, 5, "shifted right by 5 cells");
}
#[test]
fn display_none_ancestor_suppresses_inline_block_descendant() {
let mut dom = tui_dom();
let root = dom.root();
let hidden = dom.create_element("hidden");
let btn = dom.create_element("btn");
let label = dom.create_text_node("Should not render");
dom.append_child(btn, label).unwrap();
dom.append_child(hidden, btn).unwrap();
dom.append_child(root, hidden).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("hidden", TuiStyle::new().display(Display::None))
.rule_unchecked("btn", TuiStyle::new().display(Display::InlineBlock));
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let lb = layout_rect_of(&dom, btn);
assert_eq!(lb.width, 0);
assert_eq!(lb.height, 0);
}
#[test]
fn display_none_collapses_a_previously_visible_rect() {
use crate::layout::{Direction, Flow, Size};
let mut dom = tui_dom();
let root = dom.root();
let row = dom.create_element("row"); let a = dom.create_element("a"); let inner = dom.create_element("inner"); dom.append_child(a, inner).unwrap();
let b = dom.create_element("b"); dom.append_child(row, a).unwrap();
dom.append_child(row, b).unwrap();
dom.append_child(root, row).unwrap();
let dims = |s: TuiStyle| s.width(Size::Fixed(6)).height(Size::Fixed(1));
let row_rule = TuiStyle::new().flow(Flow::Flex).direction(Direction::Row);
let visible = Stylesheet::bare()
.rule_unchecked("row", row_rule.clone())
.rule_unchecked("a", dims(TuiStyle::new()))
.rule_unchecked(
"inner",
TuiStyle::new().width(Size::Fixed(3)).height(Size::Fixed(1)),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(4)).height(Size::Fixed(1)),
);
cascade(&mut dom, &visible);
dom.layout_dom(Rect::new(0, 0, 80, 24));
assert!(
layout_rect_of(&dom, a).width > 0,
"a is laid out while visible"
);
assert!(
layout_rect_of(&dom, inner).width > 0,
"so is its descendant"
);
let hidden = Stylesheet::bare()
.rule_unchecked("row", row_rule)
.rule_unchecked("a", dims(TuiStyle::new()).display(Display::None))
.rule_unchecked(
"inner",
TuiStyle::new().width(Size::Fixed(3)).height(Size::Fixed(1)),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(4)).height(Size::Fixed(1)),
);
cascade(&mut dom, &hidden);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let ra = layout_rect_of(&dom, a);
assert_eq!(
(ra.width, ra.height),
(0, 0),
"hidden element's rect collapses"
);
let ri = layout_rect_of(&dom, inner);
assert_eq!(
(ri.width, ri.height),
(0, 0),
"and its descendant's rect too"
);
}
#[test]
fn layout_records_scroll_content_height_for_scrollable_column() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let list = dom.create_element("list");
dom.append_child(root, list).unwrap();
for _ in 0..50 {
let row = dom.create_element("row");
dom.append_child(list, row).unwrap();
}
let sheet = Stylesheet::bare()
.rule_unchecked(
"list",
TuiStyle::new()
.flow(crate::layout::Flow::Flex)
.direction(crate::layout::Direction::Column)
.height(crate::layout::Size::Flex(1))
.overflow(crate::layout::Overflow::Scroll),
)
.rule_unchecked(
"row",
TuiStyle::new()
.height(crate::layout::Size::Fixed(1))
.flex_shrink(0),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let ext = dom.node(list).ext().unwrap();
assert_eq!(ext.scroll_content_height, 50);
assert!(ext.scroll_content_width > 0);
assert!(ext.scroll_content_width <= 80);
}
#[test]
fn layout_skips_scroll_content_recording_for_visible_overflow() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let child = dom.create_element("p");
dom.append_child(div, child).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("div", TuiStyle::new());
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let ext = dom.node(div).ext().unwrap();
assert_eq!(ext.scroll_content_height, 0);
assert_eq!(ext.scroll_content_width, 0);
}
#[test]
fn intrinsic_height_ignores_whitespace_text_between_block_children() {
let mut dom = tui_dom();
let root = dom.root();
let card = dom.create_element("card");
let ws1 = dom.create_text_node("\n ");
let label = dom.create_element("label");
let label_text = dom.create_text_node("Author CSS");
let ws2 = dom.create_text_node("\n ");
let note = dom.create_element("note");
let note_text = dom.create_text_node("This whole card is styled by the block.");
let ws3 = dom.create_text_node("\n ");
dom.append_child(label, label_text).unwrap();
dom.append_child(note, note_text).unwrap();
dom.append_child(card, ws1).unwrap();
dom.append_child(card, label).unwrap();
dom.append_child(card, ws2).unwrap();
dom.append_child(card, note).unwrap();
dom.append_child(card, ws3).unwrap();
dom.append_child(root, card).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"card",
TuiStyle::new()
.display(Display::Block)
.border(crate::layout::Border::rounded())
.padding(crate::layout::Padding {
top: crate::layout::PaddingValue::Cells(1),
right: crate::layout::PaddingValue::Cells(2),
bottom: crate::layout::PaddingValue::Cells(1),
left: crate::layout::PaddingValue::Cells(2),
})
.gap(1),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 80, 24));
let card_rect = layout_rect_of(&dom, card);
assert_eq!(
card_rect.height, 7,
"whitespace between block children must not inflate intrinsic height"
);
}
#[test]
fn intrinsic_size_block_with_only_whitespace_text_is_chrome_only() {
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
let ws = dom.create_text_node("\n \n ");
dom.append_child(host, ws).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"host",
TuiStyle::new()
.display(Display::Block)
.padding(crate::layout::Padding {
top: crate::layout::PaddingValue::Cells(1),
right: crate::layout::PaddingValue::Cells(0),
bottom: crate::layout::PaddingValue::Cells(1),
left: crate::layout::PaddingValue::Cells(0),
}),
);
cascade(&mut dom, &sheet);
let h = super::intrinsic::intrinsic_size(&dom, host, Direction::Column, 40);
assert_eq!(h, 2, "padding only — whitespace text contributes nothing");
}
#[test]
fn intrinsic_size_ignores_display_none_children() {
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
let visible = dom.create_element("v");
let visible_text = dom.create_text_node("one");
dom.append_child(visible, visible_text).unwrap();
let hidden = dom.create_element("h");
let hidden_text = dom.create_text_node("five\nlines\nof\nhidden\ncontent");
dom.append_child(hidden, hidden_text).unwrap();
dom.append_child(host, visible).unwrap();
dom.append_child(host, hidden).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("v", TuiStyle::new().display(Display::Block))
.rule_unchecked("h", TuiStyle::new().display(Display::None));
cascade(&mut dom, &sheet);
let h = super::intrinsic::intrinsic_size(&dom, host, Direction::Column, 40);
assert_eq!(
h, 1,
"intrinsic = visible child's 1 row; hidden 5-line child contributes 0"
);
}
#[test]
fn intrinsic_size_ignores_absolutely_positioned_children() {
use crate::layout::Position;
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
let visible = dom.create_element("v");
let vt = dom.create_text_node("one");
dom.append_child(visible, vt).unwrap();
let abs = dom.create_element("a");
let at = dom.create_text_node("five\nlines\nof\noff-flow\ncontent");
dom.append_child(abs, at).unwrap();
dom.append_child(host, visible).unwrap();
dom.append_child(host, abs).unwrap();
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("v", TuiStyle::new().display(Display::Block))
.rule_unchecked(
"a",
TuiStyle::new()
.display(Display::Block)
.position(Position::Absolute),
);
cascade(&mut dom, &sheet);
let h = super::intrinsic::intrinsic_size(&dom, host, Direction::Column, 40);
assert_eq!(h, 1, "absolute child contributes 0 to in-flow intrinsic");
}
#[test]
fn intrinsic_size_text_only_block_uses_inline_layout() {
let mut dom = tui_dom();
let root = dom.root();
let note = dom.create_element("note");
let text = dom.create_text_node("hello world");
dom.append_child(note, text).unwrap();
dom.append_child(root, note).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("note", TuiStyle::new().display(Display::Block));
cascade(&mut dom, &sheet);
let h = super::intrinsic::intrinsic_size(&dom, note, Direction::Column, 40);
assert_eq!(h, 1, "short text fits on one line at width 40");
}
#[test]
fn ifc_block_text_only_returns_false_for_pseudo_paint_routing() {
let mut dom = tui_dom();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
cascade(&mut dom, &Stylesheet::bare());
assert!(
!super::ifc::is_ifc_block(&dom, p),
"text-only block stays non-IFC so pseudos paint via paint_inline_content"
);
}
#[test]
fn ifc_block_whitespace_only_text_returns_false() {
let mut dom = tui_dom();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node(" \n\t ");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
cascade(&mut dom, &Stylesheet::bare());
assert!(
!super::ifc::is_ifc_block(&dom, p),
"whitespace-only text does not establish an IFC"
);
}
#[test]
fn ifc_block_block_child_disqualifies_even_with_text() {
let mut dom = tui_dom();
let root = dom.root();
let card = dom.create_element("card");
let text = dom.create_text_node("\n ");
let child = dom.create_element("child");
dom.append_child(card, text).unwrap();
dom.append_child(card, child).unwrap();
dom.append_child(root, card).unwrap();
let sheet = Stylesheet::bare().rule_unchecked("child", TuiStyle::new().display(Display::Block));
cascade(&mut dom, &sheet);
assert!(
!super::ifc::is_ifc_block(&dom, card),
"block element child wins over text — not IFC"
);
}
#[test]
fn ifc_block_inline_block_only_returns_false() {
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
let btn = dom.create_element("btn");
dom.append_child(host, btn).unwrap();
dom.append_child(root, host).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("btn", TuiStyle::new().display(Display::InlineBlock));
cascade(&mut dom, &sheet);
assert!(
!super::ifc::is_ifc_block(&dom, host),
"InlineBlock-only parent stays non-IFC"
);
}
#[test]
fn block_flow_container_ignores_flex_direction_row() {
let mut dom = tui_dom();
let root = dom.root();
let container = dom.create_element("div");
let h1 = dom.create_element("h1");
let p = dom.create_element("p");
dom.append_child(container, h1).unwrap();
dom.append_child(container, p).unwrap();
dom.append_child(root, container).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked("div", TuiStyle::new().direction(Direction::Row))
.rule_unchecked(
"h1",
TuiStyle::new()
.height(Size::Fixed(2))
.width(Size::Fixed(15)),
)
.rule_unchecked(
"p",
TuiStyle::new()
.height(Size::Fixed(3))
.width(Size::Fixed(20)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 20));
let h1_rect = layout_rect_of(&dom, h1);
let p_rect = layout_rect_of(&dom, p);
assert_eq!(
h1_rect.x, 0,
"h1 at container left (block layout, not flex)"
);
assert_eq!(p_rect.x, 0, "p at container left (block layout, not flex)");
assert_eq!(h1_rect.y, 0, "h1 at container top");
assert_eq!(p_rect.y, 2, "p stacks directly below h1 (block layout)");
assert_eq!(h1_rect.width, 15, "h1 declared width");
assert_eq!(p_rect.width, 20, "p declared width");
assert_eq!(h1_rect.height, 2);
assert_eq!(p_rect.height, 3);
}
#[test]
fn overflow_auto_does_not_reserve_gutter_under_default_scrollbar_gutter_auto() {
use crate::layout::Overflow;
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"host",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.overflow(Overflow::Auto),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 12, 8));
let inner = dom.node(host).ext().unwrap().content_layout;
assert_eq!(inner.width, 10, "no gutter reserved on auto+auto");
assert_eq!(inner.height, 5);
}
#[test]
fn overflow_auto_with_scrollbar_gutter_stable_reserves_gutter() {
use crate::layout::{Overflow, ScrollbarGutter};
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"host",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.overflow(Overflow::Auto)
.scrollbar_gutter(ScrollbarGutter::Stable),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 12, 8));
let inner = dom.node(host).ext().unwrap().content_layout;
assert_eq!(
inner.width, 9,
"scrollbar-gutter: stable reserves Y-axis gutter"
);
assert_eq!(
inner.height, 4,
"scrollbar-gutter: stable reserves X-axis gutter"
);
}
#[test]
fn overflow_scroll_always_reserves_gutter_regardless_of_scrollbar_gutter_value() {
use crate::layout::{Overflow, ScrollbarGutter};
let mut dom = tui_dom();
let root = dom.root();
let host = dom.create_element("host");
dom.append_child(root, host).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"host",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.overflow(Overflow::Scroll)
.scrollbar_gutter(ScrollbarGutter::Auto),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 12, 8));
let inner = dom.node(host).ext().unwrap().content_layout;
assert_eq!(inner.width, 9, "overflow: scroll always reserves Y-gutter");
assert_eq!(inner.height, 4, "overflow: scroll always reserves X-gutter");
}
#[test]
fn table_in_a_horizontal_scroll_wrapper_scrolls_header_and_body_together() {
let mut dom = tui_dom();
let root = dom.root();
let wrap = dom.create_element("div");
let table = dom.create_element("table");
let mk = |dom: &mut TuiDom, tag: &str, text: &str| {
let c = dom.create_element(tag);
let t = dom.create_text_node(text);
dom.append_child(c, t).unwrap();
c
};
let thead = dom.create_element("thead");
let htr = dom.create_element("tr");
let th0 = mk(&mut dom, "th", "A");
let th1 = mk(&mut dom, "th", "second-col");
dom.append_child(htr, th0).unwrap();
dom.append_child(htr, th1).unwrap();
dom.append_child(thead, htr).unwrap();
dom.append_child(table, thead).unwrap();
let tbody = dom.create_element("tbody");
let btr = dom.create_element("tr");
let td0 = mk(&mut dom, "td", "x");
let td1 = mk(&mut dom, "td", "y");
dom.append_child(btr, td0).unwrap();
dom.append_child(btr, td1).unwrap();
dom.append_child(tbody, btr).unwrap();
dom.append_child(table, tbody).unwrap();
dom.append_child(wrap, table).unwrap();
dom.append_child(root, wrap).unwrap();
crate::runtime::builtins::table::size_columns(&mut dom, table);
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.overflow(Overflow::Scroll)
.width(Size::Fixed(8)),
);
cascade(&mut dom, &sheet);
dom.layout_dom(Rect::new(0, 0, 40, 10));
let base_h = layout_rect_of(&dom, th0).x;
let base_b = layout_rect_of(&dom, td0).x;
assert_eq!(base_h, base_b, "header & body column 0 start aligned");
dom.node_mut(wrap).ext_mut().unwrap().scroll_x = 4;
dom.layout_dom(Rect::new(0, 0, 40, 10));
let sh = layout_rect_of(&dom, th0).x;
let sb = layout_rect_of(&dom, td0).x;
assert_eq!(sh, sb, "header & body stay aligned while scrolled");
assert_eq!(sh, base_h - 4, "column scrolled left by the scroll offset");
}