Improve logic for establishing a stacking context (#35947)

In particular:
 - `z-index` will now work on unpositioned grid items.
 - `will-change: z-index` will only establish a stacking context if
   `z-index` applies, i.e. if the box is positioned or a flex/grid item.
 - The conditions in `establishes_stacking_context()` are reordered,
   so that the most likely ones are checked first.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-03-13 07:49:08 +01:00 committed by GitHub
parent 205b97d5ed
commit f93006af95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 233 additions and 114 deletions

View file

@ -257,7 +257,9 @@ impl FlexLineItem<'_> {
} }
let mut fragment_info = self.item.box_.base_fragment_info(); let mut fragment_info = self.item.box_.base_fragment_info();
fragment_info.flags.insert(FragmentFlags::IS_FLEX_ITEM); fragment_info
.flags
.insert(FragmentFlags::IS_FLEX_OR_GRID_ITEM);
if self.item.depends_on_block_constraints { if self.item.depends_on_block_constraints {
fragment_info.flags.insert( fragment_info.flags.insert(
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,

View file

@ -80,8 +80,8 @@ bitflags! {
const IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT = 1 << 0; const IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT = 1 << 0;
/// Whether or not the node that created this Fragment is a `<br>` element. /// Whether or not the node that created this Fragment is a `<br>` element.
const IS_BR_ELEMENT = 1 << 1; const IS_BR_ELEMENT = 1 << 1;
/// Whether or not this Fragment is a flex item. /// Whether or not this Fragment is a flex item or a grid item.
const IS_FLEX_ITEM = 1 << 2; const IS_FLEX_OR_GRID_ITEM = 1 << 2;
/// Whether or not this Fragment was created to contain a replaced element or is /// Whether or not this Fragment was created to contain a replaced element or is
/// a replaced element. /// a replaced element.
const IS_REPLACED = 1 << 3; const IS_REPLACED = 1 << 3;

View file

@ -301,6 +301,7 @@ pub(crate) trait ComputedValuesExt {
) -> LogicalSides<LengthPercentageOrAuto<'_>>; ) -> LogicalSides<LengthPercentageOrAuto<'_>>;
fn is_transformable(&self, fragment_flags: FragmentFlags) -> bool; fn is_transformable(&self, fragment_flags: FragmentFlags) -> bool;
fn has_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool; fn has_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool;
fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool;
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32; fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32;
fn effective_overflow(&self, fragment_flags: FragmentFlags) -> AxesOverflow; fn effective_overflow(&self, fragment_flags: FragmentFlags) -> AxesOverflow;
fn establishes_block_formatting_context(&self, fragment_flags: FragmentFlags) -> bool; fn establishes_block_formatting_context(&self, fragment_flags: FragmentFlags) -> bool;
@ -503,18 +504,35 @@ impl ComputedValuesExt for ComputedValues {
self.get_box().perspective != Perspective::None) self.get_box().perspective != Perspective::None)
} }
/// Get the effective z-index of this fragment. Z-indices only apply to positioned elements /// Whether the `z-index` property applies to this fragment.
/// per CSS 2 9.9.1 (<http://www.w3.org/TR/CSS2/visuren.html#z-index>), so this value may differ fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool {
/// from the value specified in the style. // As per CSS 2 § 9.9.1, `z-index` applies to positioned elements.
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32 { // <http://www.w3.org/TR/CSS2/visuren.html#z-index>
// From <https://drafts.csswg.org/css-flexbox/#painting>: if self.get_box().position != ComputedPosition::Static {
return true;
}
// More modern specs also apply it to flex and grid items.
// - From <https://www.w3.org/TR/css-flexbox-1/#painting>:
// > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified // > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified
// > document order is used in place of raw document order, and z-index values other than auto // > document order is used in place of raw document order, and z-index values other than auto
// > create a stacking context even if position is static (behaving exactly as if position // > create a stacking context even if position is static (behaving exactly as if position
// > were relative). // > were relative).
match self.get_box().position { // - From <https://drafts.csswg.org/css-flexbox/#painting>:
ComputedPosition::Static if !fragment_flags.contains(FragmentFlags::IS_FLEX_ITEM) => 0, // > The painting order of grid items is exactly the same as inline blocks [CSS2], except that
_ => self.get_position().z_index.integer_or(0), // > order-modified document order is used in place of raw document order, and z-index values
// > other than auto create a stacking context even if position is static (behaving exactly
// > as if position were relative).
fragment_flags.contains(FragmentFlags::IS_FLEX_OR_GRID_ITEM)
}
/// Get the effective z-index of this fragment. Z-indices only apply to positioned elements
/// per CSS 2 9.9.1 (<http://www.w3.org/TR/CSS2/visuren.html#z-index>), so this value may differ
/// from the value specified in the style.
fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32 {
if self.z_index_applies(fragment_flags) {
self.get_position().z_index.integer_or(0)
} else {
0
} }
} }
@ -617,78 +635,92 @@ impl ComputedValuesExt for ComputedValues {
/// Returns true if this fragment establishes a new stacking context and false otherwise. /// Returns true if this fragment establishes a new stacking context and false otherwise.
fn establishes_stacking_context(&self, fragment_flags: FragmentFlags) -> bool { fn establishes_stacking_context(&self, fragment_flags: FragmentFlags) -> bool {
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>:
// > If any non-initial value of a property would create a stacking context on the element,
// > specifying that property in will-change must create a stacking context on the element.
let will_change_bits = self.clone_will_change().bits;
if will_change_bits
.intersects(WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::OPACITY)
{
return true;
}
// From <https://www.w3.org/TR/CSS2/visuren.html#z-index>, values different than `auto`
// make the box establish a stacking context.
if self.z_index_applies(fragment_flags) &&
(!self.get_position().z_index.is_auto() ||
will_change_bits.intersects(WillChangeBits::Z_INDEX))
{
return true;
}
// Fixed position and sticky position always create stacking contexts.
// Note `will-change: position` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if matches!(
self.get_box().position,
ComputedPosition::Fixed | ComputedPosition::Sticky
) {
return true;
}
// From <https://www.w3.org/TR/css-transforms-1/#transform-rendering>
// > For elements whose layout is governed by the CSS box model, any value other than
// > `none` for the `transform` property results in the creation of a stacking context.
// From <https://www.w3.org/TR/css-transforms-2/#transform-style-property>
// > A computed value of `preserve-3d` for `transform-style` on a transformable element
// > establishes both a stacking context and a containing block for all descendants.
// From <https://www.w3.org/TR/css-transforms-2/#perspective-property>
// > any value other than none establishes a stacking context.
// TODO: handle individual transform properties (`translate`, `scale` and `rotate`).
// <https://www.w3.org/TR/css-transforms-2/#individual-transforms>
if self.is_transformable(fragment_flags) &&
(!self.get_box().transform.0.is_empty() ||
self.get_box().transform_style == ComputedTransformStyle::Preserve3d ||
self.get_box().perspective != Perspective::None ||
will_change_bits
.intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE))
{
return true;
}
// From <https://www.w3.org/TR/css-color-3/#transparency>
// > implementations must create a new stacking context for any element with opacity less than 1.
// Note `will-change: opacity` is handled above by `WillChangeBits::OPACITY`.
let effects = self.get_effects(); let effects = self.get_effects();
if effects.opacity != 1.0 { if effects.opacity != 1.0 {
return true; return true;
} }
if effects.mix_blend_mode != ComputedMixBlendMode::Normal { // From <https://www.w3.org/TR/filter-effects-1/#FilterProperty>
return true; // > A computed value of other than `none` results in the creation of a stacking context
} // Note `will-change: filter` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if !effects.filter.0.is_empty() { if !effects.filter.0.is_empty() {
return true; return true;
} }
if self.has_transform_or_perspective(fragment_flags) { // From <https://www.w3.org/TR/compositing-1/#mix-blend-mode>
return true; // > Applying a blendmode other than `normal` to the element must establish a new stacking context
} // Note `will-change: mix-blend-mode` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if effects.mix_blend_mode != ComputedMixBlendMode::Normal {
// See <https://drafts.csswg.org/css-transforms-2/#transform-style-property>.
if self.is_transformable(fragment_flags) &&
self.get_box().transform_style == ComputedTransformStyle::Preserve3d
{
return true;
}
if self.get_box().isolation == ComputedIsolation::Isolate {
return true;
}
// Fixed position and sticky position always create stacking contexts.
// TODO(mrobinson): We need to handle sticky positioning here when we support it.
if self.get_box().position == ComputedPosition::Fixed {
return true; return true;
} }
// From <https://www.w3.org/TR/css-masking-1/#the-clip-path>
// > A computed value of other than `none` results in the creation of a stacking context.
// Note `will-change: clip-path` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
if self.get_svg().clip_path != ClipPath::None { if self.get_svg().clip_path != ClipPath::None {
return true; return true;
} }
// From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>: // From <https://www.w3.org/TR/compositing-1/#isolation>
// > If any non-initial value of a property would create a stacking context on the element, // > For CSS, setting `isolation` to `isolate` will turn the element into a stacking context.
// > specifying that property in will-change must create a stacking context on the element. // Note `will-change: isolation` is handled above by `STACKING_CONTEXT_UNCONDITIONAL`.
let will_change_bits = self.clone_will_change().bits; if self.get_box().isolation == ComputedIsolation::Isolate {
if will_change_bits.intersects(
WillChangeBits::OPACITY |
WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL |
WillChangeBits::Z_INDEX,
) || (will_change_bits
.intersects(WillChangeBits::PERSPECTIVE | WillChangeBits::TRANSFORM) &&
self.is_transformable(fragment_flags))
{
return true; return true;
} }
// Statically positioned fragments don't establish stacking contexts if the previous // TODO: We need to handle CSS Contain here.
// conditions are not fulfilled. Furthermore, z-index doesn't apply to statically false
// positioned fragments (except for flex items, see below).
//
// From <https://drafts.csswg.org/css-flexbox/#painting>:
// > Flex items paint exactly the same as inline blocks [CSS2], except that order-modified
// > document order is used in place of raw document order, and z-index values other than auto
// > create a stacking context even if position is static (behaving exactly as if position
// > were relative).
if self.get_box().position == ComputedPosition::Static &&
!fragment_flags.contains(FragmentFlags::IS_FLEX_ITEM)
{
return false;
}
// For absolutely and relatively positioned fragments we only establish a stacking
// context if there is a z-index set.
// See https://www.w3.org/TR/CSS2/visuren.html#z-index
!self.get_position().z_index.is_auto()
} }
/// Returns true if this style establishes a containing block for absolute /// Returns true if this style establishes a containing block for absolute

