/* * Copyright (c) 2022-2023, Andreas Kling * Copyright (c) 2025, Sam Atkins * Copyright (c) 2026, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::Painting { Paintable::Paintable(Layout::Node const& layout_node) : m_layout_node(layout_node) { auto& computed_values = layout_node.computed_values(); if ((layout_node.is_flex_item() || layout_node.is_grid_item()) && computed_values.z_index().has_value()) { // https://drafts.csswg.org/css-flexbox-1/#painting // https://drafts.csswg.org/css-grid-2/#z-order // Flex and grid items with z-index values other than "auto" behave as if position were "relative". m_positioned = true; } else { m_positioned = computed_values.position() != CSS::Positioning::Static; } m_fixed_position = computed_values.position() == CSS::Positioning::Fixed; m_sticky_position = computed_values.position() == CSS::Positioning::Sticky; m_absolutely_positioned = computed_values.position() == CSS::Positioning::Absolute; m_floating = layout_node.is_floating(); m_inline = layout_node.is_inline(); m_display = layout_node.display(); } Paintable::~Paintable() = default; String Paintable::debug_description() const { return MUST(String::formatted("{}({})", class_name(), layout_node().debug_description())); } DOM::Document const& Paintable::document() const { return layout_node().document(); } DOM::Document& Paintable::document() { return layout_node().document(); } RefPtr Paintable::containing_block() const { if (m_containing_block.has_value()) { if (auto containing_block = m_containing_block->strong_ref()) return containing_block; } auto containing_block = [&] -> RefPtr { auto containing_layout_box = layout_node().containing_block(); if (!containing_layout_box) return nullptr; auto paintable_box = containing_layout_box->paintable_box(); if (!paintable_box) return nullptr; return const_cast(*paintable_box); }(); m_containing_block = containing_block; return containing_block; } CSS::ImmutableComputedValues const& Paintable::computed_values() const { return layout_node().computed_values(); } bool Paintable::visible_for_hit_testing() const { if (auto node = dom_node(); node && node->is_inert()) return false; return computed_values().pointer_events() != CSS::PointerEvents::None; } void Paintable::set_dom_node(GC::Ptr dom_node) { m_dom_node = dom_node.ptr(); } GC::Ptr Paintable::dom_node() { return m_dom_node.ptr(); } GC::Ptr Paintable::dom_node() const { return m_dom_node.ptr(); } GC::Ptr Paintable::navigable() const { return document().navigable(); } bool Paintable::handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, double, double) { return false; } bool Paintable::has_stacking_context() const { if (auto const* paintable_box = as_if(this)) return paintable_box->stacking_context(); return false; } DOM::Node* HitTestResult::dom_node() { for (auto* current = paintable.ptr(); current; current = current->parent()) { if (auto node = current->dom_node()) return node; } return nullptr; } DOM::Node const* HitTestResult::dom_node() const { for (auto const* current = paintable.ptr(); current; current = current->parent()) { if (auto node = current->dom_node()) return node; } return nullptr; } RefPtr Paintable::enclosing_stacking_context() { for (auto ancestor = parent(); ancestor; ancestor = ancestor->parent()) { auto* paintable_box = as_if(ancestor.ptr()); if (!paintable_box) continue; if (auto stacking_context = paintable_box->stacking_context()) return stacking_context; } // We should always reach the viewport's stacking context. VERIFY_NOT_REACHED(); } void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) const { paint_with_inspector_overlay_context(context, [&] { paint_inspector_overlay_internal(context); }); } void Paintable::paint_with_inspector_overlay_context(DisplayListRecordingContext& context, Function const& callback) const { auto& display_list_recorder = context.display_list_recorder(); auto previous_visual_context_index = display_list_recorder.accumulated_visual_context(); RefPtr paintable_box; if (is(*this)) paintable_box = static_cast(*this); else paintable_box = first_ancestor_of_type(); if (paintable_box) { auto viewport_paintable = document().paintable(); VERIFY(viewport_paintable); auto& visual_context_tree = const_cast(*viewport_paintable).visual_context_tree(); auto visual_context_index = paintable_box->accumulated_visual_context_index(); if (visual_context_index != VISUAL_VIEWPORT_NODE_INDEX) { Vector relevant_indices; for (auto i = visual_context_index; i != VISUAL_VIEWPORT_NODE_INDEX; i = visual_context_tree.node_at(i).parent_index) { auto should_keep = visual_context_tree.node_at(i).data.visit( [](ScrollData const&) { return true; }, [](ClipData const&) { return false; }, [](TransformData const&) { return true; }, [](PerspectiveData const&) { return true; }, [](ClipPathData const&) { return false; }, [](EffectsData const&) { return false; }, [](ScrollCompensation const&) { return true; }); if (should_keep) relevant_indices.append(i); } auto overlay_visual_context_index = VISUAL_VIEWPORT_NODE_INDEX; for (auto const& source_visual_context_index : relevant_indices.in_reverse()) overlay_visual_context_index = visual_context_tree.append(visual_context_tree.node_at(source_visual_context_index).data, overlay_visual_context_index); if (overlay_visual_context_index != VISUAL_VIEWPORT_NODE_INDEX) display_list_recorder.set_accumulated_visual_context(overlay_visual_context_index); } } callback(); display_list_recorder.set_accumulated_visual_context(previous_visual_context_index); } void Paintable::set_needs_repaint(InvalidateDisplayList should_invalidate_display_list) { if (should_invalidate_display_list == InvalidateDisplayList::Yes) { if (auto containing_block = this->containing_block()) containing_block->invalidate_paint_cache(); } document().set_needs_repaint(Badge {}, should_invalidate_display_list); } CSSPixelPoint Paintable::box_type_agnostic_position() const { if (auto const* paintable_box = as_if(this)) return paintable_box->absolute_position(); VERIFY(is_inline()); CSSPixelPoint position; if (auto block = containing_block(); block && is(*block)) { auto const& fragments = static_cast(*block).fragments(); if (!fragments.is_empty()) { position = fragments[0].absolute_rect().location(); } } return position; } Painting::BorderRadiiData normalize_border_radii_data(Layout::Node const& node, CSSPixelRect const& border_rect, CSSPixelRect const& reference_rect, CSS::BorderRadiusData const& top_left_radius, CSS::BorderRadiusData const& top_right_radius, CSS::BorderRadiusData const& bottom_right_radius, CSS::BorderRadiusData const& bottom_left_radius) { Painting::BorderRadiiData radii_px { .top_left = { top_left_radius.horizontal_radius.to_px(node, reference_rect.width()), top_left_radius.vertical_radius.to_px(node, reference_rect.height()) }, .top_right = { top_right_radius.horizontal_radius.to_px(node, reference_rect.width()), top_right_radius.vertical_radius.to_px(node, reference_rect.height()) }, .bottom_right = { bottom_right_radius.horizontal_radius.to_px(node, reference_rect.width()), bottom_right_radius.vertical_radius.to_px(node, reference_rect.height()) }, .bottom_left = { bottom_left_radius.horizontal_radius.to_px(node, reference_rect.width()), bottom_left_radius.vertical_radius.to_px(node, reference_rect.height()) } }; // Scale overlapping curves according to https://www.w3.org/TR/css-backgrounds-3/#corner-overlap // Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, // Si is the sum of the two corresponding radii of the corners on side i, // and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box. // // NOTE: We iterate twice as a form of iterative refinement. A single scaling pass using // fixed-point arithmetic can result in small rounding errors, causing the scaled radii to // still slightly overflow the box dimensions. A second pass corrects this remaining error. auto border_width = max(CSSPixels(0), border_rect.width()); auto border_height = max(CSSPixels(0), border_rect.height()); for (int iteration = 0; iteration < 2; ++iteration) { auto s_top = radii_px.top_left.horizontal_radius + radii_px.top_right.horizontal_radius; auto s_right = radii_px.top_right.vertical_radius + radii_px.bottom_right.vertical_radius; auto s_bottom = radii_px.bottom_right.horizontal_radius + radii_px.bottom_left.horizontal_radius; auto s_left = radii_px.bottom_left.vertical_radius + radii_px.top_left.vertical_radius; CSSPixelFraction f = 1; if (s_top > 0 && s_top > border_width) f = min(f, border_width / s_top); if (s_right > 0 && s_right > border_height) f = min(f, border_height / s_right); if (s_bottom > 0 && s_bottom > border_width) f = min(f, border_width / s_bottom); if (s_left > 0 && s_left > border_height) f = min(f, border_height / s_left); // If f is 1 or more, the radii fit perfectly and no more scaling is needed if (f >= 1) break; Painting::BorderRadiusData* corners[] = { &radii_px.top_left, &radii_px.top_right, &radii_px.bottom_right, &radii_px.bottom_left }; for (auto* corner : corners) { corner->horizontal_radius *= f; corner->vertical_radius *= f; } } return radii_px; } // https://drafts.csswg.org/css-pseudo-4/#highlight-styling // FIXME: Support additional ::selection properties: text-underline-offset, text-underline-position, stroke-color, // fill-color, stroke-width, and CSS custom properties. Paintable::SelectionStyle Paintable::selection_style() const { auto default_style_for_color_scheme = [&](CSS::PreferredColorScheme color_scheme, bool use_palette_for_normal_color_scheme = true) { auto palette = document().page().palette(); auto palette_color_scheme = palette.is_dark() ? CSS::PreferredColorScheme::Dark : CSS::PreferredColorScheme::Light; if (color_scheme == palette_color_scheme || use_palette_for_normal_color_scheme) return SelectionStyle { CSS::SystemColor::transform_selection_background_color(palette.selection()) }; return SelectionStyle { CSS::SystemColor::transform_selection_background_color(CSS::SystemColor::highlight(color_scheme)) }; }; // For text nodes, check the parent element since text nodes don't have computed properties. auto node = dom_node(); if (!node) return default_style_for_color_scheme(computed_values().color_scheme()); DOM::Element const* element = as_if(*node); if (!element) element = node->parent_element(); if (!element) return default_style_for_color_scheme(computed_values().color_scheme()); auto color_scheme_is_normal = element->computed_properties()->property(CSS::PropertyID::ColorScheme).as_color_scheme().schemes().is_empty(); auto use_palette_for_normal_color_scheme = color_scheme_is_normal && !document().supported_color_schemes().has_value(); auto default_style = default_style_for_color_scheme(computed_values().color_scheme(), use_palette_for_normal_color_scheme); auto style_from_element = [&](DOM::Element const& element) -> Optional { auto element_layout_node = element.layout_node(); if (!element_layout_node) return {}; auto computed_selection_style = element.computed_properties(CSS::PseudoElement::Selection); if (!computed_selection_style) return {}; auto context = CSS::ColorResolutionContext::for_layout_node_with_style(*element_layout_node); SelectionStyle style; style.background_color = computed_selection_style->color(CSS::PropertyID::BackgroundColor, context); // Only use text color if it was explicitly set in the ::selection rule, not inherited. if (!computed_selection_style->is_property_inherited(CSS::PropertyID::Color)) style.text_color = computed_selection_style->color(CSS::PropertyID::Color, context); // Only use text-shadow if it was explicitly set in the ::selection rule, not inherited. if (!computed_selection_style->is_property_inherited(CSS::PropertyID::TextShadow)) { auto const& css_shadows = computed_selection_style->text_shadow(*element_layout_node); Vector shadows; shadows.ensure_capacity(css_shadows.size()); for (auto const& shadow : css_shadows) shadows.unchecked_append(ShadowData::from_css(shadow, *element_layout_node)); style.text_shadow = move(shadows); } // Only use text-decoration if it was explicitly set in the ::selection rule, not inherited. if (!computed_selection_style->is_property_inherited(CSS::PropertyID::TextDecorationLine)) { style.text_decoration = TextDecorationStyle { .line = computed_selection_style->text_decoration_line(), .style = computed_selection_style->text_decoration_style(), .color = computed_selection_style->color(CSS::PropertyID::TextDecorationColor, context), }; } // Only return a style if there's a meaningful customization. This allows us to continue checking shadow hosts // when the current element only has UA default styles. if (!style.has_styling()) return {}; return style; }; // Check the element itself. if (auto style = style_from_element(*element); style.has_value()) return style.release_value(); // If inside a shadow tree, check the shadow host. This enables ::selection styling on elements like to // apply to text rendered inside their shadow DOM. if (auto shadow_root = element->containing_shadow_root(); shadow_root && shadow_root->is_user_agent_internal()) { if (auto const* host = shadow_root->host()) { if (auto style = style_from_element(*host); style.has_value()) return style.release_value(); } } return default_style; } void Paintable::set_selection_state(SelectionState state) { if (m_selection_state == state) return; m_selection_state = state; if (auto* box = as_if(this)) { box->invalidate_paint_cache(); } else if (auto containing_block = this->containing_block()) { containing_block->invalidate_paint_cache(); for (auto const* ancestor = layout_node().parent(); ancestor && ancestor != &containing_block->layout_node(); ancestor = ancestor->parent()) { for (auto& paintable : ancestor->paintables()) { if (auto* ancestor_box = as_if(paintable.ptr())) ancestor_box->invalidate_paint_cache(); } } } } void Paintable::scroll_ancestor_to_offset_into_view(size_t offset) { auto scroll_to_cursor = [&](PaintableFragment const& fragment, Paintable const& fragment_paintable) { auto cursor_rect = fragment.range_rect(SelectionState::StartAndEnd, offset, offset); for (auto ancestor = fragment_paintable.containing_block(); ancestor; ancestor = ancestor->containing_block()) { if (ancestor->has_scrollable_overflow()) { ancestor->scroll_into_view(cursor_rect); return; } } }; // Find the paintable fragment containing the cursor offset and scroll it into view. auto scan_layout_fragment = [&](Paintable const& slice_paintable) -> bool { auto paintable_with_lines = slice_paintable.first_ancestor_of_type(); if (!paintable_with_lines) return false; for (auto const& fragment : paintable_with_lines->fragments()) { if (&fragment.paintable() != &slice_paintable) continue; if (offset < fragment.dom_start_offset_in_node() || offset > fragment.dom_end_offset_in_node()) continue; scroll_to_cursor(fragment, slice_paintable); return true; } return false; }; if (auto const* text = as_if(dom_node().ptr())) { Layout::TextOffsetMapping mapping { *text }; bool scrolled = false; mapping.for_each_fragment([&](Layout::TextNode const& slice) { if (scrolled) return; if (auto slice_paintable = slice.first_paintable()) { if (scan_layout_fragment(*slice_paintable)) scrolled = true; } }); return; } scan_layout_fragment(*this); } }