From e7656d9d9c0b488f5fc4d4ece5c9ad20fc52aea8 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:55:22 +0000 Subject: [PATCH] Repair intrinsic flex heights with negative margins --- crates/obscura-render/src/dom.rs | 117 +++++++++++++++++++++ crates/obscura-render/tests/layout_test.rs | 48 +++++++++ 2 files changed, 165 insertions(+) diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index 05e1f94..065efd1 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -2831,6 +2831,17 @@ fn layout_dom_once( ); } let _ = taffy_tree.compute_layout_with_measure(taffy_root, available, &mut measure); + if repair_intrinsic_column_flex_negative_margins( + &mut taffy_tree, + &id_map, + &styles, + ) { + let _ = taffy_tree.compute_layout_with_measure( + taffy_root, + available, + &mut measure, + ); + } if resolve_deferred_flex_inline_sizes( tree, &mut taffy_tree, @@ -2926,6 +2937,13 @@ fn layout_dom_once( ); } let _ = taffy_tree.compute_layout(taffy_root, available); + if repair_intrinsic_column_flex_negative_margins( + &mut taffy_tree, + &id_map, + &styles, + ) { + let _ = taffy_tree.compute_layout(taffy_root, available); + } if resolve_deferred_flex_inline_sizes( tree, &mut taffy_tree, @@ -4540,6 +4558,105 @@ fn apply_table_cell_block_alignment( changed } +/// Repair Taffy 0.12's intrinsic main-size calculation for column flexboxes +/// containing a negative main-axis margin. +/// +/// In the max-content path Taffy feeds the margin-adjusted contribution into +/// its flex-fraction calculation. A negative margin on a shrink-disabled item +/// can therefore subtract the item's entire flex basis (or many multiples of +/// it) from the container: a `margin-top:-15px` child with a 100px basis can +/// collapse an otherwise auto-height flex column to zero. Final item sizes and +/// placement are still correct; only the container's automatic block size is +/// corrupted. +/// +/// CSS defines that automatic size from the flex items' outer main sizes. Use +/// the already-resolved final item margins and border-box sizes to freeze that +/// intrinsic content height, then let the caller run Taffy again so ordinary +/// min/max sizing and ancestor layout consume the corrected contribution. +fn repair_intrinsic_column_flex_negative_margins( + taffy_tree: &mut TaffyTree, + id_map: &HashMap, + styles: &HashMap, +) -> bool { + let mut repairs = Vec::new(); + + for (&node, &dom_id) in id_map { + let Some(style) = styles.get(&dom_id) else { + continue; + }; + if style.display != crate::Display::Flex + || style.internal_flex_container + || style.height != crate::Dimension::Auto + || effective_container_type(style) == crate::ContainerType::Size + || !matches!( + style.flex_direction, + Some(taffy::FlexDirection::Column | taffy::FlexDirection::ColumnReverse) + ) + { + continue; + } + + let Ok(children) = taffy_tree.children(node) else { + continue; + }; + let mut item_count = 0usize; + let mut content_height = 0.0f32; + let mut has_negative_main_margin = false; + for child in children { + let (Ok(child_style), Ok(child_layout)) = + (taffy_tree.style(child), taffy_tree.layout(child)) + else { + continue; + }; + if child_style.display == taffy::style::Display::None + || child_style.position == taffy::style::Position::Absolute + { + continue; + } + let margin = child_layout.margin.top + child_layout.margin.bottom; + has_negative_main_margin |= + child_layout.margin.top < 0.0 || child_layout.margin.bottom < 0.0; + content_height += child_layout.size.height + margin; + item_count += 1; + } + if !has_negative_main_margin || item_count == 0 { + continue; + } + content_height += + style.row_gap.unwrap_or(0.0) * item_count.saturating_sub(1) as f32; + content_height = content_height.max(0.0); + + let Ok(layout) = taffy_tree.layout(node) else { + continue; + }; + let outer_height = content_height + + layout.padding.top + + layout.padding.bottom + + layout.border.top + + layout.border.bottom; + if (layout.size.height - outer_height).abs() < 0.01 { + continue; + } + let declared_height = if style.box_sizing == crate::BoxSizing::ContentBox { + content_height + } else { + outer_height + }; + repairs.push((node, declared_height)); + } + + let mut changed = false; + for (node, height) in repairs { + let Ok(current) = taffy_tree.style(node) else { + continue; + }; + let mut repaired = current.clone(); + repaired.size.height = taffy::Dimension::length(height); + changed |= taffy_tree.set_style(node, repaired).is_ok(); + } + changed +} + fn resolve_static_positions_and_reparent( taffy_tree: &mut TaffyTree, candidates: &[StaticPositionCandidate], diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs index daa8944..86d42aa 100644 --- a/crates/obscura-render/tests/layout_test.rs +++ b/crates/obscura-render/tests/layout_test.rs @@ -494,6 +494,54 @@ fn negative_flex_margin_overlays_at_container_start() { assert!((sidebar.x - 0.0).abs() < 0.01, "sidebar: {sidebar:?}"); } +#[test] +fn auto_height_column_flex_counts_negative_main_axis_margins_once() { + let html = r##" + +
+
+
+
+
+
+ + "##; + let tree = parse_html(html); + let layout = layout_dom(&tree, (900.0, 1200.0)); + let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()]; + let column = rect("column"); + let first = rect("first"); + let second = rect("second"); + let third = rect("third"); + + assert!( + (column.height - 871.0).abs() < 0.01, + "100 + (100 - 15) + (666 + 20) must size the auto-height column: {column:?}" + ); + assert!((first.y - column.y).abs() < 0.01, "first: {first:?}"); + assert!( + (second.y - (first.y + first.height - 15.0)).abs() < 0.01, + "negative margin must overlap the preceding item: first={first:?} second={second:?}" + ); + assert!( + (third.y - (second.y + second.height + 20.0)).abs() < 0.01, + "the following item must retain normal sequential placement: second={second:?} third={third:?}" + ); + assert!( + rect("contained").height.abs() < 0.01, + "size containment must keep its intentional zero intrinsic block size" + ); +} + #[test] fn percentage_flex_overlay_uses_auto_width_parent_content_box() { let html = r##"