Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Libraries/LibWeb/Rust/src/layout/formatting_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,13 @@ pub(crate) fn layout_inside_child<'pass>(
force_independent_context_run: bool,
) -> ChildLayoutOutcome {
let facts = run.state.node_facts(&run.callbacks, child);
if let Some((padding_top, padding_bottom)) = input.sizing.table_cell_intrinsic_block_padding {
debug_assert!(facts.is_table_cell());
debug_assert!(matches!(input.participation, ParticipationInParentFormattingContext::Item));
let used = run.state.used_values(&run.callbacks, child);
used.padding_top.set(used.padding_top.get() + padding_top);
used.padding_bottom.set(used.padding_bottom.get() + padding_bottom);
}
let used = run.state.try_used_values(&run.callbacks, child);
if !force_independent_context_run
&& layout_mode == LayoutMode::IntrinsicSizing
Expand Down
3 changes: 3 additions & 0 deletions Libraries/LibWeb/Rust/src/layout/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ pub(crate) struct RootSizingDirectives {
pub(crate) forced_content_inline_size: Option<CssPixels>,
pub(crate) forced_content_block_size: Option<CssPixels>,
pub(crate) forced_min_border_box_block_size: Option<CssPixels>,
// Input-only: the table formatting context supplies the cell's intrinsic block padding
// (the vertical-alignment stretch) before laying out the cell's contents.
pub(crate) table_cell_intrinsic_block_padding: Option<(CssPixels, CssPixels)>,
// Input-only: the wrapper's BFC tells the table formatting context where the table box's
// content box sits in the wrapper, so rows and row groups (whose containing block is the
// wrapper) can be placed in wrapper coordinates.
Expand Down
176 changes: 116 additions & 60 deletions Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,9 @@ struct TableFormattingContext<'pass> {
min_border_box_block_size_from_flex_item: Option<CssPixels>,
needs_fixed_mode_row_measurement: bool,
cells: Vec<TableCell>,
cell_inside_layout_inputs: Vec<AvailableSpace>,
cell_pre_layout_content_block_sizes: Vec<CssPixels>,
deferred_cell_inside_layouts: Vec<bool>,
columns: Vec<Column>,
rows: Vec<Row>,
derived_baselines_of_root_box: Cell<DerivedBaselines>,
Expand Down Expand Up @@ -840,6 +843,9 @@ impl<'pass> TableFormattingContext<'pass> {
min_border_box_block_size_from_flex_item: None,
needs_fixed_mode_row_measurement: false,
cells: Vec::new(),
cell_inside_layout_inputs: Vec::new(),
cell_pre_layout_content_block_sizes: Vec::new(),
deferred_cell_inside_layouts: Vec::new(),
columns: Vec::new(),
rows: Vec::new(),
derived_baselines_of_root_box: Cell::new(DerivedBaselines::default()),
Expand Down Expand Up @@ -1838,6 +1844,9 @@ impl<'pass> TableFormattingContext<'pass> {
// Determine the number of rows/columns the table requires.
let table_grid = calculate_table_grid(self, self.table_box);
self.cells = table_grid.cells;
self.cell_inside_layout_inputs = vec![AvailableSpace::default(); self.cells.len()];
self.cell_pre_layout_content_block_sizes = vec![CssPixels::default(); self.cells.len()];
self.deferred_cell_inside_layouts = vec![false; self.cells.len()];
self.rows = table_grid.rows;
self.columns = vec![Column::default(); table_grid.column_count];
for cell in &self.cells {
Expand Down Expand Up @@ -1903,13 +1912,15 @@ impl<'pass> TableFormattingContext<'pass> {
cell: TableCell,
input: AvailableSpace,
adopt_automatic_content_block_size: bool,
intrinsic_block_padding: Option<(CssPixels, CssPixels)>,
) {
let layout_input = LayoutInput {
available_space: input,
containing_block_constraints: ContainingBlockConstraints::default(),
content_box_position_in_bfc_root: None,
sizing: RootSizingDirectives {
adopt_automatic_content_block_size,
table_cell_intrinsic_block_padding: intrinsic_block_padding,
..RootSizingDirectives::default()
},
participation: ParticipationInParentFormattingContext::Item,
Expand All @@ -1930,6 +1941,7 @@ impl<'pass> TableFormattingContext<'pass> {
cell: TableCell,
used: &UsedValues,
inner: AvailableSpace,
adopt_automatic_content_block_size: bool,
) -> Option<MeasuredCellContent> {
// The table formatting context owns the cell's outer geometry. Seed the inputs
// needed to lay out its contents without copying placement or layout outputs.
Expand Down Expand Up @@ -1963,11 +1975,17 @@ impl<'pass> TableFormattingContext<'pass> {
let result = measurement.run_with_layout_mode(
cell.box_,
self.layout_mode,
LayoutInput::new(inner, ContainingBlockConstraints::default(), ParticipationInParentFormattingContext::Item),
LayoutInput {
available_space: inner,
containing_block_constraints: ContainingBlockConstraints::default(),
content_box_position_in_bfc_root: None,
sizing: RootSizingDirectives {
adopt_automatic_content_block_size,
..RootSizingDirectives::default()
},
participation: ParticipationInParentFormattingContext::Item,
},
);
measurement
.root_used()
.set_content_block_size(result.automatic_content_block_size);
Some(MeasuredCellContent {
content_block_size: result.automatic_content_block_size,
first_baseline: crate::layout::box_baseline(
Expand Down Expand Up @@ -2036,16 +2054,22 @@ impl<'pass> TableFormattingContext<'pass> {

let outer_space = self.available_space;
let inner = used.available_inner_space_or_constraints_from(outer_space);
self.cell_inside_layout_inputs[cell_index] = inner;
self.cell_pre_layout_content_block_sizes[cell_index] = used.content_block_size.get();
let defer_inside_layout = style.height().is_percentage()
|| style.vertical_align_is_keyword()
|| self.anonymous_cell_wraps_flex_or_grid(cell);
self.deferred_cell_inside_layouts[cell_index] = defer_inside_layout;
let mut measured_baseline = None;
if style.height().is_percentage() {
// This cell's final inside layout happens in the second pass below; measure its
if defer_inside_layout {
// This cell's final inside layout happens once row heights are final; measure its
// content in a throwaway state instead of laying out the committing state twice.
if let Some(measured) = self.measure_cell(cell, used, inner) {
if let Some(measured) = self.measure_cell(cell, used, inner, true) {
used.set_content_block_size(measured.content_block_size);
measured_baseline = Some(measured.first_baseline);
}
} else {
self.layout_inside_cell(run, cell, inner, true);
self.layout_inside_cell(run, cell, inner, true, None);
}
if self.needs_fixed_mode_row_measurement {
let min_size = style.min_height().to_px(participant_block_basis);
Expand Down Expand Up @@ -2144,6 +2168,7 @@ impl<'pass> TableFormattingContext<'pass> {
let cell_size = style.height().to_px(self.table_block_size);
let used = self.used_values(cell.box_);
used.set_content_block_size(cell_size - used.border_box_top(collapsed) - used.border_box_bottom(collapsed));
self.cell_pre_layout_content_block_sizes[cell_index] = used.content_block_size.get();
if !self.rows[cell.row_index].is_collapsed {
self.rows[cell.row_index].reference_block_size =
self.rows[cell.row_index].reference_block_size.max(cell_size);
Expand All @@ -2156,10 +2181,12 @@ impl<'pass> TableFormattingContext<'pass> {
+ inline_spacing * (cell.column_span - 1),
);
let inner = used.available_inner_space_or_constraints_from(self.available_space);
// The first pass only measured this cell in a throwaway state; this is its one and
// only inside layout in the committing state.
self.layout_inside_cell(run, cell, inner, false);
let baseline = self.box_baseline(cell.box_);
self.cell_inside_layout_inputs[cell_index] = inner;
// The first pass measured this cell at its automatic block size; measure it again at
// the percentage-resolved size to preserve the baseline its final inside layout will use.
let baseline = self
.measure_cell(cell, used, inner, false)
.map_or_else(|| self.box_baseline(cell.box_), |measured| measured.first_baseline);
self.cells[cell_index].baseline = baseline;
if !self.rows[cell.row_index].is_collapsed {
let border_size = used.border_box_block_size(collapsed);
Expand Down Expand Up @@ -2283,6 +2310,81 @@ impl<'pass> TableFormattingContext<'pass> {
self.table_block_size = self.table_block_size.max(total);
}

fn layout_deferred_cells_inside(&mut self, run: &FormattingContextRun<'pass>) {
// Deferred cells get their one and only committing inside layout here, once row
// block sizes are final.
let collapsed = self.style_facts(self.table_box).border_collapse() != BORDER_COLLAPSE_SEPARATE;
for cell_index in 0..self.cells.len() {
if !self.deferred_cell_inside_layouts[cell_index] {
continue;
}
let cell = self.cells[cell_index];
let adopt_automatic_content_block_size = !self.style_facts(cell.box_).height().is_percentage();
let intrinsic_block_padding = self.cell_intrinsic_block_padding(cell, collapsed);
let used = self.used_values(cell.box_);
let measured_content_block_size = used.content_block_size.get();
// The first pass adopted the measured automatic block size so row sizing could read
// it; restore the pre-layout size so the cell's children resolve percentages against
// the same basis the measurement saw.
used.set_content_block_size(self.cell_pre_layout_content_block_sizes[cell_index]);
let inner = self.cell_inside_layout_inputs[cell_index];
self.layout_inside_cell(run, cell, inner, adopt_automatic_content_block_size, intrinsic_block_padding);
if adopt_automatic_content_block_size {
debug_assert_eq!(
used.content_block_size.get(),
measured_content_block_size,
"measured and committed automatic block sizes diverged"
);
}
}
}

fn cell_intrinsic_block_padding(&mut self, cell: TableCell, collapsed: bool) -> Option<(CssPixels, CssPixels)> {
let row_size = self.compute_row_content_block_size(cell);
let used = self.used_values(cell.box_);
let style = self.style_facts(cell.box_);
// When a table cell is an anonymous wrapper around a flex or grid container (e.g., a <td> with display:flex is
// wrapped in an anonymous table-cell box per CSS Tables 3), the cell should be aligned to the top. This allows
// the flex/grid container to fill the cell and handle alignment of its children via its own properties.
if self.anonymous_cell_wraps_flex_or_grid(cell) {
return Some((CssPixels::default(), row_size - used.border_box_block_size(collapsed)));
}
if !style.vertical_align_is_keyword() {
return None;
}
// The following image shows various alignment lines of a row:
// https://www.w3.org/TR/css-tables-3/images/cell-align-explainer.png
// https://drafts.csswg.org/css2/#height-layout
// In the context of tables, values for vertical-align have the following meanings:
match style.vertical_align_keyword() {
vertical_align::MIDDLE => {
// The center of the cell is aligned with the center of the rows it spans.
let difference = row_size - used.border_box_block_size(collapsed);
Some((difference / 2, difference / 2))
}
vertical_align::TOP => {
// The top of the cell box is aligned with the top of the first row it spans.
Some((CssPixels::default(), row_size - used.border_box_block_size(collapsed)))
}
vertical_align::BOTTOM => {
// The bottom of the cell box is aligned with the bottom of the last row it spans.
Some((row_size - used.border_box_block_size(collapsed), CssPixels::default()))
}
vertical_align::SUB
| vertical_align::SUPER
| vertical_align::TEXT_BOTTOM
| vertical_align::TEXT_TOP
| vertical_align::BASELINE => {
// These values do not apply to cells; the cell is aligned at the baseline instead.

// The baseline of the cell is put at the same height as the baseline of the first of the rows it spans.
let padding_top = self.rows[cell.row_index].baseline - cell.baseline;
Some((padding_top, row_size - (used.border_box_block_size(collapsed) + padding_top)))
}
_ => panic!("invalid vertical-align keyword"),
}
}

fn compute_row_content_block_size(&mut self, cell: TableCell) -> CssPixels {
// The block size of a cell is the sum of all spanned rows, as described in
// https://www.w3.org/TR/css-tables-3/#bounding-box-assignment
Expand Down Expand Up @@ -2313,7 +2415,7 @@ impl<'pass> TableFormattingContext<'pass> {
span + self.border_spacing_block() * (cell.row_span - 1)
}

fn anonymous_cell_wraps_flex_or_grid(&mut self, cell: TableCell) -> bool {
fn anonymous_cell_wraps_flex_or_grid(&self, cell: TableCell) -> bool {
if !self.node_facts(cell.box_).is_anonymous() {
return false;
}
Expand All @@ -2337,53 +2439,6 @@ impl<'pass> TableFormattingContext<'pass> {
let cell = self.cells[cell_index];
let used = self.used_values(cell.box_);
let row_used = self.used_values(self.rows[cell.row_index].box_);
let row_size = self.compute_row_content_block_size(cell);
let style = self.style_facts(cell.box_);
// When a table cell is an anonymous wrapper around a flex or grid container (e.g., a <td> with display:flex is
// wrapped in an anonymous table-cell box per CSS Tables 3), the cell should be aligned to the top. This allows
// the flex/grid container to fill the cell and handle alignment of its children via its own properties.
let anonymous_wrapper = self.anonymous_cell_wraps_flex_or_grid(cell);
if anonymous_wrapper {
used.padding_bottom
.set(used.padding_bottom.get() + row_size - used.border_box_block_size(collapsed));
} else if style.vertical_align_is_keyword() {
// The following image shows various alignment lines of a row:
// https://www.w3.org/TR/css-tables-3/images/cell-align-explainer.png
// https://drafts.csswg.org/css2/#height-layout
// In the context of tables, values for vertical-align have the following meanings:
match style.vertical_align_keyword() {
vertical_align::MIDDLE => {
// The center of the cell is aligned with the center of the rows it spans.
let difference = row_size - used.border_box_block_size(collapsed);
used.padding_top.set(used.padding_top.get() + difference / 2);
used.padding_bottom.set(used.padding_bottom.get() + difference / 2);
}
vertical_align::TOP => {
// The top of the cell box is aligned with the top of the first row it spans.
used.padding_bottom
.set(used.padding_bottom.get() + row_size - used.border_box_block_size(collapsed));
}
vertical_align::BOTTOM => {
// The bottom of the cell box is aligned with the bottom of the last row it spans.
used.padding_top
.set(used.padding_top.get() + row_size - used.border_box_block_size(collapsed));
}
vertical_align::SUB
| vertical_align::SUPER
| vertical_align::TEXT_BOTTOM
| vertical_align::TEXT_TOP
| vertical_align::BASELINE => {
// These values do not apply to cells; the cell is aligned at the baseline instead.

// The baseline of the cell is put at the same height as the baseline of the first of the rows it spans.
used.padding_top
.set(used.padding_top.get() + self.rows[cell.row_index].baseline - cell.baseline);
used.padding_bottom
.set(used.padding_bottom.get() + row_size - used.border_box_block_size(collapsed));
}
_ => panic!("invalid vertical-align keyword"),
}
}
// Compute cell position as specified by https://www.w3.org/TR/css-tables-3/#bounding-box-assignment:
// left/top location is the sum of:
// - for top: the height reserved for top captions (including margins), if any
Expand Down Expand Up @@ -2440,6 +2495,7 @@ impl<'pass> TableFormattingContext<'pass> {
self.compute_table_block_size(run);
self.distribute_block_size_to_rows();
self.position_row_boxes();
self.layout_deferred_cells_inside(run);
self.position_cell_boxes();
table_used.set_content_block_size(self.table_block_size);
// Derive baselines for the table internals bottom-up (rows, then row groups, then the table box)
Expand Down
Loading