render: z-index paint layers and clear ending float zones

Paint order was pure tree order, so a dropdown/overlay declared before a
later sibling lost to it and a z:-1 decorative backdrop covered its own
content. A positioned element with a non-zero z-index now lifts its
whole subtree into a separate paint layer: negative layers under the
normal flow, positive above it, each sorted ascending with tree order
preserved within equal z. Nested z roots paint inside their ancestor
root's subtree in tree order, the pragmatic core of CSS stacking
contexts.

clear (left/right/both, previously unparsed) now ends the float zone,
so a clearfix element drops below the float at full width instead of
joining the flow column beside it.

Repro-verified: an earlier z:10 overlay paints over a later sibling, a
later z:-1 backdrop paints under earlier content, and the clear repro
matches Chromium within 3px on every measure. Spot set unchanged or
slightly improved; obstacle course 33/33.
This commit is contained in:
SGavrl committed 2026-07-16 13:10:28 +00:00
1 parent 953292563d
commit 22f33e4dec
4 files changed
+93 -2

No files matched your search

+47 -1
View File
@@ -49,8 +49,54 @@ pub fn paint_dom(tree: &DomTree, viewport: (f32, f32), base_url: Option<&str>) -
// zero, so skip the per-node ancestor walk entirely and keep the paint
// path free of any added cost.
// Tree order so later elements paint over earlier ones (normal flow).
// Paint order: tree order for the normal flow (later elements paint over
// earlier ones), except that a positioned element with a non-zero
// z-index lifts its whole subtree into a separate layer: negative layers
// paint under the normal flow, positive ones above it, each sorted by
// z-index ascending (stable, so equal z keeps tree order). This is the
// pragmatic core of CSS stacking contexts: dropdowns/overlays/badges
// (z>0) stop losing to later siblings, and z:-1 decorative backdrops
// stop covering their content. Nested z roots paint inside their
// ancestor root's subtree in tree order.
let mut neg_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
let mut pos_layers: Vec<(i32, Vec<obscura_dom::tree::NodeId>)> = Vec::new();
let mut normal: Vec<obscura_dom::tree::NodeId> = Vec::new();
let mut consumed: std::collections::HashSet<obscura_dom::tree::NodeId> = std::collections::HashSet::new();
for nid in tree.descendants(tree.document()) {
if consumed.contains(&nid) {
continue;
}
let z = laid
.styles
.get(&nid)
.filter(|s| s.position.is_some())
.and_then(|s| s.z_index)
.filter(|&z| z != 0);
if let Some(z) = z {
let mut sub = vec![nid];
sub.extend(tree.descendants(nid));
for &m in &sub {
consumed.insert(m);
}
if z < 0 {
neg_layers.push((z, sub));
} else {
pos_layers.push((z, sub));
}
} else {
normal.push(nid);
}
}
neg_layers.sort_by_key(|(z, _)| *z);
pos_layers.sort_by_key(|(z, _)| *z);
let paint_order: Vec<obscura_dom::tree::NodeId> = neg_layers
.into_iter()
.flat_map(|(_, sub)| sub)
.chain(normal)
.chain(pos_layers.into_iter().flat_map(|(_, sub)| sub))
.collect();
for nid in paint_order {
if svg_subtree_skip.contains(&nid) {
continue;
}