layout: Simplify PositioningContext by having it hold a single Vec (#36795)

`PositioningContext` held two vectors, one inside an `Option`, to
differentiate between the version used for a containing block for all
descendants (including `position: absolute` and `position: fixed`) or
only for `position: absolute` descendants. This distinction was really
hard to reason about and required a lot of bookkeeping about what kind
of `PositioningContext` a layout box's parent expected. In addition, it
led to a lot of mistakes.

This change simplifies things so that `PositioningContext` only holds a
single vector. When it comes time to lay out hoisted absolutely
positioned
fragments, the code then:
 - lays out all of them (in the case of a `PositioningContext` for all
   descendants), or
 - only lays out the `position: absolute` descendants and preserves the
   `position: fixed` descendants (in the case the `PositioningContext`
   is only for `position: absolute`.), or
- lays out none of them if the `PositioningContext` was created for
  box that did not establish a containing block for absolutes.

It's possible that this way of dealing with hoisted absolutes is a bit
less efficient, but, the number of these descendants is typically quite
small, so it should not be significant. In addition, this decreases the
size in memory of all `PositioningContexts` which are created in more
situations as time goes on.

Testing: There is a new WPT test with this change.
Fixes: #36696.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-05-02 14:20:11 +02:00 committed by GitHub
parent e25e63b587
commit 9bc16482a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 203 additions and 323 deletions

View file

@ -42,16 +42,6 @@ pub(crate) struct AbsolutelyPositionedBox {
pub context: IndependentFormattingContext,
}
#[derive(Clone, MallocSizeOf)]
pub(crate) struct PositioningContext {
for_nearest_positioned_ancestor: Option<Vec<HoistedAbsolutelyPositionedBox>>,
// For nearest `containing block for all descendants` as defined by the CSS transforms
// spec.
// https://www.w3.org/TR/css-transforms-1/#containing-block-for-all-descendants
for_nearest_containing_block_for_all_descendants: Vec<HoistedAbsolutelyPositionedBox>,
}
#[derive(Clone, MallocSizeOf)]
pub(crate) struct HoistedAbsolutelyPositionedBox {
absolutely_positioned_box: ArcRefCell<AbsolutelyPositionedBox>,
@ -104,55 +94,26 @@ impl AbsolutelyPositionedBox {
}
}
impl IndependentFormattingContext {
#[inline]
pub(crate) fn new_positioning_context(&self) -> Option<PositioningContext> {
self.base.new_positioning_context()
}
}
impl LayoutBoxBase {
#[inline]
pub(crate) fn new_positioning_context(&self) -> Option<PositioningContext> {
PositioningContext::new_for_style(&self.style, &self.base_fragment_info.flags)
}
#[derive(Clone, Default, MallocSizeOf)]
pub(crate) struct PositioningContext {
absolutes: Vec<HoistedAbsolutelyPositionedBox>,
}
impl PositioningContext {
pub(crate) fn new_for_containing_block_for_all_descendants() -> Self {
Self {
for_nearest_positioned_ancestor: None,
for_nearest_containing_block_for_all_descendants: Vec::new(),
}
#[inline]
pub(crate) fn new_for_layout_box_base(layout_box_base: &LayoutBoxBase) -> Option<Self> {
Self::new_for_style_and_fragment_flags(
&layout_box_base.style,
&layout_box_base.base_fragment_info.flags,
)
}
/// Create a [PositioningContext] to use for laying out a subtree. The idea is that
/// when subtree layout is finished, the newly hoisted boxes can be processed
/// (normally adjusting their static insets) and then appended to the parent
/// [PositioningContext].
pub(crate) fn new_for_subtree(collects_for_nearest_positioned_ancestor: bool) -> Self {
Self {
for_nearest_positioned_ancestor: if collects_for_nearest_positioned_ancestor {
Some(Vec::new())
} else {
None
},
for_nearest_containing_block_for_all_descendants: Vec::new(),
}
}
pub(crate) fn collects_for_nearest_positioned_ancestor(&self) -> bool {
self.for_nearest_positioned_ancestor.is_some()
}
fn new_for_style(style: &ComputedValues, flags: &FragmentFlags) -> Option<Self> {
if style.establishes_containing_block_for_all_descendants(*flags) {
Some(Self::new_for_containing_block_for_all_descendants())
} else if style.establishes_containing_block_for_absolute_descendants(*flags) {
Some(Self {
for_nearest_positioned_ancestor: Some(Vec::new()),
for_nearest_containing_block_for_all_descendants: Vec::new(),
})
fn new_for_style_and_fragment_flags(
style: &ComputedValues,
flags: &FragmentFlags,
) -> Option<Self> {
if style.establishes_containing_block_for_absolute_descendants(*flags) {
Some(Self::default())
} else {
None
}
@ -195,20 +156,9 @@ impl PositioningContext {
offset: &PhysicalVec<Au>,
index: PositioningContextLength,
) {
if let Some(hoisted_boxes) = self.for_nearest_positioned_ancestor.as_mut() {
hoisted_boxes
.iter_mut()
.skip(index.for_nearest_positioned_ancestor)
.for_each(|hoisted_fragment| {
hoisted_fragment
.fragment
.borrow_mut()
.adjust_offsets(offset)
})
}
self.for_nearest_containing_block_for_all_descendants
self.absolutes
.iter_mut()
.skip(index.for_nearest_containing_block_for_all_descendants)
.skip(index.0)
.for_each(|hoisted_fragment| {
hoisted_fragment
.fragment
@ -227,19 +177,23 @@ impl PositioningContext {
base: &LayoutBoxBase,
fragment_layout_fn: impl FnOnce(&mut Self) -> BoxFragment,
) -> BoxFragment {
// Try to create a context, but if one isn't necessary, simply create the fragment
// using the given closure and the current `PositioningContext`.
let mut new_context = match base.new_positioning_context() {
Some(new_context) => new_context,
None => return fragment_layout_fn(self),
};
// If a new `PositioningContext` isn't necessary, simply create the fragment using
// the given closure and the current `PositioningContext`.
let establishes_containing_block_for_absolutes = base
.style
.establishes_containing_block_for_absolute_descendants(base.base_fragment_info.flags);
if !establishes_containing_block_for_absolutes {
return fragment_layout_fn(self);
}
let mut new_context = PositioningContext::default();
let mut new_fragment = fragment_layout_fn(&mut new_context);
new_context.layout_collected_children(layout_context, &mut new_fragment);
// If the new context has any hoisted boxes for the nearest containing block for
// pass them up the tree.
// Lay out all of the absolutely positioned children for this fragment, and, if it
// isn't a containing block for fixed elements, then pass those up to the parent.
new_context.layout_collected_children(layout_context, &mut new_fragment);
self.append(new_context);
if base.style.clone_position() == Position::Relative {
new_fragment.content_rect.origin += relative_adjustement(&base.style, containing_block)
.to_physical_vector(containing_block.style.writing_mode)
@ -248,13 +202,61 @@ impl PositioningContext {
new_fragment
}
fn take_boxes_for_fragment(
&mut self,
new_fragment: &BoxFragment,
boxes_to_layout_out: &mut Vec<HoistedAbsolutelyPositionedBox>,
boxes_to_continue_hoisting_out: &mut Vec<HoistedAbsolutelyPositionedBox>,
) {
debug_assert!(
new_fragment
.style
.establishes_containing_block_for_absolute_descendants(new_fragment.base.flags)
);
if new_fragment
.style
.establishes_containing_block_for_all_descendants(new_fragment.base.flags)
{
boxes_to_layout_out.append(&mut self.absolutes);
return;
}
// TODO: This could potentially use `extract_if` when that is stabilized.
let (mut boxes_to_layout, mut boxes_to_continue_hoisting) = self
.absolutes
.drain(..)
.partition(|hoisted_box| hoisted_box.position() != Position::Fixed);
boxes_to_layout_out.append(&mut boxes_to_layout);
boxes_to_continue_hoisting_out.append(&mut boxes_to_continue_hoisting);
}
// Lay out the hoisted boxes collected into this `PositioningContext` and add them
// to the given `BoxFragment`.
pub fn layout_collected_children(
pub(crate) fn layout_collected_children(
&mut self,
layout_context: &LayoutContext,
new_fragment: &mut BoxFragment,
) {
if self.absolutes.is_empty() {
return;
}
// Sometimes we create temporary PositioningContexts just to collect hoisted absolutes and
// then these are processed later. In that case and if this fragment doesn't establish a
// containing block for absolutes at all, we just do nothing. All hoisted fragments will
// later be passed up to a parent PositioningContext.
//
// Handling this case here, when the PositioningContext is completely ineffectual other than
// as a temporary container for hoisted boxes, means that callers can execute less conditional
// code.
if !new_fragment
.style
.establishes_containing_block_for_absolute_descendants(new_fragment.base.flags)
{
return;
}
let padding_rect = PhysicalRect::new(
// Ignore the content rects position in its own containing block:
PhysicalPoint::origin(),
@ -268,83 +270,58 @@ impl PositioningContext {
style: &new_fragment.style,
};
let take_hoisted_boxes_pending_layout =
|context: &mut Self| match context.for_nearest_positioned_ancestor.as_mut() {
Some(fragments) => mem::take(fragments),
None => mem::take(&mut context.for_nearest_containing_block_for_all_descendants),
};
let mut fixed_position_boxes_to_hoist = Vec::new();
let mut boxes_to_layout = Vec::new();
self.take_boxes_for_fragment(
new_fragment,
&mut boxes_to_layout,
&mut fixed_position_boxes_to_hoist,
);
// Loop because its possible that we discover (the static position of)
// more absolutely-positioned boxes while doing layout for others.
let mut hoisted_boxes = take_hoisted_boxes_pending_layout(self);
let mut laid_out_child_fragments = Vec::new();
while !hoisted_boxes.is_empty() {
// Laying out a `position: absolute` child (which only establishes a containing block for
// `position: absolute` descendants) can result in more `position: fixed` descendants
// collecting in `self.absolutes`. We need to loop here in order to keep either laying them
// out or putting them into `fixed_position_boxes_to_hoist`. We know there aren't any more
// when `self.absolutes` is empty.
while !boxes_to_layout.is_empty() {
HoistedAbsolutelyPositionedBox::layout_many(
layout_context,
&mut hoisted_boxes,
&mut laid_out_child_fragments,
&mut self.for_nearest_containing_block_for_all_descendants,
std::mem::take(&mut boxes_to_layout),
&mut new_fragment.children,
&mut self.absolutes,
&containing_block,
new_fragment.padding,
);
hoisted_boxes = take_hoisted_boxes_pending_layout(self);
self.take_boxes_for_fragment(
new_fragment,
&mut boxes_to_layout,
&mut fixed_position_boxes_to_hoist,
);
}
new_fragment.children.extend(laid_out_child_fragments);
// We replace here instead of simply preserving these in `take_boxes_for_fragment`
// so that we don't have to continually re-iterate over them when laying out in the
// loop above.
self.absolutes = fixed_position_boxes_to_hoist;
}
pub(crate) fn push(&mut self, box_: HoistedAbsolutelyPositionedBox) {
if let Some(nearest) = &mut self.for_nearest_positioned_ancestor {
let position = box_
.absolutely_positioned_box
.borrow()
.context
.style()
.clone_position();
match position {
Position::Fixed => {}, // fall through
Position::Absolute => return nearest.push(box_),
Position::Static | Position::Relative | Position::Sticky => unreachable!(),
}
}
self.for_nearest_containing_block_for_all_descendants
.push(box_)
pub(crate) fn push(&mut self, hoisted_box: HoistedAbsolutelyPositionedBox) {
debug_assert!(matches!(
hoisted_box.position(),
Position::Absolute | Position::Fixed
));
self.absolutes.push(hoisted_box);
}
pub(crate) fn is_empty(&self) -> bool {
self.for_nearest_containing_block_for_all_descendants
.is_empty() &&
self.for_nearest_positioned_ancestor
.as_ref()
.is_none_or(|vector| vector.is_empty())
}
pub(crate) fn append(&mut self, other: Self) {
if other.is_empty() {
pub(crate) fn append(&mut self, mut other: Self) {
if other.absolutes.is_empty() {
return;
}
vec_append_owned(
&mut self.for_nearest_containing_block_for_all_descendants,
other.for_nearest_containing_block_for_all_descendants,
);
match (
self.for_nearest_positioned_ancestor.as_mut(),
other.for_nearest_positioned_ancestor,
) {
(Some(us), Some(them)) => vec_append_owned(us, them),
(None, Some(them)) => {
// This is the case where we have laid out the absolute children in a containing
// block for absolutes and we then are passing up the fixed-position descendants
// to the containing block for all descendants.
vec_append_owned(
&mut self.for_nearest_containing_block_for_all_descendants,
them,
);
},
(None, None) => {},
_ => unreachable!(),
if self.absolutes.is_empty() {
self.absolutes = other.absolutes;
} else {
self.absolutes.append(&mut other.absolutes)
}
}
@ -354,19 +331,16 @@ impl PositioningContext {
initial_containing_block: &DefiniteContainingBlock,
fragments: &mut Vec<Fragment>,
) {
debug_assert!(self.for_nearest_positioned_ancestor.is_none());
// Loop because its possible that we discover (the static position of)
// more absolutely-positioned boxes while doing layout for others.
while !self
.for_nearest_containing_block_for_all_descendants
.is_empty()
{
// Laying out a `position: absolute` child (which only establishes a containing block for
// `position: absolute` descendants) can result in more `position: fixed` descendants
// collecting in `self.absolutes`. We need to loop here in order to keep laying them out. We
// know there aren't any more when `self.absolutes` is empty.
while !self.absolutes.is_empty() {
HoistedAbsolutelyPositionedBox::layout_many(
layout_context,
&mut mem::take(&mut self.for_nearest_containing_block_for_all_descendants),
mem::take(&mut self.absolutes),
fragments,
&mut self.for_nearest_containing_block_for_all_descendants,
&mut self.absolutes,
initial_containing_block,
Default::default(),
)
@ -375,58 +349,46 @@ impl PositioningContext {
/// Get the length of this [PositioningContext].
pub(crate) fn len(&self) -> PositioningContextLength {
PositioningContextLength {
for_nearest_positioned_ancestor: self
.for_nearest_positioned_ancestor
.as_ref()
.map_or(0, |vec| vec.len()),
for_nearest_containing_block_for_all_descendants: self
.for_nearest_containing_block_for_all_descendants
.len(),
}
PositioningContextLength(self.absolutes.len())
}
/// Truncate this [PositioningContext] to the given [PositioningContextLength]. This
/// is useful for "unhoisting" boxes in this context and returning it to the state at
/// the time that [`PositioningContext::len()`] was called.
pub(crate) fn truncate(&mut self, length: &PositioningContextLength) {
if let Some(vec) = self.for_nearest_positioned_ancestor.as_mut() {
vec.truncate(length.for_nearest_positioned_ancestor);
}
self.for_nearest_containing_block_for_all_descendants
.truncate(length.for_nearest_containing_block_for_all_descendants);
self.absolutes.truncate(length.0)
}
}
/// A data structure which stores the size of a positioning context.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct PositioningContextLength {
/// The number of boxes that will be hoisted the the nearest positioned ancestor for
/// layout.
for_nearest_positioned_ancestor: usize,
/// The number of boxes that will be hoisted the the nearest ancestor which
/// establishes a containing block for all descendants for layout.
for_nearest_containing_block_for_all_descendants: usize,
}
pub(crate) struct PositioningContextLength(usize);
impl Zero for PositioningContextLength {
fn zero() -> Self {
PositioningContextLength {
for_nearest_positioned_ancestor: 0,
for_nearest_containing_block_for_all_descendants: 0,
}
Self(0)
}
fn is_zero(&self) -> bool {
self.for_nearest_positioned_ancestor == 0 &&
self.for_nearest_containing_block_for_all_descendants == 0
self.0.is_zero()
}
}
impl HoistedAbsolutelyPositionedBox {
fn position(&self) -> Position {
let position = self
.absolutely_positioned_box
.borrow()
.context
.style()
.clone_position();
assert!(position == Position::Fixed || position == Position::Absolute);
position
}
pub(crate) fn layout_many(
layout_context: &LayoutContext,
boxes: &mut [Self],
mut boxes: Vec<Self>,
fragments: &mut Vec<Fragment>,
for_nearest_containing_block_for_all_descendants: &mut Vec<HoistedAbsolutelyPositionedBox>,
containing_block: &DefiniteContainingBlock,
@ -473,7 +435,7 @@ impl HoistedAbsolutelyPositionedBox {
pub(crate) fn layout(
&mut self,
layout_context: &LayoutContext,
for_nearest_containing_block_for_all_descendants: &mut Vec<HoistedAbsolutelyPositionedBox>,
hoisted_absolutes_from_children: &mut Vec<HoistedAbsolutelyPositionedBox>,
containing_block: &DefiniteContainingBlock,
containing_block_padding: PhysicalSides<Au>,
) -> Fragment {
@ -596,7 +558,7 @@ impl HoistedAbsolutelyPositionedBox {
.sizes
}));
let mut positioning_context = context.new_positioning_context().unwrap();
let mut positioning_context = PositioningContext::default();
let mut new_fragment = {
let content_size: LogicalVec2<Au>;
let fragments;
@ -709,6 +671,10 @@ impl HoistedAbsolutelyPositionedBox {
)
.with_specific_layout_info(specific_layout_info)
};
// This is an absolutely positioned element, which means it also establishes a
// containing block for absolutes. We lay out any absolutely positioned children
// here and pass the rest to `hoisted_absolutes_from_children.`
positioning_context.layout_collected_children(layout_context, &mut new_fragment);
// Any hoisted boxes that remain in this positioning context are going to be hoisted
@ -721,8 +687,7 @@ impl HoistedAbsolutelyPositionedBox {
PositioningContextLength::zero(),
);
for_nearest_containing_block_for_all_descendants
.extend(positioning_context.for_nearest_containing_block_for_all_descendants);
hoisted_absolutes_from_children.extend(positioning_context.absolutes);
let fragment = Fragment::Box(ArcRefCell::new(new_fragment));
context.base.set_fragment(fragment.clone());
@ -1024,14 +989,6 @@ impl AbsoluteAxisSolver<'_> {
}
}
fn vec_append_owned<T>(a: &mut Vec<T>, mut b: Vec<T>) {
if a.is_empty() {
*a = b
} else {
a.append(&mut b)
}
}
/// <https://drafts.csswg.org/css2/visuren.html#relative-positioning>
pub(crate) fn relative_adjustement(
style: &ComputedValues,