Serenity Operating System
at master 975 lines 48 kB view raw
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}