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, text: Option<&'static str>, children: Vec, metrics: Option, image: Option, selection: Option, } impl Node { fn element( label: &'static str, local_name: &'static str, category: LayoutElementCategory, replaced: Option, children: Vec, ) -> 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); impl LayoutSource for Source { type NodeId = usize; type ChildIter<'a> = std::iter::Copied>; fn root(&self) -> Self::NodeId { 0 } fn flat_parent(&self, node: Self::NodeId) -> Option { 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 { 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 { self.0[node].selection } fn replaced_metrics(&self, node: Self::NodeId) -> Option { self.0[node].metrics } fn replaced_image( &self, node: Self::NodeId, _style: &ResolvedLayoutStyle, ) -> Option { self.0[node].image.clone() } } #[derive(Default)] struct Styles { primary: HashMap, pseudo: HashMap<(usize, LayoutPseudo), ResolvedLayoutStyle>, } impl LayoutStyleResolver for Styles { fn element_styles( &mut self, node: usize, ) -> Result, LayoutError> { Ok(self .primary .get(&node) .cloned() .map(ResolvedLayoutElementStyles::from_primary)) } fn marker_style( &mut self, node: usize, ) -> Result, 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)) -> Self; } impl StyleTestExt for ResolvedLayoutStyle { fn tap_taffy(mut self, update: impl FnOnce(&mut Style)) -> 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::>(); 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 /// . #[test] fn fractional_replaced_images_project_contiguous_pre_transform_destinations() { let svg = Arc::new( moli_image::decode_svg_image( br##""##, ) .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::>(); 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::>(); 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::>(); 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::>(); 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 ))); }