diff --git a/moli-layout/src/inline.rs b/moli-layout/src/inline.rs index 9e17c7fcdb..ba8def41ff 100644 --- a/moli-layout/src/inline.rs +++ b/moli-layout/src/inline.rs @@ -103,11 +103,19 @@ pub(crate) struct SourceOrigin { pub(crate) enum InlineObjectRole { Atomic, Float, - OutOfFlow, + OutOfFlow(OutOfFlowDisplay), StartEdge, EndEdge, } +/// Positioning blockifies the used display. Inline layout must retain the +/// hypothetical outer display to choose the current line or the next line. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum OutOfFlowDisplay { + Inline, + Block, +} + #[derive(Clone, Debug)] pub(crate) struct InlineObject { pub(crate) box_id: LayoutBoxId, @@ -587,7 +595,7 @@ pub(crate) fn build_inline_fragments( } InlineObjectRole::Atomic | InlineObjectRole::Float - | InlineObjectRole::OutOfFlow => {} + | InlineObjectRole::OutOfFlow(_) => {} } } } @@ -898,7 +906,7 @@ fn resolve_inline_lines( .and_then(|index| structural_edge_contributions.get(index)) .copied() .unwrap_or(false), - InlineObjectRole::Float | InlineObjectRole::OutOfFlow => false, + InlineObjectRole::Float | InlineObjectRole::OutOfFlow(_) => false, }), glyph_key: None, anchor: LineVerticalAnchor::Root, @@ -1074,13 +1082,57 @@ fn resolve_inline_lines( states[state_index].global_offset = states[state_index].relative_offset + anchor_global_offset(states[state_index].anchor, &states); } + // Static positions belong to the CSS line box, not an inline + // baseline or an ancestor's vertical-align state. Like Blink's + // PlaceOutOfFlowObjects, block-level placeholders move to the + // next line only when in-flow content logically precedes them. + // Parley exposes items in visual order, so RTL searches backwards. + let first_in_flow = if layout.is_rtl() { + geometries + .iter() + .rposition(|geometry| geometry.creates_line) + } else { + geometries.iter().position(|geometry| geometry.creates_line) + }; + let line_top = raw_top + preceding_adjustment; let item_offsets = geometries .iter() - .map(|geometry| { - let desired_top = root_baseline - + anchor_global_offset(geometry.anchor, &states) - + geometry.relative_offset - + geometry.bounds.top; + .zip(line.items()) + .enumerate() + .map(|(index, (geometry, item))| { + let static_display = match item { + PositionedLayoutItem::InlineBox(positioned) => context + .object(positioned.id) + .and_then(|object| match object.role { + InlineObjectRole::OutOfFlow(display) => Some(display), + _ => None, + }), + PositionedLayoutItem::GlyphRun(_) => None, + }; + let desired_top = match static_display { + Some(OutOfFlowDisplay::Inline) => line_top, + Some(OutOfFlowDisplay::Block) => { + let has_preceding_content = first_in_flow.is_some_and(|first| { + if layout.is_rtl() { + index < first + } else { + index > first + } + }); + line_top + + if has_preceding_content { + line_height + } else { + 0.0 + } + } + None => { + root_baseline + + anchor_global_offset(geometry.anchor, &states) + + geometry.relative_offset + + geometry.bounds.top + } + }; desired_top - geometry.initial_top }) .collect::>(); @@ -1928,9 +1980,17 @@ fn collect_box( let out_of_flow = world.boxes[id.index()].style.is_out_of_flow(); if out_of_flow { + let display = if world.boxes[id.index()] + .style + .hypothetical_display_is_inline_level() + { + OutOfFlowDisplay::Inline + } else { + OutOfFlowDisplay::Block + }; normalizer.push_object( id, - InlineObjectRole::OutOfFlow, + InlineObjectRole::OutOfFlow(display), InlineBoxKind::OutOfFlow, ancestors, world.boxes[id.index()].style.vertical_align(), @@ -2292,7 +2352,7 @@ impl InlineNormalizer { self.flush_pending_carriage_return(); if matches!( role, - InlineObjectRole::Atomic | InlineObjectRole::Float | InlineObjectRole::OutOfFlow + InlineObjectRole::Atomic | InlineObjectRole::Float | InlineObjectRole::OutOfFlow(_) ) { self.flush_pending(); self.line_has_content = true; diff --git a/moli-layout/src/taffy_tree.rs b/moli-layout/src/taffy_tree.rs index 5591965242..f33c34a38a 100644 --- a/moli-layout/src/taffy_tree.rs +++ b/moli-layout/src/taffy_tree.rs @@ -18,8 +18,9 @@ use crate::{ LayoutWorld, PaintRect, PaintViewport, inline::{ InlineContentWidthsMemo, InlineFormattingContext, InlineFragments, InlineLinePlacement, - InlineObjectRole, break_inline_lines, build_inline_fragments, build_inline_line_placements, - measure_inline_lines, relative_atomic_inset_offset, reset_inline_layout_for_probe, + InlineObjectRole, OutOfFlowDisplay, break_inline_lines, build_inline_fragments, + build_inline_line_placements, measure_inline_lines, relative_atomic_inset_offset, + reset_inline_layout_for_probe, }, positioned::resolve_absolute_axis_margins, replaced::measure_replaced, @@ -961,7 +962,7 @@ where child, area, static_in_area, - static_position.inline_level, + static_position.direction, numeric_parent_origin, ); } else { @@ -970,7 +971,7 @@ where child, area, static_in_area, - area.direction == taffy::Direction::Rtl && static_position.inline_level, + static_position.direction == InlineDirection::Rtl, numeric_parent_origin, ); } @@ -1120,7 +1121,7 @@ fn layout_inline_absolute_child( child: LayoutBoxId, area: PositionedContainingArea, static_position: Point, - inline_level: bool, + static_direction: InlineDirection, numeric_parent_origin: Point, ) where N: Copy + Debug + Eq + Hash, @@ -1224,9 +1225,7 @@ fn layout_inline_absolute_child( let available_width = match (left, right) { (Some(left), None) => area_width - left, (None, Some(right)) => area_width - right, - (None, None) if area.direction == taffy::Direction::Rtl && inline_level => { - static_position.x - } + (None, None) if static_direction == InlineDirection::Rtl => static_position.x, (None, None) => area_width - static_position.x, (Some(_), Some(_)) => unreachable!("both insets already resolve auto width"), } - non_auto_margin_width; @@ -1333,7 +1332,7 @@ fn layout_inline_absolute_child( } (Some(left), None) => left + resolved_margin.left, (None, Some(right)) => area_width - final_size.width - right - resolved_margin.right, - (None, None) if area.direction == taffy::Direction::Rtl && inline_level => { + (None, None) if static_direction == InlineDirection::Rtl => { static_position.x - final_size.width - resolved_margin.right } (None, None) => static_position.x + resolved_margin.left, @@ -2129,7 +2128,7 @@ where margins, }); } - InlineObjectRole::OutOfFlow => { + InlineObjectRole::OutOfFlow(_) => { inline_box.width = 0.0; inline_box.height = 0.0; } @@ -2582,20 +2581,24 @@ where let vertical_offset = line_placement .map(|placement| placement.item_offset(item_index)) .unwrap_or_default(); - if object.role == InlineObjectRole::OutOfFlow { - let inline_level = self.boxes[object.box_id.index()] - .style - .hypothetical_display_is_inline_level(); + if let InlineObjectRole::OutOfFlow(display) = object.role { + let inline_offset = match display { + OutOfFlowDisplay::Inline => positioned.x, + OutOfFlowDisplay::Block => match container_direction { + InlineDirection::Ltr => 0.0, + InlineDirection::Rtl => containing_block_size.width, + }, + }; self.boxes[object.box_id.index()].inline_static_position = Some(InlineStaticPosition { owner: self.boxes[object.box_id.index()] .inline_context_owner .unwrap_or_else(|| panic!("out-of-flow IFC object lost its owner")), point: Point { - x: content_offset.x + if inline_level { positioned.x } else { 0.0 }, + x: content_offset.x + inline_offset, y: content_offset.y + positioned.y + vertical_offset, }, - inline_level, + direction: container_direction, }); continue; } diff --git a/moli-layout/src/world.rs b/moli-layout/src/world.rs index 24a37f24a6..824e0b533f 100644 --- a/moli-layout/src/world.rs +++ b/moli-layout/src/world.rs @@ -6,8 +6,10 @@ use taffy::{Cache, Layout, Point, Style}; use crate::{ LayoutCssImageReference, LayoutElementSemantics, LayoutError, LayoutPoint, LayoutPseudo, LayoutResolvedGridTracks, LayoutScrollbarAxis, LayoutScrollbarColors, LayoutScrollbarGutter, - LayoutScrollbarWidth, ResolvedLayoutStyle, inline::InlineFormattingContext, - replaced::ReplacedContext, style::LayoutOverflowMode, + LayoutScrollbarWidth, ResolvedLayoutStyle, + inline::InlineFormattingContext, + replaced::ReplacedContext, + style::{InlineDirection, LayoutOverflowMode}, }; /// Dense identifier scoped to exactly one [`LayoutWorld`]. @@ -190,7 +192,9 @@ pub enum LayoutCapabilityDiagnostic { pub(crate) struct InlineStaticPosition { pub(crate) owner: LayoutBoxId, pub(crate) point: Point, - pub(crate) inline_level: bool, + /// Direction of the formatting context that produced the static point, + /// which need not be the direction of the actual containing block. + pub(crate) direction: InlineDirection, } impl LayoutCapabilityDiagnostic { diff --git a/moli-renderer-v8/src/runtime/phase_one/mod.rs b/moli-renderer-v8/src/runtime/phase_one/mod.rs index 4ad3a19081..8ac1697913 100644 --- a/moli-renderer-v8/src/runtime/phase_one/mod.rs +++ b/moli-renderer-v8/src/runtime/phase_one/mod.rs @@ -832,6 +832,60 @@ html, body { display: block; margin: 0; padding: 0 } })); } + #[test] + fn layout_renderer_anchors_inline_static_positions_to_line_boxes() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("current-thread runtime should build"); + runtime.block_on(tokio::task::LocalSet::new().run_until(async move { + let mut page = parse_phase_one_html_into_page_vm_for_test_with_env( + include_str!("../../../tests/fixtures/inline-static-position.html"), + default_test_page_vm_env_config_with(|env| { + env.layout_policy = moli_page_types::LayoutPolicy::OnDemand; + }), + ) + .await; + page.vm_mut().sync_live_document_style_sources(); + page.vm_mut() + .screenshot_layout_snapshot(moli_layout::PaintViewport::new(800, 600, 1.0)) + .expect("inline static-position layout should succeed") + .expect("fixture should have a document element"); + let geometry = page + .vm_mut() + .eval( + r#" +JSON.stringify(Array.from(document.querySelectorAll('.slot'), slot => { + const origin = slot.getBoundingClientRect(); + const item = slot.querySelector('.item').getBoundingClientRect(); + return { + id: slot.id, + actual: [item.x - origin.x, item.y - origin.y, item.width, item.height], + expected: slot.dataset.expected.split(',').map(Number) + }; +})) +"#, + ) + .expect("read published inline geometry"); + let cases: serde_json::Value = serde_json::from_str(&geometry).expect("geometry JSON"); + let cases = cases.as_array().expect("geometry cases"); + assert_eq!(cases.len(), 26); + for case in cases { + for axis in 0..4 { + let actual = case["actual"][axis].as_f64().expect("actual coordinate"); + let expected = case["expected"][axis] + .as_f64() + .expect("expected coordinate"); + assert!( + (actual - expected).abs() <= 1.0 / 64.0, + "{}[{axis}]: expected {expected}, got {actual}", + case["id"], + ); + } + } + })); + } + #[test] fn layout_renderer_preserves_calc_min_width_in_float_intrinsic_contribution() { let runtime = tokio::runtime::Builder::new_current_thread() diff --git a/moli-renderer-v8/tests/fixtures/inline-static-position.html b/moli-renderer-v8/tests/fixtures/inline-static-position.html new file mode 100644 index 0000000000..08b02d3bd9 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/inline-static-position.html @@ -0,0 +1,44 @@ + + +Out-of-flow static positions belong to CSS line boxes + + +
+
+

+
+
+
+
+
+
+

+
+
+ +
א
+
א
+
+
א
+
+
+
+
+
+
+
+
+
א
+