/* * Copyright (c) 2023, Andreas Kling * Copyright (c) 2024-2026, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #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::reset_for_relayout() { PaintableWithLines::reset_for_relayout(); m_scroll_state.clear(); m_scroll_state_snapshot = {}; m_needs_to_refresh_scroll_state = true; m_paintable_boxes_with_auto_content_visibility.clear(); m_next_accumulated_visual_context_id = 1; } 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(heap().allocate(*this, nullptr, 0)); size_t index_in_tree_order = 1; for_each_in_subtree_of_type([&](auto& paintable_box) { paintable_box.invalidate_stacking_context(); auto* parent_context = 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); paintable_box.set_stacking_context(heap().allocate(paintable_box, parent_context, index_in_tree_order++)); return TraversalDecision::Continue; }); stacking_context()->sort(); } void ViewportPaintable::paint_all_phases(DisplayListRecordingContext& 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() { auto precompute_sticky_constraints = [](ScrollFrame& sticky_frame, PaintableBox const& paintable_box) { auto nearest_scrolling_ancestor_frame = sticky_frame.nearest_scrolling_ancestor(); if (!nearest_scrolling_ancestor_frame) return; auto const& scroll_ancestor_paintable = nearest_scrolling_ancestor_frame->paintable_box(); auto sticky_border_box_rect = paintable_box.absolute_border_box_rect(); auto const* containing_block_of_sticky = paintable_box.containing_block(); CSSPixelRect containing_block_region; bool needs_parent_offset_adjustment = false; if (containing_block_of_sticky == &scroll_ancestor_paintable) { containing_block_region = { {}, containing_block_of_sticky->scrollable_overflow_rect()->size() }; } else { containing_block_region = containing_block_of_sticky->absolute_border_box_rect() .translated(-scroll_ancestor_paintable.absolute_rect().top_left()); needs_parent_offset_adjustment = true; } sticky_frame.set_sticky_constraints({ .position_relative_to_scroll_ancestor = sticky_border_box_rect.top_left() - scroll_ancestor_paintable.absolute_rect().top_left(), .border_box_size = sticky_border_box_rect.size(), .scrollport_size = scroll_ancestor_paintable.absolute_rect().size(), .containing_block_region = containing_block_region, .needs_parent_offset_adjustment = needs_parent_offset_adjustment, .insets = paintable_box.sticky_insets(), }); }; for_each_in_inclusive_subtree_of_type([&](auto& paintable_box) { RefPtr sticky_scroll_frame; if (paintable_box.is_sticky_position()) { auto parent_scroll_frame = paintable_box.nearest_scroll_frame(); sticky_scroll_frame = m_scroll_state.create_sticky_frame_for(paintable_box, parent_scroll_frame); precompute_sticky_constraints(*sticky_scroll_frame, paintable_box); paintable_box.set_enclosing_scroll_frame(sticky_scroll_frame); 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& paintable) { if (paintable.is_fixed_position() || 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 (auto* paintable_box = as_if(paintable)) paintable_box->set_enclosing_scroll_frame(*scroll_frame); return TraversalDecision::Continue; } if (block->is_fixed_position()) { return TraversalDecision::Continue; } } VERIFY_NOT_REACHED(); }); } static CSSPixelRect effective_css_clip_rect(CSSPixelRect const& css_clip) { if (css_clip.width() < 0 || css_clip.height() < 0) return CSSPixelRect { 0, 0, 0, 0 }; return css_clip; } // Converts a CSS-pixel-space 4x4 matrix to device-pixel-space. // - Translation column (column 3, rows 0-2) is scaled up by DPR // - Perspective row (row 3, columns 0-2) is scaled down by DPR // - All other elements are unaffected (the scale factors cancel out) static FloatMatrix4x4 scale_matrix_for_device_pixels(FloatMatrix4x4 matrix, float scale) { matrix[0, 3] *= scale; matrix[1, 3] *= scale; matrix[2, 3] *= scale; matrix[3, 0] /= scale; matrix[3, 1] /= scale; matrix[3, 2] /= scale; return matrix; } static Optional compute_transform(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values, double pixel_ratio) { if (!paintable_box.has_css_transform()) return {}; auto matrix = Gfx::FloatMatrix4x4::identity(); if (auto const& translate = computed_values.translate()) matrix = matrix * translate->to_matrix(paintable_box).release_value(); if (auto const& rotate = computed_values.rotate()) matrix = matrix * rotate->to_matrix(paintable_box).release_value(); if (auto const& scale = computed_values.scale()) matrix = matrix * scale->to_matrix(paintable_box).release_value(); for (auto const& transform : computed_values.transformations()) matrix = matrix * transform->to_matrix(paintable_box).release_value(); auto const& css_transform_origin = computed_values.transform_origin(); auto reference_box = paintable_box.transform_reference_box(); CSSPixelPoint origin { reference_box.left() + css_transform_origin.x.to_px(paintable_box.layout_node(), reference_box.width()), reference_box.top() + css_transform_origin.y.to_px(paintable_box.layout_node(), reference_box.height()), }; auto scale = static_cast(pixel_ratio); auto device_origin = origin.to_type() * scale; return TransformData { scale_matrix_for_device_pixels(matrix, scale), device_origin }; } // https://drafts.csswg.org/css-transforms-2/#perspective-matrix static Optional compute_perspective_matrix(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values) { auto perspective = computed_values.perspective(); if (!perspective.has_value()) return {}; // The perspective matrix is computed as follows: // 1. Start with the identity matrix. // 2. Translate by the computed X and Y values of 'perspective-origin' // https://drafts.csswg.org/css-transforms-2/#perspective-origin-property // Percentages: refer to the size of the reference box auto reference_box = paintable_box.transform_reference_box(); auto perspective_origin = computed_values.perspective_origin().resolved(paintable_box.layout_node(), reference_box); auto computed_x = perspective_origin.x().to_float(); auto computed_y = perspective_origin.y().to_float(); auto perspective_matrix = Gfx::translation_matrix(Vector3(computed_x, computed_y, 0)); // 3. Multiply by the matrix that would be obtained from the 'perspective()' transform function, where the // length is provided by the value of the perspective property // NB: Length values less than 1px being clamped to 1px is handled by the perspective() function already. // FIXME: Create the matrix directly. perspective_matrix = perspective_matrix * CSS::TransformationStyleValue::create(CSS::PropertyID::Transform, CSS::TransformFunction::Perspective, CSS::StyleValueVector { CSS::LengthStyleValue::create(CSS::Length::make_px(perspective.value())) })->to_matrix({}).release_value(); // 4. Translate by the negated computed X and Y values of 'perspective-origin' perspective_matrix = perspective_matrix * Gfx::translation_matrix(Vector3(-computed_x, -computed_y, 0)); return perspective_matrix; } static Optional compute_clip_data(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values, DevicePixelConverter const& converter) { auto overflow_x = computed_values.overflow_x(); auto overflow_y = computed_values.overflow_y(); // 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 (paintable_box.layout_node().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. overflow_x = CSS::Overflow::Clip; overflow_y = CSS::Overflow::Clip; } auto has_hidden_overflow = overflow_x != CSS::Overflow::Visible || overflow_y != CSS::Overflow::Visible; if (has_hidden_overflow && paintable_box.overflow_property_applies()) { auto clip_rect = paintable_box.absolute_padding_box_rect(); // https://drafts.csswg.org/css-overflow-3/#propdef-overflow // 'clip' // This value indicates that the box’s content is clipped to its overflow clip edge auto overflow_clip_edge = paintable_box.overflow_clip_edge_rect(); if (overflow_x == CSS::Overflow::Visible) { clip_rect.set_left(0); clip_rect.set_right(CSSPixels::max_integer_value); } else if (overflow_x == CSS::Overflow::Clip) { clip_rect.set_left(overflow_clip_edge.left()); clip_rect.set_right(overflow_clip_edge.right()); } if (overflow_y == CSS::Overflow::Visible) { clip_rect.set_top(0); clip_rect.set_bottom(CSSPixels::max_integer_value); } else if (overflow_y == CSS::Overflow::Clip) { clip_rect.set_top(overflow_clip_edge.top()); clip_rect.set_bottom(overflow_clip_edge.bottom()); } // 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: // - 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. // - However, when one of 'overflow-x' or 'overflow-y' computes to 'clip' and the other computes to 'visible', the // clipping region is not rounded. // FIXME: Adjust the border radii for the overflow-clip-margin case. (see https://drafts.csswg.org/css-overflow-4/#valdef-overflow-clip-margin-length-0 ) auto radii = (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) ? paintable_box.normalized_border_radii_data(PaintableBox::ShrinkRadiiForBorders::Yes) : BorderRadiiData {}; return ClipData { converter.rounded_device_rect(clip_rect), radii.as_corners(converter) }; } return {}; } void ViewportPaintable::assign_accumulated_visual_contexts() { m_next_accumulated_visual_context_id = 1; auto pixel_ratio = document().page().client().device_pixels_per_css_pixel(); DevicePixelConverter converter { pixel_ratio }; auto scale = static_cast(pixel_ratio); auto append_node = [&](RefPtr parent, VisualContextData data) { return AccumulatedVisualContext::create(allocate_accumulated_visual_context_id(), move(data), parent); }; auto make_effects_data = [&](PaintableBox const& box) -> Optional { auto const& computed_values = box.computed_values(); auto gfx_filter = to_gfx_filter(box.filter(), pixel_ratio); EffectsData effects { computed_values.opacity(), mix_blend_mode_to_compositing_and_blending_operator(computed_values.mix_blend_mode()), move(gfx_filter) }; if (!effects.needs_layer()) return {}; return effects; }; // Create visual viewport transform as root (if not identity) m_visual_viewport_context = nullptr; auto transform = document().visual_viewport()->transform(); if (!transform.is_identity()) { auto matrix = scale_matrix_for_device_pixels(transform.to_matrix(), scale); m_visual_viewport_context = append_node(nullptr, TransformData { matrix, { 0.f, 0.f } }); } RefPtr viewport_state_for_descendants = m_visual_viewport_context; if (own_scroll_frame()) viewport_state_for_descendants = append_node(m_visual_viewport_context, ScrollData { own_scroll_frame()->id(), false }); set_accumulated_visual_context(nullptr); set_accumulated_visual_context_for_descendants(viewport_state_for_descendants); for_each_in_subtree_of_type([&](auto& paintable_box) { auto* visual_parent = as_if(paintable_box.parent()); if (!visual_parent) return TraversalDecision::Continue; RefPtr inherited_state; if (paintable_box.is_fixed_position()) { inherited_state = m_visual_viewport_context; } else if (paintable_box.is_absolutely_positioned()) { // For position: absolute, use containing block's state to correctly escape scroll containers. auto* containing = paintable_box.containing_block(); inherited_state = containing->accumulated_visual_context_for_descendants(); // Abspos elements escape scroll containers and overflow clips of non-positioned // ancestors, but cannot escape stacking contexts created by intermediate effects // (opacity, mix-blend-mode, isolation). Walk from visual parent to containing // block and collect these intermediate effects. // NOTE: transforms/perspectives/filters establish containing blocks for abspos, // so they cannot appear as intermediates. Vector intermediate_effects; for (Paintable* paintable = visual_parent; paintable && paintable != containing; paintable = paintable->parent()) { auto* ancestor_box = as_if(paintable); if (!ancestor_box) continue; if (auto effects = make_effects_data(*ancestor_box); effects.has_value()) intermediate_effects.append(effects.release_value()); } for (auto& effects : intermediate_effects.in_reverse()) inherited_state = append_node(inherited_state, move(effects)); } else { // For position: relative/static, use visual parent's state directly. // This avoids duplicate transform/perspective allocations that would occur with // the containing block + intermediate walk approach. inherited_state = visual_parent->accumulated_visual_context_for_descendants(); } // Build this element's own state from inherited state. RefPtr own_state = inherited_state; if (paintable_box.is_sticky_position()) { // For sticky elements, use enclosing_scroll_frame which holds the sticky frame. // own_scroll_frame may be a different scroll frame if the sticky element also has scrollable overflow. if (auto sticky_frame = paintable_box.enclosing_scroll_frame(); sticky_frame && sticky_frame->is_sticky()) own_state = append_node(own_state, ScrollData { sticky_frame->id(), true }); } auto const& computed_values = paintable_box.computed_values(); if (auto effects = make_effects_data(paintable_box); effects.has_value()) own_state = append_node(own_state, effects.release_value()); if (auto transform_data = compute_transform(paintable_box, computed_values, pixel_ratio); transform_data.has_value()) { paintable_box.set_has_non_invertible_css_transform(!transform_data->matrix.is_invertible()); own_state = append_node(own_state, *transform_data); } else { paintable_box.set_has_non_invertible_css_transform(false); } if (auto css_clip = paintable_box.get_clip_rect(); css_clip.has_value()) { auto effective_rect = effective_css_clip_rect(*css_clip); own_state = append_node(own_state, ClipData { converter.rounded_device_rect(effective_rect), {} }); } // FIXME: Support other geometry boxes. See: https://drafts.fxtf.org/css-masking/#typedef-geometry-box if (auto const& clip_path = computed_values.clip_path(); clip_path.has_value() && clip_path->is_basic_shape()) { auto masking_area = paintable_box.absolute_border_box_rect(); auto reference_box = CSSPixelRect { {}, masking_area.size() }; auto const& basic_shape = clip_path->basic_shape(); auto path = basic_shape.to_path(reference_box, paintable_box.layout_node()); path.offset(masking_area.top_left().template to_type()); auto fill_rule = basic_shape.basic_shape().visit( [](CSS::Polygon const& polygon) { return polygon.fill_rule; }, [](CSS::Path const& path) { return path.fill_rule; }, [](auto const&) { return Gfx::WindingRule::Nonzero; }); auto device_path = path.copy_transformed(Gfx::AffineTransform {}.set_scale(scale, scale)); auto device_bounding_rect = converter.rounded_device_rect(masking_area); own_state = append_node(own_state, ClipPathData { move(device_path), device_bounding_rect, fill_rule }); } paintable_box.set_accumulated_visual_context(own_state); // Build state for descendants: own state + perspective + clip + scroll. RefPtr state_for_descendants = own_state; if (auto perspective_matrix = compute_perspective_matrix(paintable_box, computed_values); perspective_matrix.has_value()) { auto scaled_matrix = scale_matrix_for_device_pixels(*perspective_matrix, scale); state_for_descendants = append_node(state_for_descendants, PerspectiveData { scaled_matrix }); } if (auto clip_data = compute_clip_data(paintable_box, computed_values, converter); clip_data.has_value()) state_for_descendants = append_node(state_for_descendants, clip_data.value()); if (paintable_box.own_scroll_frame()) { auto is_sticky_without_scrollable_overflow = paintable_box.is_sticky_position() && paintable_box.enclosing_scroll_frame() == paintable_box.own_scroll_frame(); if (!is_sticky_without_scrollable_overflow) state_for_descendants = append_node(state_for_descendants, ScrollData { paintable_box.own_scroll_frame()->id(), false }); } paintable_box.set_accumulated_visual_context_for_descendants(state_for_descendants); return TraversalDecision::Continue; }); } 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 nearest_scrolling_ancestor_frame = scroll_frame->nearest_scrolling_ancestor(); if (!nearest_scrolling_ancestor_frame || !scroll_frame->has_sticky_constraints()) return; auto const& sticky_data = scroll_frame->sticky_constraints(); auto const& sticky_insets = sticky_data.insets; auto const& scroll_ancestor_paintable = nearest_scrolling_ancestor_frame->paintable_box(); // For nested sticky elements, the parent sticky's offset is applied via cumulative_offset. // We need to adjust all position calculations to account for this, so we work in the // coordinate space where the parent sticky is at its current (offset) position. CSSPixelPoint parent_sticky_offset; if (auto parent = scroll_frame->parent(); parent && parent->is_sticky()) parent_sticky_offset = parent->cumulative_offset(); auto sticky_position_in_ancestor = sticky_data.position_relative_to_scroll_ancestor + parent_sticky_offset; auto containing_block_region = sticky_data.containing_block_region; if (sticky_data.needs_parent_offset_adjustment) containing_block_region.translate_by(parent_sticky_offset); CSSPixelPoint min_offset_within_containing_block = containing_block_region.top_left(); CSSPixelPoint max_offset_within_containing_block = { containing_block_region.right() - sticky_data.border_box_size.width(), containing_block_region.bottom() - sticky_data.border_box_size.height() }; CSSPixelRect scrollport_rect { scroll_ancestor_paintable.scroll_offset(), sticky_data.scrollport_size }; CSSPixelPoint sticky_offset; if (sticky_insets.top.has_value()) { if (scrollport_rect.top() > sticky_position_in_ancestor.y() - *sticky_insets.top) sticky_offset.set_y(min(scrollport_rect.top() + *sticky_insets.top, max_offset_within_containing_block.y()) - sticky_position_in_ancestor.y()); } if (sticky_insets.left.has_value()) { if (scrollport_rect.left() > sticky_position_in_ancestor.x() - *sticky_insets.left) sticky_offset.set_x(min(scrollport_rect.left() + *sticky_insets.left, max_offset_within_containing_block.x()) - sticky_position_in_ancestor.x()); } if (sticky_insets.bottom.has_value()) { if (scrollport_rect.bottom() < sticky_position_in_ancestor.y() + sticky_data.border_box_size.height() + *sticky_insets.bottom) sticky_offset.set_y(max(scrollport_rect.bottom() - sticky_data.border_box_size.height() - *sticky_insets.bottom, min_offset_within_containing_block.y()) - sticky_position_in_ancestor.y()); } if (sticky_insets.right.has_value()) { if (scrollport_rect.right() < sticky_position_in_ancestor.x() + sticky_data.border_box_size.width() + *sticky_insets.right) sticky_offset.set_x(max(scrollport_rect.right() - sticky_data.border_box_size.width() - *sticky_insets.right, min_offset_within_containing_block.x()) - sticky_position_in_ancestor.x()); } 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()); }); m_scroll_state_snapshot = m_scroll_state.snapshot(document().page().client().device_pixels_per_css_pixel()); } static void resolve_paint_only_properties_in_subtree(Paintable& root) { root.for_each_in_inclusive_subtree([&](auto& paintable) { paintable.resolve_paint_properties(); paintable.set_needs_paint_only_properties_update(false); return TraversalDecision::Continue; }); } 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) { if (paintable.needs_paint_only_properties_update()) { resolve_paint_only_properties_in_subtree(paintable); return TraversalDecision::SkipChildrenAndContinue; } return TraversalDecision::Continue; }); } GC::Ptr ViewportPaintable::selection() const { return 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 nodes between the start and end of the selection as Full. auto* start_at = start_container->child_at_index(range.start_offset()); // If the start container has no child at that index, we need to start on the node right after the start container. if (!start_at) { if (auto* last_child = start_container->last_child()) { start_at = last_child->next_in_pre_order(); } else { start_at = start_container->next_in_pre_order(); } } DOM::Node* stop_at = end_container->child_at_index(range.end_offset()); // Only stop at the end container if it has no children that may need to be included. for (auto* node = start_at; node && (node != stop_at && !(node == end_container && !end_container->has_children())); node = node->next_in_pre_order(end_container)) { if (node->is_inert()) continue; if (auto* paintable = node->paintable()) paintable->set_selection_state(SelectionState::Full); } // 5. Mark the selection end node as End if it is a text node. if (auto* paintable = end_container->paintable(); paintable && !range.end().node->is_inert() && is(*end_container)) { paintable->set_selection_state(SelectionState::End); } } bool ViewportPaintable::handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, int, int) { return false; } void ViewportPaintable::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_paintable_boxes_with_auto_content_visibility); } }