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

2990 lines
94 KiB
Rust

use std::{collections::HashMap, sync::Arc};
use moli_layout::{
DocumentLayoutServices, LayoutDisplay, LayoutElementCategory, LayoutElementMetadata,
LayoutElementSemantics, LayoutError, LayoutFormControlData, LayoutFormControlKind,
LayoutImageResource, LayoutInputControlKind, LayoutListData, LayoutListMarkerPosition,
LayoutListMarkerType, LayoutListRole, LayoutNamespace, LayoutPosition, LayoutPseudo,
LayoutReplacedKind, LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTableData,
LayoutTableRole, LayoutTextSelection, PaintBrush, PaintColor, PaintFragment, PaintRect,
PaintShape, PaintSnapshot, PaintTransform2D, PaintViewport, ReplacedMetrics,
ResolvedLayoutElementStyles, ResolvedLayoutPseudoStyle, ResolvedLayoutStyle,
ScreenshotLayoutRequest, build_screenshot_snapshot,
};
use style::Atom;
use taffy::{
BoxSizing, Clear, Dimension, FlexDirection, Float, Overflow, Point, Rect, Size, Style,
};
const RED: PaintColor = PaintColor::new(0.9, 0.1, 0.1, 1.0);
const GREEN: PaintColor = PaintColor::new(0.1, 0.7, 0.2, 1.0);
const BLUE: PaintColor = PaintColor::new(0.1, 0.25, 0.8, 1.0);
const YELLOW: PaintColor = PaintColor::new(0.95, 0.8, 0.1, 1.0);
#[derive(Clone)]
struct Node {
label: &'static str,
kind: LayoutSourceKind,
semantics: Option<LayoutElementSemantics>,
text: Option<&'static str>,
children: Vec<usize>,
metrics: Option<ReplacedMetrics>,
image: Option<LayoutImageResource>,
selection: Option<LayoutTextSelection>,
}
impl Node {
fn element(
label: &'static str,
local_name: &'static str,
category: LayoutElementCategory,
replaced: Option<LayoutReplacedKind>,
children: Vec<usize>,
) -> Self {
Self {
label,
kind: LayoutSourceKind::Element,
semantics: Some(LayoutElementSemantics::new(
LayoutNamespace::Html,
local_name,
category,
replaced,
)),
text: None,
children,
metrics: None,
image: None,
selection: None,
}
}
fn text(label: &'static str, text: &'static str) -> Self {
Self {
label,
kind: LayoutSourceKind::Text,
semantics: None,
text: Some(text),
children: Vec::new(),
metrics: None,
image: None,
selection: None,
}
}
fn with_metadata(mut self, metadata: LayoutElementMetadata) -> Self {
self.semantics.as_mut().unwrap().metadata = metadata;
self
}
fn with_selection(mut self, selection: LayoutTextSelection) -> Self {
self.selection = Some(selection);
self
}
fn with_metrics(mut self, metrics: ReplacedMetrics) -> Self {
self.metrics = Some(metrics);
self
}
fn with_image(mut self, image: LayoutImageResource) -> Self {
self.image = Some(image);
self
}
}
struct Source(Vec<Node>);
impl LayoutSource for Source {
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> {
if node == 0 {
return None;
}
self.0
.iter()
.position(|candidate| candidate.children.contains(&node))
}
fn flat_children(&self, node: Self::NodeId) -> Self::ChildIter<'_> {
self.0[node].children.iter().copied()
}
fn node_kind(&self, node: Self::NodeId) -> LayoutSourceKind {
self.0[node].kind
}
fn element_semantics(&self, node: Self::NodeId) -> Option<LayoutElementSemantics> {
self.0[node].semantics.clone()
}
fn text(&self, node: Self::NodeId) -> Option<&str> {
self.0[node].text
}
fn label(&self, node: Self::NodeId) -> String {
self.0[node].label.to_owned()
}
fn text_selection(&self, node: Self::NodeId) -> Option<LayoutTextSelection> {
self.0[node].selection
}
fn replaced_metrics(&self, node: Self::NodeId) -> Option<ReplacedMetrics> {
self.0[node].metrics
}
fn replaced_image(
&self,
node: Self::NodeId,
_style: &ResolvedLayoutStyle,
) -> Option<LayoutImageResource> {
self.0[node].image.clone()
}
}
#[derive(Default)]
struct Styles {
primary: HashMap<usize, ResolvedLayoutStyle>,
pseudo: HashMap<(usize, LayoutPseudo), ResolvedLayoutStyle>,
}
impl LayoutStyleResolver<usize> for Styles {
fn element_styles(
&mut self,
node: usize,
) -> Result<Option<ResolvedLayoutElementStyles>, LayoutError> {
Ok(self
.primary
.get(&node)
.cloned()
.map(ResolvedLayoutElementStyles::from_primary))
}
fn marker_style(
&mut self,
node: usize,
) -> Result<Option<ResolvedLayoutPseudoStyle>, LayoutError> {
Ok(self
.pseudo
.get(&(node, LayoutPseudo::Marker))
.cloned()
.map(ResolvedLayoutPseudoStyle::new))
}
}
fn style(display: LayoutDisplay, color: PaintColor) -> ResolvedLayoutStyle {
ResolvedLayoutStyle::synthetic(display, Style::default(), color)
}
fn sized(
display: LayoutDisplay,
width: f32,
height: f32,
color: PaintColor,
) -> ResolvedLayoutStyle {
style(display, color)
.with_position(LayoutPosition::Static)
.tap_taffy(|taffy| {
taffy.size = Size {
width: Dimension::length(width),
height: Dimension::length(height),
};
})
}
trait StyleTestExt {
fn tap_taffy(self, update: impl FnOnce(&mut Style<Atom>)) -> Self;
}
impl StyleTestExt for ResolvedLayoutStyle {
fn tap_taffy(mut self, update: impl FnOnce(&mut Style<Atom>)) -> Self {
// Synthetic tests deliberately need direct numeric inputs. Recreate
// through the public constructor because the retained Taffy style is
// otherwise correctly encapsulated.
let display = self.display();
let color = self.background_color();
let mut taffy = Style::default();
update(&mut taffy);
self = ResolvedLayoutStyle::synthetic(display, taffy, color);
self
}
}
fn render(source: &Source, styles: &mut Styles, width: u32, height: u32) -> PaintSnapshot {
build_screenshot_snapshot(
source,
styles,
&mut DocumentLayoutServices::new(),
ScreenshotLayoutRequest::new(PaintViewport::new(width, height, 1.0)),
)
.unwrap()
}
fn 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:?} in {:?}", snapshot.fragments))
}
fn assert_close(actual: f32, expected: f32) {
assert!((actual - expected).abs() < 0.05, "{actual} != {expected}");
}
#[test]
fn table_caption_tracks_rows_cells_and_common_spans_share_one_wrapper_geometry() {
use LayoutElementCategory::Table;
use LayoutTableRole::{BodyGroup, Caption, Cell, Row, Table as Root};
let cell = |label, metadata| {
Node::element(label, "td", Table(Cell), None, Vec::new()).with_metadata(
LayoutElementMetadata {
table: Some(metadata),
..LayoutElementMetadata::default()
},
)
};
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element("table", "table", Table(Root), None, vec![2, 3]),
Node::element("caption", "caption", Table(Caption), None, Vec::new()),
Node::element("tbody", "tbody", Table(BodyGroup), None, vec![4, 7]),
Node::element("row-one", "tr", Table(Row), None, vec![5, 6]),
cell("cell-a", LayoutTableData::default()),
cell("cell-b", LayoutTableData::default()),
Node::element("row-two", "tr", Table(Row), None, vec![8]),
cell(
"cell-span",
LayoutTableData {
column_span: 2,
..LayoutTableData::default()
},
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 400.0, 200.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
sized(
LayoutDisplay::Table,
300.0,
70.0,
PaintColor::new(0.7, 0.7, 0.7, 1.0),
),
);
styles
.primary
.insert(2, sized(LayoutDisplay::TableCaption, 300.0, 20.0, YELLOW));
styles.primary.insert(
3,
style(LayoutDisplay::TableRowGroup, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(4, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
styles
.primary
.insert(5, sized(LayoutDisplay::TableCell, 100.0, 30.0, RED));
styles
.primary
.insert(6, sized(LayoutDisplay::TableCell, 200.0, 30.0, GREEN));
styles
.primary
.insert(7, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
styles
.primary
.insert(8, sized(LayoutDisplay::TableCell, 300.0, 40.0, BLUE));
let snapshot = render(&source, &mut styles, 400, 200);
let caption = rect(&snapshot, YELLOW);
let first = rect(&snapshot, RED);
let second = rect(&snapshot, GREEN);
let span = rect(&snapshot, BLUE);
assert_close(caption.width, 300.0);
assert_close(caption.y, 0.0);
assert_close(first.x, 0.0);
assert_close(first.y, 20.0);
assert_close(first.width, 100.0);
assert_close(second.x, 100.0);
assert_close(second.width, 200.0);
assert_close(span.x, 0.0);
assert_close(span.y, 50.0);
assert_close(span.width, 300.0);
}
#[test]
fn zero_row_span_reaches_the_end_of_its_row_group() {
use LayoutElementCategory::Table;
use LayoutTableRole::{BodyGroup, Cell, Row, Table as Root};
let cell = |label, row_span| {
Node::element(label, "td", Table(Cell), None, Vec::new()).with_metadata(
LayoutElementMetadata {
table: Some(LayoutTableData {
row_span,
..LayoutTableData::default()
}),
..LayoutElementMetadata::default()
},
)
};
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element("table", "table", Table(Root), None, vec![2, 8]),
Node::element("first-body", "tbody", Table(BodyGroup), None, vec![3, 6]),
Node::element("first-row", "tr", Table(Row), None, vec![4, 5]),
cell("zero-span", 0),
cell("first-reference", 1),
Node::element("second-row", "tr", Table(Row), None, vec![7]),
cell("second-reference", 1),
Node::element("second-body", "tbody", Table(BodyGroup), None, vec![9]),
Node::element("third-row", "tr", Table(Row), None, vec![10]),
cell("next-group", 1),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
sized(LayoutDisplay::Table, 100.0, 60.0, PaintColor::TRANSPARENT),
);
for id in [2, 8] {
styles.primary.insert(
id,
style(LayoutDisplay::TableRowGroup, PaintColor::TRANSPARENT),
);
}
for id in [3, 6, 9] {
styles
.primary
.insert(id, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
}
styles
.primary
.insert(4, sized(LayoutDisplay::TableCell, 50.0, 20.0, RED));
styles
.primary
.insert(5, sized(LayoutDisplay::TableCell, 50.0, 20.0, GREEN));
styles
.primary
.insert(7, sized(LayoutDisplay::TableCell, 50.0, 20.0, BLUE));
styles
.primary
.insert(10, sized(LayoutDisplay::TableCell, 100.0, 20.0, YELLOW));
let snapshot = render(&source, &mut styles, 200, 100);
let zero_span = rect(&snapshot, RED);
let first_reference = rect(&snapshot, GREEN);
let next_group = rect(&snapshot, YELLOW);
assert_close(zero_span.height, 40.0);
assert_close(first_reference.height, 20.0);
assert_close(next_group.y, 40.0);
}
#[test]
fn separated_table_parts_ignore_authored_border_padding_and_margin() {
use LayoutElementCategory::Table;
use LayoutTableRole::{BodyGroup, Cell, Row, Table as Root};
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element("table", "table", Table(Root), None, vec![2]),
Node::element("body", "tbody", Table(BodyGroup), None, vec![3]),
Node::element("row", "tr", Table(Row), None, vec![4]),
Node::element("cell", "td", Table(Cell), None, Vec::new()).with_metadata(
LayoutElementMetadata {
table: Some(LayoutTableData::default()),
..LayoutElementMetadata::default()
},
),
]);
let table = PaintColor::new(0.11, 0.12, 0.13, 1.0);
let group = PaintColor::new(0.21, 0.22, 0.23, 1.0);
let row = PaintColor::new(0.31, 0.32, 0.33, 1.0);
let cell = PaintColor::new(0.41, 0.42, 0.43, 1.0);
let structural_style = |display, color| {
style(display, color).tap_taffy(|taffy| {
taffy.border = Rect {
left: taffy::LengthPercentage::length(20.0),
right: taffy::LengthPercentage::length(20.0),
top: taffy::LengthPercentage::length(20.0),
bottom: taffy::LengthPercentage::length(20.0),
};
taffy.padding = Rect {
left: taffy::LengthPercentage::length(12.0),
right: taffy::LengthPercentage::length(12.0),
top: taffy::LengthPercentage::length(12.0),
bottom: taffy::LengthPercentage::length(12.0),
};
taffy.margin = Rect {
left: taffy::LengthPercentageAuto::length(9.0),
right: taffy::LengthPercentageAuto::length(9.0),
top: taffy::LengthPercentageAuto::length(9.0),
bottom: taffy::LengthPercentageAuto::length(9.0),
};
})
};
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(1, sized(LayoutDisplay::Table, 50.0, 20.0, table));
styles
.primary
.insert(2, structural_style(LayoutDisplay::TableRowGroup, group));
styles
.primary
.insert(3, structural_style(LayoutDisplay::TableRow, row));
styles
.primary
.insert(4, sized(LayoutDisplay::TableCell, 50.0, 20.0, cell));
let snapshot = render(&source, &mut styles, 200, 100);
for color in [table, group, row, cell] {
assert_eq!(rect(&snapshot, color), PaintRect::new(0.0, 0.0, 50.0, 20.0));
}
assert!(
snapshot
.fragments
.iter()
.all(|fragment| !matches!(fragment, PaintFragment::Border { .. })),
"table structural borders must not reach generic paint: {:?}",
snapshot.fragments
);
}
#[test]
fn table_sections_use_visual_header_body_footer_order() {
use LayoutElementCategory::Table;
use LayoutTableRole::{BodyGroup, Cell, FooterGroup, HeaderGroup, Row, Table as Root};
let cell = |label| {
Node::element(label, "td", Table(Cell), None, Vec::new()).with_metadata(
LayoutElementMetadata {
table: Some(LayoutTableData::default()),
..LayoutElementMetadata::default()
},
)
};
// Deliberately place tbody and tfoot before thead in tree order. CSS table
// layout promotes the first header group before all bodies and the first
// footer group after them.
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element("table", "table", Table(Root), None, vec![2, 6, 10]),
Node::element("body", "tbody", Table(BodyGroup), None, vec![3]),
Node::element("body-row", "tr", Table(Row), None, vec![4, 5]),
cell("body-a"),
cell("body-b"),
Node::element("footer", "tfoot", Table(FooterGroup), None, vec![7]),
Node::element("footer-row", "tr", Table(Row), None, vec![8, 9]),
cell("footer-a"),
cell("footer-b"),
Node::element("header", "thead", Table(HeaderGroup), None, vec![11]),
Node::element("header-row", "tr", Table(Row), None, vec![12, 13]),
cell("header-a"),
cell("header-b"),
]);
let body_a = PaintColor::new(0.11, 0.12, 0.13, 1.0);
let body_b = PaintColor::new(0.21, 0.22, 0.23, 1.0);
let footer_a = PaintColor::new(0.31, 0.32, 0.33, 1.0);
let footer_b = PaintColor::new(0.41, 0.42, 0.43, 1.0);
let header_a = PaintColor::new(0.51, 0.52, 0.53, 1.0);
let header_b = PaintColor::new(0.61, 0.62, 0.63, 1.0);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
sized(LayoutDisplay::Table, 100.0, 30.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
2,
style(LayoutDisplay::TableRowGroup, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(3, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
styles
.primary
.insert(4, sized(LayoutDisplay::TableCell, 20.0, 10.0, body_a));
styles
.primary
.insert(5, sized(LayoutDisplay::TableCell, 80.0, 10.0, body_b));
styles.primary.insert(
6,
style(LayoutDisplay::TableFooterGroup, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(7, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
styles
.primary
.insert(8, sized(LayoutDisplay::TableCell, 50.0, 10.0, footer_a));
styles
.primary
.insert(9, sized(LayoutDisplay::TableCell, 50.0, 10.0, footer_b));
styles.primary.insert(
10,
style(LayoutDisplay::TableHeaderGroup, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(11, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
styles
.primary
.insert(12, sized(LayoutDisplay::TableCell, 75.0, 10.0, header_a));
styles
.primary
.insert(13, sized(LayoutDisplay::TableCell, 25.0, 10.0, header_b));
let snapshot = render(&source, &mut styles, 200, 100);
// Chromium combines all three rows into 75px/80px specified guesses,
// distributes the table's 100px used width to 48.375px/51.625px, then
// snaps the shared paint edge to 48px at device scale 1.
for (color, expected) in [
(header_a, (0.0, 0.0, 48.0)),
(header_b, (48.0, 0.0, 52.0)),
(body_a, (0.0, 10.0, 48.0)),
(body_b, (48.0, 10.0, 52.0)),
(footer_a, (0.0, 20.0, 48.0)),
(footer_b, (48.0, 20.0, 52.0)),
] {
let actual = rect(&snapshot, color);
assert_close(actual.x, expected.0);
assert_close(actual.y, expected.1);
assert_close(actual.width, expected.2);
assert_close(actual.height, 10.0);
}
}
#[test]
fn left_float_restricts_inline_slots_and_clear_moves_the_next_block_below_it() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2],
),
Node::element(
"float",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"atom",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
sized(LayoutDisplay::Block, 60.0, 40.0, BLUE).with_float(Float::Left, Clear::None),
);
styles
.primary
.insert(2, sized(LayoutDisplay::InlineBlock, 100.0, 20.0, RED));
let snapshot = render(&source, &mut styles, 200, 100);
assert_close(rect(&snapshot, BLUE).x, 0.0);
assert_close(rect(&snapshot, RED).x, 60.0);
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2],
),
Node::element(
"float",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"clear",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
sized(LayoutDisplay::Block, 60.0, 40.0, BLUE).with_float(Float::Left, Clear::None),
);
styles.primary.insert(
2,
sized(LayoutDisplay::Block, 100.0, 10.0, GREEN).with_float(Float::None, Clear::Both),
);
let snapshot = render(&source, &mut styles, 200, 100);
assert_close(rect(&snapshot, GREEN).y, 40.0);
}
#[test]
fn atomic_inline_content_moves_to_the_first_float_slot_that_fits() {
// Chromium oracle: a full-width inline button in a normal-flow form
// following a full-width float belongs below that float. Smaller buttons
// can stay alongside it, or move only as far as the first wider segment.
for (left, right, atom_width, expected_x, expected_y) in [
(Some((200.0, 80.0)), None, 200.0, 0.0, 80.0),
(None, Some((200.0, 80.0)), 200.0, 0.0, 80.0),
(Some((120.0, 80.0)), None, 120.0, 0.0, 80.0),
(Some((60.0, 80.0)), None, 100.0, 60.0, 0.0),
(None, Some((60.0, 80.0)), 100.0, 0.0, 0.0),
(Some((120.0, 40.0)), Some((60.0, 80.0)), 80.0, 0.0, 40.0),
(Some((120.0, 40.0)), Some((60.0, 80.0)), 160.0, 0.0, 80.0),
(Some((60.0, 80.0)), Some((120.0, 40.0)), 80.0, 60.0, 40.0),
(Some((60.0, 80.0)), None, 240.0, 0.0, 80.0),
(None, None, 240.0, 0.0, 0.0),
] {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"container",
"div",
LayoutElementCategory::Generic,
None,
vec![2, 3, 4],
),
Node::element("left", "div", LayoutElementCategory::Generic, None, vec![]),
Node::element("right", "div", LayoutElementCategory::Generic, None, vec![]),
Node::element(
"form",
"form",
LayoutElementCategory::Generic,
None,
vec![5, 7, 6],
),
Node::element(
"loading-hint",
"span",
LayoutElementCategory::Generic,
None,
vec![8],
),
Node::element("atom", "span", LayoutElementCategory::Generic, None, vec![]),
Node::text("space", " "),
Node::element(
"screenreader-hint",
"span",
LayoutElementCategory::Generic,
None,
vec![9],
),
Node::text("loading", "Loading"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 400.0, 200.0, PaintColor::TRANSPARENT),
);
for (id, color) in [(1, YELLOW), (4, GREEN)] {
styles.primary.insert(
id,
style(LayoutDisplay::Block, color)
.tap_taffy(|s| s.size.width = Dimension::length(200.0))
.with_text_metrics(16.0, 20.0),
);
}
for (id, size, direction) in [(2, left, Float::Left), (3, right, Float::Right)] {
styles.primary.insert(
id,
size.map_or_else(
|| style(LayoutDisplay::None, PaintColor::TRANSPARENT),
|(width, height)| {
sized(LayoutDisplay::Block, width, height, BLUE)
.with_float(direction, Clear::None)
},
),
);
}
styles
.primary
.insert(5, style(LayoutDisplay::Inline, PaintColor::TRANSPARENT));
styles.primary.insert(
6,
sized(LayoutDisplay::InlineBlock, atom_width, 24.0, RED)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Top),
);
styles.primary.insert(
8,
sized(LayoutDisplay::Block, 1.0, 1.0, PaintColor::TRANSPARENT)
.with_position(LayoutPosition::Absolute),
);
let snapshot = render(&source, &mut styles, 400, 200);
let atom = rect(&snapshot, RED);
assert_close(atom.x, expected_x);
assert_close(atom.y, expected_y);
assert_close(atom.width, atom_width);
assert_close(rect(&snapshot, GREEN).height, expected_y + 24.0);
assert_close(rect(&snapshot, YELLOW).height, expected_y + 24.0);
}
}
#[test]
fn intrinsic_inline_probes_clamp_widths_consumed_by_margins_beside_floats() {
// A grid/flex intrinsic probe can give the paragraph a known width of
// `container_width - 40px`. Parley's float-aware breaker must still get
// a nonnegative paragraph width when the margin consumes all the space.
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"container",
"div",
LayoutElementCategory::Generic,
None,
vec![2, 3],
),
Node::element("float", "div", LayoutElementCategory::Generic, None, vec![]),
Node::element(
"paragraph",
"p",
LayoutElementCategory::Generic,
None,
vec![4],
),
Node::text("text", "text"),
]);
for display in [LayoutDisplay::Grid, LayoutDisplay::Flex] {
for container_width in [0.0, 20.0, 40.0, 100.0] {
let mut styles = Styles::default();
styles
.primary
.insert(0, sized(display, 200.0, 100.0, PaintColor::TRANSPARENT));
styles.primary.insert(
1,
style(LayoutDisplay::Block, PaintColor::TRANSPARENT).tap_taffy(|taffy| {
taffy.size.width = Dimension::length(container_width);
}),
);
styles.primary.insert(
2,
sized(LayoutDisplay::Block, 10.0, 10.0, BLUE).with_float(Float::Left, Clear::None),
);
styles.primary.insert(
3,
style(LayoutDisplay::Block, PaintColor::TRANSPARENT).tap_taffy(|taffy| {
taffy.margin.left = taffy::LengthPercentageAuto::length(40.0);
}),
);
let snapshot = render(&source, &mut styles, 200, 100);
assert_close(rect(&snapshot, BLUE).x, 0.0);
let glyphs = snapshot
.fragments
.iter()
.filter_map(|fragment| match fragment {
PaintFragment::GlyphRun(run) => Some(run.glyphs_in_surface()),
_ => None,
})
.flatten()
.collect::<Vec<_>>();
assert!(!glyphs.is_empty(), "text must survive the intrinsic probe");
assert_close(glyphs[0].x, 40.0);
assert!(
glyphs
.iter()
.all(|glyph| glyph.x.is_finite() && glyph.y.is_finite())
);
}
}
}
#[test]
fn float_descendant_of_structural_inline_rounds_with_its_ifc_owner() {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element("link", "a", LayoutElementCategory::Generic, None, vec![2]),
Node::element(
"float",
"div",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::element(
"image",
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
intrinsic_width: Some(166.0),
intrinsic_height: Some(42.0),
intrinsic_ratio: Some(166.0 / 42.0),
..ReplacedMetrics::default()
}),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 40.0, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(1, style(LayoutDisplay::Inline, PaintColor::TRANSPARENT));
styles.primary.insert(
2,
sized(LayoutDisplay::Block, 60.0, 20.0, BLUE).with_float(Float::Left, Clear::None),
);
styles.primary.insert(
3,
sized(LayoutDisplay::Inline, 55.0, 14.0, GREEN)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Top),
);
let snapshot = render(&source, &mut styles, 200, 40);
let floated = rect(&snapshot, BLUE);
let image = rect(&snapshot, GREEN);
assert_close(floated.x, 0.0);
assert_close(floated.y, 0.0);
assert_close(floated.width, 60.0);
assert_close(floated.height, 20.0);
assert_close(image.x, 0.0);
assert_close(image.y, 0.0);
assert_close(image.width, 55.0);
assert_close(image.height, 14.0);
}
#[test]
fn block_content_alignment_moves_the_inline_float_fragment_group() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 3],
),
Node::element(
"float",
"div",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"float-child",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"atom",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Block, PaintColor::TRANSPARENT).tap_taffy(|taffy| {
taffy.size = Size {
width: Dimension::length(200.0),
height: Dimension::length(100.0),
};
taffy.align_content = Some(taffy::AlignContent::SAFE_CENTER);
}),
);
styles.primary.insert(
1,
sized(LayoutDisplay::Block, 60.0, 20.0, BLUE).with_float(Float::Left, Clear::None),
);
styles
.primary
.insert(2, sized(LayoutDisplay::Block, 10.0, 5.0, RED));
styles.primary.insert(
3,
sized(LayoutDisplay::InlineBlock, 20.0, 10.0, GREEN)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Top),
);
let snapshot = render(&source, &mut styles, 200, 100);
let floated = rect(&snapshot, BLUE);
let floated_child = rect(&snapshot, RED);
let atom = rect(&snapshot, GREEN);
// The float remains outside normal-flow auto height, but Chromium includes
// its 20px block-end extent in the single align-content subject.
assert_close(floated.y, 40.0);
assert_close(floated_child.y, 40.0);
assert_close(atom.y, 40.0);
assert_close(floated_child.y - floated.y, 0.0);
}
#[test]
fn shrink_to_fit_inline_context_accounts_for_left_and_right_float_bands() {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"float-root",
"span",
LayoutElementCategory::Generic,
None,
vec![2, 3, 4],
),
Node::element(
"left",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"right",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"zero-width-atom",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 100.0, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(1, style(LayoutDisplay::InlineBlock, YELLOW));
styles.primary.insert(
2,
style(LayoutDisplay::Block, BLUE)
.tap_taffy(|taffy| {
taffy.size = Size {
width: Dimension::length(50.0),
height: Dimension::length(40.0),
};
taffy.margin.right = taffy::LengthPercentageAuto::length(10.0);
})
.with_float(Float::Left, Clear::None),
);
styles.primary.insert(
3,
style(LayoutDisplay::Block, GREEN)
.tap_taffy(|taffy| {
taffy.size = Size {
width: Dimension::length(40.0),
height: Dimension::length(30.0),
};
taffy.margin.left = taffy::LengthPercentageAuto::length(10.0);
})
.with_float(Float::Right, Clear::None),
);
styles.primary.insert(
4,
sized(
LayoutDisplay::InlineBlock,
0.0,
0.0,
PaintColor::TRANSPARENT,
),
);
let snapshot = render(&source, &mut styles, 300, 100);
let float_root = rect(&snapshot, YELLOW);
assert_close(float_root.width, 110.0);
assert_close(float_root.height, 40.0);
assert_close(rect(&snapshot, BLUE).x, 0.0);
assert_close(rect(&snapshot, GREEN).x, 70.0);
}
#[test]
fn html_list_metadata_and_marker_style_produce_inside_and_outside_glyph_geometry() {
let source = Source(vec![
Node::element(
"list",
"ol",
LayoutElementCategory::List(LayoutListRole::Container),
None,
vec![1, 3, 5],
)
.with_metadata(LayoutElementMetadata {
list: Some(LayoutListData {
ordered: true,
start: Some(3),
reversed: false,
value: None,
}),
..LayoutElementMetadata::default()
}),
Node::element(
"first",
"li",
LayoutElementCategory::List(LayoutListRole::Item),
None,
vec![2],
),
Node::text("first-text", "first"),
Node::element(
"second",
"li",
LayoutElementCategory::List(LayoutListRole::Item),
None,
vec![4],
)
.with_metadata(LayoutElementMetadata {
list: Some(LayoutListData {
value: Some(9),
..LayoutListData::default()
}),
..LayoutElementMetadata::default()
}),
Node::text("second-text", "second"),
Node::element(
"custom",
"li",
LayoutElementCategory::List(LayoutListRole::Item),
None,
vec![6],
),
Node::text("custom-text", "custom"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 240.0, 100.0, PaintColor::TRANSPARENT),
);
for (item, color) in [(1, RED), (3, GREEN)] {
styles.primary.insert(
item,
style(LayoutDisplay::BlockListItem, color).with_list_marker(
LayoutListMarkerType::Decimal,
LayoutListMarkerPosition::Outside,
),
);
styles.pseudo.insert(
(item, LayoutPseudo::Marker),
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_normal_generated_content(),
);
}
styles.primary.insert(
5,
style(LayoutDisplay::BlockListItem, BLUE).with_list_marker(
LayoutListMarkerType::None,
LayoutListMarkerPosition::Outside,
),
);
styles.pseudo.insert(
(5, LayoutPseudo::Marker),
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_generated_text("X "),
);
let snapshot = render(&source, &mut styles, 240, 100);
let glyph_count = snapshot
.fragments
.iter()
.filter(|fragment| matches!(fragment, PaintFragment::GlyphRun(_)))
.count();
assert!(
glyph_count >= 6,
"expected item and marker glyph runs: {:?}",
snapshot.fragments
);
let first = rect(&snapshot, RED);
let second = rect(&snapshot, GREEN);
let custom = rect(&snapshot, BLUE);
assert_close(second.y, first.y + first.height);
assert_close(custom.y, second.y + second.height);
assert!(
snapshot.fragments.iter().any(|fragment| {
matches!(fragment, PaintFragment::GlyphRun(run) if run.glyphs_in_surface().iter().any(|glyph| glyph.x < first.x))
}),
"outside markers must own geometry in the marker gutter: {:?}",
snapshot.fragments
);
assert!(
snapshot
.diagnostics
.iter()
.all(|diagnostic| diagnostic.code != "list-marker-layout-deferred")
);
}
#[test]
fn degenerate_css_ratio_falls_back_to_the_replaced_intrinsic_ratio() {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"image",
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
intrinsic_width: Some(80.0),
intrinsic_height: Some(40.0),
attribute_width: None,
attribute_height: None,
intrinsic_ratio: Some(2.0),
}),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 200.0, PaintColor::TRANSPARENT),
);
let image_style = ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::length(120.0),
height: Dimension::auto(),
},
aspect_ratio: Some(0.0),
..Style::default()
},
BLUE,
);
styles.primary.insert(1, image_style);
let snapshot = render(&source, &mut styles, 300, 200);
let image = rect(&snapshot, BLUE);
assert_close(image.width, 120.0);
assert_close(image.height, 60.0);
assert!(
snapshot
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "replaced-content-placeholder")
);
}
/// Regression for
/// <https://wpt.live/css/css-sizing/image-fractional-height-with-wide-aspect-ratio.html>.
#[test]
fn fractional_replaced_images_project_contiguous_pre_transform_destinations() {
let svg = Arc::new(
moli_image::decode_svg_image(
br##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 12"><rect width="96" height="12" fill="green"/></svg>"##,
)
.expect("fixture SVG should parse"),
);
let image = LayoutImageResource {
intrinsic_width: 96.0,
intrinsic_height: 12.0,
pixels: None,
svg: Some(svg),
};
let labels = [
"row-1", "row-2", "row-3", "row-4", "row-5", "row-6", "row-7", "row-8",
];
let mut nodes = vec![Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
(1..=8).collect(),
)];
nodes.extend(labels.into_iter().map(|label| {
Node::element(
label,
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
intrinsic_width: Some(96.0),
intrinsic_height: Some(12.0),
intrinsic_ratio: Some(8.0),
..ReplacedMetrics::default()
})
.with_image(image.clone())
}));
let source = Source(nodes);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 100.0, 100.0, PaintColor::TRANSPARENT),
);
for node in 1..=8 {
styles.primary.insert(
node,
sized(LayoutDisplay::Block, 100.0, 12.5, PaintColor::TRANSPARENT),
);
}
let snapshot = render(&source, &mut styles, 100, 101);
let images = snapshot
.fragments
.iter()
.filter_map(|fragment| match fragment {
PaintFragment::SvgImage(image) => Some(image),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(images.len(), 8);
for (row, image) in images.into_iter().enumerate() {
assert_eq!(
image.destination,
PaintRect::new(0.0, row as f32 * 12.5, 100.0, 12.5)
);
assert_eq!(image.transform, PaintTransform2D::IDENTITY);
}
}
#[test]
fn replaced_attributes_canvas_defaults_and_image_button_share_resource_free_sizing() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2, 3],
),
Node::element(
"image",
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
attribute_width: Some(80.0),
attribute_height: Some(40.0),
..ReplacedMetrics::default()
}),
Node::element(
"canvas",
"canvas",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Canvas),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
attribute_width: Some(600.0),
..ReplacedMetrics::default()
}),
Node::element(
"image-button",
"input",
LayoutElementCategory::FormControl(LayoutFormControlKind::Input(
LayoutInputControlKind::Image,
)),
Some(LayoutReplacedKind::FormControl),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
attribute_width: Some(90.0),
attribute_height: Some(45.0),
..ReplacedMetrics::default()
}),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 700.0, 400.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::length(120.0),
height: Dimension::auto(),
},
..Style::default()
},
RED,
),
);
styles.primary.insert(2, style(LayoutDisplay::Block, GREEN));
styles.primary.insert(3, style(LayoutDisplay::Block, BLUE));
let snapshot = render(&source, &mut styles, 700, 400);
assert_eq!(rect(&snapshot, RED), PaintRect::new(0.0, 0.0, 120.0, 60.0));
assert_eq!(
rect(&snapshot, GREEN),
PaintRect::new(0.0, 60.0, 600.0, 150.0)
);
assert_eq!(
rect(&snapshot, BLUE),
PaintRect::new(0.0, 210.0, 90.0, 45.0)
);
assert_eq!(
snapshot
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "replaced-content-placeholder")
.count(),
2
);
assert_eq!(
snapshot
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "canvas-content-unavailable")
.count(),
1
);
}
#[test]
fn unavailable_images_use_zero_default_size_and_a_content_box_outline() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2, 3, 4],
),
Node::element(
"canvas",
"canvas",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Canvas),
Vec::new(),
)
.with_metrics(ReplacedMetrics::default()),
Node::element(
"sized-image",
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
)
.with_metrics(ReplacedMetrics {
attribute_width: Some(80.0),
attribute_height: Some(40.0),
..ReplacedMetrics::default()
}),
Node::element(
"unsized-image",
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
)
.with_metrics(ReplacedMetrics::default()),
Node::element(
"following-block",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 300.0, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(1, style(LayoutDisplay::Block, PaintColor::TRANSPARENT));
styles
.primary
.insert(2, style(LayoutDisplay::Block, PaintColor::TRANSPARENT));
styles
.primary
.insert(3, style(LayoutDisplay::Block, PaintColor::TRANSPARENT));
styles
.primary
.insert(4, sized(LayoutDisplay::Block, 50.0, 10.0, YELLOW));
let snapshot = render(&source, &mut styles, 300, 300);
let old_placeholder = PaintColor::new(0.82, 0.84, 0.87, 1.0);
assert!(
snapshot
.fragments
.iter()
.filter_map(PaintFragment::solid_fill_in_surface)
.all(|(_, color)| color != old_placeholder)
);
assert_eq!(
rect(&snapshot, YELLOW),
PaintRect::new(0.0, 190.0, 50.0, 10.0)
);
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,
widths,
colors,
transform,
..
} if rect.width == 80.0
&& rect.height == 40.0
&& *widths == moli_layout::PaintEdgeSizes::new(1.0, 1.0, 1.0, 1.0)
&& colors.top == light_gray
&& colors.right == light_gray
&& colors.bottom == light_gray
&& colors.left == light_gray
&& transform.map_point(moli_layout::LayoutPoint::new(rect.x, rect.y))
== moli_layout::LayoutPoint::new(0.0, 150.0)
)
}),
"missing Chromium-style unavailable image outline: {:?}",
snapshot.fragments
);
assert_eq!(
snapshot
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "canvas-content-unavailable")
.count(),
1
);
assert_eq!(
snapshot
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.code == "replaced-content-placeholder")
.count(),
2
);
}
#[test]
fn inline_blocks_use_their_internal_last_line_baseline_and_overflow_fallback() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 3, 5],
),
Node::element("news", "a", LayoutElementCategory::Generic, None, vec![2]),
Node::text("news-text", "News"),
Node::element("hao", "a", LayoutElementCategory::Generic, None, vec![4]),
Node::text("hao-text", "hao123"),
Node::element("more", "div", LayoutElementCategory::Generic, None, vec![6]),
Node::element(
"more-link",
"a",
LayoutElementCategory::Generic,
None,
vec![7],
),
Node::text("more-text", "More"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 100.0, PaintColor::TRANSPARENT)
.with_text_metrics(12.0, 19.2),
);
let nav_item_style = |color, padding_bottom, overflow| {
style(LayoutDisplay::InlineBlock, color)
.tap_taffy(|taffy| {
taffy.margin = Rect {
left: taffy::LengthPercentageAuto::length(0.0),
right: taffy::LengthPercentageAuto::length(24.0),
top: taffy::LengthPercentageAuto::length(19.0),
bottom: taffy::LengthPercentageAuto::length(0.0),
};
taffy.padding.bottom = taffy::LengthPercentage::length(padding_bottom);
taffy.overflow = Point {
x: overflow,
y: overflow,
};
})
.with_text_metrics(13.0, 23.0)
};
for (node, color, padding_bottom) in [(1, RED, 0.0), (3, GREEN, 0.0), (5, BLUE, 19.0)] {
styles.primary.insert(
node,
nav_item_style(color, padding_bottom, Overflow::Visible),
);
}
styles.primary.insert(
6,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(13.0, 23.0),
);
let snapshot = render(&source, &mut styles, 300, 100);
let news = rect(&snapshot, RED);
let hao = rect(&snapshot, GREEN);
let more = rect(&snapshot, BLUE);
assert_close(news.y, 19.0);
assert_close(hao.y, news.y);
assert_close(more.y, news.y);
assert_close(news.height, 23.0);
assert_close(hao.height, 23.0);
assert_close(more.height, 42.0);
styles
.primary
.insert(5, nav_item_style(BLUE, 19.0, Overflow::Hidden));
let fallback = render(&source, &mut styles, 300, 100);
let more = rect(&fallback, BLUE);
assert_close(more.y, 19.0);
assert_close(more.height, 42.0);
assert_close(rect(&fallback, RED).y, rect(&fallback, GREEN).y);
// A clipped inline-block uses its bottom margin edge as the baseline.
// Check the adjacent text's actual baselines: its box top depends on the
// platform font's ascent, even with an explicit font size and line height.
let baselines = fallback
.fragments
.iter()
.filter_map(|fragment| match fragment {
PaintFragment::GlyphRun(run) => run.glyphs_in_surface().first().map(|glyph| glyph.y),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
baselines.len(),
3,
"each nav item should paint one text run"
);
assert_close(baselines[0], more.y + more.height);
assert_close(baselines[1], more.y + more.height);
}
#[test]
fn inline_flex_and_grid_use_their_first_container_baseline() {
for atomic_display in [LayoutDisplay::InlineFlex, LayoutDisplay::InlineGrid] {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 4],
),
Node::element(
"atomic",
"span",
LayoutElementCategory::Generic,
None,
vec![2, 3],
),
Node::element(
"first-item",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"last-item",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"tail",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Block, PaintColor::TRANSPARENT)
.tap_taffy(|taffy| taffy.size.width = Dimension::length(100.0))
.with_text_metrics(0.0, 0.0),
);
styles.primary.insert(
1,
style(atomic_display, YELLOW).tap_taffy(|taffy| {
taffy.size.width = Dimension::length(10.0);
if atomic_display == LayoutDisplay::InlineFlex {
taffy.flex_direction = FlexDirection::Column;
} else {
taffy.grid_template_columns = vec![taffy::style_helpers::length(10.0)];
}
}),
);
styles
.primary
.insert(2, sized(LayoutDisplay::Block, 10.0, 10.0, GREEN));
styles
.primary
.insert(3, sized(LayoutDisplay::Block, 10.0, 10.0, BLUE));
styles
.primary
.insert(4, sized(LayoutDisplay::InlineBlock, 10.0, 10.0, RED));
let snapshot = render(&source, &mut styles, 100, 60);
let atomic = rect(&snapshot, YELLOW);
let tail = rect(&snapshot, RED);
assert_close(atomic.width, 10.0);
assert_close(atomic.height, 20.0);
assert_close(tail.y, atomic.y);
}
}
#[test]
fn negative_atomic_block_margin_uses_signed_baseline_metrics() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 4],
),
Node::element(
"atomic",
"span",
LayoutElementCategory::Generic,
None,
vec![2, 3],
),
Node::element(
"first-item",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"last-item",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"tail",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Block, GREEN)
.tap_taffy(|taffy| taffy.size.width = Dimension::length(100.0))
.with_text_metrics(0.0, 0.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::InlineFlex, YELLOW).tap_taffy(|taffy| {
taffy.size.width = Dimension::length(10.0);
taffy.flex_direction = FlexDirection::Column;
taffy.margin.top = taffy::LengthPercentageAuto::length(-5.0);
}),
);
styles
.primary
.insert(2, sized(LayoutDisplay::Block, 10.0, 10.0, BLUE));
styles.primary.insert(
3,
sized(LayoutDisplay::Block, 10.0, 10.0, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(4, sized(LayoutDisplay::InlineBlock, 10.0, 10.0, RED));
let snapshot = render(&source, &mut styles, 100, 60);
let root = rect(&snapshot, GREEN);
let atomic = rect(&snapshot, YELLOW);
let tail = rect(&snapshot, RED);
assert_close(root.height, 20.0);
assert_close(atomic.height, 20.0);
assert_close(atomic.y, tail.y);
}
#[test]
fn inline_table_baseline_includes_top_caption_offset() {
use LayoutElementCategory::Table;
use LayoutTableRole::{BodyGroup, Caption, Cell, Row, Table as Root};
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 6],
),
Node::element("table", "table", Table(Root), None, vec![2, 3]),
Node::element("caption", "caption", Table(Caption), None, Vec::new()),
Node::element("tbody", "tbody", Table(BodyGroup), None, vec![4]),
Node::element("row", "tr", Table(Row), None, vec![5]),
Node::element("cell", "td", Table(Cell), None, Vec::new()),
Node::element(
"tail",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Block, PaintColor::TRANSPARENT)
.tap_taffy(|taffy| taffy.size.width = Dimension::length(100.0))
.with_text_metrics(0.0, 0.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::InlineTable, YELLOW)
.tap_taffy(|taffy| taffy.size.width = Dimension::length(10.0)),
);
styles
.primary
.insert(2, sized(LayoutDisplay::TableCaption, 10.0, 10.0, GREEN));
styles.primary.insert(
3,
style(LayoutDisplay::TableRowGroup, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(4, style(LayoutDisplay::TableRow, PaintColor::TRANSPARENT));
styles
.primary
.insert(5, sized(LayoutDisplay::TableCell, 10.0, 10.0, BLUE));
styles
.primary
.insert(6, sized(LayoutDisplay::InlineBlock, 10.0, 10.0, RED));
let snapshot = render(&source, &mut styles, 100, 60);
let table = rect(&snapshot, YELLOW);
let cell = rect(&snapshot, BLUE);
let tail = rect(&snapshot, RED);
assert_close(table.height, 20.0);
assert_close(cell.y, table.y + 10.0);
assert_close(tail.y, table.y + 10.0);
}
#[test]
fn inline_block_propagates_scroll_block_end_baseline_through_block_children() {
let render_variant = |inner_display, inner_overflow, margin_bottom| {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 4],
),
Node::element(
"outer",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"inner",
"div",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::text("inner-text", "X"),
Node::element(
"tail",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Block, PaintColor::TRANSPARENT)
.tap_taffy(|taffy| {
taffy.size.width = Dimension::length(200.0);
})
.with_text_metrics(16.0, 20.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::InlineBlock, YELLOW)
.tap_taffy(|taffy| {
taffy.box_sizing = BoxSizing::ContentBox;
if inner_display == LayoutDisplay::InlineBlock {
taffy.size.height = Dimension::length(30.0 + margin_bottom);
}
taffy.padding.bottom = taffy::LengthPercentage::length(20.0);
})
.with_text_metrics(16.0, 20.0),
);
styles.primary.insert(
2,
style(inner_display, BLUE)
.tap_taffy(|taffy| {
taffy.size = Size {
width: Dimension::length(30.0),
height: Dimension::length(30.0),
};
taffy.margin.bottom = taffy::LengthPercentageAuto::length(margin_bottom);
taffy.overflow = Point {
x: inner_overflow,
y: inner_overflow,
};
})
.with_text_metrics(16.0, 20.0),
);
styles
.primary
.insert(4, sized(LayoutDisplay::InlineBlock, 10.0, 10.0, RED));
render(&source, &mut styles, 200, 120)
};
for margin_bottom in [0.0, 30.0] {
let test = render_variant(LayoutDisplay::Block, Overflow::Hidden, margin_bottom);
let reference = render_variant(LayoutDisplay::InlineBlock, Overflow::Hidden, margin_bottom);
assert_eq!(rect(&test, YELLOW), rect(&reference, YELLOW));
assert_eq!(rect(&test, BLUE), rect(&reference, BLUE));
assert_eq!(rect(&test, RED), rect(&reference, RED));
}
}
#[test]
fn phantom_inline_line_does_not_supply_an_inline_block_baseline() {
let source = Source(vec![
Node::element(
"wrapper",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2],
),
Node::element(
"empty-atomic",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"block-atomic",
"div",
LayoutElementCategory::Generic,
None,
vec![3, 4],
),
Node::element(
"empty-inline",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"green-block",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Block, RED)
.tap_taffy(|taffy| {
taffy.size.width = Dimension::length(200.0);
})
.with_text_metrics(0.0, 0.0),
);
styles
.primary
.insert(1, sized(LayoutDisplay::InlineBlock, 100.0, 200.0, GREEN));
styles.primary.insert(
2,
style(LayoutDisplay::InlineBlock, PaintColor::TRANSPARENT).with_text_metrics(0.0, 0.0),
);
styles.primary.insert(
3,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(0.0, 0.0),
);
styles
.primary
.insert(4, sized(LayoutDisplay::Block, 100.0, 200.0, GREEN));
let snapshot = render(&source, &mut styles, 240, 240);
let mut green_rects = snapshot
.fragments
.iter()
.filter_map(PaintFragment::solid_fill_in_surface)
.filter_map(|(rect, color)| (color == GREEN).then_some(rect))
.collect::<Vec<_>>();
green_rects.sort_by(|left, right| left.x.total_cmp(&right.x));
assert_eq!(
green_rects,
vec![
PaintRect::new(0.0, 0.0, 100.0, 200.0),
PaintRect::new(100.0, 0.0, 100.0, 200.0),
]
);
}
#[test]
fn top_and_middle_inline_blocks_align_against_the_parent_line_strut() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 3],
),
Node::element(
"settings",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::text("settings-text", "Settings"),
Node::element("login", "a", LayoutElementCategory::Generic, None, vec![4]),
Node::text("login-text", "Login"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 60.0, PaintColor::TRANSPARENT)
.with_text_metrics(12.0, 14.4),
);
styles.primary.insert(
1,
style(LayoutDisplay::InlineBlock, RED)
.tap_taffy(|taffy| {
taffy.margin.top = taffy::LengthPercentageAuto::length(19.0);
})
.with_text_metrics(13.0, 23.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Top),
);
styles.primary.insert(
3,
style(LayoutDisplay::InlineBlock, BLUE)
.tap_taffy(|taffy| {
taffy.size = Size {
width: Dimension::length(48.0),
height: Dimension::length(24.0),
};
taffy.margin.top = taffy::LengthPercentageAuto::length(18.0);
})
.with_text_metrics(13.0, 24.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Middle),
);
let snapshot = render(&source, &mut styles, 300, 60);
let settings = rect(&snapshot, RED);
let login = rect(&snapshot, BLUE);
assert_close(settings.y, 19.0);
assert_close(login.y, 18.0);
assert_close(settings.y + settings.height, 42.0);
assert_close(login.y + login.height, 42.0);
}
#[test]
fn middle_inline_block_uses_its_nearest_inline_parents_x_height() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 3],
),
Node::element(
"large-parent",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"large-icon",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"small-parent",
"span",
LayoutElementCategory::Generic,
None,
vec![4],
),
Node::element(
"small-icon",
"span",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 80.0, PaintColor::TRANSPARENT)
.with_text_metrics(10.0, 60.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(40.0, 60.0),
);
styles.primary.insert(
2,
sized(LayoutDisplay::InlineBlock, 18.0, 18.0, RED)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Middle),
);
styles.primary.insert(
3,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(10.0, 60.0),
);
styles.primary.insert(
4,
sized(LayoutDisplay::InlineBlock, 18.0, 18.0, BLUE)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Middle),
);
let snapshot = render(&source, &mut styles, 300, 80);
let large_parent_icon = rect(&snapshot, RED);
let small_parent_icon = rect(&snapshot, BLUE);
assert!(
large_parent_icon.y + 5.0 < small_parent_icon.y,
"a larger nearest-parent x-height must raise the middle-aligned icon: large={large_parent_icon:?}, small={small_parent_icon:?}"
);
}
#[test]
fn middle_aligned_text_uses_its_nearest_inline_parents_x_height() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 4],
),
Node::element(
"large-parent",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"large-middle",
"span",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::text("large-text", "A"),
Node::element(
"small-parent",
"span",
LayoutElementCategory::Generic,
None,
vec![5],
),
Node::element(
"small-middle",
"span",
LayoutElementCategory::Generic,
None,
vec![6],
),
Node::text("small-text", "A"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 80.0, PaintColor::TRANSPARENT)
.with_text_metrics(10.0, 60.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(40.0, 60.0),
);
styles.primary.insert(
2,
style(LayoutDisplay::Inline, RED)
.with_text_metrics(10.0, 20.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Middle),
);
styles.primary.insert(
4,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(10.0, 60.0),
);
styles.primary.insert(
5,
style(LayoutDisplay::Inline, BLUE)
.with_text_metrics(10.0, 20.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Middle),
);
let snapshot = render(&source, &mut styles, 300, 80);
let large_parent_text = rect(&snapshot, RED);
let small_parent_text = rect(&snapshot, BLUE);
assert!(
large_parent_text.y + 5.0 < small_parent_text.y,
"a larger nearest-parent x-height must raise middle-aligned text: large={large_parent_text:?}, small={small_parent_text:?}"
);
}
#[test]
fn structural_inline_own_strut_contributes_without_direct_text() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 5],
),
Node::element("line", "div", LayoutElementCategory::Generic, None, vec![2]),
Node::element(
"large-structural-inline",
"span",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::element(
"small-child",
"span",
LayoutElementCategory::Generic,
None,
vec![4],
),
Node::text("text", "A"),
Node::element(
"following-block",
"div",
LayoutElementCategory::Generic,
None,
vec![],
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
style(LayoutDisplay::Block, BLUE).with_text_metrics(10.0, 10.0),
);
styles.primary.insert(
2,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(40.0, 60.0),
);
styles.primary.insert(
3,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(10.0, 10.0),
);
styles
.primary
.insert(5, sized(LayoutDisplay::Block, 10.0, 5.0, RED));
let snapshot = render(&source, &mut styles, 200, 100);
let line = rect(&snapshot, BLUE);
let following = rect(&snapshot, RED);
assert!(
line.height >= 59.0,
"a structural inline's own 60px strut must size the line even when all direct text is in a 10px child: {line:?}"
);
assert!(
following.y >= line.y + 59.0,
"following flow content must start after the structural strut: line={line:?}, following={following:?}"
);
}
#[test]
fn inline_fragment_uses_its_own_font_box_instead_of_descendant_union() {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"small-parent",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"large-child",
"span",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::text("text", "A"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT)
.with_text_metrics(10.0, 60.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::Inline, RED).with_text_metrics(10.0, 10.0),
);
styles.primary.insert(
2,
style(LayoutDisplay::Inline, BLUE).with_text_metrics(40.0, 40.0),
);
let snapshot = render(&source, &mut styles, 200, 100);
let parent = rect(&snapshot, RED);
let child = rect(&snapshot, BLUE);
assert!(
parent.height + 10.0 < child.height,
"an inline fragment's block size comes from its own primary font, not the union of a larger descendant: parent={parent:?}, child={child:?}"
);
}
#[test]
fn empty_decorated_inline_fragment_does_not_fill_the_line_height() {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"empty-inline",
"span",
LayoutElementCategory::Generic,
None,
vec![],
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT)
.with_text_metrics(10.0, 60.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::Inline, RED)
.tap_taffy(|taffy| {
taffy.padding.left = taffy::LengthPercentage::length(4.0);
taffy.padding.right = taffy::LengthPercentage::length(4.0);
})
.with_text_metrics(10.0, 10.0),
);
let snapshot = render(&source, &mut styles, 200, 100);
let empty = rect(&snapshot, RED);
assert!(empty.width >= 7.9, "missing inline-axis padding: {empty:?}");
assert!(
empty.height < 30.0,
"an empty decorated inline uses its own font box rather than the root's 60px line box: {empty:?}"
);
}
#[test]
fn nested_inline_alignments_move_each_structural_subtree_in_order() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 4],
),
Node::element(
"middle-outer",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"text-top-inner",
"span",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::text("middle-text", "A"),
Node::element(
"baseline-outer",
"span",
LayoutElementCategory::Generic,
None,
vec![5],
),
Node::element(
"reference-text-top-inner",
"span",
LayoutElementCategory::Generic,
None,
vec![6],
),
Node::text("reference-text", "A"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 100.0, PaintColor::TRANSPARENT)
.with_text_metrics(10.0, 20.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT)
.with_text_metrics(40.0, 50.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Middle),
);
styles.primary.insert(
2,
style(LayoutDisplay::Inline, RED)
.with_text_metrics(10.0, 10.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::TextTop),
);
styles.primary.insert(
4,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT).with_text_metrics(40.0, 50.0),
);
styles.primary.insert(
5,
style(LayoutDisplay::Inline, BLUE)
.with_text_metrics(10.0, 10.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::TextTop),
);
let snapshot = render(&source, &mut styles, 300, 100);
let nested = rect(&snapshot, RED);
let reference = rect(&snapshot, BLUE);
assert!(
(nested.y - reference.y).abs() > 2.0,
"the outer middle alignment must move the already text-top-aligned inner subtree: nested={nested:?}, reference={reference:?}"
);
}
#[test]
fn nested_bottom_alignment_targets_the_nearest_top_or_bottom_subtree() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 4],
),
Node::element(
"top-outer",
"span",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"nested-bottom",
"span",
LayoutElementCategory::Generic,
None,
vec![3],
),
Node::text("nested-text", "A"),
Node::element(
"root-bottom",
"span",
LayoutElementCategory::Generic,
None,
vec![5],
),
Node::text("root-text", "A"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 120.0, PaintColor::TRANSPARENT)
.with_text_metrics(10.0, 100.0),
);
styles.primary.insert(
1,
style(LayoutDisplay::Inline, PaintColor::TRANSPARENT)
.with_text_metrics(20.0, 40.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Top),
);
styles.primary.insert(
2,
style(LayoutDisplay::Inline, RED)
.with_text_metrics(10.0, 10.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Bottom),
);
styles.primary.insert(
4,
style(LayoutDisplay::Inline, BLUE)
.with_text_metrics(10.0, 10.0)
.with_inline_alignment(moli_layout::LayoutInlineAlignment::Bottom),
);
let snapshot = render(&source, &mut styles, 300, 120);
let nested = rect(&snapshot, RED);
let root_aligned = rect(&snapshot, BLUE);
assert!(
nested.y + 30.0 < root_aligned.y,
"the nested bottom must align to its 40px top-aligned ancestor, while the direct bottom aligns to the 100px root line: nested={nested:?}, root={root_aligned:?}"
);
}
#[test]
fn replaced_box_model_is_applied_exactly_once() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2],
),
Node::element(
"image",
"img",
LayoutElementCategory::Generic,
Some(LayoutReplacedKind::Image),
Vec::new(),
),
Node::element(
"following",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 200.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
box_sizing: taffy::BoxSizing::ContentBox,
size: Size {
width: Dimension::length(80.0),
height: Dimension::length(40.0),
},
padding: Rect {
left: taffy::LengthPercentage::length(5.0),
right: taffy::LengthPercentage::length(7.0),
top: taffy::LengthPercentage::length(11.0),
bottom: taffy::LengthPercentage::length(13.0),
},
border: Rect {
left: taffy::LengthPercentage::length(2.0),
right: taffy::LengthPercentage::length(2.0),
top: taffy::LengthPercentage::length(2.0),
bottom: taffy::LengthPercentage::length(2.0),
},
..Style::default()
},
BLUE,
),
);
styles
.primary
.insert(2, sized(LayoutDisplay::Block, 10.0, 10.0, GREEN));
let snapshot = render(&source, &mut styles, 300, 200);
assert_eq!(rect(&snapshot, BLUE), PaintRect::new(0.0, 0.0, 96.0, 68.0));
assert_eq!(
rect(&snapshot, GREEN),
PaintRect::new(0.0, 68.0, 10.0, 10.0)
);
}
#[test]
fn single_row_textarea_does_not_expand_its_flex_search_bar() {
let source = Source(vec![
Node::element(
"search",
"div",
LayoutElementCategory::Generic,
None,
vec![1],
),
Node::element(
"query",
"textarea",
LayoutElementCategory::FormControl(LayoutFormControlKind::TextArea),
Some(LayoutReplacedKind::FormControl),
Vec::new(),
)
.with_metadata(LayoutElementMetadata {
form_control: Some(LayoutFormControlData {
rows: 1,
..LayoutFormControlData::default()
}),
..LayoutElementMetadata::default()
}),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
style(LayoutDisplay::Flex, PaintColor::TRANSPARENT).tap_taffy(|taffy| {
taffy.display = taffy::Display::Flex;
taffy.size.width = Dimension::length(300.0);
taffy.min_size.height = Dimension::length(50.0);
}),
);
styles.primary.insert(
1,
style(LayoutDisplay::Flex, YELLOW)
.tap_taffy(|taffy| {
taffy.display = taffy::Display::Flex;
taffy.flex_grow = 1.0;
taffy.overflow = Point {
x: Overflow::Hidden,
y: Overflow::Scroll,
};
taffy.padding.top = taffy::LengthPercentage::length(14.0);
taffy.border.bottom = taffy::LengthPercentage::length(8.0);
})
.with_text_metrics(16.0, 22.0),
);
let snapshot = render(&source, &mut styles, 300, 100);
assert_eq!(
rect(&snapshot, YELLOW),
PaintRect::new(0.0, 0.0, 300.0, 50.0)
);
}
#[test]
fn form_controls_have_deterministic_intrinsic_geometry_content_and_basic_appearance() {
let form_data = LayoutFormControlData {
placeholder: "Search".into(),
size: Some(4),
..LayoutFormControlData::default()
};
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 2, 3],
),
Node::element(
"text",
"input",
LayoutElementCategory::FormControl(LayoutFormControlKind::Input(
LayoutInputControlKind::Text,
)),
Some(LayoutReplacedKind::FormControl),
Vec::new(),
)
.with_metadata(LayoutElementMetadata {
form_control: Some(form_data),
..LayoutElementMetadata::default()
}),
Node::element(
"checked",
"input",
LayoutElementCategory::FormControl(LayoutFormControlKind::Input(
LayoutInputControlKind::Checkbox,
)),
Some(LayoutReplacedKind::FormControl),
Vec::new(),
)
.with_metadata(LayoutElementMetadata {
form_control: Some(LayoutFormControlData {
checked: true,
..LayoutFormControlData::default()
}),
..LayoutElementMetadata::default()
}),
Node::element(
"button",
"button",
LayoutElementCategory::FormControl(LayoutFormControlKind::Button),
None,
vec![4],
),
Node::text("button-text", "Button"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
style(LayoutDisplay::Block, YELLOW).with_text_metrics(20.0, 20.0),
);
styles
.primary
.insert(2, style(LayoutDisplay::Block, PaintColor::WHITE));
styles
.primary
.insert(3, style(LayoutDisplay::InlineBlock, GREEN));
let snapshot = render(&source, &mut styles, 300, 100);
let input = rect(&snapshot, YELLOW);
assert_close(input.width, 48.0);
assert_close(input.height, 20.0);
let button = rect(&snapshot, GREEN);
assert!(
button.width > 20.0 && button.width < 100.0,
"button must shrink to its real DOM content instead of stretching: {button:?}"
);
assert!(
snapshot
.fragments
.iter()
.any(|fragment| matches!(fragment, PaintFragment::GlyphRun(_)))
);
assert!(snapshot.fragments.iter().any(|fragment| {
matches!(
fragment,
PaintFragment::Stroke(stroke)
if stroke.color == PaintColor::WHITE
&& stroke.width > 0.0
&& stroke.start_cap == moli_layout::PaintLineCap::Round
&& stroke.end_cap == moli_layout::PaintLineCap::Round
)
}));
assert!(
snapshot
.diagnostics
.iter()
.all(|diagnostic| diagnostic.code != "replaced-content-placeholder")
);
}
#[test]
fn flow_button_centers_real_dom_content_in_its_content_box() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 3],
),
Node::element(
"button",
"button",
LayoutElementCategory::FormControl(LayoutFormControlKind::Button),
None,
vec![2],
),
Node::text("button-text", "B"),
Node::element(
"tight-button",
"button",
LayoutElementCategory::FormControl(LayoutFormControlKind::Button),
None,
vec![4],
),
Node::text("tight-button-text", "B"),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 100.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
sized(LayoutDisplay::Block, 108.0, 44.0, GREEN).with_text_metrics(20.0, 20.0),
);
styles.primary.insert(
3,
sized(LayoutDisplay::Block, 108.0, 20.0, BLUE).with_text_metrics(20.0, 20.0),
);
let snapshot = render(&source, &mut styles, 300, 100);
let centered = rect(&snapshot, GREEN);
let tight = rect(&snapshot, BLUE);
assert_eq!(centered, PaintRect::new(0.0, 0.0, 108.0, 44.0));
assert_eq!(tight, PaintRect::new(0.0, 44.0, 108.0, 20.0));
let mut glyphs = snapshot
.fragments
.iter()
.filter_map(|fragment| match fragment {
PaintFragment::GlyphRun(run) => Some(run.glyphs_in_surface()),
_ => None,
})
.flatten()
.collect::<Vec<_>>();
glyphs.sort_by(|left, right| left.y.total_cmp(&right.y));
assert_eq!(
glyphs.len(),
2,
"button fixtures must each produce one glyph"
);
let centered_local_baseline = glyphs[0].y - centered.y;
let tight_local_baseline = glyphs[1].y - tight.y;
assert_close(centered_local_baseline - tight_local_baseline, 12.0);
}
#[test]
fn flow_button_delegates_block_child_centering_to_taffy() {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::element(
"button",
"button",
LayoutElementCategory::FormControl(LayoutFormControlKind::Button),
None,
vec![2],
),
Node::element(
"button-child",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 100.0, PaintColor::TRANSPARENT),
);
styles
.primary
.insert(1, sized(LayoutDisplay::Block, 108.0, 44.0, GREEN));
styles
.primary
.insert(2, sized(LayoutDisplay::Block, 20.0, 10.0, RED));
let snapshot = render(&source, &mut styles, 300, 100);
assert_eq!(
rect(&snapshot, GREEN),
PaintRect::new(0.0, 0.0, 108.0, 44.0)
);
assert_eq!(rect(&snapshot, RED), PaintRect::new(0.0, 17.0, 20.0, 10.0));
}
#[test]
fn text_selection_and_caret_use_parley_geometry_in_the_owned_snapshot() {
let selection_color = PaintColor::new(180.0 / 255.0, 213.0 / 255.0, 1.0, 1.0);
let render_text = |selection| {
let source = Source(vec![
Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]),
Node::text("text", "hello world").with_selection(selection),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 200.0, 40.0, PaintColor::TRANSPARENT)
.with_text_metrics(20.0, 24.0),
);
render(&source, &mut styles, 200, 40)
};
let selected = render_text(LayoutTextSelection::new(1, 5));
assert!(selected.fragments.iter().any(|fragment| {
matches!(
fragment,
PaintFragment::Fill {
shape: PaintShape::Rect(rect),
brush: PaintBrush::Solid(color),
..
} if *color == selection_color && rect.width > 0.0 && rect.height > 0.0
)
}));
let caret = render_text(LayoutTextSelection::new(5, 5));
assert!(caret.fragments.iter().any(|fragment| {
matches!(
fragment,
PaintFragment::Fill {
shape: PaintShape::Rect(rect),
brush: PaintBrush::Solid(PaintColor::BLACK),
..
} if (rect.width - 1.5).abs() < 0.05 && rect.height > 0.0
)
}));
}
#[test]
fn sticky_uses_the_scrollport_and_transformed_ancestor_contains_fixed_descendants() {
let source = Source(vec![
Node::element(
"root",
"div",
LayoutElementCategory::Generic,
None,
vec![1, 3],
),
Node::element(
"scroll",
"div",
LayoutElementCategory::Generic,
None,
vec![2],
),
Node::element(
"sticky",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
Node::element(
"transform",
"div",
LayoutElementCategory::Generic,
None,
vec![4],
),
Node::element(
"fixed",
"div",
LayoutElementCategory::Generic,
None,
Vec::new(),
),
]);
let mut styles = Styles::default();
styles.primary.insert(
0,
sized(LayoutDisplay::Block, 300.0, 200.0, PaintColor::TRANSPARENT),
);
styles.primary.insert(
1,
ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::length(100.0),
height: Dimension::length(50.0),
},
overflow: Point {
x: Overflow::Hidden,
y: Overflow::Hidden,
},
..Style::default()
},
PaintColor::TRANSPARENT,
),
);
styles.primary.insert(
2,
ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::length(50.0),
height: Dimension::length(20.0),
},
inset: Rect {
top: taffy::LengthPercentageAuto::length(10.0),
right: taffy::LengthPercentageAuto::auto(),
bottom: taffy::LengthPercentageAuto::auto(),
left: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
RED,
)
.with_position(LayoutPosition::Sticky),
);
styles.primary.insert(
3,
ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::length(100.0),
height: Dimension::length(100.0),
},
margin: Rect {
left: taffy::LengthPercentageAuto::length(50.0),
right: taffy::LengthPercentageAuto::length(0.0),
top: taffy::LengthPercentageAuto::length(0.0),
bottom: taffy::LengthPercentageAuto::length(0.0),
},
..Style::default()
},
PaintColor::TRANSPARENT,
)
.with_transform_containing_block(),
);
styles.primary.insert(
4,
ResolvedLayoutStyle::synthetic(
LayoutDisplay::Block,
Style {
size: Size {
width: Dimension::length(20.0),
height: Dimension::length(20.0),
},
inset: Rect {
right: taffy::LengthPercentageAuto::length(0.0),
top: taffy::LengthPercentageAuto::length(0.0),
left: taffy::LengthPercentageAuto::auto(),
bottom: taffy::LengthPercentageAuto::auto(),
},
..Style::default()
},
BLUE,
)
.with_position(LayoutPosition::Fixed),
);
let snapshot = render(&source, &mut styles, 300, 200);
assert_close(rect(&snapshot, RED).y, 10.0);
assert_close(rect(&snapshot, BLUE).x, 130.0);
assert!(snapshot.fragments.iter().any(|fragment| matches!(
fragment,
PaintFragment::PushClip {
shape: moli_layout::PaintShape::Rect(rect),
..
} if rect.width == 100.0 && rect.height == 50.0
)));
}