diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 1077b02..d28abd2 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -2962,13 +2962,6 @@ fn layout_dom_once( } } - if !static_position_candidates.is_empty() { - let _ = taffy_tree.compute_layout_with_measure(taffy_root, available, &mut measure); - resolve_static_positions_and_reparent( - &mut taffy_tree, - &static_position_candidates, - ); - } let _ = taffy_tree.compute_layout_with_measure(taffy_root, available, &mut measure); if repair_intrinsic_column_flex_negative_margins( &mut taffy_tree, @@ -3065,16 +3058,28 @@ fn layout_dom_once( &mut measure, ); } - } - #[cfg(not(feature = "paint"))] - { + // A fully-auto positioned axis uses the box's static position + // in its original formatting context. Harvest that coordinate + // only after every intrinsic/table/fragmentation repair has + // produced final in-flow geometry; otherwise reparenting pins + // the box to a stale preliminary document offset and can + // manufacture scrolling overflow. The resolver snapshots all + // candidates before changing any parent, preserving nested + // static-position candidates. if !static_position_candidates.is_empty() { - let _ = taffy_tree.compute_layout(taffy_root, available); resolve_static_positions_and_reparent( &mut taffy_tree, &static_position_candidates, ); + let _ = taffy_tree.compute_layout_with_measure( + taffy_root, + available, + &mut measure, + ); } + } + #[cfg(not(feature = "paint"))] + { let _ = taffy_tree.compute_layout(taffy_root, available); if repair_intrinsic_column_flex_negative_margins( &mut taffy_tree, @@ -3142,6 +3147,16 @@ fn layout_dom_once( ) { let _ = taffy_tree.compute_layout(taffy_root, available); } + // Keep the no-paint geometry path in the same final-position + // contract as screenshots: static coordinates are resolved + // after every pass that can move in-flow placeholders. + if !static_position_candidates.is_empty() { + resolve_static_positions_and_reparent( + &mut taffy_tree, + &static_position_candidates, + ); + let _ = taffy_tree.compute_layout(taffy_root, available); + } } sync_resolved_percentage_padding( &taffy_tree, @@ -9435,6 +9450,53 @@ mod tests { ); } + #[test] + fn static_positions_use_final_post_repair_geometry_without_phantom_overflow() { + let tree = parse_html( + r#" +
+
+
+
+
+
+
"#, + ); + let laid = layout_dom(&tree, (800.0, 400.0)); + let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()]; + let columns = rect("columns"); + let holder = rect("holder"); + let outer = rect("outer"); + let wrapper = rect("wrapper"); + let nested = rect("nested"); + + assert!((columns.height - 200.0).abs() < 0.01, "{columns:?}"); + assert!((holder.y - 200.0).abs() < 0.01, "{holder:?}"); + assert!( + (outer.y - holder.y).abs() < 0.01, + "outer static position was harvested before multicol repair: \ + holder={holder:?} outer={outer:?}" + ); + assert!( + (nested.y - wrapper.y).abs() < 0.01, + "nested static-position candidate changed while reparenting its ancestor: \ + wrapper={wrapper:?} nested={nested:?}" + ); + assert_eq!( + laid.scrolling_content_size(&tree, (800.0, 400.0)), + (800.0, 400.0), + "a stale preliminary static position must not create scrolling overflow" + ); + } + #[test] fn multicol_does_not_atomize_breakable_prose_boxes() { let tree = parse_html(