use super::*;
use crate::TuiDom;
use crate::layout::Display;
use crate::style::{CascadeExt, Stylesheet, TuiStyle};
fn prepared(template: impl FnOnce(&mut TuiDom) -> NodeId, sheet: &Stylesheet) -> (TuiDom, NodeId) {
let mut dom: TuiDom = TuiDom::new();
let id = template(&mut dom);
dom.cascade(sheet);
(dom, id)
}
fn default_sheet() -> Stylesheet {
Stylesheet::bare()
.rule_unchecked("p", TuiStyle::new().display(Display::Block))
.rule_unchecked("b", TuiStyle::new().display(Display::Inline))
.rule_unchecked("i", TuiStyle::new().display(Display::Inline))
.rule_unchecked("span", TuiStyle::new().display(Display::Inline))
}
#[test]
fn single_line_fits() {
let (dom, p) = prepared(
|dom| {
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();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 20);
assert_eq!(layout.height(), 1);
assert_eq!(layout.lines[0].fragments.len(), 1);
assert_eq!(layout.lines[0].fragments[0].text, "hello");
assert_eq!(layout.lines[0].width, 5);
}
#[test]
fn wraps_at_word_boundary() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("the quick brown fox");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 10);
assert!(layout.height() >= 2, "must wrap");
let line_strings: Vec<String> = layout
.lines
.iter()
.map(|l| {
l.fragments
.iter()
.map(|f| f.text.clone())
.collect::<String>()
})
.collect();
assert_eq!(line_strings, vec!["the quick", "brown fox"]);
}
#[test]
fn long_word_overflows_its_line() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("supercalifragilisticexpialidocious");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 10);
assert_eq!(layout.height(), 1);
assert!(layout.lines[0].width > 10);
}
#[test]
fn cjk_breaks_between_graphemes() {
let (dom, p) = prepared(
|dom| {
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();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 6);
assert_eq!(layout.height(), 2);
assert_eq!(layout.lines[0].width, 6);
assert_eq!(layout.lines[1].width, 6);
}
#[test]
fn hyphen_breaks_after() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("state-of-the-art");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 10);
assert!(layout.height() >= 2);
let line_texts: Vec<String> = layout
.lines
.iter()
.map(|l| {
l.fragments
.iter()
.map(|f| f.text.clone())
.collect::<String>()
})
.collect();
assert_eq!(line_texts, vec!["state-of-", "the-art"]);
}
#[test]
fn fragment_owner_is_direct_element_parent() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("a ");
let b = dom.create_element("b");
let bt = dom.create_text_node("bold");
dom.append_child(b, bt).unwrap();
let t2 = dom.create_text_node(" c");
dom.append_child(p, t1).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(p, t2).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 20);
let line = &layout.lines[0];
assert_eq!(line.fragments.len(), 3);
assert_eq!(line.fragments[0].text, "a ");
assert_eq!(line.fragments[1].text, "bold");
assert_eq!(line.fragments[2].text, " c");
assert_ne!(line.fragments[0].node, line.fragments[1].node);
assert_eq!(line.fragments[0].node, line.fragments[2].node);
}
#[test]
fn wrap_preserves_fragment_ownership() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t1 = dom.create_text_node("aaa ");
let b = dom.create_element("b");
let bt = dom.create_text_node("bbb ccc");
dom.append_child(b, bt).unwrap();
let t2 = dom.create_text_node(" ddd");
dom.append_child(p, t1).unwrap();
dom.append_child(p, b).unwrap();
dom.append_child(p, t2).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 8);
let line1 = &layout.lines[0];
let line2 = &layout.lines[1];
let l1_text: String = line1.fragments.iter().map(|f| f.text.clone()).collect();
let l2_text: String = line2.fragments.iter().map(|f| f.text.clone()).collect();
assert_eq!(l1_text, "aaa bbb");
assert_eq!(l2_text, "ccc ddd");
let b_on_line2: Vec<_> = line2
.fragments
.iter()
.filter(|f| f.text.contains("ccc"))
.collect();
assert_eq!(b_on_line2.len(), 1);
}
#[test]
fn whitespace_collapse_during_pack() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node(" foo bar ");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet(),
);
let layout = compute_inline_layout(&dom, p, 20);
assert_eq!(layout.height(), 1);
let text: String = layout.lines[0]
.fragments
.iter()
.map(|f| f.text.clone())
.collect();
assert_eq!(text, "foo bar");
}
#[test]
fn pre_mode_treats_whole_text_as_one_word() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello world");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.white_space(WhiteSpace::Pre),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline)),
);
let layout = compute_inline_layout(&dom, p, 5);
assert_eq!(layout.height(), 1);
assert_eq!(layout.lines[0].width, 11);
}
#[test]
fn pre_wrap_mode_wraps_at_whitespace_and_preserves_spaces() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("hello world foo");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.white_space(WhiteSpace::PreWrap),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline)),
);
let layout = compute_inline_layout(&dom, p, 8);
assert!(layout.height() >= 2);
}
#[test]
fn pre_wrap_mode_preserves_explicit_newlines_as_hard_breaks() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("a\nb");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.white_space(WhiteSpace::PreWrap),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline)),
);
let layout = compute_inline_layout(&dom, p, 20);
assert_eq!(layout.height(), 2);
}
#[test]
fn nowrap_keeps_everything_on_one_line() {
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let t = dom.create_text_node("the quick brown fox");
dom.append_child(p, t).unwrap();
let span = dom.create_element("span");
dom.append_child(p, span).unwrap();
dom.append_child(root, p).unwrap();
p
},
&Stylesheet::bare()
.rule_unchecked(
"p",
TuiStyle::new()
.display(Display::Block)
.white_space(WhiteSpace::NoWrap),
)
.rule_unchecked("span", TuiStyle::new().display(Display::Inline)),
);
let layout = compute_inline_layout(&dom, p, 10);
assert_eq!(layout.height(), 1);
assert_eq!(layout.lines[0].width, 19);
}
#[test]
fn static_pseudos_pack_as_generated_fragments() {
use crate::style::Content;
let (dom, p) = prepared(
|dom| {
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();
p
},
&default_sheet()
.rule_unchecked(
"p::before",
TuiStyle::new().content(Content::Str("> ".into())),
)
.rule_unchecked(
"p::after",
TuiStyle::new().content(Content::Str(" <".into())),
),
);
let layout = compute_inline_layout(&dom, p, 10);
assert_eq!(layout.height(), 2);
let line0 = &layout.lines[0];
let gen0: Vec<_> = line0
.generated
.iter()
.map(|g| (g.slot, g.x, g.text.as_str()))
.collect();
assert_eq!(gen0, vec![(PseudoSlot::Before, 0, "> ")]);
assert_eq!(line0.fragments[0].x, 2);
assert_eq!(line0.fragments[0].text, "abcdefg");
assert_eq!(line0.width, 9);
let line1 = &layout.lines[1];
assert_eq!(line1.fragments[0].text, "hi");
let gen1: Vec<_> = line1
.generated
.iter()
.map(|g| (g.host, g.slot, g.x, g.text.as_str()))
.collect();
assert_eq!(gen1, vec![(p, PseudoSlot::After, 2, " <")]);
assert!(
layout
.lines
.iter()
.flat_map(|l| &l.fragments)
.all(|f| f.text_node != p),
"no text fragment stands for generated content"
);
}
#[test]
fn inline_element_pseudos_pack_at_its_start_and_end() {
use crate::style::Content;
let (dom, p) = prepared(
|dom| {
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();
p
},
&default_sheet()
.rule_unchecked(
"b::before",
TuiStyle::new().content(Content::Str("[".into())),
)
.rule_unchecked(
"b::after",
TuiStyle::new().content(Content::Str("]".into())),
),
);
let b = dom.node(p).child_nodes().nth(1).unwrap().id();
let layout = compute_inline_layout(&dom, p, 20);
assert_eq!(layout.height(), 1);
let line = &layout.lines[0];
let generated: Vec<_> = line
.generated
.iter()
.map(|g| (g.host, g.slot, g.x, g.text.as_str()))
.collect();
assert_eq!(
generated,
vec![
(b, PseudoSlot::Before, 2, "["),
(b, PseudoSlot::After, 7, "]")
]
);
let bold = line.fragments.iter().find(|f| f.node == b).unwrap();
assert_eq!((bold.x, bold.text.as_str()), (3, "bold"));
assert_eq!(line.width, 10);
}
#[test]
fn run_layout_packs_host_pseudos_past_whitespace_only_edges() {
use crate::style::Content;
let (dom, p) = prepared(
|dom| {
let root = dom.root();
let p = dom.create_element("p");
let lead = dom.create_text_node("\n");
let span = dom.create_element("span");
let x = dom.create_text_node("x");
let trail = dom.create_text_node("\n");
dom.append_child(span, x).unwrap();
dom.append_child(p, lead).unwrap();
dom.append_child(p, span).unwrap();
dom.append_child(p, trail).unwrap();
dom.append_child(root, p).unwrap();
p
},
&default_sheet()
.rule_unchecked(
"p::before",
TuiStyle::new().content(Content::Str("<".into())),
)
.rule_unchecked(
"p::after",
TuiStyle::new().content(Content::Str(">".into())),
),
);
let span = dom.node(p).child_nodes().nth(1).unwrap().id();
let generated = |children: &[NodeId]| -> Vec<(NodeId, PseudoSlot, String)> {
compute_inline_layout_for_run(&dom, p, children, 20)
.lines
.iter()
.flat_map(|l| &l.generated)
.map(|g| (g.host, g.slot, g.text.clone()))
.collect()
};
let children: Vec<NodeId> = dom.node(p).child_nodes().map(|c| c.id()).collect();
let both = vec![
(p, PseudoSlot::Before, "<".to_string()),
(p, PseudoSlot::After, ">".to_string()),
];
assert_eq!(generated(&children), both, "whole child list");
assert_eq!(
generated(&[span]),
both,
"the span alone is the whole content"
);
}