layout: Unify scrollable overflow calculation and include position: absolute (#37475)

Previously, layout was handling scrollable overflow and srolling area
calculation separately, only excluding the "unreachable scrollable
overflow region" at the last step. In addition, `position: absolute` was
not included in scrollable overflow calculation.

This change combines the two concepts into a single scrollable overflow
calculation and starts taking into account `position: absolute`.
Finally, `BoxFragment::scrollable_overflow_for_parent` is converted to
use early returns which reduces the amount of indentation.

Fixes #35928.
Fixes #37204.
Testing: This causes some WPT test to pass, but also two to start
failing:
- `/css/css-masking/clip-path/clip-path-fixed-scroll.html`: This seems
to fail
because script is scrolling past the boundaries of the document. This is
a
failure that was uncovered by the fixed element now being added to the
   page's scroll area.
- `/css/css-overflow/overflow-outside-padding.html`: One test has
started to fail
here because now the absolutely positioned element is included in the
scroll area,
and I think there is an issue with how we are placing RTL items with
negative margins.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-06-16 13:30:31 +02:00 committed by GitHub
parent 29e618dcf7
commit 0f61361e27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 123 additions and 142 deletions

View file

@ -88,7 +88,10 @@ pub(crate) struct BoxFragment {
block_margins_collapsed_with_children: Option<Box<CollapsedBlockMargins>>,
/// The scrollable overflow of this box fragment.
/// The scrollable overflow of this box fragment in the same coordiante system as
/// [`Self::content_rect`] ie a rectangle within the parent fragment's content
/// rectangle. This does not take into account any transforms this fragment applies.
/// This is handled when calling [`Self::scrollable_overflow_for_parent`].
scrollable_overflow: Option<PhysicalRect<Au>>,
/// The resolved box insets if this box is `position: sticky`. These are calculated
@ -202,19 +205,57 @@ impl BoxFragment {
.expect("Should only call `scrollable_overflow()` after calculating overflow")
}
/// This is an implementation of:
/// - <https://drafts.csswg.org/css-overflow-3/#scrollable>.
/// - <https://drafts.csswg.org/cssom-view/#scrolling-area>
pub(crate) fn calculate_scrollable_overflow(&mut self) {
let scrollable_overflow_from_children = self
.children
.iter()
.fold(PhysicalRect::zero(), |acc, child| {
acc.union(&child.calculate_scrollable_overflow_for_parent())
});
let physical_padding_rect = self.padding_rect();
let content_origin = self.content_rect.origin.to_vector();
self.scrollable_overflow = Some(
physical_padding_rect
.union(&scrollable_overflow_from_children.translate(content_origin)),
);
// > The scrollable overflow area is the union of:
// > * The scroll containers own padding box.
// > * All line boxes directly contained by the scroll container.
// > * The border boxes of all boxes for which it is the containing block and
// > whose border boxes are positioned not wholly in the unreachable
// > scrollable overflow region, accounting for transforms by projecting
// > each box onto the plane of the element that establishes its 3D
// > rendering context.
// > * The margin areas of grid item and flex item boxes for which the box
// > establishes a containing block.
// > * The scrollable overflow areas of all of the above boxes (including zero-area
// > boxes and accounting for transforms as described above), provided they
// > themselves have overflow: visible (i.e. do not themselves trap the overflow)
// > and that scrollable overflow is not already clipped (e.g. by the clip property
// > or the contain property).
// > * Additional padding added to the scrollable overflow rectangle as necessary
// to enable scroll positions that satisfy the requirements of both place-content:
// start and place-content: end alignment.
//
// TODO(mrobinson): Below we are handling the border box and the scrollable
// overflow together, but from the specification it seems that if the border
// box of an item is in the "wholly unreachable scrollable overflow region", but
// its scrollable overflow is not, it should also be excluded.
let scrollable_overflow = self
.children
.iter()
.fold(physical_padding_rect, |acc, child| {
let scrollable_overflow_from_child = child
.calculate_scrollable_overflow_for_parent()
.translate(content_origin);
// Note that this doesn't just exclude scrollable overflow outside the
// wholly unrechable scrollable overflow area, but also clips it. This
// makes the resulting value more like the "scroll area" rather than the
// "scrollable overflow."
let scrollable_overflow_from_child = self
.clip_wholly_unreachable_scrollable_overflow(
scrollable_overflow_from_child,
physical_padding_rect,
);
acc.union(&scrollable_overflow_from_child)
});
self.scrollable_overflow = Some(scrollable_overflow)
}
pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) {
@ -281,11 +322,6 @@ impl BoxFragment {
}
pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
// TODO: Properly handle absolutely positioned fragments.
if self.style.get_box().position.is_absolutely_positioned() {
return PhysicalRect::zero();
}
let mut overflow = self.border_rect();
if !self.style.establishes_scroll_container(self.base.flags) {
// https://www.w3.org/TR/css-overflow-3/#scrollable
@ -308,45 +344,46 @@ impl BoxFragment {
}
}
if !self
.style
.has_effective_transform_or_perspective(self.base.flags)
{
return overflow;
}
// <https://drafts.csswg.org/css-overflow-3/#scrollable-overflow-region>
// > ...accounting for transforms by projecting each box onto the plane of
// > the element that establishes its 3D rendering context. [CSS3-TRANSFORMS]
// Both boxes and its scrollable overflow (if it is included) should be transformed accordingly.
//
// TODO(stevennovaryo): We are supposed to handle perspective transform and 3d context, but it is yet to happen.
if self
.style
.has_effective_transform_or_perspective(self.base.flags)
{
if let Some(transform) =
self.calculate_transform_matrix(&self.border_rect().to_untyped())
{
if let Some(transformed_overflow_box) =
transform.outer_transformed_rect(&overflow.to_webrender().to_rect())
{
overflow =
f32_rect_to_au_rect(transformed_overflow_box.to_untyped()).cast_unit();
}
}
}
overflow
// TODO(stevennovaryo): We are supposed to handle perspective transform and 3d
// contexts, but it is yet to happen.
self.calculate_transform_matrix(&self.border_rect().to_untyped())
.and_then(|transform| {
transform.outer_transformed_rect(&overflow.to_webrender().to_rect())
})
.map(|transformed_rect| f32_rect_to_au_rect(transformed_rect.to_untyped()).cast_unit())
.unwrap_or(overflow)
}
/// <https://drafts.csswg.org/css-overflow/#unreachable-scrollable-overflow-region>
/// > area beyond the scroll origin in either axis is considered the unreachable scrollable overflow region
///
/// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction.
/// For an element, the clip rect is the padding rect and for viewport, it is the initial containing block.
pub(crate) fn clip_unreachable_scrollable_overflow_region(
/// Return the clipped the scrollable overflow based on its scroll origin, determined
/// by overflow direction. For an element, the clip rect is the padding rect and for
/// viewport, it is the initial containing block.
pub(crate) fn clip_wholly_unreachable_scrollable_overflow(
&self,
scrollable_overflow: PhysicalRect<Au>,
clipping_rect: PhysicalRect<Au>,
) -> PhysicalRect<Au> {
// From <https://drafts.csswg.org/css-overflow/#unreachable-scrollable-overflow-region>:
// > Unless otherwise adjusted (e.g. by content alignment [css-align-3]), the area
// > beyond the scroll origin in either axis is considered the unreachable scrollable
// > overflow region: content rendered here is not accessible to the reader, see § 2.2
// > Scrollable Overflow. A scroll container is said to be scrolled to its scroll
// > origin when its scroll origin coincides with the corresponding corner of its
// > scrollport. This scroll position, the scroll origin position, usually, but not
// > always, coincides with the initial scroll position.
let scrolling_direction = self.style.overflow_direction();
let mut scrollable_overflow_box = scrollable_overflow.to_box2d();
let mut clipping_box = clipping_rect.to_box2d();
if scrolling_direction.rightward {
clipping_box.max.x = MAX_AU;
} else {
@ -359,7 +396,9 @@ impl BoxFragment {
clipping_box.min.y = MIN_AU;
}
scrollable_overflow_box = scrollable_overflow_box.intersection_unchecked(&clipping_box);
let scrollable_overflow_box = scrollable_overflow
.to_box2d()
.intersection_unchecked(&clipping_box);
match scrollable_overflow_box.is_negative() {
true => PhysicalRect::zero(),
@ -367,18 +406,6 @@ impl BoxFragment {
}
}
/// <https://drafts.csswg.org/css-overflow/#unreachable-scrollable-overflow-region>
/// > area beyond the scroll origin in either axis is considered the unreachable scrollable overflow region
///
/// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction.
/// This will coincides with the scrollport if the fragment is a scroll container.
pub(crate) fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> {
self.clip_unreachable_scrollable_overflow_region(
self.scrollable_overflow(),
self.padding_rect(),
)
}
pub(crate) fn calculate_resolved_insets_if_positioned(&self) -> PhysicalSides<AuOrAuto> {
let position = self.style.get_box().position;
debug_assert_ne!(

View file

@ -162,7 +162,7 @@ impl Fragment {
}
}
pub fn unclipped_scrolling_area(&self) -> PhysicalRect<Au> {
pub(crate) fn scrolling_area(&self) -> PhysicalRect<Au> {
match self {
Fragment::Box(fragment) | Fragment::Float(fragment) => {
let fragment = fragment.borrow();
@ -172,17 +172,6 @@ impl Fragment {
}
}
pub fn scrolling_area(&self) -> PhysicalRect<Au> {
match self {
Fragment::Box(fragment) | Fragment::Float(fragment) => {
let fragment = fragment.borrow();
fragment
.offset_by_containing_block(&fragment.reachable_scrollable_overflow_region())
},
_ => self.scrollable_overflow_for_parent(),
}
}
pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
match self {
Fragment::Box(fragment) | Fragment::Float(fragment) => {

View file

@ -102,29 +102,41 @@ impl FragmentTree {
.expect("Should only call `scrollable_overflow()` after calculating overflow")
}
/// Calculate the scrollable overflow / scrolling area for this [`FragmentTree`] according
/// to <https://drafts.csswg.org/cssom-view/#scrolling-area>.
pub(crate) fn calculate_scrollable_overflow(&self) {
self.scrollable_overflow
.set(Some(self.root_fragments.iter().fold(
PhysicalRect::zero(),
|acc, child| {
let child_overflow = child.calculate_scrollable_overflow_for_parent();
let scrollable_overflow = || {
let Some(first_root_fragment) = self.root_fragments.first() else {
return self.initial_containing_block;
};
// https://drafts.csswg.org/css-overflow/#scrolling-direction
// We want to clip scrollable overflow on box-start and inline-start
// sides of the scroll container.
//
// FIXME(mrobinson, bug 25564): This should take into account writing
// mode.
let child_overflow = PhysicalRect::new(
euclid::Point2D::zero(),
euclid::Size2D::new(
child_overflow.size.width + child_overflow.origin.x,
child_overflow.size.height + child_overflow.origin.y,
),
);
acc.union(&child_overflow)
let scrollable_overflow = self.root_fragments.iter().fold(
self.initial_containing_block,
|overflow, fragment| {
fragment
.calculate_scrollable_overflow_for_parent()
.union(&overflow)
},
)));
);
// Assuming that the first fragment is the root element, ensure that
// scrollable overflow that is unreachable is not included in the final
// rectangle. See
// <https://drafts.csswg.org/css-overflow/#scrolling-direction>.
let first_root_fragment = match first_root_fragment {
Fragment::Box(fragment) | Fragment::Float(fragment) => fragment.borrow(),
_ => return scrollable_overflow,
};
if !first_root_fragment.is_root_element() {
return scrollable_overflow;
}
first_root_fragment.clip_wholly_unreachable_scrollable_overflow(
scrollable_overflow,
self.initial_containing_block,
)
};
self.scrollable_overflow.set(Some(scrollable_overflow()))
}
pub(crate) fn find<T>(
@ -141,29 +153,6 @@ impl FragmentTree {
.find_map(|child| child.find(&info, 0, &mut process_func))
}
/// <https://drafts.csswg.org/cssom-view/#scrolling-area>
///
/// Scrolling area for a viewport that is clipped according to overflow direction of root element.
pub fn get_scrolling_area_for_viewport(&self) -> PhysicalRect<Au> {
let mut scroll_area = self.initial_containing_block;
if let Some(root_fragment) = self.root_fragments.first() {
for fragment in self.root_fragments.iter() {
scroll_area = fragment.unclipped_scrolling_area().union(&scroll_area);
}
match root_fragment {
Fragment::Box(fragment) | Fragment::Float(fragment) => fragment
.borrow()
.clip_unreachable_scrollable_overflow_region(
scroll_area,
self.initial_containing_block,
),
_ => scroll_area,
}
} else {
scroll_area
}
}
/// Find the `<body>` element's [`Fragment`], if it exists in this [`FragmentTree`].
pub(crate) fn body_fragment(&self) -> Option<ArcRefCell<BoxFragment>> {
fn find_body(children: &[Fragment]) -> Option<ArcRefCell<BoxFragment>> {