fix(layout): paint flex and grid items atomically

This commit is contained in:
ldm0
2026-09-07 22:43:22 +08:00
committed by Donough Liu
parent be2c6882dd
commit acb59e7107
3 changed files with 151 additions and 52 deletions
+71 -52
View File
@@ -53,9 +53,15 @@ impl AtomicPaintEntry {
}
}
/// Paint level inherited while traversing a CSS atomic pseudo-context.
///
/// Floats, positioned boxes, and inline-level atomic boxes keep their
/// non-stacking descendants together at one parent-context paint level. Real
/// stacking contexts are still hoisted before this grouping is applied.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AtomicGroup {
Normal,
enum PaintGroup {
NormalFlow,
AtomicInline,
Float,
Positioned,
}
@@ -160,18 +166,15 @@ fn emit_context<N>(
fn collect_subtree<N>(
world: &LayoutWorld<N>,
id: LayoutBoxId,
inherited_group: Option<AtomicGroup>,
inherited_atomic_group: Option<PaintGroup>,
sequence: &mut usize,
collection: &mut ContextCollection,
) where
N: Copy + Debug + Eq + Hash,
{
let layout_box = &world.boxes[id.index()];
let parent_is_flex_or_grid = layout_box.parent.is_some_and(|parent| {
let display = world.boxes[parent.index()].style.display();
display.is_flex_container() || display.is_grid_container()
});
if layout_box.creates_stacking_context(false, parent_is_flex_or_grid) {
let is_flex_or_grid_item = is_flex_or_grid_item(world, id);
if layout_box.creates_stacking_context(false, is_flex_or_grid_item) {
let context = ChildContext {
id,
z_index: layout_box.style.explicit_z_index().unwrap_or(0),
@@ -187,15 +190,27 @@ fn collect_subtree<N>(
return;
}
let group = inherited_group.unwrap_or_else(|| {
if layout_box.style.position() != LayoutPosition::Static {
AtomicGroup::Positioned
} else if layout_box.style.is_floated() {
AtomicGroup::Float
} else {
AtomicGroup::Normal
}
});
// Positioned descendants escape the pseudo-context of an atomic inline or
// float and participate in the nearest real stacking context. Resolve
// that level before inheriting the atomic group; ordinary descendants
// remain inside their atomic ancestor.
let group = if layout_box.style.position() != LayoutPosition::Static {
PaintGroup::Positioned
} else {
inherited_atomic_group.unwrap_or_else(|| {
if is_flex_or_grid_item {
// CSS Flexbox/Grid paint each item as an atomic inline-level box.
// Chromium carries the same boundary as IsPaintedAtomically on
// the item's constraint space. Floats do not apply to flex/grid
// items, so this classification precedes the float level.
PaintGroup::AtomicInline
} else if layout_box.style.is_floated() {
PaintGroup::Float
} else {
PaintGroup::NormalFlow
}
})
};
push_unit(
collection,
group,
@@ -207,66 +222,70 @@ fn collect_subtree<N>(
);
push_unit(
collection,
if group == AtomicGroup::Normal {
AtomicGroup::Normal
} else {
group
},
group,
PaintUnit {
id,
kind: UnitKind::Contents,
sequence: next_sequence(sequence),
},
);
// Floats and positioned descendants are painted atomically at their
// ancestor's paint level. Ordinary in-flow descendants are not: each child
// must still classify itself as normal, floating, or positioned. Carrying
// `Normal` down here would incorrectly bury a positioned grandchild in the
// block-background/inline-content buckets.
// Atomic pseudo-context descendants inherit their ancestor's paint level.
// Ordinary in-flow descendants do not: each child must classify itself as
// normal, floating, atomic-inline, or positioned. Positioned descendants
// override an inherited pseudo-context at the start of this function.
let descendant_group = match group {
AtomicGroup::Normal => None,
AtomicGroup::Float | AtomicGroup::Positioned => Some(group),
PaintGroup::NormalFlow => None,
PaintGroup::AtomicInline | PaintGroup::Float | PaintGroup::Positioned => Some(group),
};
for child in ordered_children(world, id) {
collect_subtree(world, child, descendant_group, sequence, collection);
}
if layout_box.collapsed_table_borders.is_some() {
let unit = PaintUnit {
push_unit(
collection,
group,
PaintUnit {
id,
kind: UnitKind::TableCollapsedBorders,
sequence: next_sequence(sequence),
},
);
}
push_unit(
collection,
group,
PaintUnit {
id,
kind: UnitKind::TableCollapsedBorders,
kind: UnitKind::Outline,
sequence: next_sequence(sequence),
};
match group {
AtomicGroup::Normal => collection.table_collapsed_borders.push(unit),
AtomicGroup::Float => collection.floats.push(unit),
AtomicGroup::Positioned => collection.positioned.push(AtomicPaintEntry::Unit(unit)),
}
}
let outline = PaintUnit {
id,
kind: UnitKind::Outline,
sequence: next_sequence(sequence),
};
match group {
AtomicGroup::Normal => collection.outlines.push(outline),
AtomicGroup::Float => collection.floats.push(outline),
AtomicGroup::Positioned => collection.positioned.push(AtomicPaintEntry::Unit(outline)),
}
},
);
}
fn push_unit(collection: &mut ContextCollection, group: AtomicGroup, unit: PaintUnit) {
fn push_unit(collection: &mut ContextCollection, group: PaintGroup, unit: PaintUnit) {
match group {
AtomicGroup::Normal => match unit.kind {
PaintGroup::NormalFlow => match unit.kind {
UnitKind::Background => collection.block_backgrounds.push(unit),
UnitKind::TableCollapsedBorders => collection.table_collapsed_borders.push(unit),
UnitKind::Contents => collection.inline_contents.push(unit),
UnitKind::Outline => collection.outlines.push(unit),
},
AtomicGroup::Float => collection.floats.push(unit),
AtomicGroup::Positioned => collection.positioned.push(AtomicPaintEntry::Unit(unit)),
PaintGroup::AtomicInline => collection.inline_contents.push(unit),
PaintGroup::Float => collection.floats.push(unit),
PaintGroup::Positioned => collection.positioned.push(AtomicPaintEntry::Unit(unit)),
}
}
fn is_flex_or_grid_item<N>(world: &LayoutWorld<N>, id: LayoutBoxId) -> bool
where
N: Copy + Debug + Eq + Hash,
{
world.boxes[id.index()].parent.is_some_and(|parent| {
let display = world.boxes[parent.index()].style.display();
display.is_flex_container() || display.is_grid_container()
})
}
fn emit_units(units: Vec<PaintUnit>, events: &mut Vec<PaintOrderEvent>) {
for unit in units {
emit_unit(unit, events);
@@ -0,0 +1,79 @@
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn screenshot_paints_flex_and_grid_items_as_atomic_inline_level_boxes() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let mut page_vm = test_page_vm_with_loader_and_document_url(
&loader,
Vec::new(),
Url::parse("https://example.com/flex-grid-atomic-paint.html")?,
);
page_vm.vm_mut().eval(
r#"
document.head.innerHTML = `<style>
html,body{margin:0;background:white}
.case{width:100px;height:100px;margin-bottom:10px}
.grid,.inline-grid,.ordered,.stacked,.positioned-descendant{grid-template:100px/100px}
.grid,.ordered,.stacked,.positioned-descendant{display:grid}
.inline-grid{display:inline-grid}
.grid>*,.inline-grid>*,.ordered>*,.stacked>*,.positioned-descendant>*{grid-area:1/1;width:100px;height:100px}
.flex{display:flex}
.flex>*{flex:none;width:100px;height:100px}
.flex>.cover{margin-left:-100px}
.cover{background:green}
.ordered>.cover{order:1}
.ordered>.content{order:0}
.stacked>.content{z-index:1}
.positioned-descendant .elevated{position:relative;width:100px;height:100px;background:red}
</style>`;
const content = className => `<svg class="${className}" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="red"/></svg>`;
document.body.innerHTML = `
<div class="case grid">${content('content')}<div class=cover></div></div>
<div class="case inline-grid">${content('content')}<div class=cover></div></div>
<div class="case flex">${content('content')}<div class=cover></div></div>
<div class="case ordered"><div class=cover></div>${content('content')}</div>
<div class="case stacked">${content('content')}<div class=cover></div></div>
<div class="case positioned-descendant"><div><div class=elevated></div></div><div class=cover></div></div>`;
'installed'
"#,
)?;
page_vm.vm_mut().sync_live_document_style_sources();
let snapshot = page_vm
.vm_mut()
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(120, 660, 1.0))?
.expect("flex/grid atomic-paint fixture should retain a layout root");
let raster = moli_paint::raster_snapshot(&snapshot)?;
let pixel = |x: u32, y: u32| {
let index = ((y * raster.width + x) * 4) as usize;
<[u8; 4]>::try_from(&raster.rgba[index..index + 4]).expect("RGBA pixel")
};
for (label, y) in [
("grid item", 50),
("inline-grid item", 160),
("flex item", 270),
("order-modified grid item", 380),
] {
assert_eq!(
pixel(50, y),
[0, 128, 0, 255],
"{label} descendants must not escape the item's atomic paint boundary",
);
}
assert_eq!(
pixel(50, 490),
[255, 0, 0, 255],
"a non-auto z-index on a static grid item must still establish a stacking context",
);
assert_eq!(
pixel(50, 600),
[255, 0, 0, 255],
"a positioned descendant of an atomic grid item must participate in the parent stacking context",
);
Ok::<_, anyhow::Error>(())
})
.await
.expect("flex/grid atomic-paint fixture should run");
}
@@ -125,6 +125,7 @@ mod fetch_xhr;
mod file_entry_file_callback;
mod file_system_directory_reader;
mod grid_item_box_generation;
mod grid_item_paint_order;
mod grid_resolved_track_values;
mod hash_change_delivery;
mod history_traversal;