Skip to content

Commit

Permalink
Fix stretch sizes on replaced abspos (servo#34430)
Browse files Browse the repository at this point in the history
We were sizing absolutely positioned replaced elements within their
actual containing block instead of the inset-modified containing block.
Then the `stretch` keyword would result in a wrong size.

Signed-off-by: Oriol Brufau <[email protected]>
  • Loading branch information
Loirooriol authored Dec 5, 2024
1 parent 54761b4 commit 5201c84
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 120 deletions.
246 changes: 128 additions & 118 deletions components/layout_2020/positioned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,29 +458,9 @@ impl HoistedAbsolutelyPositionedBox {
let style = context.style().clone();
let containing_block = &containing_block.into();
let pbm = style.padding_border_margin(containing_block);

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 content_box_sizes_and_pbm =
style.content_box_sizes_and_padding_border_margin(&containing_block.into());
let used_size = replaced
.contents
.used_size_as_if_inline_element(
containing_block,
&style,
&content_box_sizes_and_pbm,
)
.map(|size| Size::Numeric(*size));
(used_size, Default::default(), Default::default())
},
IndependentFormattingContext::NonReplaced(_) => (
style.content_box_size(containing_block, &pbm),
style.content_min_box_size(containing_block, &pbm),
style.content_max_box_size(containing_block, &pbm),
),
};
let computed_size = style.content_box_size(containing_block, &pbm);
let computed_min_size = style.content_min_box_size(containing_block, &pbm);
let computed_max_size = style.content_max_box_size(containing_block, &pbm);

let shared_fragment = self.fragment.borrow();
let static_position_rect = shared_fragment
Expand Down Expand Up @@ -541,6 +521,28 @@ impl HoistedAbsolutelyPositionedBox {
flip_anchor: false,
};

if let IndependentFormattingContext::Replaced(replaced) = context {
// https://drafts.csswg.org/css2/visudet.html#abs-replaced-width
// https://drafts.csswg.org/css2/visudet.html#abs-replaced-height
let inset_sums = LogicalVec2 {
inline: inline_axis_solver.inset_sum(),
block: block_axis_solver.inset_sum(),
};
let used_size = replaced
.contents
.used_size_as_if_inline_element_from_content_box_sizes(
containing_block,
&style,
replaced.preferred_aspect_ratio(&pbm.padding_border_sums),
computed_size,
computed_min_size,
computed_max_size,
pbm.padding_border_sums + pbm.margin.auto_is(Au::zero).sum() + inset_sums,
);
inline_axis_solver.override_size(used_size.inline);
block_axis_solver.override_size(used_size.block);
}

// The block axis can depend on layout results, so we only solve it tentatively,
// we may have to resolve it properly later on.
let mut block_axis = block_axis_solver.solve_tentatively();
Expand All @@ -563,7 +565,10 @@ impl HoistedAbsolutelyPositionedBox {
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.map(|size| size.to_numeric().unwrap());
content_size = LogicalVec2 {
inline: inline_axis.size.to_definite().unwrap(),
block: block_axis.size.to_definite().unwrap(),
};
fragments = replaced.contents.make_fragments(
&style,
content_size.to_physical_size(containing_block_writing_mode),
Expand Down Expand Up @@ -592,28 +597,29 @@ impl HoistedAbsolutelyPositionedBox {
containing_block,
);

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 != SizeConstraint::Definite(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.to_definite().unwrap(), 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_solver.override_size(table_inline_size);
inline_axis = inline_axis_solver.solve_tentatively();
}
let table_block_size = independent_layout.content_block_size;
if block_axis.size != SizeConstraint::Definite(table_block_size) {
block_axis_solver.override_size(table_block_size);
block_axis = block_axis_solver.solve_tentatively();
}
(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.to_definite().unwrap(), inline_size)
},
};

content_size = LogicalVec2 {
inline: inline_size,
Expand Down Expand Up @@ -718,6 +724,10 @@ impl AbsoluteBoxOffsets<'_> {
pub(crate) fn either_specified(&self) -> bool {
!self.start.is_auto() || !self.end.is_auto()
}

pub(crate) fn either_auto(&self) -> bool {
self.start.is_auto() || self.end.is_auto()
}
}

struct AxisResult {
Expand All @@ -742,6 +752,32 @@ struct AbsoluteAxisSolver<'a> {
}

