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
2026-07-16 13:10:28 +00:00
parent 953292563d
commit 22f33e4dec
4 changed files with 93 additions and 2 deletions

View File

@@ -2250,9 +2250,24 @@ fn build_children_with_float_zone(
// approximate bound beats an unbounded one.
let float_height_budget = estimate_float_height(tree, dom_children[float_idx], styles);
const ASSUMED_FLOW_WIDTH: f32 = 500.0;
// `clear` on a sibling ends the zone: the cleared element moves below the
// float (the clearfix idiom), so it must not join the flow column beside it.
let clears_this_float = |cid: NodeId| {
let Some(c) = styles.get(&cid).and_then(|s| s.clear) else { return false };
match (float_side, c) {
(_, crate::Clear::Both) => true,
(Some(crate::Float::Left), crate::Clear::Left) => true,
(Some(crate::Float::Right), crate::Clear::Right) => true,
_ => false,
}
};
let mut zone_end = float_idx + 1;
let mut flow_height_estimate = 0.0f32;
while zone_end < dom_children.len() && !is_heading(dom_children[zone_end]) && !is_float(dom_children[zone_end]) {
while zone_end < dom_children.len()
&& !is_heading(dom_children[zone_end])
&& !is_float(dom_children[zone_end])
&& !clears_this_float(dom_children[zone_end])
{
flow_height_estimate += estimate_text_height(tree, dom_children[zone_end], styles, ASSUMED_FLOW_WIDTH);
zone_end += 1;
if flow_height_estimate >= float_height_budget {

View File

@@ -325,6 +325,13 @@ pub struct LayoutStyle {
/// cells inherit it); obscura applies it as main-axis alignment of the
/// cell's flex-column stand-in. `None` on non-cell elements.
pub vertical_align: Option<VerticalAlign>,
/// `z-index` on a positioned element. `None` is `auto` (tree order). A
/// non-zero value lifts the element's whole subtree into a separate paint
/// layer: negatives under the normal flow, positives above it, sorted.
pub z_index: Option<i32>,
/// `clear`, when set: this element moves below preceding floats on the
/// given side(s), ending their float zone.
pub clear: Option<Clear>,
/// Resolved during the inheritance pass: true when this element should
/// not be painted at all, either from its own or an inherited
/// `visibility: hidden`, or because the product of its own and every
@@ -463,6 +470,15 @@ pub enum VerticalAlign {
Bottom,
}
/// `clear`: which floated side(s) an element moves below. Ends the float
/// zone in `dom::build_children_with_float_zone` (the clearfix idiom).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Clear {
Left,
Right,
Both,
}
/// `line-height`: `normal` (a font-relative default), a unitless multiple of
/// font-size, or an absolute pixel length.
#[derive(Debug, Clone, Copy, PartialEq)]

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;
}

View File

@@ -442,6 +442,20 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
}
"visibility" => style.visibility_hidden = Some(value.eq_ignore_ascii_case("hidden")),
"opacity" => style.opacity = value.trim().parse::<f32>().ok(),
"z-index" => {
style.z_index = match value.trim() {
"auto" | "inherit" | "initial" => None,
v => v.parse::<i32>().ok(),
};
}
"clear" => {
style.clear = match value.trim().to_ascii_lowercase().as_str() {
"left" | "inline-start" => Some(crate::Clear::Left),
"right" | "inline-end" => Some(crate::Clear::Right),
"both" => Some(crate::Clear::Both),
_ => None,
};
}
"vertical-align" => {
style.vertical_align = match value.trim().to_ascii_lowercase().as_str() {
"top" | "baseline" | "text-top" => Some(crate::VerticalAlign::Top),