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 <LibWeb/Dump.h>
8#include <LibWeb/Layout/BlockFormattingContext.h>
9#include <LibWeb/Layout/Box.h>
10#include <LibWeb/Layout/FlexFormattingContext.h>
11#include <LibWeb/Layout/FormattingContext.h>
12#include <LibWeb/Layout/GridFormattingContext.h>
13#include <LibWeb/Layout/ReplacedBox.h>
14#include <LibWeb/Layout/SVGFormattingContext.h>
15#include <LibWeb/Layout/SVGSVGBox.h>
16#include <LibWeb/Layout/TableBox.h>
17#include <LibWeb/Layout/TableCellBox.h>
18#include <LibWeb/Layout/TableFormattingContext.h>
19#include <LibWeb/Layout/Viewport.h>
20
21namespace Web::Layout {
22
23FormattingContext::FormattingContext(Type type, LayoutState& state, Box const& context_box, FormattingContext* parent)
24 : m_type(type)
25 , m_parent(parent)
26 , m_context_box(context_box)
27 , m_state(state)
28{
29}
30
31FormattingContext::~FormattingContext() = default;
32
33// https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context
34bool FormattingContext::creates_block_formatting_context(Box const& box)
35{
36 // NOTE: Replaced elements never create a BFC.
37 if (box.is_replaced_box())
38 return false;
39
40 // display: table
41 if (box.display().is_table_inside()) {
42 return false;
43 }
44
45 // display: flex
46 if (box.display().is_flex_inside()) {
47 return false;
48 }
49
50 // display: grid
51 if (box.display().is_grid_inside()) {
52 return false;
53 }
54
55 // NOTE: This function uses MDN as a reference, not because it's authoritative,
56 // but because they've gathered all the conditions in one convenient location.
57
58 // The root element of the document (<html>).
59 if (box.is_root_element())
60 return true;
61
62 // Floats (elements where float isn't none).
63 if (box.is_floating())
64 return true;
65
66 // Absolutely positioned elements (elements where position is absolute or fixed).
67 if (box.is_absolutely_positioned())
68 return true;
69
70 // Inline-blocks (elements with display: inline-block).
71 if (box.display().is_inline_block())
72 return true;
73
74 // Table cells (elements with display: table-cell, which is the default for HTML table cells).
75 if (box.display().is_table_cell())
76 return true;
77
78 // Table captions (elements with display: table-caption, which is the default for HTML table captions).
79 if (box.display().is_table_caption())
80 return true;
81
82 // FIXME: Anonymous table cells implicitly created by the elements with display: table, table-row, table-row-group, table-header-group, table-footer-group
83 // (which is the default for HTML tables, table rows, table bodies, table headers, and table footers, respectively), or inline-table.
84
85 // Block elements where overflow has a value other than visible and clip.
86 CSS::Overflow overflow_x = box.computed_values().overflow_x();
87 if ((overflow_x != CSS::Overflow::Visible) && (overflow_x != CSS::Overflow::Clip))
88 return true;
89 CSS::Overflow overflow_y = box.computed_values().overflow_y();
90 if ((overflow_y != CSS::Overflow::Visible) && (overflow_y != CSS::Overflow::Clip))
91 return true;
92
93 // display: flow-root.
94 if (box.display().is_flow_root_inside())
95 return true;
96
97 // FIXME: Elements with contain: layout, content, or paint.
98
99 if (box.parent()) {
100 auto parent_display = box.parent()->display();
101
102 // Flex items (direct children of the element with display: flex or inline-flex) if they are neither flex nor grid nor table containers themselves.
103 if (parent_display.is_flex_inside())
104 return true;
105 // Grid items (direct children of the element with display: grid or inline-grid) if they are neither flex nor grid nor table containers themselves.
106 if (parent_display.is_grid_inside())
107 return true;
108 }
109
110 // FIXME: Multicol containers (elements where column-count or column-width isn't auto, including elements with column-count: 1).
111
112 // FIXME: column-span: all should always create a new formatting context, even when the column-span: all element isn't contained by a multicol container (Spec change, Chrome bug).
113
114 return false;
115}
116
117OwnPtr<FormattingContext> FormattingContext::create_independent_formatting_context_if_needed(LayoutState& state, Box const& child_box)
118{
119 if (child_box.is_replaced_box() && !child_box.can_have_children()) {
120 // NOTE: This is a bit strange.
121 // Basically, we create a pretend formatting context for replaced elements that does nothing.
122 // This allows other formatting contexts to treat them like elements that actually need inside layout
123 // without having separate code to handle replaced elements.
124 // FIXME: Find a better abstraction for this.
125 struct ReplacedFormattingContext : public FormattingContext {
126 ReplacedFormattingContext(LayoutState& state, Box const& box)
127 : FormattingContext(Type::Block, state, box)
128 {
129 }
130 virtual CSSPixels automatic_content_height() const override { return 0; };
131 virtual void run(Box const&, LayoutMode, AvailableSpace const&) override { }
132 };
133 return make<ReplacedFormattingContext>(state, child_box);
134 }
135
136 if (!child_box.can_have_children())
137 return {};
138
139 auto child_display = child_box.display();
140
141 if (is<SVGSVGBox>(child_box))
142 return make<SVGFormattingContext>(state, child_box, this);
143
144 if (child_display.is_flex_inside())
145 return make<FlexFormattingContext>(state, child_box, this);
146
147 if (creates_block_formatting_context(child_box))
148 return make<BlockFormattingContext>(state, verify_cast<BlockContainer>(child_box), this);
149
150 if (child_display.is_table_inside())
151 return make<TableFormattingContext>(state, verify_cast<TableBox>(child_box), this);
152
153 if (child_display.is_grid_inside()) {
154 return make<GridFormattingContext>(state, child_box, this);
155 }
156
157 VERIFY(is_block_formatting_context());
158 if (child_box.children_are_inline())
159 return {};
160
161 // The child box is a block container that doesn't create its own BFC.
162 // It will be formatted by this BFC.
163 if (!child_display.is_flow_inside()) {
164 dbgln("FIXME: Child box doesn't create BFC, but inside is also not flow! display={}", MUST(child_display.to_string()));
165 // HACK: Instead of crashing, create a dummy formatting context that does nothing.
166 // FIXME: Remove this once it's no longer needed. It currently swallows problem with standalone
167 // table-related boxes that don't get fixed up by CSS anonymous table box generation.
168 struct DummyFormattingContext : public FormattingContext {
169 DummyFormattingContext(LayoutState& state, Box const& box)
170 : FormattingContext(Type::Block, state, box)
171 {
172 }
173 virtual CSSPixels automatic_content_height() const override { return 0; };
174 virtual void run(Box const&, LayoutMode, AvailableSpace const&) override { }
175 };
176 return make<DummyFormattingContext>(state, child_box);
177 }
178 VERIFY(child_box.is_block_container());
179 VERIFY(child_display.is_flow_inside());
180 return {};
181}
182
183OwnPtr<FormattingContext> FormattingContext::layout_inside(Box const& child_box, LayoutMode layout_mode, AvailableSpace const& available_space)
184{
185 {
186 // OPTIMIZATION: If we're doing intrinsic sizing and `child_box` has definite size in both axes,
187 // we don't need to layout its insides. The size is resolvable without learning
188 // the metrics of whatever's inside the box.
189 auto const& used_values = m_state.get(child_box);
190 if (layout_mode == LayoutMode::IntrinsicSizing
191 && used_values.width_constraint == SizeConstraint::None
192 && used_values.height_constraint == SizeConstraint::None
193 && used_values.has_definite_width()
194 && used_values.has_definite_height()) {
195 return nullptr;
196 }
197 }
198
199 if (!child_box.can_have_children())
200 return {};
201
202 auto independent_formatting_context = create_independent_formatting_context_if_needed(m_state, child_box);
203 if (independent_formatting_context)
204 independent_formatting_context->run(child_box, layout_mode, available_space);
205 else
206 run(child_box, layout_mode, available_space);
207
208 return independent_formatting_context;
209}
210
211CSSPixels FormattingContext::greatest_child_width(Box const& box)
212{
213 CSSPixels max_width = 0;
214 if (box.children_are_inline()) {
215 for (auto& line_box : m_state.get(box).line_boxes) {
216 max_width = max(max_width, line_box.width());
217 }
218 } else {
219 box.for_each_child_of_type<Box>([&](Box const& child) {
220 if (!child.is_absolutely_positioned())
221 max_width = max(max_width, m_state.get(child).margin_box_width());
222 });
223 }
224 return max_width;
225}
226
227FormattingContext::ShrinkToFitResult FormattingContext::calculate_shrink_to_fit_widths(Box const& box)
228{
229 return {
230 .preferred_width = calculate_max_content_width(box),
231 .preferred_minimum_width = calculate_min_content_width(box),
232 };
233}
234
235static CSSPixelSize solve_replaced_size_constraint(LayoutState const& state, CSSPixels w, CSSPixels h, ReplacedBox const& box)
236{
237 // 10.4 Minimum and maximum widths: 'min-width' and 'max-width'
238
239 auto const& containing_block = *box.containing_block();
240 auto const& containing_block_state = state.get(containing_block);
241 auto width_of_containing_block = CSS::Length::make_px(containing_block_state.content_width());
242 auto height_of_containing_block = CSS::Length::make_px(containing_block_state.content_height());
243
244 CSSPixels specified_min_width = box.computed_values().min_width().is_auto() ? 0 : box.computed_values().min_width().resolved(box, width_of_containing_block).to_px(box);
245 CSSPixels specified_max_width = box.computed_values().max_width().is_none() ? w : box.computed_values().max_width().resolved(box, width_of_containing_block).to_px(box);
246 CSSPixels specified_min_height = box.computed_values().min_height().is_auto() ? 0 : box.computed_values().min_height().resolved(box, height_of_containing_block).to_px(box);
247 CSSPixels specified_max_height = box.computed_values().max_height().is_none() ? h : box.computed_values().max_height().resolved(box, height_of_containing_block).to_px(box);
248
249 auto min_width = min(specified_min_width, specified_max_width);
250 auto max_width = max(specified_min_width, specified_max_width);
251 auto min_height = min(specified_min_height, specified_max_height);
252 auto max_height = max(specified_min_height, specified_max_height);
253
254 if (w > max_width)
255 return { w, max(max_width * (h / w), min_height) };
256 if (w < min_width)
257 return { max_width, min(min_width * (h / w), max_height) };
258 if (h > max_height)
259 return { max(max_height * (w / h), min_width), max_height };
260 if (h < min_height)
261 return { min(min_height * (w / h), max_width), min_height };
262 if ((w > max_width && h > max_height) && (max_width / w < max_height / h))
263 return { max_width, max(min_height, max_width * (h / w)) };
264 if ((w > max_width && h > max_height) && (max_width / w > max_height / h))
265 return { max(min_width, max_height * (w / h)), max_height };
266 if ((w < min_width && h < min_height) && (min_width / w < min_height / h))
267 return { min(max_width, min_height * (w / h)), min_height };
268 if ((w < min_width && h < min_height) && (min_width / w > min_height / h))
269 return { min_width, min(max_height, min_width * (h / w)) };
270 if (w < min_width && h > max_height)
271 return { min_width, max_height };
272 if (w > max_width && h < min_height)
273 return { max_width, min_height };
274 return { w, h };
275}
276
277// https://www.w3.org/TR/CSS22/visudet.html#root-height
278CSSPixels FormattingContext::compute_auto_height_for_block_formatting_context_root(Box const& root) const
279{
280 // 10.6.7 'Auto' heights for block formatting context roots
281 Optional<CSSPixels> top;
282 Optional<CSSPixels> bottom;
283
284 if (root.children_are_inline()) {
285 // If it only has inline-level children, the height is the distance between
286 // the top content edge and the bottom of the bottommost line box.
287 auto const& line_boxes = m_state.get(root).line_boxes;
288 top = 0;
289 if (!line_boxes.is_empty())
290 bottom = line_boxes.last().bottom();
291 } else {
292 // If it has block-level children, the height is the distance between
293 // the top margin-edge of the topmost block-level child box
294 // and the bottom margin-edge of the bottommost block-level child box.
295 root.for_each_child_of_type<Box>([&](Layout::Box& child_box) {
296 // Absolutely positioned children are ignored,
297 // and relatively positioned boxes are considered without their offset.
298 // Note that the child box may be an anonymous block box.
299 if (child_box.is_absolutely_positioned())
300 return IterationDecision::Continue;
301
302 // FIXME: This doesn't look right.
303 if ((root.computed_values().overflow_y() == CSS::Overflow::Visible) && child_box.is_floating())
304 return IterationDecision::Continue;
305
306 auto const& child_box_state = m_state.get(child_box);
307
308 CSSPixels child_box_top = child_box_state.offset.y() - child_box_state.margin_box_top();
309 CSSPixels child_box_bottom = child_box_state.offset.y() + child_box_state.content_height() + child_box_state.margin_box_bottom();
310
311 if (!top.has_value() || child_box_top < top.value())
312 top = child_box_top;
313
314 if (!bottom.has_value() || child_box_bottom > bottom.value())
315 bottom = child_box_bottom;
316
317 return IterationDecision::Continue;
318 });
319 }
320
321 // In addition, if the element has any floating descendants
322 // whose bottom margin edge is below the element's bottom content edge,
323 // then the height is increased to include those edges.
324 for (auto* floating_box : m_state.get(root).floating_descendants()) {
325 // NOTE: Floating box coordinates are relative to their own containing block,
326 // which may or may not be the BFC root.
327 auto margin_box = margin_box_rect_in_ancestor_coordinate_space(*floating_box, root, m_state);
328 CSSPixels floating_box_bottom_margin_edge = margin_box.bottom() + 1;
329 if (!bottom.has_value() || floating_box_bottom_margin_edge > bottom.value())
330 bottom = floating_box_bottom_margin_edge;
331 }
332
333 return max(CSSPixels(0.0f), bottom.value_or(0) - top.value_or(0));
334}
335
336// 10.3.2 Inline, replaced elements, https://www.w3.org/TR/CSS22/visudet.html#inline-replaced-width
337CSSPixels FormattingContext::tentative_width_for_replaced_element(LayoutState const& state, ReplacedBox const& box, CSS::Size const& computed_width, AvailableSpace const& available_space)
338{
339 // Treat percentages of indefinite containing block widths as 0 (the initial width).
340 if (computed_width.is_percentage() && !state.get(*box.containing_block()).has_definite_width())
341 return 0;
342
343 auto height_of_containing_block = CSS::Length::make_px(containing_block_height_for(box, state));
344 auto computed_height = should_treat_height_as_auto(box, available_space) ? CSS::Size::make_auto() : box.computed_values().height();
345
346 CSSPixels used_width = computed_width.resolved(box, CSS::Length::make_px(available_space.width.to_px())).to_px(box);
347
348 // If 'height' and 'width' both have computed values of 'auto' and the element also has an intrinsic width,
349 // then that intrinsic width is the used value of 'width'.
350 if (computed_height.is_auto() && computed_width.is_auto() && box.has_intrinsic_width())
351 return box.intrinsic_width().value();
352
353 // If 'height' and 'width' both have computed values of 'auto' and the element has no intrinsic width,
354 // but does have an intrinsic height and intrinsic ratio;
355 // or if 'width' has a computed value of 'auto',
356 // 'height' has some other computed value, and the element does have an intrinsic ratio; then the used value of 'width' is:
357 //
358 // (used height) * (intrinsic ratio)
359 if ((computed_height.is_auto() && computed_width.is_auto() && !box.has_intrinsic_width() && box.has_intrinsic_height() && box.has_intrinsic_aspect_ratio())
360 || (computed_width.is_auto() && !computed_height.is_auto() && box.has_intrinsic_aspect_ratio())) {
361 return compute_height_for_replaced_element(state, box, available_space) * box.intrinsic_aspect_ratio().value();
362 }
363
364 // If 'height' and 'width' both have computed values of 'auto' and the element has an intrinsic ratio but no intrinsic height or width,
365 // then the used value of 'width' is undefined in CSS 2.2. However, it is suggested that, if the containing block's width does not itself
366 // depend on the replaced element's width, then the used value of 'width' is calculated from the constraint equation used for block-level,
367 // non-replaced elements in normal flow.
368
369 // Otherwise, if 'width' has a computed value of 'auto', and the element has an intrinsic width, then that intrinsic width is the used value of 'width'.
370 if (computed_width.is_auto() && box.has_intrinsic_width())
371 return box.intrinsic_width().value();
372
373 // Otherwise, if 'width' has a computed value of 'auto', but none of the conditions above are met, then the used value of 'width' becomes 300px.
374 // If 300px is too wide to fit the device, UAs should use the width of the largest rectangle that has a 2:1 ratio and fits the device instead.
375 if (computed_width.is_auto())
376 return 300;
377
378 return used_width;
379}
380
381void FormattingContext::compute_width_for_absolutely_positioned_element(Box const& box, AvailableSpace const& available_space)
382{
383 if (is<ReplacedBox>(box))
384 compute_width_for_absolutely_positioned_replaced_element(verify_cast<ReplacedBox>(box), available_space);
385 else
386 compute_width_for_absolutely_positioned_non_replaced_element(box, available_space);
387}
388
389void FormattingContext::compute_height_for_absolutely_positioned_element(Box const& box, AvailableSpace const& available_space, BeforeOrAfterInsideLayout before_or_after_inside_layout)
390{
391 if (is<ReplacedBox>(box))
392 compute_height_for_absolutely_positioned_replaced_element(static_cast<ReplacedBox const&>(box), available_space, before_or_after_inside_layout);
393 else
394 compute_height_for_absolutely_positioned_non_replaced_element(box, available_space, before_or_after_inside_layout);
395}
396
397CSSPixels FormattingContext::compute_width_for_replaced_element(LayoutState const& state, ReplacedBox const& box, AvailableSpace const& available_space)
398{
399 // 10.3.4 Block-level, replaced elements in normal flow...
400 // 10.3.2 Inline, replaced elements
401
402 auto zero_value = CSS::Length::make_px(0);
403 auto width_of_containing_block_as_length = CSS::Length::make_px(available_space.width.to_px());
404
405 auto margin_left = box.computed_values().margin().left().resolved(box, width_of_containing_block_as_length).resolved(box);
406 auto margin_right = box.computed_values().margin().right().resolved(box, width_of_containing_block_as_length).resolved(box);
407
408 // A computed value of 'auto' for 'margin-left' or 'margin-right' becomes a used value of '0'.
409 if (margin_left.is_auto())
410 margin_left = zero_value;
411 if (margin_right.is_auto())
412 margin_right = zero_value;
413
414 auto computed_width = should_treat_width_as_auto(box, available_space) ? CSS::Size::make_auto() : box.computed_values().width();
415
416 // 1. The tentative used width is calculated (without 'min-width' and 'max-width')
417 auto used_width = tentative_width_for_replaced_element(state, box, computed_width, available_space);
418
419 // 2. The tentative used width is greater than 'max-width', the rules above are applied again,
420 // but this time using the computed value of 'max-width' as the computed value for 'width'.
421 auto computed_max_width = box.computed_values().max_width();
422 if (!computed_max_width.is_none()) {
423 if (used_width > computed_max_width.resolved(box, width_of_containing_block_as_length).to_px(box)) {
424 used_width = tentative_width_for_replaced_element(state, box, computed_max_width, available_space);
425 }
426 }
427
428 // 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
429 // but this time using the value of 'min-width' as the computed value for 'width'.
430 auto computed_min_width = box.computed_values().min_width();
431 if (!computed_min_width.is_auto()) {
432 if (used_width < computed_min_width.resolved(box, width_of_containing_block_as_length).to_px(box)) {
433 used_width = tentative_width_for_replaced_element(state, box, computed_min_width, available_space);
434 }
435 }
436
437 return used_width;
438}
439
440// 10.6.2 Inline replaced elements, block-level replaced elements in normal flow, 'inline-block' replaced elements in normal flow and floating replaced elements
441// https://www.w3.org/TR/CSS22/visudet.html#inline-replaced-height
442CSSPixels FormattingContext::tentative_height_for_replaced_element(LayoutState const& state, ReplacedBox const& box, CSS::Size const& computed_height, AvailableSpace const& available_space)
443{
444 // If 'height' and 'width' both have computed values of 'auto' and the element also has
445 // an intrinsic height, then that intrinsic height is the used value of 'height'.
446 if (should_treat_width_as_auto(box, available_space) && should_treat_height_as_auto(box, available_space) && box.has_intrinsic_height())
447 return box.intrinsic_height().value();
448
449 // Otherwise, if 'height' has a computed value of 'auto', and the element has an intrinsic ratio then the used value of 'height' is:
450 //
451 // (used width) / (intrinsic ratio)
452 if (computed_height.is_auto() && box.has_intrinsic_aspect_ratio())
453 return compute_width_for_replaced_element(state, box, available_space) / box.intrinsic_aspect_ratio().value();
454
455 // Otherwise, if 'height' has a computed value of 'auto', and the element has an intrinsic height, then that intrinsic height is the used value of 'height'.
456 if (computed_height.is_auto() && box.has_intrinsic_height())
457 return box.intrinsic_height().value();
458
459 // Otherwise, if 'height' has a computed value of 'auto', but none of the conditions above are met,
460 // then the used value of 'height' must be set to the height of the largest rectangle that has a 2:1 ratio, has a height not greater than 150px,
461 // and has a width not greater than the device width.
462 if (computed_height.is_auto())
463 return 150;
464
465 return computed_height.resolved(box, CSS::Length::make_px(available_space.height.to_px())).to_px(box);
466}
467
468CSSPixels FormattingContext::compute_height_for_replaced_element(LayoutState const& state, ReplacedBox const& box, AvailableSpace const& available_space)
469{
470 // 10.6.2 Inline replaced elements, block-level replaced elements in normal flow,
471 // 'inline-block' replaced elements in normal flow and floating replaced elements
472
473 auto width_of_containing_block_as_length = CSS::Length::make_px(available_space.width.to_px());
474 auto height_of_containing_block_as_length = CSS::Length::make_px(available_space.height.to_px());
475 auto computed_width = should_treat_width_as_auto(box, available_space) ? CSS::Size::make_auto() : box.computed_values().width();
476 auto computed_height = should_treat_height_as_auto(box, available_space) ? CSS::Size::make_auto() : box.computed_values().height();
477
478 CSSPixels used_height = tentative_height_for_replaced_element(state, box, computed_height, available_space);
479
480 if (computed_width.is_auto() && computed_height.is_auto() && box.has_intrinsic_aspect_ratio()) {
481 CSSPixels w = tentative_width_for_replaced_element(state, box, computed_width, available_space);
482 CSSPixels h = used_height;
483 used_height = solve_replaced_size_constraint(state, w, h, box).height();
484 }
485
486 return used_height;
487}
488
489void FormattingContext::compute_width_for_absolutely_positioned_non_replaced_element(Box const& box, AvailableSpace const& available_space)
490{
491 auto width_of_containing_block = available_space.width.to_px();
492 auto width_of_containing_block_as_length = CSS::Length::make_px(width_of_containing_block);
493 auto& computed_values = box.computed_values();
494 auto zero_value = CSS::Length::make_px(0);
495
496 auto margin_left = CSS::Length::make_auto();
497 auto margin_right = CSS::Length::make_auto();
498 auto const border_left = computed_values.border_left().width;
499 auto const border_right = computed_values.border_right().width;
500 auto const padding_left = computed_values.padding().left().resolved(box, width_of_containing_block_as_length).to_px(box);
501 auto const padding_right = computed_values.padding().right().resolved(box, width_of_containing_block_as_length).to_px(box);
502
503 auto try_compute_width = [&](auto const& a_width) {
504 margin_left = computed_values.margin().left().resolved(box, width_of_containing_block_as_length).resolved(box);
505 margin_right = computed_values.margin().right().resolved(box, width_of_containing_block_as_length).resolved(box);
506
507 auto left = computed_values.inset().left().resolved(box, width_of_containing_block_as_length).resolved(box);
508 auto right = computed_values.inset().right().resolved(box, width_of_containing_block_as_length).resolved(box);
509 auto width = a_width;
510
511 auto solve_for_left = [&] {
512 return CSS::Length::make_px(width_of_containing_block - margin_left.to_px(box) - border_left - padding_left - width.to_px(box) - padding_right - border_right - margin_right.to_px(box) - right.to_px(box));
513 };
514
515 auto solve_for_width = [&] {
516 return CSS::Length::make_px(width_of_containing_block - left.to_px(box) - margin_left.to_px(box) - border_left - padding_left - padding_right - border_right - margin_right.to_px(box) - right.to_px(box));
517 };
518
519 auto solve_for_right = [&] {
520 return CSS::Length::make_px(width_of_containing_block - left.to_px(box) - margin_left.to_px(box) - border_left - padding_left - width.to_px(box) - padding_right - border_right - margin_right.to_px(box));
521 };
522
523 // If all three of 'left', 'width', and 'right' are 'auto':
524 if (left.is_auto() && width.is_auto() && right.is_auto()) {
525 // First set any 'auto' values for 'margin-left' and 'margin-right' to 0.
526 if (margin_left.is_auto())
527 margin_left = CSS::Length::make_px(0);
528 if (margin_right.is_auto())
529 margin_right = CSS::Length::make_px(0);
530 // Then, if the 'direction' property of the element establishing the static-position containing block
531 // is 'ltr' set 'left' to the static position and apply rule number three below;
532 // otherwise, set 'right' to the static position and apply rule number one below.
533 // FIXME: This is very hackish.
534 left = CSS::Length::make_px(0);
535 goto Rule3;
536 }
537
538 if (!left.is_auto() && !width.is_auto() && !right.is_auto()) {
539 // FIXME: This should be solved in a more complicated way.
540 return width;
541 }
542
543 if (margin_left.is_auto())
544 margin_left = CSS::Length::make_px(0);
545 if (margin_right.is_auto())
546 margin_right = CSS::Length::make_px(0);
547
548 // 1. 'left' and 'width' are 'auto' and 'right' is not 'auto',
549 // then the width is shrink-to-fit. Then solve for 'left'
550 if (left.is_auto() && width.is_auto() && !right.is_auto()) {
551 auto result = calculate_shrink_to_fit_widths(box);
552 auto available_width = solve_for_width();
553 width = CSS::Length::make_px(min(max(result.preferred_minimum_width, available_width.to_px(box)), result.preferred_width));
554 left = solve_for_left();
555 }
556
557 // 2. 'left' and 'right' are 'auto' and 'width' is not 'auto',
558 // then if the 'direction' property of the element establishing
559 // the static-position containing block is 'ltr' set 'left'
560 // to the static position, otherwise set 'right' to the static position.
561 // Then solve for 'left' (if 'direction is 'rtl') or 'right' (if 'direction' is 'ltr').
562 else if (left.is_auto() && right.is_auto() && !width.is_auto()) {
563 // FIXME: Check direction
564 // FIXME: Use the static-position containing block
565 left = zero_value;
566 right = solve_for_right();
567 }
568
569 // 3. 'width' and 'right' are 'auto' and 'left' is not 'auto',
570 // then the width is shrink-to-fit. Then solve for 'right'
571 else if (width.is_auto() && right.is_auto() && !left.is_auto()) {
572 Rule3:
573 auto result = calculate_shrink_to_fit_widths(box);
574 auto available_width = solve_for_width();
575 width = CSS::Length::make_px(min(max(result.preferred_minimum_width, available_width.to_px(box)), result.preferred_width));
576 right = solve_for_right();
577 }
578
579 // 4. 'left' is 'auto', 'width' and 'right' are not 'auto', then solve for 'left'
580 else if (left.is_auto() && !width.is_auto() && !right.is_auto()) {
581 left = solve_for_left();
582 }
583
584 // 5. 'width' is 'auto', 'left' and 'right' are not 'auto', then solve for 'width'
585 else if (width.is_auto() && !left.is_auto() && !right.is_auto()) {
586 width = solve_for_width();
587 }
588
589 // 6. 'right' is 'auto', 'left' and 'width' are not 'auto', then solve for 'right'
590 else if (right.is_auto() && !left.is_auto() && !width.is_auto()) {
591 right = solve_for_right();
592 }
593
594 return width;
595 };
596
597 // 1. The tentative used width is calculated (without 'min-width' and 'max-width')
598 auto used_width = try_compute_width(calculate_inner_width(box, available_space.width, computed_values.width()));
599
600 // 2. The tentative used width is greater than 'max-width', the rules above are applied again,
601 // but this time using the computed value of 'max-width' as the computed value for 'width'.
602 if (!computed_values.max_width().is_none()) {
603 auto max_width = calculate_inner_width(box, available_space.width, computed_values.max_width());
604 if (used_width.to_px(box) > max_width.to_px(box)) {
605 used_width = try_compute_width(max_width);
606 }
607 }
608
609 // 3. If the resulting width is smaller than 'min-width', the rules above are applied again,
610 // but this time using the value of 'min-width' as the computed value for 'width'.
611 if (!computed_values.min_width().is_auto()) {
612 auto min_width = calculate_inner_width(box, available_space.width, computed_values.min_width());
613 if (used_width.to_px(box) < min_width.to_px(box)) {
614 used_width = try_compute_width(min_width);
615 }
616 }
617
618 auto& box_state = m_state.get_mutable(box);
619 box_state.set_content_width(used_width.to_px(box));
620
621 box_state.margin_left = margin_left.to_px(box);
622 box_state.margin_right = margin_right.to_px(box);
623 box_state.border_left = border_left;
624 box_state.border_right = border_right;
625 box_state.padding_left = padding_left;
626 box_state.padding_right = padding_right;
627}
628
629void FormattingContext::compute_width_for_absolutely_positioned_replaced_element(ReplacedBox const& box, AvailableSpace const& available_space)
630{
631 // 10.3.8 Absolutely positioned, replaced elements
632 // The used value of 'width' is determined as for inline replaced elements.
633 // FIXME: This const_cast is gross.
634 const_cast<ReplacedBox&>(box).prepare_for_replaced_layout();
635 m_state.get_mutable(box).set_content_width(compute_width_for_replaced_element(m_state, box, available_space));
636}
637
638// https://drafts.csswg.org/css-position-3/#abs-non-replaced-height
639void FormattingContext::compute_height_for_absolutely_positioned_non_replaced_element(Box const& box, AvailableSpace const& available_space, BeforeOrAfterInsideLayout before_or_after_inside_layout)
640{
641 // 5.3. The Height Of Absolutely Positioned, Non-Replaced Elements
642
643 // For absolutely positioned elements, the used values of the vertical dimensions must satisfy this constraint:
644 // top + margin-top + border-top-width + padding-top + height + padding-bottom + border-bottom-width + margin-bottom + bottom = height of containing block
645
646 // NOTE: This function is called twice: both before and after inside layout.
647 // In the before pass, if it turns out we need the automatic height of the box, we abort these steps.
648 // This allows the box to retain an indefinite height from the perspective of inside layout.
649
650 auto margin_top = box.computed_values().margin().top();
651 auto margin_bottom = box.computed_values().margin().bottom();
652 auto top = box.computed_values().inset().top();
653 auto bottom = box.computed_values().inset().bottom();
654 auto height = box.computed_values().height();
655
656 auto width_of_containing_block = containing_block_width_for(box);
657 auto width_of_containing_block_as_length = CSS::Length::make_px(width_of_containing_block);
658 auto height_of_containing_block = available_space.height.to_px();
659 auto height_of_containing_block_as_length = CSS::Length::make_px(height_of_containing_block);
660
661 auto solve_for = [&](CSS::Length length) {
662 return CSS::Length::make_px(
663 height_of_containing_block
664 - top.resolved(box, height_of_containing_block_as_length).to_px(box)
665 - margin_top.resolved(box, width_of_containing_block_as_length).to_px(box)
666 - box.computed_values().border_top().width
667 - box.computed_values().padding().top().resolved(box, width_of_containing_block_as_length).to_px(box)
668 - height.resolved(box, height_of_containing_block_as_length).to_px(box)
669 - box.computed_values().padding().bottom().resolved(box, width_of_containing_block_as_length).to_px(box)
670 - box.computed_values().border_bottom().width
671 - margin_bottom.resolved(box, width_of_containing_block_as_length).to_px(box)
672 - bottom.resolved(box, height_of_containing_block_as_length).to_px(box)
673 + length.to_px(box));
674 };
675
676 auto solve_for_top = [&] {
677 top = solve_for(top.resolved(box, height_of_containing_block_as_length));
678 };
679
680 auto solve_for_bottom = [&] {
681 bottom = solve_for(bottom.resolved(box, height_of_containing_block_as_length));
682 };
683
684 auto solve_for_height = [&] {
685 height = CSS::Size::make_length(solve_for(height.resolved(box, height_of_containing_block_as_length)));
686 };
687
688 auto solve_for_margin_top = [&] {
689 margin_top = solve_for(margin_top.resolved(box, width_of_containing_block_as_length));
690 };
691
692 auto solve_for_margin_bottom = [&] {
693 margin_bottom = solve_for(margin_bottom.resolved(box, width_of_containing_block_as_length));
694 };
695
696 auto solve_for_margin_top_and_margin_bottom = [&] {
697 auto remainder = solve_for(CSS::Length::make_px(margin_top.resolved(box, width_of_containing_block_as_length).to_px(box) + margin_bottom.resolved(box, width_of_containing_block_as_length).to_px(box))).to_px(box);
698 margin_top = CSS::Length::make_px(remainder / 2);
699 margin_bottom = CSS::Length::make_px(remainder / 2);
700 };
701
702 // If all three of top, height, and bottom are auto:
703 if (top.is_auto() && height.is_auto() && bottom.is_auto()) {
704 // (If we haven't done inside layout yet, we can't compute the auto height.)
705 if (before_or_after_inside_layout == BeforeOrAfterInsideLayout::Before)
706 return;
707
708 // First set any auto values for margin-top and margin-bottom to 0,
709 if (margin_top.is_auto())
710 margin_top = CSS::Length::make_px(0);
711 if (margin_bottom.is_auto())
712 margin_bottom = CSS::Length::make_px(0);
713
714 // then set top to the static position,
715 auto static_position = calculate_static_position(box);
716 top = CSS::Length::make_px(static_position.y());
717
718 // and finally apply rule number three below.
719 height = CSS::Size::make_px(compute_auto_height_for_block_formatting_context_root(box));
720 solve_for_bottom();
721 }
722
723 // If none of the three are auto:
724 else if (!top.is_auto() && !height.is_auto() && !bottom.is_auto()) {
725 // If both margin-top and margin-bottom are auto,
726 if (margin_top.is_auto() && margin_bottom.is_auto()) {
727 // solve the equation under the extra constraint that the two margins get equal values.
728 solve_for_margin_top_and_margin_bottom();
729 }
730
731 // If one of margin-top or margin-bottom is auto,
732 else if (margin_top.is_auto() || margin_bottom.is_auto()) {
733 // solve the equation for that value.
734 if (margin_top.is_auto())
735 solve_for_margin_top();
736 else
737 solve_for_margin_bottom();
738 }
739
740 // If the values are over-constrained,
741 else {
742 // ignore the value for bottom and solve for that value.
743 solve_for_bottom();
744 }
745 }
746
747 // Otherwise,
748 else {
749 // set auto values for margin-top and margin-bottom to 0,
750 if (margin_top.is_auto())
751 margin_top = CSS::Length::make_px(0);
752 if (margin_bottom.is_auto())
753 margin_bottom = CSS::Length::make_px(0);
754
755 // and pick one of the following six rules that apply.
756
757 // 1. If top and height are auto and bottom is not auto,
758 if (top.is_auto() && height.is_auto() && !bottom.is_auto()) {
759 // (If we haven't done inside layout yet, we can't compute the auto height.)
760 if (before_or_after_inside_layout == BeforeOrAfterInsideLayout::Before)
761 return;
762
763 // then the height is based on the Auto heights for block formatting context roots,
764 height = CSS::Size::make_px(compute_auto_height_for_block_formatting_context_root(box));
765
766 // and solve for top.
767 solve_for_top();
768 }
769
770 // 2. If top and bottom are auto and height is not auto,
771 else if (top.is_auto() && bottom.is_auto() && !height.is_auto()) {
772 // then set top to the static position,
773 top = CSS::Length::make_px(calculate_static_position(box).y());
774
775 // then solve for bottom.
776 solve_for_bottom();
777 }
778
779 // 3. If height and bottom are auto and top is not auto,
780 else if (height.is_auto() && bottom.is_auto() && !top.is_auto()) {
781 // (If we haven't done inside layout yet, we can't compute the auto height.)
782 if (before_or_after_inside_layout == BeforeOrAfterInsideLayout::Before)
783 return;
784
785 // then the height is based on the Auto heights for block formatting context roots,
786 height = CSS::Size::make_px(compute_auto_height_for_block_formatting_context_root(box));
787
788 // and solve for bottom.
789 solve_for_bottom();
790 }
791
792 // 4. If top is auto, height and bottom are not auto,
793 else if (top.is_auto() && !height.is_auto() && !bottom.is_auto()) {
794 // then solve for top.
795 solve_for_top();
796 }
797
798 // 5. If height is auto, top and bottom are not auto,
799 else if (height.is_auto() && !top.is_auto() && !bottom.is_auto()) {
800 // then solve for height.
801 solve_for_height();
802 }
803
804 // 6. If bottom is auto, top and height are not auto,
805 else if (bottom.is_auto() && !top.is_auto() && !height.is_auto()) {
806 // then solve for bottom.
807 solve_for_bottom();
808 }
809 }
810
811 auto used_height = height.resolved(box, height_of_containing_block_as_length).to_px(box);
812 auto const& computed_min_height = box.computed_values().min_height();
813 auto const& computed_max_height = box.computed_values().max_height();
814
815 if (!computed_max_height.is_none())
816 used_height = min(used_height, computed_max_height.resolved(box, height_of_containing_block_as_length).resolved(box).to_px(box));
817 if (!computed_min_height.is_auto())
818 used_height = max(used_height, computed_min_height.resolved(box, height_of_containing_block_as_length).resolved(box).to_px(box));
819
820 // NOTE: The following is not directly part of any spec, but this is where we resolve
821 // the final used values for vertical margin/border/padding.
822
823 auto& box_state = m_state.get_mutable(box);
824 box_state.margin_top = margin_top.resolved(box, width_of_containing_block_as_length).to_px(box);
825 box_state.margin_bottom = margin_bottom.resolved(box, width_of_containing_block_as_length).to_px(box);
826 box_state.border_top = box.computed_values().border_top().width;
827 box_state.border_bottom = box.computed_values().border_bottom().width;
828 box_state.padding_top = box.computed_values().padding().top().resolved(box, width_of_containing_block_as_length).to_px(box);
829 box_state.padding_bottom = box.computed_values().padding().bottom().resolved(box, width_of_containing_block_as_length).to_px(box);
830
831 // And here is where we assign the box's content height.
832 box_state.set_content_height(used_height);
833}
834
835// NOTE: This is different from content_box_rect_in_ancestor_coordinate_space() as this does *not* follow the containing block chain up, but rather the parent() chain.
836static CSSPixelRect content_box_rect_in_static_position_ancestor_coordinate_space(Box const& box, Box const& ancestor_box, LayoutState const& state)
837{
838 auto rect = content_box_rect(box, state);
839 if (&box == &ancestor_box)
840 return rect;
841 for (auto const* current = box.parent(); current; current = current->parent()) {
842 if (current == &ancestor_box)
843 return rect;
844 auto const& current_state = state.get(static_cast<Box const&>(*current));
845 rect.translate_by(current_state.offset);
846 }
847 // If we get here, ancestor_box was not an ancestor of `box`!
848 VERIFY_NOT_REACHED();
849}
850
851// https://www.w3.org/TR/css-position-3/#staticpos-rect
852CSSPixelPoint FormattingContext::calculate_static_position(Box const& box) const
853{
854 // NOTE: This is very ad-hoc.
855 // The purpose of this function is to calculate the approximate position that `box`
856 // would have had if it were position:static.
857
858 CSSPixels x = 0.0f;
859 CSSPixels y = 0.0f;
860
861 VERIFY(box.parent());
862 if (box.parent()->children_are_inline()) {
863 // We're an abspos box with inline siblings. This is gonna get messy!
864 if (auto* sibling = box.previous_sibling()) {
865 // Hard case: there's a previous sibling. This means there's already inline content
866 // preceding the hypothetical static position of `box` within its containing block.
867 // If we had been position:static, that inline content would have been wrapped in
868 // anonymous block box, so now we get to imagine what the world might have looked like
869 // in that scenario..
870 // Basically, we find its last associated line box fragment and place `box` under it.
871 // FIXME: I'm 100% sure this can be smarter, better and faster.
872 LineBoxFragment const* last_fragment = nullptr;
873 auto& cb_state = m_state.get(*sibling->containing_block());
874 for (auto& line_box : cb_state.line_boxes) {
875 for (auto& fragment : line_box.fragments()) {
876 if (&fragment.layout_node() == sibling)
877 last_fragment = &fragment;
878 }
879 }
880 if (last_fragment) {
881 y = (last_fragment->offset().y() + last_fragment->height()).value();
882 }
883 } else {
884 // Easy case: no previous sibling, we're at the top of the containing block.
885 }
886 } else {
887 x = m_state.get(box).margin_box_left();
888 // We're among block siblings, Y can be calculated easily.
889 y = m_state.get(box).margin_box_top();
890 }
891 auto offset_to_static_parent = content_box_rect_in_static_position_ancestor_coordinate_space(box, *box.containing_block(), m_state);
892 return offset_to_static_parent.location().translated(x, y);
893}
894
895void FormattingContext::layout_absolutely_positioned_element(Box const& box, AvailableSpace const& available_space)
896{
897 auto& containing_block_state = m_state.get_mutable(*box.containing_block());
898 auto& box_state = m_state.get_mutable(box);
899
900 auto width_of_containing_block = available_space.width.to_px();
901 auto height_of_containing_block = available_space.height.to_px();
902 auto width_of_containing_block_as_length = CSS::Length::make_px(width_of_containing_block);
903 auto height_of_containing_block_as_length = CSS::Length::make_px(height_of_containing_block);
904
905 auto specified_width = box.computed_values().width().resolved(box, width_of_containing_block_as_length).resolved(box);
906
907 compute_width_for_absolutely_positioned_element(box, available_space);
908
909 // NOTE: We compute height before *and* after doing inside layout.
910 // This is done so that inside layout can resolve percentage heights.
911 // In some situations, e.g with non-auto top & bottom values, the height can be determined early.
912 compute_height_for_absolutely_positioned_element(box, available_space, BeforeOrAfterInsideLayout::Before);
913
914 auto independent_formatting_context = layout_inside(box, LayoutMode::Normal, box_state.available_inner_space_or_constraints_from(available_space));
915
916 compute_height_for_absolutely_positioned_element(box, available_space, BeforeOrAfterInsideLayout::After);
917
918 box_state.margin_left = box.computed_values().margin().left().resolved(box, width_of_containing_block_as_length).to_px(box);
919 box_state.margin_top = box.computed_values().margin().top().resolved(box, width_of_containing_block_as_length).to_px(box);
920 box_state.margin_right = box.computed_values().margin().right().resolved(box, width_of_containing_block_as_length).to_px(box);
921 box_state.margin_bottom = box.computed_values().margin().bottom().resolved(box, width_of_containing_block_as_length).to_px(box);
922
923 box_state.border_left = box.computed_values().border_left().width;
924 box_state.border_right = box.computed_values().border_right().width;
925 box_state.border_top = box.computed_values().border_top().width;
926 box_state.border_bottom = box.computed_values().border_bottom().width;
927
928 auto const& computed_left = box.computed_values().inset().left();
929 auto const& computed_right = box.computed_values().inset().right();
930 auto const& computed_top = box.computed_values().inset().top();
931 auto const& computed_bottom = box.computed_values().inset().bottom();
932
933 box_state.inset_left = computed_left.resolved(box, width_of_containing_block_as_length).to_px(box);
934 box_state.inset_top = computed_top.resolved(box, height_of_containing_block_as_length).to_px(box);
935 box_state.inset_right = computed_right.resolved(box, width_of_containing_block_as_length).to_px(box);
936 box_state.inset_bottom = computed_bottom.resolved(box, height_of_containing_block_as_length).to_px(box);
937
938 if (computed_left.is_auto() && box.computed_values().width().is_auto() && computed_right.is_auto()) {
939 if (box.computed_values().margin().left().is_auto())
940 box_state.margin_left = 0;
941 if (box.computed_values().margin().right().is_auto())
942 box_state.margin_right = 0;
943 }
944
945 auto static_position = calculate_static_position(box);
946
947 CSSPixelPoint used_offset;
948
949 if (!computed_left.is_auto()) {
950 CSSPixels x_offset = box_state.inset_left
951 + box_state.border_box_left();
952 used_offset.set_x(x_offset + box_state.margin_left);
953 } else if (!computed_right.is_auto()) {
954 CSSPixels x_offset = CSSPixels(0)
955 - box_state.inset_right
956 - box_state.border_box_right();
957 used_offset.set_x(width_of_containing_block + x_offset - box_state.content_width() - box_state.margin_right);
958 } else {
959 // NOTE: static position is content box position so border_box and margin should not be added
960 used_offset.set_x(static_position.x());
961 }
962
963 if (!computed_top.is_auto()) {
964 CSSPixels y_offset = box_state.inset_top
965 + box_state.border_box_top();
966 used_offset.set_y(y_offset + box_state.margin_top);
967 } else if (!computed_bottom.is_auto()) {
968 CSSPixels y_offset = CSSPixels(0)
969 - box_state.inset_bottom
970 - box_state.border_box_bottom();
971 used_offset.set_y(height_of_containing_block + y_offset - box_state.content_height() - box_state.margin_bottom);
972 } else {
973 // NOTE: static position is content box position so border_box and margin should not be added
974 used_offset.set_y(static_position.y());
975 }
976
977 // NOTE: Absolutely positioned boxes are relative to the *padding edge* of the containing block.
978 used_offset.translate_by(-containing_block_state.padding_left, -containing_block_state.padding_top);
979
980 box_state.set_content_offset(used_offset);
981
982 if (independent_formatting_context)
983 independent_formatting_context->parent_context_did_dimension_child_root_box();
984}
985
986void FormattingContext::compute_height_for_absolutely_positioned_replaced_element(ReplacedBox const& box, AvailableSpace const& available_space, BeforeOrAfterInsideLayout)
987{
988 // 10.6.5 Absolutely positioned, replaced elements
989 // The used value of 'height' is determined as for inline replaced elements.
990 m_state.get_mutable(box).set_content_height(compute_height_for_replaced_element(m_state, box, available_space));
991}
992
993// https://www.w3.org/TR/css-position-3/#relpos-insets
994void FormattingContext::compute_inset(Box const& box)
995{
996 if (box.computed_values().position() != CSS::Position::Relative)
997 return;
998
999 auto resolve_two_opposing_insets = [&](CSS::LengthPercentage const& computed_start, CSS::LengthPercentage const& computed_end, CSSPixels& used_start, CSSPixels& used_end, CSSPixels reference_for_percentage) {
1000 auto resolved_first = computed_start.resolved(box, CSS::Length::make_px(reference_for_percentage)).resolved(box);
1001 auto resolved_second = computed_end.resolved(box, CSS::Length::make_px(reference_for_percentage)).resolved(box);
1002
1003 if (resolved_first.is_auto() && resolved_second.is_auto()) {
1004 // If opposing inset properties in an axis both compute to auto (their initial values),
1005 // their used values are zero (i.e., the boxes stay in their original position in that axis).
1006 used_start = 0;
1007 used_end = 0;
1008 } else if (resolved_first.is_auto() || resolved_second.is_auto()) {
1009 // If only one is auto, its used value becomes the negation of the other, and the box is shifted by the specified amount.
1010 if (resolved_first.is_auto()) {
1011 used_end = resolved_second.to_px(box);
1012 used_start = -used_end;
1013 } else {
1014 used_start = resolved_first.to_px(box);
1015 used_end = -used_start;
1016 }
1017 } else {
1018 // If neither is auto, the position is over-constrained; (with respect to the writing mode of its containing block)
1019 // the computed end side value is ignored, and its used value becomes the negation of the start side.
1020 used_start = resolved_first.to_px(box);
1021 used_end = -used_start;
1022 }
1023 };
1024
1025 auto& box_state = m_state.get_mutable(box);
1026 auto const& computed_values = box.computed_values();
1027
1028 // FIXME: Respect the containing block's writing-mode.
1029 resolve_two_opposing_insets(computed_values.inset().left(), computed_values.inset().right(), box_state.inset_left, box_state.inset_right, containing_block_width_for(box));
1030 resolve_two_opposing_insets(computed_values.inset().top(), computed_values.inset().bottom(), box_state.inset_top, box_state.inset_bottom, containing_block_height_for(box));
1031}
1032
1033// https://drafts.csswg.org/css-sizing-3/#fit-content-size
1034CSSPixels FormattingContext::calculate_fit_content_width(Layout::Box const& box, AvailableSpace const& available_space) const
1035{
1036 // If the available space in a given axis is definite,
1037 // equal to clamp(min-content size, stretch-fit size, max-content size)
1038 // (i.e. max(min-content size, min(max-content size, stretch-fit size))).
1039 if (available_space.width.is_definite()) {
1040 return max(calculate_min_content_width(box),
1041 min(calculate_stretch_fit_width(box, available_space.width),
1042 calculate_max_content_width(box)));
1043 }
1044
1045 // When sizing under a min-content constraint, equal to the min-content size.
1046 if (available_space.width.is_min_content())
1047 return calculate_min_content_width(box);
1048
1049 // Otherwise, equal to the max-content size in that axis.
1050 return calculate_max_content_width(box);
1051}
1052
1053// https://drafts.csswg.org/css-sizing-3/#fit-content-size
1054CSSPixels FormattingContext::calculate_fit_content_height(Layout::Box const& box, AvailableSpace const& available_space) const
1055{
1056 // If the available space in a given axis is definite,
1057 // equal to clamp(min-content size, stretch-fit size, max-content size)
1058 // (i.e. max(min-content size, min(max-content size, stretch-fit size))).
1059 if (available_space.height.is_definite()) {
1060 return max(calculate_min_content_height(box, available_space.width),
1061 min(calculate_stretch_fit_height(box, available_space.height),
1062 calculate_max_content_height(box, available_space.width)));
1063 }
1064
1065 // When sizing under a min-content constraint, equal to the min-content size.
1066 if (available_space.height.is_min_content())
1067 return calculate_min_content_height(box, available_space.width);
1068
1069 // Otherwise, equal to the max-content size in that axis.
1070 return calculate_max_content_height(box, available_space.width);
1071}
1072
1073CSSPixels FormattingContext::calculate_min_content_width(Layout::Box const& box) const
1074{
1075 if (box.has_intrinsic_width())
1076 return *box.intrinsic_width();
1077
1078 auto& root_state = m_state.m_root;
1079
1080 auto& cache = *root_state.intrinsic_sizes.ensure(&box, [] { return adopt_own(*new LayoutState::IntrinsicSizes); });
1081 if (cache.min_content_width.has_value())
1082 return *cache.min_content_width;
1083
1084 LayoutState throwaway_state(&m_state);
1085
1086 auto& box_state = throwaway_state.get_mutable(box);
1087 box_state.width_constraint = SizeConstraint::MinContent;
1088
1089 auto context = const_cast<FormattingContext*>(this)->create_independent_formatting_context_if_needed(throwaway_state, box);
1090 VERIFY(context);
1091
1092 auto available_width = AvailableSize::make_min_content();
1093 auto available_height = AvailableSize::make_indefinite();
1094 context->run(box, LayoutMode::IntrinsicSizing, AvailableSpace(available_width, available_height));
1095
1096 if (context->type() == FormattingContext::Type::Flex) {
1097 cache.min_content_width = box_state.content_width();
1098 } else {
1099 cache.min_content_width = context->greatest_child_width(box).value();
1100 }
1101
1102 if (!isfinite(cache.min_content_width->value())) {
1103 // HACK: If layout calculates a non-finite result, something went wrong. Force it to zero and log a little whine.
1104 dbgln("FIXME: Calculated non-finite min-content width for {}", box.debug_description());
1105 cache.min_content_width = 0;
1106 }
1107
1108 return *cache.min_content_width;
1109}
1110
1111CSSPixels FormattingContext::calculate_max_content_width(Layout::Box const& box) const
1112{
1113 if (box.has_intrinsic_width())
1114 return *box.intrinsic_width();
1115
1116 auto& root_state = m_state.m_root;
1117
1118 auto& cache = *root_state.intrinsic_sizes.ensure(&box, [] { return adopt_own(*new LayoutState::IntrinsicSizes); });
1119 if (cache.max_content_width.has_value())
1120 return *cache.max_content_width;
1121
1122 LayoutState throwaway_state(&m_state);
1123
1124 auto& box_state = throwaway_state.get_mutable(box);
1125 box_state.width_constraint = SizeConstraint::MaxContent;
1126
1127 auto context = const_cast<FormattingContext*>(this)->create_independent_formatting_context_if_needed(throwaway_state, box);
1128 VERIFY(context);
1129
1130 auto available_width = AvailableSize::make_max_content();
1131 auto available_height = AvailableSize::make_indefinite();
1132 context->run(box, LayoutMode::IntrinsicSizing, AvailableSpace(available_width, available_height));
1133
1134 if (context->type() == FormattingContext::Type::Flex) {
1135 cache.max_content_width = box_state.content_width();
1136 } else {
1137 cache.max_content_width = context->greatest_child_width(box).value();
1138 }
1139
1140 if (!isfinite(cache.max_content_width->value())) {
1141 // HACK: If layout calculates a non-finite result, something went wrong. Force it to zero and log a little whine.
1142 dbgln("FIXME: Calculated non-finite max-content width for {}", box.debug_description());
1143 cache.max_content_width = 0;
1144 }
1145
1146 return *cache.max_content_width;
1147}
1148
1149// https://www.w3.org/TR/css-sizing-3/#min-content-block-size
1150CSSPixels FormattingContext::calculate_min_content_height(Layout::Box const& box, AvailableSize const& available_width) const
1151{
1152 // For block containers, tables, and inline boxes, this is equivalent to the max-content block size.
1153 if (box.is_block_container() || box.is_table())
1154 return calculate_max_content_height(box, available_width);
1155
1156 if (box.has_intrinsic_height())
1157 return *box.intrinsic_height();
1158
1159 bool is_cacheable = available_width.is_definite() || available_width.is_intrinsic_sizing_constraint();
1160 Optional<CSSPixels>* cache_slot = nullptr;
1161 if (is_cacheable) {
1162 auto& root_state = m_state.m_root;
1163 auto& cache = *root_state.intrinsic_sizes.ensure(&box, [] { return adopt_own(*new LayoutState::IntrinsicSizes); });
1164 if (available_width.is_definite()) {
1165 cache_slot = &cache.min_content_height_with_definite_available_width.ensure(available_width.to_px());
1166 } else if (available_width.is_min_content()) {
1167 cache_slot = &cache.min_content_height_with_min_content_available_width;
1168 } else if (available_width.is_max_content()) {
1169 cache_slot = &cache.min_content_height_with_max_content_available_width;
1170 }
1171 }
1172
1173 if (cache_slot && cache_slot->has_value())
1174 return cache_slot->value();
1175
1176 LayoutState throwaway_state(&m_state);
1177
1178 auto& box_state = throwaway_state.get_mutable(box);
1179 box_state.height_constraint = SizeConstraint::MinContent;
1180
1181 auto context = const_cast<FormattingContext*>(this)->create_independent_formatting_context_if_needed(throwaway_state, box);
1182 VERIFY(context);
1183
1184 context->run(box, LayoutMode::IntrinsicSizing, AvailableSpace(available_width, AvailableSize::make_min_content()));
1185
1186 auto min_content_height = context->automatic_content_height();
1187 if (!isfinite(min_content_height.value())) {
1188 // HACK: If layout calculates a non-finite result, something went wrong. Force it to zero and log a little whine.
1189 dbgln("FIXME: Calculated non-finite min-content height for {}", box.debug_description());
1190 min_content_height = 0;
1191 }
1192
1193 if (cache_slot) {
1194 *cache_slot = min_content_height;
1195 }
1196 return min_content_height;
1197}
1198
1199CSSPixels FormattingContext::calculate_max_content_height(Layout::Box const& box, AvailableSize const& available_width) const
1200{
1201 if (box.has_intrinsic_height())
1202 return *box.intrinsic_height();
1203
1204 bool is_cacheable = available_width.is_definite() || available_width.is_intrinsic_sizing_constraint();
1205 Optional<CSSPixels>* cache_slot = nullptr;
1206 if (is_cacheable) {
1207 auto& root_state = m_state.m_root;
1208 auto& cache = *root_state.intrinsic_sizes.ensure(&box, [] { return adopt_own(*new LayoutState::IntrinsicSizes); });
1209 if (available_width.is_definite()) {
1210 cache_slot = &cache.max_content_height_with_definite_available_width.ensure(available_width.to_px());
1211 } else if (available_width.is_min_content()) {
1212 cache_slot = &cache.max_content_height_with_min_content_available_width;
1213 } else if (available_width.is_max_content()) {
1214 cache_slot = &cache.max_content_height_with_max_content_available_width;
1215 }
1216 }
1217
1218 if (cache_slot && cache_slot->has_value())
1219 return cache_slot->value();
1220
1221 LayoutState throwaway_state(&m_state);
1222
1223 auto& box_state = throwaway_state.get_mutable(box);
1224 box_state.height_constraint = SizeConstraint::MaxContent;
1225
1226 auto context = const_cast<FormattingContext*>(this)->create_independent_formatting_context_if_needed(throwaway_state, box);
1227 VERIFY(context);
1228
1229 context->run(box, LayoutMode::IntrinsicSizing, AvailableSpace(available_width, AvailableSize::make_max_content()));
1230
1231 auto max_content_height = context->automatic_content_height();
1232
1233 if (!isfinite(max_content_height.value())) {
1234 // HACK: If layout calculates a non-finite result, something went wrong. Force it to zero and log a little whine.
1235 dbgln("FIXME: Calculated non-finite max-content height for {}", box.debug_description());
1236 max_content_height = 0;
1237 }
1238
1239 if (cache_slot) {
1240 *cache_slot = max_content_height;
1241 }
1242
1243 return max_content_height;
1244}
1245
1246CSS::Length FormattingContext::calculate_inner_width(Layout::Box const& box, AvailableSize const& available_width, CSS::Size const& width) const
1247{
1248 auto width_of_containing_block = available_width.to_px();
1249 auto width_of_containing_block_as_length_for_resolve = CSS::Length::make_px(width_of_containing_block);
1250 if (width.is_auto()) {
1251 return width.resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
1252 }
1253
1254 if (!available_width.is_definite())
1255 width_of_containing_block_as_length_for_resolve = CSS::Length::make_px(0);
1256
1257 auto& computed_values = box.computed_values();
1258 if (computed_values.box_sizing() == CSS::BoxSizing::BorderBox) {
1259 auto const padding_left = computed_values.padding().left().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
1260 auto const padding_right = computed_values.padding().right().resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
1261
1262 auto inner_width = width.resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box).to_px(box)
1263 - computed_values.border_left().width
1264 - padding_left.to_px(box)
1265 - computed_values.border_right().width
1266 - padding_right.to_px(box);
1267 return CSS::Length::make_px(max(inner_width, 0));
1268 }
1269
1270 return width.resolved(box, width_of_containing_block_as_length_for_resolve).resolved(box);
1271}
1272
1273CSS::Length FormattingContext::calculate_inner_height(Layout::Box const& box, AvailableSize const& available_height, CSS::Size const& height) const
1274{
1275 auto height_of_containing_block = available_height.to_px();
1276 auto height_of_containing_block_as_length_for_resolve = CSS::Length::make_px(height_of_containing_block);
1277 if (height.is_auto()) {
1278 return height.resolved(box, height_of_containing_block_as_length_for_resolve).resolved(box);
1279 }
1280
1281 if (!available_height.is_definite())
1282 height_of_containing_block_as_length_for_resolve = CSS::Length::make_px(0);
1283
1284 auto& computed_values = box.computed_values();
1285 if (computed_values.box_sizing() == CSS::BoxSizing::BorderBox) {
1286 auto width_of_containing_block = CSS::Length::make_px(containing_block_width_for(box));
1287
1288 auto const padding_top = computed_values.padding().top().resolved(box, width_of_containing_block).resolved(box);
1289 auto const padding_bottom = computed_values.padding().bottom().resolved(box, width_of_containing_block).resolved(box);
1290
1291 auto inner_height = height.resolved(box, height_of_containing_block_as_length_for_resolve).resolved(box).to_px(box)
1292 - computed_values.border_top().width
1293 - padding_top.to_px(box)
1294 - computed_values.border_bottom().width
1295 - padding_bottom.to_px(box);
1296 return CSS::Length::make_px(max(inner_height, 0));
1297 }
1298
1299 return height.resolved(box, height_of_containing_block_as_length_for_resolve).resolved(box);
1300}
1301
1302CSSPixels FormattingContext::containing_block_width_for(Box const& box, LayoutState const& state)
1303{
1304 auto& containing_block_state = state.get(*box.containing_block());
1305 auto& box_state = state.get(box);
1306
1307 switch (box_state.width_constraint) {
1308 case SizeConstraint::MinContent:
1309 return 0;
1310 case SizeConstraint::MaxContent:
1311 return INFINITY;
1312 case SizeConstraint::None:
1313 return containing_block_state.content_width();
1314 }
1315 VERIFY_NOT_REACHED();
1316}
1317
1318CSSPixels FormattingContext::containing_block_height_for(Box const& box, LayoutState const& state)
1319{
1320 auto& containing_block_state = state.get(*box.containing_block());
1321 auto& box_state = state.get(box);
1322
1323 switch (box_state.height_constraint) {
1324 case SizeConstraint::MinContent:
1325 return 0;
1326 case SizeConstraint::MaxContent:
1327 return INFINITY;
1328 case SizeConstraint::None:
1329 return containing_block_state.content_height();
1330 }
1331 VERIFY_NOT_REACHED();
1332}
1333
1334// https://drafts.csswg.org/css-sizing-3/#stretch-fit-size
1335CSSPixels FormattingContext::calculate_stretch_fit_width(Box const& box, AvailableSize const& available_width) const
1336{
1337 // The size a box would take if its outer size filled the available space in the given axis;
1338 // in other words, the stretch fit into the available space, if that is definite.
1339 // Undefined if the available space is indefinite.
1340 auto const& box_state = m_state.get(box);
1341 return available_width.to_px()
1342 - box_state.margin_left
1343 - box_state.margin_right
1344 - box_state.padding_left
1345 - box_state.padding_right
1346 - box_state.border_left
1347 - box_state.border_right;
1348}
1349
1350// https://drafts.csswg.org/css-sizing-3/#stretch-fit-size
1351CSSPixels FormattingContext::calculate_stretch_fit_height(Box const& box, AvailableSize const& available_height) const
1352{
1353 // The size a box would take if its outer size filled the available space in the given axis;
1354 // in other words, the stretch fit into the available space, if that is definite.
1355 // Undefined if the available space is indefinite.
1356 auto const& box_state = m_state.get(box);
1357 return available_height.to_px()
1358 - box_state.margin_top
1359 - box_state.margin_bottom
1360 - box_state.padding_top
1361 - box_state.padding_bottom
1362 - box_state.border_top
1363 - box_state.border_bottom;
1364}
1365
1366bool FormattingContext::should_treat_width_as_auto(Box const& box, AvailableSpace const& available_space)
1367{
1368 return box.computed_values().width().is_auto()
1369 || (box.computed_values().width().contains_percentage() && !available_space.width.is_definite());
1370}
1371
1372bool FormattingContext::should_treat_height_as_auto(Box const& box, AvailableSpace const& available_space)
1373{
1374 return box.computed_values().height().is_auto()
1375 || (box.computed_values().height().contains_percentage() && !available_space.height.is_definite());
1376}
1377
1378bool FormattingContext::can_skip_is_anonymous_text_run(Box& box)
1379{
1380 if (box.is_anonymous() && !box.is_generated() && !box.first_child_of_type<BlockContainer>()) {
1381 bool contains_only_white_space = true;
1382 box.for_each_in_subtree([&](auto const& node) {
1383 if (!is<TextNode>(node) || !static_cast<TextNode const&>(node).dom_node().data().is_whitespace()) {
1384 contains_only_white_space = false;
1385 return IterationDecision::Break;
1386 }
1387 return IterationDecision::Continue;
1388 });
1389 if (contains_only_white_space)
1390 return true;
1391 }
1392 return false;
1393}
1394
1395}