render: honor modern containing-block triggers

Track transform, filter, backdrop-filter, perspective, containment,
will-change, and content-visibility triggers independently. Use the combined
state for absolute and fixed containing-block ancestry.
This commit is contained in:
SGavrl committed 2026-07-23 12:02:55 +00:00
1 parent b44e59ef71
commit 7827a6a788
4 files changed
+124 -8

No files matched your search

+3 -2
View File
@@ -1190,12 +1190,13 @@ fn reparent_inset_positioned_nodes(
// boxes capture absolute descendants; only transformed boxes capture
// fixed descendants. The full walk stays O(n).
let own_box = reverse.get(&dom_id).copied();
let abs_child_cb = if style.position.is_some() || style.transform_establishes_containing_block {
let establishes_cb = style.establishes_positioning_containing_block();
let abs_child_cb = if style.position.is_some() || establishes_cb {
own_box.unwrap_or(inherited_abs_cb)
} else {
inherited_abs_cb
};
let fixed_child_cb = if style.transform_establishes_containing_block {
let fixed_child_cb = if establishes_cb {
own_box.unwrap_or(inherited_fixed_cb)
} else {
inherited_fixed_cb
+18 -4
View File
@@ -415,10 +415,10 @@ pub struct LayoutStyle {
/// off-screen skip-link out of view instead of painting it on-screen. Not
/// inherited (transform is a non-inherited property).
pub transform_translate: Option<(Dimension, Dimension)>,
/// Any computed transform other than `none` establishes containing blocks
/// for absolute and fixed descendants, even when its visual transform
/// function is not yet supported by the paint engine.
pub transform_establishes_containing_block: bool,
/// Independent CSS-property triggers that establish containing blocks for
/// absolute and fixed descendants. Kept as a bitset so `filter:none`
/// cannot clear a transform/containment trigger from another property.
pub containing_block_triggers: u16,
/// `transform: scale(sx[, sy])` / `scaleX` / `scaleY`, captured as (sx, sy).
/// Parsed and stored so a value that mixes scale with translate still yields
/// its translate part; scale is not yet folded into paint geometry (doing so
@@ -432,6 +432,20 @@ pub struct LayoutStyle {
pub box_shadow: Option<BoxShadow>,
}
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;
pub(crate) const CB_TRIGGER_PERSPECTIVE: u16 = 1 << 3;
pub(crate) const CB_TRIGGER_CONTAIN: u16 = 1 << 4;
pub(crate) const CB_TRIGGER_WILL_CHANGE: u16 = 1 << 5;
pub(crate) const CB_TRIGGER_CONTENT_VISIBILITY: u16 = 1 << 6;
impl LayoutStyle {
pub(crate) fn establishes_positioning_containing_block(&self) -> bool {
self.containing_block_triggers != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Float {
Left,
+69 -2
View File
@@ -599,6 +599,39 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
"grid-column" => set_grid_placement(style, value, true),
"grid-row" => set_grid_placement(style, value, false),
"transform" => parse_transform(style, value),
"filter" => set_containing_block_trigger(
style,
crate::CB_TRIGGER_FILTER,
non_none_value(value),
),
"backdrop-filter" | "-webkit-backdrop-filter" => set_containing_block_trigger(
style,
crate::CB_TRIGGER_BACKDROP_FILTER,
non_none_value(value),
),
"perspective" => set_containing_block_trigger(
style,
crate::CB_TRIGGER_PERSPECTIVE,
non_none_value(value),
),
"contain" => {
let establishes = value
.split_whitespace()
.any(|v| matches!(v.to_ascii_lowercase().as_str(), "layout" | "paint" | "strict" | "content"));
set_containing_block_trigger(style, crate::CB_TRIGGER_CONTAIN, establishes);
}
"will-change" => {
let establishes = value
.split([',', ' '])
.map(str::trim)
.any(|v| matches!(v.to_ascii_lowercase().as_str(), "transform" | "filter" | "backdrop-filter" | "perspective" | "contain"));
set_containing_block_trigger(style, crate::CB_TRIGGER_WILL_CHANGE, establishes);
}
"content-visibility" => set_containing_block_trigger(
style,
crate::CB_TRIGGER_CONTENT_VISIBILITY,
value.trim().eq_ignore_ascii_case("auto"),
),
"box-shadow" | "-webkit-box-shadow" => {
style.box_shadow = parse_box_shadow(value, style.color);
}
@@ -623,14 +656,14 @@ fn parse_transform(style: &mut LayoutStyle, value: &str) {
if v.eq_ignore_ascii_case("none") {
style.transform_translate = None;
style.transform_scale = None;
style.transform_establishes_containing_block = false;
set_containing_block_trigger(style, crate::CB_TRIGGER_TRANSFORM, false);
return;
}
let functions = transform_functions(v);
if functions.is_empty() {
return;
}
style.transform_establishes_containing_block = true;
set_containing_block_trigger(style, crate::CB_TRIGGER_TRANSFORM, true);
let zero = crate::Dimension::Px(0.0);
let (mut tx, mut ty): (Option<crate::Dimension>, Option<crate::Dimension>) = (None, None);
let (mut sx, mut sy): (Option<f32>, Option<f32>) = (None, None);
@@ -686,6 +719,19 @@ fn parse_transform(style: &mut LayoutStyle, value: &str) {
}
}
fn non_none_value(value: &str) -> bool {
let value = value.trim();
!value.is_empty() && !value.eq_ignore_ascii_case("none")
}
fn set_containing_block_trigger(style: &mut LayoutStyle, trigger: u16, enabled: bool) {
if enabled {
style.containing_block_triggers |= trigger;
} else {
style.containing_block_triggers &= !trigger;
}
}
/// Split a `transform` value into its `name(args)` functions, in source order.
/// Tokenizes on parentheses (tracking depth so a nested `calc(...)` inside an
/// argument stays intact); the function name is the trailing identifier run
@@ -2098,6 +2144,27 @@ mod tests {
assert_eq!(s.height, crate::Dimension::Px(50.0));
}
#[test]
fn containing_block_property_triggers_are_independent() {
let s = compute_style("div", Some("transform:rotate(0deg);filter:none"));
assert!(s.establishes_positioning_containing_block());
let s = compute_style("div", Some("filter:blur(0);transform:none"));
assert!(s.establishes_positioning_containing_block());
let s = compute_style(
"div",
Some("contain:layout;content-visibility:visible;filter:none"),
);
assert!(s.establishes_positioning_containing_block());
let s = compute_style(
"div",
Some("contain:none;content-visibility:visible;filter:none;perspective:none"),
);
assert!(!s.establishes_positioning_containing_block());
}
#[test]
fn margin_shorthand_expands() {
let s = compute_style("div", Some("margin: 10px 20px"));
@@ -203,6 +203,40 @@ fn transforms_establish_absolute_and_fixed_containing_blocks() {
assert!((fixed_viewport.y - 40.0).abs() < 1.0, "positioned ancestor captured fixed y: {fixed_viewport:?}");
}
#[test]
fn modern_effects_establish_containing_blocks_independently() {
let tree = parse_html(include_str!("../../../render-repros/modern-containing-block-triggers.html"));
let layout = layout_dom(&tree, (900.0, 700.0));
let cases = [
("filter-cb", "filter-badge"),
("perspective-cb", "perspective-badge"),
("contain-cb", "contain-badge"),
("will-change-cb", "will-change-badge"),
("visibility-cb", "visibility-badge"),
];
for (cb_id, badge_id) in cases {
let cb = layout.rects[&tree.get_element_by_id(cb_id).unwrap()];
let badge = layout.rects[&tree.get_element_by_id(badge_id).unwrap()];
assert!((badge.x - cb.x - 75.0).abs() < 1.0, "{badge_id} wrong x: cb={cb:?} badge={badge:?}");
assert!((badge.y - cb.y - 55.0).abs() < 1.0, "{badge_id} wrong y: cb={cb:?} badge={badge:?}");
}
let filter_cb = layout.rects[&tree.get_element_by_id("filter-cb").unwrap()];
let filter_fixed = layout.rects[&tree.get_element_by_id("filter-fixed").unwrap()];
assert!((filter_fixed.x - filter_cb.x - 5.0).abs() < 1.0);
assert!((filter_fixed.y - filter_cb.y - 5.0).abs() < 1.0);
let contain_cb = layout.rects[&tree.get_element_by_id("contain-cb").unwrap()];
let contain_fixed = layout.rects[&tree.get_element_by_id("contain-fixed").unwrap()];
assert!((contain_fixed.x - contain_cb.x - 5.0).abs() < 1.0);
assert!((contain_fixed.y - contain_cb.y - 5.0).abs() < 1.0);
// Pinned Chromium 145 does not make container-type:inline-size a
// positioning containing block. Keep it as a negative control.
let container_badge = layout.rects[&tree.get_element_by_id("container-badge").unwrap()];
assert!((container_badge.x - 775.0).abs() < 1.0, "container-type control x: {container_badge:?}");
assert!((container_badge.y - 175.0).abs() < 1.0, "container-type control y: {container_badge:?}");
}
#[test]
fn long_text_run_wraps_across_multiple_lines() {
// A long single text node with no inline elements breaking it up must