layout: Paint collapsed table borders on their own (#35075)

We were previously splitting collapsed borders into two halves, and then
paint each one as part of the corresponding cell. This looked wrong when
the border style wasn't solid, or when a cell spanned multiple tracks
and the border wasn't the same for all of them.

Now the borders of a table wrapper, table grid or table cell aren't
painted in collapsed borders mode. Instead, the resulting collapsed
borders are painted on their own.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-01-21 05:10:27 -08:00 committed by GitHub
parent e43baed585
commit d00d76c1e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 208 additions and 124 deletions

View file

@ -47,7 +47,9 @@ use crate::fragment_tree::{
BackgroundMode, BoxFragment, Fragment, FragmentFlags, FragmentTree, SpecificLayoutInfo, Tag,
TextFragment,
};
use crate::geom::{LengthPercentageOrAuto, PhysicalPoint, PhysicalRect};
use crate::geom::{
LengthPercentageOrAuto, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize,
};
use crate::replaced::NaturalSizes;
use crate::style_ext::{BorderStyleColor, ComputedValuesExt};
@ -650,9 +652,16 @@ impl<'a> BuilderForBoxFragment<'a> {
return;
}
if section == StackingContextSection::Outline {
self.build_outline(builder);
return;
match section {
StackingContextSection::CollapsedTableBorders => {
self.build_collapsed_table_borders(builder);
return;
},
StackingContextSection::Outline => {
self.build_outline(builder);
return;
},
_ => {},
}
self.build_hit_test(builder, self.border_rect);
@ -893,7 +902,90 @@ impl<'a> BuilderForBoxFragment<'a> {
}
}
fn build_collapsed_table_borders(&mut self, builder: &mut DisplayListBuilder) {
let Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(table_info)) =
&self.fragment.detailed_layout_info
else {
return;
};
let mut common =
builder.common_properties(units::LayoutRect::default(), &self.fragment.style);
let radius = wr::BorderRadius::default();
let mut column_sum = Au::zero();
for (x, column_size) in table_info.track_sizes.x.iter().enumerate() {
let mut row_sum = Au::zero();
for (y, row_size) in table_info.track_sizes.y.iter().enumerate() {
let left_border = &table_info.collapsed_borders.x[x].list[y];
let right_border = &table_info.collapsed_borders.x[x + 1].list[y];
let top_border = &table_info.collapsed_borders.y[y].list[x];
let bottom_border = &table_info.collapsed_borders.y[y + 1].list[x];
let details = wr::BorderDetails::Normal(wr::NormalBorder {
left: self.build_border_side(left_border.style_color.clone()),
right: self.build_border_side(right_border.style_color.clone()),
top: self.build_border_side(top_border.style_color.clone()),
bottom: self.build_border_side(bottom_border.style_color.clone()),
radius,
do_aa: true,
});
let mut border_widths = PhysicalSides::new(
top_border.width,
right_border.width,
bottom_border.width,
left_border.width,
);
let mut origin = PhysicalPoint::new(column_sum, row_sum);
let mut size = PhysicalSize::new(*column_size, *row_size);
if x == 0 {
origin.x -= table_info.wrapper_border.left;
size.width += table_info.wrapper_border.left;
} else {
border_widths.left = Au::zero();
origin.x += left_border.width / 2;
size.width -= left_border.width / 2;
}
if y == 0 {
origin.y -= table_info.wrapper_border.top;
size.height += table_info.wrapper_border.top;
} else {
border_widths.top = Au::zero();
origin.y += top_border.width / 2;
size.height -= top_border.width / 2;
}
if x + 1 == table_info.track_sizes.x.len() {
size.width += table_info.wrapper_border.right;
} else {
size.width += border_widths.right / 2;
}
if y + 1 == table_info.track_sizes.y.len() {
size.height += table_info.wrapper_border.bottom;
} else {
size.height += border_widths.bottom / 2;
}
let border_rect = PhysicalRect::new(origin, size)
.translate(self.fragment.content_rect.origin.to_vector())
.translate(self.containing_block.origin.to_vector())
.to_webrender();
common.clip_rect = border_rect;
builder.wr().push_border(
&common,
border_rect,
border_widths.to_webrender(),
details,
);
row_sum += *row_size;
}
column_sum += *column_size;
}
}
fn build_border(&mut self, builder: &mut DisplayListBuilder) {
if self.fragment.has_collapsed_borders() {
// Avoid painting borders for tables and table parts in collapsed-borders mode,
// since the resulting collapsed borders are painted on their own in a special way.
return;
}
let border = self.fragment.style.get_border();
let border_widths = self.fragment.border.to_webrender();
@ -907,12 +999,7 @@ impl<'a> BuilderForBoxFragment<'a> {
return;
}
let style_color = match &self.fragment.detailed_layout_info {
Some(SpecificLayoutInfo::TableGridOrTableCell(table_info)) => {
table_info.border_style_color.clone()
},
_ => BorderStyleColor::from_border(border),
};
let style_color = BorderStyleColor::from_border(border);
let details = wr::BorderDetails::Normal(wr::NormalBorder {
top: self.build_border_side(style_color.top),
right: self.build_border_side(style_color.right),

View file

@ -34,7 +34,8 @@ use super::DisplayList;
use crate::display_list::conversions::{FilterToWebRender, ToWebRender};
use crate::display_list::{BuilderForBoxFragment, DisplayListBuilder};
use crate::fragment_tree::{
BoxFragment, ContainingBlockManager, Fragment, FragmentFlags, FragmentTree, PositioningFragment,
BoxFragment, ContainingBlockManager, Fragment, FragmentFlags, FragmentTree,
PositioningFragment, SpecificLayoutInfo,
};
use crate::geom::{AuOrAuto, PhysicalRect, PhysicalSides};
use crate::style_ext::ComputedValuesExt;
@ -87,6 +88,7 @@ pub(crate) type ContainingBlockInfo<'a> = ContainingBlockManager<'a, ContainingB
pub(crate) enum StackingContextSection {
OwnBackgroundsAndBorders,
DescendantBackgroundsAndBorders,
CollapsedTableBorders,
Foreground,
Outline,
}
@ -726,6 +728,16 @@ impl StackingContext {
child.build_display_list(builder, &self.atomic_inline_stacking_containers);
}
// Additional step 4.5: Collapsed table borders
// This step isn't in the spec, but other browsers seem to paint them at this point.
while contents.peek().is_some_and(|(_, child)| {
child.section() == StackingContextSection::CollapsedTableBorders
}) {
let (i, child) = contents.next().unwrap();
self.debug_push_print_item(DebugPrintField::Contents, i);
child.build_display_list(builder, &self.atomic_inline_stacking_containers);
}
// Step 5: Float stacking containers
for (i, child) in self.float_stacking_containers.iter().enumerate() {
self.debug_push_print_item(DebugPrintField::FloatStackingContainers, i);
@ -1181,30 +1193,34 @@ impl BoxFragment {
.for_absolute_and_fixed_descendants
.scroll_node_id
};
stacking_context
.contents
.push(StackingContextContent::Fragment {
scroll_node_id: new_scroll_node_id,
reference_frame_scroll_node_id: reference_frame_scroll_node_id_for_fragments,
clip_chain_id: new_clip_chain_id,
section: self.get_stacking_context_section(),
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
});
if !self.style.get_outline().outline_width.is_zero() {
let mut add_fragment = |section| {
stacking_context
.contents
.push(StackingContextContent::Fragment {
scroll_node_id: new_scroll_node_id,
reference_frame_scroll_node_id: reference_frame_scroll_node_id_for_fragments,
clip_chain_id: new_clip_chain_id,
section: StackingContextSection::Outline,
section,
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
});
};
add_fragment(self.get_stacking_context_section());
if let Fragment::Box(box_fragment) = &fragment {
if matches!(
box_fragment.borrow().detailed_layout_info,
Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(_))
) {
add_fragment(StackingContextSection::CollapsedTableBorders);
}
}
if !self.style.get_outline().outline_width.is_zero() {
add_fragment(StackingContextSection::Outline);
}
// We want to build the scroll frame after the background and border, because

View file

@ -6,6 +6,7 @@ use app_units::Au;
use atomic_refcell::AtomicRefCell;
use base::print_tree::PrintTree;
use servo_arc::Arc as ServoArc;
use style::computed_values::border_collapse::T as BorderCollapse;
use style::computed_values::overflow_x::T as ComputedOverflow;
use style::computed_values::position::T as ComputedPosition;
use style::logical_geometry::WritingMode;
@ -19,7 +20,7 @@ use crate::geom::{
AuOrAuto, LengthPercentageOrAuto, PhysicalPoint, PhysicalRect, PhysicalSides, ToLogical,
};
use crate::style_ext::ComputedValuesExt;
use crate::table::SpecificTableGridOrTableCellInfo;
use crate::table::SpecificTableGridInfo;
use crate::taffy::SpecificTaffyGridInfo;
/// Describes how a [`BoxFragment`] paints its background.
@ -43,7 +44,8 @@ pub(crate) struct ExtraBackground {
#[derive(Clone, Debug)]
pub(crate) enum SpecificLayoutInfo {
Grid(Box<SpecificTaffyGridInfo>),
TableGridOrTableCell(Box<SpecificTableGridOrTableCellInfo>),
TableCellWithCollapsedBorders,
TableGridWithCollapsedBorders(Box<SpecificTableGridInfo>),
TableWrapper,
}
@ -356,4 +358,15 @@ impl BoxFragment {
Some(SpecificLayoutInfo::TableWrapper)
)
}
pub(crate) fn has_collapsed_borders(&self) -> bool {
match &self.detailed_layout_info {
Some(SpecificLayoutInfo::TableCellWithCollapsedBorders) => true,
Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(_)) => true,
Some(SpecificLayoutInfo::TableWrapper) => {
self.style.get_inherited_table().border_collapse == BorderCollapse::Collapse
},
_ => false,
}
}
}

View file

@ -3,6 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use core::cmp::Ordering;
use std::mem;
use std::ops::Range;
use app_units::{Au, MAX_AU};
@ -24,7 +25,8 @@ use style::values::generics::box_::{GenericVerticalAlign as VerticalAlign, Verti
use style::Zero;
use super::{
ArcRefCell, Table, TableCaption, TableSlot, TableSlotCell, TableTrack, TableTrackGroup,
ArcRefCell, CollapsedBorder, CollapsedBorderLine, SpecificTableGridInfo, Table, TableCaption,
TableSlot, TableSlotCell, TableSlotCoordinates, TableTrack, TableTrackGroup,
};
use crate::context::LayoutContext;
use crate::formatting_contexts::{Baselines, IndependentLayout};
@ -34,29 +36,17 @@ use crate::fragment_tree::{
};
use crate::geom::{
AuOrAuto, LogicalRect, LogicalSides, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides,
Size, SizeConstraint, ToLogical, ToLogicalWithContainingBlock,
PhysicalVec, Size, SizeConstraint, ToLogical, ToLogicalWithContainingBlock,
};
use crate::positioned::{relative_adjustement, PositioningContext, PositioningContextLength};
use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult};
use crate::style_ext::{
BorderStyleColor, Clamp, ComputedValuesExt, LayoutStyle, PaddingBorderMargin,
};
use crate::table::{SpecificTableGridOrTableCellInfo, TableSlotCoordinates};
use crate::{
ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock, WritingMode,
};
fn detailed_layout_info(
border_style_color: Option<LogicalSides<BorderStyleColor>>,
writing_mode: WritingMode,
) -> Option<SpecificLayoutInfo> {
Some(SpecificLayoutInfo::TableGridOrTableCell(Box::new(
SpecificTableGridOrTableCellInfo {
border_style_color: border_style_color?.to_physical(writing_mode),
},
)))
}
/// A result of a final or speculative layout of a single cell in
/// the table. Note that this is only done for slots that are not
/// covered by spans or empty.
@ -64,7 +54,6 @@ struct CellLayout {
layout: IndependentLayout,
padding: LogicalSides<Au>,
border: LogicalSides<Au>,
detailed_layout_info: Option<SpecificLayoutInfo>,
positioning_context: PositioningContext,
}
@ -113,13 +102,6 @@ struct ColumnLayout {
percentage: Percentage,
}
/// A calculated collapsed border.
#[derive(Clone, Debug, Default, PartialEq)]
struct CollapsedBorder {
style_color: BorderStyleColor,
width: Au,
}
impl CollapsedBorder {
fn new(style_color: BorderStyleColor, width: Au) -> Self {
Self { style_color, width }
@ -180,13 +162,6 @@ impl PartialOrd for CollapsedBorder {
impl Eq for CollapsedBorder {}
/// Represents a piecewise sequence of collapsed borders along a line.
#[derive(Clone, Debug, Default)]
struct CollapsedBorderLine {
max_width: Au,
list: Vec<CollapsedBorder>,
}
impl CollapsedBorderLine {
fn max_assign(&mut self, collapsed_border: &CollapsedBorder, range: &Range<usize>) {
self.max_width.max_assign(collapsed_border.width);
@ -196,9 +171,6 @@ impl CollapsedBorderLine {
}
}
/// The calculated collapsed borders.
type CollapsedBorders = LogicalVec2<Vec<CollapsedBorderLine>>;
/// A helper struct that performs the layout of the box tree version
/// of a table into the fragment tree version. This implements
/// <https://drafts.csswg.org/css-tables/#table-layout-algorithm>
@ -222,7 +194,7 @@ pub(crate) struct TableLayout<'a> {
cells_laid_out: Vec<Vec<Option<CellLayout>>>,
basis_for_cell_padding_percentage: Au,
/// Information about collapsed borders.
collapsed_borders: Option<CollapsedBorders>,
collapsed_borders: Option<LogicalVec2<Vec<CollapsedBorderLine>>>,
}
#[derive(Clone, Debug)]
@ -1213,10 +1185,6 @@ impl<'a> TableLayout<'a> {
block_start: row_index,
block_end: row_index + cell.rowspan,
};
let detailed_layout_info = detailed_layout_info(
self.get_collapsed_border_style_colors_for_area(area),
self.table.style.writing_mode,
);
let layout_style = cell.layout_style();
let border = self
.get_collapsed_border_widths_for_area(area)
@ -1257,7 +1225,6 @@ impl<'a> TableLayout<'a> {
layout,
padding,
border,
detailed_layout_info,
positioning_context,
})
})
@ -1870,14 +1837,6 @@ impl<'a> TableLayout<'a> {
assert_eq!(self.table.size.height, self.row_sizes.len());
assert_eq!(self.table.size.width, self.distributed_column_widths.len());
let border_style_color = self.get_collapsed_border_style_colors_for_area(LogicalSides {
inline_start: 0,
inline_end: self.table.size.width,
block_start: 0,
block_end: self.table.size.height,
});
let detailed_layout_info = detailed_layout_info(border_style_color, table_writing_mode);
if self.table.size.width == 0 && self.table.size.height == 0 {
let content_rect = LogicalRect {
start_corner: LogicalVec2::zero(),
@ -1898,7 +1857,7 @@ impl<'a> TableLayout<'a> {
None, /* clearance */
CollapsedBlockMargins::zero(),
)
.with_detailed_layout_info(detailed_layout_info);
.with_detailed_layout_info(self.specific_layout_info_for_grid());
}
let mut table_fragments = Vec::new();
@ -2025,7 +1984,37 @@ impl<'a> TableLayout<'a> {
CollapsedBlockMargins::zero(),
)
.with_baselines(baselines)
.with_detailed_layout_info(detailed_layout_info)
.with_detailed_layout_info(self.specific_layout_info_for_grid())
}
fn specific_layout_info_for_grid(&mut self) -> Option<SpecificLayoutInfo> {
mem::take(&mut self.collapsed_borders).map(|mut collapsed_borders| {
let writing_mode = self.table.style.writing_mode;
let mut track_sizes = LogicalVec2 {
inline: mem::take(&mut self.distributed_column_widths),
block: mem::take(&mut self.row_sizes),
};
if !writing_mode.is_bidi_ltr() {
track_sizes.inline.reverse();
collapsed_borders.inline.reverse();
for border_line in &mut collapsed_borders.block {
border_line.list.reverse();
}
}
SpecificLayoutInfo::TableGridWithCollapsedBorders(Box::new(SpecificTableGridInfo {
collapsed_borders: if writing_mode.is_horizontal() {
PhysicalVec::new(collapsed_borders.inline, collapsed_borders.block)
} else {
PhysicalVec::new(collapsed_borders.block, collapsed_borders.inline)
},
track_sizes: if writing_mode.is_horizontal() {
PhysicalVec::new(track_sizes.inline, track_sizes.block)
} else {
PhysicalVec::new(track_sizes.block, track_sizes.inline)
},
wrapper_border: self.pbm.border.to_physical(writing_mode),
}))
})
}
fn is_row_collapsed(&self, row_index: usize) -> bool {
@ -2212,7 +2201,7 @@ impl<'a> TableLayout<'a> {
return;
}
let mut collapsed_borders = CollapsedBorders {
let mut collapsed_borders = LogicalVec2 {
block: vec![
CollapsedBorderLine {
max_width: Au::zero(),
@ -2318,30 +2307,6 @@ impl<'a> TableLayout<'a> {
},
})
}
fn get_collapsed_border_style_colors_for_area(
&self,
area: LogicalSides<usize>,
) -> Option<LogicalSides<BorderStyleColor>> {
let collapsed_borders = self.collapsed_borders.as_ref()?;
if self.table.size.width == 0 || self.table.size.height == 0 {
return Some(LogicalSides::default());
}
let inline_start = &collapsed_borders.inline[area.inline_start];
let inline_end = &collapsed_borders.inline[area.inline_end];
let block_start = &collapsed_borders.block[area.block_start];
let block_end = &collapsed_borders.block[area.block_end];
// This area may span multiple rows and columns, each of which can have different
// collapsed borders. However, we don't have support for one side of a box to have
// a piecewise border. Therefore, we just pick the first piece for the entire side.
Some(LogicalSides {
inline_start: inline_start.list[area.block_start].style_color.clone(),
inline_end: inline_end.list[area.block_start].style_color.clone(),
block_start: block_start.list[area.inline_start].style_color.clone(),
block_end: block_end.list[area.inline_start].style_color.clone(),
})
}
}
struct RowFragmentLayout<'a> {
@ -2922,6 +2887,10 @@ impl TableSlotCell {
);
positioning_context.append(layout.positioning_context);
let detailed_layout_info = (table_style.get_inherited_table().border_collapse ==
BorderCollapse::Collapse)
.then_some(SpecificLayoutInfo::TableCellWithCollapsedBorders);
BoxFragment::new(
base_fragment_info,
self.base.style.clone(),
@ -2934,7 +2903,7 @@ impl TableSlotCell {
CollapsedBlockMargins::zero(),
)
.with_baselines(layout.layout.baselines)
.with_detailed_layout_info(layout.detailed_layout_info)
.with_detailed_layout_info(detailed_layout_info)
}
}

View file

@ -70,6 +70,7 @@ mod layout;
use std::ops::Range;
use app_units::Au;
pub(crate) use construct::AnonymousTableContent;
pub use construct::TableBuilder;
use euclid::{Point2D, Size2D, UnknownUnit, Vector2D};
@ -83,7 +84,7 @@ use crate::cell::ArcRefCell;
use crate::flow::BlockContainer;
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::BaseFragmentInfo;
use crate::geom::PhysicalSides;
use crate::geom::{PhysicalSides, PhysicalVec};
use crate::layout_box_base::LayoutBoxBase;
use crate::style_ext::BorderStyleColor;
@ -317,9 +318,23 @@ pub struct TableCaption {
context: ArcRefCell<IndependentFormattingContext>,
}
#[derive(Clone, Debug)]
pub(crate) struct SpecificTableGridOrTableCellInfo {
/// For tables is in collapsed-borders mode, this is used as an override for the
/// style and color of the border of the table and table cells.
pub border_style_color: PhysicalSides<BorderStyleColor>,
/// A calculated collapsed border.
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct CollapsedBorder {
pub style_color: BorderStyleColor,
pub width: Au,
}
/// Represents a piecewise sequence of collapsed borders along a line.
#[derive(Clone, Debug, Default)]
pub(crate) struct CollapsedBorderLine {
max_width: Au,
pub list: Vec<CollapsedBorder>,
}
#[derive(Clone, Debug)]
pub(crate) struct SpecificTableGridInfo {
pub collapsed_borders: PhysicalVec<Vec<CollapsedBorderLine>>,
pub track_sizes: PhysicalVec<Vec<Au>>,
pub wrapper_border: PhysicalSides<Au>,
}

View file

@ -1,2 +0,0 @@
[block-formatting-contexts-003.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[border-conflict-element-001d.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[collapsing-border-model-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[collapsing-border-model-004.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[box-shadow-table-border-collapse-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ttwf-reftest-borderRadius.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[border-collapse-double-border.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[collapsed-border-paint-phase-002.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[collapsed-border-positioned-tr-td.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[356774-1.html]
expected: FAIL