View file

@ -20,7 +20,9 @@ use crate::formatting_contexts::{
Baselines, IndependentFormattingContext, IndependentFormattingContextContents, Baselines, IndependentFormattingContext, IndependentFormattingContextContents,
IndependentLayout, IndependentLayout,
}; };
use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, Fragment, SpecificLayoutInfo}; use crate::fragment_tree::{
BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo,
};
use crate::geom::{ use crate::geom::{
LogicalSides, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size, LogicalSides, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size,
SizeConstraint, Sizes, SizeConstraint, Sizes,
@ -560,8 +562,12 @@ impl TaffyContainer {
match &mut child.taffy_level_box { match &mut child.taffy_level_box {
TaffyItemBoxInner::InFlowBox(independent_box) => { TaffyItemBoxInner::InFlowBox(independent_box) => {
let mut fragment_info = independent_box.base_fragment_info();
fragment_info
.flags
.insert(FragmentFlags::IS_FLEX_OR_GRID_ITEM);
let mut box_fragment = BoxFragment::new( let mut box_fragment = BoxFragment::new(
independent_box.base_fragment_info(), fragment_info,
independent_box.style().clone(), independent_box.style().clone(),
std::mem::take(&mut child.child_fragments), std::mem::take(&mut child.child_fragments),
content_size, content_size,

View file

@ -307667,6 +307667,45 @@
{} {}
] ]
], ],
"will-change-stacking-context-z-index-2.html": [
"9379185048a78b031e8ac290bd7bb3e5df24fa5f",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"will-change-stacking-context-z-index-3.html": [
"4ec40ae1a430a095ea6f703c8c27698e04792220",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"will-change-stacking-context-z-index-4.html": [
"d8a87b23415c3c10a6f4875f16cd63301e789152",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square.xht",
"=="
]
],
{}
]
],
"will-change-transform-add-content.html": [ "will-change-transform-add-content.html": [
"1d8568ee629e48adfe99abab2e2194a73ae1301a", "1d8568ee629e48adfe99abab2e2194a73ae1301a",
[ [

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-overlapped-items-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-overlapped-items-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-overlapped-items-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-overlapped-items-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-overlapped-items-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-inline-z-axis-ordering-overlapped-items-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-layout-z-order-a.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-layout-z-order-b.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-overlapped-items-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-overlapped-items-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-overlapped-items-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-overlapped-items-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-overlapped-items-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[grid-z-axis-ordering-overlapped-items-006.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[will-change-stacking-context-z-index-3.html]
prefs: ["layout_grid_enabled:true"]

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<title>CSS Test: `will-change: z-index`</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident">
<link rel="help" href="https://www.w3.org/TR/css-flexbox-1/#painting">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11827">
<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
`will-change: z-index` establishes a stacking context on a flex item.
">
<style>
.test {
will-change: z-index;
width: 100px;
background: red;
}
.test::before {
content: "";
display: block;
position: relative;
z-index: -1;
height: 100px;
background: green;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div style="display: flex">
<div class="test"></div>
</div>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<title>CSS Test: `will-change: z-index`</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident">
<link rel="help" href="https://www.w3.org/TR/css-grid-2/#z-order">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11827">
<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
`will-change: z-index` establishes a stacking context on a grid item.
">
<style>
.test {
will-change: z-index;
width: 100px;
background: red;
}
.test::before {
content: "";
display: block;
position: relative;
z-index: -1;
height: 100px;
background: green;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div style="display: grid">
<div class="test"></div>
</div>

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<title>CSS Test: `will-change: z-index`</title>
<link rel="author" title="Oriol Brufau" href="obrufau@igalia.com">
<link rel="help" href="https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident">
<link rel="help" href="https://www.w3.org/TR/CSS2/visuren.html#z-index">
<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/11827">
<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
<meta name="assert" content="
`will-change: z-index` doesn't establish a stacking context on a non-positioned block box,
because `z-index` doesn't apply in that case.
">
<style>
.test {
will-change: z-index;
width: 100px;
background: green;
}
.test::before {
content: "";
display: block;
position: relative;
z-index: -1;
height: 100px;
background: red;
}
</style>
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div class="test"></div>