Serenity Operating System
1/*
2 * Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/TemporaryChange.h>
8#include <LibWeb/CSS/Length.h>
9#include <LibWeb/DOM/Node.h>
10#include <LibWeb/Dump.h>
11#include <LibWeb/HTML/BrowsingContext.h>
12#include <LibWeb/Layout/BlockContainer.h>
13#include <LibWeb/Layout/BlockFormattingContext.h>
14#include <LibWeb/Layout/Box.h>
15#include <LibWeb/Layout/InlineFormattingContext.h>
16#include <LibWeb/Layout/LineBuilder.h>
17#include <LibWeb/Layout/ListItemBox.h>
18#include <LibWeb/Layout/ListItemMarkerBox.h>
19#include <LibWeb/Layout/ReplacedBox.h>
20#include <LibWeb/Layout/TableBox.h>
21#include <LibWeb/Layout/TableWrapper.h>
22#include <LibWeb/Layout/Viewport.h>
23
24namespace Web::Layout {
25
26BlockFormattingContext::BlockFormattingContext(LayoutState& state, BlockContainer const& root, FormattingContext* parent)
27 : FormattingContext(Type::Block, state, root, parent)
28{
29}
30
31BlockFormattingContext::~BlockFormattingContext()
32{
33 if (!m_was_notified_after_parent_dimensioned_my_root_box) {
34 // HACK: The parent formatting context never notified us after assigning dimensions to our root box.
35 // Pretend that it did anyway, to make sure absolutely positioned children get laid out.
36 // FIXME: Get rid of this hack once parent contexts behave properly.
37 parent_context_did_dimension_child_root_box();
38 }
39}
40
41CSSPixels BlockFormattingContext::automatic_content_height() const
42{
43 return compute_auto_height_for_block_formatting_context_root(root());
44}
45
46void BlockFormattingContext::run(Box const&, LayoutMode layout_mode, AvailableSpace const& available_space)
47{
48 if (is<Viewport>(root())) {
49 layout_viewport(layout_mode, available_space);
50 return;
51 }
52
53 if (root().children_are_inline())
54 layout_inline_children(root(), layout_mode, available_space);
55 else
56 layout_block_level_children(root(), layout_mode, available_space);
57
58 // Assign collapsed margin left after children layout of formatting context to the last child box
59 if (m_margin_state.current_collapsed_margin() != 0) {
60 for (auto* child_box = root().last_child_of_type<Box>(); child_box; child_box = child_box->previous_sibling_of_type<Box>()) {
61 if (child_box->is_absolutely_positioned() || child_box->is_floating())
62 continue;
63 m_state.get_mutable(*child_box).margin_bottom = m_margin_state.current_collapsed_margin().value();
64 break;
65 }
66 }
67}
68
69void BlockFormattingContext::parent_context_did_dimension_child_root_box()
70{
71 m_was_notified_after_parent_dimensioned_my_root_box = true;
72
73 // Left-side floats: offset_from_edge is from left edge (0) to left content edge of floating_box.
74 for (auto& floating_box : m_left_floats.all_boxes) {
75 auto& box_state = m_state.get_mutable(floating_box->box);
76 box_state.set_content_x(floating_box->offset_from_edge.value());
77 }
78
79 // Right-side floats: offset_from_edge is from right edge (float_containing_block_width) to the left content edge of floating_box.
80 for (auto& floating_box : m_right_floats.all_boxes) {
81 auto float_containing_block_width = containing_block_width_for(floating_box->box);
82 auto& box_state = m_state.get_mutable(floating_box->box);
83 box_state.set_content_x((float_containing_block_width - floating_box->offset_from_edge).value());
84 }
85
86 // We can also layout absolutely positioned boxes within this BFC.
87 for (auto& box : m_absolutely_positioned_boxes) {
88 auto& cb_state = m_state.get(*box.containing_block());
89 auto available_width = AvailableSize::make_definite(cb_state.content_width() + cb_state.padding_left + cb_state.padding_right);
90 auto available_height = AvailableSize::make_definite(cb_state.content_height() + cb_state.padding_top + cb_state.padding_bottom);
91 layout_absolutely_positioned_element(box, AvailableSpace(available_width, available_height));
92 }
93}
94
95void BlockFormattingContext::compute_width(Box const& box, AvailableSpace const& available_space, LayoutMode)
96{
97 if (box.is_absolutely_positioned()) {
98 compute_width_for_absolutely_positioned_element(box, available_space);
99 return;
100 }
101
102 if (is<ReplacedBox>(box)) {
103 // FIXME: This should not be done *by* ReplacedBox
104 auto& replaced = verify_cast<ReplacedBox>(box);
105 // FIXME: This const_cast is gross.
106 const_cast<ReplacedBox&>(replaced).prepare_for_replaced_layout();
107 compute_width_for_block_level_replaced_element_in_normal_flow(replaced, available_space);
108 // NOTE: We don't return here.
109 }
110
111 if (box.is_floating()) {
112 compute_width_for_floating_box(box, available_space);
113 return;
114 }
115
116 auto const& computed_values = box.computed_values();
117
118 auto width_of_containing_block = available_space.width.to_px();
119 auto width_of_containing_block_as_length_for_resolve = available_space.width.is_definite() ? CSS::Length::make_px(width_of_containing_block) : CSS::Length::make_px(0);
120
121 auto zero_value = CSS::Length::make_px(0);
122
123 auto margin_left = CSS::Length::make_auto();
124 auto margin_right = CSS::Length::make_auto();
125 auto const padding_left = computed_values.padding().left().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
126 auto const padding_right = computed_values.padding().right().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
127
128 auto& box_state = m_state.get_mutable(box);
129 box_state.border_left = computed_values.border_left().width;
130 box_state.border_right = computed_values.border_right().width;
131 box_state.padding_left = padding_left.to_px(box);
132 box_state.padding_right = padding_right.to_px(box);
133
134 if (should_treat_width_as_auto(box, available_space) && available_space.width.is_intrinsic_sizing_constraint())
135 return;
136
137 auto try_compute_width = [&](auto const& a_width) {
138 CSS::Length width = a_width;
139 margin_left = computed_values.margin().left().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
140 margin_right = computed_values.margin().right().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
141 CSSPixels total_px = computed_values.border_left().width + computed_values.border_right().width;
142 for (auto& value : { margin_left, padding_left, width, padding_right, margin_right }) {
143 total_px += value.to_px(box);
144 }
145
146 if (!box.is_inline()) {
147 // 10.3.3 Block-level, non-replaced elements in normal flow
148 // If 'width' is not 'auto' and 'border-left-width' + 'padding-left' + 'width' + 'padding-right' + 'border-right-width' (plus any of 'margin-left' or 'margin-right' that are not 'auto') is larger than the width of the containing block, then any 'auto' values for 'margin-left' or 'margin-right' are, for the following rules, treated as zero.
149 if (width.is_auto() && total_px > width_of_containing_block) {
150 if (margin_left.is_auto())
151 margin_left = zero_value;
152 if (margin_right.is_auto())
153 margin_right = zero_value;
154 }
155
156 // 10.3.3 cont'd.
157 auto underflow_px = width_of_containing_block - total_px;
158 if (!isfinite(underflow_px.value()))
159 underflow_px = 0;
160
161 if (width.is_auto()) {
162 if (margin_left.is_auto())
163 margin_left = zero_value;
164 if (margin_right.is_auto())
165 margin_right = zero_value;
166
167 if (available_space.width.is_definite()) {
168 if (underflow_px >= 0) {
169 width = CSS::Length::make_px(underflow_px);
170 } else {
171 width = zero_value;
172 margin_right = CSS::Length::make_px(margin_right.to_px(box) + underflow_px.value());
173 }
174 }
175 } else {
176 if (!margin_left.is_auto() && !margin_right.is_auto()) {
177 margin_right = CSS::Length::make_px(margin_right.to_px(box) + underflow_px.value());
178 } else if (!margin_left.is_auto() && margin_right.is_auto()) {
179 margin_right = CSS::Length::make_px(underflow_px);
180 } else if (margin_left.is_auto() && !margin_right.is_auto()) {
181 margin_left = CSS::Length::make_px(underflow_px);
182 } else { // margin_left.is_auto() && margin_right.is_auto()
183 auto half_of_the_underflow = CSS::Length::make_px(underflow_px / 2);
184 margin_left = half_of_the_underflow;
185 margin_right = half_of_the_underflow;
186 }
187 }
188 }
189
190 return width;
191 };
192
193 auto input_width = [&] {
194 if (is<TableWrapper>(box))
195 return CSS::Length::make_px(compute_width_for_table_wrapper(box, available_space));
196 if (should_treat_width_as_auto(box, available_space))
197 return CSS::Length::make_auto();
198 return calculate_inner_width(box, available_space.width, computed_values.width());
199 }();
200
201 // 1. The tentative used width is calculated (without 'min-width' and 'max-width')
202 auto used_width = try_compute_width(input_width);
203
204 // 2. The tentative used width is greater than 'max-width', the rules above are applied again,
205 // but this time using the computed value of 'max-width' as the computed value for 'width'.
206 if (!computed_values.max_width().is_none()) {
207 auto max_width = calculate_inner_width(box, available_space.width, computed_values.max_width());
208 if (used_width.to_px(box) > max_width.to_px(box)) {
209 used_width = try_compute_width(max_width);
210 }
211 }
212
213 // 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
214 // but this time using the value of 'min-width' as the computed value for 'width'.
215 if (!computed_values.min_width().is_auto()) {
216 auto min_width = calculate_inner_width(box, available_space.width, computed_values.min_width());
217 if (used_width.to_px(box) < min_width.to_px(box)) {
218 used_width = try_compute_width(min_width);
219 }
220 }
221
222 if (!is<ReplacedBox>(box) && !used_width.is_auto())
223 box_state.set_content_width(used_width.to_px(box));
224
225 box_state.margin_left = margin_left.to_px(box);
226 box_state.margin_right = margin_right.to_px(box);
227
228 resolve_vertical_box_model_metrics(box, m_state);
229}
230
231void BlockFormattingContext::compute_width_for_floating_box(Box const& box, AvailableSpace const& available_space)
232{
233 // 10.3.5 Floating, non-replaced elements
234 auto& computed_values = box.computed_values();
235
236 auto zero_value = CSS::Length::make_px(0);
237 auto width_of_containing_block = available_space.width.to_px();
238 auto width_of_containing_block_as_length_for_resolve = CSS::Length::make_px(width_of_containing_block);
239 if (!available_space.width.is_definite())
240 width_of_containing_block_as_length_for_resolve = CSS::Length::make_px(0);
241
242 auto margin_left = computed_values.margin().left().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
243 auto margin_right = computed_values.margin().right().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
244 auto const padding_left = computed_values.padding().left().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
245 auto const padding_right = computed_values.padding().right().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
246
247 // If 'margin-left', or 'margin-right' are computed as 'auto', their used value is '0'.
248 if (margin_left.is_auto())
249 margin_left = zero_value;
250 if (margin_right.is_auto())
251 margin_right = zero_value;
252
253 auto compute_width = [&](auto width) {
254 // If 'width' is computed as 'auto', the used value is the "shrink-to-fit" width.
255 if (width.is_auto()) {
256
257 // Find the available width: in this case, this is the width of the containing
258 // block minus the used values of 'margin-left', 'border-left-width', 'padding-left',
259 // 'padding-right', 'border-right-width', 'margin-right', and the widths of any relevant scroll bars.
260 auto available_width = width_of_containing_block
261 - margin_left.to_px(box) - computed_values.border_left().width - padding_left.to_px(box)
262 - padding_right.to_px(box) - computed_values.border_right().width - margin_right.to_px(box);
263
264 auto result = calculate_shrink_to_fit_widths(box);
265
266 // Then the shrink-to-fit width is: min(max(preferred minimum width, available width), preferred width).
267 width = CSS::Length::make_px(min(max(result.preferred_minimum_width, available_width), result.preferred_width));
268 }
269
270 return width;
271 };
272
273 auto input_width = [&] {
274 if (should_treat_width_as_auto(box, available_space))
275 return CSS::Length::make_auto();
276 return calculate_inner_width(box, available_space.width, computed_values.width());
277 }();
278
279 // 1. The tentative used width is calculated (without 'min-width' and 'max-width')
280 auto width = compute_width(input_width);
281
282 // 2. The tentative used width is greater than 'max-width', the rules above are applied again,
283 // but this time using the computed value of 'max-width' as the computed value for 'width'.
284 if (!computed_values.max_width().is_none()) {
285 auto max_width = calculate_inner_width(box, available_space.width, computed_values.max_width());
286 if (width.to_px(box) > max_width.to_px(box))
287 width = compute_width(max_width);
288 }
289
290 // 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
291 // but this time using the value of 'min-width' as the computed value for 'width'.
292 if (!computed_values.min_width().is_auto()) {
293 auto min_width = calculate_inner_width(box, available_space.width, computed_values.min_width());
294 if (width.to_px(box) < min_width.to_px(box))
295 width = compute_width(min_width);
296 }
297
298 auto& box_state = m_state.get_mutable(box);
299 box_state.set_content_width(width.to_px(box));
300 box_state.margin_left = margin_left.to_px(box);
301 box_state.margin_right = margin_right.to_px(box);
302 box_state.border_left = computed_values.border_left().width;
303 box_state.border_right = computed_values.border_right().width;
304 box_state.padding_left = padding_left.to_px(box);
305 box_state.padding_right = padding_right.to_px(box);
306
307 resolve_vertical_box_model_metrics(box, m_state);
308}
309
310void BlockFormattingContext::compute_width_for_block_level_replaced_element_in_normal_flow(ReplacedBox const& box, AvailableSpace const& available_space)
311{
312 m_state.get_mutable(box).set_content_width(compute_width_for_replaced_element(m_state, box, available_space));
313}
314
315CSSPixels BlockFormattingContext::compute_width_for_table_wrapper(Box const& box, AvailableSpace const& available_space)
316{
317 // 17.5.2
318 // Table wrapper width should be equal to width of table box it contains
319 LayoutState throwaway_state(&m_state);
320 auto context = create_independent_formatting_context_if_needed(throwaway_state, box);
321 VERIFY(context);
322 context->run(box, LayoutMode::IntrinsicSizing, m_state.get(box).available_inner_space_or_constraints_from(available_space));
323 auto const* table_box = box.first_child_of_type<TableBox>();
324 return throwaway_state.get(*table_box).content_width();
325}
326
327void BlockFormattingContext::compute_height(Box const& box, AvailableSpace const& available_space)
328{
329 auto const& computed_values = box.computed_values();
330 auto containing_block_height = CSS::Length::make_px(available_space.height.to_px());
331
332 // Then work out what the height is, based on box type and CSS properties.
333 CSSPixels height = 0;
334 if (is<ReplacedBox>(box)) {
335 height = compute_height_for_replaced_element(m_state, verify_cast<ReplacedBox>(box), available_space);
336 } else {
337 if (should_treat_height_as_auto(box, available_space)) {
338 height = compute_auto_height_for_block_level_element(box, available_space);
339 } else {
340 height = calculate_inner_height(box, available_space.height, computed_values.height()).to_px(box);
341 }
342 }
343
344 if (!computed_values.max_height().is_none()) {
345 auto max_height = calculate_inner_height(box, available_space.height, computed_values.max_height());
346 if (!max_height.is_auto())
347 height = min(height, max_height.to_px(box));
348 }
349 if (!computed_values.min_height().is_auto()) {
350 height = max(height, calculate_inner_height(box, available_space.height, computed_values.min_height()).to_px(box));
351 }
352
353 m_state.get_mutable(box).set_content_height(height);
354}
355
356void BlockFormattingContext::layout_inline_children(BlockContainer const& block_container, LayoutMode layout_mode, AvailableSpace const& available_space)
357{
358 VERIFY(block_container.children_are_inline());
359
360 auto& block_container_state = m_state.get_mutable(block_container);
361
362 InlineFormattingContext context(m_state, block_container, *this);
363 context.run(
364 block_container,
365 layout_mode,
366 available_space);
367
368 if (!block_container_state.has_definite_width())
369 block_container_state.set_content_width(context.automatic_content_width());
370 if (!block_container_state.has_definite_height())
371 block_container_state.set_content_height(context.automatic_content_height());
372}
373
374static bool margins_collapse_through(Box const& box, LayoutState& state)
375{
376 // FIXME: A box's own margins collapse if the 'min-height' property is zero, and it has neither top or bottom borders
377 // nor top or bottom padding, and it has a 'height' of either 0 or 'auto', and it does not contain a line box, and
378 // all of its in-flow children's margins (if any) collapse.
379 // https://www.w3.org/TR/CSS22/box.html#collapsing-margins
380 return state.get(box).border_box_height() == 0;
381}
382
383CSSPixels BlockFormattingContext::compute_auto_height_for_block_level_element(Box const& box, AvailableSpace const& available_space)
384{
385 if (creates_block_formatting_context(box)) {
386 return compute_auto_height_for_block_formatting_context_root(box);
387 }
388
389 auto const& box_state = m_state.get(box);
390
391 auto display = box.display();
392 if (display.is_flex_inside()) {
393 // https://drafts.csswg.org/css-flexbox-1/#algo-main-container
394 // NOTE: The automatic block size of a block-level flex container is its max-content size.
395 return calculate_max_content_height(box, available_space.width);
396 }
397 if (display.is_grid_inside()) {
398 // https://www.w3.org/TR/css-grid-2/#intrinsic-sizes
399 // In both inline and block formatting contexts, the grid container’s auto block size is its
400 // max-content size.
401 return calculate_max_content_height(box, available_space.width);
402 }
403 if (display.is_table_inside()) {
404 return calculate_max_content_height(box, available_space.height);
405 }
406
407 // https://www.w3.org/TR/CSS22/visudet.html#normal-block
408 // 10.6.3 Block-level non-replaced elements in normal flow when 'overflow' computes to 'visible'
409
410 // The element's height is the distance from its top content edge to the first applicable of the following:
411
412 // 1. the bottom edge of the last line box, if the box establishes a inline formatting context with one or more lines
413 if (box.children_are_inline() && !box_state.line_boxes.is_empty())
414 return box_state.line_boxes.last().bottom().value();
415
416 // 2. the bottom edge of the bottom (possibly collapsed) margin of its last in-flow child, if the child's bottom margin does not collapse with the element's bottom margin
417 // 3. the bottom border edge of the last in-flow child whose top margin doesn't collapse with the element's bottom margin
418 if (!box.children_are_inline()) {
419 for (auto* child_box = box.last_child_of_type<Box>(); child_box; child_box = child_box->previous_sibling_of_type<Box>()) {
420 if (child_box->is_absolutely_positioned() || child_box->is_floating())
421 continue;
422
423 // FIXME: This is hack. If the last child is a list-item marker box, we ignore it for purposes of height calculation.
424 // Perhaps markers should not be considered in-flow(?) Perhaps they should always be the first child of the list-item
425 // box instead of the last child.
426 if (child_box->is_list_item_marker_box())
427 continue;
428
429 auto const& child_box_state = m_state.get(*child_box);
430
431 // Ignore anonymous block containers with no lines. These don't count as in-flow block boxes.
432 if (!child_box->is_table_wrapper() && child_box->is_anonymous() && child_box->is_block_container() && child_box_state.line_boxes.is_empty())
433 continue;
434
435 auto margin_bottom = m_margin_state.current_collapsed_margin();
436 if (box_state.padding_bottom == 0 && box_state.border_bottom == 0) {
437 m_margin_state.box_last_in_flow_child_margin_bottom_collapsed = true;
438 margin_bottom = 0;
439 }
440
441 return max(0.0f, (child_box_state.offset.y() + child_box_state.content_height() + child_box_state.border_box_bottom() + margin_bottom).value());
442 }
443 }
444
445 // 4. zero, otherwise
446 return 0;
447}
448
449void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContainer const& block_container, LayoutMode layout_mode, CSSPixels& bottom_of_lowest_margin_box, AvailableSpace const& available_space)
450{
451 auto& box_state = m_state.get_mutable(box);
452
453 if (box.is_absolutely_positioned()) {
454 m_absolutely_positioned_boxes.append(box);
455 return;
456 }
457
458 // NOTE: ListItemMarkerBoxes are placed by their corresponding ListItemBox.
459 if (is<ListItemMarkerBox>(box))
460 return;
461
462 auto const y = m_y_offset_of_current_block_container.value();
463
464 if (box.is_floating()) {
465 auto margin_top = !m_margin_state.has_block_container_waiting_for_final_y_position() ? m_margin_state.current_collapsed_margin() : 0;
466 layout_floating_box(box, block_container, layout_mode, available_space, margin_top + y);
467 bottom_of_lowest_margin_box = max(bottom_of_lowest_margin_box, box_state.offset.y() + box_state.content_height() + box_state.margin_box_bottom());
468 return;
469 }
470
471 compute_width(box, available_space, layout_mode);
472
473 if (box_state.has_definite_height()) {
474 compute_height(box, available_space);
475 }
476
477 if (box.computed_values().clear() != CSS::Clear::None) {
478 m_margin_state.reset();
479 }
480
481 m_margin_state.add_margin(box_state.margin_top);
482 m_margin_state.update_block_waiting_for_final_y_position();
483
484 auto margin_top = m_margin_state.current_collapsed_margin();
485 if (m_margin_state.has_block_container_waiting_for_final_y_position()) {
486 // If first child margin top will collapse with margin-top of containing block then margin-top of child is 0
487 margin_top = 0;
488 }
489
490 place_block_level_element_in_normal_flow_vertically(box, y + margin_top);
491 place_block_level_element_in_normal_flow_horizontally(box, available_space);
492
493 OwnPtr<FormattingContext> independent_formatting_context;
494 if (!box.is_replaced_box() && box.has_children()) {
495 independent_formatting_context = create_independent_formatting_context_if_needed(m_state, box);
496 if (independent_formatting_context) {
497 // Margins of elements that establish new formatting contexts do not collapse with their in-flow children
498 m_margin_state.reset();
499
500 independent_formatting_context->run(box, layout_mode, box_state.available_inner_space_or_constraints_from(available_space));
501 } else {
502 if (box.children_are_inline()) {
503 layout_inline_children(verify_cast<BlockContainer>(box), layout_mode, box_state.available_inner_space_or_constraints_from(available_space));
504 } else {
505 if (box_state.border_top > 0 || box_state.padding_top > 0) {
506 // margin-top of block container can't collapse with it's children if it has non zero border or padding
507 m_margin_state.reset();
508 } else if (!m_margin_state.has_block_container_waiting_for_final_y_position()) {
509 // margin-top of block container can be updated during children layout hence it's final y position yet to be determined
510 m_margin_state.register_block_container_y_position_update_callback([&](CSSPixels margin_top) {
511 place_block_level_element_in_normal_flow_vertically(box, margin_top + y + box_state.border_box_top());
512 });
513 }
514
515 layout_block_level_children(verify_cast<BlockContainer>(box), layout_mode, box_state.available_inner_space_or_constraints_from(available_space));
516 }
517 }
518 }
519
520 compute_height(box, available_space);
521
522 if (!margins_collapse_through(box, m_state)) {
523 if (!m_margin_state.box_last_in_flow_child_margin_bottom_collapsed) {
524 m_margin_state.reset();
525 }
526 m_y_offset_of_current_block_container = box_state.offset.y() + box_state.content_height() + box_state.border_box_bottom();
527 }
528 m_margin_state.box_last_in_flow_child_margin_bottom_collapsed = false;
529
530 m_margin_state.add_margin(box_state.margin_bottom);
531 m_margin_state.update_block_waiting_for_final_y_position();
532
533 compute_inset(box);
534
535 if (is<ListItemBox>(box)) {
536 layout_list_item_marker(static_cast<ListItemBox const&>(box));
537 }
538
539 bottom_of_lowest_margin_box = max(bottom_of_lowest_margin_box, box_state.offset.y() + box_state.content_height() + box_state.margin_box_bottom());
540
541 if (independent_formatting_context)
542 independent_formatting_context->parent_context_did_dimension_child_root_box();
543}
544
545void BlockFormattingContext::layout_block_level_children(BlockContainer const& block_container, LayoutMode layout_mode, AvailableSpace const& available_space)
546{
547 VERIFY(!block_container.children_are_inline());
548
549 CSSPixels bottom_of_lowest_margin_box = 0;
550
551 TemporaryChange<Optional<CSSPixels>> change { m_y_offset_of_current_block_container, CSSPixels(0) };
552 block_container.for_each_child_of_type<Box>([&](Box& box) {
553 layout_block_level_box(box, block_container, layout_mode, bottom_of_lowest_margin_box, available_space);
554 return IterationDecision::Continue;
555 });
556
557 m_margin_state.block_container_y_position_update_callback = {};
558
559 if (layout_mode == LayoutMode::IntrinsicSizing) {
560 auto& block_container_state = m_state.get_mutable(block_container);
561 if (!block_container_state.has_definite_width())
562 block_container_state.set_content_width(greatest_child_width(block_container));
563 if (!block_container_state.has_definite_height())
564 block_container_state.set_content_height(bottom_of_lowest_margin_box);
565 }
566}
567
568void BlockFormattingContext::resolve_vertical_box_model_metrics(Box const& box, LayoutState& state)
569{
570 auto& box_state = state.get_mutable(box);
571 auto const& computed_values = box.computed_values();
572 auto width_of_containing_block = CSS::Length::make_px(containing_block_width_for(box, state));
573
574 box_state.margin_top = computed_values.margin().top().resolved(box, width_of_containing_block).to_px(box);
575 box_state.margin_bottom = computed_values.margin().bottom().resolved(box, width_of_containing_block).to_px(box);
576 box_state.border_top = computed_values.border_top().width;
577 box_state.border_bottom = computed_values.border_bottom().width;
578 box_state.padding_top = computed_values.padding().top().resolved(box, width_of_containing_block).to_px(box);
579 box_state.padding_bottom = computed_values.padding().bottom().resolved(box, width_of_containing_block).to_px(box);
580}
581
582CSSPixels BlockFormattingContext::BlockMarginState::current_collapsed_margin() const
583{
584 CSSPixels smallest_margin = 0;
585 CSSPixels largest_margin = 0;
586 size_t negative_margin_count = 0;
587 for (auto margin : current_collapsible_margins) {
588 if (margin < 0)
589 ++negative_margin_count;
590 largest_margin = max(largest_margin, margin);
591 smallest_margin = min(smallest_margin, margin);
592 }
593
594 CSSPixels collapsed_margin = 0;
595 if (negative_margin_count == current_collapsible_margins.size()) {
596 // When all margins are negative, the size of the collapsed margin is the smallest (most negative) margin.
597 collapsed_margin = smallest_margin;
598 } else if (negative_margin_count > 0) {
599 // When negative margins are involved, the size of the collapsed margin is the sum of the largest positive margin and the smallest (most negative) negative margin.
600 collapsed_margin = largest_margin + smallest_margin;
601 } else {
602 // Otherwise, collapse all the adjacent margins by using only the largest one.
603 collapsed_margin = largest_margin;
604 }
605
606 return collapsed_margin;
607}
608
609void BlockFormattingContext::place_block_level_element_in_normal_flow_vertically(Box const& child_box, CSSPixels y)
610{
611 auto& box_state = m_state.get_mutable(child_box);
612 auto const& computed_values = child_box.computed_values();
613
614 auto clear_floating_boxes = [&](FloatSideData& float_side) {
615 if (!float_side.current_boxes.is_empty()) {
616 // NOTE: Floating boxes are globally relevant within this BFC, *but* their offset coordinates
617 // are relative to their containing block.
618 // This means that we have to first convert to a root-space Y coordinate before clearing,
619 // and then convert back to a local Y coordinate when assigning the cleared offset to
620 // the `child_box` layout state.
621
622 // First, find the lowest margin box edge on this float side and calculate the Y offset just below it.
623 CSSPixels clearance_y_in_root = 0;
624 for (auto const& floating_box : float_side.current_boxes) {
625 auto floating_box_rect_in_root = margin_box_rect_in_ancestor_coordinate_space(floating_box.box, root(), m_state);
626 clearance_y_in_root = max(clearance_y_in_root, floating_box_rect_in_root.bottom() + 1);
627 }
628
629 // Then, convert the clearance Y to a coordinate relative to the containing block of `child_box`.
630 CSSPixels clearance_y_in_containing_block = clearance_y_in_root;
631 for (auto* containing_block = child_box.containing_block(); containing_block && containing_block != &root(); containing_block = containing_block->containing_block())
632 clearance_y_in_containing_block -= m_state.get(*containing_block).offset.y();
633
634 if (clearance_y_in_containing_block > y)
635 m_y_offset_of_current_block_container = clearance_y_in_containing_block;
636 y = max(y, clearance_y_in_containing_block);
637 float_side.clear();
638 }
639 };
640
641 // Flex-items don't float and also don't clear.
642 if ((computed_values.clear() == CSS::Clear::Left || computed_values.clear() == CSS::Clear::Both) && !child_box.is_flex_item())
643 clear_floating_boxes(m_left_floats);
644 if ((computed_values.clear() == CSS::Clear::Right || computed_values.clear() == CSS::Clear::Both) && !child_box.is_flex_item())
645 clear_floating_boxes(m_right_floats);
646
647 y += box_state.border_box_top();
648
649 box_state.set_content_offset(CSSPixelPoint { box_state.offset.x(), y.value() });
650}
651
652void BlockFormattingContext::place_block_level_element_in_normal_flow_horizontally(Box const& child_box, AvailableSpace const& available_space)
653{
654 auto& box_state = m_state.get_mutable(child_box);
655
656 CSSPixels x = 0;
657 CSSPixels available_width_within_containing_block = available_space.width.to_px();
658
659 if ((!m_left_floats.current_boxes.is_empty() || !m_right_floats.current_boxes.is_empty())
660 && creates_block_formatting_context(child_box)) {
661 auto box_in_root_rect = content_box_rect_in_ancestor_coordinate_space(child_box, root(), m_state);
662 auto space = space_used_by_floats(box_in_root_rect.y());
663 available_width_within_containing_block -= space.left + space.right;
664 x += space.left;
665 }
666
667 if (child_box.containing_block()->computed_values().text_align() == CSS::TextAlign::LibwebCenter) {
668 x += (available_width_within_containing_block / 2) - box_state.content_width() / 2;
669 } else {
670 x += box_state.margin_box_left();
671 }
672
673 box_state.set_content_offset({ x.value(), box_state.offset.y() });
674}
675
676static void measure_scrollable_overflow(LayoutState const& state, Box const& box, CSSPixels& bottom_edge, CSSPixels& right_edge)
677{
678 auto const& child_state = state.get(box);
679 auto child_rect = absolute_content_rect(box, state);
680 child_rect.inflate(child_state.border_box_top(), child_state.border_box_right(), child_state.border_box_bottom(), child_state.border_box_left());
681
682 bottom_edge = max(bottom_edge, child_rect.bottom());
683 right_edge = max(right_edge, child_rect.right());
684
685 if (box.computed_values().overflow_x() == CSS::Overflow::Hidden && box.computed_values().overflow_y() == CSS::Overflow::Hidden)
686 return;
687
688 box.for_each_child_of_type<Box>([&](Box const& child) {
689 measure_scrollable_overflow(state, child, bottom_edge, right_edge);
690 return IterationDecision::Continue;
691 });
692}
693
694void BlockFormattingContext::layout_viewport(LayoutMode layout_mode, AvailableSpace const& available_space)
695{
696 auto viewport_rect = root().browsing_context().viewport_rect();
697
698 auto& viewport = verify_cast<Layout::Viewport>(root());
699 auto& viewport_state = m_state.get_mutable(viewport);
700
701 if (root().children_are_inline())
702 layout_inline_children(root(), layout_mode, available_space);
703 else
704 layout_block_level_children(root(), layout_mode, available_space);
705
706 CSSPixels bottom_edge = 0;
707 CSSPixels right_edge = 0;
708 measure_scrollable_overflow(m_state, viewport, bottom_edge, right_edge);
709
710 if (bottom_edge >= viewport_rect.height() || right_edge >= viewport_rect.width()) {
711 // FIXME: Move overflow data to LayoutState!
712 auto& overflow_data = viewport_state.ensure_overflow_data();
713 overflow_data.scrollable_overflow_rect = viewport_rect;
714 // NOTE: The edges are *within* the rectangle, so we add 1 to get the width and height.
715 overflow_data.scrollable_overflow_rect.set_size(right_edge + 1, bottom_edge + 1);
716 }
717}
718
719void BlockFormattingContext::layout_floating_box(Box const& box, BlockContainer const&, LayoutMode layout_mode, AvailableSpace const& available_space, CSSPixels y, LineBuilder* line_builder)
720{
721 VERIFY(box.is_floating());
722
723 auto& box_state = m_state.get_mutable(box);
724 CSSPixels width_of_containing_block = available_space.width.to_px();
725
726 compute_width(box, available_space, layout_mode);
727 auto independent_formatting_context = layout_inside(box, layout_mode, box_state.available_inner_space_or_constraints_from(available_space));
728 compute_height(box, available_space);
729
730 // First we place the box normally (to get the right y coordinate.)
731 // If we have a LineBuilder, we're in the middle of inline layout, otherwise this is block layout.
732 if (line_builder) {
733 auto y = line_builder->y_for_float_to_be_inserted_here(box);
734 box_state.set_content_y(y + box_state.margin_box_top());
735 } else {
736 place_block_level_element_in_normal_flow_vertically(box, y + box_state.margin_top);
737 place_block_level_element_in_normal_flow_horizontally(box, available_space);
738 }
739
740 // Then we float it to the left or right.
741 auto float_box = [&](FloatSide side, FloatSideData& side_data, FloatSideData& other_side_data) {
742 CSSPixels offset_from_edge = 0;
743 auto float_to_edge = [&] {
744 if (side == FloatSide::Left)
745 offset_from_edge = box_state.margin_box_left();
746 else
747 offset_from_edge = box_state.content_width() + box_state.margin_box_right();
748 };
749
750 auto box_in_root_rect = content_box_rect_in_ancestor_coordinate_space(box, root(), m_state);
751 CSSPixels y_in_root = box_in_root_rect.y();
752 CSSPixels y = box_state.offset.y();
753
754 if (side_data.current_boxes.is_empty()) {
755 // This is the first floating box on this side. Go all the way to the edge.
756 float_to_edge();
757 side_data.y_offset = 0;
758 } else {
759
760 // NOTE: If we're in inline layout, the LineBuilder has already provided the right Y offset.
761 // In block layout, we adjust by the side's current Y offset here.
762 if (!line_builder)
763 y_in_root += side_data.y_offset;
764
765 bool did_touch_preceding_float = false;
766 bool did_place_next_to_preceding_float = false;
767
768 // Walk all currently tracked floats on the side we're floating towards.
769 // We're looking for the innermost preceding float that intersects vertically with `box`.
770 for (auto& preceding_float : side_data.current_boxes.in_reverse()) {
771 auto const preceding_float_rect = margin_box_rect_in_ancestor_coordinate_space(preceding_float.box, root(), m_state);
772 if (!preceding_float_rect.contains_vertically(y_in_root))
773 continue;
774 // We found a preceding float that intersects vertically with the current float.
775 // Now we need to find out if there's enough inline-axis space to stack them next to each other.
776 auto const& preceding_float_state = m_state.get(preceding_float.box);
777 CSSPixels tentative_offset_from_edge = 0;
778 bool fits_next_to_preceding_float = false;
779 if (side == FloatSide::Left) {
780 tentative_offset_from_edge = preceding_float.offset_from_edge + preceding_float_state.content_width() + preceding_float_state.margin_box_right() + box_state.margin_box_left();
781 fits_next_to_preceding_float = (tentative_offset_from_edge + box_state.content_width() + box_state.margin_box_right()) <= width_of_containing_block;
782 } else {
783 tentative_offset_from_edge = preceding_float.offset_from_edge + preceding_float_state.margin_box_left() + box_state.margin_box_right() + box_state.content_width();
784 fits_next_to_preceding_float = tentative_offset_from_edge >= 0;
785 }
786 did_touch_preceding_float = true;
787 if (!fits_next_to_preceding_float)
788 continue;
789 offset_from_edge = tentative_offset_from_edge;
790 did_place_next_to_preceding_float = true;
791 break;
792 }
793
794 if (!did_touch_preceding_float) {
795 // This box does not touch another floating box, go all the way to the edge.
796 float_to_edge();
797
798 // Also, forget all previous boxes floated to this side while since they're no longer relevant.
799 side_data.clear();
800 } else if (!did_place_next_to_preceding_float) {
801 // We ran out of horizontal space on this "float line", and need to break.
802 float_to_edge();
803 CSSPixels lowest_margin_edge = 0;
804 for (auto const& box : side_data.current_boxes) {
805 auto const& box_state = m_state.get(box.box);
806 lowest_margin_edge = max(lowest_margin_edge, box_state.margin_box_height());
807 }
808
809 side_data.y_offset += lowest_margin_edge;
810
811 // Also, forget all previous boxes floated to this side while since they're no longer relevant.
812 side_data.clear();
813 }
814 }
815
816 // NOTE: If we're in inline layout, the LineBuilder has already provided the right Y offset.
817 // In block layout, we adjust by the side's current Y offset here.
818 // FIXME: It's annoying that we have different behavior for inline vs block here.
819 // Find a way to unify the behavior so we don't need to branch here.
820
821 if (!line_builder)
822 y += side_data.y_offset;
823
824 side_data.all_boxes.append(adopt_own(*new FloatingBox {
825 .box = box,
826 .offset_from_edge = offset_from_edge,
827 .top_margin_edge = y - box_state.margin_box_top(),
828 .bottom_margin_edge = y + box_state.content_height() + box_state.margin_box_bottom(),
829 }));
830 side_data.current_boxes.append(*side_data.all_boxes.last());
831
832 if (side == FloatSide::Left) {
833 side_data.current_width = offset_from_edge + box_state.content_width() + box_state.margin_box_right();
834 } else {
835 side_data.current_width = offset_from_edge + box_state.margin_box_left();
836 }
837 side_data.max_width = max(side_data.current_width, side_data.max_width);
838
839 // NOTE: We don't set the X position here, that happens later, once we know the root block width.
840 // See parent_context_did_dimension_child_root_box() for that logic.
841 box_state.set_content_y(y.value());
842
843 // If the new box was inserted below the bottom of the opposite side,
844 // we reset the other side back to its edge.
845 if (y > other_side_data.y_offset)
846 other_side_data.clear();
847 };
848
849 // Next, float to the left and/or right
850 if (box.computed_values().float_() == CSS::Float::Left) {
851 float_box(FloatSide::Left, m_left_floats, m_right_floats);
852 } else if (box.computed_values().float_() == CSS::Float::Right) {
853 float_box(FloatSide::Right, m_right_floats, m_left_floats);
854 }
855
856 m_state.get_mutable(root()).add_floating_descendant(box);
857
858 if (line_builder)
859 line_builder->recalculate_available_space();
860
861 if (independent_formatting_context)
862 independent_formatting_context->parent_context_did_dimension_child_root_box();
863}
864
865void BlockFormattingContext::layout_list_item_marker(ListItemBox const& list_item_box)
866{
867 if (!list_item_box.marker())
868 return;
869
870 auto& marker = *list_item_box.marker();
871 auto& marker_state = m_state.get_mutable(marker);
872 auto& list_item_state = m_state.get_mutable(list_item_box);
873
874 CSSPixels image_width = 0;
875 CSSPixels image_height = 0;
876 if (auto const* list_style_image = marker.list_style_image()) {
877 image_width = list_style_image->natural_width().value_or(0);
878 image_height = list_style_image->natural_height().value_or(0);
879 }
880
881 CSSPixels default_marker_width = max(4, marker.font().pixel_size_rounded_up() - 4);
882
883 if (marker.text().is_empty()) {
884 marker_state.set_content_width((image_width + default_marker_width).value());
885 } else {
886 auto text_width = marker.font().width(marker.text());
887 marker_state.set_content_width((image_width + text_width).value());
888 }
889
890 marker_state.set_content_height(max(image_height, marker.font().pixel_size_rounded_up() + 1).value());
891
892 marker_state.set_content_offset({ -(marker_state.content_width() + default_marker_width),
893 max(CSSPixels(0.f), (CSSPixels(marker.line_height()) - marker_state.content_height()) / 2.f) });
894
895 if (marker_state.content_height() > list_item_state.content_height())
896 list_item_state.set_content_height(marker_state.content_height());
897}
898
899BlockFormattingContext::SpaceUsedByFloats BlockFormattingContext::space_used_by_floats(CSSPixels y) const
900{
901 SpaceUsedByFloats space_used_by_floats;
902
903 for (auto const& floating_box_ptr : m_left_floats.all_boxes.in_reverse()) {
904 auto const& floating_box = *floating_box_ptr;
905 auto const& floating_box_state = m_state.get(floating_box.box);
906 // NOTE: The floating box is *not* in the final horizontal position yet, but the size and vertical position is valid.
907 auto rect = margin_box_rect_in_ancestor_coordinate_space(floating_box.box, root(), m_state);
908 if (rect.contains_vertically(y.value())) {
909 space_used_by_floats.left = floating_box.offset_from_edge
910 + floating_box_state.content_width()
911 + floating_box_state.margin_box_right();
912 break;
913 }
914 }
915
916 for (auto const& floating_box_ptr : m_right_floats.all_boxes.in_reverse()) {
917 auto const& floating_box = *floating_box_ptr;
918 auto const& floating_box_state = m_state.get(floating_box.box);
919 // NOTE: The floating box is *not* in the final horizontal position yet, but the size and vertical position is valid.
920 auto rect = margin_box_rect_in_ancestor_coordinate_space(floating_box.box, root(), m_state);
921 if (rect.contains_vertically(y.value())) {
922 space_used_by_floats.right = floating_box.offset_from_edge
923 + floating_box_state.margin_box_left();
924 break;
925 }
926 }
927
928 return space_used_by_floats;
929}
930
931CSSPixels BlockFormattingContext::greatest_child_width(Box const& box)
932{
933 // Similar to FormattingContext::greatest_child_width()
934 // but this one takes floats into account!
935 CSSPixels max_width = m_left_floats.max_width + m_right_floats.max_width;
936 if (box.children_are_inline()) {
937 for (auto const& line_box : m_state.get(verify_cast<BlockContainer>(box)).line_boxes) {
938 CSSPixels width_here = line_box.width();
939 CSSPixels extra_width_from_left_floats = 0;
940 for (auto& left_float : m_left_floats.all_boxes) {
941 if (line_box.baseline() >= left_float->top_margin_edge.value() || line_box.baseline() <= left_float->bottom_margin_edge.value()) {
942 auto const& left_float_state = m_state.get(left_float->box);
943 extra_width_from_left_floats = max(extra_width_from_left_floats, left_float->offset_from_edge + left_float_state.content_width() + left_float_state.margin_box_right());
944 }
945 }
946 CSSPixels extra_width_from_right_floats = 0;
947 for (auto& right_float : m_right_floats.all_boxes) {
948 if (line_box.baseline() >= right_float->top_margin_edge.value() || line_box.baseline() <= right_float->bottom_margin_edge.value()) {
949 auto const& right_float_state = m_state.get(right_float->box);
950 extra_width_from_right_floats = max(extra_width_from_right_floats, right_float->offset_from_edge + right_float_state.margin_box_left());
951 }
952 }
953 width_here += extra_width_from_left_floats + extra_width_from_right_floats;
954 max_width = max(max_width, width_here);
955 }
956 } else {
957 box.for_each_child_of_type<Box>([&](Box const& child) {
958 if (!child.is_absolutely_positioned())
959 max_width = max(max_width, m_state.get(child).margin_box_width());
960 });
961 }
962 return max_width;
963}
964
965void BlockFormattingContext::determine_width_of_child(Box const& box, AvailableSpace const& available_space)
966{
967 compute_width(box, available_space);
968}
969
970void BlockFormattingContext::determine_height_of_child(Box const& box, AvailableSpace const& available_space)
971{
972 compute_height(box, available_space);
973}
974
975}