layout: Make bottom table captions obey relative positioning offsets (#39388)

#33426 only added support for relative positioning on captions with
`caption-side: top`, but forgot about `caption-side: bottom`. This
unifies the logic for both kinds of captions to avoid divergences.

Testing: Modifying an existing test to also cover this case.
Fixes: #39386

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-09-19 14:44:17 +02:00 committed by GitHub
parent d08be14c7a
commit 754c938722
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 109 additions and 129 deletions

1
Cargo.lock generated
View file

@ -4881,6 +4881,7 @@ dependencies = [
"servo_malloc_size_of", "servo_malloc_size_of",
"servo_url", "servo_url",
"smallvec", "smallvec",
"strum",
"stylo", "stylo",
"stylo_atoms", "stylo_atoms",
"stylo_traits", "stylo_traits",

View file

@ -54,6 +54,7 @@ servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" } servo_geometry = { path = "../geometry" }
servo_url = { path = "../url" } servo_url = { path = "../url" }
smallvec = { workspace = true } smallvec = { workspace = true }
strum = { workspace = true }
stylo = { workspace = true } stylo = { workspace = true }
stylo_atoms = { workspace = true } stylo_atoms = { workspace = true }
stylo_traits = { workspace = true } stylo_traits = { workspace = true }

View file

@ -11,6 +11,7 @@ use atomic_refcell::AtomicRef;
use log::warn; use log::warn;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use servo_arc::Arc; use servo_arc::Arc;
use strum::{EnumIter, IntoEnumIterator};
use style::Zero; use style::Zero;
use style::computed_values::border_collapse::T as BorderCollapse; use style::computed_values::border_collapse::T as BorderCollapse;
use style::computed_values::box_sizing::T as BoxSizing; use style::computed_values::box_sizing::T as BoxSizing;
@ -1593,59 +1594,23 @@ impl<'a> TableLayout<'a> {
collapsible_margins_in_children: CollapsedBlockMargins::zero(), collapsible_margins_in_children: CollapsedBlockMargins::zero(),
}; };
table_layout #[derive(EnumIter, PartialEq)]
.fragments enum TableWrapperSection {
.extend(self.table.captions.iter().filter_map(|caption| { TopCaptions,
let caption = caption.borrow(); Grid,
if caption.context.style().clone_caption_side() != CaptionSide::Top { BottomCaptions,
return None; }
impl TableWrapperSection {
fn accepts_caption(&self, caption: &TableCaption) -> bool {
match caption.context.style().clone_caption_side() {
CaptionSide::Top => *self == TableWrapperSection::TopCaptions,
CaptionSide::Bottom => *self == TableWrapperSection::BottomCaptions,
}
}
} }
let original_positioning_context_length = positioning_context.len(); for section in TableWrapperSection::iter() {
let mut caption_fragment = if section == TableWrapperSection::Grid {
self.layout_caption(&caption, layout_context, positioning_context);
// The caption is not placed yet. Construct a rectangle for it in the adjusted containing block
// for the table children and only then convert the result to physical geometry.
let caption_pbm = caption_fragment
.padding_border_margin()
.to_logical(table_writing_mode);
let caption_relative_offset = match caption_fragment.style.clone_position() {
Position::Relative => {
relative_adjustement(&caption_fragment.style, containing_block_for_children)
},
_ => LogicalVec2::zero(),
};
caption_fragment.content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: offset_from_wrapper.inline_start + caption_pbm.inline_start,
block: current_block_offset + caption_pbm.block_start,
} + caption_relative_offset,
size: caption_fragment
.content_rect
.size
.to_logical(table_writing_mode),
}
.as_physical(Some(&containing_block_for_logical_conversion));
current_block_offset += caption_fragment
.margin_rect()
.size
.to_logical(table_writing_mode)
.block;
let caption_fragment = Fragment::Box(ArcRefCell::new(caption_fragment));
positioning_context.adjust_static_position_of_hoisted_fragments(
&caption_fragment,
original_positioning_context_length,
);
caption.context.base.set_fragment(caption_fragment.clone());
Some(caption_fragment)
}));
let original_positioning_context_length = positioning_context.len(); let original_positioning_context_length = positioning_context.len();
let mut grid_fragment = self.layout_grid( let mut grid_fragment = self.layout_grid(
layout_context, layout_context,
@ -1697,12 +1662,10 @@ impl<'a> TableLayout<'a> {
original_positioning_context_length, original_positioning_context_length,
); );
table_layout.fragments.push(grid_fragment); table_layout.fragments.push(grid_fragment);
} else {
table_layout let caption_fragments = self.table.captions.iter().filter_map(|caption| {
.fragments
.extend(self.table.captions.iter().filter_map(|caption| {
let caption = caption.borrow(); let caption = caption.borrow();
if caption.context.style().clone_caption_side() != CaptionSide::Bottom { if !section.accepts_caption(&caption) {
return None; return None;
} }
@ -1715,11 +1678,20 @@ impl<'a> TableLayout<'a> {
let caption_pbm = caption_fragment let caption_pbm = caption_fragment
.padding_border_margin() .padding_border_margin()
.to_logical(table_writing_mode); .to_logical(table_writing_mode);
let caption_relative_offset = match caption_fragment.style.clone_position() {
Position::Relative => relative_adjustement(
&caption_fragment.style,
containing_block_for_children,
),
_ => LogicalVec2::zero(),
};
caption_fragment.content_rect = LogicalRect { caption_fragment.content_rect = LogicalRect {
start_corner: LogicalVec2 { start_corner: LogicalVec2 {
inline: offset_from_wrapper.inline_start + caption_pbm.inline_start, inline: offset_from_wrapper.inline_start + caption_pbm.inline_start,
block: current_block_offset + caption_pbm.block_start, block: current_block_offset + caption_pbm.block_start,
}, } + caption_relative_offset,
size: caption_fragment size: caption_fragment
.content_rect .content_rect
.size .size
@ -1741,7 +1713,10 @@ impl<'a> TableLayout<'a> {
caption.context.base.set_fragment(caption_fragment.clone()); caption.context.base.set_fragment(caption_fragment.clone());
Some(caption_fragment) Some(caption_fragment)
})); });
table_layout.fragments.extend(caption_fragments);
}
}
table_layout.content_block_size = current_block_offset + offset_from_wrapper.block_end; table_layout.content_block_size = current_block_offset + offset_from_wrapper.block_end;
table_layout table_layout

View file

@ -257401,7 +257401,7 @@
] ]
], ],
"caption-relative-positioning.html": [ "caption-relative-positioning.html": [
"2be1e86bc077f1293ccc9ea851f23123ae679157", "083a39fdf6800bfbec43a418068573f014c8adc5",
[ [
null, null,
[ [

View file

@ -11,7 +11,7 @@
position: relative; position: relative;
background: green; background: green;
width: 100px; width: 100px;
height: 100px; height: 50px;
margin-left: 200px; margin-left: 200px;
left: -200px; left: -200px;
} }
@ -20,7 +20,10 @@
<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> <p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
<div style="width: 100px; background: red;"> <div style="width: 100px; background: red;">
<table> <table>
<caption></caption> <caption style="caption-side: top"></caption>
</table>
<table>
<caption style="caption-side: bottom"></caption>
</table> </table>
</div> </div>
</html> </html>