/* * Copyright (c) 2023, Andreas Kling * Copyright (c) 2024, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include namespace Web::Painting { GC_DEFINE_ALLOCATOR(ViewportPaintable); GC::Ref ViewportPaintable::create(Layout::Viewport const& layout_viewport) { return layout_viewport.heap().allocate(layout_viewport); } ViewportPaintable::ViewportPaintable(Layout::Viewport const& layout_viewport) : PaintableWithLines(layout_viewport) { } ViewportPaintable::~ViewportPaintable() = default; void ViewportPaintable::build_stacking_context_tree_if_needed() { if (stacking_context()) return; build_stacking_context_tree(); } void ViewportPaintable::build_stacking_context_tree() { set_stacking_context(make(*this, nullptr, 0)); size_t index_in_tree_order = 1; for_each_in_subtree_of_type([&](auto const& paintable_box) { const_cast(paintable_box).invalidate_stacking_context(); auto* parent_context = const_cast(paintable_box).enclosing_stacking_context(); auto establishes_stacking_context = paintable_box.layout_node().establishes_stacking_context(); if ((paintable_box.is_positioned() || establishes_stacking_context) && paintable_box.computed_values().z_index().value_or(0) == 0) parent_context->m_positioned_descendants_and_stacking_contexts_with_stack_level_0.append(paintable_box); if (!paintable_box.is_positioned() && paintable_box.is_floating()) parent_context->m_non_positioned_floating_descendants.append(paintable_box); if (!establishes_stacking_context) { VERIFY(!paintable_box.stacking_context()); return TraversalDecision::Continue; } VERIFY(parent_context); const_cast(paintable_box).set_stacking_context(make(const_cast(paintable_box), parent_context, index_in_tree_order++)); return TraversalDecision::Continue; }); stacking_context()->sort(); } void ViewportPaintable::paint_all_phases(PaintContext& context) { build_stacking_context_tree_if_needed(); context.display_list_recorder().save_layer(); stacking_context()->paint(context); context.display_list_recorder().restore(); } void ViewportPaintable::assign_scroll_frames() { for_each_in_inclusive_subtree_of_type([&](auto& paintable_box) { RefPtr sticky_scroll_frame; if (paintable_box.is_sticky_position()) { auto const* nearest_scrollable_ancestor = paintable_box.nearest_scrollable_ancestor(); RefPtr parent_scroll_frame; if (nearest_scrollable_ancestor) { parent_scroll_frame = nearest_scrollable_ancestor->nearest_scroll_frame(); } sticky_scroll_frame = m_scroll_state.create_sticky_frame_for(paintable_box, parent_scroll_frame); const_cast(paintable_box).set_enclosing_scroll_frame(sticky_scroll_frame); const_cast(paintable_box).set_own_scroll_frame(sticky_scroll_frame); } if (paintable_box.has_scrollable_overflow() || is(paintable_box)) { RefPtr parent_scroll_frame; if (sticky_scroll_frame) { parent_scroll_frame = sticky_scroll_frame; } else { parent_scroll_frame = paintable_box.nearest_scroll_frame(); } auto scroll_frame = m_scroll_state.create_scroll_frame_for(paintable_box, parent_scroll_frame); paintable_box.set_own_scroll_frame(scroll_frame); } return TraversalDecision::Continue; }); for_each_in_subtree([&](auto const& paintable) { if (paintable.is_fixed_position()) { return TraversalDecision::Continue; } if (paintable.is_sticky_position()) { return TraversalDecision::Continue; } for (auto block = paintable.containing_block(); block; block = block->containing_block()) { if (auto scroll_frame = block->own_scroll_frame(); scroll_frame) { if (paintable.is_paintable_box()) { auto const& paintable_box = static_cast(paintable); const_cast(paintable_box).set_enclosing_scroll_frame(*scroll_frame); } return TraversalDecision::Continue; } if (block->is_fixed_position()) { return TraversalDecision::Continue; } } VERIFY_NOT_REACHED(); }); } void ViewportPaintable::assign_clip_frames() { for_each_in_subtree_of_type([&](auto const& paintable_box) { auto overflow_x = paintable_box.computed_values().overflow_x(); auto overflow_y = paintable_box.computed_values().overflow_y(); // Note: Overflow may be clip on one axis and visible on the other. auto has_hidden_overflow = overflow_x != CSS::Overflow::Visible || overflow_y != CSS::Overflow::Visible; if (has_hidden_overflow || paintable_box.get_clip_rect().has_value() || paintable_box.layout_node().has_paint_containment()) { auto clip_frame = adopt_ref(*new ClipFrame()); clip_state.set(paintable_box, move(clip_frame)); } return TraversalDecision::Continue; }); for_each_in_subtree([&](auto const& paintable) { if (paintable.is_paintable_box()) { auto const& paintable_box = static_cast(paintable); if (auto clip_frame = clip_state.get(paintable_box); clip_frame.has_value()) { const_cast(paintable_box).set_own_clip_frame(clip_frame.value()); } } for (auto block = paintable.containing_block(); !block->is_viewport(); block = block->containing_block()) { if (auto clip_frame = clip_state.get(block); clip_frame.has_value()) { if (paintable.is_paintable_box()) { auto const& paintable_box = static_cast(paintable); const_cast(paintable_box).set_enclosing_clip_frame(clip_frame.value()); } break; } if (block->has_css_transform()) { break; } } return TraversalDecision::Continue; }); for (auto& it : clip_state) { auto const& paintable_box = *it.key; auto& clip_frame = *it.value; for (auto const* block = &paintable_box.layout_node_with_style_and_box_metrics(); !block->is_viewport(); block = block->containing_block()) { auto const& paintable = block->first_paintable(); if (!paintable->is_paintable_box()) { continue; } auto const& block_paintable_box = static_cast(*paintable); bool clip_x = paintable->computed_values().overflow_x() != CSS::Overflow::Visible; bool clip_y = paintable->computed_values().overflow_y() != CSS::Overflow::Visible; auto clip_rect = block_paintable_box.overflow_clip_edge_rect(); if (block_paintable_box.get_clip_rect().has_value()) { clip_rect.intersect(block_paintable_box.get_clip_rect().value()); clip_x = true; clip_y = true; } // https://drafts.csswg.org/css-contain-2/#paint-containment // 1. The contents of the element including any ink or scrollable overflow must be clipped to the overflow clip // edge of the paint containment box, taking corner clipping into account. This does not include the creation of // any mechanism to access or indicate the presence of the clipped content; nor does it inhibit the creation of // any such mechanism through other properties, such as overflow, resize, or text-overflow. // NOTE: This clipping shape respects overflow-clip-margin, allowing an element with paint containment // to still slightly overflow its normal bounds. if (block->has_paint_containment()) { // NOTE: Note: The behavior is described in this paragraph is equivalent to changing 'overflow-x: visible' into // 'overflow-x: clip' and 'overflow-y: visible' into 'overflow-y: clip' at used value time, while leaving other // values of 'overflow-x' and 'overflow-y' unchanged. clip_x = true; clip_y = true; } if (clip_x || clip_y) { // https://drafts.csswg.org/css-overflow-3/#corner-clipping // As mentioned in CSS Backgrounds 3 § 4.3 Corner Clipping, the clipping region established by overflow can be // rounded: if (clip_x && clip_y) { // - When overflow-x and overflow-y compute to hidden, scroll, or auto, the clipping region is rounded // based on the border radius, adjusted to the padding edge, as described in CSS Backgrounds 3 § 4.2 Corner // Shaping. // - When both overflow-x and overflow-y compute to clip, the clipping region is rounded as described in § 3.2 // Expanding Clipping Bounds: the overflow-clip-margin property. // FIXME: Implement overflow-clip-margin clip_frame.add_clip_rect(clip_rect, block_paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes), block_paintable_box.enclosing_scroll_frame()); } else { // - However, when one of overflow-x or overflow-y computes to clip and the other computes to visible, the // clipping region is not rounded. if (clip_x) { clip_rect.set_top(0); clip_rect.set_bottom(CSSPixels::max_integer_value); } else { clip_rect.set_left(0); clip_rect.set_right(CSSPixels::max_integer_value); } clip_frame.add_clip_rect(clip_rect, {}, block_paintable_box.enclosing_scroll_frame()); } } if (block->has_css_transform()) { break; } } } } void ViewportPaintable::refresh_scroll_state() { if (!m_needs_to_refresh_scroll_state) return; m_needs_to_refresh_scroll_state = false; m_scroll_state.for_each_sticky_frame([&](auto& scroll_frame) { auto const& sticky_box = scroll_frame->paintable_box(); auto const& sticky_insets = sticky_box.sticky_insets(); auto const* nearest_scrollable_ancestor = sticky_box.nearest_scrollable_ancestor(); if (!nearest_scrollable_ancestor) { return; } // Min and max offsets are needed to clamp the sticky box's position to stay within bounds of containing block. CSSPixels min_y_offset_relative_to_nearest_scrollable_ancestor; CSSPixels max_y_offset_relative_to_nearest_scrollable_ancestor; CSSPixels min_x_offset_relative_to_nearest_scrollable_ancestor; CSSPixels max_x_offset_relative_to_nearest_scrollable_ancestor; auto const* containing_block_of_sticky_box = sticky_box.containing_block(); if (containing_block_of_sticky_box->could_be_scrolled_by_wheel_event()) { min_y_offset_relative_to_nearest_scrollable_ancestor = 0; max_y_offset_relative_to_nearest_scrollable_ancestor = containing_block_of_sticky_box->scrollable_overflow_rect()->height() - sticky_box.absolute_border_box_rect().height(); min_x_offset_relative_to_nearest_scrollable_ancestor = 0; max_x_offset_relative_to_nearest_scrollable_ancestor = containing_block_of_sticky_box->scrollable_overflow_rect()->width() - sticky_box.absolute_border_box_rect().width(); } else { auto containing_block_rect_relative_to_nearest_scrollable_ancestor = containing_block_of_sticky_box->absolute_border_box_rect().translated(-nearest_scrollable_ancestor->absolute_rect().top_left()); min_y_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.top(); max_y_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.bottom() - sticky_box.absolute_border_box_rect().height(); min_x_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.left(); max_x_offset_relative_to_nearest_scrollable_ancestor = containing_block_rect_relative_to_nearest_scrollable_ancestor.right() - sticky_box.absolute_border_box_rect().width(); } auto border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor = sticky_box.border_box_rect_relative_to_nearest_scrollable_ancestor(); // By default, the sticky box is shifted by the scroll offset of the nearest scrollable ancestor. CSSPixelPoint sticky_offset = -nearest_scrollable_ancestor->scroll_offset(); CSSPixelRect const scrollport_rect { nearest_scrollable_ancestor->scroll_offset(), nearest_scrollable_ancestor->absolute_rect().size() }; if (sticky_insets.top.has_value()) { auto top_inset = sticky_insets.top.value(); auto stick_to_top_scroll_offset_threshold = border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.top() - top_inset; if (scrollport_rect.top() > stick_to_top_scroll_offset_threshold) { sticky_offset.translate_by({ 0, -border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.top() }); sticky_offset.translate_by({ 0, min(scrollport_rect.top() + top_inset, max_y_offset_relative_to_nearest_scrollable_ancestor) }); } } if (sticky_insets.left.has_value()) { auto left_inset = sticky_insets.left.value(); auto stick_to_left_scroll_offset_threshold = border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.left() - left_inset; if (scrollport_rect.left() > stick_to_left_scroll_offset_threshold) { sticky_offset.translate_by({ -border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.left(), 0 }); sticky_offset.translate_by({ min(scrollport_rect.left() + left_inset, max_x_offset_relative_to_nearest_scrollable_ancestor), 0 }); } } if (sticky_insets.bottom.has_value()) { auto bottom_inset = sticky_insets.bottom.value(); auto stick_to_bottom_scroll_offset_threshold = border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.bottom() + bottom_inset; if (scrollport_rect.bottom() < stick_to_bottom_scroll_offset_threshold) { sticky_offset.translate_by({ 0, -border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.top() }); sticky_offset.translate_by({ 0, max(scrollport_rect.bottom() - sticky_box.absolute_border_box_rect().height() - bottom_inset, min_y_offset_relative_to_nearest_scrollable_ancestor) }); } } if (sticky_insets.right.has_value()) { auto right_inset = sticky_insets.right.value(); auto stick_to_right_scroll_offset_threshold = border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.right() + right_inset; if (scrollport_rect.right() < stick_to_right_scroll_offset_threshold) { sticky_offset.translate_by({ -border_rect_of_sticky_box_relative_to_nearest_scrollable_ancestor.left(), 0 }); sticky_offset.translate_by({ max(scrollport_rect.right() - sticky_box.absolute_border_box_rect().width() - right_inset, min_x_offset_relative_to_nearest_scrollable_ancestor), 0 }); } } scroll_frame->set_own_offset(sticky_offset); }); m_scroll_state.for_each_scroll_frame([&](auto& scroll_frame) { scroll_frame->set_own_offset(-scroll_frame->paintable_box().scroll_offset()); }); } void ViewportPaintable::resolve_paint_only_properties() { // Resolves layout-dependent properties not handled during layout and stores them in the paint tree. // Properties resolved include: // - Border radii // - Box shadows // - Text shadows // - Transforms // - Transform origins // - Outlines for_each_in_inclusive_subtree([&](Paintable& paintable) { paintable.resolve_paint_properties(); return TraversalDecision::Continue; }); } GC::Ptr ViewportPaintable::selection() const { return const_cast(document()).get_selection(); } void ViewportPaintable::recompute_selection_states(DOM::Range& range) { // 1. Start by resetting the selection state of all layout nodes to None. for_each_in_inclusive_subtree([&](auto& layout_node) { layout_node.set_selection_state(SelectionState::None); return TraversalDecision::Continue; }); auto start_container = range.start_container(); auto end_container = range.end_container(); // 2. If the selection starts and ends in the same node: if (start_container == end_container) { // 1. If the selection starts and ends at the same offset, return. if (range.start_offset() == range.end_offset()) { // NOTE: A zero-length selection should not be visible. return; } // 2. If it's a text node, mark it as StartAndEnd and return. if (is(*start_container) && !range.start().node->is_inert()) { if (auto* paintable = start_container->paintable()) paintable->set_selection_state(SelectionState::StartAndEnd); return; } } // 3. Mark the selection start node as Start (if text) or Full (if anything else). if (auto* paintable = start_container->paintable(); paintable && !range.start().node->is_inert()) { if (is(*start_container)) paintable->set_selection_state(SelectionState::Start); else paintable->set_selection_state(SelectionState::Full); } // 4. Mark the selection end node as End (if text) or Full (if anything else). if (auto* paintable = end_container->paintable(); paintable && !range.end().node->is_inert()) { if (is(*end_container) || end_container->is_ancestor_of(start_container)) { paintable->set_selection_state(SelectionState::End); } else { paintable->for_each_in_inclusive_subtree([&](auto& layout_node) { if (!layout_node.dom_node() || !layout_node.dom_node()->is_inert()) layout_node.set_selection_state(SelectionState::Full); return TraversalDecision::Continue; }); } } // 5. Mark the nodes between start node and end node (in tree order) as Full. for (auto* node = start_container->next_in_pre_order(); node && (node->is_before(end_container) || node->is_descendant_of(end_container)); node = node->next_in_pre_order()) { if (node->is_inert()) continue; if (auto* paintable = node->paintable()) paintable->set_selection_state(SelectionState::Full); } } bool ViewportPaintable::handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, int, int) { return false; } void ViewportPaintable::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(clip_state); } }