Refactor positioned layout (#33922)

This unifies the size resolution into AbsoluteAxisSolver, since it needs
to know the size in order to resolve auto margins correctly anyways.
This will allow adding support for sizing keywords in a follow-up patch.

Also, this avoids doing multiple layouts due to min and max constraints,
improving performance.

Additionally, tables may end up having a custom size, different than
what we would expect by just looking at the sizing properties. This
patch ensures that we resolve margins correctly with the final size,
resulting in 2 tests now passing.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2024-10-21 12:41:42 +02:00 committed by GitHub
parent fee927475b
commit 2319764a1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 195 additions and 256 deletions

View file

@ -30,7 +30,6 @@ pub use flow::BoxTree;
pub use fragment_tree::FragmentTree;
use geom::AuOrAuto;
use style::properties::ComputedValues;
use style_ext::{Clamp, ComputedValuesExt};
use crate::geom::LogicalVec2;
@ -59,22 +58,6 @@ impl<'a> IndefiniteContainingBlock<'a> {
style,
}
}
fn new_for_intrinsic_inline_size_of_child(
&self,
style: &'a ComputedValues,
auto_minimum: &LogicalVec2<Au>,
) -> Self {
let (content_box_size, content_min_size, content_max_size, _, _) =
style.content_box_sizes_and_padding_border_margin_deprecated(self);
let block_size = content_box_size.block.map(|v| {
v.clamp_between_extremums(
content_min_size.block.auto_is(|| auto_minimum.block),
content_max_size.block,
)
});
IndefiniteContainingBlock::new_for_style_and_block_size(style, block_size)
}
}
impl<'a> From<&'_ ContainingBlock<'a>> for IndefiniteContainingBlock<'a> {

View file

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::mem;
use app_units::Au;
use rayon::iter::IntoParallelRefMutIterator;
use rayon::prelude::{IndexedParallelIterator, ParallelIterator};
@ -25,7 +27,8 @@ use crate::geom::{
AuOrAuto, LengthPercentageOrAuto, LogicalRect, LogicalSides, LogicalVec2, PhysicalPoint,
PhysicalRect, PhysicalVec, ToLogical, ToLogicalWithContainingBlock,
};
use crate::style_ext::{ComputedValuesExt, DisplayInside};
use crate::sizing::ContentSizes;
use crate::style_ext::{Clamp, ComputedValuesExt, DisplayInside};
use crate::{ContainingBlock, DefiniteContainingBlock, IndefiniteContainingBlock};
#[derive(Debug, Serialize)]
@ -240,12 +243,10 @@ 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) => std::mem::take(fragments),
None => std::mem::take(&mut context.for_nearest_containing_block_for_all_descendants),
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),
};
// Loop because its possible that we discover (the static position of)
@ -337,7 +338,7 @@ impl PositioningContext {
{
HoistedAbsolutelyPositionedBox::layout_many(
layout_context,
&mut std::mem::take(&mut self.for_nearest_containing_block_for_all_descendants),
&mut mem::take(&mut self.for_nearest_containing_block_for_all_descendants),
fragments,
&mut self.for_nearest_containing_block_for_all_descendants,
initial_containing_block,
@ -345,14 +346,6 @@ impl PositioningContext {
}
}
pub(crate) fn clear(&mut self) {
self.for_nearest_containing_block_for_all_descendants
.clear();
if let Some(v) = self.for_nearest_positioned_ancestor.as_mut() {
v.clear()
}
}
/// Get the length of this [PositioningContext].
pub(crate) fn len(&self) -> PositioningContextLength {
PositioningContextLength {
@ -454,37 +447,40 @@ impl HoistedAbsolutelyPositionedBox {
) -> BoxFragment {
let cbis = containing_block.size.inline;
let cbbs = containing_block.size.block;
let mut absolutely_positioned_box = self.absolutely_positioned_box.borrow_mut();
let containing_block_writing_mode = containing_block.style.writing_mode;
let style = absolutely_positioned_box.context.style().clone();
let pbm = style.padding_border_margin(&containing_block.into());
let indefinite_containing_block = containing_block.into();
let mut absolutely_positioned_box = self.absolutely_positioned_box.borrow_mut();
let context = &mut absolutely_positioned_box.context;
let style = context.style().clone();
let containing_block = &containing_block.into();
let pbm = style.padding_border_margin(containing_block);
let computed_size = match &absolutely_positioned_box.context {
let (computed_size, computed_min_size, computed_max_size) = match context {
IndependentFormattingContext::Replaced(replaced) => {
// https://drafts.csswg.org/css2/visudet.html#abs-replaced-width
// https://drafts.csswg.org/css2/visudet.html#abs-replaced-height
let used_size = replaced.contents.used_size_as_if_inline_element(
&containing_block.into(),
&replaced.style,
&pbm,
);
LogicalVec2 {
inline: AuOrAuto::LengthPercentage(used_size.inline),
block: AuOrAuto::LengthPercentage(used_size.block),
}
let used_size = replaced
.contents
.used_size_as_if_inline_element(containing_block, &style, &pbm)
.map(|size| AuOrAuto::LengthPercentage(*size));
(used_size, Default::default(), Default::default())
},
IndependentFormattingContext::NonReplaced(non_replaced) => non_replaced
.style
.content_box_size_deprecated(&indefinite_containing_block, &pbm),
IndependentFormattingContext::NonReplaced(_) => (
style.content_box_size_deprecated(containing_block, &pbm),
style
.content_min_box_size_deprecated(containing_block, &pbm)
.map(|value| value.map(Au::from).auto_is(Au::zero)),
style
.content_max_box_size_deprecated(containing_block, &pbm)
.map(|value| value.map(Au::from)),
),
};
let shared_fragment = self.fragment.borrow();
let static_position_rect = shared_fragment
.static_position_rect
.to_logical(&indefinite_containing_block);
.to_logical(containing_block);
let box_offset = style.box_offsets(&indefinite_containing_block);
let box_offset = style.box_offsets(containing_block);
// When the "static-position rect" doesn't come into play, we do not do any alignment
// in the inline axis.
@ -497,18 +493,21 @@ impl HoistedAbsolutelyPositionedBox {
false => shared_fragment.resolved_alignment.inline,
};
let inline_axis_solver = AbsoluteAxisSolver {
let mut inline_axis_solver = AbsoluteAxisSolver {
axis: AxisDirection::Inline,
containing_size: cbis,
padding_border_sum: pbm.padding_border_sums.inline,
computed_margin_start: pbm.margin.inline_start,
computed_margin_end: pbm.margin.inline_end,
computed_size: computed_size.inline,
computed_min_size: computed_min_size.inline,
computed_max_size: computed_max_size.inline,
avoid_negative_margin_start: true,
box_offsets: inline_box_offsets,
static_position_rect_axis: static_position_rect.get_axis(AxisDirection::Inline),
alignment: inline_alignment,
flip_anchor: shared_fragment.original_parent_writing_mode.is_bidi_ltr() !=
indefinite_containing_block.style.writing_mode.is_bidi_ltr(),
containing_block_writing_mode.is_bidi_ltr(),
};
// When the "static-position rect" doesn't come into play, we re-resolve "align-self"
@ -521,12 +520,15 @@ impl HoistedAbsolutelyPositionedBox {
true => style.clone_align_self().0 .0,
false => shared_fragment.resolved_alignment.block,
};
let block_axis_solver = AbsoluteAxisSolver {
let mut block_axis_solver = AbsoluteAxisSolver {
axis: AxisDirection::Block,
containing_size: cbbs,
padding_border_sum: pbm.padding_border_sums.block,
computed_margin_start: pbm.margin.block_start,
computed_margin_end: pbm.margin.block_end,
computed_size: computed_size.block,
computed_min_size: computed_min_size.block,
computed_max_size: computed_max_size.block,
avoid_negative_margin_start: false,
box_offsets: block_box_offsets,
static_position_rect_axis: static_position_rect.get_axis(AxisDirection::Block),
@ -534,168 +536,90 @@ impl HoistedAbsolutelyPositionedBox {
flip_anchor: false,
};
let mut inline_axis = inline_axis_solver.solve_for_size(computed_size.inline);
let mut block_axis = block_axis_solver.solve_for_size(computed_size.block);
// We can solve the inline axis, but the block one can depend on layout results,
// so we may have to resolve it properly later on.
let mut inline_axis = inline_axis_solver.solve(Some(|| {
let block_size = computed_size.block.map(|v| {
v.clamp_between_extremums(computed_min_size.block, computed_max_size.block)
});
let containing_block_for_children =
IndefiniteContainingBlock::new_for_style_and_block_size(&style, block_size);
context
.inline_content_sizes(
layout_context,
&containing_block_for_children,
&containing_block.into(),
)
.sizes
}));
let mut block_axis = block_axis_solver.solve_tentatively();
let mut positioning_context = PositioningContext::new_for_style(&style).unwrap();
let mut new_fragment = {
let content_size: LogicalVec2<Au>;
let fragments;
match &mut absolutely_positioned_box.context {
match context {
IndependentFormattingContext::Replaced(replaced) => {
// https://drafts.csswg.org/css2/visudet.html#abs-replaced-width
// https://drafts.csswg.org/css2/visudet.html#abs-replaced-height
content_size = computed_size.auto_is(|| unreachable!());
fragments = replaced.contents.make_fragments(
&style,
&containing_block.into(),
containing_block,
content_size.to_physical_size(containing_block_writing_mode),
);
},
IndependentFormattingContext::NonReplaced(non_replaced) => {
// https://drafts.csswg.org/css2/#min-max-widths
// https://drafts.csswg.org/css2/#min-max-heights
let min_size = non_replaced
.style
.content_min_box_size_deprecated(&indefinite_containing_block, &pbm)
.map(|t| t.map(Au::from).auto_is(Au::zero));
let max_size = style
.content_max_box_size_deprecated(&containing_block.into(), &pbm)
.map(|t| t.map(Au::from));
// https://drafts.csswg.org/css2/visudet.html#abs-non-replaced-width
// https://drafts.csswg.org/css2/visudet.html#abs-non-replaced-height
let mut inline_size = inline_axis.size.auto_is(|| {
let anchor = match inline_axis.anchor {
Anchor::Start(start) => start,
Anchor::End(end) => end,
};
let margin_sum = inline_axis.margin_start + inline_axis.margin_end;
let available_size =
cbis - anchor - pbm.padding_border_sums.inline - margin_sum;
let style = non_replaced.style.clone();
let containing_block_for_children =
IndefiniteContainingBlock::from(containing_block)
.new_for_intrinsic_inline_size_of_child(
&style,
&LogicalVec2::zero(),
);
non_replaced
.inline_content_sizes(layout_context, &containing_block_for_children)
.sizes
.shrink_to_fit(available_size)
});
// If the tentative used inline size is greater than max-inline-size,
// recalculate the inline size and margins with max-inline-size as the
// computed inline-size. We can assume the new inline size wont be auto,
// because a non-auto computed inline-size always becomes the used value.
// https://drafts.csswg.org/css2/#min-max-widths (step 2)
if let Some(max) = max_size.inline {
if inline_size > max {
inline_axis =
inline_axis_solver.solve_for_size(AuOrAuto::LengthPercentage(max));
inline_size = inline_axis.size.auto_is(|| unreachable!());
}
}
// If the tentative used inline size is less than min-inline-size,
// recalculate the inline size and margins with min-inline-size as the
// computed inline-size. We can assume the new inline size wont be auto,
// because a non-auto computed inline-size always becomes the used value.
// https://drafts.csswg.org/css2/#min-max-widths (step 3)
if inline_size < min_size.inline {
inline_axis = inline_axis_solver
.solve_for_size(AuOrAuto::LengthPercentage(min_size.inline));
inline_size = inline_axis.size.auto_is(|| unreachable!());
}
struct Result {
content_size: LogicalVec2<Au>,
fragments: Vec<Fragment>,
}
// If we end up recalculating the block size and margins below, we also need
// to relayout the children with a containing block of that size, otherwise
// percentages may be resolved incorrectly.
let mut try_layout = |size| {
let inline_size = inline_axis.size.non_auto().unwrap();
let containing_block_for_children = ContainingBlock {
inline_size,
block_size: size,
block_size: block_axis.size,
style: &style,
};
// https://drafts.csswg.org/css-writing-modes/#orthogonal-flows
assert_eq!(
containing_block.style.writing_mode.is_horizontal(),
containing_block_for_children
.style
.writing_mode
.is_horizontal(),
containing_block_writing_mode.is_horizontal(),
style.writing_mode.is_horizontal(),
"Mixed horizontal and vertical writing modes are not supported yet"
);
// Clear the context since we will lay out the same descendants
// more than once. Otherwise, absolute descendants will create
// multiple fragments which could later lead to double-borrow
// errors.
positioning_context.clear();
let independent_layout = non_replaced.layout(
layout_context,
&mut positioning_context,
&containing_block_for_children,
&containing_block.into(),
containing_block,
);
let (block_size, inline_size) =
match independent_layout.content_inline_size_for_table {
Some(inline_size) => {
(independent_layout.content_block_size, inline_size)
let (block_size, inline_size) = match independent_layout
.content_inline_size_for_table
{
Some(table_inline_size) => {
// Tables can override their sizes regardless of the sizing properties,
// so we may need to solve again to update margins.
if inline_size != table_inline_size {
inline_axis = inline_axis_solver.solve_with_size(table_inline_size);
}
let table_block_size = independent_layout.content_block_size;
if block_axis.size != AuOrAuto::LengthPercentage(table_block_size) {
block_axis = block_axis_solver.solve_with_size(table_block_size);
}
(table_block_size, table_inline_size)
},
None => {
// Now we can properly solve the block size.
block_axis = block_axis_solver
.solve(Some(|| independent_layout.content_block_size.into()));
(block_axis.size.non_auto().unwrap(), inline_size)
},
None => (
size.auto_is(|| independent_layout.content_block_size),
inline_size,
),
};
Result {
content_size: LogicalVec2 {
content_size = LogicalVec2 {
inline: inline_size,
block: block_size,
},
fragments: independent_layout.fragments,
}
};
let mut result = try_layout(block_axis.size);
// If the tentative used block size is greater than max-block-size,
// recalculate the block size and margins with max-block-size as the
// computed block-size. We can assume the new block size wont be auto,
// because a non-auto computed block-size always becomes the used value.
// https://drafts.csswg.org/css2/#min-max-heights (step 2)
if let Some(max) = max_size.block {
if result.content_size.block > max {
block_axis =
block_axis_solver.solve_for_size(AuOrAuto::LengthPercentage(max));
result = try_layout(AuOrAuto::LengthPercentage(max));
}
}
// If the tentative used block size is less than min-block-size,
// recalculate the block size and margins with min-block-size as the
// computed block-size. We can assume the new block size wont be auto,
// because a non-auto computed block-size always becomes the used value.
// https://drafts.csswg.org/css2/#min-max-heights (step 3)
if result.content_size.block < min_size.block {
block_axis = block_axis_solver
.solve_for_size(AuOrAuto::LengthPercentage(min_size.block));
result = try_layout(AuOrAuto::LengthPercentage(min_size.block));
}
content_size = result.content_size;
fragments = result.fragments;
fragments = independent_layout.fragments;
},
};
@ -736,10 +660,10 @@ impl HoistedAbsolutelyPositionedBox {
inline_axis_solver.solve_alignment(margin_box_rect, &mut content_rect);
BoxFragment::new(
absolutely_positioned_box.context.base_fragment_info(),
context.base_fragment_info(),
style,
fragments,
content_rect.to_physical(Some(&containing_block.into())),
content_rect.to_physical(Some(containing_block)),
pbm.padding.to_physical(containing_block_writing_mode),
pbm.border.to_physical(containing_block_writing_mode),
margin.to_physical(containing_block_writing_mode),
@ -800,11 +724,21 @@ impl AbsoluteBoxOffsets<'_> {
!self.start.is_auto() || !self.end.is_auto()
}
}
enum Anchor {
Start(Au),
End(Au),
}
impl Anchor {
fn inset(&self) -> Au {
match self {
Self::Start(start) => *start,
Self::End(end) => *end,
}
}
}
struct AxisResult {
anchor: Anchor,
size: AuOrAuto,
@ -818,12 +752,16 @@ struct AbsoluteAxisSolver<'a> {
padding_border_sum: Au,
computed_margin_start: AuOrAuto,
computed_margin_end: AuOrAuto,
computed_size: AuOrAuto,
computed_min_size: Au,
computed_max_size: Option<Au>,
avoid_negative_margin_start: bool,
box_offsets: AbsoluteBoxOffsets<'a>,
static_position_rect_axis: RectAxis,
alignment: AlignFlags,
flip_anchor: bool,
}
impl<'a> AbsoluteAxisSolver<'a> {
/// This unifies some of the parts in common in:
///
@ -836,78 +774,80 @@ impl<'a> AbsoluteAxisSolver<'a> {
/// * <https://drafts.csswg.org/css2/visudet.html#abs-replaced-height>
///
/// In the replaced case, `size` is never `Auto`.
fn solve_for_size(&self, computed_size: AuOrAuto) -> AxisResult {
fn solve(&self, get_content_size: Option<impl FnOnce() -> ContentSizes>) -> AxisResult {
let solve_for_anchor = |anchor: Anchor| {
let margin_start = self.computed_margin_start.auto_is(Au::zero);
let margin_end = self.computed_margin_end.auto_is(Au::zero);
let size = self
.computed_size
.non_auto()
.or_else(|| {
let content_size = get_content_size?();
let available_size = self.containing_size -
anchor.inset() -
self.padding_border_sum -
margin_start -
margin_end;
Some(content_size.shrink_to_fit(available_size))
})
.map(|size| {
size.clamp_between_extremums(self.computed_min_size, self.computed_max_size)
})
.map_or(AuOrAuto::Auto, AuOrAuto::LengthPercentage);
AxisResult {
anchor,
size,
margin_start,
margin_end,
}
};
match (
self.box_offsets.start.non_auto(),
self.box_offsets.end.non_auto(),
) {
(None, None) => AxisResult {
anchor: if self.flip_anchor {
(None, None) => solve_for_anchor(if self.flip_anchor {
Anchor::End(self.containing_size - self.static_position_rect_axis.origin)
} else {
Anchor::Start(self.static_position_rect_axis.origin)
}),
(Some(start), None) => {
solve_for_anchor(Anchor::Start(start.to_used_value(self.containing_size)))
},
size: computed_size,
margin_start: self.computed_margin_start.auto_is(Au::zero),
margin_end: self.computed_margin_end.auto_is(Au::zero),
},
(Some(start), None) => AxisResult {
anchor: Anchor::Start(start.to_used_value(self.containing_size)),
size: computed_size,
margin_start: self.computed_margin_start.auto_is(Au::zero),
margin_end: self.computed_margin_end.auto_is(Au::zero),
},
(None, Some(end)) => AxisResult {
anchor: Anchor::End(end.to_used_value(self.containing_size)),
size: computed_size,
margin_start: self.computed_margin_start.auto_is(Au::zero),
margin_end: self.computed_margin_end.auto_is(Au::zero),
(None, Some(end)) => {
solve_for_anchor(Anchor::End(end.to_used_value(self.containing_size)))
},
(Some(start), Some(end)) => {
let start = start.to_used_value(self.containing_size);
let end = end.to_used_value(self.containing_size);
let margin_start;
let margin_end;
let used_size;
if let AuOrAuto::LengthPercentage(s) = computed_size {
used_size = s;
let margins = self.containing_size - start - end - self.padding_border_sum - s;
let mut free_space = self.containing_size - start - end - self.padding_border_sum;
let used_size = self
.computed_size
.auto_is(|| {
free_space -
self.computed_margin_start.auto_is(Au::zero) -
self.computed_margin_end.auto_is(Au::zero)
})
.clamp_between_extremums(self.computed_min_size, self.computed_max_size);
free_space -= used_size;
let (margin_start, margin_end) =
match (self.computed_margin_start, self.computed_margin_end) {
(AuOrAuto::Auto, AuOrAuto::Auto) => {
if self.avoid_negative_margin_start && margins < Au::zero() {
margin_start = Au::zero();
margin_end = margins;
if self.avoid_negative_margin_start && free_space < Au::zero() {
(Au::zero(), free_space)
} else {
margin_start = margins / 2;
margin_end = margins - margin_start;
let margin_start = free_space / 2;
(margin_start, free_space - margin_start)
}
},
(AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => {
margin_start = margins - end;
margin_end = end;
(free_space - end, end)
},
(AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => {
margin_start = start;
margin_end = margins - start;
(start, free_space - start)
},
(AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => {
margin_start = start;
margin_end = end;
(start, end)
},
}
} else {
margin_start = self.computed_margin_start.auto_is(Au::zero);
margin_end = self.computed_margin_end.auto_is(Au::zero);
// This may be negative, but the caller will later effectively
// clamp it to min-inline-size or min-block-size.
used_size = self.containing_size -
start -
end -
self.padding_border_sum -
margin_start -
margin_end;
};
AxisResult {
anchor: Anchor::Start(start),
@ -919,6 +859,26 @@ impl<'a> AbsoluteAxisSolver<'a> {
}
}
fn solve_tentatively(&mut self) -> AxisResult {
self.solve(None::<fn() -> ContentSizes>)
}
fn solve_with_size(&mut self, size: Au) -> AxisResult {
// Override sizes
let old_size = mem::replace(&mut self.computed_size, AuOrAuto::LengthPercentage(size));
let old_min_size = mem::replace(&mut self.computed_min_size, Au::zero());
let old_max_size = mem::replace(&mut self.computed_max_size, None);
let result = self.solve_tentatively();
// Restore original sizes
self.computed_size = old_size;
self.computed_min_size = old_min_size;
self.computed_max_size = old_max_size;
result
}
fn origin_for_alignment_or_justification(&self, margin_box_axis: RectAxis) -> Option<Au> {
let alignment_container = match (
self.box_offsets.start.non_auto(),

View file

@ -1,2 +0,0 @@
[position-absolute-center-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[position-absolute-center-007.html]
expected: FAIL