Files
moli/moli-layout/tests/main_layout_contract.rs

1656 lines
47 KiB
Rust

use std::collections::HashMap;
use moli_layout::{
DocumentLayoutServices, LayoutDisplay, LayoutElementCategory, LayoutElementSemantics,
LayoutError, LayoutFlushReason, LayoutNamespace, LayoutPassRequest, LayoutPosition,
LayoutReplacedKind, LayoutSource, LayoutSourceKind, LayoutStyleResolver, PaintBlendMode,
PaintCaptureRequest, PaintColor, PaintFragment, PaintRect, PaintSnapshot, PaintViewport,
ReplacedMetrics, ResolvedLayoutElementStyles, ResolvedLayoutStyle, ScreenshotLayoutRequest,
build_layout_pass, build_screenshot_snapshot,
};
use style::Atom;
use taffy::{
AlignContent, BoxSizing, Dimension, Display, FlexDirection, FlexWrap, Line, Rect, Size, Style,
style_helpers::{fr, length, line, percent},
};
const RED: PaintColor = PaintColor::new(1.0, 0.0, 0.0, 1.0);
const GREEN: PaintColor = PaintColor::new(0.0, 1.0, 0.0, 1.0);
const BLUE: PaintColor = PaintColor::new(0.0, 0.0, 1.0, 1.0);
const YELLOW: PaintColor = PaintColor::new(1.0, 1.0, 0.0, 1.0);
#[derive(Clone, Debug)]
struct FixtureNode {
label: &'static str,
semantics: LayoutElementSemantics,
children: Vec<usize>,
replaced_metrics: Option<ReplacedMetrics>,
}
impl FixtureNode {
fn div(label: &'static str, children: Vec<usize>) -> Self {
Self::html(label, "div", children)
}
fn html(label: &'static str, local_name: &'static str, children: Vec<usize>) -> Self {
Self {
label,
semantics: LayoutElementSemantics::new(
LayoutNamespace::Html,
local_name,
LayoutElementCategory::Generic,
None,
),
children,
replaced_metrics: None,
}
}
fn svg(label: &'static str, width: f32, height: f32) -> Self {
Self {
label,
semantics: LayoutElementSemantics::new(
LayoutNamespace::Svg,
"svg",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Svg),
),
children: Vec::new(),
replaced_metrics: Some(ReplacedMetrics {
intrinsic_width: Some(width),
intrinsic_height: Some(height),
intrinsic_ratio: Some(width / height),
..ReplacedMetrics::default()
}),
}
}
fn iframe(label: &'static str) -> Self {
Self {
label,
semantics: LayoutElementSemantics::new(
LayoutNamespace::Html,
"iframe",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Frame),
),
children: Vec::new(),
replaced_metrics: None,
}
}
}
#[derive(Debug)]
struct FixtureSource {
nodes: Vec<FixtureNode>,
}
impl LayoutSource for FixtureSource {
type NodeId = usize;
type ChildIter<'a> = std::iter::Copied<std::slice::Iter<'a, usize>>;
fn root(&self) -> Self::NodeId {
0
}
fn flat_parent(&self, node: Self::NodeId) -> Option<Self::NodeId> {
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 {
LayoutSourceKind::Element
}
fn element_semantics(&self, node: Self::NodeId) -> Option<LayoutElementSemantics> {
Some(self.nodes[node].semantics.clone())
}
fn text(&self, _node: Self::NodeId) -> Option<&str> {
None
}
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 FixtureStyles(HashMap<usize, ResolvedLayoutStyle>);
impl LayoutStyleResolver<usize> for FixtureStyles {
fn element_styles(
&mut self,
node: usize,
) -> Result<Option<ResolvedLayoutElementStyles>, LayoutError> {
Ok(self
.0
.get(&node)
.cloned()
.map(ResolvedLayoutElementStyles::from_primary))
}
fn anonymous_style(
&mut self,
_owner: usize,
parent: &ResolvedLayoutStyle,
display: LayoutDisplay,
) -> Result<ResolvedLayoutStyle, LayoutError> {
Ok(ResolvedLayoutStyle::anonymous_from(parent, display))
}
}
fn resolved(display: LayoutDisplay, taffy: Style<Atom>, color: PaintColor) -> ResolvedLayoutStyle {
ResolvedLayoutStyle::synthetic(display, taffy, color)
}
fn fixed_box(
display: LayoutDisplay,
width: f32,
height: f32,
color: PaintColor,
) -> ResolvedLayoutStyle {
resolved(
display,
Style {
size: Size {
width: Dimension::length(width),
height: Dimension::length(height),
},
..Style::default()
},
color,
)
}
fn render(
source: &FixtureSource,
styles: &mut FixtureStyles,
viewport: PaintViewport,
) -> PaintSnapshot {
build_screenshot_snapshot(
source,
styles,
&mut DocumentLayoutServices::new(),
ScreenshotLayoutRequest::new(viewport),
)
.unwrap()
}
fn solid_rect(snapshot: &PaintSnapshot, color: PaintColor) -> PaintRect {
snapshot
.fragments
.iter()
.find_map(|fragment| {
fragment
.solid_fill_in_surface()
.filter(|(_, actual)| *actual == color)
.map(|(rect, _)| rect)
})
.unwrap_or_else(|| panic!("missing {color:?} rect in {:?}", snapshot.fragments))
}
fn assert_rect(actual: PaintRect, expected: PaintRect) {
for (name, actual, expected) in [
("x", actual.x, expected.x),
("y", actual.y, expected.y),
("width", actual.width, expected.width),
("height", actual.height, expected.height),
] {
assert!(
(actual - expected).abs() <= 0.01,
"{name}: expected {expected}, got {actual}; actual rect={actual:?}"
);
}
}
#[test]
fn explicit_and_implicit_grid_tracks_use_real_grid_layout() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("grid", vec![1, 2, 3]),
FixtureNode::div("first", Vec::new()),
FixtureNode::div("second", Vec::new()),
FixtureNode::div("implicit", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Grid,
Style {
display: Display::Grid,
size: Size {
width: length(400.0),
height: length(200.0),
},
gap: Size {
width: length(10.0),
height: length(20.0),
},
grid_template_columns: vec![length(100.0), fr(1.0)],
grid_template_rows: vec![length(50.0), fr(1.0)],
grid_auto_rows: vec![length(30.0)],
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
grid_column: line(1),
grid_row: line(1),
..Style::default()
},
RED,
),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
grid_column: line(2),
grid_row: Line {
start: line(1),
end: line(3),
},
..Style::default()
},
GREEN,
),
);
styles.0.insert(
3,
resolved(
LayoutDisplay::Block,
Style {
grid_column: line(1),
grid_row: line(3),
..Style::default()
},
BLUE,
),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
assert!(snapshot.diagnostics.is_empty());
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(0.0, 0.0, 100.0, 50.0),
);
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(110.0, 0.0, 290.0, 150.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(0.0, 170.0, 100.0, 30.0),
);
}
#[test]
fn flex_wrap_gap_and_cross_axis_distribution_are_numeric() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("flex", vec![1, 2, 3]),
FixtureNode::div("first", Vec::new()),
FixtureNode::div("second", Vec::new()),
FixtureNode::div("third", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Flex,
Style {
display: Display::Flex,
size: Size {
width: length(250.0),
height: length(160.0),
},
flex_direction: FlexDirection::Row,
flex_wrap: FlexWrap::Wrap,
gap: Size {
width: length(10.0),
height: length(20.0),
},
align_content: Some(AlignContent::SPACE_BETWEEN),
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
for (node, color) in [(1, RED), (2, GREEN), (3, BLUE)] {
styles
.0
.insert(node, fixed_box(LayoutDisplay::Block, 100.0, 40.0, color));
}
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(0.0, 0.0, 100.0, 40.0),
);
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(110.0, 0.0, 100.0, 40.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(0.0, 120.0, 100.0, 40.0),
);
}
#[test]
fn flex_order_changes_layout_and_paint_order_stably() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("flex", vec![1, 2, 3]),
FixtureNode::div("late", Vec::new()),
FixtureNode::div("early", Vec::new()),
FixtureNode::div("middle", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Flex,
Style {
display: Display::Flex,
size: Size {
width: length(300.0),
height: length(40.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
fixed_box(LayoutDisplay::Block, 50.0, 40.0, RED).with_order(2),
);
styles.0.insert(
2,
fixed_box(LayoutDisplay::Block, 50.0, 40.0, GREEN).with_order(-1),
);
styles.0.insert(
3,
fixed_box(LayoutDisplay::Block, 50.0, 40.0, BLUE).with_order(0),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(300, 100, 1.0));
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(0.0, 0.0, 50.0, 40.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(50.0, 0.0, 50.0, 40.0),
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(100.0, 0.0, 50.0, 40.0),
);
let paint_order = snapshot
.fragments
.iter()
.filter_map(|fragment| fragment.solid_fill().map(|(_, color, _)| color))
.collect::<Vec<_>>();
assert_eq!(paint_order, vec![GREEN, BLUE, RED]);
}
#[test]
fn stacking_context_levels_and_effect_layer_are_atomic() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1, 2, 3, 4]),
FixtureNode::div("positive", Vec::new()),
FixtureNode::div("auto", Vec::new()),
FixtureNode::div("zero", Vec::new()),
FixtureNode::div("negative", Vec::new()),
],
};
let positioned = |color| {
fixed_box(LayoutDisplay::Block, 20.0, 20.0, color).with_position(LayoutPosition::Absolute)
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
fixed_box(LayoutDisplay::Block, 40.0, 40.0, PaintColor::TRANSPARENT)
.with_position(LayoutPosition::Relative),
);
styles.0.insert(1, positioned(YELLOW).with_z_index(2));
styles.0.insert(2, positioned(RED));
styles.0.insert(
3,
positioned(BLUE)
.with_z_index(0)
.with_opacity(0.5)
.with_blend_mode(PaintBlendMode::Multiply),
);
styles.0.insert(4, positioned(GREEN).with_z_index(-1));
let snapshot = render(&source, &mut styles, PaintViewport::new(40, 40, 1.0));
let paint_order = snapshot
.fragments
.iter()
.filter_map(|fragment| fragment.solid_fill().map(|(_, color, _)| color))
.collect::<Vec<_>>();
assert_eq!(paint_order, vec![GREEN, RED, BLUE, YELLOW]);
let blue = snapshot
.fragments
.iter()
.position(|fragment| {
fragment
.solid_fill()
.is_some_and(|(_, color, _)| color == BLUE)
})
.unwrap();
assert!(matches!(
snapshot.fragments.get(blue - 1),
Some(PaintFragment::PushLayer {
opacity,
blend_mode: PaintBlendMode::Multiply,
..
}) if (*opacity - 0.5).abs() < f32::EPSILON
));
assert!(matches!(
snapshot.fragments.get(blue + 1),
Some(PaintFragment::PopLayer)
));
}
#[test]
fn flex_grow_shrink_and_cross_axis_alignment_are_numeric() {
let grow_source = FixtureSource {
nodes: vec![
FixtureNode::div("grow", vec![1, 2]),
FixtureNode::div("one", Vec::new()),
FixtureNode::div("three", Vec::new()),
],
};
let mut grow_styles = FixtureStyles::default();
grow_styles.0.insert(
0,
resolved(
LayoutDisplay::Flex,
Style {
display: Display::Flex,
size: Size {
width: length(300.0),
height: length(100.0),
},
align_items: Some(taffy::AlignItems::CENTER),
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
grow_styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::auto(),
height: length(20.0),
},
flex_basis: length(100.0),
flex_grow: 1.0,
..Style::default()
},
RED,
),
);
grow_styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::auto(),
height: length(40.0),
},
flex_basis: length(100.0),
flex_grow: 3.0,
..Style::default()
},
BLUE,
),
);
let grown = render(
&grow_source,
&mut grow_styles,
PaintViewport::new(300, 100, 1.0),
);
assert_rect(
solid_rect(&grown, RED),
PaintRect::new(0.0, 40.0, 125.0, 20.0),
);
assert_rect(
solid_rect(&grown, BLUE),
PaintRect::new(125.0, 30.0, 175.0, 40.0),
);
let mut shrink_styles = FixtureStyles::default();
shrink_styles.0.insert(
0,
resolved(
LayoutDisplay::Flex,
Style {
display: Display::Flex,
size: Size {
width: length(140.0),
height: length(30.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
for (node, color, shrink) in [(1, RED, 1.0), (2, BLUE, 3.0)] {
shrink_styles.0.insert(
node,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::auto(),
height: length(20.0),
},
flex_basis: length(100.0),
flex_shrink: shrink,
..Style::default()
},
color,
),
);
}
let shrunk = render(
&grow_source,
&mut shrink_styles,
PaintViewport::new(300, 100, 1.0),
);
assert_rect(
solid_rect(&shrunk, RED),
PaintRect::new(0.0, 0.0, 85.0, 20.0),
);
assert_rect(
solid_rect(&shrunk, BLUE),
PaintRect::new(85.0, 0.0, 55.0, 20.0),
);
}
#[test]
fn grid_order_controls_auto_placement_order() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("grid", vec![1, 2, 3]),
FixtureNode::div("late", Vec::new()),
FixtureNode::div("early", Vec::new()),
FixtureNode::div("middle", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Grid,
Style {
display: Display::Grid,
size: Size {
width: length(300.0),
height: length(50.0),
},
grid_template_columns: vec![length(100.0), length(100.0), length(100.0)],
grid_template_rows: vec![length(50.0)],
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(LayoutDisplay::Block, Style::default(), RED).with_order(2),
);
styles.0.insert(
2,
resolved(LayoutDisplay::Block, Style::default(), GREEN).with_order(-1),
);
styles.0.insert(
3,
resolved(LayoutDisplay::Block, Style::default(), BLUE).with_order(0),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(300, 100, 1.0));
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(0.0, 0.0, 100.0, 50.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(100.0, 0.0, 100.0, 50.0),
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(200.0, 0.0, 100.0, 50.0),
);
}
#[test]
fn grid_item_placement_and_self_alignment_use_the_selected_grid_area() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("grid", vec![1]),
FixtureNode::div("aligned", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Grid,
Style {
display: Display::Grid,
size: Size {
width: length(300.0),
height: length(200.0),
},
grid_template_columns: vec![length(100.0), length(200.0)],
grid_template_rows: vec![length(50.0), length(150.0)],
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(20.0),
height: length(10.0),
},
grid_column: line(2),
grid_row: line(2),
justify_self: Some(taffy::AlignSelf::END),
align_self: Some(taffy::AlignSelf::CENTER),
..Style::default()
},
RED,
),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(300, 200, 1.0));
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(280.0, 120.0, 20.0, 10.0),
);
}
#[test]
fn grid_container_justify_items_center_shrink_wraps_an_auto_sized_item() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("grid", vec![1]),
FixtureNode::div("item", vec![2]),
FixtureNode::div("intrinsic-child", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Grid,
Style {
display: Display::Grid,
size: Size {
width: length(300.0),
height: length(200.0),
},
justify_items: Some(taffy::AlignItems::CENTER),
align_items: Some(taffy::AlignItems::CENTER),
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles
.0
.insert(1, resolved(LayoutDisplay::Block, Style::default(), GREEN));
styles
.0
.insert(2, fixed_box(LayoutDisplay::Block, 20.0, 10.0, RED));
let snapshot = render(&source, &mut styles, PaintViewport::new(300, 200, 1.0));
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(140.0, 95.0, 20.0, 10.0),
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(140.0, 95.0, 20.0, 10.0),
);
}
#[test]
fn centered_grid_item_with_percent_height_and_auto_block_margin_keeps_intrinsic_inline_size() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("grid", vec![1]),
FixtureNode::div("item", vec![2]),
FixtureNode::svg("logo", 272.0, 92.0),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Grid,
Style {
display: Display::Grid,
size: Size {
width: length(1280.0),
height: length(160.0),
},
padding: Rect {
left: length(96.0),
right: length(96.0),
top: length(0.0),
bottom: length(0.0),
},
grid_template_rows: vec![fr(1.0)],
justify_items: Some(taffy::AlignItems::CENTER),
align_items: Some(taffy::AlignItems::CENTER),
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::auto(),
height: percent(1.0),
},
max_size: Size {
width: Dimension::auto(),
height: length(92.0),
},
margin: Rect {
left: length(0.0),
right: length(0.0),
top: taffy::LengthPercentageAuto::auto(),
bottom: length(0.0),
},
..Style::default()
},
GREEN,
)
.with_position(LayoutPosition::Relative),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Inline,
Style {
size: Size {
width: Dimension::auto(),
height: length(92.0),
},
max_size: Size {
width: percent(1.0),
height: percent(1.0),
},
..Style::default()
},
RED,
),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(1280, 720, 1.0));
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(504.0, 68.0, 272.0, 92.0),
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(504.0, 68.0, 272.0, 92.0),
);
}
#[test]
fn oversized_percentage_inline_iframe_is_not_clamped_to_the_line_width() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1]),
FixtureNode::iframe("iframe"),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
fixed_box(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Inline,
Style {
size: Size {
width: percent(1.25),
height: length(40.0),
},
..Style::default()
},
BLUE,
),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(300, 200, 1.0));
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(0.0, 0.0, 250.0, 40.0),
);
}
#[test]
fn static_insets_are_ignored_but_relative_insets_shift_the_box() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1, 2]),
FixtureNode::div("static", Vec::new()),
FixtureNode::div("relative", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style::default(),
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(20.0),
height: length(10.0),
},
inset: Rect {
left: length(80.0),
right: taffy::LengthPercentageAuto::auto(),
top: length(40.0),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
RED,
)
.with_position(LayoutPosition::Static),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(20.0),
height: length(10.0),
},
inset: Rect {
left: length(30.0),
right: taffy::LengthPercentageAuto::auto(),
top: length(5.0),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
BLUE,
)
.with_position(LayoutPosition::Relative),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(200, 100, 1.0));
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(0.0, 0.0, 20.0, 10.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(30.0, 15.0, 20.0, 10.0),
);
}
#[test]
fn root_percentages_and_root_level_absolute_boxes_resolve_against_viewport() {
let source = FixtureSource {
nodes: vec![
FixtureNode::html("html", "html", vec![1]),
FixtureNode::div("static", vec![2]),
FixtureNode::div("absolute", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: percent(0.5),
height: percent(1.0),
},
margin: Rect {
left: length(20.0),
right: length(0.0),
top: length(10.0),
bottom: length(0.0),
},
..Style::default()
},
BLUE,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(100.0),
height: length(50.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: percent(0.5),
height: length(20.0),
},
inset: Rect {
left: percent(0.1),
right: taffy::LengthPercentageAuto::auto(),
top: percent(0.25),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
RED,
)
.with_position(LayoutPosition::Absolute),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
// The HTML background propagates to the canvas, so its box is intentionally
// not emitted as a local blue fragment.
assert_eq!(snapshot.canvas_color, BLUE);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(80.0, 150.0, 400.0, 20.0),
);
}
#[test]
fn horizontal_auto_margins_center_a_definite_block() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1]),
FixtureNode::div("centered", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(400.0),
height: length(100.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(100.0),
height: length(20.0),
},
margin: Rect {
left: taffy::LengthPercentageAuto::auto(),
right: taffy::LengthPercentageAuto::auto(),
top: length(0.0),
bottom: length(0.0),
},
..Style::default()
},
RED,
),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(150.0, 0.0, 100.0, 20.0),
);
}
#[test]
fn block_percentage_box_sizing_and_min_max_resolve_against_containing_block() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1, 2]),
FixtureNode::div("content-box", Vec::new()),
FixtureNode::div("clamped", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style {
box_sizing: BoxSizing::ContentBox,
size: Size {
width: length(400.0),
height: Dimension::auto(),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
box_sizing: BoxSizing::ContentBox,
size: Size {
width: percent(0.5),
height: length(20.0),
},
padding: Rect {
left: percent(0.1),
right: percent(0.1),
top: length(5.0),
bottom: length(5.0),
},
border: Rect {
left: length(5.0),
right: length(5.0),
top: length(2.0),
bottom: length(2.0),
},
..Style::default()
},
RED,
),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: percent(0.9),
height: length(10.0),
},
max_size: Size {
width: length(240.0),
height: Dimension::auto(),
},
margin: Rect {
left: length(30.0),
right: taffy::LengthPercentageAuto::auto(),
top: length(10.0),
bottom: length(0.0),
},
..Style::default()
},
GREEN,
),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(0.0, 0.0, 290.0, 34.0),
);
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(30.0, 44.0, 240.0, 10.0),
);
}
#[test]
fn block_margin_collapse_matches_parent_child_and_sibling_rules() {
let source = FixtureSource {
nodes: vec![
FixtureNode::html("body", "body", vec![1, 4]),
FixtureNode::div("collapsing-parent", vec![2, 3]),
FixtureNode::div("first", Vec::new()),
FixtureNode::div("second", Vec::new()),
FixtureNode::div("after", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style::default(),
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(400.0),
height: Dimension::auto(),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::auto(),
height: length(20.0),
},
margin: Rect {
left: length(0.0),
right: length(0.0),
top: length(30.0),
bottom: length(40.0),
},
..Style::default()
},
RED,
),
);
styles.0.insert(
3,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::auto(),
height: length(10.0),
},
margin: Rect {
left: length(0.0),
right: length(0.0),
top: length(20.0),
bottom: length(50.0),
},
..Style::default()
},
BLUE,
),
);
styles
.0
.insert(4, fixed_box(LayoutDisplay::Block, 800.0, 5.0, GREEN));
// This test inspects all three painted boxes to validate margin collapse,
// so request the document surface instead of relying on offscreen viewport
// paint that capture culling now intentionally omits.
let snapshot = build_layout_pass(
&source,
&mut styles,
&mut DocumentLayoutServices::new(),
LayoutPassRequest::with_capture(
PaintViewport::new(800, 100, 1.0),
LayoutFlushReason::Test,
PaintCaptureRequest::full_document(),
),
)
.unwrap()
.into_paint_snapshot()
.expect("full-document paint");
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(0.0, 30.0, 400.0, 20.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(0.0, 90.0, 400.0, 10.0),
);
assert_rect(
solid_rect(&snapshot, GREEN),
PaintRect::new(0.0, 150.0, 800.0, 5.0),
);
assert_eq!(snapshot.content_size.height, 155.0);
}
#[test]
fn aspect_ratio_and_absolute_auto_size_use_the_containing_block() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("positioned", vec![1, 2]),
FixtureNode::div("ratio", Vec::new()),
FixtureNode::div("absolute", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style {
box_sizing: BoxSizing::ContentBox,
size: Size {
width: length(400.0),
height: length(300.0),
},
padding: Rect {
left: length(20.0),
right: length(20.0),
top: length(20.0),
bottom: length(20.0),
},
border: Rect {
left: length(5.0),
right: length(5.0),
top: length(5.0),
bottom: length(5.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
)
.with_position(LayoutPosition::Relative),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(120.0),
height: Dimension::auto(),
},
aspect_ratio: Some(2.0),
..Style::default()
},
BLUE,
),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
inset: Rect {
left: length(10.0),
right: length(30.0),
top: length(20.0),
bottom: length(40.0),
},
..Style::default()
},
RED,
)
.with_position(LayoutPosition::Absolute),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(25.0, 25.0, 120.0, 60.0),
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(15.0, 25.0, 400.0, 280.0),
);
}
#[test]
fn absolute_uses_nearest_positioned_ancestor_and_fixed_uses_viewport() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1]),
FixtureNode::div("positioned", vec![2, 4]),
FixtureNode::div("static", vec![3]),
FixtureNode::div("absolute", Vec::new()),
FixtureNode::div("fixed", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style::default(),
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
1,
resolved(
LayoutDisplay::Block,
Style {
box_sizing: BoxSizing::ContentBox,
size: Size {
width: length(400.0),
height: length(300.0),
},
margin: Rect {
left: length(30.0),
right: length(0.0),
top: length(30.0),
bottom: length(30.0),
},
padding: Rect {
left: length(20.0),
right: length(20.0),
top: length(20.0),
bottom: length(20.0),
},
border: Rect {
left: length(5.0),
right: length(5.0),
top: length(5.0),
bottom: length(5.0),
},
..Style::default()
},
YELLOW,
)
.with_position(LayoutPosition::Relative),
);
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(200.0),
height: length(100.0),
},
margin: Rect {
left: length(10.0),
right: length(0.0),
top: length(10.0),
bottom: length(0.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.0.insert(
3,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: percent(0.5),
height: length(10.0),
},
inset: Rect {
left: percent(0.1),
right: taffy::LengthPercentageAuto::auto(),
top: percent(0.25),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
RED,
)
.with_position(LayoutPosition::Absolute),
);
styles.0.insert(
4,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(30.0),
height: length(40.0),
},
inset: Rect {
left: taffy::LengthPercentageAuto::auto(),
right: length(10.0),
top: taffy::LengthPercentageAuto::auto(),
bottom: length(20.0),
},
..Style::default()
},
BLUE,
)
.with_position(LayoutPosition::Fixed),
);
let snapshot = render(&source, &mut styles, PaintViewport::new(800, 600, 1.0));
assert_rect(
solid_rect(&snapshot, YELLOW),
PaintRect::new(30.0, 30.0, 450.0, 350.0),
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(79.0, 120.0, 220.0, 10.0),
);
assert_rect(
solid_rect(&snapshot, BLUE),
PaintRect::new(760.0, 540.0, 30.0, 40.0),
);
assert!(
!snapshot
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "fixed-position-layout-deferred")
);
}
#[test]
fn html_body_canvas_background_propagation_has_one_paint_owner() {
let source = FixtureSource {
nodes: vec![
FixtureNode::html("html", "html", vec![1]),
FixtureNode::html("body", "body", vec![2]),
FixtureNode::div("content", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style::default(),
PaintColor::TRANSPARENT,
),
);
styles
.0
.insert(1, resolved(LayoutDisplay::Block, Style::default(), BLUE));
styles
.0
.insert(2, fixed_box(LayoutDisplay::Block, 40.0, 30.0, RED));
let snapshot = render(&source, &mut styles, PaintViewport::new(200, 100, 1.0));
assert_eq!(snapshot.canvas_color, BLUE);
assert!(
snapshot.fragments.iter().all(|fragment| {
!fragment
.solid_fill()
.is_some_and(|(_, color, _)| color == BLUE)
}),
"the propagated body background must not also paint at the body box"
);
assert_rect(
solid_rect(&snapshot, RED),
PaintRect::new(0.0, 0.0, 40.0, 30.0),
);
styles
.0
.insert(0, resolved(LayoutDisplay::Block, Style::default(), GREEN));
let root_wins = render(&source, &mut styles, PaintViewport::new(200, 100, 1.0));
assert_eq!(root_wins.canvas_color, GREEN);
assert!(root_wins.fragments.iter().any(|fragment| {
fragment
.solid_fill()
.is_some_and(|(_, color, _)| color == BLUE)
}));
}
#[test]
fn document_extent_includes_flow_and_absolute_overflow_but_excludes_fixed_subtrees() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1, 2, 3]),
FixtureNode::div("flow", Vec::new()),
FixtureNode::div("absolute", Vec::new()),
FixtureNode::div("fixed", vec![4]),
FixtureNode::div("fixed-child", Vec::new()),
],
};
let mut styles = FixtureStyles::default();
styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style::default(),
PaintColor::TRANSPARENT,
)
.with_position(LayoutPosition::Relative),
);
styles
.0
.insert(1, fixed_box(LayoutDisplay::Block, 20.0, 500.0, RED));
styles.0.insert(
2,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(20.0),
height: length(50.0),
},
inset: Rect {
left: length(0.0),
right: taffy::LengthPercentageAuto::auto(),
top: length(700.0),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
GREEN,
)
.with_position(LayoutPosition::Absolute),
);
styles.0.insert(
3,
resolved(
LayoutDisplay::Block,
Style {
size: Size {
width: length(20.0),
height: length(100.0),
},
inset: Rect {
left: length(0.0),
right: taffy::LengthPercentageAuto::auto(),
top: length(900.0),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
BLUE,
)
.with_position(LayoutPosition::Fixed),
);
styles
.0
.insert(4, fixed_box(LayoutDisplay::Block, 600.0, 300.0, YELLOW));
let snapshot = render(&source, &mut styles, PaintViewport::new(300, 200, 1.0));
assert_eq!(snapshot.content_size.width, 300.0);
assert_eq!(snapshot.content_size.height, 750.0);
}
#[test]
fn device_pixel_ratio_does_not_change_css_layout_coordinates() {
let source = FixtureSource {
nodes: vec![
FixtureNode::div("root", vec![1]),
FixtureNode::div("box", Vec::new()),
],
};
let mut first_styles = FixtureStyles::default();
first_styles.0.insert(
0,
resolved(
LayoutDisplay::Block,
Style::default(),
PaintColor::TRANSPARENT,
),
);
first_styles
.0
.insert(1, fixed_box(LayoutDisplay::Block, 75.0, 25.0, RED));
let mut second_styles = FixtureStyles(first_styles.0.clone());
let one_x = render(
&source,
&mut first_styles,
PaintViewport::new(300, 200, 1.0),
);
let two_x = render(
&source,
&mut second_styles,
PaintViewport::new(300, 200, 2.0),
);
assert_eq!(solid_rect(&one_x, RED), solid_rect(&two_x, RED));
}