mod common;
use common::{FONT_ASCENT_EM, STRUT_EM, SYNTHETIC_ASCENT_EM, register_synthetic};
use velox_dom::layout::{LayoutNode, compute_layout};
use velox_dom::{VNode, h};
const FS: i32 = 16;
const STRUT: i32 = (FS as f32 * STRUT_EM).round() as i32;
const TALL_RUN: i32 = 40;
fn text(s: &str) -> VNode {
VNode::Text(s.to_string())
}
fn inner_of(v: &VNode) -> LayoutNode {
compute_layout(&h("div", vec![], vec![v.clone()]), 600, 600).children[0].clone()
}
fn one_line_h(s: &str) -> i32 {
register_synthetic();
let st = format!("font-size:{FS}px;white-space:pre");
inner_of(&h("div", vec![("style", st.as_str())], vec![text(s)]))
.rect
.h
}
fn pre_div(children: Vec<VNode>) -> VNode {
let st = format!("font-size:{FS}px;white-space:pre");
h("div", vec![("style", st.as_str())], children)
}
#[test]
fn every_emitted_node_carries_the_index_of_the_vnode_it_came_from() {
register_synthetic();
let d = inner_of(&pre_div(vec![
h("b", vec![], vec![text("ccc")]),
text(" ooo"),
h("i", vec![], vec![text("xxx")]),
text(" ccc"),
]));
let idx = |n: &LayoutNode| n.source_index.expect("a node with no index is unreachable");
assert_eq!(
d.children.len(),
4,
"four VNode children, four layout nodes"
);
assert_eq!(idx(&d.children[0]), 0, "the <b>");
assert_eq!(idx(&d.children[1]), 1, "the text between");
assert_eq!(idx(&d.children[2]), 2, "the <i>");
assert_eq!(idx(&d.children[3]), 3, "the text after");
assert_eq!(d.children[0].children.len(), 1);
assert_eq!(idx(&d.children[0].children[0]), 0, "the <b>'s own text");
}
#[test]
fn an_inline_element_with_no_text_still_gets_a_box() {
register_synthetic();
let d = inner_of(&pre_div(vec![h("span", vec![], vec![]), text("ccc")]));
assert_eq!(
d.children.len(),
2,
"the empty span must not vanish from the tree"
);
let empty = &d.children[0];
assert_eq!(empty.source_index, Some(0));
assert_eq!(empty.rect.w, 0, "a box with nothing in it is zero wide");
assert_eq!(
d.children[1].rect.x, 0,
"and it takes no room: the text starts at the line's left edge"
);
}
#[test]
fn a_line_is_as_tall_as_the_run_in_it_when_the_run_is_taller_than_the_strut() {
assert_eq!(
one_line_h("Hg"),
TALL_RUN,
"2.5em of ink over a 1.362em strut"
);
assert_eq!(one_line_h("xxx"), STRUT, "0.30em of ink under the strut");
}
#[test]
fn two_lines_are_two_independent_line_boxes() {
register_synthetic();
let d = inner_of(&pre_div(vec![text("Hg\nxxx")]));
assert_eq!(
d.rect.h,
TALL_RUN + STRUT,
"one tall line, then one strut line"
);
assert_eq!(
d.children[0].rect.y,
((SYNTHETIC_ASCENT_EM - FONT_ASCENT_EM) * FS as f32).round() as i32,
"a fragment hangs from the baseline, so it starts below its line's top \
by exactly the amount its ink overshot the strut"
);
assert_eq!(
d.children[1].rect.y, TALL_RUN,
"the second line starts below the first"
);
}
#[test]
fn a_text_fragment_box_is_its_own_fonts_content_box_hanging_from_the_baseline() {
register_synthetic();
let d = inner_of(&pre_div(vec![h(
"span",
vec![("style", "font-size:32px")],
vec![text("xxx")],
)]));
let frag = &d.children[0].children[0];
assert_eq!(
frag.rect.h,
(32.0 * STRUT_EM).round() as i32,
"a 32px run's box is 32px's content area, not its 0.30em of x-height ink"
);
assert_eq!(
d.rect.h, frag.rect.h,
"so the line is the 32px content area, not the 16px strut it is laid out \
in: a run in a larger font grows the line even when its own x-height \
ink is tiny"
);
assert_eq!(
d.children[0].rect.h, frag.rect.h,
"and the inline element's box is the union of its fragments"
);
}
#[test]
fn a_run_that_overshoots_the_strut_makes_the_line_taller_without_taller_boxes() {
register_synthetic();
let d = inner_of(&pre_div(vec![text("Hg\nccc")]));
for line in &d.children {
assert_eq!(
line.rect.h, STRUT,
"the 2.5em ink grew its LINE, and no box on it"
);
}
assert_eq!(d.rect.h, TALL_RUN + STRUT);
}
#[test]
fn a_line_may_break_at_the_edge_of_an_inline_element() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:40px")],
vec![h("b", vec![], vec![text("ccc")]), text(" ooo ccc")],
));
let x = |n: &LayoutNode| n.rect.x;
let y = |n: &LayoutNode| n.rect.y;
let w = |n: &LayoutNode| n.rect.w;
assert_eq!(
d.children.len(),
4,
"the break splits the one text VNode in two"
);
assert_eq!(
(x(&d.children[0]), y(&d.children[0]), w(&d.children[0])),
(0, 0, 24)
);
assert_eq!(
(x(&d.children[1]), y(&d.children[1]), w(&d.children[1])),
(24, 0, 8),
"the space hangs on the first line, where it was written"
);
assert_eq!(
(x(&d.children[2]), y(&d.children[2]), w(&d.children[2])),
(0, STRUT, 32),
"\"ooo \" is the second line"
);
assert_eq!(
(x(&d.children[3]), y(&d.children[3]), w(&d.children[3])),
(0, 2 * STRUT, 24)
);
assert_eq!(d.rect.h, 3 * STRUT, "three lines");
}
#[test]
fn an_inline_element_deep_inside_another_one_joins_the_same_run() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:40px")],
vec![h(
"b",
vec![],
vec![
text("ccc "),
h("i", vec![], vec![text("ooo")]),
text(" ccc"),
],
)],
));
assert_eq!(d.children.len(), 3, "one <b> node per line it is on");
assert_eq!(d.children[0].rect.w, 32, "\"ccc \"");
assert_eq!(d.children[1].rect.y, STRUT, "the middle line");
assert_eq!(d.children[1].rect.w, 32, "\"ooo \"");
assert_eq!(
d.children[1].children.len(),
2,
"the <i> and the space after it"
);
assert_eq!(d.children[2].rect.y, 2 * STRUT, "the last line");
assert_eq!(d.children[2].rect.w, 24, "\"ccc\"");
assert_eq!(
d.children[2].children[0].source_index,
Some(2),
"and it is the <b>'s own third child, not the <i> again"
);
}
#[test]
fn a_word_is_never_split_across_lines() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:40px")],
vec![text("cccccccccc")],
));
assert_eq!(
d.children.len(),
1,
"one fragment, however far it overflows"
);
assert_eq!(d.children[0].rect.w, 80, "5 chars fit, 10 were written");
}
#[test]
fn an_inline_block_moves_to_the_next_line_rather_than_overflowing() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:40px")],
vec![
text("ccc "),
h(
"span",
vec![("style", "display:inline-block")],
vec![text("ccc ccc ccc")],
),
],
));
assert_eq!(d.children[1].rect.y, STRUT, "it starts the second line");
assert_eq!(d.children[1].rect.x, 0, "at the line's left edge");
assert_eq!(
d.children[1].rect.w, 40,
"and is sized against the line it is ON, not the one it was offered"
);
}
#[test]
fn an_inline_element_has_no_box_of_its_own() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![text("Hg "), h("b", vec![], vec![text("ccc")])],
));
let b = &d.children[1];
assert_eq!(b.rect.w, 24, "exactly its text, not the 200px line");
assert_eq!(
b.rect.h, STRUT,
"exactly its own font's content area, not the line box it is on"
);
assert_eq!(
d.rect.h, TALL_RUN,
"the line is the 2.5em run beside it, NOT grown by the <b>: an inline \
element has no box, so it is not 40px tall"
);
}
#[test]
fn a_block_child_ends_the_inline_run() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![
text("ccc"),
h("div", vec![("style", "height:10px")], vec![]),
text("ccc"),
],
));
assert_eq!(d.children.len(), 3);
assert_eq!(
d.children[1].rect.y, STRUT,
"the block starts below the first line"
);
assert_eq!(
d.children[1].rect.h, 10,
"and is a block: 10px, not the strut"
);
assert_eq!(
d.children[2].rect.y,
STRUT + 10,
"the run after it starts a fresh line below the block, and the line it \
ended with is charged once, not twice"
);
assert_eq!(d.rect.h, 2 * STRUT + 10);
}
#[test]
fn an_inline_block_is_atomic_and_shares_the_line_with_the_text_around_it() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![
h(
"span",
vec![("style", "display:inline-block")],
vec![text("ccc")],
),
text(" ooo ccc"),
],
));
let (ib, txt) = (&d.children[0], &d.children[1]);
assert_eq!(
(ib.rect.x, ib.rect.w),
(0, 24),
"sized to its content, not the 200px line"
);
assert_eq!(txt.rect.x, 24, "the text after it starts where it ends");
assert!(
(txt.rect.y - ib.rect.y).abs() < STRUT,
"one line, so both are in it: {:?}",
(ib.rect.y, txt.rect.y)
);
assert_eq!(d.children.len(), 2, "the atomic was not split");
}
#[test]
fn an_inline_block_is_never_split_across_lines() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![h(
"span",
vec![("style", "display:inline-block;width:80px")],
vec![text("ccc ccc ccc ccc")],
)],
));
let ib = &d.children[0];
assert_eq!(
d.children.len(),
1,
"one node: the atomic is not a run of fragments"
);
assert_eq!(ib.rect.w, 80, "its declared width");
assert_eq!(
ib.rect.h,
2 * STRUT,
"it WRAPPED INSIDE ITSELF, which is the whole difference between an \
atomic inline and the text around it"
);
assert_eq!(ib.children.len(), 2, "its own two line boxes");
}
#[test]
fn an_inline_block_shrinks_to_the_space_left_on_its_line() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:40px")],
vec![h(
"span",
vec![("style", "display:inline-block")],
vec![text("ccc ccc ccc")],
)],
));
assert_eq!(d.children[0].rect.w, 40, "72px clamped to the 40px line");
assert_eq!(
d.children[0].rect.h,
3 * STRUT,
"so it wrapped to three lines"
);
}
#[test]
fn an_inline_block_shares_the_baseline_of_the_text_beside_it() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![
text("Hg "),
h(
"span",
vec![("style", "display:inline-block")],
vec![text("ccc")],
),
],
));
assert_eq!(
d.children[0].rect.y, d.children[1].rect.y,
"the atomic's box is aligned by its own baseline, which here coincides \
with the text's"
);
assert_eq!(
d.rect.h, TALL_RUN,
"and the line is the tall run's, not the strut's"
);
}
#[test]
fn all_four_supported_alignments_land_somewhere_different() {
register_synthetic();
let y_of = |al: &str| -> i32 {
let st = format!("font-size:8px;vertical-align:{al}");
let d = inner_of(&pre_div(vec![
text("ccc\n"),
h("span", vec![("style", st.as_str())], vec![text("ccc")]),
]));
assert_eq!(
d.children[0].rect.h, STRUT,
"the first line is a strut line"
);
d.children[1].rect.y
};
let top = y_of("top");
let bottom = y_of("bottom");
let baseline = y_of("baseline");
let middle = y_of("middle");
assert_eq!(top, STRUT, "`top` puts the box at the line box's top edge");
assert!(
top < baseline,
"a baseline-aligned box is below the line's top edge"
);
assert!(top < bottom, "and so is a bottom-aligned one");
for (name, v) in [
("baseline", baseline),
("bottom", bottom),
("middle", middle),
] {
assert_ne!(v, top, "a {name}-aligned box is not a top-aligned one");
}
}
#[test]
fn baseline_is_the_default() {
register_synthetic();
let declared = inner_of(&pre_div(vec![
text("ccc\n"),
h(
"span",
vec![("style", "font-size:8px;vertical-align:baseline")],
vec![text("ccc")],
),
]));
let absent = inner_of(&pre_div(vec![
text("ccc\n"),
h("span", vec![("style", "font-size:8px")], vec![text("ccc")]),
]));
assert_eq!(
declared.children[1].rect.y, absent.children[1].rect.y,
"saying `baseline` and saying nothing are the same box"
);
}
#[test]
fn vertical_align_is_inherited() {
register_synthetic();
let st = format!("font-size:{FS}px;white-space:pre;vertical-align:top");
let d = inner_of(&h(
"div",
vec![("style", st.as_str())],
vec![text("Hg\nHg")],
));
assert_eq!(
d.rect.h,
2 * STRUT,
"both lines are strut lines: an 8px-free top-aligned 2.5em run does not \
grow a line, because what is aligned is its BOX, not its ink"
);
assert_eq!(
d.children[0].rect.y, 0,
"the first box is at the first line's top"
);
assert_eq!(
d.children[1].rect.y, STRUT,
"and the second at the second line's top"
);
}
#[test]
fn sub_and_super_are_not_supported_and_say_so_by_not_being_parsed() {
use velox_dom::style::VerticalAlign;
assert_eq!(
VerticalAlign::parse("sub"),
None,
"documented as unsupported"
);
assert_eq!(
VerticalAlign::parse("super"),
None,
"documented as unsupported"
);
assert_eq!(VerticalAlign::parse("text-top"), None, "also unsupported");
assert_eq!(
VerticalAlign::parse("10%"),
None,
"a length is legal CSS and is not read as baseline"
);
register_synthetic();
let sub = inner_of(&pre_div(vec![
text("ccc\n"),
h(
"span",
vec![("style", "font-size:8px;vertical-align:sub")],
vec![text("ccc")],
),
]));
let plain = inner_of(&pre_div(vec![
text("ccc\n"),
h("span", vec![("style", "font-size:8px")], vec![text("ccc")]),
]));
assert_eq!(
sub.children[1].rect.y, plain.children[1].rect.y,
"an unparsed value leaves the box exactly where no value would"
);
}
#[test]
fn inline_flex_is_not_flattened_into_the_enclosing_line() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![h(
"div",
vec![("style", "display:inline-flex;width:200px")],
vec![
h("div", vec![("style", "width:40px")], vec![]),
h("div", vec![("style", "width:60px")], vec![]),
],
)],
));
assert_eq!(
d.children.len(),
1,
"the inline-flex box is a block child, not run material"
);
assert_eq!(
d.children[0].children.len(),
2,
"its children are inside it"
);
}
#[test]
fn a_flex_container_inside_a_block_still_lays_out_as_flex() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![h(
"div",
vec![("style", "display:flex")],
vec![
h("div", vec![("style", "width:40px")], vec![]),
h("div", vec![("style", "width:60px")], vec![]),
],
)],
));
assert_eq!(d.children.len(), 1);
assert_eq!(d.children[0].children.len(), 2);
}
#[test]
fn an_out_of_flow_inline_is_still_out_of_flow() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px")],
vec![
h(
"span",
vec![("style", "position:absolute;left:0;top:0")],
vec![text("ccc")],
),
text("ccc"),
],
));
assert_eq!(d.children.len(), 2, "one out-of-flow box and one text node");
assert_eq!(
d.children[0].source_index,
Some(1),
"the in-flow text is in the line"
);
assert_eq!(
d.children[0].rect.x, 0,
"and it starts at the line's left edge"
);
assert_eq!(
d.children[1].source_index,
Some(0),
"the out-of-flow box is not"
);
assert_eq!(
d.rect.h, STRUT,
"one line: the out-of-flow box did not add one"
);
}
#[test]
fn text_align_center_still_centres_an_inline_line() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px;text-align:center")],
vec![h("b", vec![], vec![text("ccc")])],
));
assert_eq!(d.children[0].rect.w, 24, "\"ccc\" is 3 * 0.5em");
assert_eq!(
d.children[0].rect.x,
(200 - 24) / 2,
"and it is centred on the 200px line"
);
}
#[test]
fn text_align_right_still_right_aligns_an_inline_line() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "font-size:16px;width:200px;text-align:right")],
vec![h("b", vec![], vec![text("ccc")])],
));
assert_eq!(d.children[0].rect.x, 200 - 24);
}
#[test]
fn an_ellipsis_is_reserved_space_in_the_layout_tree_and_never_overflows_its_line() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![(
"style",
"width:40px;white-space:nowrap;text-overflow:ellipsis",
)],
vec![text("aaaaaaaaaa")],
));
assert_eq!(
d.children.len(),
1,
"one overflowing line is one fragment, got {:?}",
d.children.iter().map(|c| c.rect).collect::<Vec<_>>()
);
assert_eq!(
d.children[0].rect.w, 40,
"four characters plus the ellipsis is exactly the 40px line; anything \
less means a character was dropped that fitted"
);
assert!(
d.children[0].rect.w <= 40,
"an ellipsis is RESERVED space, so a truncated fragment can never be \
wider than the line it is clipped in; got {}",
d.children[0].rect.w
);
}
#[test]
fn without_the_property_the_same_text_overflows_its_line_unchanged() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:40px;white-space:nowrap")],
vec![text("aaaaaaaaaa")],
));
assert_eq!(
d.children[0].rect.w, 80,
"with no `text-overflow` the fragment keeps its full width and overflows, \
which is what distinguishes this case from the truncated one"
);
}
#[test]
fn the_ellipsis_lands_in_the_piece_where_the_text_ran_out_not_the_first_one() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![(
"style",
"width:40px;white-space:nowrap;text-overflow:ellipsis",
)],
vec![h("b", vec![], vec![text("aa")]), text(" bbbbbb")],
));
let b = d
.children
.iter()
.find(|c| c.children.len() == 1)
.expect("the <b> is a leaf one level down");
let after = d
.children
.iter()
.find(|c| c.children.is_empty())
.expect("the text after it is a bare fragment");
assert_eq!(
(b.rect.w, after.rect.w),
(16, 24),
"the <b> keeps `aa` at 16 and the text piece becomes ` b` + ellipsis at 24; \
if the ellipsis had gone into the <b> these would be 24 and 16"
);
}
#[test]
fn an_atomic_inline_box_with_a_declared_width_truncates_its_content_to_it() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:200px")],
vec![h(
"span",
vec![(
"style",
"display:inline-block;width:40px;white-space:nowrap;text-overflow:ellipsis",
)],
vec![text("aaaaaaaaaa")],
)],
));
let atomic = &d.children[0];
assert_eq!(atomic.rect.w, 40, "the atomic keeps its declared width");
assert_eq!(
atomic.children[0].rect.w, 40,
"and its own content truncates to it, so 80px of text becomes four \
characters plus an ellipsis and the box does not overflow"
);
}
#[test]
fn an_atomic_with_no_declared_width_shrink_fits_to_the_line_and_truncates_its_label() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:40px")],
vec![h(
"span",
vec![(
"style",
"display:inline-block;white-space:nowrap;text-overflow:ellipsis",
)],
vec![text("aaaaaaaaaa")],
)],
));
let atomic = &d.children[0];
assert_eq!(
atomic.rect.w, 40,
"the preferred width is 80, the line is 40, and `min` is the line -- this is \
the clamp, and `available`, not a declared width, is what put it here"
);
assert_eq!(
atomic.children[0].rect.w, 40,
"and the label inside it is truncated to that clamp, four characters plus the \
ellipsis, which is the case the declared-width test cannot reach: nothing \
here short-circuits the shrink-to-fit"
);
}
#[test]
fn without_the_property_the_shrink_fitted_label_overflows_the_clamp_that_did_not_move() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:40px")],
vec![h(
"span",
vec![("style", "display:inline-block;white-space:nowrap")],
vec![text("aaaaaaaaaa")],
)],
));
let atomic = &d.children[0];
assert_eq!(
atomic.rect.w, 40,
"the clamp is the line either way -- this number is NOT what the property \
controls, and the test above's identical 40 is not evidence of anything"
);
assert_eq!(
atomic.children[0].rect.w, 80,
"without `text-overflow` the label keeps its full 80 and overflows the 40 it \
was clamped into, which is the whole difference between these two cases and \
what makes the test above a measurement rather than a coincidence"
);
}
#[test]
fn a_wrapping_block_does_not_truncate_which_is_a_recorded_divergence() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:40px;text-overflow:ellipsis")],
vec![text("aaaa aaaa")],
));
assert_eq!(
d.children.iter().map(|c| c.rect.w).collect::<Vec<_>>(),
vec![40, 32],
"the run wrapped, so the wrapper's rule leaves it alone; matching a browser \
needs to know which line is the clipped one, which needs the clip region \
the layout tree does not carry"
);
}
#[test]
fn a_pre_block_truncates_each_of_its_own_overflowing_lines() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:40px;white-space:pre;text-overflow:ellipsis")],
vec![text("aaaaaaaaaa\nbbbbbbbbbb")],
));
assert_eq!(
d.children.iter().map(|c| c.rect.w).collect::<Vec<_>>(),
vec![40, 40],
"both lines are their own line box and both overflow, so both truncate"
);
}
#[test]
fn an_empty_atomic_inline_box_is_zero_wide_plus_its_own_padding_and_border() {
register_synthetic();
for (label, style, want) in [
("no padding", "display:inline-block", 0),
("5px padding", "display:inline-block;padding:5px", 10),
] {
let d = inner_of(&h(
"div",
vec![("style", "width:200px")],
vec![h("span", vec![("style", style)], vec![])],
));
assert_eq!(
d.children[0].rect.w, want,
"an empty atomic ({label}) is its content area of 0 plus its own inset, \
not the probe width it used to get"
);
}
}
#[test]
fn an_atomic_inline_box_whose_only_child_is_display_none_is_zero_wide() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:200px")],
vec![h(
"span",
vec![("style", "display:inline-block")],
vec![h(
"div",
vec![("style", "display:none")],
vec![text("aaaaaaaaaa")],
)],
)],
));
assert_eq!(
d.children[0].rect.w, 0,
"a child that is not laid out contributes no content, so the atomic's \
content width is 0"
);
}
#[test]
fn an_atomic_inline_box_wider_than_the_line_is_the_line_wide() {
register_synthetic();
let d = inner_of(&h(
"div",
vec![("style", "width:40px")],
vec![h(
"span",
vec![("style", "display:inline-block")],
vec![text("aaaaaaaaaa")],
)],
));
assert_eq!(
d.children[0].rect.w, 40,
"a preferred width wider than the line is clamped to the line, which is \
what `min(max_content, available)` says and what the probe width used to \
override"
);
}