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:
1 parent
b44e59ef71
commit
7827a6a788
4 files changed
+124
-8
No files matched your search
@@ -1190,12 +1190,13 @@ fn reparent_inset_positioned_nodes(
|
|||||||
// boxes capture absolute descendants; only transformed boxes capture
|
// boxes capture absolute descendants; only transformed boxes capture
|
||||||
// fixed descendants. The full walk stays O(n).
|
// fixed descendants. The full walk stays O(n).
|
||||||
let own_box = reverse.get(&dom_id).copied();
|
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)
|
own_box.unwrap_or(inherited_abs_cb)
|
||||||
} else {
|
} else {
|
||||||
inherited_abs_cb
|
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)
|
own_box.unwrap_or(inherited_fixed_cb)
|
||||||
} else {
|
} else {
|
||||||
inherited_fixed_cb
|
inherited_fixed_cb
|
||||||
|
|||||||
@@ -415,10 +415,10 @@ pub struct LayoutStyle {
|
|||||||
/// off-screen skip-link out of view instead of painting it on-screen. Not
|
/// off-screen skip-link out of view instead of painting it on-screen. Not
|
||||||
/// inherited (transform is a non-inherited property).
|
/// inherited (transform is a non-inherited property).
|
||||||
pub transform_translate: Option<(Dimension, Dimension)>,
|
pub transform_translate: Option<(Dimension, Dimension)>,
|
||||||
/// Any computed transform other than `none` establishes containing blocks
|
/// Independent CSS-property triggers that establish containing blocks for
|
||||||
/// for absolute and fixed descendants, even when its visual transform
|
/// absolute and fixed descendants. Kept as a bitset so `filter:none`
|
||||||
/// function is not yet supported by the paint engine.
|
/// cannot clear a transform/containment trigger from another property.
|
||||||
pub transform_establishes_containing_block: bool,
|
pub containing_block_triggers: u16,
|
||||||
/// `transform: scale(sx[, sy])` / `scaleX` / `scaleY`, captured as (sx, sy).
|
/// `transform: scale(sx[, sy])` / `scaleX` / `scaleY`, captured as (sx, sy).
|
||||||
/// Parsed and stored so a value that mixes scale with translate still yields
|
/// 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
|
/// 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 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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Float {
|
pub enum Float {
|
||||||
Left,
|
Left,
|
||||||
|
|||||||
@@ -599,6 +599,39 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
"grid-column" => set_grid_placement(style, value, true),
|
"grid-column" => set_grid_placement(style, value, true),
|
||||||
"grid-row" => set_grid_placement(style, value, false),
|
"grid-row" => set_grid_placement(style, value, false),
|
||||||
"transform" => parse_transform(style, value),
|
"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" => {
|
"box-shadow" | "-webkit-box-shadow" => {
|
||||||
style.box_shadow = parse_box_shadow(value, style.color);
|
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") {
|
if v.eq_ignore_ascii_case("none") {
|
||||||
style.transform_translate = None;
|
style.transform_translate = None;
|
||||||
style.transform_scale = None;
|
style.transform_scale = None;
|
||||||
style.transform_establishes_containing_block = false;
|
set_containing_block_trigger(style, crate::CB_TRIGGER_TRANSFORM, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let functions = transform_functions(v);
|
let functions = transform_functions(v);
|
||||||
if functions.is_empty() {
|
if functions.is_empty() {
|
||||||
return;
|
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 zero = crate::Dimension::Px(0.0);
|
||||||
let (mut tx, mut ty): (Option<crate::Dimension>, Option<crate::Dimension>) = (None, None);
|
let (mut tx, mut ty): (Option<crate::Dimension>, Option<crate::Dimension>) = (None, None);
|
||||||
let (mut sx, mut sy): (Option<f32>, Option<f32>) = (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.
|
/// Split a `transform` value into its `name(args)` functions, in source order.
|
||||||
/// Tokenizes on parentheses (tracking depth so a nested `calc(...)` inside an
|
/// Tokenizes on parentheses (tracking depth so a nested `calc(...)` inside an
|
||||||
/// argument stays intact); the function name is the trailing identifier run
|
/// 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));
|
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]
|
#[test]
|
||||||
fn margin_shorthand_expands() {
|
fn margin_shorthand_expands() {
|
||||||
let s = compute_style("div", Some("margin: 10px 20px"));
|
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:?}");
|
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]
|
#[test]
|
||||||
fn long_text_run_wraps_across_multiple_lines() {
|
fn long_text_run_wraps_across_multiple_lines() {
|
||||||
// A long single text node with no inline elements breaking it up must
|
// A long single text node with no inline elements breaking it up must
|
||||||
|
|||||||
Reference in new issue
Block a user