use super::*;
use crate::TuiDom;
use crate::layout::{Border, Direction, Display, Flow, Overflow, Padding, Size};
use crate::node::TuiNodeExt;
use crate::render::{LayoutExt, Rect};
use crate::style::{CascadeExt, Color, Stylesheet, TuiStyle};
use rdom_core::NodeId;
fn prepare(dom: &mut TuiDom, sheet: &Stylesheet, viewport: Rect) {
dom.cascade(sheet);
dom.layout_dom(viewport);
}
#[test]
fn point_on_single_block_returns_it() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(5, 1), Some(div));
}
#[test]
fn point_outside_viewport_returns_none() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(2)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(50, 50), None);
}
#[test]
fn empty_tree_returns_none() {
let mut dom: TuiDom = TuiDom::new();
prepare(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(5, 5), None);
}
#[test]
fn deepest_block_wins_on_nesting() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let outer = dom.create_element("div");
let inner = dom.create_element("span");
dom.append_child(outer, inner).unwrap();
dom.append_child(root, outer).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.padding(Padding::all(1)),
)
.rule_unchecked(
"span",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(3, 1), Some(inner));
}
#[test]
fn paint_order_stacking_later_sibling_wins() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let a = dom.create_element("a");
let b = dom.create_element("b");
dom.append_child(parent, a).unwrap();
dom.append_child(parent, b).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.flow(Flow::Flex)
.direction(Direction::Row)
.gap(0)
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
)
.rule_unchecked(
"a",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 1), Some(a));
assert_eq!(dom.hit_test(7, 1), Some(b));
dom.node_mut(parent).ext_mut().unwrap().scroll_x = 0;
dom.layout_dom(Rect::new(0, 0, 20, 10)); let overlap = dom.node(a).layout_rect().unwrap();
dom.node_mut(b).ext_mut().unwrap().layout = overlap;
assert_eq!(dom.hit_test(2, 1), Some(b));
}
#[test]
fn overflow_hidden_clips_children_hit_area() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.padding(Padding::all(1))
.overflow(Overflow::Hidden),
)
.rule_unchecked(
"span",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(0, 0), Some(parent));
assert_eq!(dom.hit_test(2, 1), Some(child));
}
#[test]
fn overflow_visible_allows_child_hit_past_parent_rect() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let parent = dom.create_element("div");
let child = dom.create_element("span");
dom.append_child(parent, child).unwrap();
dom.append_child(root, parent).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
)
.rule_unchecked(
"span",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 30, 10));
let mut rect = dom.node(child).layout_rect().unwrap();
rect.x = 3;
rect.width = 8; dom.node_mut(child).ext_mut().unwrap().layout = rect;
assert_eq!(dom.hit_test(1, 1), Some(parent));
assert_eq!(dom.hit_test(4, 1), Some(child));
assert_eq!(dom.hit_test(9, 1), None);
}
#[test]
fn ifc_block_returns_fragment_owner() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("ab ");
let code = dom.create_element("code");
let ct = dom.create_text_node("X");
dom.append_child(code, ct).unwrap();
dom.append_child(p, t1).unwrap();
dom.append_child(p, code).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(20))
.height(Size::Fixed(1)),
)
.rule_unchecked("code", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
let path = dom.hit_test_path(0, 0);
assert_eq!(path.last().copied(), Some(p));
let path = dom.hit_test_path(3, 0);
assert_eq!(path.last().copied(), Some(code));
assert!(
path.contains(&p),
"path must include p (IFC block) as ancestor"
);
}
#[test]
fn ifc_click_in_padding_returns_ifc_block_not_fragment() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("X");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap(); dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.padding(Padding::all(1))
.overflow(Overflow::Hidden),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(0, 0), Some(p));
}
#[test]
fn ifc_wrapped_inline_hittable_on_any_line() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let b = dom.create_element("b");
let bt = dom.create_text_node("aaa bbb ccc");
dom.append_child(b, bt).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(4)),
)
.rule_unchecked("b", TuiStyle::new().display(Display::Inline).bold(true));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
for y in 0..3 {
let path = dom.hit_test_path(0, y);
let last = path.last().copied();
assert_eq!(last, Some(b), "wrapped <b> must be hittable on line {y}");
assert!(path.contains(&p));
}
}
#[test]
fn path_includes_full_ancestor_chain() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
let b = dom.create_element("b");
let c = dom.create_element("c");
dom.append_child(b, c).unwrap();
dom.append_child(a, b).unwrap();
dom.append_child(root, a).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"a",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5)),
)
.rule_unchecked(
"b",
TuiStyle::new().width(Size::Fixed(8)).height(Size::Fixed(3)),
)
.rule_unchecked(
"c",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
let path = dom.hit_test_path(1, 0);
assert_eq!(path, vec![a, b, c]);
}
#[test]
fn path_empty_when_nothing_hit() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new().width(Size::Fixed(3)).height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test_path(50, 50), Vec::<NodeId>::new());
}
#[test]
fn reverse_document_order_descent_picks_last_sibling() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let first = dom.create_element("first");
let second = dom.create_element("second");
dom.append_child(root, first).unwrap();
dom.append_child(root, second).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"first",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(2)),
)
.rule_unchecked(
"second",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(2)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
let r = dom.node(first).layout_rect().unwrap();
dom.node_mut(second).ext_mut().unwrap().layout = r;
assert_eq!(dom.hit_test(2, 1), Some(second));
}
#[test]
fn hit_on_border_returns_the_element() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(6))
.height(Size::Fixed(4))
.border(Border::single())
.fg(Color::Rgb(255, 255, 255)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(0, 0), Some(div));
}
use crate::layout::UserSelect;
use rdom_core::Position;
#[test]
fn position_at_outside_viewport_returns_none() {
let mut dom: TuiDom = TuiDom::new();
prepare(&mut dom, &Stylesheet::bare(), Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(99, 99), None);
}
#[test]
fn position_at_inside_text_fragment_returns_source_position() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t, 2)));
assert_eq!(dom.position_at(4, 0), Some(Position::new(t, 4)));
}
#[test]
fn position_at_across_inline_element_boundary_uses_text_node_of_fragment() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("ab");
dom.append_child(p, t1).unwrap();
let code = dom.create_element("code");
let t2 = dom.create_text_node("XY");
dom.append_child(code, t2).unwrap();
dom.append_child(p, code).unwrap();
let t3 = dom.create_text_node("cd");
dom.append_child(p, t3).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("code", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t1, 0)));
assert_eq!(dom.position_at(1, 0), Some(Position::new(t1, 1)));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t2, 0)));
assert_eq!(dom.position_at(3, 0), Some(Position::new(t2, 1)));
assert_eq!(dom.position_at(4, 0), Some(Position::new(t3, 0)));
assert_eq!(dom.position_at(5, 0), Some(Position::new(t3, 1)));
}
#[test]
fn position_at_on_cjk_grapheme_snaps_to_start() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("中文");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(1, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t, 3)));
assert_eq!(dom.position_at(3, 0), Some(Position::new(t, 3)));
}
#[test]
fn position_at_respects_user_select_none() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hi");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.user_select(UserSelect::None),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), None);
assert_eq!(dom.position_at(1, 0), None);
}
#[test]
fn position_at_empty_space_in_user_select_none_does_not_snap_out() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let row = dom.create_element("row");
let cell = dom.create_element("cell");
let ct = dom.create_text_node("x");
dom.append_child(cell, ct).unwrap();
let cspan = dom.create_element("span");
dom.append_child(cell, cspan).unwrap();
dom.append_child(row, cell).unwrap();
dom.append_child(root, row).unwrap();
let title = dom.create_element("p");
let tt = dom.create_text_node("title");
dom.append_child(title, tt).unwrap();
let tspan = dom.create_element("span");
dom.append_child(title, tspan).unwrap();
dom.append_child(root, title).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"row",
TuiStyle::new()
.display(Display::Block)
.flow(Flow::Flex)
.direction(Direction::Row)
.width(Size::Fixed(20))
.height(Size::Fixed(1))
.user_select(UserSelect::None),
)
.rule_unchecked(
"cell",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(5)),
)
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(10, 0), None);
}
#[test]
fn position_at_user_select_none_reaches_the_auto_subtree() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let wrapper = dom.create_element("wrapper");
let p = dom.create_element("p");
let t = dom.create_text_node("hi");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(wrapper, p).unwrap();
dom.append_child(root, wrapper).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("wrapper", TuiStyle::new().user_select(UserSelect::None))
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), None);
}
#[test]
fn position_at_finds_explicit_text_inside_a_user_select_none_block() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t_no = dom.create_text_node("NO");
dom.append_child(p, t_no).unwrap();
let b = dom.create_element("b");
let t_yes = dom.create_text_node("YES");
dom.append_child(b, t_yes).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10))
.user_select(UserSelect::None),
)
.rule_unchecked(
"b",
TuiStyle::new()
.display(Display::Inline)
.user_select(UserSelect::Text),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(0, 0), None, "the `none` text");
assert_eq!(dom.position_at(3, 0), Some(Position::new(t_yes, 1)));
}
#[test]
fn higher_z_index_catches_click_first() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let hi = dom.create_element("hi");
let lo = dom.create_element("lo");
dom.append_child(root, hi).unwrap();
dom.append_child(root, lo).unwrap();
let base = || {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(5))
.height(Size::Fixed(2))
};
let sheet = Stylesheet::bare()
.rule_unchecked("hi", base().z_index(crate::layout::ZIndex::Value(5)))
.rule_unchecked("lo", base().z_index(crate::layout::ZIndex::Value(1)));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 0), Some(hi));
}
#[test]
fn positioned_catches_click_over_in_flow_content() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let tip = dom.create_element("tip");
let bar = dom.create_element("bar");
dom.append_child(root, tip).unwrap();
dom.append_child(root, bar).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"bar",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(3)),
)
.rule_unchecked(
"tip",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(5))
.height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 0), Some(tip));
assert_eq!(dom.hit_test(10, 0), Some(bar));
}
#[test]
fn positioned_layer_uses_doc_order_for_auto() {
let mut dom: TuiDom = TuiDom::new();
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 base = || {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(5))
.height(Size::Fixed(2))
};
let sheet = Stylesheet::bare()
.rule_unchecked("a", base())
.rule_unchecked("b", base());
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(2, 0), Some(b));
}
#[test]
fn click_outside_positioned_falls_through_to_in_flow() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let tip = dom.create_element("tip");
let bar = dom.create_element("bar");
dom.append_child(root, tip).unwrap();
dom.append_child(root, bar).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"bar",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(3)),
)
.rule_unchecked(
"tip",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(2))
.left(crate::layout::Length::Cells(10))
.width(Size::Fixed(3))
.height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test(5, 1), Some(bar));
}
#[test]
fn position_at_in_non_ifc_returns_none() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let sheet = Stylesheet::bare().rule_unchecked(
"div",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(3, 1), None);
}
#[test]
fn position_at_below_all_blocks_snaps_to_last_block_end() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p1 = dom.create_element("p");
let t1 = dom.create_text_node("alpha");
dom.append_child(p1, t1).unwrap();
let s1 = dom.create_element("span"); dom.append_child(p1, s1).unwrap();
dom.append_child(root, p1).unwrap();
let p2 = dom.create_element("p");
let t2 = dom.create_text_node("omega");
dom.append_child(p2, t2).unwrap();
let s2 = dom.create_element("span");
dom.append_child(p2, s2).unwrap();
dom.append_child(root, p2).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(3, 5), Some(Position::new(t2, 5)));
}
#[test]
fn position_at_above_all_blocks_snaps_to_first_block_start() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let spacer = dom.create_element("div"); dom.append_child(root, spacer).unwrap();
let p = dom.create_element("p");
let t = dom.create_text_node("hello");
dom.append_child(p, t).unwrap();
let s = dom.create_element("span");
dom.append_child(p, s).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("div", TuiStyle::new().height(Size::Fixed(3)))
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.width(Size::Fixed(10)),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.position_at(2, 1), Some(Position::new(t, 0)));
}
#[test]
fn pointer_events_none_falls_through_and_auto_child_is_hittable() {
use crate::layout::PointerEvents;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let base = dom.create_element("base");
let overlay = dom.create_element("overlay");
let button = dom.create_element("button");
dom.append_child(root, base).unwrap();
dom.append_child(root, overlay).unwrap();
dom.append_child(overlay, button).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"base",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(20))
.height(Size::Fixed(6)),
)
.rule_unchecked(
"overlay",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(20))
.height(Size::Fixed(6))
.pointer_events(PointerEvents::None),
)
.rule_unchecked(
"button",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(4))
.left(crate::layout::Length::Cells(10))
.width(Size::Fixed(5))
.height(Size::Fixed(1))
.pointer_events(PointerEvents::Auto),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 30, 10));
assert_eq!(
dom.hit_test(2, 1),
Some(base),
"overlay is transparent to the pointer"
);
assert_eq!(
dom.hit_test(12, 4),
Some(button),
"auto child inside a none parent is hit"
);
}
#[test]
fn scrolled_ifc_block_hits_the_fragment_owner_visible_on_that_row() {
use crate::layout::WhiteSpace;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let a1 = dom.create_element("a");
let a2 = dom.create_element("a");
let t1 = dom.create_text_node("first");
let gap = dom.create_text_node("\n\n");
let t2 = dom.create_text_node("third");
dom.append_child(a1, t1).unwrap();
dom.append_child(a2, t2).unwrap();
dom.append_child(p, a1).unwrap();
dom.append_child(p, gap).unwrap();
dom.append_child(p, a2).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(1))
.white_space(WhiteSpace::Pre)
.overflow_y(Overflow::Scroll)
.padding(Padding::all(0))
.border(Border::none()),
)
.rule_unchecked("a", TuiStyle::new().display(Display::Inline));
prepare(&mut dom, &sheet, Rect::new(0, 0, 30, 10));
assert_eq!(
dom.hit_test(1, 0),
Some(a1),
"unscrolled: first link on row 0"
);
dom.node_mut(p).ext_mut().unwrap().scroll_y = 2;
dom.layout_dom(Rect::new(0, 0, 30, 10));
assert_eq!(
dom.hit_test(1, 0),
Some(a2),
"scrolled by two: the third line's link is on row 0"
);
}
fn abs_box() -> TuiStyle {
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1))
}
#[test]
fn nested_context_click_goes_to_the_higher_sibling_context() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
let a1 = dom.create_element("a1");
let b = dom.create_element("b");
dom.append_child(a, a1).unwrap();
dom.append_child(root, a).unwrap();
dom.append_child(root, b).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", abs_box().z_index(crate::layout::ZIndex::Value(1)))
.rule_unchecked("a1", abs_box().z_index(crate::layout::ZIndex::Value(100)))
.rule_unchecked("b", abs_box().z_index(crate::layout::ZIndex::Value(2)));
prepare(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(dom.hit_test(1, 0), Some(b));
}
#[test]
fn negative_z_click_falls_to_in_flow_content_above_it() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let wrap = dom.create_element("wrap");
let c = dom.create_element("c");
let n = dom.create_element("n");
dom.append_child(wrap, c).unwrap();
dom.append_child(wrap, n).unwrap();
dom.append_child(root, wrap).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"wrap",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.z_index(crate::layout::ZIndex::Value(0))
.width(Size::Fixed(10))
.height(Size::Fixed(2)),
)
.rule_unchecked(
"c",
TuiStyle::new().width(Size::Fixed(5)).height(Size::Fixed(1)),
)
.rule_unchecked(
"n",
abs_box()
.width(Size::Fixed(10))
.z_index(crate::layout::ZIndex::Value(-1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(dom.hit_test(1, 0), Some(c), "in-flow content above z:-1");
assert_eq!(
dom.hit_test(7, 0),
Some(n),
"z:-1 above the context's own box"
);
}
#[test]
fn relative_element_catches_click_over_a_later_sibling() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let wrap = dom.create_element("wrap");
let r = dom.create_element("r");
let s = dom.create_element("s");
dom.append_child(wrap, r).unwrap();
dom.append_child(wrap, s).unwrap();
dom.append_child(root, wrap).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("wrap", TuiStyle::new().width(Size::Fixed(10)))
.rule_unchecked(
"r",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.top(crate::layout::Length::Cells(1))
.height(Size::Fixed(1)),
)
.rule_unchecked("s", TuiStyle::new().height(Size::Fixed(1)));
prepare(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(dom.hit_test(1, 1), Some(r));
}
#[test]
fn absolute_clipped_by_a_scroll_container_is_not_hittable_outside_it() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let list = dom.create_element("list");
let item = dom.create_element("item");
let pop = dom.create_element("pop");
dom.append_child(item, pop).unwrap();
dom.append_child(list, item).unwrap();
dom.append_child(root, list).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"list",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(2))
.overflow(Overflow::Hidden),
)
.rule_unchecked(
"item",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.height(Size::Fixed(1)),
)
.rule_unchecked("pop", abs_box().top(crate::layout::Length::Cells(4)));
prepare(&mut dom, &sheet, Rect::new(0, 0, 10, 8));
assert_eq!(dom.hit_test(0, 4), None);
}
#[test]
fn path_through_a_positioned_box_keeps_its_ancestors() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let pane = dom.create_element("pane");
let rel = dom.create_element("rel");
let button = dom.create_element("button");
let abs = dom.create_element("abs");
dom.append_child(rel, button).unwrap();
dom.append_child(rel, abs).unwrap();
dom.append_child(pane, rel).unwrap();
dom.append_child(root, pane).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"pane",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(5))
.overflow(Overflow::Auto),
)
.rule_unchecked(
"rel",
TuiStyle::new()
.position(crate::layout::Position::Relative)
.height(Size::Fixed(3)),
)
.rule_unchecked(
"button",
TuiStyle::new().width(Size::Fixed(4)).height(Size::Fixed(1)),
)
.rule_unchecked(
"abs",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(2))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(3))
.height(Size::Fixed(1)),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(dom.hit_test_path(1, 0), vec![pane, rel, button]);
assert_eq!(dom.hit_test_path(1, 2), vec![pane, rel, abs]);
}
#[test]
fn path_through_nested_stacking_contexts_keeps_every_root() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let a = dom.create_element("a");
let mid = dom.create_element("mid");
let a1 = dom.create_element("a1");
dom.append_child(mid, a1).unwrap();
dom.append_child(a, mid).unwrap();
dom.append_child(root, a).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("a", abs_box().z_index(crate::layout::ZIndex::Value(1)))
.rule_unchecked("mid", TuiStyle::new().height(Size::Fixed(1)))
.rule_unchecked("a1", abs_box().z_index(crate::layout::ZIndex::Value(2)));
prepare(&mut dom, &sheet, Rect::new(0, 0, 10, 5));
assert_eq!(dom.hit_test_path(1, 0), vec![a, mid, a1]);
}
#[test]
fn transparent_context_root_falls_through_but_its_content_is_hittable() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let under = dom.create_element("under");
let ctx = dom.create_element("ctx");
let child = dom.create_element("child");
dom.append_child(ctx, child).unwrap();
dom.append_child(root, under).unwrap();
dom.append_child(root, ctx).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"under",
TuiStyle::new()
.width(Size::Fixed(10))
.height(Size::Fixed(3)),
)
.rule_unchecked(
"ctx",
abs_box()
.width(Size::Fixed(10))
.height(Size::Fixed(3))
.z_index(crate::layout::ZIndex::Value(1))
.pointer_events(crate::layout::PointerEvents::None),
)
.rule_unchecked(
"child",
TuiStyle::new()
.width(Size::Fixed(3))
.height(Size::Fixed(1))
.pointer_events(crate::layout::PointerEvents::Auto),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 10));
assert_eq!(
dom.hit_test_path(1, 0),
vec![child],
"content of a transparent root"
);
assert_eq!(
dom.hit_test_path(8, 2),
vec![under],
"bare area falls through"
);
}
#[test]
fn transparent_ifc_block_still_exposes_its_auto_inline_descendants() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let under = dom.create_element("under");
let p = dom.create_element("p");
let lead = dom.create_text_node("aaaa ");
let b = dom.create_element("b");
let bt = dom.create_text_node("bbbb");
dom.append_child(b, bt).unwrap();
dom.append_child(p, lead).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(root, under).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"under",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"p",
TuiStyle::new()
.position(crate::layout::Position::Absolute)
.top(crate::layout::Length::Cells(0))
.left(crate::layout::Length::Cells(0))
.width(Size::Fixed(20))
.height(Size::Fixed(1))
.pointer_events(crate::layout::PointerEvents::None),
)
.rule_unchecked(
"b",
TuiStyle::new()
.display(Display::Inline)
.pointer_events(crate::layout::PointerEvents::Auto),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 5));
assert_eq!(
dom.hit_test_path(6, 0),
vec![b],
"the auto inline is hit, the block is not on the path"
);
assert_eq!(
dom.hit_test_path(1, 0),
vec![under],
"the block's own text falls through"
);
}
#[test]
fn transparent_inline_resolves_to_its_nearest_auto_ancestor() {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let lead = dom.create_text_node("aaaa ");
let b = dom.create_element("b");
let bt = dom.create_text_node("bbbb");
dom.append_child(b, bt).unwrap();
dom.append_child(p, lead).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.width(Size::Fixed(20))
.height(Size::Fixed(1)),
)
.rule_unchecked(
"b",
TuiStyle::new()
.display(Display::Inline)
.pointer_events(crate::layout::PointerEvents::None),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 5));
assert_eq!(dom.hit_test_path(6, 0), vec![p]);
}
#[test]
fn position_at_maps_cells_past_a_before_pseudo_to_text_offsets() {
use crate::style::Content;
use rdom_core::Position;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("abcdefg hi");
dom.append_child(p, t).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::bare()
.rule_unchecked("p", TuiStyle::new().width(Size::Fixed(10)))
.rule_unchecked(
"p::before",
TuiStyle::new().content(Content::Str("> ".into())),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 5));
assert_eq!(dom.position_at(2, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(4, 0), Some(Position::new(t, 2)));
assert_eq!(dom.position_at(0, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(1, 0), Some(Position::new(t, 0)));
assert_eq!(dom.position_at(1, 1), Some(Position::new(t, 9)));
}
#[test]
fn position_at_skips_an_inline_elements_generated_cells() {
use crate::style::Content;
use rdom_core::Position;
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let a = dom.create_text_node("a ");
let b = dom.create_element("b");
let bold = dom.create_text_node("bold");
let c = dom.create_text_node(" c");
dom.append_child(b, bold).unwrap();
dom.append_child(p, a).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(p, c).unwrap();
dom.append_child(root, p).unwrap();
let sheet = Stylesheet::new()
.rule_unchecked(
"b::before",
TuiStyle::new().content(Content::Str("[".into())),
)
.rule_unchecked(
"b::after",
TuiStyle::new().content(Content::Str("]".into())),
);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 2));
assert_eq!(dom.position_at(0, 0), Some(Position::new(a, 0)));
assert_eq!(dom.position_at(3, 0), Some(Position::new(bold, 0)));
assert_eq!(dom.position_at(6, 0), Some(Position::new(bold, 3)));
assert_eq!(dom.position_at(9, 0), Some(Position::new(c, 1)));
for x in [2, 7] {
let pos = dom.position_at(x, 0).expect("clamps to text");
assert!(
[a, bold, c].contains(&pos.node),
"cell {x} resolved to {pos:?}"
);
}
}
#[test]
fn hit_test_on_an_inline_elements_generated_cell_targets_the_host() {
use crate::layout::PointerEvents;
use crate::style::Content;
let build = |pe: Option<PointerEvents>| {
let mut dom: TuiDom = TuiDom::new();
let root = dom.root();
let p = dom.create_element("p");
let x = dom.create_text_node("x ");
let a = dom.create_element("a");
dom.set_attribute(a, "href", "https://example.com").unwrap();
let go = dom.create_text_node("go");
dom.append_child(a, go).unwrap();
dom.append_child(p, x).unwrap();
dom.append_child(p, a).unwrap();
dom.append_child(root, p).unwrap();
let mut before = TuiStyle::new().content(Content::Str("> ".into()));
if let Some(pe) = pe {
before = before.pointer_events(pe);
}
let sheet = Stylesheet::new().rule_unchecked("a::before", before);
prepare(&mut dom, &sheet, Rect::new(0, 0, 20, 2));
(dom, p, a)
};
let (dom, _p, a) = build(None);
assert_eq!(dom.hit_test(2, 0), Some(a));
assert_eq!(dom.hit_test(4, 0), Some(a));
let (dom, p, _a) = build(Some(PointerEvents::None));
assert_eq!(dom.hit_test(2, 0), Some(p));
}