layout: Start work on table row height and vertical-align (#31246)

This implements a very naive row height allocation approach. It has just
enough to implement `vertical-align` in table cells. Rowspanned cells
get enough space for their content, with the extra space necessary being
allocated to the last row. There's still a lot missing here, including
proper distribution of row height to rowspanned cells.

Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2024-02-10 09:03:01 +01:00 committed by GitHub
parent 39b3beda5d
commit 35fb95ca85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 261 additions and 132 deletions

View file

@ -13,6 +13,7 @@ use style::computed_values::float::T as Float;
use style::logical_geometry::WritingMode;
use style::properties::ComputedValues;
use style::values::computed::{Length, LengthOrAuto};
use style::values::specified::Display;
use style::Zero;
use crate::cell::ArcRefCell;
@ -418,22 +419,33 @@ fn layout_block_level_children(
mut sequential_layout_state: Option<&mut SequentialLayoutState>,
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
) -> FlowLayout {
match sequential_layout_state {
let mut placement_state =
PlacementState::new(collapsible_with_parent_start_margin, containing_block.style);
let fragments = match sequential_layout_state {
Some(ref mut sequential_layout_state) => layout_block_level_children_sequentially(
layout_context,
positioning_context,
child_boxes,
containing_block,
sequential_layout_state,
collapsible_with_parent_start_margin,
&mut placement_state,
),
None => layout_block_level_children_in_parallel(
layout_context,
positioning_context,
child_boxes,
containing_block,
collapsible_with_parent_start_margin,
&mut placement_state,
),
};
let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish();
FlowLayout {
fragments,
content_block_size,
collapsible_margins_in_children,
baselines,
}
}
@ -442,8 +454,8 @@ fn layout_block_level_children_in_parallel(
positioning_context: &mut PositioningContext,
child_boxes: &[ArcRefCell<BlockLevelBox>],
containing_block: &ContainingBlock,
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
) -> FlowLayout {
placement_state: &mut PlacementState,
) -> Vec<Fragment> {
let collects_for_nearest_positioned_ancestor =
positioning_context.collects_for_nearest_positioned_ancestor();
let layout_results: Vec<(Fragment, PositioningContext)> = child_boxes
@ -462,8 +474,7 @@ fn layout_block_level_children_in_parallel(
})
.collect();
let mut placement_state = PlacementState::new(collapsible_with_parent_start_margin);
let fragments = layout_results
layout_results
.into_iter()
.map(|(mut fragment, mut child_positioning_context)| {
placement_state.place_fragment_and_update_baseline(&mut fragment, None);
@ -474,15 +485,7 @@ fn layout_block_level_children_in_parallel(
positioning_context.append(child_positioning_context);
fragment
})
.collect();
let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish();
FlowLayout {
fragments,
content_block_size,
collapsible_margins_in_children,
baselines,
}
.collect()
}
fn layout_block_level_children_sequentially(
@ -491,14 +494,12 @@ fn layout_block_level_children_sequentially(
child_boxes: &[ArcRefCell<BlockLevelBox>],
containing_block: &ContainingBlock,
sequential_layout_state: &mut SequentialLayoutState,
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
) -> FlowLayout {
let mut placement_state = PlacementState::new(collapsible_with_parent_start_margin);
placement_state: &mut PlacementState,
) -> Vec<Fragment> {
// Because floats are involved, we do layout for this block formatting context in tree
// order without parallelism. This enables mutable access to a `SequentialLayoutState` that
// tracks every float encountered so far (again in tree order).
let fragments = child_boxes
child_boxes
.iter()
.map(|child_box| {
let positioning_context_length_before_layout = positioning_context.len();
@ -521,15 +522,7 @@ fn layout_block_level_children_sequentially(
fragment
})
.collect();
let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish();
FlowLayout {
fragments,
content_block_size,
collapsible_margins_in_children,
baselines,
}
.collect()
}
impl BlockLevelBox {
@ -1387,12 +1380,16 @@ struct PlacementState {
current_margin: CollapsedMargin,
current_block_direction_position: Length,
inflow_baselines: Baselines,
is_inline_block_context: bool,
}
impl PlacementState {
fn new(
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
containing_block_style: &ComputedValues,
) -> PlacementState {
let is_inline_block_context =
containing_block_style.get_box().clone_display() == Display::InlineBlock;
PlacementState {
next_in_flow_margin_collapses_with_parent_start_margin:
collapsible_with_parent_start_margin.0,
@ -1401,6 +1398,7 @@ impl PlacementState {
current_margin: CollapsedMargin::zero(),
current_block_direction_position: Length::zero(),
inflow_baselines: Baselines::default(),
is_inline_block_context,
}
}
@ -1411,15 +1409,27 @@ impl PlacementState {
) {
self.place_fragment(fragment, sequential_layout_state);
if let Fragment::Box(box_fragment) = fragment {
let box_block_offset = box_fragment.content_rect.start_corner.block.into();
match (self.inflow_baselines.first, box_fragment.baselines.first) {
(None, Some(first)) => self.inflow_baselines.first = Some(first + box_block_offset),
_ => {},
}
if let Some(last) = box_fragment.baselines.last {
self.inflow_baselines.last = Some(last + box_block_offset);
}
let box_fragment = match fragment {
Fragment::Box(box_fragment) => box_fragment,
_ => return,
};
// From <https://drafts.csswg.org/css-align-3/#baseline-export>:
// > When finding the first/last baseline set of an inline-block, any baselines
// > contributed by table boxes must be skipped. (This quirk is a legacy behavior from
// > [CSS2].)
let display = box_fragment.style.clone_display();
let is_table = display == Display::Table;
if self.is_inline_block_context && is_table {
return;
}
let box_block_offset = box_fragment.content_rect.start_corner.block.into();
if let (None, Some(first)) = (self.inflow_baselines.first, box_fragment.baselines.first) {
self.inflow_baselines.first = Some(first + box_block_offset);
}
if let Some(last) = box_fragment.baselines.last {
self.inflow_baselines.last = Some(last + box_block_offset);
}
}

View file

@ -61,7 +61,7 @@ pub(crate) enum NonReplacedFormattingContextContents {
/// The baselines of a layout or a [`BoxFragment`]. Some layout uses the first and some layout uses
/// the last.
#[derive(Default, Serialize)]
#[derive(Debug, Default, Serialize)]
pub(crate) struct Baselines {
pub first: Option<Au>,
pub last: Option<Au>,

View file

@ -202,6 +202,7 @@ impl BoxFragment {
\nmargin={:?}\
\nclearance={:?}\
\nscrollable_overflow={:?}\
\nbaselines={:?}\
\noverflow={:?} / {:?}",
self.base,
self.content_rect,
@ -210,6 +211,7 @@ impl BoxFragment {
self.margin,
self.clearance,
self.scrollable_overflow(&PhysicalRect::zero()),
self.baselines,
self.style.get_box().overflow_x,
self.style.get_box().overflow_y,
));

View file

@ -107,11 +107,14 @@ impl Table {
}
}
let mut table = table_builder.finish();
table.anonymous = true;
IndependentFormattingContext::NonReplaced(NonReplacedFormattingContext {
base_fragment_info: (&anonymous_info).into(),
style: anonymous_style,
content_sizes: None,
contents: NonReplacedFormattingContextContents::Table(table_builder.finish()),
contents: NonReplacedFormattingContextContents::Table(table),
})
}
@ -226,6 +229,24 @@ impl TableBuilder {
for row in self.table.slots.iter_mut() {
row.resize_with(self.table.size.width, || TableSlot::Empty);
}
// Turn all rowspan=0 rows into the real value to avoid having to
// make the calculation continually during layout. In addition, make
// sure that there are no rowspans that extend past the end of the
// table.
for row_index in 0..self.table.size.height {
for cell in self.table.slots[row_index].iter_mut() {
if let TableSlot::Cell(ref mut cell) = cell {
let rowspan_to_end_of_table = self.table.size.height - row_index;
if cell.rowspan == 0 {
cell.rowspan = rowspan_to_end_of_table;
} else {
cell.rowspan = cell.rowspan.min(rowspan_to_end_of_table);
}
}
}
}
self.table
}

View file

@ -2,18 +2,21 @@
* 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::ops::Add;
use app_units::{Au, MAX_AU};
use euclid::num::Zero;
use log::warn;
use style::computed_values::border_collapse::T as BorderCollapse;
use style::logical_geometry::WritingMode;
use style::values::computed::{CSSPixelLength, Length, LengthOrAuto, Percentage};
use style::values::generics::box_::{GenericVerticalAlign as VerticalAlign, VerticalAlignKeyword};
use style::values::generics::length::GenericLengthPercentageOrAuto::{Auto, LengthPercentage};
use super::{Table, TableSlot, TableSlotCell};
use crate::context::LayoutContext;
use crate::formatting_contexts::{Baselines, IndependentLayout};
use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, Fragment};
use crate::fragment_tree::{AnonymousFragment, BoxFragment, CollapsedBlockMargins, Fragment};
use crate::geom::{LogicalRect, LogicalSides, LogicalVec2};
use crate::positioned::{PositioningContext, PositioningContextLength};
use crate::sizing::ContentSizes;
@ -29,7 +32,20 @@ struct CellLayout {
padding: LogicalSides<Length>,
border: LogicalSides<Length>,
positioning_context: PositioningContext,
rowspan: usize,
}
impl CellLayout {
fn ascent(&self) -> Au {
self.layout
.baselines
.first
.unwrap_or(self.layout.content_block_size)
}
/// The block size of this laid out cell including its border and padding.
fn outer_block_size(&self) -> Au {
self.layout.content_block_size + (self.border.block_sum() + self.padding.block_sum()).into()
}
}
/// A helper struct that performs the layout of the box tree version
@ -45,6 +61,7 @@ struct TableLayout<'a> {
column_measures: Vec<CellOrColumnMeasure>,
distributed_column_widths: Vec<Au>,
row_sizes: Vec<Au>,
row_baselines: Vec<Au>,
cells_laid_out: Vec<Vec<Option<CellLayout>>>,
}
@ -75,6 +92,7 @@ impl<'a> TableLayout<'a> {
column_measures: Vec::new(),
distributed_column_widths: Vec::new(),
row_sizes: Vec::new(),
row_baselines: Vec::new(),
cells_laid_out: Vec::new(),
}
}
@ -883,6 +901,8 @@ impl<'a> TableLayout<'a> {
}
}
/// This is an implementation of *Row layout (first pass)* from
/// <https://drafts.csswg.org/css-tables/#row-layout>.
fn do_row_layout_first_pass(
&mut self,
layout_context: &LayoutContext,
@ -936,7 +956,6 @@ impl<'a> TableLayout<'a> {
padding,
border,
positioning_context,
rowspan: cell.rowspan,
}))
}
self.cells_laid_out.push(cells_laid_out_row);
@ -945,52 +964,110 @@ impl<'a> TableLayout<'a> {
fn distribute_height_to_rows(&mut self) {
for row_index in 0..self.table.size.height {
let mut max_row_height = Au::zero();
let (mut max_ascent, mut max_descent, mut max_row_height) =
(Au::zero(), Au::zero(), Au::zero());
for column_index in 0..self.table.size.width {
let coords = TableSlotCoordinates::new(column_index, row_index);
self.table
.resolve_first_cell_coords(coords)
.map(|resolved_coords| {
let cell = self.cells_laid_out[resolved_coords.y][resolved_coords.x]
.as_ref()
.unwrap();
let total_height = cell.layout.content_block_size +
cell.border.block_sum().into() +
cell.padding.block_sum().into();
// TODO: We are accounting for rowspan=0 here, but perhaps this should be
// translated into a real rowspan during table box tree construction.
let effective_rowspan = match cell.rowspan {
0 => (self.table.size.height - resolved_coords.y) as i32,
rowspan => rowspan as i32,
};
max_row_height = (total_height / effective_rowspan).max(max_row_height)
});
let cell = match self.table.slots[row_index][column_index] {
TableSlot::Cell(ref cell) => cell,
TableSlot::Spanned(ref spanned_cells) if spanned_cells[0].y != 0 => {
let offset = spanned_cells[0];
let origin = coords - offset;
// We only allocate the remaining space for the last row of the rowspanned cell.
if let Some(TableSlot::Cell(origin_cell)) = self.table.get_slot(origin) {
if origin_cell.rowspan != offset.y + 1 {
continue;
}
}
// This is all of the rows that are spanned except this one.
let used_block_size = (origin.y..coords.y)
.map(|row_index| self.row_sizes[row_index])
.fold(Au::zero(), |sum, size| sum + size);
if let Some(layout) = &self.cells_laid_out[origin.y][origin.x] {
max_row_height =
max_row_height.max(layout.outer_block_size() - used_block_size);
}
continue;
},
_ => continue,
};
let layout = match self.cells_laid_out[row_index][column_index] {
Some(ref layout) => layout,
None => {
warn!("Did not find a layout at a slot index with an originating cell.");
continue;
},
};
let outer_block_size = layout.outer_block_size();
if cell.rowspan == 1 {
max_row_height = max_row_height.max(outer_block_size);
}
if cell.effective_vertical_align() == VerticalAlignKeyword::Baseline {
let ascent = layout.ascent();
let border_padding_start =
layout.border.block_start + layout.padding.block_start;
let border_padding_end = layout.border.block_end + layout.padding.block_end;
max_ascent = max_ascent.max(ascent + border_padding_start.into());
// Only take into account the descent of this cell if doesn't span
// rows. The descent portion of the cell in cells that do span rows
// will be allocated to the other rows that it spans.
if cell.rowspan == 1 {
max_descent = max_descent.max(
layout.layout.content_block_size - ascent + border_padding_end.into(),
);
}
}
}
self.row_sizes.push(max_row_height);
self.row_baselines.push(max_ascent);
self.row_sizes
.push(max_row_height.max(max_ascent + max_descent));
}
}
/// Lay out the table of this [`TableLayout`] into fragments. This should only be be called
/// after calling [`TableLayout.compute_measures`].
fn layout_into_box_fragments(
mut self,
positioning_context: &mut PositioningContext,
) -> (Vec<Fragment>, Au) {
fn layout(mut self, positioning_context: &mut PositioningContext) -> IndependentLayout {
assert_eq!(self.table.size.height, self.row_sizes.len());
assert_eq!(self.table.size.width, self.distributed_column_widths.len());
let mut baselines = Baselines::default();
let border_spacing = self.table.border_spacing();
let mut fragments = Vec::new();
let mut row_offset = border_spacing.block;
for row_index in 0..self.table.size.height {
let mut column_offset = border_spacing.inline;
let row_size = self.row_sizes[row_index];
let row_baseline = self.row_baselines[row_index];
// From <https://drafts.csswg.org/css-align-3/#baseline-export>
// > If any cells in the row participate in first baseline/last baseline alignment along
// > the inline axis, the first/last baseline set of the row is generated from their
// > shared alignment baseline and the rows first available font, after alignment has
// > been performed. Otherwise, the first/last baseline set of the row is synthesized from
// > the lowest and highest content edges of the cells in the row. [CSS2]
//
// If any cell below has baseline alignment, these values will be overwritten,
// but they are initialized to the content edge of the first row.
if row_index == 0 {
baselines.first = Some(row_offset + row_size);
baselines.last = Some(row_offset + row_size);
}
for column_index in 0..self.table.size.width {
let column_size = self.distributed_column_widths[column_index];
let layout = match self.cells_laid_out[row_index][column_index].take() {
Some(layout) => layout,
None => continue,
None => {
continue;
},
};
let cell = match self.table.slots[row_index][column_index] {
@ -1001,30 +1078,58 @@ impl<'a> TableLayout<'a> {
},
};
// If this cell has baseline alignment, it can adjust the table's overall baseline.
if cell.effective_vertical_align() == VerticalAlignKeyword::Baseline {
if row_index == 0 {
baselines.first = Some(row_offset + row_baseline);
}
baselines.last = Some(row_offset + row_baseline);
}
// Calculate the inline and block size of all rows and columns that this cell spans.
let inline_size: Au = (column_index..column_index + cell.colspan)
.map(|index| self.distributed_column_widths[index])
.fold(Au::zero(), Au::add) +
((cell.colspan - 1) as i32 * border_spacing.inline);
let block_size: Au = (row_index..row_index + cell.rowspan)
.map(|index| self.row_sizes[index])
.fold(Au::zero(), Au::add) +
((cell.rowspan - 1) as i32 * border_spacing.block);
let cell_rect: LogicalRect<Length> = LogicalRect {
start_corner: LogicalVec2 {
inline: column_offset.into(),
block: row_offset.into(),
},
size: LogicalVec2 {
inline: column_size.into(),
block: row_size.into(),
inline: inline_size.into(),
block: block_size.into(),
},
};
fragments.push(Fragment::Box(cell.create_fragment(
layout,
cell_rect,
row_baseline,
positioning_context,
)));
column_offset += column_size + border_spacing.inline;
column_offset += inline_size + border_spacing.inline;
}
row_offset += row_size + border_spacing.block;
}
(fragments, row_offset)
if self.table.anonymous {
baselines.first = None;
baselines.last = None;
}
IndependentLayout {
fragments,
content_block_size: row_offset,
baselines,
}
}
}
@ -1100,13 +1205,7 @@ impl Table {
) -> IndependentLayout {
let mut table_layout = TableLayout::new(self);
table_layout.compute_measures(layout_context, positioning_context, containing_block);
let (fragments, content_block_size) =
table_layout.layout_into_box_fragments(positioning_context);
IndependentLayout {
fragments,
content_block_size,
baselines: Baselines::default(),
}
table_layout.layout(positioning_context)
}
}
@ -1136,24 +1235,63 @@ impl TableSlotCell {
sizes
}
fn effective_vertical_align(&self) -> VerticalAlignKeyword {
match self.style.clone_vertical_align() {
VerticalAlign::Keyword(VerticalAlignKeyword::Top) => VerticalAlignKeyword::Top,
VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => VerticalAlignKeyword::Bottom,
VerticalAlign::Keyword(VerticalAlignKeyword::Middle) => VerticalAlignKeyword::Middle,
_ => VerticalAlignKeyword::Baseline,
}
}
fn create_fragment(
&self,
mut layout: CellLayout,
cell_rect: LogicalRect<Length>,
cell_baseline: Au,
positioning_context: &mut PositioningContext,
) -> BoxFragment {
// This must be scoped to this function because it conflicts with euclid's Zero.
use style::Zero as StyleZero;
let fragments = layout.layout.fragments;
let content_rect = cell_rect.deflate(&(&layout.padding + &layout.border));
let cell_content_rect = cell_rect.deflate(&(&layout.padding + &layout.border));
let content_block_size = layout.layout.content_block_size.into();
let vertical_align_offset = match self.effective_vertical_align() {
VerticalAlignKeyword::Top => Length::new(0.),
VerticalAlignKeyword::Bottom => cell_content_rect.size.block - content_block_size,
VerticalAlignKeyword::Middle => {
(cell_content_rect.size.block - content_block_size).scale_by(0.5)
},
_ => {
Length::from(cell_baseline) -
(layout.padding.block_start + layout.border.block_start) -
Length::from(layout.ascent())
},
};
// Create an `AnonymousFragment` to move the cell contents to the cell baseline.
let mut vertical_align_fragment_rect = cell_content_rect.clone();
vertical_align_fragment_rect.start_corner = LogicalVec2 {
inline: Length::new(0.),
block: vertical_align_offset,
};
let vertical_align_fragment = AnonymousFragment::new(
vertical_align_fragment_rect,
layout.layout.fragments,
self.style.writing_mode,
);
// Adjust the static position of all absolute children based on the
// final content rect of this fragment.
// final content rect of this fragment. Note that we are not shifting by the position of the
// Anonymous fragment we use to shift content to the baseline.
//
// TODO(mrobinson): This is correct for absolutes that are direct children of the table
// cell, but wrong for absolute fragments that are more deeply nested in the hierarchy of
// fragments.
layout
.positioning_context
.adjust_static_position_of_hoisted_fragments_with_offset(
&content_rect.start_corner,
&cell_content_rect.start_corner,
PositioningContextLength::zero(),
);
positioning_context.append(layout.positioning_context);
@ -1161,8 +1299,8 @@ impl TableSlotCell {
BoxFragment::new(
self.base_fragment_info,
self.style.clone(),
fragments,
content_rect,
vec![Fragment::Anonymous(vertical_align_fragment)],
cell_content_rect,
layout.padding,
layout.border,
LogicalSides::zero(), /* margin */

View file

@ -37,6 +37,9 @@ pub struct Table {
/// The size of this table.
pub size: TableSize,
/// Whether or not this Table is anonymous.
anonymous: bool,
}
impl Table {
@ -45,6 +48,7 @@ impl Table {
style,
slots: Vec::new(),
size: TableSize::zero(),
anonymous: false,
}
}

View file

@ -1,2 +0,0 @@
[margin-collapse-clear-002.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[margin-collapse-clear-003.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[margin-collapse-clear-008.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[margin-collapse-clear-009.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-002.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-003.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-004.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-005.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-006.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[table-vertical-align-baseline-007.xht]
expected: FAIL

View file

@ -5,9 +5,6 @@
[Anonymous consecutive columns spanned by the same set of cells are merged]
expected: FAIL
[Anonymous consecutive rows spanned by the same set of cells are merged]
expected: FAIL
[Explicitely-defined consecutive columns spanned by the same set of cells are not merged]
expected: FAIL

View file

@ -1,16 +1,4 @@
[baseline-table.html]
[.container 3]
expected: FAIL
[.container 4]
expected: FAIL
[.container 5]
expected: FAIL
[.container 6]
expected: FAIL
[.container 11]
expected: FAIL
@ -19,6 +7,3 @@
[.container 13]
expected: FAIL
[.container 14]
expected: FAIL

View file

@ -17,9 +17,6 @@
[table 7]
expected: FAIL
[table 8]
expected: FAIL
[table 9]
expected: FAIL

View file

@ -29,9 +29,6 @@
[table 12]
expected: FAIL
[table 13]
expected: FAIL
[table 14]
expected: FAIL