/* * Copyright (c) 2022-2024, Andreas Kling * Copyright (c) 2024, Sam Atkins * Copyright (c) 2024, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::Layout { LayoutState::LayoutState(NodeWithStyle const& subtree_root) : m_subtree_root(&subtree_root) { } LayoutState::~LayoutState() { } void LayoutState::ensure_capacity(u32 node_count) { m_used_values_store.ensure_capacity(node_count); } LayoutState::UsedValues& LayoutState::get_mutable(NodeWithStyle const& node) { return ensure_used_values_for(node); } LayoutState::UsedValues const& LayoutState::get(NodeWithStyle const& node) const { return const_cast(this)->ensure_used_values_for(node); } LayoutState::UsedValues& LayoutState::populate_from_paintable(NodeWithStyle const& node, Painting::PaintableBox const& paintable) { VERIFY(m_subtree_root); auto index = node.layout_index(); // NOTE: We skip set_node() here since it performs size resolution that requires a containing block, // and materialize_from_paintable() overwrites all computed sizes immediately after. auto& used_values = m_used_values_store.allocate(index); used_values.m_node = &node; used_values.materialize_from_paintable(paintable); return used_values; } LayoutState::UsedValues& LayoutState::populate_node_from(LayoutState const& source, NodeWithStyle const& node) { VERIFY(m_subtree_root); auto index = node.layout_index(); VERIFY(!m_used_values_store.get(index)); auto& values = m_used_values_store.allocate(index); values = source.get(node); values.m_containing_block_used_values = nullptr; return values; } LayoutState::UsedValues& LayoutState::ensure_used_values_for(NodeWithStyle const& node) { auto index = node.layout_index(); if (auto* used_values = m_used_values_store.get(index)) return *used_values; // During subtree layout, only the subtree root and nodes inside the subtree are allowed. VERIFY(!m_subtree_root || m_subtree_root == &node || m_subtree_root->is_inclusive_ancestor_of(node)); UsedValues const* containing_block_used_values = nullptr; if (m_subtree_root == &node) { // For the subtree root, ancestor values are not available in the throwaway state. containing_block_used_values = try_get(*node.containing_block()); } else if (!node.is_viewport()) { containing_block_used_values = &get(*node.containing_block()); } auto& used_values = m_used_values_store.allocate(index); used_values.set_node(node, containing_block_used_values); return used_values; } LayoutState::UsedValues const* LayoutState::try_get(NodeWithStyle const& node) const { return m_used_values_store.get(node.layout_index()); } LayoutState::UsedValues* LayoutState::try_get_mutable(NodeWithStyle const& node) { return m_used_values_store.get(node.layout_index()); } LayoutState::UsedValues const* LayoutState::try_get(Node const& node) const { auto* node_with_style = as_if(node); if (!node_with_style) return nullptr; return try_get(*node_with_style); } // https://drafts.csswg.org/css-overflow-3/#scrollable-overflow-region using ContainedBoxesMap = HashMap>; struct PhysicalOverflowDirections { bool x_positive { true }; bool y_positive { true }; }; struct LogicalAxis { bool is_horizontal { false }; bool is_reverse { false }; }; static bool inline_axis_is_horizontal(CSS::WritingMode writing_mode) { return writing_mode == CSS::WritingMode::HorizontalTb; } static PhysicalOverflowDirections physical_overflow_directions(Box const& box) { auto const& computed_values = box.computed_values(); LogicalAxis inline_axis { .is_horizontal = inline_axis_is_horizontal(computed_values.writing_mode()), .is_reverse = computed_values.inline_axis_is_reverse(), }; LogicalAxis block_axis { .is_horizontal = !inline_axis.is_horizontal, .is_reverse = computed_values.block_axis_is_reverse(), }; auto horizontal_and_vertical_axes = [&]() { if (!box.display().is_flex_inside()) return AK::Tuple { inline_axis.is_horizontal ? inline_axis : block_axis, inline_axis.is_horizontal ? block_axis : inline_axis }; auto is_row_layout = computed_values.flex_direction() == CSS::FlexDirection::Row || computed_values.flex_direction() == CSS::FlexDirection::RowReverse; auto main_axis = is_row_layout ? inline_axis : block_axis; if (computed_values.flex_direction() == CSS::FlexDirection::RowReverse || computed_values.flex_direction() == CSS::FlexDirection::ColumnReverse) { main_axis.is_reverse = !main_axis.is_reverse; } auto cross_axis = is_row_layout ? block_axis : inline_axis; if (computed_values.flex_wrap() == CSS::FlexWrap::WrapReverse) cross_axis.is_reverse = !cross_axis.is_reverse; return AK::Tuple { main_axis.is_horizontal ? main_axis : cross_axis, main_axis.is_horizontal ? cross_axis : main_axis }; }; auto axes = horizontal_and_vertical_axes(); auto horizontal_axis = axes.get<0>(); auto vertical_axis = axes.get<1>(); return { .x_positive = !horizontal_axis.is_reverse, .y_positive = !vertical_axis.is_reverse, }; } static CSSPixelRect measure_scrollable_overflow(Box const& box, ContainedBoxesMap const& contained_boxes_map) { if (!box.paintable_box()) return {}; auto const& paintable_box = *box.paintable_box(); if (paintable_box.scrollable_overflow_rect().has_value()) return paintable_box.scrollable_overflow_rect().value(); // The scrollable overflow area is the union of: // - The scroll container’s own padding box. auto const paintable_absolute_padding_box = paintable_box.absolute_padding_box_rect(); auto scrollable_overflow_rect = paintable_absolute_padding_box; auto overflow_directions = physical_overflow_directions(box); // - All line boxes directly contained by the scroll container. if (auto const* paintable_with_lines = as_if(box.first_paintable())) { for (auto const& fragment : paintable_with_lines->fragments()) scrollable_overflow_rect.unite(fragment.absolute_rect()); } auto content_overflow_rect = scrollable_overflow_rect; // - The border boxes of all boxes for which it is the containing block and whose border boxes are positioned not // wholly in the negative scrollable overflow region, // FIXME: accounting for transforms by projecting each box onto the plane of the element that establishes its 3D rendering context. [CSS3-TRANSFORMS] if (auto it = contained_boxes_map.find(&box); it != contained_boxes_map.end()) { for (auto const* child_ptr : it->value) { auto const& child = *child_ptr; // https://drafts.csswg.org/css-position/#fixed-positioning-containing-block // [..] As a result, parts of fixed-positioned boxes that extend outside the layout viewport/page area // cannot be scrolled to and will not print. // FIXME: Properly establish the fixed positioning containing block for `position: fixed` if (child.is_fixed_position()) continue; auto child_border_box = child.paintable_box()->absolute_border_box_rect(); // NOTE: Only boxes that are not wholly in the unreachable scrollable overflow region contribute. auto wholly_in_unreachable_x = overflow_directions.x_positive ? child_border_box.right() < paintable_absolute_padding_box.x() : child_border_box.x() > paintable_absolute_padding_box.right(); auto wholly_in_unreachable_y = overflow_directions.y_positive ? child_border_box.bottom() < paintable_absolute_padding_box.y() : child_border_box.y() > paintable_absolute_padding_box.bottom(); if (wholly_in_unreachable_x || wholly_in_unreachable_y) continue; // Border boxes with zero area do not affect the scrollable overflow area. if (!child_border_box.is_empty()) { scrollable_overflow_rect.unite(child_border_box); content_overflow_rect.unite(child_border_box); } // - The scrollable overflow areas of all of the above boxes (including zero-area boxes and accounting for // transforms as described above), provided they themselves have overflow: visible (i.e. do not themselves // trap the overflow) and that scrollable overflow is not already clipped (e.g. by the clip property or the // contain property). // Scrollable overflow is already clipped by the contain property. if (child.has_layout_containment() || child.has_paint_containment()) continue; if (child.computed_values().overflow_x() == CSS::Overflow::Visible || child.computed_values().overflow_y() == CSS::Overflow::Visible) { auto child_scrollable_overflow = measure_scrollable_overflow(child, contained_boxes_map); if (!child_scrollable_overflow.is_empty()) { if (child.computed_values().overflow_x() == CSS::Overflow::Visible) scrollable_overflow_rect.unite_horizontally(child_scrollable_overflow); if (child.computed_values().overflow_y() == CSS::Overflow::Visible) scrollable_overflow_rect.unite_vertically(child_scrollable_overflow); } } } } // FIXME: - The margin areas of grid item and flex item boxes for which the box establishes a containing block. // - Additional padding added to the scrollable overflow rectangle as necessary to enable scroll positions that // satisfy the requirements of both place-content: start and place-content: end alignment. auto has_scrollable_overflow = !paintable_absolute_padding_box.contains(scrollable_overflow_rect) && box.is_scroll_container(); if (has_scrollable_overflow) { auto left = scrollable_overflow_rect.x(); auto top = scrollable_overflow_rect.y(); auto right = scrollable_overflow_rect.right(); auto bottom = scrollable_overflow_rect.bottom(); if (overflow_directions.x_positive) right = max(right, content_overflow_rect.right() + paintable_box.box_model().padding.right); else left = min(left, content_overflow_rect.x() - paintable_box.box_model().padding.left); if (overflow_directions.y_positive) bottom = max(bottom, content_overflow_rect.bottom() + paintable_box.box_model().padding.bottom); else top = min(top, content_overflow_rect.y() - paintable_box.box_model().padding.top); scrollable_overflow_rect = { left, top, max(right - left, CSSPixels { 0 }), max(bottom - top, CSSPixels { 0 }), }; } // Additionally, due to Web-compatibility constraints (caused by authors exploiting legacy bugs to surreptitiously // hide content from visual readers but not search engines and/or speech output), UAs must clip any content in the // unreachable scrollable overflow region. // // https://drafts.csswg.org/css-overflow-3/#unreachable-scrollable-overflow-region // Unless otherwise adjusted (e.g. by content alignment [css-align-3]), the area beyond the scroll origin in either // axis is considered the unreachable scrollable overflow region: content rendered here is not accessible to the // reader, see § 2.2 Scrollable Overflow. auto left = overflow_directions.x_positive ? max(scrollable_overflow_rect.x(), paintable_absolute_padding_box.x()) : scrollable_overflow_rect.x(); auto top = overflow_directions.y_positive ? max(scrollable_overflow_rect.y(), paintable_absolute_padding_box.y()) : scrollable_overflow_rect.y(); auto right = overflow_directions.x_positive ? scrollable_overflow_rect.right() : min(scrollable_overflow_rect.right(), paintable_absolute_padding_box.right()); auto bottom = overflow_directions.y_positive ? scrollable_overflow_rect.bottom() : min(scrollable_overflow_rect.bottom(), paintable_absolute_padding_box.bottom()); if (left != scrollable_overflow_rect.x() || top != scrollable_overflow_rect.y() || right != scrollable_overflow_rect.right() || bottom != scrollable_overflow_rect.bottom()) { scrollable_overflow_rect = { left, top, max(right - left, CSSPixels { 0 }), max(bottom - top, CSSPixels { 0 }), }; has_scrollable_overflow = !paintable_absolute_padding_box.contains(scrollable_overflow_rect) && box.is_scroll_container(); } const_cast(paintable_box).set_overflow_data({ .scrollable_overflow_rect = scrollable_overflow_rect, .has_scrollable_overflow = has_scrollable_overflow, }); return scrollable_overflow_rect; } void LayoutState::resolve_relative_positions() { // This function resolves relative position offsets of fragments that belong to inline paintables. // It runs *after* the paint tree has been constructed, so it modifies paintable node & fragment offsets directly. m_used_values_store.for_each([&](UsedValues& used_values) { auto& node = const_cast(used_values.node()); for (auto& paintable : node.paintables()) { if (!(is(paintable) && is(paintable.layout_node()))) continue; auto const& inline_paintable = static_cast(paintable); for (auto& fragment : inline_paintable.fragments()) { auto const& fragment_node = fragment.layout_node(); if (!is(*fragment_node.parent())) continue; // Collect effective relative position offset from inline-flow parent chain. CSSPixelPoint offset; for (auto* ancestor = fragment_node.parent(); ancestor; ancestor = ancestor->parent()) { if (!is(*ancestor)) break; if (!ancestor->display().is_inline_outside() || !ancestor->display().is_flow_inside()) break; if (ancestor->computed_values().position() == CSS::Positioning::Relative) { VERIFY(ancestor->first_paintable()); auto const& ancestor_node = as(*ancestor->first_paintable()); auto const& inset = ancestor_node.box_model().inset; offset.translate_by(inset.left, inset.top); } } const_cast(fragment).set_offset(fragment.offset().translated(offset)); } } }); } static void build_paint_tree(Node& node, Painting::Paintable* parent_paintable = nullptr) { for (auto& paintable : node.paintables()) { if (parent_paintable && !paintable.forms_unconnected_subtree()) { VERIFY(!paintable.parent()); parent_paintable->append_child(paintable); } paintable.set_dom_node(node.dom_node()); if (node.dom_node()) node.dom_node()->set_paintable(paintable); } for (auto* child = node.first_child(); child; child = child->next_sibling()) { build_paint_tree(*child, node.first_paintable()); } } void LayoutState::commit(Box& root) { Painting::Paintable* parent_paintable = nullptr; if (!root.is_viewport()) { if (auto* existing = as_if(root.first_paintable())) { parent_paintable = existing->parent(); if (parent_paintable) parent_paintable->remove_child(*existing); } } // Cache existing paintables before clearing. GC::RootHashMap> paintable_cache(root.document().heap()); root.for_each_in_inclusive_subtree([&](Node& node) { if (auto* paintable_box = as_if(node.first_paintable())) { // InlineNodes are excluded because they can span multiple lines, with a separate // InlinePaintable created for each line via create_paintable_for_line_with_index(). // This 1:N relationship between layout node and paintables, combined with the // dynamic nature of fragment relocation, makes simple 1:1 caching inapplicable. if (!is(node)) paintable_cache.set(&node, *paintable_box); } return TraversalDecision::Continue; }); // Go through the layout tree and detach all paintables. The layout tree should only point to the new paintable tree // which we're about to build. root.for_each_in_inclusive_subtree([](Node& node) { node.clear_paintables(); return TraversalDecision::Continue; }); // After this point, we should have a clean slate to build the new paint tree. HashTable inline_nodes; root.for_each_in_inclusive_subtree([&](Node& node) { if (auto* dom_node = node.dom_node()) dom_node->clear_paintable(); if (is(node) && node.dom_node()) { // Inline nodes might have a continuation chain; add all inline nodes that are part of it. for (GC::Ptr inline_node = static_cast(&node); inline_node; inline_node = inline_node->continuation_of_node()) { if (is(*inline_node)) inline_nodes.set(static_cast(inline_node.ptr())); } } return TraversalDecision::Continue; }); HashTable text_nodes; HashTable inline_node_paintables; auto transfer_box_model_metrics = [](Painting::BoxModelMetrics& box_model, UsedValues const& used_values) { box_model.inset = { used_values.inset_top, used_values.inset_right, used_values.inset_bottom, used_values.inset_left }; box_model.padding = { used_values.padding_top, used_values.padding_right, used_values.padding_bottom, used_values.padding_left }; box_model.border = { used_values.border_top, used_values.border_right, used_values.border_bottom, used_values.border_left }; box_model.margin = { used_values.margin_top, used_values.margin_right, used_values.margin_bottom, used_values.margin_left }; }; auto try_to_relocate_fragment_in_inline_node = [&](auto& fragment, size_t line_index) -> bool { for (auto const* parent = fragment.layout_node().parent(); parent; parent = parent->parent()) { if (parent->is_atomic_inline()) break; if (is(*parent)) { auto& inline_node = const_cast(static_cast(*parent)); auto line_paintable = inline_node.create_paintable_for_line_with_index(line_index); line_paintable->add_fragment(fragment); if (auto const* used_values = try_get(inline_node)) transfer_box_model_metrics(line_paintable->box_model(), *used_values); if (!inline_node_paintables.contains(line_paintable.ptr())) { inline_node_paintables.set(line_paintable.ptr()); inline_node.add_paintable(line_paintable); } return true; } } return false; }; m_used_values_store.for_each([&](UsedValues& used_values) { auto& node = used_values.node(); if (m_subtree_root && !m_subtree_root->is_inclusive_ancestor_of(node)) return; GC::Ptr paintable; // Try to reuse cached paintable for Box nodes if (auto cached = paintable_cache.get(&node); cached.has_value()) { auto cached_paintable = cached.value(); cached_paintable->reset_for_relayout(); paintable = cached_paintable; } // Fall back to creating new if no reusable paintable if (!paintable) paintable = node.create_paintable(); node.add_paintable(paintable); // For boxes, transfer all the state needed for painting. if (auto* paintable_box = as_if(paintable.ptr())) { transfer_box_model_metrics(paintable_box->box_model(), used_values); paintable_box->set_offset(used_values.offset); paintable_box->set_content_size(used_values.content_width(), used_values.content_height()); if (used_values.override_borders_data().has_value()) paintable_box->set_override_borders_data(used_values.override_borders_data().value()); if (used_values.table_cell_coordinates().has_value()) paintable_box->set_table_cell_coordinates(used_values.table_cell_coordinates().value()); if (auto* paintable_with_lines = as_if(*paintable_box)) { for (size_t line_index = 0; line_index < used_values.line_boxes.size(); ++line_index) { auto& line_box = used_values.line_boxes[line_index]; for (auto const& fragment : line_box.fragments()) { if (fragment.is_fully_truncated()) continue; if (auto const* text_node = as_if(fragment.layout_node())) text_nodes.set(const_cast(text_node)); auto did_relocate_fragment = try_to_relocate_fragment_in_inline_node(fragment, line_index); if (!did_relocate_fragment) paintable_with_lines->add_fragment(fragment); } } } if (auto* svg_graphics_paintable = as_if(paintable.ptr()); svg_graphics_paintable && used_values.computed_svg_transforms().has_value()) { svg_graphics_paintable->set_computed_transforms(*used_values.computed_svg_transforms()); } if (auto* svg_path_paintable = as_if(paintable.ptr()); svg_path_paintable && used_values.computed_svg_path().has_value()) { svg_path_paintable->set_computed_path(move(*used_values.computed_svg_path())); } if (node.display().is_grid_inside()) { paintable_box->set_used_values_for_grid_template_columns(used_values.grid_template_columns()); paintable_box->set_used_values_for_grid_template_rows(used_values.grid_template_rows()); } } }); // Create paintables for inline nodes without fragments to make possible querying their geometry. for (auto& inline_node : inline_nodes) { if (inline_node->first_paintable()) continue; auto line_paintable = inline_node->create_paintable_for_line_with_index(0); inline_node->add_paintable(line_paintable); inline_node_paintables.set(line_paintable.ptr()); if (auto const* used_values = try_get(*inline_node)) transfer_box_model_metrics(line_paintable->box_model(), *used_values); } // Resolve relative positions for regular boxes (not line box fragments): // NOTE: This needs to occur before fragments are transferred into the corresponding inline paintables, because // after this transfer, the containing_line_box_fragment will no longer be valid. m_used_values_store.for_each([&](UsedValues& used_values) { auto& node = const_cast(used_values.node()); if (!node.is_box()) return; auto& paintable = as(*node.first_paintable()); CSSPixelPoint offset; if (used_values.containing_line_box_fragment.has_value()) { // Atomic inline case: // We know that `node` is an atomic inline because `containing_line_box_fragments` refers to the // line box fragment in the parent block container that contains it. auto const& containing_line_box_fragment = used_values.containing_line_box_fragment.value(); auto const& containing_block = *node.containing_block(); auto const& containing_block_used_values = get(containing_block); auto const& fragment = containing_block_used_values.line_boxes[containing_line_box_fragment.line_box_index].fragments()[containing_line_box_fragment.fragment_index]; // The fragment has the final offset for the atomic inline, so we just need to copy it from there. offset = fragment.offset(); } else { // Not an atomic inline, much simpler case. offset = used_values.offset; } // Apply relative position inset if appropriate. if (node.computed_values().position() == CSS::Positioning::Relative && is(node)) { auto const& inset = paintable.box_model().inset; offset.translate_by(inset.left, inset.top); } paintable.set_offset(offset); }); for (auto* text_node : text_nodes) text_node->add_paintable(text_node->create_paintable()); build_paint_tree(root, parent_paintable); resolve_relative_positions(); // Measure size of paintables created for inline nodes. for (auto* paintable_with_lines : inline_node_paintables) { if (!is(paintable_with_lines->layout_node())) continue; Optional offset; CSSPixelSize size; auto line_index = paintable_with_lines->line_index(); paintable_with_lines->for_each_in_inclusive_subtree_of_type([&](auto& paintable) { if (paintable.line_index() != line_index) return TraversalDecision::Continue; if (is(paintable.layout_node())) return TraversalDecision::Continue; auto const* used_values = try_get(paintable.layout_node_with_style_and_box_metrics()); if (&paintable != paintable_with_lines && used_values) size.set_width(size.width() + used_values->margin_box_left() + used_values->margin_box_right()); auto const& fragments = paintable.fragments(); if (!fragments.is_empty()) { if (!offset.has_value() || (fragments.first().offset().x() < offset->x())) offset = fragments.first().offset(); if (&paintable == paintable_with_lines->first_child() && used_values) offset->translate_by(-used_values->margin_box_left(), 0); } for (auto const& fragment : fragments) size.set_width(size.width() + fragment.width()); return TraversalDecision::Continue; }); if (offset.has_value()) { if (!paintable_with_lines->fragments().is_empty()) offset.value().set_y(paintable_with_lines->fragments().first().offset().y()); // FIXME: If this paintable does not have any fragment we do no know the y offset. It should be where text should // start if there had been any for this node. Pick y offset of the leftmost fragment in the inclusive subtree in the meantime. paintable_with_lines->set_offset(offset.value()); } if (!paintable_with_lines->fragments().is_empty()) { for (auto const& fragment : paintable_with_lines->fragments()) size.set_height(max(size.height(), fragment.height())); } else { size.set_height(paintable_with_lines->layout_node().computed_values().line_height()); } paintable_with_lines->set_content_size(size); } // Build a map from each containing block to the boxes it contains. ContainedBoxesMap contained_boxes_map; m_used_values_store.for_each([&](UsedValues& used_values) { auto const* box = as_if(used_values.node()); if (!box || !box->paintable_box()) return; if (auto containing_block = box->containing_block()) contained_boxes_map.ensure(containing_block.ptr()).append(box); }); // Measure overflow in scroll containers. m_used_values_store.for_each([&](UsedValues& used_values) { auto const* box = as_if(used_values.node()); if (!box) return; measure_scrollable_overflow(*box, contained_boxes_map); // The scroll offset can become invalid if the scrollable overflow rectangle has changed after layout. // For example, if the scroll container has been scrolled to the very end and is then resized to become larger // (scrollable overflow rect become smaller), the scroll offset would be out of bounds. auto& paintable_box = const_cast(*box->paintable_box()); if (!paintable_box.scroll_offset().is_zero()) paintable_box.set_scroll_offset(paintable_box.scroll_offset()); }); m_used_values_store.for_each([&](UsedValues& used_values) { auto& node = used_values.node(); for (auto& paintable : node.paintables()) { auto* paintable_box = as_if(paintable); if (!paintable_box) continue; if (node.is_sticky_position()) { // https://drafts.csswg.org/css-position/#insets // For sticky positioned boxes, the inset is instead relative to the relevant scrollport’s size. // Negative values are allowed. auto sticky_insets = make(); auto const& inset = node.computed_values().inset(); auto const* nearest_scrollable_ancestor = paintable_box->nearest_scrollable_ancestor(); CSSPixelSize scrollport_size; if (nearest_scrollable_ancestor) scrollport_size = nearest_scrollable_ancestor->absolute_rect().size(); if (!inset.top().is_auto()) sticky_insets->top = inset.top().to_px_or_zero(node, scrollport_size.height()); if (!inset.right().is_auto()) sticky_insets->right = inset.right().to_px_or_zero(node, scrollport_size.width()); if (!inset.bottom().is_auto()) sticky_insets->bottom = inset.bottom().to_px_or_zero(node, scrollport_size.height()); if (!inset.left().is_auto()) sticky_insets->left = inset.left().to_px_or_zero(node, scrollport_size.width()); paintable_box->set_sticky_insets(move(sticky_insets)); } } }); } void LayoutState::UsedValues::set_node(NodeWithStyle const& node, UsedValues const* containing_block_used_values) { m_node = &node; m_containing_block_used_values = containing_block_used_values; // NOTE: In the code below, we decide if `node` has definite width and/or height. // This attempts to cover all the *general* cases where CSS considers sizes to be definite. // If `node` has definite values for min/max-width or min/max-height and a definite // preferred size in the same axis, we clamp the preferred size here as well. // // There are additional cases where CSS considers values to be definite. We model all of // those by having our engine consider sizes to be definite *once they are assigned to // the UsedValues by calling set_content_width() or set_content_height(). auto const& computed_values = node.computed_values(); auto adjust_for_box_sizing = [&](CSSPixels unadjusted_pixels, CSS::Size const& computed_size, bool width) -> CSSPixels { // box-sizing: content-box and/or automatic size don't require any adjustment. if (computed_values.box_sizing() == CSS::BoxSizing::ContentBox || computed_size.is_auto()) return unadjusted_pixels; // box-sizing: border-box requires us to subtract the relevant border and padding from the size. CSSPixels border_and_padding; if (width) { border_and_padding = computed_values.border_left().width + computed_values.padding().left().to_px_or_zero(*m_node, containing_block_used_values->content_width()) + computed_values.border_right().width + computed_values.padding().right().to_px_or_zero(*m_node, containing_block_used_values->content_width()); } else { border_and_padding = computed_values.border_top().width + computed_values.padding().top().to_px_or_zero(*m_node, containing_block_used_values->content_width()) + computed_values.border_bottom().width + computed_values.padding().bottom().to_px_or_zero(*m_node, containing_block_used_values->content_width()); } return unadjusted_pixels - border_and_padding; }; auto is_definite_size = [&](CSS::Size const& size, CSSPixels& resolved_definite_size, bool width) { // A size that can be determined without performing layout; that is, // a , // a measure of text (without consideration of line-wrapping), // a size of the initial containing block, // or a or other formula (such as the “stretch-fit” sizing of non-replaced blocks [CSS2]) that is resolved solely against definite sizes. auto containing_block_has_definite_size = containing_block_used_values ? (width ? containing_block_used_values->has_definite_width() : containing_block_used_values->has_definite_height()) : false; if (size.is_auto()) { // NOTE: The width of a non-flex-item block is considered definite if it's auto and the containing block has definite width. if (width && !node.is_floating() && !node.is_absolutely_positioned() && node.display().is_block_outside() && node.parent() && !node.parent()->is_floating() && (node.parent()->display().is_flow_root_inside() || node.parent()->display().is_flow_inside())) { if (containing_block_has_definite_size) { CSSPixels available_width = containing_block_used_values->content_width(); resolved_definite_size = clamp_to_max_dimension_value( available_width - margin_left - margin_right - padding_left - padding_right - border_left - border_right); return true; } return false; } return false; } if (size.is_calculated()) { CSS::CalculationResolutionContext context { .length_resolution_context = CSS::Length::ResolutionContext::for_layout_node(node), }; if (size.calculated().contains_percentage()) { if (!containing_block_has_definite_size) return false; auto containing_block_size_as_length = width ? containing_block_used_values->content_width() : containing_block_used_values->content_height(); context.percentage_basis = CSS::Length::make_px(containing_block_size_as_length); } resolved_definite_size = clamp_to_max_dimension_value(adjust_for_box_sizing(size.calculated().resolve_length(context)->to_px(node), size, width)); return true; } if (size.is_length()) { VERIFY(!size.is_auto()); // This should have been covered by the Size::is_auto() branch above. resolved_definite_size = clamp_to_max_dimension_value(adjust_for_box_sizing(size.length().to_px(node), size, width)); return true; } if (size.is_percentage()) { if (containing_block_has_definite_size) { auto containing_block_size = width ? containing_block_used_values->content_width() : containing_block_used_values->content_height(); resolved_definite_size = clamp_to_max_dimension_value(adjust_for_box_sizing(containing_block_size.scaled(size.percentage().as_fraction()), size, width)); return true; } return false; } return false; }; CSSPixels min_width = 0; bool has_definite_min_width = is_definite_size(computed_values.min_width(), min_width, true); CSSPixels max_width = 0; bool has_definite_max_width = is_definite_size(computed_values.max_width(), max_width, true); CSSPixels min_height = 0; bool has_definite_min_height = is_definite_size(computed_values.min_height(), min_height, false); CSSPixels max_height = 0; bool has_definite_max_height = is_definite_size(computed_values.max_height(), max_height, false); m_has_definite_width = is_definite_size(computed_values.width(), m_content_width, true); m_has_definite_height = is_definite_size(computed_values.height(), m_content_height, false); if (m_has_definite_width) { if (has_definite_min_width) m_content_width = clamp_to_max_dimension_value(max(min_width, m_content_width)); if (has_definite_max_width) m_content_width = clamp_to_max_dimension_value(min(max_width, m_content_width)); } if (m_has_definite_height) { if (has_definite_min_height) m_content_height = clamp_to_max_dimension_value(max(min_height, m_content_height)); if (has_definite_max_height) m_content_height = clamp_to_max_dimension_value(min(max_height, m_content_height)); } } void LayoutState::UsedValues::materialize_from_paintable(Painting::PaintableBox const& paintable) { auto const& box_model = paintable.box_model(); set_content_width(paintable.content_width()); set_content_height(paintable.content_height()); m_has_definite_width = true; m_has_definite_height = true; set_content_offset(paintable.offset()); m_cumulative_offset = paintable.absolute_rect().location(); margin_left = box_model.margin.left; margin_right = box_model.margin.right; margin_top = box_model.margin.top; margin_bottom = box_model.margin.bottom; padding_left = box_model.padding.left; padding_right = box_model.padding.right; padding_top = box_model.padding.top; padding_bottom = box_model.padding.bottom; border_left = box_model.border.left; border_right = box_model.border.right; border_top = box_model.border.top; border_bottom = box_model.border.bottom; inset_left = box_model.inset.left; inset_right = box_model.inset.right; inset_top = box_model.inset.top; inset_bottom = box_model.inset.bottom; if (auto const* svg_graphics_paintable = as_if(paintable)) m_computed_svg_transforms = svg_graphics_paintable->computed_transforms(); } void LayoutState::UsedValues::set_content_width(CSSPixels width) { if (width < 0) { // Negative widths are not allowed in CSS. We have a bug somewhere! Clamp to 0 to avoid doing too much damage. dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Layout calculated a negative width for {}: {}", m_node->debug_description(), width); width = 0; } m_content_width = clamp_to_max_dimension_value(width); // FIXME: We should not do this! Definiteness of widths should be determined early, // and not changed later (except for some special cases in flex layout..) m_has_definite_width = true; } void LayoutState::UsedValues::set_content_height(CSSPixels height) { if (height < 0) { // Negative heights are not allowed in CSS. We have a bug somewhere! Clamp to 0 to avoid doing too much damage. dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Layout calculated a negative height for {}: {}", m_node->debug_description(), height); height = 0; } m_content_height = clamp_to_max_dimension_value(height); } AvailableSize LayoutState::UsedValues::available_width_inside() const { if (width_constraint == SizeConstraint::MinContent) return AvailableSize::make_min_content(); if (width_constraint == SizeConstraint::MaxContent) return AvailableSize::make_max_content(); if (has_definite_width()) return AvailableSize::make_definite(m_content_width); return AvailableSize::make_indefinite(); } AvailableSize LayoutState::UsedValues::available_height_inside() const { if (height_constraint == SizeConstraint::MinContent) return AvailableSize::make_min_content(); if (height_constraint == SizeConstraint::MaxContent) return AvailableSize::make_max_content(); if (has_definite_height()) return AvailableSize::make_definite(m_content_height); return AvailableSize::make_indefinite(); } AvailableSpace LayoutState::UsedValues::available_inner_space_or_constraints_from(AvailableSpace const& outer_space) const { auto inner_width = available_width_inside(); auto inner_height = available_height_inside(); if (inner_width.is_indefinite() && outer_space.width.is_intrinsic_sizing_constraint()) inner_width = outer_space.width; if (inner_height.is_indefinite() && outer_space.height.is_intrinsic_sizing_constraint()) inner_height = outer_space.height; return AvailableSpace(inner_width, inner_height); } void LayoutState::UsedValues::set_indefinite_content_width() { m_has_definite_width = false; } void LayoutState::UsedValues::set_indefinite_content_height() { m_has_definite_height = false; } }