render: position inset boxes against CSS containers

Reparent inset-positioned absolute boxes to the nearest non-static ancestor
and fixed boxes to the initial containing block. Keep auto-axis boxes on the
existing static-position path, distinguish fixed from absolute, and blockify
positioned inline boxes and floats.
This commit is contained in:
SGavrl committed 2026-07-23 11:34:23 +00:00
1 parent d36809058f
commit 6ea1739121
4 files changed
+134 -2

No files matched your search

+76
View File
@@ -530,6 +530,18 @@ pub fn layout_dom_with_images(
}
}
// 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.
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;
}
}
// The initial containing block is modelled by the root taffy node's
// definite viewport height (set at build), which is what `html,body{
// height:100%}` chains and sticky-footer app shells resolve against.
@@ -563,6 +575,7 @@ pub fn layout_dom_with_images(
}
if let Some(taffy_root) = build(tree, root_id, &mut taffy_tree, &mut id_map, &mut words, &mut engine, &mut ifc_items, &styles) {
reparent_inset_positioned_nodes(tree, &mut taffy_tree, taffy_root, &id_map, &styles);
let available = taffy::Size {
width: taffy::AvailableSpace::Definite(viewport.0),
height: taffy::AvailableSpace::Definite(viewport.1),
@@ -1139,6 +1152,69 @@ fn compute_absolute_rects(
}
}
/// Attach inset-positioned boxes to their CSS containing block rather than
/// their immediate DOM parent.
///
/// Taffy resolves an absolute child's insets against its direct layout-tree
/// parent. CSS instead uses the nearest non-static ancestor, or the initial
/// containing block for `position:fixed`. Gecko represents that distinction
/// by reparenting the absolute frame and leaving a placeholder in normal
/// flow. We can safely do the same without a placeholder when each axis has
/// at least one specified inset: no static-position coordinate is needed.
/// Boxes with an entirely auto axis stay under their DOM parent until the
/// placeholder/static-position path is implemented.
fn reparent_inset_positioned_nodes(
tree: &DomTree,
taffy_tree: &mut TaffyTree<usize>,
taffy_root: taffy::NodeId,
id_map: &HashMap<taffy::NodeId, NodeId>,
styles: &HashMap<NodeId, crate::LayoutStyle>,
) {
let reverse: HashMap<NodeId, taffy::NodeId> =
id_map.iter().map(|(&taffy_id, &dom_id)| (dom_id, taffy_id)).collect();
let mut nearest_cb_for_children: HashMap<NodeId, taffy::NodeId> = HashMap::new();
for dom_id in tree.descendants(tree.document()) {
let Some(style) = styles.get(&dom_id) else { continue };
let parent = tree.get_node(dom_id).and_then(|node| node.parent);
let inherited_cb = parent
.and_then(|id| nearest_cb_for_children.get(&id).copied())
.unwrap_or(taffy_root);
// Record this before any candidate early-exit so all descendants get
// an O(1) nearest-containing-block lookup. The full walk stays O(n)
// even on deeply nested or absolute-heavy documents.
let child_cb = if style.position.is_some() {
reverse.get(&dom_id).copied().unwrap_or(inherited_cb)
} else {
inherited_cb
};
nearest_cb_for_children.insert(dom_id, child_cb);
if !matches!(style.position, Some(taffy::Position::Absolute)) {
continue;
}
let has_block_inset = style.inset[0].is_some() || style.inset[2].is_some();
let has_inline_inset = style.inset[1].is_some() || style.inset[3].is_some();
if !has_block_inset || !has_inline_inset {
continue;
}
let Some(&child) = reverse.get(&dom_id) else { continue };
let target = if style.position_fixed {
taffy_root
} else {
inherited_cb
};
let Some(current) = taffy_tree.parent(child) else { continue };
if current == target {
continue;
}
if taffy_tree.remove_child(current, child).is_ok() {
let _ = taffy_tree.add_child(target, child);
}
}
}
/// Does `id` have any direct child that is inline-level (a non-whitespace
/// text node, or an element whose resolved display is `Inline`)? Used to
/// decide whether a block container needs the flex-row-wrap approximation of
+3
View File
@@ -301,6 +301,9 @@ pub struct LayoutStyle {
// Positioning. `position: absolute|fixed` takes the box out of normal flow.
pub position: Option<taffy::Position>,
/// Distinguishes `fixed` from `absolute`; both map to taffy's absolute
/// layout mode, but fixed boxes use the initial containing block.
pub position_fixed: bool,
pub inset: [Option<Dimension>; 4], // top, right, bottom, left
/// `overflow`/-x/-y other than `visible`: clips this element's descendants
+16 -2
View File
@@ -434,8 +434,22 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
"flex" => parse_flex_shorthand(style, value),
"position" => {
match value {
"absolute" | "fixed" => style.position = Some(taffy::Position::Absolute),
"relative" | "sticky" | "static" => style.position = Some(taffy::Position::Relative),
"absolute" => {
style.position = Some(taffy::Position::Absolute);
style.position_fixed = false;
}
"fixed" => {
style.position = Some(taffy::Position::Absolute);
style.position_fixed = true;
}
"relative" | "sticky" => {
style.position = Some(taffy::Position::Relative);
style.position_fixed = false;
}
"static" => {
style.position = None;
style.position_fixed = false;
}
_ => {}
}
},
@@ -122,6 +122,45 @@ fn percentage_padding_top_reserves_aspect_ratio_box() {
);
}
#[test]
fn inset_absolute_uses_nearest_positioned_ancestor() {
let html = r##"
<body style="margin:0">
<div id="cb" style="position:relative;margin:40px 0 0 60px;width:400px;height:240px;padding:20px;border:10px solid black;box-sizing:border-box">
<div style="position:static;margin:50px 0 0 70px;width:120px;height:80px">
<span id="abs" style="position:absolute;left:15px;top:25px;width:80px;height:60px"></span>
<div id="abs-end" style="position:absolute;right:30px;bottom:20px;width:70px;height:50px"></div>
<div id="abs-percent" style="position:absolute;left:50%;top:50%;width:30px;height:30px"></div>
<div id="fixed" style="position:fixed;left:520px;top:40px;width:90px;height:55px"></div>
</div>
</div>
</body>
"##;
let tree = parse_html(html);
let layout = layout_dom(&tree, (900.0, 700.0));
let cb = layout.rects[&tree.get_element_by_id("cb").expect("containing block")];
let abs = layout.rects[&tree.get_element_by_id("abs").expect("absolute child")];
let abs_end = layout.rects[&tree.get_element_by_id("abs-end").expect("end-inset child")];
let abs_percent = layout.rects[&tree.get_element_by_id("abs-percent").expect("percent-inset child")];
let fixed = layout.rects[&tree.get_element_by_id("fixed").expect("fixed child")];
// Absolute insets are measured from the positioned ancestor's padding
// edge, not from the intervening static wrapper.
assert!((abs.x - cb.x - 25.0).abs() < 1.0, "wrong absolute x: cb={cb:?} abs={abs:?}");
assert!((abs.y - cb.y - 35.0).abs() < 1.0, "wrong absolute y: cb={cb:?} abs={abs:?}");
assert_eq!(
layout.styles[&tree.get_element_by_id("abs").unwrap()].display,
obscura_render::Display::Block,
"positioned inline should be blockified"
);
assert!((abs_end.x - 350.0).abs() < 1.0, "wrong right-inset x: {abs_end:?}");
assert!((abs_end.y - 200.0).abs() < 1.0, "wrong bottom-inset y: {abs_end:?}");
assert!((abs_percent.x - 260.0).abs() < 1.0, "wrong percent-inset x: {abs_percent:?}");
assert!((abs_percent.y - 160.0).abs() < 1.0, "wrong percent-inset y: {abs_percent:?}");
assert!((fixed.x - 520.0).abs() < 1.0, "fixed box did not use viewport x: {fixed:?}");
assert!((fixed.y - 40.0).abs() < 1.0, "fixed box did not use viewport y: {fixed:?}");
}
#[test]
fn long_text_run_wraps_across_multiple_lines() {
// A long single text node with no inline elements breaking it up must