mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 08:00:45 +00:00
fix(layout): anchor inline out-of-flow boxes to line boxes
Retain the hypothetical outer display in the inline object stream. Resolve static positions from CSS line-box edges rather than glyph baselines or ancestor vertical-align offsets, and carry the source IFC direction across containing-block changes. Cover 26 font-independent Chromium differential cases.
This commit is contained in:
+70
-10
@@ -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::<Vec<_>>();
|
||||
@@ -1928,9 +1980,17 @@ fn collect_box<N>(
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<N>(
|
||||
child: LayoutBoxId,
|
||||
area: PositionedContainingArea,
|
||||
static_position: Point<f32>,
|
||||
inline_level: bool,
|
||||
static_direction: InlineDirection,
|
||||
numeric_parent_origin: Point<f32>,
|
||||
) where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
@@ -1224,9 +1225,7 @@ fn layout_inline_absolute_child<N>(
|
||||
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<N>(
|
||||
}
|
||||
(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;
|
||||
}
|
||||
|
||||
@@ -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<f32>,
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Out-of-flow static positions belong to CSS line boxes</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; }
|
||||
html { overflow: hidden; }
|
||||
.slot { display: flow-root; position: relative; width: 200px; height: 120px; font-size: 0; line-height: 30px; }
|
||||
.atom { display: inline-block; width: 30px; height: 10px; }
|
||||
.item { display: inline; position: absolute; width: 20px; height: 10px; background: green; }
|
||||
.block { display: block; }
|
||||
.rtl { direction: rtl; }
|
||||
</style>
|
||||
<!-- Zero-size text and fixed-size in-flow boxes isolate static positioning
|
||||
from installed fonts. Rectangles are relative to the .slot border box. -->
|
||||
<div class="slot" id="inline-before" data-expected="0,0,20,10"><span class="item"></span><span class="atom"></span></div>
|
||||
<div class="slot" id="inline-after" data-expected="30,0,20,10"><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="inline-next-line" data-expected="30,30,20,10"><span class="atom"></span><br><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="inline-raised-ancestor" data-expected="30,0,20,10"><span class="atom"></span><span style="vertical-align:40px"><span class="item"></span></span></div>
|
||||
<div class="slot" id="inline-tall-line" data-expected="30,0,20,10"><span class="atom" style="height:50px"></span><span class="item"></span></div>
|
||||
<div class="slot" id="inline-margins" data-expected="41,3,20,10"><span class="atom"></span><span class="item" style="margin:3px 7px 9px 11px"></span></div>
|
||||
<div class="slot" id="inline-empty" data-expected="0,0,20,10"><span class="item"></span></div>
|
||||
<div class="slot" id="block-before" data-expected="0,0,20,10"><span class="item block"></span><span class="atom"></span></div>
|
||||
<div class="slot" id="block-after" data-expected="0,30,20,10"><span class="atom"></span><span class="item block"></span></div>
|
||||
<div class="slot" id="block-next-line" data-expected="0,30,20,10"><span class="atom"></span><br><span class="item block"></span><span class="atom"></span></div>
|
||||
<div class="slot" id="block-tall-line" data-expected="0,65,20,10"><span class="atom" style="height:50px"></span><span class="item block"></span></div>
|
||||
<div class="slot" id="block-empty" data-expected="0,0,20,10"><span class="item block"></span></div>
|
||||
<!-- Real RTL text supplies the paragraph direction; its zero font size keeps
|
||||
these positioning assertions independent of the selected glyph font.
|
||||
Paragraph direction and bidi levels of leading inline objects are a
|
||||
separate Parley input limitation, not part of line-box block placement. -->
|
||||
<div class="slot rtl" id="rtl-inline-before-atomic" data-expected="180,0,20,10">א<span class="item"></span><span class="atom"></span></div>
|
||||
<div class="slot rtl" id="rtl-inline-after-atomic" data-expected="150,0,20,10">א<span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot rtl" id="rtl-block-before-content" data-expected="180,0,20,10"><span class="item block"></span><span class="atom"></span></div>
|
||||
<div class="slot rtl" id="rtl-block-after-content" data-expected="180,30,20,10">א<span class="atom"></span><span class="item block"></span></div>
|
||||
<div class="slot" id="padded-line" data-expected="43,9,20,10" style="padding:7px 11px;border:2px solid"><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="indented-line" data-expected="55,0,20,10" style="text-indent:25px"><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="indented-block" data-expected="0,30,20,10" style="text-indent:25px"><span class="atom"></span><span class="item block"></span></div>
|
||||
<div class="slot" id="different-containing-block" data-expected="41,7,20,10"><div style="margin:7px 11px"><span class="atom"></span><span class="item"></span></div></div>
|
||||
<div class="slot" id="definite-top" data-expected="30,17,20,10"><span class="atom"></span><span class="item" style="top:17px"></span></div>
|
||||
<div class="slot" id="inline-after-float" data-expected="50,0,20,10"><span style="float:left;width:20px;height:10px"></span><span class="atom"></span><span class="item"></span></div>
|
||||
<div class="slot" id="block-after-float" data-expected="0,30,20,10"><span style="float:left;width:20px;height:10px"></span><span class="atom"></span><span class="item block"></span></div>
|
||||
<div class="slot" id="block-after-only-out-of-flow" data-expected="0,0,20,10"><span style="position:absolute"></span><span class="item block"></span><span class="atom"></span></div>
|
||||
<div class="slot" id="rtl-ifc-ltr-containing-block" data-expected="139,7,20,10"><div style="direction:rtl;margin:7px 11px">א<span class="atom"></span><span class="item"></span></div></div>
|
||||
<div class="slot rtl" id="ltr-ifc-rtl-containing-block" data-expected="41,7,20,10"><div style="direction:ltr;margin:7px 11px"><span class="atom"></span><span class="item"></span></div></div>
|
||||
Reference in New Issue
Block a user