mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
2344 lines
78 KiB
Rust
2344 lines
78 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use moli_layout::{
|
|
DocumentLayoutServices, LayoutAnonymousReason, LayoutBoxKind, LayoutCapabilityDiagnostic,
|
|
LayoutDisplay, LayoutElementCategory, LayoutElementSemantics, LayoutError,
|
|
LayoutFormControlKind, LayoutInlineAlignment, LayoutInputControlKind, LayoutListRole,
|
|
LayoutNamespace, LayoutPosition, LayoutPseudo, LayoutReplacedKind, LayoutSource,
|
|
LayoutSourceKind, LayoutStyleResolver, LayoutTableRole, PaintColor, PaintFragment, PaintRect,
|
|
PaintViewport, ReplacedMetrics, ResolvedLayoutElementStyles, ResolvedLayoutPseudoStyle,
|
|
ResolvedLayoutStyle, ScreenshotLayoutRequest, build_layout_world, build_screenshot_snapshot,
|
|
normalize_layout_source,
|
|
};
|
|
use style::Atom;
|
|
use taffy::{Dimension, Display, FlexDirection, Rect, Size, Style, style_helpers::length};
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct TestNode {
|
|
label: &'static str,
|
|
kind: LayoutSourceKind,
|
|
text: Option<&'static str>,
|
|
children: Vec<usize>,
|
|
element_semantics: Option<LayoutElementSemantics>,
|
|
replaced_metrics: Option<ReplacedMetrics>,
|
|
}
|
|
|
|
impl TestNode {
|
|
fn element(label: &'static str, children: Vec<usize>) -> Self {
|
|
Self {
|
|
label,
|
|
kind: LayoutSourceKind::Element,
|
|
text: None,
|
|
children,
|
|
element_semantics: Some(LayoutElementSemantics::new(
|
|
LayoutNamespace::Html,
|
|
"div",
|
|
LayoutElementCategory::Generic,
|
|
None,
|
|
)),
|
|
replaced_metrics: None,
|
|
}
|
|
}
|
|
|
|
fn semantic_element(
|
|
label: &'static str,
|
|
local_name: &'static str,
|
|
category: LayoutElementCategory,
|
|
replaced: Option<LayoutReplacedKind>,
|
|
children: Vec<usize>,
|
|
) -> Self {
|
|
Self {
|
|
label,
|
|
kind: LayoutSourceKind::Element,
|
|
text: None,
|
|
children,
|
|
element_semantics: Some(LayoutElementSemantics::new(
|
|
LayoutNamespace::Html,
|
|
local_name,
|
|
category,
|
|
replaced,
|
|
)),
|
|
replaced_metrics: None,
|
|
}
|
|
}
|
|
|
|
fn text(label: &'static str, text: &'static str) -> Self {
|
|
Self {
|
|
label,
|
|
kind: LayoutSourceKind::Text,
|
|
text: Some(text),
|
|
children: Vec::new(),
|
|
element_semantics: None,
|
|
replaced_metrics: None,
|
|
}
|
|
}
|
|
|
|
fn comment(label: &'static str) -> Self {
|
|
Self {
|
|
label,
|
|
kind: LayoutSourceKind::Comment,
|
|
text: None,
|
|
children: Vec::new(),
|
|
element_semantics: None,
|
|
replaced_metrics: None,
|
|
}
|
|
}
|
|
|
|
fn replaced(label: &'static str, width: f32, height: f32) -> Self {
|
|
Self {
|
|
label,
|
|
kind: LayoutSourceKind::Element,
|
|
text: None,
|
|
children: Vec::new(),
|
|
element_semantics: Some(LayoutElementSemantics::new(
|
|
LayoutNamespace::Html,
|
|
"img",
|
|
LayoutElementCategory::Generic,
|
|
Some(LayoutReplacedKind::Image),
|
|
)),
|
|
replaced_metrics: Some(ReplacedMetrics {
|
|
intrinsic_width: Some(width),
|
|
intrinsic_height: Some(height),
|
|
attribute_width: None,
|
|
attribute_height: None,
|
|
intrinsic_ratio: (height > 0.0).then_some(width / height),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TestSource {
|
|
root: usize,
|
|
nodes: Vec<TestNode>,
|
|
}
|
|
|
|
impl LayoutSource for TestSource {
|
|
type NodeId = usize;
|
|
type ChildIter<'a> = std::iter::Copied<std::slice::Iter<'a, usize>>;
|
|
|
|
fn root(&self) -> Self::NodeId {
|
|
self.root
|
|
}
|
|
|
|
fn flat_parent(&self, node: Self::NodeId) -> Option<Self::NodeId> {
|
|
if node == self.root {
|
|
return None;
|
|
}
|
|
self.nodes
|
|
.iter()
|
|
.position(|candidate| candidate.children.contains(&node))
|
|
}
|
|
|
|
fn flat_children(&self, node: Self::NodeId) -> Self::ChildIter<'_> {
|
|
self.nodes[node].children.iter().copied()
|
|
}
|
|
|
|
fn node_kind(&self, node: Self::NodeId) -> LayoutSourceKind {
|
|
self.nodes[node].kind
|
|
}
|
|
|
|
fn element_semantics(&self, node: Self::NodeId) -> Option<LayoutElementSemantics> {
|
|
self.nodes[node].element_semantics.clone()
|
|
}
|
|
|
|
fn text(&self, node: Self::NodeId) -> Option<&str> {
|
|
self.nodes[node].text
|
|
}
|
|
|
|
fn label(&self, node: Self::NodeId) -> String {
|
|
self.nodes[node].label.to_owned()
|
|
}
|
|
|
|
fn replaced_metrics(&self, node: Self::NodeId) -> Option<ReplacedMetrics> {
|
|
self.nodes[node].replaced_metrics
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct TestStyles {
|
|
primary: HashMap<usize, ResolvedLayoutStyle>,
|
|
pseudo: HashMap<(usize, LayoutPseudo), ResolvedLayoutStyle>,
|
|
element_queries: Vec<usize>,
|
|
marker_queries: Vec<usize>,
|
|
anonymous_queries: Vec<(usize, LayoutDisplay)>,
|
|
}
|
|
|
|
impl LayoutStyleResolver<usize> for TestStyles {
|
|
fn element_styles(
|
|
&mut self,
|
|
node: usize,
|
|
) -> Result<Option<ResolvedLayoutElementStyles>, LayoutError> {
|
|
self.element_queries.push(node);
|
|
let Some(primary) = self.primary.get(&node).cloned() else {
|
|
return Ok(None);
|
|
};
|
|
Ok(Some(ResolvedLayoutElementStyles::new(
|
|
primary,
|
|
self.pseudo.get(&(node, LayoutPseudo::Before)).cloned(),
|
|
self.pseudo.get(&(node, LayoutPseudo::After)).cloned(),
|
|
)))
|
|
}
|
|
|
|
fn marker_style(
|
|
&mut self,
|
|
node: usize,
|
|
) -> Result<Option<ResolvedLayoutPseudoStyle>, LayoutError> {
|
|
self.marker_queries.push(node);
|
|
Ok(self
|
|
.pseudo
|
|
.get(&(node, LayoutPseudo::Marker))
|
|
.cloned()
|
|
.map(ResolvedLayoutPseudoStyle::new))
|
|
}
|
|
|
|
fn anonymous_style(
|
|
&mut self,
|
|
owner: usize,
|
|
parent: &ResolvedLayoutStyle,
|
|
display: LayoutDisplay,
|
|
) -> Result<ResolvedLayoutStyle, LayoutError> {
|
|
self.anonymous_queries.push((owner, display));
|
|
Ok(ResolvedLayoutStyle::anonymous_from(parent, display))
|
|
}
|
|
}
|
|
|
|
fn style(display: LayoutDisplay) -> ResolvedLayoutStyle {
|
|
ResolvedLayoutStyle::synthetic(display, Style::<Atom>::default(), PaintColor::TRANSPARENT)
|
|
}
|
|
|
|
fn colored_box(
|
|
display: LayoutDisplay,
|
|
width: f32,
|
|
height: f32,
|
|
color: PaintColor,
|
|
) -> ResolvedLayoutStyle {
|
|
let taffy = Style::<Atom> {
|
|
size: Size {
|
|
width: Dimension::length(width),
|
|
height: Dimension::length(height),
|
|
},
|
|
..Style::default()
|
|
};
|
|
ResolvedLayoutStyle::synthetic(display, taffy, color)
|
|
}
|
|
|
|
#[test]
|
|
fn simple_block_ignores_comment_and_is_stable() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2]),
|
|
TestNode::comment("comment"),
|
|
TestNode::element("child", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(2, style(LayoutDisplay::Block));
|
|
|
|
let first = build_layout_world(&source, &mut styles).unwrap();
|
|
first.validate_invariants().unwrap();
|
|
assert_eq!(first.len(), 2);
|
|
assert_eq!(first.source_box(1), None);
|
|
let first_normalized = first.normalized_tree().to_string();
|
|
|
|
let second = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(first_normalized, second.normalized_tree().to_string());
|
|
assert_eq!(styles.element_queries, vec![0, 2, 0, 2]);
|
|
}
|
|
|
|
#[test]
|
|
fn display_none_prunes_the_whole_subtree() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("hidden", vec![2]),
|
|
TestNode::element("hidden-child", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::None));
|
|
styles.primary.insert(2, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.len(), 1);
|
|
assert_eq!(world.source_box(1), None);
|
|
assert_eq!(world.source_box(2), None);
|
|
assert_eq!(styles.element_queries, vec![0, 1]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_missing_non_root_style_prunes_that_source_subtree() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("unstyled", vec![2]),
|
|
TestNode::element("unstyled-child", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(2, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.len(), 1);
|
|
assert_eq!(world.source_box(1), None);
|
|
assert_eq!(world.source_box(2), None);
|
|
assert_eq!(styles.element_queries, vec![0, 1]);
|
|
}
|
|
|
|
#[test]
|
|
fn display_contents_hoists_children_without_a_principal_box() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("contents", vec![2]),
|
|
TestNode::element("leaf", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Contents));
|
|
styles.primary.insert(2, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.source_box(1), None);
|
|
let leaf = world.source_box(2).unwrap();
|
|
assert_eq!(world.box_by_id(world.root()).unwrap().children(), &[leaf]);
|
|
}
|
|
|
|
#[test]
|
|
fn display_contents_on_unusual_html_elements_suppresses_the_element_and_subtree() {
|
|
let unusual_elements = [
|
|
"br", "wbr", "meter", "progress", "canvas", "embed", "object", "audio", "iframe", "img",
|
|
"video", "frame", "frameset", "input", "textarea", "select",
|
|
];
|
|
|
|
for local_name in unusual_elements {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::semantic_element(
|
|
"unusual",
|
|
local_name,
|
|
LayoutElementCategory::Generic,
|
|
None,
|
|
vec![2],
|
|
),
|
|
TestNode::text("suppressed-child", "must-not-layout"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Contents));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.len(), 1, "element={local_name}");
|
|
assert_eq!(world.source_box(1), None, "element={local_name}");
|
|
assert_eq!(world.source_box(2), None, "element={local_name}");
|
|
assert_eq!(styles.element_queries, vec![0, 1]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn display_contents_remains_transparent_for_button_fallback_content() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::semantic_element(
|
|
"button",
|
|
"button",
|
|
LayoutElementCategory::FormControl(LayoutFormControlKind::Button),
|
|
None,
|
|
vec![2],
|
|
),
|
|
TestNode::text("button-text", "button"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Contents));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.source_box(1), None);
|
|
assert_eq!(
|
|
world.box_by_id(world.root()).unwrap().children(),
|
|
&[world.source_box(2).unwrap()]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hidden_input_never_constructs_a_box_even_when_css_requests_block() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::semantic_element(
|
|
"hidden-input",
|
|
"input",
|
|
LayoutElementCategory::FormControl(LayoutFormControlKind::Input(
|
|
LayoutInputControlKind::Hidden,
|
|
)),
|
|
Some(LayoutReplacedKind::FormControl),
|
|
vec![2],
|
|
),
|
|
TestNode::text("hidden-child", "must-not-layout"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.len(), 1);
|
|
assert_eq!(world.source_box(1), None);
|
|
assert_eq!(world.source_box(2), None);
|
|
assert_eq!(styles.element_queries, vec![0, 1]);
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_flow_wraps_each_inline_run_in_one_anonymous_block() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::text("text-before", "before"),
|
|
TestNode::element("block", Vec::new()),
|
|
TestNode::text("text-after", "after"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(2, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(
|
|
world.normalized_tree().to_string(),
|
|
concat!(
|
|
"box-tree-schema=3\n",
|
|
"principal-block path=0 display=block source=root element=html:div category=generic fc=block\n",
|
|
" anonymous-block path=0/0 display=block owner=root anonymous=mixed-flow-inline-run fc=inline\n",
|
|
" text path=0/0/0 display=inline source=text-before text=\"before\"\n",
|
|
" principal-block path=0/1 display=block source=block element=html:div category=generic fc=block\n",
|
|
" anonymous-block path=0/2 display=block owner=root anonymous=mixed-flow-inline-run fc=inline\n",
|
|
" text path=0/2/0 display=inline source=text-after text=\"after\"\n",
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn all_inline_children_make_the_principal_box_an_inline_context() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2]),
|
|
TestNode::text("text", "hello"),
|
|
TestNode::element("span", vec![3]),
|
|
TestNode::text("span-text", "world"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(2, style(LayoutDisplay::Inline));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert!(root.establishes_inline_formatting_context());
|
|
let span = world.box_by_id(world.source_box(2).unwrap()).unwrap();
|
|
assert!(span.establishes_inline_formatting_context());
|
|
assert_eq!(root.children().len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn computed_display_matrix_keeps_every_phase_one_box_role() {
|
|
let cases = [
|
|
(LayoutDisplay::Block, LayoutBoxKind::PrincipalBlock),
|
|
(LayoutDisplay::FlowRoot, LayoutBoxKind::PrincipalFlowRoot),
|
|
(LayoutDisplay::Inline, LayoutBoxKind::PrincipalInline),
|
|
(
|
|
LayoutDisplay::InlineBlock,
|
|
LayoutBoxKind::PrincipalInlineBlock,
|
|
),
|
|
(LayoutDisplay::Flex, LayoutBoxKind::PrincipalFlex),
|
|
(
|
|
LayoutDisplay::InlineFlex,
|
|
LayoutBoxKind::PrincipalInlineFlex,
|
|
),
|
|
(LayoutDisplay::Grid, LayoutBoxKind::PrincipalGrid),
|
|
(
|
|
LayoutDisplay::InlineGrid,
|
|
LayoutBoxKind::PrincipalInlineGrid,
|
|
),
|
|
(LayoutDisplay::BlockListItem, LayoutBoxKind::ListItem),
|
|
(LayoutDisplay::InlineListItem, LayoutBoxKind::InlineListItem),
|
|
(LayoutDisplay::Table, LayoutBoxKind::TableWrapper),
|
|
(
|
|
LayoutDisplay::InlineTable,
|
|
LayoutBoxKind::InlineTableWrapper,
|
|
),
|
|
(LayoutDisplay::TableCaption, LayoutBoxKind::TableCaption),
|
|
(LayoutDisplay::TableRowGroup, LayoutBoxKind::TableRowGroup),
|
|
(
|
|
LayoutDisplay::TableHeaderGroup,
|
|
LayoutBoxKind::TableHeaderGroup,
|
|
),
|
|
(
|
|
LayoutDisplay::TableFooterGroup,
|
|
LayoutBoxKind::TableFooterGroup,
|
|
),
|
|
(
|
|
LayoutDisplay::TableColumnGroup,
|
|
LayoutBoxKind::TableColumnGroup,
|
|
),
|
|
(LayoutDisplay::TableColumn, LayoutBoxKind::TableColumn),
|
|
(LayoutDisplay::TableRow, LayoutBoxKind::TableRow),
|
|
(LayoutDisplay::TableCell, LayoutBoxKind::TableCell),
|
|
];
|
|
|
|
for (display, expected_kind) in cases {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(display));
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert_eq!(root.kind(), expected_kind, "display={display:?}");
|
|
assert_eq!(root.style().display(), display);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn flex_direct_text_uses_an_anonymous_flex_item_and_preserves_run_whitespace() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::text("text", "card"),
|
|
TestNode::text("whitespace", " \n "),
|
|
TestNode::element("span", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Flex));
|
|
styles.primary.insert(3, style(LayoutDisplay::Inline));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert_eq!(root.children().len(), 2);
|
|
assert_eq!(
|
|
world.box_by_id(root.children()[0]).unwrap().kind(),
|
|
LayoutBoxKind::AnonymousFlexItem
|
|
);
|
|
assert_eq!(
|
|
world
|
|
.box_by_id(root.children()[0])
|
|
.unwrap()
|
|
.anonymous_reason(),
|
|
Some(LayoutAnonymousReason::FlexTextRun)
|
|
);
|
|
let flex_text_item = world.box_by_id(root.children()[0]).unwrap();
|
|
assert_eq!(flex_text_item.children().len(), 2);
|
|
assert_eq!(world.source_box(2), Some(flex_text_item.children()[1]));
|
|
assert_eq!(
|
|
world.box_by_id(root.children()[1]).unwrap().kind(),
|
|
LayoutBoxKind::PrincipalInline
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn grid_direct_text_uses_one_anonymous_grid_item_and_keeps_text_boundaries() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3, 4]),
|
|
TestNode::text("left", "left"),
|
|
TestNode::text("space", " "),
|
|
TestNode::text("right", "right"),
|
|
TestNode::element("item", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Grid));
|
|
styles.primary.insert(4, style(LayoutDisplay::Inline));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert_eq!(root.kind(), LayoutBoxKind::PrincipalGrid);
|
|
assert_eq!(root.children().len(), 2);
|
|
let text_item = world.box_by_id(root.children()[0]).unwrap();
|
|
assert_eq!(text_item.kind(), LayoutBoxKind::AnonymousGridItem);
|
|
assert_eq!(
|
|
text_item.anonymous_reason(),
|
|
Some(LayoutAnonymousReason::GridTextRun)
|
|
);
|
|
assert_eq!(text_item.children().len(), 3);
|
|
assert_eq!(world.source_box(2), Some(text_item.children()[1]));
|
|
assert!(root.capability_diagnostics().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn flex_and_grid_distinguish_css_whitespace_from_non_breaking_space_items() {
|
|
let cases = [
|
|
(LayoutDisplay::Flex, LayoutBoxKind::AnonymousFlexItem),
|
|
(LayoutDisplay::Grid, LayoutBoxKind::AnonymousGridItem),
|
|
];
|
|
|
|
for (container_display, anonymous_kind) in cases {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::text("css-whitespace", " \t\n"),
|
|
TestNode::element("item", Vec::new()),
|
|
TestNode::text("non-breaking-space", "\u{00a0}"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(container_display));
|
|
styles.primary.insert(2, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert_eq!(root.children().len(), 2, "display={container_display:?}");
|
|
assert_eq!(world.source_box(1), None, "display={container_display:?}");
|
|
assert_eq!(
|
|
world.box_by_id(root.children()[1]).unwrap().kind(),
|
|
anonymous_kind,
|
|
"display={container_display:?}",
|
|
);
|
|
assert!(
|
|
world.source_box(3).is_some(),
|
|
"display={container_display:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contents_text_is_an_item_of_the_flattened_flex_or_grid_container() {
|
|
let cases = [
|
|
(
|
|
LayoutDisplay::Flex,
|
|
LayoutBoxKind::AnonymousFlexItem,
|
|
LayoutAnonymousReason::FlexTextRun,
|
|
),
|
|
(
|
|
LayoutDisplay::Grid,
|
|
LayoutBoxKind::AnonymousGridItem,
|
|
LayoutAnonymousReason::GridTextRun,
|
|
),
|
|
];
|
|
|
|
for (container_display, expected_kind, expected_reason) in cases {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("contents", vec![2, 3]),
|
|
TestNode::text("left", "left"),
|
|
TestNode::text("right", "right"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(container_display));
|
|
styles.primary.insert(1, style(LayoutDisplay::Contents));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.source_box(1), None);
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert_eq!(root.children().len(), 1, "display={container_display:?}");
|
|
let item = world.box_by_id(root.children()[0]).unwrap();
|
|
assert_eq!(item.kind(), expected_kind);
|
|
assert_eq!(item.anonymous_reason(), Some(expected_reason));
|
|
assert_eq!(item.owner(), Some(0));
|
|
assert_eq!(item.children().len(), 2);
|
|
assert_eq!(styles.anonymous_queries, vec![(0, LayoutDisplay::Block)]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn whitespace_does_not_create_an_ifc_around_only_out_of_flow_content() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::text("leading-space", " \n "),
|
|
TestNode::element("absolute", Vec::new()),
|
|
TestNode::text("trailing-space", "\t"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles
|
|
.primary
|
|
.insert(2, style(LayoutDisplay::Block).with_out_of_flow());
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root = world.box_by_id(world.root()).unwrap();
|
|
assert!(!root.establishes_inline_formatting_context());
|
|
assert_eq!(root.children().len(), 1);
|
|
assert_eq!(root.children()[0], world.source_box(2).unwrap());
|
|
assert_eq!(world.source_box(1), None);
|
|
assert_eq!(world.source_box(3), None);
|
|
}
|
|
|
|
#[test]
|
|
fn out_of_flow_box_terminates_each_mixed_flow_anonymous_run() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3, 4]),
|
|
TestNode::text("before", "before"),
|
|
TestNode::element("absolute", Vec::new()),
|
|
TestNode::text("after", "after"),
|
|
TestNode::element("block", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles
|
|
.primary
|
|
.insert(2, style(LayoutDisplay::Block).with_out_of_flow());
|
|
styles.primary.insert(4, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let children = world.box_by_id(world.root()).unwrap().children();
|
|
assert_eq!(children.len(), 4);
|
|
assert_eq!(
|
|
world.box_by_id(children[0]).unwrap().kind(),
|
|
LayoutBoxKind::AnonymousBlock
|
|
);
|
|
assert_eq!(children[1], world.source_box(2).unwrap());
|
|
assert_eq!(
|
|
world.box_by_id(children[2]).unwrap().kind(),
|
|
LayoutBoxKind::AnonymousBlock
|
|
);
|
|
assert_eq!(children[3], world.source_box(4).unwrap());
|
|
assert_eq!(styles.anonymous_queries.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn list_marker_precedes_before_content_and_keeps_normal_marker_state() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::semantic_element(
|
|
"item",
|
|
"li",
|
|
LayoutElementCategory::List(LayoutListRole::Item),
|
|
None,
|
|
Vec::new(),
|
|
)],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles
|
|
.primary
|
|
.insert(0, style(LayoutDisplay::BlockListItem));
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::Marker),
|
|
style(LayoutDisplay::Inline).with_normal_generated_content(),
|
|
);
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::Before),
|
|
style(LayoutDisplay::Inline).with_generated_text("before"),
|
|
);
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let children = world.box_by_id(world.root()).unwrap().children();
|
|
assert_eq!(children.len(), 2);
|
|
let marker = world.box_by_id(children[0]).unwrap();
|
|
assert_eq!(marker.kind(), LayoutBoxKind::PseudoMarker);
|
|
assert_eq!(marker.pseudo(), Some(LayoutPseudo::Marker));
|
|
assert_eq!(marker.text(), None);
|
|
assert!(marker.capability_diagnostics().is_empty());
|
|
assert_eq!(
|
|
world.box_by_id(children[1]).unwrap().pseudo(),
|
|
Some(LayoutPseudo::Before)
|
|
);
|
|
assert_eq!(styles.marker_queries, vec![0],);
|
|
}
|
|
|
|
#[test]
|
|
fn list_item_and_marker_construction_follow_computed_display_not_the_html_tag() {
|
|
let generic_source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("generic", Vec::new())],
|
|
};
|
|
let mut generic_styles = TestStyles::default();
|
|
generic_styles
|
|
.primary
|
|
.insert(0, style(LayoutDisplay::BlockListItem));
|
|
generic_styles.pseudo.insert(
|
|
(0, LayoutPseudo::Marker),
|
|
style(LayoutDisplay::Inline).with_normal_generated_content(),
|
|
);
|
|
let generic_world = build_layout_world(&generic_source, &mut generic_styles).unwrap();
|
|
let generic = generic_world.box_by_id(generic_world.root()).unwrap();
|
|
assert_eq!(generic.kind(), LayoutBoxKind::ListItem);
|
|
assert_eq!(generic.children().len(), 1);
|
|
assert_eq!(
|
|
generic_world
|
|
.box_by_id(generic.children()[0])
|
|
.unwrap()
|
|
.kind(),
|
|
LayoutBoxKind::PseudoMarker
|
|
);
|
|
|
|
let li_source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::semantic_element(
|
|
"li",
|
|
"li",
|
|
LayoutElementCategory::List(LayoutListRole::Item),
|
|
None,
|
|
Vec::new(),
|
|
)],
|
|
};
|
|
let mut li_styles = TestStyles::default();
|
|
li_styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
li_styles.pseudo.insert(
|
|
(0, LayoutPseudo::Marker),
|
|
style(LayoutDisplay::Inline).with_normal_generated_content(),
|
|
);
|
|
let li_world = build_layout_world(&li_source, &mut li_styles).unwrap();
|
|
let li = li_world.box_by_id(li_world.root()).unwrap();
|
|
assert_eq!(li.kind(), LayoutBoxKind::PrincipalBlock);
|
|
assert!(li.children().is_empty());
|
|
assert_eq!(li_styles.marker_queries, Vec::<usize>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_generated_item_keeps_the_pseudo_and_reports_capability() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::Before),
|
|
style(LayoutDisplay::Inline).with_unsupported_generated_content(),
|
|
);
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let pseudo = world
|
|
.box_by_id(world.box_by_id(world.root()).unwrap().children()[0])
|
|
.unwrap();
|
|
assert_eq!(pseudo.kind(), LayoutBoxKind::PseudoBefore);
|
|
assert_eq!(pseudo.text(), None);
|
|
assert!(pseudo.children().is_empty());
|
|
assert_eq!(
|
|
pseudo.capability_diagnostics(),
|
|
&[LayoutCapabilityDiagnostic::GeneratedContentUnsupported]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pseudo_generated_content_uses_the_owning_formatting_context_rules() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Flex));
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::Before),
|
|
style(LayoutDisplay::InlineFlex).with_generated_text("generated"),
|
|
);
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let pseudo = world
|
|
.box_by_id(world.box_by_id(world.root()).unwrap().children()[0])
|
|
.unwrap();
|
|
assert_eq!(pseudo.kind(), LayoutBoxKind::PseudoBefore);
|
|
assert_eq!(pseudo.style().display(), LayoutDisplay::InlineFlex);
|
|
assert_eq!(pseudo.children().len(), 1);
|
|
let item = world.box_by_id(pseudo.children()[0]).unwrap();
|
|
assert_eq!(item.kind(), LayoutBoxKind::AnonymousFlexItem);
|
|
assert_eq!(item.children().len(), 1);
|
|
assert_eq!(
|
|
world.box_by_id(item.children()[0]).unwrap().text(),
|
|
Some("generated")
|
|
);
|
|
assert_eq!(styles.anonymous_queries, vec![(0, LayoutDisplay::Block)]);
|
|
}
|
|
|
|
#[test]
|
|
fn table_display_on_a_pseudo_participates_in_missing_parent_fixup() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::After),
|
|
style(LayoutDisplay::TableCell).with_generated_text("cell"),
|
|
);
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let table = world
|
|
.box_by_id(world.box_by_id(world.root()).unwrap().children()[0])
|
|
.unwrap();
|
|
let row_group = world.box_by_id(table.children()[0]).unwrap();
|
|
let row = world.box_by_id(row_group.children()[0]).unwrap();
|
|
let pseudo = world.box_by_id(row.children()[0]).unwrap();
|
|
assert_eq!(table.kind(), LayoutBoxKind::AnonymousTableWrapper);
|
|
assert_eq!(row_group.kind(), LayoutBoxKind::AnonymousTableRowGroup);
|
|
assert_eq!(row.kind(), LayoutBoxKind::AnonymousTableRow);
|
|
assert_eq!(pseudo.kind(), LayoutBoxKind::PseudoAfter);
|
|
assert_eq!(pseudo.style().display(), LayoutDisplay::TableCell);
|
|
assert_eq!(pseudo.pseudo(), Some(LayoutPseudo::After));
|
|
assert_eq!(pseudo.children().len(), 1);
|
|
assert_eq!(
|
|
world.box_by_id(pseudo.children()[0]).unwrap().text(),
|
|
Some("cell")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn display_contents_pseudo_hoists_supported_text_and_retains_unsupported_diagnostics() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::Before),
|
|
style(LayoutDisplay::Contents).with_generated_text("before"),
|
|
);
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::After),
|
|
style(LayoutDisplay::Contents).with_unsupported_generated_content(),
|
|
);
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let children = world.box_by_id(world.root()).unwrap().children();
|
|
assert_eq!(children.len(), 2);
|
|
let before = world.box_by_id(children[0]).unwrap();
|
|
let after = world.box_by_id(children[1]).unwrap();
|
|
assert_eq!(before.kind(), LayoutBoxKind::Text);
|
|
assert_eq!(before.owner(), Some(0));
|
|
assert_eq!(before.pseudo(), Some(LayoutPseudo::Before));
|
|
assert_eq!(before.text(), Some("before"));
|
|
assert_eq!(after.kind(), LayoutBoxKind::Text);
|
|
assert_eq!(after.pseudo(), Some(LayoutPseudo::After));
|
|
assert_eq!(
|
|
after.capability_diagnostics(),
|
|
&[LayoutCapabilityDiagnostic::GeneratedContentUnsupported]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn inline_with_block_descendant_is_split_into_owned_continuations() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("span", vec![2, 3, 4]),
|
|
TestNode::text("before", "before"),
|
|
TestNode::element("block", Vec::new()),
|
|
TestNode::text("after", "after"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Inline));
|
|
styles.primary.insert(3, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let root_children = world.box_by_id(world.root()).unwrap().children();
|
|
assert_eq!(root_children.len(), 3);
|
|
let before_wrapper = world.box_by_id(root_children[0]).unwrap();
|
|
let block = world.box_by_id(root_children[1]).unwrap();
|
|
let after_wrapper = world.box_by_id(root_children[2]).unwrap();
|
|
assert_eq!(before_wrapper.kind(), LayoutBoxKind::AnonymousBlock);
|
|
assert_eq!(block.kind(), LayoutBoxKind::PrincipalBlock);
|
|
assert_eq!(after_wrapper.kind(), LayoutBoxKind::AnonymousBlock);
|
|
let first_fragment_id = before_wrapper.children()[0];
|
|
let first_fragment = world.box_by_id(first_fragment_id).unwrap();
|
|
let continuation = world.box_by_id(after_wrapper.children()[0]).unwrap();
|
|
assert_eq!(first_fragment.kind(), LayoutBoxKind::PrincipalInline);
|
|
assert_eq!(first_fragment.source(), Some(1));
|
|
assert_eq!(continuation.kind(), LayoutBoxKind::InlineContinuation);
|
|
assert_eq!(continuation.source(), None);
|
|
assert_eq!(continuation.owner(), Some(1));
|
|
assert_eq!(
|
|
continuation.anonymous_reason(),
|
|
Some(LayoutAnonymousReason::InlineSplitContinuation)
|
|
);
|
|
assert_eq!(
|
|
block.structural_parent(),
|
|
Some(first_fragment_id),
|
|
"formatting promotion must not erase the block's LayoutObject parent"
|
|
);
|
|
assert_eq!(continuation.structural_parent(), Some(world.root()));
|
|
assert_eq!(world.source_box(1), Some(before_wrapper.children()[0]));
|
|
}
|
|
|
|
#[test]
|
|
fn missing_table_parents_form_a_well_typed_anonymous_chain() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("cell", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::TableCell));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let table = world
|
|
.box_by_id(world.box_by_id(world.root()).unwrap().children()[0])
|
|
.unwrap();
|
|
let row_group = world.box_by_id(table.children()[0]).unwrap();
|
|
let row = world.box_by_id(row_group.children()[0]).unwrap();
|
|
let cell = world.box_by_id(row.children()[0]).unwrap();
|
|
assert_eq!(table.kind(), LayoutBoxKind::AnonymousTableWrapper);
|
|
assert_eq!(row_group.kind(), LayoutBoxKind::AnonymousTableRowGroup);
|
|
assert_eq!(row.kind(), LayoutBoxKind::AnonymousTableRow);
|
|
assert_eq!(cell.kind(), LayoutBoxKind::TableCell);
|
|
assert_eq!(cell.source(), Some(1));
|
|
assert_eq!(table.owner(), Some(0));
|
|
assert_eq!(
|
|
styles.anonymous_queries,
|
|
vec![
|
|
(0, LayoutDisplay::Table),
|
|
(0, LayoutDisplay::TableRowGroup),
|
|
(0, LayoutDisplay::TableRow),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_internal_table_role_gets_the_minimal_missing_parent_chain() {
|
|
let cases: &[(LayoutDisplay, &[LayoutDisplay])] = &[
|
|
(
|
|
LayoutDisplay::TableCaption,
|
|
&[LayoutDisplay::Table, LayoutDisplay::TableCaption],
|
|
),
|
|
(
|
|
LayoutDisplay::TableRowGroup,
|
|
&[LayoutDisplay::Table, LayoutDisplay::TableRowGroup],
|
|
),
|
|
(
|
|
LayoutDisplay::TableColumnGroup,
|
|
&[LayoutDisplay::Table, LayoutDisplay::TableColumnGroup],
|
|
),
|
|
(
|
|
LayoutDisplay::TableColumn,
|
|
&[LayoutDisplay::Table, LayoutDisplay::TableColumn],
|
|
),
|
|
(
|
|
LayoutDisplay::TableRow,
|
|
&[
|
|
LayoutDisplay::Table,
|
|
LayoutDisplay::TableRowGroup,
|
|
LayoutDisplay::TableRow,
|
|
],
|
|
),
|
|
(
|
|
LayoutDisplay::TableCell,
|
|
&[
|
|
LayoutDisplay::Table,
|
|
LayoutDisplay::TableRowGroup,
|
|
LayoutDisplay::TableRow,
|
|
LayoutDisplay::TableCell,
|
|
],
|
|
),
|
|
];
|
|
|
|
for (source_display, expected_chain) in cases {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("table-part", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(*source_display));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let mut actual = Vec::new();
|
|
let mut current = Some(world.box_by_id(world.root()).unwrap().children()[0]);
|
|
while let Some(id) = current {
|
|
let layout_box = world.box_by_id(id).unwrap();
|
|
actual.push(layout_box.style().display());
|
|
current = layout_box.children().first().copied();
|
|
}
|
|
assert_eq!(&actual, expected_chain, "source display={source_display:?}");
|
|
assert_eq!(
|
|
world
|
|
.box_by_id(world.source_box(1).unwrap())
|
|
.unwrap()
|
|
.style()
|
|
.display(),
|
|
*source_display
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn collapsible_whitespace_reuses_one_anonymous_table_but_text_breaks_it() {
|
|
let whitespace_source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::element("left-cell", Vec::new()),
|
|
TestNode::text("space", " \n "),
|
|
TestNode::element("right-cell", Vec::new()),
|
|
],
|
|
};
|
|
let mut whitespace_styles = TestStyles::default();
|
|
whitespace_styles
|
|
.primary
|
|
.insert(0, style(LayoutDisplay::Block));
|
|
whitespace_styles
|
|
.primary
|
|
.insert(1, style(LayoutDisplay::TableCell));
|
|
whitespace_styles
|
|
.primary
|
|
.insert(3, style(LayoutDisplay::TableCell));
|
|
let whitespace_world = build_layout_world(&whitespace_source, &mut whitespace_styles).unwrap();
|
|
let root = whitespace_world.box_by_id(whitespace_world.root()).unwrap();
|
|
assert_eq!(root.children().len(), 1);
|
|
let table = whitespace_world.box_by_id(root.children()[0]).unwrap();
|
|
let row_group = whitespace_world.box_by_id(table.children()[0]).unwrap();
|
|
let row = whitespace_world.box_by_id(row_group.children()[0]).unwrap();
|
|
assert_eq!(row.children().len(), 2);
|
|
assert_eq!(whitespace_world.source_box(2), None);
|
|
|
|
let text_source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::element("left-cell", Vec::new()),
|
|
TestNode::text("text", "X"),
|
|
TestNode::element("right-cell", Vec::new()),
|
|
],
|
|
};
|
|
let mut text_styles = TestStyles::default();
|
|
text_styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
text_styles
|
|
.primary
|
|
.insert(1, style(LayoutDisplay::TableCell));
|
|
text_styles
|
|
.primary
|
|
.insert(3, style(LayoutDisplay::TableCell));
|
|
let text_world = build_layout_world(&text_source, &mut text_styles).unwrap();
|
|
let children = text_world.box_by_id(text_world.root()).unwrap().children();
|
|
assert_eq!(children.len(), 3);
|
|
assert_eq!(
|
|
text_world.box_by_id(children[0]).unwrap().kind(),
|
|
LayoutBoxKind::AnonymousTableWrapper
|
|
);
|
|
assert_eq!(
|
|
text_world.box_by_id(children[1]).unwrap().kind(),
|
|
LayoutBoxKind::AnonymousBlock
|
|
);
|
|
assert_eq!(
|
|
text_world.box_by_id(children[2]).unwrap().kind(),
|
|
LayoutBoxKind::AnonymousTableWrapper
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn anonymous_table_is_inline_only_for_a_true_inline_flow_parent() {
|
|
for (parent_display, expected_table_display) in [
|
|
(LayoutDisplay::Inline, LayoutDisplay::InlineTable),
|
|
(LayoutDisplay::InlineBlock, LayoutDisplay::Table),
|
|
(LayoutDisplay::InlineFlex, LayoutDisplay::Table),
|
|
(LayoutDisplay::InlineGrid, LayoutDisplay::Table),
|
|
] {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("parent", vec![2]),
|
|
TestNode::element("cell", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(parent_display));
|
|
styles.primary.insert(2, style(LayoutDisplay::TableCell));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let parent = world.box_by_id(world.source_box(1).unwrap()).unwrap();
|
|
let table = world.box_by_id(parent.children()[0]).unwrap();
|
|
assert_eq!(table.kind(), LayoutBoxKind::AnonymousTableWrapper);
|
|
assert_eq!(table.style().display(), expected_table_display);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn column_group_keeps_only_columns_and_columns_are_leaf_boxes() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("table", vec![1]),
|
|
TestNode::element("column-group", vec![2, 3]),
|
|
TestNode::element("column", vec![4]),
|
|
TestNode::element("invalid-child", Vec::new()),
|
|
TestNode::text("column-text", "must-not-layout"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Table));
|
|
styles
|
|
.primary
|
|
.insert(1, style(LayoutDisplay::TableColumnGroup));
|
|
styles.primary.insert(2, style(LayoutDisplay::TableColumn));
|
|
styles.primary.insert(3, style(LayoutDisplay::Block));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let table = world.box_by_id(world.root()).unwrap();
|
|
let column_group = world.box_by_id(table.children()[0]).unwrap();
|
|
assert_eq!(column_group.children().len(), 1);
|
|
let column = world.box_by_id(column_group.children()[0]).unwrap();
|
|
assert_eq!(column.kind(), LayoutBoxKind::TableColumn);
|
|
assert!(column.children().is_empty());
|
|
assert_eq!(world.source_box(3), None);
|
|
assert_eq!(world.source_box(4), None);
|
|
}
|
|
|
|
#[test]
|
|
fn display_contents_is_transparent_to_structural_table_fixup() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("table", vec![1]),
|
|
TestNode::element("contents", vec![2]),
|
|
TestNode::element("cell", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Table));
|
|
styles.primary.insert(1, style(LayoutDisplay::Contents));
|
|
styles.primary.insert(2, style(LayoutDisplay::TableCell));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(world.source_box(1), None);
|
|
let table = world.box_by_id(world.root()).unwrap();
|
|
let row_group = world.box_by_id(table.children()[0]).unwrap();
|
|
let row = world.box_by_id(row_group.children()[0]).unwrap();
|
|
assert_eq!(row.children(), &[world.source_box(2).unwrap()]);
|
|
}
|
|
|
|
#[test]
|
|
fn table_non_structural_content_is_wrapped_through_cell_and_keeps_order() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("table", vec![1, 2]),
|
|
TestNode::text("left", "left"),
|
|
TestNode::text("right", "right"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Table));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let table = world.box_by_id(world.root()).unwrap();
|
|
let row_group = world.box_by_id(table.children()[0]).unwrap();
|
|
let row = world.box_by_id(row_group.children()[0]).unwrap();
|
|
let cell = world.box_by_id(row.children()[0]).unwrap();
|
|
assert_eq!(cell.kind(), LayoutBoxKind::AnonymousTableCell);
|
|
assert!(cell.establishes_inline_formatting_context());
|
|
assert_eq!(cell.children().len(), 2);
|
|
assert_eq!(
|
|
world.box_by_id(cell.children()[0]).unwrap().text(),
|
|
Some("left")
|
|
);
|
|
assert_eq!(
|
|
world.box_by_id(cell.children()[1]).unwrap().text(),
|
|
Some("right")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn computed_display_overrides_html_table_semantics() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::semantic_element(
|
|
"table",
|
|
"table",
|
|
LayoutElementCategory::Table(LayoutTableRole::Table),
|
|
None,
|
|
Vec::new(),
|
|
)],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
assert_eq!(
|
|
world.box_by_id(world.root()).unwrap().kind(),
|
|
LayoutBoxKind::PrincipalBlock
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn form_control_kind_and_replaced_state_are_both_retained() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::semantic_element(
|
|
"checkbox",
|
|
"input",
|
|
LayoutElementCategory::FormControl(LayoutFormControlKind::Input(
|
|
LayoutInputControlKind::Checkbox,
|
|
)),
|
|
Some(LayoutReplacedKind::FormControl),
|
|
Vec::new(),
|
|
)],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::InlineBlock));
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let control = world.box_by_id(world.root()).unwrap();
|
|
assert_eq!(control.kind(), LayoutBoxKind::FormControl);
|
|
assert!(control.capability_diagnostics().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn replaced_and_line_break_boxes_are_leaves_but_object_fallback_content_is_not() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 3, 5]),
|
|
TestNode::semantic_element(
|
|
"image",
|
|
"img",
|
|
LayoutElementCategory::Generic,
|
|
Some(LayoutReplacedKind::Image),
|
|
vec![2],
|
|
),
|
|
TestNode::text("image-fallback", "must-not-layout"),
|
|
TestNode::semantic_element(
|
|
"break",
|
|
"br",
|
|
LayoutElementCategory::LineBreak,
|
|
None,
|
|
vec![4],
|
|
),
|
|
TestNode::text("break-child", "must-not-layout"),
|
|
TestNode::semantic_element(
|
|
"object",
|
|
"object",
|
|
LayoutElementCategory::Generic,
|
|
None,
|
|
vec![6],
|
|
),
|
|
TestNode::text("object-fallback", "fallback"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Inline));
|
|
styles.primary.insert(3, style(LayoutDisplay::Inline));
|
|
styles.primary.insert(5, style(LayoutDisplay::InlineBlock));
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let image = world.box_by_id(world.source_box(1).unwrap()).unwrap();
|
|
let line_break = world.box_by_id(world.source_box(3).unwrap()).unwrap();
|
|
let object = world.box_by_id(world.source_box(5).unwrap()).unwrap();
|
|
assert_eq!(image.kind(), LayoutBoxKind::Replaced);
|
|
assert!(image.children().is_empty());
|
|
assert_eq!(line_break.kind(), LayoutBoxKind::LineBreak);
|
|
assert!(line_break.children().is_empty());
|
|
assert_eq!(object.kind(), LayoutBoxKind::PrincipalInlineBlock);
|
|
assert_eq!(object.children(), &[world.source_box(6).unwrap()]);
|
|
assert_eq!(world.source_box(2), None);
|
|
assert_eq!(world.source_box(4), None);
|
|
assert_eq!(styles.element_queries, vec![0, 1, 3, 5]);
|
|
assert!(line_break.capability_diagnostics().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn pseudo_boxes_have_an_owner_not_a_fake_source_and_keep_empty_content() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::Before),
|
|
style(LayoutDisplay::Inline).with_generated_text(""),
|
|
);
|
|
styles.pseudo.insert(
|
|
(0, LayoutPseudo::After),
|
|
style(LayoutDisplay::Inline).with_generated_text("tail"),
|
|
);
|
|
|
|
let world = build_layout_world(&source, &mut styles).unwrap();
|
|
let children = world.box_by_id(world.root()).unwrap().children();
|
|
assert_eq!(children.len(), 2);
|
|
let before = world.box_by_id(children[0]).unwrap();
|
|
let after = world.box_by_id(children[1]).unwrap();
|
|
assert_eq!(before.source(), None);
|
|
assert_eq!(before.owner(), Some(0));
|
|
assert_eq!(before.pseudo(), Some(LayoutPseudo::Before));
|
|
assert_eq!(before.text(), None);
|
|
assert!(before.children().is_empty());
|
|
assert_eq!(after.pseudo(), Some(LayoutPseudo::After));
|
|
assert_eq!(after.text(), None);
|
|
assert_eq!(after.children().len(), 1);
|
|
let after_text = world.box_by_id(after.children()[0]).unwrap();
|
|
assert_eq!(after_text.kind(), LayoutBoxKind::Text);
|
|
assert_eq!(after_text.source(), None);
|
|
assert_eq!(after_text.owner(), Some(0));
|
|
assert_eq!(after_text.pseudo(), Some(LayoutPseudo::After));
|
|
assert_eq!(after_text.text(), Some("tail"));
|
|
}
|
|
|
|
#[test]
|
|
fn source_cycle_is_a_structured_error() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("child", vec![0]),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(1, style(LayoutDisplay::Block));
|
|
|
|
let error = build_layout_world(&source, &mut styles).unwrap_err();
|
|
assert_eq!(
|
|
error,
|
|
LayoutError::SourceCycle {
|
|
source_label: "root".to_owned()
|
|
}
|
|
);
|
|
assert_eq!(
|
|
error.to_string(),
|
|
"layout source flat tree contains a cycle at root"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn normalized_source_dump_locks_element_semantics_before_box_construction() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3, 4, 5]),
|
|
TestNode::semantic_element(
|
|
"break",
|
|
"br",
|
|
LayoutElementCategory::LineBreak,
|
|
None,
|
|
Vec::new(),
|
|
),
|
|
TestNode::semantic_element(
|
|
"body-group",
|
|
"tbody",
|
|
LayoutElementCategory::Table(LayoutTableRole::BodyGroup),
|
|
None,
|
|
Vec::new(),
|
|
),
|
|
TestNode::semantic_element(
|
|
"item",
|
|
"li",
|
|
LayoutElementCategory::List(LayoutListRole::Item),
|
|
None,
|
|
Vec::new(),
|
|
),
|
|
TestNode::semantic_element(
|
|
"control",
|
|
"input",
|
|
LayoutElementCategory::FormControl(LayoutFormControlKind::Input(
|
|
LayoutInputControlKind::Text,
|
|
)),
|
|
Some(LayoutReplacedKind::FormControl),
|
|
Vec::new(),
|
|
),
|
|
TestNode::text("text", "hello"),
|
|
],
|
|
};
|
|
|
|
assert_eq!(
|
|
normalize_layout_source(&source).unwrap().to_string(),
|
|
concat!(
|
|
"source-tree-schema=1\n",
|
|
"Element path=0 source=root element=html:div category=generic\n",
|
|
" Element path=0/0 source=break element=html:br category=line-break\n",
|
|
" Element path=0/1 source=body-group element=html:tbody category=table-row-group\n",
|
|
" Element path=0/2 source=item element=html:li category=list-item\n",
|
|
" Element path=0/3 source=control element=html:input category=form-input-text replaced=form-control\n",
|
|
" Text path=0/4 source=text text=\"hello\"\n",
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_element_semantics_is_a_structured_source_contract_error() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode {
|
|
label: "root",
|
|
kind: LayoutSourceKind::Element,
|
|
text: None,
|
|
children: Vec::new(),
|
|
element_semantics: None,
|
|
replaced_metrics: None,
|
|
}],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
|
|
assert_eq!(
|
|
build_layout_world(&source, &mut styles).unwrap_err(),
|
|
LayoutError::SourceContract {
|
|
source_label: "root".to_owned(),
|
|
detail: "element source has no element semantics".to_owned(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_flat_tree_child_is_rejected_before_box_identity_can_diverge() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2]),
|
|
TestNode::element("first-parent", vec![3]),
|
|
TestNode::element("second-parent", vec![3]),
|
|
TestNode::element("shared-child", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
for node in 0..4 {
|
|
styles.primary.insert(node, style(LayoutDisplay::Block));
|
|
}
|
|
|
|
let error = build_layout_world(&source, &mut styles).unwrap_err();
|
|
assert!(matches!(
|
|
error,
|
|
LayoutError::SourceContract {
|
|
source_label,
|
|
detail,
|
|
} if source_label == "shared-child" && detail.contains("flat_parent")
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn taffy_flex_geometry_enters_the_owned_paint_snapshot() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3]),
|
|
TestNode::element("red", Vec::new()),
|
|
TestNode::element("green", Vec::new()),
|
|
TestNode::element("blue", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
let mut root_taffy = Style::<Atom> {
|
|
display: Display::Flex,
|
|
flex_direction: FlexDirection::Row,
|
|
..Style::default()
|
|
};
|
|
root_taffy.size.height = Dimension::length(100.0);
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(LayoutDisplay::Flex, root_taffy, PaintColor::TRANSPARENT),
|
|
);
|
|
styles.primary.insert(
|
|
1,
|
|
colored_box(
|
|
LayoutDisplay::Block,
|
|
100.0,
|
|
50.0,
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
),
|
|
);
|
|
styles.primary.insert(
|
|
2,
|
|
colored_box(
|
|
LayoutDisplay::Block,
|
|
100.0,
|
|
50.0,
|
|
PaintColor::new(0.0, 1.0, 0.0, 1.0),
|
|
),
|
|
);
|
|
styles.primary.insert(
|
|
3,
|
|
colored_box(
|
|
LayoutDisplay::Block,
|
|
100.0,
|
|
50.0,
|
|
PaintColor::new(0.0, 0.0, 1.0, 1.0),
|
|
),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(400, 200, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(snapshot.canvas_color, PaintColor::WHITE);
|
|
assert_eq!(snapshot.fragments.len(), 3);
|
|
assert_eq!(
|
|
snapshot.fragments[0].solid_fill_in_surface(),
|
|
Some((
|
|
PaintRect::new(0.0, 0.0, 100.0, 50.0),
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0)
|
|
))
|
|
);
|
|
assert_eq!(
|
|
snapshot.fragments[1].solid_fill_in_surface(),
|
|
Some((
|
|
PaintRect::new(100.0, 0.0, 100.0, 50.0),
|
|
PaintColor::new(0.0, 1.0, 0.0, 1.0)
|
|
))
|
|
);
|
|
assert_eq!(
|
|
snapshot.fragments[2].solid_fill_in_surface(),
|
|
Some((
|
|
PaintRect::new(200.0, 0.0, 100.0, 50.0),
|
|
PaintColor::new(0.0, 0.0, 1.0, 1.0)
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reparented_absolute_child_is_not_measured_twice_by_an_inline_context() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("inline-context", vec![2, 3]),
|
|
TestNode::text("text", "in flow"),
|
|
TestNode::element("absolute", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(200.0),
|
|
height: length(100.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
)
|
|
.with_position(LayoutPosition::Relative),
|
|
);
|
|
styles.primary.insert(1, style(LayoutDisplay::Block));
|
|
styles.primary.insert(
|
|
3,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(30.0),
|
|
height: length(10.0),
|
|
},
|
|
inset: Rect {
|
|
left: length(10.0),
|
|
right: taffy::LengthPercentageAuto::auto(),
|
|
top: length(20.0),
|
|
bottom: taffy::LengthPercentageAuto::auto(),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
)
|
|
.with_position(LayoutPosition::Absolute),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(200, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
snapshot
|
|
.diagnostics
|
|
.iter()
|
|
.all(|diagnostic| diagnostic.code != "positioned-inline-context-deferred")
|
|
);
|
|
assert!(snapshot.fragments.iter().any(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.is_some_and(|(rect, color)| {
|
|
rect == PaintRect::new(10.0, 20.0, 30.0, 10.0)
|
|
&& color == PaintColor::new(1.0, 0.0, 0.0, 1.0)
|
|
})
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn positioned_child_of_inline_context_uses_its_parley_static_position() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("inline", vec![2, 3]),
|
|
TestNode::text("text", "in flow"),
|
|
TestNode::element("absolute", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(200.0),
|
|
height: length(100.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
)
|
|
.with_position(LayoutPosition::Relative),
|
|
);
|
|
styles.primary.insert(1, style(LayoutDisplay::Inline));
|
|
styles.primary.insert(
|
|
3,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(30.0),
|
|
height: length(10.0),
|
|
},
|
|
inset: Rect {
|
|
left: length(10.0),
|
|
right: taffy::LengthPercentageAuto::auto(),
|
|
top: length(20.0),
|
|
bottom: taffy::LengthPercentageAuto::auto(),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
)
|
|
.with_position(LayoutPosition::Absolute),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(200, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(snapshot.diagnostics.iter().all(|diagnostic| {
|
|
diagnostic.code != "positioned-inline-context-deferred"
|
|
&& diagnostic.code != "positioned-static-position-deferred"
|
|
}));
|
|
assert!(snapshot.fragments.iter().any(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.is_some_and(|(rect, color)| {
|
|
rect == PaintRect::new(10.0, 20.0, 30.0, 10.0)
|
|
&& color == PaintColor::new(1.0, 0.0, 0.0, 1.0)
|
|
})
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn positioned_inline_containing_block_lays_out_its_absolute_descendant() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2]),
|
|
TestNode::text("text", "in flow"),
|
|
TestNode::element("positioned-inline", vec![3]),
|
|
TestNode::element("absolute", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(
|
|
2,
|
|
style(LayoutDisplay::Inline).with_position(LayoutPosition::Relative),
|
|
);
|
|
styles.primary.insert(
|
|
3,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(30.0),
|
|
height: length(10.0),
|
|
},
|
|
inset: Rect {
|
|
left: length(10.0),
|
|
right: taffy::LengthPercentageAuto::auto(),
|
|
top: length(20.0),
|
|
bottom: taffy::LengthPercentageAuto::auto(),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
)
|
|
.with_position(LayoutPosition::Absolute),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(200, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(snapshot.diagnostics.iter().all(|diagnostic| {
|
|
diagnostic.code != "positioned-inline-context-deferred"
|
|
&& diagnostic.code != "positioned-static-position-deferred"
|
|
}));
|
|
assert!(snapshot.fragments.iter().any(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.is_some_and(|(rect, color)| {
|
|
color == PaintColor::new(1.0, 0.0, 0.0, 1.0)
|
|
&& rect.width == 30.0
|
|
&& rect.height == 10.0
|
|
&& rect.x >= 10.0
|
|
&& rect.y >= 20.0
|
|
})
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn relative_atomic_inline_resolves_insets_against_the_ifc_content_box() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::element("relative-atomic", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(200.0),
|
|
height: length(40.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
),
|
|
);
|
|
let red = PaintColor::new(1.0, 0.0, 0.0, 1.0);
|
|
styles.primary.insert(
|
|
1,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::InlineBlock,
|
|
Style {
|
|
size: Size {
|
|
width: length(20.0),
|
|
height: length(10.0),
|
|
},
|
|
inset: Rect {
|
|
left: taffy::style_helpers::percent(0.1),
|
|
right: length(40.0),
|
|
top: taffy::style_helpers::percent(0.25),
|
|
bottom: length(20.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
red,
|
|
)
|
|
.with_position(LayoutPosition::Relative)
|
|
.with_inline_alignment(LayoutInlineAlignment::Top),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(200, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(snapshot.fragments.iter().any(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.is_some_and(|(rect, color)| {
|
|
rect == PaintRect::new(20.0, 10.0, 20.0, 10.0) && color == red
|
|
})
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn all_auto_absolute_inline_uses_the_zero_width_placeholder_after_atomic_content() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2]),
|
|
TestNode::element("atomic", Vec::new()),
|
|
TestNode::element("absolute-inline", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(200.0),
|
|
height: length(100.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
)
|
|
.with_position(LayoutPosition::Relative),
|
|
);
|
|
styles.primary.insert(
|
|
1,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::InlineBlock,
|
|
Style {
|
|
size: Size {
|
|
width: length(50.0),
|
|
height: length(10.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::new(0.0, 0.0, 1.0, 1.0),
|
|
),
|
|
);
|
|
styles.primary.insert(
|
|
2,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::InlineBlock,
|
|
Style {
|
|
size: Size {
|
|
width: length(20.0),
|
|
height: length(10.0),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
)
|
|
.with_position(LayoutPosition::Absolute),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(200, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(snapshot.diagnostics.iter().all(|diagnostic| {
|
|
diagnostic.code != "positioned-inline-context-deferred"
|
|
&& diagnostic.code != "positioned-static-position-deferred"
|
|
}));
|
|
assert!(snapshot.fragments.iter().any(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.is_some_and(|(rect, color)| {
|
|
color == PaintColor::new(1.0, 0.0, 0.0, 1.0)
|
|
&& rect.x == 50.0
|
|
&& rect.width == 20.0
|
|
&& rect.height == 10.0
|
|
})
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn replaced_metrics_are_used_without_a_resource_backend() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::replaced("image", 64.0, 32.0),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
styles.primary.insert(
|
|
1,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style::<Atom>::default(),
|
|
PaintColor::BLACK,
|
|
),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(200, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
snapshot.fragments.first(),
|
|
Some(&PaintFragment::solid_rect(
|
|
PaintRect::new(0.0, 0.0, 64.0, 32.0),
|
|
PaintColor::BLACK
|
|
))
|
|
);
|
|
let light_gray = PaintColor::new(211.0 / 255.0, 211.0 / 255.0, 211.0 / 255.0, 1.0);
|
|
assert!(snapshot.fragments.iter().any(|fragment| {
|
|
matches!(
|
|
fragment,
|
|
PaintFragment::Border { rect, colors, .. }
|
|
if *rect == PaintRect::new(0.0, 0.0, 64.0, 32.0)
|
|
&& colors.top == light_gray
|
|
&& colors.right == light_gray
|
|
&& colors.bottom == light_gray
|
|
&& colors.left == light_gray
|
|
)
|
|
}));
|
|
assert_eq!(
|
|
snapshot
|
|
.diagnostics
|
|
.iter()
|
|
.map(|diagnostic| diagnostic.code.as_str())
|
|
.collect::<Vec<_>>(),
|
|
vec!["replaced-content-placeholder"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parley_contexts_are_lazy_reused_and_project_owned_glyph_runs() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1]),
|
|
TestNode::text("text", "Moli text"),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
let mut services = DocumentLayoutServices::new();
|
|
assert!(!services.is_initialized());
|
|
|
|
let first = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut services,
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(240, 80, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(services.is_initialized());
|
|
assert_eq!(services.text_layout_passes(), 1);
|
|
assert!(!first.fonts.is_empty());
|
|
assert!(first.fragments.iter().any(
|
|
|fragment| matches!(fragment, PaintFragment::GlyphRun(run) if !run.glyphs.is_empty())
|
|
));
|
|
|
|
let second = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut services,
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(240, 80, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(services.text_layout_passes(), 2);
|
|
assert_eq!(first, second);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_inline_baseline_shift_expands_the_line_once_for_text_and_atomic_content() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 4]),
|
|
TestNode::element("shifted-inline", vec![2, 3]),
|
|
TestNode::text("text", "A"),
|
|
TestNode::element("atomic", Vec::new()),
|
|
TestNode::element("after", Vec::new()),
|
|
],
|
|
};
|
|
let render = |baseline_shift| {
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(100.0),
|
|
height: Dimension::auto(),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
)
|
|
.with_text_metrics(20.0, 20.0),
|
|
);
|
|
styles.primary.insert(
|
|
1,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Inline,
|
|
Style::default(),
|
|
PaintColor::new(0.0, 1.0, 0.0, 1.0),
|
|
)
|
|
// Synthetic styles do not run CSS inheritance; mirror the
|
|
// structural inline's inherited parent font metrics explicitly.
|
|
.with_text_metrics(20.0, 20.0)
|
|
.with_inline_baseline_shift(baseline_shift),
|
|
);
|
|
styles.primary.insert(
|
|
3,
|
|
colored_box(
|
|
LayoutDisplay::InlineBlock,
|
|
10.0,
|
|
10.0,
|
|
PaintColor::new(0.0, 0.0, 1.0, 1.0),
|
|
),
|
|
);
|
|
styles.primary.insert(
|
|
4,
|
|
colored_box(
|
|
LayoutDisplay::Block,
|
|
20.0,
|
|
5.0,
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
),
|
|
);
|
|
build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(100, 100, 1.0)),
|
|
)
|
|
.unwrap()
|
|
};
|
|
let baseline = render(0.0);
|
|
let raised = render(10.0);
|
|
let solid_rect = |snapshot: &moli_layout::PaintSnapshot, color| {
|
|
snapshot
|
|
.fragments
|
|
.iter()
|
|
.find_map(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.filter(|(_, actual)| *actual == color)
|
|
.map(|(rect, _)| rect)
|
|
})
|
|
.expect("colored fixture box")
|
|
};
|
|
let blue = PaintColor::new(0.0, 0.0, 1.0, 1.0);
|
|
let green = PaintColor::new(0.0, 1.0, 0.0, 1.0);
|
|
let red = PaintColor::new(1.0, 0.0, 0.0, 1.0);
|
|
let baseline_atomic = solid_rect(&baseline, blue);
|
|
let raised_atomic = solid_rect(&raised, blue);
|
|
let baseline_inline = solid_rect(&baseline, green);
|
|
let raised_inline = solid_rect(&raised, green);
|
|
let baseline_after = solid_rect(&baseline, red);
|
|
let raised_after = solid_rect(&raised, red);
|
|
|
|
assert!(
|
|
(raised_atomic.y - baseline_atomic.y).abs() < 0.01,
|
|
"baseline={baseline_atomic:?}, raised={raised_atomic:?}"
|
|
);
|
|
assert_eq!(raised_inline, baseline_inline);
|
|
assert!(
|
|
(raised_after.y - baseline_after.y - 10.0).abs() < 0.01,
|
|
"baseline={baseline_after:?}, raised={raised_after:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn baseline_atomic_inline_keeps_the_parent_strut_descent_in_the_line_box() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2]),
|
|
TestNode::element("atomic", Vec::new()),
|
|
TestNode::element("after", Vec::new()),
|
|
],
|
|
};
|
|
let atomic_color = PaintColor::new(0.0, 0.0, 1.0, 1.0);
|
|
let after_color = PaintColor::new(1.0, 0.0, 0.0, 1.0);
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(100.0),
|
|
height: Dimension::auto(),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
)
|
|
.with_text_metrics(14.0, 16.0),
|
|
);
|
|
styles.primary.insert(
|
|
1,
|
|
colored_box(LayoutDisplay::InlineBlock, 48.0, 48.0, atomic_color),
|
|
);
|
|
styles
|
|
.primary
|
|
.insert(2, colored_box(LayoutDisplay::Block, 10.0, 5.0, after_color));
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(100, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
let rect_for = |color| {
|
|
snapshot
|
|
.fragments
|
|
.iter()
|
|
.find_map(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.filter(|(_, actual)| *actual == color)
|
|
.map(|(rect, _)| rect)
|
|
})
|
|
.expect("colored fixture box")
|
|
};
|
|
let atomic = rect_for(atomic_color);
|
|
let after = rect_for(after_color);
|
|
|
|
assert!(
|
|
after.y > atomic.y + atomic.height,
|
|
"the parent font descent must remain below a baseline-aligned atomic inline: \
|
|
atomic={atomic:?}, after={after:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn top_and_bottom_atomic_inlines_share_the_final_line_edges() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![
|
|
TestNode::element("root", vec![1, 2, 3, 4]),
|
|
TestNode::text("strut", "A"),
|
|
TestNode::element("top", Vec::new()),
|
|
TestNode::element("bottom", Vec::new()),
|
|
TestNode::element("after", Vec::new()),
|
|
],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(
|
|
0,
|
|
ResolvedLayoutStyle::synthetic(
|
|
LayoutDisplay::Block,
|
|
Style {
|
|
size: Size {
|
|
width: length(100.0),
|
|
height: Dimension::auto(),
|
|
},
|
|
..Style::default()
|
|
},
|
|
PaintColor::TRANSPARENT,
|
|
)
|
|
.with_text_metrics(20.0, 20.0),
|
|
);
|
|
styles.primary.insert(
|
|
2,
|
|
colored_box(
|
|
LayoutDisplay::InlineBlock,
|
|
10.0,
|
|
30.0,
|
|
PaintColor::new(0.0, 0.0, 1.0, 1.0),
|
|
)
|
|
.with_inline_alignment(LayoutInlineAlignment::Top),
|
|
);
|
|
styles.primary.insert(
|
|
3,
|
|
colored_box(
|
|
LayoutDisplay::InlineBlock,
|
|
10.0,
|
|
10.0,
|
|
PaintColor::new(0.0, 1.0, 0.0, 1.0),
|
|
)
|
|
.with_inline_alignment(LayoutInlineAlignment::Bottom),
|
|
);
|
|
styles.primary.insert(
|
|
4,
|
|
colored_box(
|
|
LayoutDisplay::Block,
|
|
10.0,
|
|
5.0,
|
|
PaintColor::new(1.0, 0.0, 0.0, 1.0),
|
|
),
|
|
);
|
|
|
|
let snapshot = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut DocumentLayoutServices::new(),
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(100, 100, 1.0)),
|
|
)
|
|
.unwrap();
|
|
let rect_for = |color| {
|
|
snapshot
|
|
.fragments
|
|
.iter()
|
|
.find_map(|fragment| {
|
|
fragment
|
|
.solid_fill_in_surface()
|
|
.filter(|(_, actual)| *actual == color)
|
|
.map(|(rect, _)| rect)
|
|
})
|
|
.expect("colored fixture box")
|
|
};
|
|
let top = rect_for(PaintColor::new(0.0, 0.0, 1.0, 1.0));
|
|
let bottom = rect_for(PaintColor::new(0.0, 1.0, 0.0, 1.0));
|
|
let after = rect_for(PaintColor::new(1.0, 0.0, 0.0, 1.0));
|
|
assert!((top.y - 0.0).abs() < 0.01, "top={top:?}, after={after:?}");
|
|
assert!(
|
|
(after.y - 30.0).abs() < 0.01,
|
|
"top={top:?}, bottom={bottom:?}, after={after:?}"
|
|
);
|
|
assert!(
|
|
(bottom.y - 20.0).abs() < 0.01,
|
|
"bottom={bottom:?}, after={after:?}"
|
|
);
|
|
assert!(
|
|
(bottom.y + bottom.height - after.y).abs() < 0.01,
|
|
"bottom={bottom:?}, after={after:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn textless_layout_does_not_initialize_parley() {
|
|
let source = TestSource {
|
|
root: 0,
|
|
nodes: vec![TestNode::element("root", Vec::new())],
|
|
};
|
|
let mut styles = TestStyles::default();
|
|
styles.primary.insert(0, style(LayoutDisplay::Block));
|
|
let mut services = DocumentLayoutServices::new();
|
|
let _ = build_screenshot_snapshot(
|
|
&source,
|
|
&mut styles,
|
|
&mut services,
|
|
ScreenshotLayoutRequest::new(PaintViewport::new(20, 20, 1.0)),
|
|
)
|
|
.unwrap();
|
|
assert!(!services.is_initialized());
|
|
assert_eq!(services.text_layout_passes(), 0);
|
|
}
|