impl<'a> AbsoluteAxisSolver<'a> {
/// Returns the amount that we need to subtract from the containing block size in order to
/// obtain the inset-modified containing block that we will use for sizing purposes.
/// (Note that for alignment purposes, we may re-resolve auto insets to a different value.)
/// <https://drafts.csswg.org/css-position/#resolving-insets>
fn inset_sum(&self) -> Au {
match (
self.box_offsets.start.non_auto(),
self.box_offsets.end.non_auto(),
) {
(None, None) => {
if self.flip_anchor {
self.containing_size -
self.static_position_rect_axis.origin -
self.static_position_rect_axis.length
} else {
self.static_position_rect_axis.origin
}
},
(Some(start), None) => start.to_used_value(self.containing_size),
(None, Some(end)) => end.to_used_value(self.containing_size),
(Some(start), Some(end)) => {
start.to_used_value(self.containing_size) + end.to_used_value(self.containing_size)
},
}
}

/// This unifies some of the parts in common in:
///
/// * <https://drafts.csswg.org/css2/visudet.html#abs-non-replaced-width>
Expand Down Expand Up @@ -789,95 +825,69 @@ impl<'a> AbsoluteAxisSolver<'a> {
SizeConstraint::new(preferred_size, min_size, max_size)
}
};
let solve_for_inset = |inset| {
if self.box_offsets.either_auto() {
let margin_start = self.computed_margin_start.auto_is(Au::zero);
let margin_end = self.computed_margin_end.auto_is(Au::zero);
let stretch_size =
self.containing_size - inset - self.padding_border_sum - margin_start - margin_end;
let stretch_size = self.containing_size -
self.inset_sum() -
self.padding_border_sum -
margin_start -
margin_end;
let size = solve_size(Size::FitContent, stretch_size);
AxisResult {
size,
margin_start,
margin_end,
}
};
match (
self.box_offsets.start.non_auto(),
self.box_offsets.end.non_auto(),
) {
(None, None) => solve_for_inset(if self.flip_anchor {
self.containing_size -
self.static_position_rect_axis.origin -
self.static_position_rect_axis.length
} else {
let mut free_space = self.containing_size - self.inset_sum() - self.padding_border_sum;
let stretch_size = free_space -
self.computed_margin_start.auto_is(Au::zero) -
self.computed_margin_end.auto_is(Au::zero);
let initial_behavior = match self.alignment.value() {
AlignFlags::STRETCH | AlignFlags::NORMAL | AlignFlags::AUTO => Size::Stretch,
_ => Size::FitContent,
};
let size = solve_size(initial_behavior, stretch_size);
if let Some(used_size) = size.to_definite() {
free_space -= used_size;
} else {
self.static_position_rect_axis.origin
}),
(Some(start), None) => solve_for_inset(start.to_used_value(self.containing_size)),
(None, Some(end)) => solve_for_inset(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 mut free_space = self.containing_size - start - end - self.padding_border_sum;
let stretch_size = free_space -
self.computed_margin_start.auto_is(Au::zero) -
self.computed_margin_end.auto_is(Au::zero);
let initial_behavior = match self.alignment.value() {
AlignFlags::STRETCH | AlignFlags::NORMAL | AlignFlags::AUTO => Size::Stretch,
_ => Size::FitContent,
free_space = Au::zero();
}
let (margin_start, margin_end) =
match (self.computed_margin_start, self.computed_margin_end) {
(AuOrAuto::Auto, AuOrAuto::Auto) => {
if self.avoid_negative_margin_start && free_space < Au::zero() {
(Au::zero(), free_space)
} else {
let margin_start = free_space / 2;
(margin_start, free_space - margin_start)
}
},
(AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => (free_space - end, end),
(AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => {
(start, free_space - start)
},
(AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => {
(start, end)
},
};
let size = solve_size(initial_behavior, stretch_size);
if let Some(used_size) = size.to_definite() {
free_space -= used_size;
} else {
free_space = Au::zero();
}
let (margin_start, margin_end) =
match (self.computed_margin_start, self.computed_margin_end) {
(AuOrAuto::Auto, AuOrAuto::Auto) => {
if self.avoid_negative_margin_start && free_space < Au::zero() {
(Au::zero(), free_space)
} else {
let margin_start = free_space / 2;
(margin_start, free_space - margin_start)
}
},
(AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => {
(free_space - end, end)
},
(AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => {
(start, free_space - start)
},
(AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => {
(start, end)
},
};
AxisResult {
size,
margin_start,
margin_end,
}
},
AxisResult {
size,
margin_start,
margin_end,
}
}
}

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, Size::Numeric(size));
let old_min_size = mem::take(&mut self.computed_min_size);
let old_max_size = mem::take(&mut self.computed_max_size);

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 override_size(&mut self, size: Au) {
self.computed_size = Size::Numeric(size);
self.computed_min_size = Size::default();
self.computed_max_size = Size::default();
}

fn origin_for_margin_box(
Expand Down
26 changes: 26 additions & 0 deletions tests/wpt/meta/MANIFEST.json
Original file line number Diff line number Diff line change
Expand Up @@ -242884,6 +242884,32 @@
],
{}
]
],
"positioned-replaced-2.html": [
"9a991d236ef2ddd7b8df1d4bff43578e5e741a68",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square-only.html",
"=="
]
],
{}
]
],
"positioned-replaced-3.html": [
"c11fcfd9521fc212929524e63f02a4ea218052c2",
[
null,
[
[
"/css/reference/ref-filled-green-100px-square-only.html",
"=="
]
],
{}
]
]
},
"svg-intrinsic-size-005.html": [
Expand Down

This file was deleted.

18 changes: 18 additions & 0 deletions tests/wpt/tests/css/css-sizing/stretch/positioned-replaced-2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<link rel="author" title="Oriol Brufau" href="mailto:[email protected]">
<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
<link rel="help" href="https://github.com/servo/servo/pull/34430">
<p>Test passes if there is a filled green square.</p>
<style>
canvas {
position: absolute;
background: green;
width: stretch;
height: stretch;
top: 50px;
left: 50px;
}
</style>
<div style="display: flow-root; position: relative; width: 150px; height: 150px; margin-top: -50px; margin-left: -50px;">
<canvas width="50" height="25"></canvas>
</div>
17 changes: 17 additions & 0 deletions tests/wpt/tests/css/css-sizing/stretch/positioned-replaced-3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<link rel="author" title="Oriol Brufau" href="mailto:[email protected]">
<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html">
<link rel="help" href="https://github.com/servo/servo/pull/34430">
<p>Test passes if there is a filled green square.</p>
<style>
canvas {
position: absolute;
background: green;
width: stretch;
height: stretch;
inset: 50px;
}
</style>
<div style="display: flow-root; position: relative; width: 200px; height: 200px; margin-top: -50px; margin-left: -50px;">
<canvas width="50" height="25"></canvas>
</div>

0 comments on commit 5201c84

Please sign in to comment.