Serenity Operating System
1/*
2 * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibWeb/DOM/Node.h>
8#include <LibWeb/HTML/BrowsingContext.h>
9#include <LibWeb/Layout/Box.h>
10#include <LibWeb/Layout/InlineFormattingContext.h>
11#include <LibWeb/Layout/TableBox.h>
12#include <LibWeb/Layout/TableCellBox.h>
13#include <LibWeb/Layout/TableFormattingContext.h>
14#include <LibWeb/Layout/TableRowBox.h>
15#include <LibWeb/Layout/TableRowGroupBox.h>
16
17struct GridPosition {
18 size_t x;
19 size_t y;
20};
21
22inline bool operator==(GridPosition const& a, GridPosition const& b)
23{
24 return a.x == b.x && a.y == b.y;
25}
26
27namespace AK {
28template<>
29struct Traits<GridPosition> : public GenericTraits<GridPosition> {
30 static unsigned hash(GridPosition const& key)
31 {
32 return pair_int_hash(key.x, key.y);
33 }
34};
35}
36
37namespace Web::Layout {
38
39TableFormattingContext::TableFormattingContext(LayoutState& state, TableBox const& root, FormattingContext* parent)
40 : FormattingContext(Type::Table, state, root, parent)
41{
42}
43
44TableFormattingContext::~TableFormattingContext() = default;
45
46void TableFormattingContext::calculate_row_column_grid(Box const& box)
47{
48 // Implements https://html.spec.whatwg.org/multipage/tables.html#forming-a-table
49 HashMap<GridPosition, bool> grid;
50
51 size_t x_width = 0, y_height = 0;
52 size_t x_current = 0, y_current = 0;
53
54 auto process_row = [&](auto& row) {
55 if (y_height == y_current)
56 y_height++;
57
58 x_current = 0;
59 while (x_current < x_width && grid.contains(GridPosition { x_current, y_current }))
60 x_current++;
61
62 for (auto* child = row.first_child(); child; child = child->next_sibling()) {
63 if (is<TableCellBox>(*child)) {
64 Box* box = static_cast<Box*>(child);
65 if (x_current == x_width)
66 x_width++;
67
68 const size_t colspan = static_cast<TableCellBox*>(child)->colspan();
69 const size_t rowspan = static_cast<TableCellBox*>(child)->rowspan();
70
71 if (x_width < x_current + colspan)
72 x_width = x_current + colspan;
73 if (y_height < y_current + rowspan)
74 y_height = y_current + rowspan;
75
76 for (size_t y = y_current; y < y_current + rowspan; y++)
77 for (size_t x = x_current; x < x_current + colspan; x++)
78 grid.set(GridPosition { x, y }, true);
79 m_cells.append(Cell { *box, x_current, y_current, colspan, rowspan });
80
81 x_current += colspan;
82 }
83 }
84
85 m_rows.append(Row { row });
86 y_current++;
87 };
88
89 box.template for_each_child_of_type<TableRowGroupBox>([&](auto& row_group_box) {
90 row_group_box.template for_each_child_of_type<TableRowBox>([&](auto& row) {
91 process_row(row);
92 return IterationDecision::Continue;
93 });
94 });
95
96 box.template for_each_child_of_type<TableRowBox>([&](auto& row) {
97 process_row(row);
98 return IterationDecision::Continue;
99 });
100
101 m_columns.resize(x_width);
102}
103
104void TableFormattingContext::compute_table_measures()
105{
106 size_t max_cell_column_span = 1;
107
108 for (auto& cell : m_cells) {
109 auto width_of_containing_block = m_state.get(*table_wrapper().containing_block()).content_width();
110 auto width_of_containing_block_as_length = CSS::Length::make_px(width_of_containing_block);
111 auto& computed_values = cell.box.computed_values();
112 CSSPixels padding_left = computed_values.padding().left().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
113 CSSPixels padding_right = computed_values.padding().right().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
114
115 auto is_left_most_cell = cell.column_index == 0;
116 auto is_right_most_cell = cell.column_index == m_columns.size() - 1;
117 auto should_hide_borders = cell.box.computed_values().border_collapse() == CSS::BorderCollapse::Collapse;
118 CSSPixels border_left = should_hide_borders && !is_left_most_cell ? 0 : computed_values.border_left().width;
119 CSSPixels border_right = should_hide_borders && !is_right_most_cell ? 0 : computed_values.border_right().width;
120
121 CSSPixels width = computed_values.width().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
122 auto cell_intrinsic_offsets = padding_left + padding_right + border_left + border_right;
123 auto min_content_width = calculate_min_content_width(cell.box);
124 auto max_content_width = calculate_max_content_width(cell.box);
125
126 CSSPixels min_width = min_content_width;
127 if (!computed_values.min_width().is_auto())
128 min_width = max(min_width, computed_values.min_width().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box));
129
130 CSSPixels max_width = computed_values.width().is_auto() ? max_content_width.value() : width;
131 if (!computed_values.max_width().is_none())
132 max_width = min(max_width, computed_values.max_width().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box));
133
134 auto computed_width = computed_values.width();
135 if (computed_width.is_percentage()) {
136 m_columns[cell.column_index].type = ColumnType::Percent;
137 m_columns[cell.column_index].percentage_width = max(m_columns[cell.column_index].percentage_width, computed_width.percentage().value());
138 } else if (computed_width.is_length()) {
139 m_columns[cell.column_index].type = ColumnType::Pixel;
140 }
141
142 cell.min_width = min_width + cell_intrinsic_offsets;
143 cell.max_width = max(max(width, min_width), max_width) + cell_intrinsic_offsets;
144
145 max_cell_column_span = max(max_cell_column_span, cell.column_span);
146 }
147
148 for (auto& cell : m_cells) {
149 if (cell.column_span == 1) {
150 m_columns[cell.column_index].min_width = max(m_columns[cell.column_index].min_width, cell.min_width);
151 m_columns[cell.column_index].max_width = max(m_columns[cell.column_index].max_width, cell.max_width);
152 }
153 }
154
155 for (size_t current_column_span = 2; current_column_span < max_cell_column_span; current_column_span++) {
156 // https://www.w3.org/TR/css-tables-3/#min-content-width-of-a-column-based-on-cells-of-span-up-to-n-n--1
157 Vector<Vector<CSSPixels>> cell_min_contributions_by_column_index;
158 cell_min_contributions_by_column_index.resize(m_columns.size());
159 // https://www.w3.org/TR/css-tables-3/#max-content-width-of-a-column-based-on-cells-of-span-up-to-n-n--1
160 Vector<Vector<CSSPixels>> cell_max_contributions_by_column_index;
161 cell_max_contributions_by_column_index.resize(m_columns.size());
162 for (auto& cell : m_cells) {
163 if (cell.column_span == current_column_span) {
164 // Define the baseline max-content width as the sum of the max-content widths based on cells of span up to N-1 of all columns that the cell spans.
165 CSSPixels baseline_max_content_width = 0;
166 auto cell_start_column_index = cell.column_index;
167 auto cell_end_column_index = cell.column_index + cell.column_span;
168 for (auto column_index = cell_start_column_index; column_index < cell_end_column_index; column_index++) {
169 baseline_max_content_width += m_columns[column_index].max_width;
170 }
171
172 // The contribution of the cell is the sum of:
173 // the min-content width of the column based on cells of span up to N-1
174 auto cell_min_contribution = m_columns[cell.column_index].min_width;
175 // and the product of:
176 // - the ratio of the max-content width based on cells of span up to N-1 of the column to the baseline max-content width
177 // - the outer min-content width of the cell minus the baseline max-content width and baseline border spacing, or 0 if this is negative
178 cell_min_contribution += (m_columns[cell.column_index].max_width / baseline_max_content_width) * max(CSSPixels(0), cell.min_width - baseline_max_content_width);
179
180 // The contribution of the cell is the sum of:
181 // the max-content width of the column based on cells of span up to N-1
182 auto cell_max_contribution = m_columns[cell.column_index].max_width;
183 // and the product of:
184 // - the ratio of the max-content width based on cells of span up to N-1 of the column to the baseline max-content width
185 // - the outer max-content width of the cell minus the baseline max-content width and the baseline border spacing, or 0 if this is negative
186 cell_max_contribution += (m_columns[cell.column_index].max_width / baseline_max_content_width) * max(CSSPixels(0), cell.max_width - baseline_max_content_width);
187
188 cell_min_contributions_by_column_index[cell.column_index].append(cell_min_contribution);
189 cell_max_contributions_by_column_index[cell.column_index].append(cell_max_contribution);
190 }
191 }
192
193 for (size_t column_index = 0; column_index < m_columns.size(); column_index++) {
194 // min-content width of a column based on cells of span up to N (N > 1) is
195 // the largest of the min-content width of the column based on cells of span up to N-1 and the contributions of the cells in the column whose colSpan is N
196 for (auto min_contribution : cell_min_contributions_by_column_index[column_index])
197 m_columns[column_index].min_width = max(m_columns[column_index].min_width, min_contribution);
198
199 // max-content width of a column based on cells of span up to N (N > 1) is
200 // the largest of the max-content width based on cells of span up to N-1 and the contributions of the cells in the column whose colSpan is N
201 for (auto max_contribution : cell_max_contributions_by_column_index[column_index])
202 m_columns[column_index].max_width = max(m_columns[column_index].max_width, max_contribution);
203 }
204 }
205}
206
207void TableFormattingContext::compute_table_width()
208{
209 auto& table_box_state = m_state.get_mutable(table_box());
210
211 auto& computed_values = table_box().computed_values();
212
213 // Percentages on 'width' and 'height' on the table are relative to the table wrapper box's containing block,
214 // not the table wrapper box itself.
215 CSSPixels width_of_table_containing_block = m_state.get(*table_wrapper().containing_block()).content_width();
216
217 // The row/column-grid width minimum (GRIDMIN) width is the sum of the min-content width
218 // of all the columns plus cell spacing or borders.
219 CSSPixels grid_min = 0.0f;
220 for (auto& column : m_columns) {
221 grid_min += column.min_width;
222 }
223
224 // The row/column-grid width maximum (GRIDMAX) width is the sum of the max-content width
225 // of all the columns plus cell spacing or borders.
226 CSSPixels grid_max = 0.0f;
227 for (auto& column : m_columns) {
228 grid_max += column.max_width;
229 }
230
231 // The used min-width of a table is the greater of the resolved min-width, CAPMIN, and GRIDMIN.
232 auto used_min_width = grid_min;
233 if (!computed_values.min_width().is_auto()) {
234 used_min_width = max(used_min_width, computed_values.min_width().resolved(table_box(), CSS::Length::make_px(width_of_table_containing_block)).to_px(table_box()));
235 }
236
237 CSSPixels used_width;
238 if (computed_values.width().is_auto()) {
239 // If the table-root has 'width: auto', the used width is the greater of
240 // min(GRIDMAX, the table’s containing block width), the used min-width of the table.
241 used_width = max(min(grid_max, width_of_table_containing_block), used_min_width);
242 table_box_state.set_content_width(used_width);
243 } else {
244 // If the table-root’s width property has a computed value (resolving to
245 // resolved-table-width) other than auto, the used width is the greater
246 // of resolved-table-width, and the used min-width of the table.
247 CSSPixels resolved_table_width = computed_values.width().resolved(table_box(), CSS::Length::make_px(width_of_table_containing_block)).to_px(table_box());
248 used_width = max(resolved_table_width, used_min_width);
249 table_box_state.set_content_width(used_width);
250 }
251}
252
253void TableFormattingContext::distribute_width_to_columns()
254{
255 // Implements https://www.w3.org/TR/css-tables-3/#width-distribution-algorithm
256
257 CSSPixels available_width = m_state.get(table_box()).content_width();
258
259 auto columns_total_used_width = [&]() {
260 CSSPixels total_used_width = 0;
261 for (auto& column : m_columns) {
262 total_used_width += column.used_width;
263 }
264 return total_used_width;
265 };
266
267 auto column_preferred_width = [&](Column& column) {
268 switch (column.type) {
269 case ColumnType::Percent: {
270 return max(column.min_width, column.percentage_width / 100 * available_width);
271 }
272 case ColumnType::Pixel:
273 case ColumnType::Auto: {
274 return column.max_width;
275 }
276 default: {
277 VERIFY_NOT_REACHED();
278 }
279 }
280 };
281
282 auto expand_columns_to_fill_available_width = [&](ColumnType column_type) {
283 CSSPixels remaining_available_width = available_width;
284 CSSPixels total_preferred_width_increment = 0;
285 for (auto& column : m_columns) {
286 remaining_available_width -= column.used_width;
287 if (column.type == column_type) {
288 total_preferred_width_increment += column_preferred_width(column) - column.min_width;
289 }
290 }
291
292 if (total_preferred_width_increment == 0)
293 return;
294
295 for (auto& column : m_columns) {
296 if (column.type == column_type) {
297 CSSPixels preferred_width_increment = column_preferred_width(column) - column.min_width;
298 column.used_width += preferred_width_increment * remaining_available_width / total_preferred_width_increment;
299 }
300 }
301 };
302
303 auto shrink_columns_to_fit_available_width = [&](ColumnType column_type) {
304 for (auto& column : m_columns) {
305 if (column.type == column_type)
306 column.used_width = column.min_width;
307 }
308
309 expand_columns_to_fill_available_width(column_type);
310 };
311
312 for (auto& column : m_columns) {
313 column.used_width = column.min_width;
314 }
315
316 for (auto& column : m_columns) {
317 if (column.type == ColumnType::Percent) {
318 column.used_width = max(column.min_width, column.percentage_width / 100 * available_width);
319 }
320 }
321
322 if (columns_total_used_width() > available_width) {
323 shrink_columns_to_fit_available_width(ColumnType::Percent);
324 return;
325 }
326
327 for (auto& column : m_columns) {
328 if (column.type == ColumnType::Pixel) {
329 column.used_width = column.max_width;
330 }
331 }
332
333 if (columns_total_used_width() > available_width) {
334 shrink_columns_to_fit_available_width(ColumnType::Pixel);
335 return;
336 }
337
338 if (columns_total_used_width() < available_width) {
339 expand_columns_to_fill_available_width(ColumnType::Auto);
340 }
341
342 if (columns_total_used_width() < available_width) {
343 expand_columns_to_fill_available_width(ColumnType::Pixel);
344 }
345
346 if (columns_total_used_width() < available_width) {
347 expand_columns_to_fill_available_width(ColumnType::Percent);
348 }
349
350 if (columns_total_used_width() < available_width) {
351 // NOTE: if all columns got their max width and there is still width to distribute left
352 // it should be assigned to columns proportionally to their max width
353 CSSPixels grid_max = 0.0f;
354 for (auto& column : m_columns) {
355 grid_max += column.max_width;
356 }
357
358 auto width_to_distribute = available_width - columns_total_used_width();
359 for (auto& column : m_columns) {
360 column.used_width += width_to_distribute * column.max_width / grid_max;
361 }
362 }
363}
364
365void TableFormattingContext::determine_intrisic_size_of_table_container(AvailableSpace const& available_space)
366{
367 auto& table_box_state = m_state.get_mutable(table_box());
368
369 if (available_space.width.is_min_content()) {
370 // The min-content width of a table is the width required to fit all of its columns min-content widths and its undistributable spaces.
371 CSSPixels grid_min = 0.0f;
372 for (auto& column : m_columns)
373 grid_min += column.min_width;
374 table_box_state.set_content_width(grid_min);
375 }
376
377 if (available_space.width.is_max_content()) {
378 // The max-content width of a table is the width required to fit all of its columns max-content widths and its undistributable spaces.
379 CSSPixels grid_max = 0.0f;
380 for (auto& column : m_columns)
381 grid_max += column.max_width;
382 table_box_state.set_content_width(grid_max);
383 }
384}
385
386void TableFormattingContext::calculate_row_heights(LayoutMode layout_mode)
387{
388 for (auto& cell : m_cells) {
389 auto& cell_state = m_state.get_mutable(cell.box);
390
391 CSSPixels span_width = 0;
392 for (size_t i = 0; i < cell.column_span; ++i)
393 span_width += m_columns[cell.column_index + i].used_width;
394
395 auto width_of_containing_block = m_state.get(*cell.box.containing_block()).content_width();
396 auto width_of_containing_block_as_length = CSS::Length::make_px(width_of_containing_block);
397
398 cell_state.padding_top = cell.box.computed_values().padding().top().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
399 cell_state.padding_bottom = cell.box.computed_values().padding().bottom().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
400 cell_state.padding_left = cell.box.computed_values().padding().left().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
401 cell_state.padding_right = cell.box.computed_values().padding().right().resolved(cell.box, width_of_containing_block_as_length).to_px(cell.box);
402
403 auto is_top_most_cell = cell.row_index == 0;
404 auto is_left_most_cell = cell.column_index == 0;
405 auto is_right_most_cell = cell.column_index == m_columns.size() - 1;
406 auto is_bottom_most_cell = cell.row_index == m_rows.size() - 1;
407 auto should_hide_borders = cell.box.computed_values().border_collapse() == CSS::BorderCollapse::Collapse;
408
409 cell_state.border_top = (should_hide_borders && is_top_most_cell) ? 0 : cell.box.computed_values().border_top().width;
410 cell_state.border_bottom = (should_hide_borders && is_bottom_most_cell) ? 0 : cell.box.computed_values().border_bottom().width;
411 cell_state.border_left = (should_hide_borders && is_left_most_cell) ? 0 : cell.box.computed_values().border_left().width;
412 cell_state.border_right = (should_hide_borders && is_right_most_cell) ? 0 : cell.box.computed_values().border_right().width;
413
414 cell_state.set_content_width((span_width - cell_state.border_box_left() - cell_state.border_box_right()));
415 if (auto independent_formatting_context = layout_inside(cell.box, layout_mode, cell_state.available_inner_space_or_constraints_from(*m_available_space))) {
416 cell_state.set_content_height(independent_formatting_context->automatic_content_height());
417 independent_formatting_context->parent_context_did_dimension_child_root_box();
418 }
419
420 cell.baseline = box_baseline(m_state, cell.box);
421
422 auto& row = m_rows[cell.row_index];
423 row.used_height = max(row.used_height, cell_state.border_box_height());
424 row.baseline = max(row.baseline, cell.baseline);
425 }
426}
427
428void TableFormattingContext::position_row_boxes(CSSPixels& total_content_height)
429{
430 auto const& table_state = m_state.get(table_box());
431
432 CSSPixels row_top_offset = table_state.border_top + table_state.padding_top;
433 CSSPixels row_left_offset = table_state.border_left + table_state.padding_left;
434 for (size_t y = 0; y < m_rows.size(); y++) {
435 auto& row = m_rows[y];
436 auto& row_state = m_state.get_mutable(row.box);
437 CSSPixels row_width = 0.0f;
438 for (auto& column : m_columns) {
439 row_width += column.used_width;
440 }
441
442 row_state.set_content_height(row.used_height);
443 row_state.set_content_width(row_width);
444 row_state.set_content_x(row_left_offset);
445 row_state.set_content_y(row_top_offset);
446 row_top_offset += row_state.content_height();
447 }
448
449 CSSPixels row_group_top_offset = table_state.border_top + table_state.padding_top;
450 CSSPixels row_group_left_offset = table_state.border_left + table_state.padding_left;
451 table_box().for_each_child_of_type<TableRowGroupBox>([&](auto& row_group_box) {
452 CSSPixels row_group_height = 0.0f;
453 CSSPixels row_group_width = 0.0f;
454
455 auto& row_group_box_state = m_state.get_mutable(row_group_box);
456 row_group_box_state.set_content_x(row_group_left_offset);
457 row_group_box_state.set_content_y(row_group_top_offset);
458
459 row_group_box.template for_each_child_of_type<TableRowBox>([&](auto& row) {
460 auto const& row_state = m_state.get(row);
461 row_group_height += row_state.border_box_height();
462 row_group_width = max(row_group_width, row_state.border_box_width());
463 });
464
465 row_group_box_state.set_content_height(row_group_height);
466 row_group_box_state.set_content_width(row_group_width);
467
468 row_group_top_offset += row_group_height;
469 });
470
471 total_content_height = max(row_top_offset, row_group_top_offset) - table_state.border_top - table_state.padding_top;
472}
473
474void TableFormattingContext::position_cell_boxes()
475{
476 CSSPixels left_column_offset = 0;
477 for (auto& column : m_columns) {
478 column.left_offset = left_column_offset;
479 left_column_offset += column.used_width;
480 }
481
482 for (auto& cell : m_cells) {
483 auto& cell_state = m_state.get_mutable(cell.box);
484 auto& row_state = m_state.get(m_rows[cell.row_index].box);
485 CSSPixels const cell_border_box_height = cell_state.content_height() + cell_state.border_box_top() + cell_state.border_box_bottom();
486 CSSPixels const row_content_height = row_state.content_height();
487 auto const& vertical_align = cell.box.computed_values().vertical_align();
488 if (vertical_align.has<CSS::VerticalAlign>()) {
489 switch (vertical_align.get<CSS::VerticalAlign>()) {
490 case CSS::VerticalAlign::Middle: {
491 cell_state.padding_top += (row_content_height - cell_border_box_height) / 2;
492 cell_state.padding_bottom += (row_content_height - cell_border_box_height) / 2;
493 break;
494 }
495 // FIXME: implement actual 'top' and 'bottom' support instead of fall back to 'baseline'
496 case CSS::VerticalAlign::Top:
497 case CSS::VerticalAlign::Bottom:
498 case CSS::VerticalAlign::Baseline: {
499 cell_state.padding_top += m_rows[cell.row_index].baseline - cell.baseline;
500 cell_state.padding_bottom += row_content_height - cell_border_box_height;
501 break;
502 }
503 default:
504 VERIFY_NOT_REACHED();
505 }
506 }
507
508 cell_state.offset = row_state.offset.translated(cell_state.border_box_left() + m_columns[cell.column_index].left_offset, cell_state.border_box_top());
509 }
510}
511
512void TableFormattingContext::run(Box const& box, LayoutMode layout_mode, AvailableSpace const& available_space)
513{
514 m_available_space = available_space;
515
516 CSSPixels total_content_height = 0;
517
518 // Determine the number of rows/columns the table requires.
519 calculate_row_column_grid(box);
520
521 // Compute the minimum width of each column.
522 compute_table_measures();
523
524 if (available_space.width.is_intrinsic_sizing_constraint()) {
525 determine_intrisic_size_of_table_container(available_space);
526 return;
527 }
528
529 // Compute the width of the table.
530 compute_table_width();
531
532 // Distribute the width of the table among columns.
533 distribute_width_to_columns();
534
535 calculate_row_heights(layout_mode);
536 position_row_boxes(total_content_height);
537 position_cell_boxes();
538
539 m_state.get_mutable(table_box()).set_content_height(total_content_height);
540
541 // FIXME: This is a total hack, we should respect the 'height' property.
542 m_automatic_content_height = total_content_height;
543}
544
545CSSPixels TableFormattingContext::automatic_content_height() const
546{
547 return m_automatic_content_height;
548}
549
550}