/* * 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 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; void Paintable::finalize() { Base::finalize(); if (m_list_node.is_in_list()) m_list_node.remove(); } void Paintable::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); TreeNode::visit_edges(visitor); visitor.visit(m_dom_node); visitor.visit(m_layout_node); visitor.visit(m_containing_block); } 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(); } PaintableBox* Paintable::containing_block() const { return m_containing_block.ensure([&] -> GC::Ptr { auto containing_layout_box = m_layout_node->containing_block(); if (!containing_layout_box) return nullptr; return const_cast(containing_layout_box->paintable_box()); }); } CSS::ImmutableComputedValues const& Paintable::computed_values() const { return m_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; } GC::Ptr Paintable::dom_node() { return m_dom_node; } GC::Ptr Paintable::dom_node() const { return m_dom_node; } GC::Ptr Paintable::navigable() const { return document().navigable(); } bool Paintable::handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, int, int) { return false; } TraversalDecision Paintable::hit_test(CSSPixelPoint, HitTestType, Function const&) const { return TraversalDecision::Continue; } bool Paintable::has_stacking_context() const { if (is_paintable_box()) return static_cast(*this).stacking_context(); return false; } StackingContext* Paintable::enclosing_stacking_context() { for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent()) { if (!ancestor->is_paintable_box()) continue; if (auto* stacking_context = static_cast(*ancestor).stacking_context()) return const_cast(stacking_context); } // We should always reach the viewport's stacking context. VERIFY_NOT_REACHED(); } void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) const { auto& display_list_recorder = context.display_list_recorder(); auto const* paintable_box = as_if(this); if (!paintable_box) paintable_box = first_ancestor_of_type(); if (paintable_box) { auto& visual_context_tree = const_cast(document().paintable())->visual_context_tree(); auto visual_context_index = paintable_box->accumulated_visual_context_index(); if (visual_context_index.value()) { Vector relevant_indices; for (auto i = visual_context_index; i.value(); 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; }); if (should_keep) relevant_indices.append(i); } VisualContextIndex overlay_visual_context_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.value()) display_list_recorder.set_accumulated_visual_context(overlay_visual_context_index); } } paint_inspector_overlay_internal(context); display_list_recorder.set_accumulated_visual_context({}); } 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 (is_paintable_box()) return static_cast(this)->absolute_position(); VERIFY(is_inline()); CSSPixelPoint position; if (auto const* 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. 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 > border_rect.width()) f = min(f, border_rect.width() / s_top); if (s_right > border_rect.height()) f = min(f, border_rect.height() / s_right); if (s_bottom > border_rect.width()) f = min(f, border_rect.width() / s_bottom); if (s_left > border_rect.height()) f = min(f, border_rect.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 color_scheme = computed_values().color_scheme(); SelectionStyle default_style { 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; DOM::Element const* element = as_if(*node); if (!element) element = node->parent_element(); if (!element) return default_style; 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)) ancestor_box->invalidate_paint_cache(); } } } } void Paintable::scroll_ancestor_to_offset_into_view(size_t offset) { // Walk up to find the containing PaintableWithLines. GC::Ptr paintable_with_lines; for (auto* ancestor = this; ancestor; ancestor = ancestor->parent()) { paintable_with_lines = as_if(*ancestor); if (paintable_with_lines) break; } if (!paintable_with_lines) return; // Find the fragment containing the offset and compute a cursor rect. for (auto const& fragment : paintable_with_lines->fragments()) { if (&fragment.paintable() != this) continue; if (offset < fragment.start_offset() || offset > fragment.start_offset() + fragment.length_in_code_units()) continue; auto cursor_rect = fragment.range_rect(SelectionState::StartAndEnd, offset, offset); // Walk up the containing block chain to find the nearest scrollable ancestor. for (auto* ancestor = containing_block(); ancestor; ancestor = ancestor->containing_block()) { if (ancestor->has_scrollable_overflow()) { ancestor->scroll_into_view(cursor_rect); break; } } return; } } }