From b1967082f9815c583235c0547ca2d587fa899147 Mon Sep 17 00:00:00 2001
From: SGavrl <277843151+SGavrl@users.noreply.github.com>
Date: Fri, 31 Jul 2026 01:07:56 +0000
Subject: [PATCH] Preserve inline flex and grid outer geometry
---
crates/obscura-render/src/dom.rs | 269 +++++++++++++++++++++++++++----
crates/obscura-render/src/lib.rs | 20 +++
2 files changed, 261 insertions(+), 28 deletions(-)
diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs
index a4d124f..c258554 100644
--- a/crates/obscura-render/src/dom.rs
+++ b/crates/obscura-render/src/dom.rs
@@ -2288,16 +2288,15 @@ fn layout_dom_once(
resolve_grid_areas(tree, root_id, &mut styles);
- // Blockify flex/grid items (CSS Display 3): a direct inline child of a
- // genuine flex or grid container gets a blockified outer display while
- // retaining its own inner formatting context. Without the flex half,
- // `
...
` collapses the code item and
- // navigation bars treat direct `` children as wrapping inline
- // containers instead of flex items.
- //
- // `internal_flex_container` is deliberately excluded: table cells use
- // a flex column only as our block-layout stand-in, and their ordinary
- // inline text must continue to form an inline formatting context.
+ // The root's outer display is always blockified. Keep a declared
+ // inline-flex/grid root's inner mode, but remove its inline
+ // participation marker before box construction.
+ if let Some(root_style) = styles.get_mut(&root_id) {
+ crate::blockify_outer_display(root_style);
+ }
+
+ // CSS Display blockification changes only the outer display. Preserve
+ // the inner Flex/Grid mode of inline-flex/grid items.
let item_parents: Vec = styles
.iter()
.filter(|(_, s)| {
@@ -2308,13 +2307,7 @@ fn layout_dom_once(
.map(|(&id, _)| id)
.collect();
for pid in item_parents {
- for cid in tree.children(pid) {
- if let Some(cs) = styles.get_mut(&cid) {
- if cs.display == crate::Display::Inline {
- cs.display = crate::Display::Block;
- }
- }
- }
+ blockify_layout_children(tree, pid, &mut styles);
}
// In an auto-height column flex container, a zero flex basis still
@@ -2352,15 +2345,11 @@ fn layout_dom_once(
}
}
- // CSS Display blockification: absolute/fixed boxes and floats compute
- // an inline outside display to block. This affects how their own mixed
- // children form lines and blocks; they remain shrink-to-fit where the
- // positioning/float algorithm requires it.
+ // Absolute/fixed boxes and floats are blockified externally but retain
+ // their inner formatting mode and their context-specific shrink-fit.
for style in styles.values_mut() {
- if (matches!(style.position, Some(taffy::Position::Absolute)) || style.float.is_some())
- && style.display == crate::Display::Inline
- {
- style.display = crate::Display::Block;
+ if matches!(style.position, Some(taffy::Position::Absolute)) || style.float.is_some() {
+ crate::blockify_outer_display(style);
}
}
@@ -2393,6 +2382,35 @@ fn layout_dom_once(
}
if let Some(taffy_root) = build(tree, root_id, &mut taffy_tree, &mut id_map, &mut words, &mut engine, &mut ifc_items, &styles) {
+ // Taffy has no outer display type and only gives an auto-width
+ // Block root the initial-containing-block width. CSS blockifies
+ // Flex/Grid roots too, so supply the equivalent used width while
+ // leaving an authored root width untouched.
+ if let Some(root_style) = styles.get(&root_id) {
+ if root_style.width == crate::Dimension::Auto
+ && matches!(root_style.display, crate::Display::Flex | crate::Display::Grid)
+ {
+ if let Ok(current) = taffy_tree.style(taffy_root) {
+ let mut adjusted = current.clone();
+ let outer = (initial_cb_width
+ - root_style.margin.left
+ - root_style.margin.right)
+ .max(0.0);
+ let declared = if root_style.box_sizing == crate::BoxSizing::ContentBox {
+ (outer
+ - root_style.padding.left
+ - root_style.padding.right
+ - root_style.border.left
+ - root_style.border.right)
+ .max(0.0)
+ } else {
+ outer
+ };
+ adjusted.size.width = taffy::Dimension::length(declared);
+ let _ = taffy_tree.set_style(taffy_root, adjusted);
+ }
+ }
+ }
let static_position_candidates =
reparent_inset_positioned_nodes(tree, &mut taffy_tree, taffy_root, &id_map, &styles);
let available = taffy::Size {
@@ -4450,7 +4468,7 @@ fn has_inline_content(tree: &DomTree, id: NodeId, styles: &HashMap,
+) {
+ for child in tree.children(parent) {
+ let transparent = styles
+ .get(&child)
+ .is_some_and(|style| style.display_contents && style.display != crate::Display::None);
+ if transparent {
+ blockify_layout_children(tree, child, styles);
+ } else if let Some(style) = styles.get_mut(&child) {
+ crate::blockify_outer_display(style);
+ }
+ }
+}
+
/// Build whichever of an element or a text node `id` is, returning every
/// taffy node it produced (usually one, but a text node fans out to one leaf
/// per word; see `build_text_words`). Callers collecting a container's
@@ -4527,7 +4565,35 @@ fn build_any(
.flat_map(|cid| build_any(tree, cid, taffy_tree, id_map, words, engine, ifc_items, styles))
.collect();
}
- build(tree, id, taffy_tree, id_map, words, engine, ifc_items, styles).into_iter().collect()
+ let Some(inner) = build(tree, id, taffy_tree, id_map, words, engine, ifc_items, styles) else {
+ return Vec::new();
+ };
+ let needs_outer = styles.get(&id).is_some_and(|style| {
+ style.is_inline_block
+ && matches!(style.display, crate::Display::Flex | crate::Display::Grid)
+ && matches!(style.width, crate::Dimension::Auto)
+ && style.size_expressions[0].is_none()
+ });
+ if !needs_outer {
+ return vec![inner];
+ }
+
+ // Taffy stores only the inner display mode. If an inline-flex/grid node is
+ // handed directly to block or flex layout, its auto width is stretched to
+ // the available width. A transparent atomic outer node supplies the
+ // shrink-wrapping inline participation while the authored node retains its
+ // real Flex/Grid layout and remains the DOM geometry/paint owner.
+ let outer_style = taffy::Style {
+ display: taffy::style::Display::Flex,
+ flex_direction: taffy::FlexDirection::Row,
+ flex_wrap: taffy::FlexWrap::Wrap,
+ align_items: Some(taffy::AlignItems::FLEX_START),
+ ..Default::default()
+ };
+ match taffy_tree.new_with_children(outer_style, &[inner]) {
+ Ok(outer) => vec![outer],
+ Err(_) => vec![inner],
+ }
}
/// Build the direct children of a genuine flex/grid container.
@@ -6356,7 +6422,7 @@ fn build_mixed_block(
let is_forced_break = node
.as_element()
.map_or(false, |element| element.local.as_ref() == "br");
- is_forced_break || s.display == crate::Display::Inline
+ is_forced_break || crate::is_inline_level_box(s)
} else {
false
};
@@ -7837,6 +7903,153 @@ mod tests {
}
}
+ #[test]
+ fn inline_outer_boxes_shrink_wrap_in_only_child_and_text_flows() {
+ let tree = parse_html(
+ r#"
+
+
+
+
+
+ "#,
+ );
+ let laid = layout_dom(&tree, (700.0, 300.0));
+ for name in ["ib", "if", "ig", "ibt", "ift", "igt"] {
+ let rect = laid.rects[&tree.get_element_by_id(name).unwrap()];
+ assert!(
+ (rect.width - 400.0).abs() < 0.01,
+ "{name} did not shrink-wrap its 400px child: {rect:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn inline_flex_and_grid_preserve_percentage_and_box_edge_geometry() {
+ let tree = parse_html(
+ r#"
+
+
+
+ "#,
+ );
+ let laid = layout_dom(&tree, (800.0, 300.0));
+ let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()];
+ for name in ["flex-percent", "grid-percent"] {
+ let item = rect(name);
+ assert!(
+ (item.width - 350.0).abs() < 0.01
+ && (item.height - 20.0).abs() < 0.01,
+ "{name}: {item:?}"
+ );
+ }
+ for name in ["flex-edges", "grid-edges"] {
+ let item = rect(name);
+ assert!(
+ (item.x - 7.0).abs() < 0.01
+ && (item.width - 150.0).abs() < 0.01
+ && (item.height - 50.0).abs() < 0.01,
+ "{name}: {item:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn blockified_inline_flex_and_grid_items_retain_inner_layout() {
+ let tree = parse_html(
+ r#"
+
+ "#,
+ );
+ let laid = layout_dom(&tree, (700.0, 300.0));
+ let rect = |name| laid.rects[&tree.get_element_by_id(name).unwrap()];
+ for (container, first, second) in [("if", "ifa", "ifb"), ("ig", "iga", "igb")] {
+ let container = rect(container);
+ let first = rect(first);
+ let second = rect(second);
+ assert!((container.width - 200.0).abs() < 0.01, "{container:?}");
+ assert!(
+ (first.y - second.y).abs() < 0.01
+ && (second.x - first.x - 100.0).abs() < 0.01,
+ "inner layout was destroyed: first={first:?} second={second:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn root_and_body_inline_outer_geometry_matches_blockification_rules() {
+ for display in ["inline-block", "inline-flex", "inline-grid"] {
+ let tree = parse_html(&format!(
+ r#""#
+ ));
+ let laid = layout_dom(&tree, (800.0, 300.0));
+ let body = tree
+ .descendants(tree.document())
+ .into_iter()
+ .find(|id| {
+ tree.get_node(*id)
+ .is_some_and(|node| {
+ node.as_element()
+ .is_some_and(|element| element.local.as_ref() == "body")
+ })
+ })
+ .unwrap();
+ assert!(
+ (laid.rects[&body].width - 400.0).abs() < 0.01,
+ "body {display}: {:?}",
+ laid.rects[&body]
+ );
+
+ let tree = parse_html(&format!(
+ r#""#
+ ));
+ let laid = layout_dom(&tree, (800.0, 300.0));
+ let root = tree
+ .descendants(tree.document())
+ .into_iter()
+ .find(|id| {
+ tree.get_node(*id)
+ .is_some_and(|node| {
+ node.as_element()
+ .is_some_and(|element| element.local.as_ref() == "html")
+ })
+ })
+ .unwrap();
+ assert!(
+ (laid.rects[&root].width - 800.0).abs() < 0.01,
+ "root {display}: {:?}",
+ laid.rects[&root]
+ );
+ }
+ }
+
#[test]
fn size_container_auto_block_size_ignores_descendants() {
let tree = parse_html(
diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs
index 1ca0157..f18f9d7 100644
--- a/crates/obscura-render/src/lib.rs
+++ b/crates/obscura-render/src/lib.rs
@@ -671,6 +671,26 @@ pub struct LayoutStyle {
pub box_shadow: Option,
}
+/// Whether this box has an inline outer display and participates in an inline
+/// formatting context. `display` retains the inner layout mode for
+/// inline-flex/grid, so this cannot be inferred from `Display::Inline` alone.
+pub(crate) fn is_inline_level_box(style: &LayoutStyle) -> bool {
+ style.display == Display::Inline || style.is_inline_block
+}
+
+/// Apply CSS Display blockification while preserving the inner display mode.
+/// Thus inline-flex becomes flex and inline-grid becomes grid, while
+/// inline-block/plain inline become block.
+pub(crate) fn blockify_outer_display(style: &mut LayoutStyle) {
+ if !is_inline_level_box(style) {
+ return;
+ }
+ if style.display == Display::Inline {
+ style.display = Display::Block;
+ }
+ style.is_inline_block = false;
+}
+
pub(crate) const CB_TRIGGER_TRANSFORM: u16 = 1 << 0;
pub(crate) const CB_TRIGGER_FILTER: u16 = 1 << 1;
pub(crate) const CB_TRIGGER_BACKDROP_FILTER: u16 = 1 << 2;