/* * Copyright (c) 2022-2023, Andreas Kling * Copyright (c) 2022-2025, Sam Atkins * Copyright (c) 2024-2026, Aliaksandr Kalenik * Copyright (c) 2025, 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 #include #include #include #include #include #include namespace Web::Painting { GC_DEFINE_ALLOCATOR(PaintableBox); static bool g_paint_viewport_scrollbars = true; void set_paint_viewport_scrollbars(bool const enabled) { g_paint_viewport_scrollbars = enabled; } ResolvedCSSFilter resolve_css_filter(CSS::Filter const& computed_filter, PaintableBox const& paintable_box) { auto const& computed_values = paintable_box.computed_values(); auto const& layout_node = paintable_box.layout_node_with_style_and_box_metrics(); ResolvedCSSFilter result; for (auto const& filter_operation : computed_filter.filters()) { filter_operation.visit( [&](CSS::FilterOperation::Blur const& blur) { auto resolved_radius = blur.resolved_radius(); result.operations.empend(ResolvedCSSFilter::Blur { .radius = CSSPixels::nearest_value_for(resolved_radius), }); }, [&](CSS::FilterOperation::DropShadow const& drop_shadow) { auto to_css_px = [&](NonnullRefPtr const& length) { return CSS::Length::from_style_value(length, {}).absolute_length_to_px(); }; auto color_context = CSS::ColorResolutionContext::for_layout_node_with_style(layout_node); auto resolved_color = drop_shadow.color ? drop_shadow.color->to_color(color_context).value_or(computed_values.color()) : computed_values.color(); result.operations.empend(ResolvedCSSFilter::DropShadow { .offset_x = to_css_px(drop_shadow.offset_x), .offset_y = to_css_px(drop_shadow.offset_y), .radius = drop_shadow.radius ? to_css_px(*drop_shadow.radius) : CSSPixels(0), .color = resolved_color, }); }, [&](CSS::FilterOperation::Color const& color_operation) { result.operations.empend(ResolvedCSSFilter::Color { .operation = color_operation.operation, .amount = color_operation.resolved_amount(), }); }, [&](CSS::FilterOperation::HueRotate const& hue_rotate) { result.operations.empend(ResolvedCSSFilter::HueRotate { .angle_degrees = hue_rotate.angle_degrees(), }); }, [&](CSS::URL const& css_url) { auto& url_string = css_url.url(); if (url_string.is_empty() || !url_string.starts_with('#')) return; auto fragment_or_error = url_string.substring_from_byte_offset(1); if (fragment_or_error.is_error()) return; auto maybe_filter = paintable_box.document().get_element_by_id(fragment_or_error.value()); if (!maybe_filter) return; if (auto* filter_element = as_if(*maybe_filter)) { result.svg_filter = filter_element->gfx_filter(layout_node); auto bounds = paintable_box.absolute_border_box_rect(); if (bounds.is_empty()) { if (auto const* svg_ancestor = paintable_box.first_ancestor_of_type()) result.svg_filter_bounds = svg_ancestor->absolute_rect(); } if (!bounds.is_empty()) result.svg_filter_bounds = bounds; } }); } return result; } GC::Ref PaintableBox::create(Layout::Box const& layout_box) { return layout_box.heap().allocate(layout_box); } GC::Ref PaintableBox::create(Layout::InlineNode const& layout_box) { return layout_box.heap().allocate(layout_box); } PaintableBox::PaintableBox(Layout::Box const& layout_box) : Paintable(layout_box) { } PaintableBox::PaintableBox(Layout::InlineNode const& layout_box) : Paintable(layout_box) { } PaintableBox::~PaintableBox() { } void PaintableBox::reset_for_relayout() { if (parent()) remove(); while (first_child()) first_child()->remove(); m_containing_block = {}; m_offset = {}; m_content_size = {}; m_box_model = {}; m_overflow_data.clear(); m_override_borders_data.clear(); m_table_cell_coordinates.clear(); m_sticky_insets = nullptr; m_absolute_rect.clear(); m_absolute_padding_box_rect.clear(); m_absolute_border_box_rect.clear(); m_enclosing_scroll_frame_index = {}; m_own_scroll_frame_index = {}; m_accumulated_visual_context_index = {}; m_accumulated_visual_context_for_descendants_index = {}; m_used_values_for_grid_template_columns = nullptr; m_used_values_for_grid_template_rows = nullptr; m_cached_phase_commands = {}; invalidate_stacking_context(); } void PaintableBox::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_stacking_context); visitor.visit(m_horizontal_scrollbar); visitor.visit(m_vertical_scrollbar); visitor.visit(m_resize_handle); } CSSPixelPoint PaintableBox::scroll_offset() const { if (is_viewport_paintable()) { auto navigable = document().navigable(); VERIFY(navigable); return navigable->viewport_scroll_offset(); } auto const& node = layout_node(); if (auto pseudo_element = node.generated_for_pseudo_element(); pseudo_element.has_value()) return node.pseudo_element_generator()->scroll_offset(*pseudo_element); if (auto const* element = as_if(dom_node().ptr())) return element->scroll_offset({}); return {}; } PaintableBox::ScrollHandled PaintableBox::set_scroll_offset(CSSPixelPoint offset) { auto scrollable_overflow_rect = this->scrollable_overflow_rect(); if (!scrollable_overflow_rect.has_value()) return ScrollHandled::No; auto padding_rect = absolute_padding_box_rect(); auto max_x_offset = max(scrollable_overflow_rect->width() - padding_rect.width(), 0); auto max_y_offset = max(scrollable_overflow_rect->height() - padding_rect.height(), 0); offset.set_x(clamp(offset.x(), 0, max_x_offset)); offset.set_y(clamp(offset.y(), 0, max_y_offset)); // FIXME: If there is horizontal and vertical scroll ignore only part of the new offset if (offset.y() < 0 || scroll_offset() == offset) return ScrollHandled::No; if (is_viewport_paintable()) { auto navigable = document().navigable(); VERIFY(navigable); navigable->perform_scroll_of_viewport_scrolling_box(offset); return ScrollHandled::Yes; } document().set_needs_to_refresh_scroll_state(true); auto& node = layout_node(); if (auto pseudo_element = node.generated_for_pseudo_element(); pseudo_element.has_value()) { node.pseudo_element_generator()->set_scroll_offset(*pseudo_element, offset); } else if (auto* element = as_if(*dom_node())) { element->set_scroll_offset({}, offset); } else { return ScrollHandled::No; } // https://drafts.csswg.org/cssom-view-1/#scrolling-events // Whenever an element gets scrolled (whether in response to user interaction or by an API), // the user agent must run these steps: // 1. Let doc be the element’s node document. auto& document = layout_node().document(); // FIXME: 2. If the element is a snap container, run the steps to update snapchanging targets for the element with // the element’s eventual snap target in the block axis as newBlockTarget and the element’s eventual snap // target in the inline axis as newInlineTarget. GC::Ptr event_target; if (auto pseudo_element = node.generated_for_pseudo_element(); pseudo_element.has_value()) event_target = node.pseudo_element_generator(); else event_target = dom_node(); if (!event_target) return ScrollHandled::Yes; // 3. If (element, "scroll") is already in doc’s pending scroll events, abort these steps. if (document.pending_scroll_events().contains_slow(DOM::Document::PendingScrollEvent { *event_target, HTML::EventNames::scroll })) return ScrollHandled::Yes; // 4. Append (element, "scroll") to doc’s pending scroll events. document.pending_scroll_events().append({ *event_target, HTML::EventNames::scroll }); set_needs_repaint(InvalidateDisplayList::No); return ScrollHandled::Yes; } PaintableBox::ScrollHandled PaintableBox::scroll_by(int delta_x, int delta_y) { return set_scroll_offset(scroll_offset().translated(delta_x, delta_y)); } void PaintableBox::scroll_into_view(CSSPixelRect rect) { auto scrollport = absolute_padding_box_rect(); auto current_offset = scroll_offset(); // Both rect and scrollport are in layout coordinate space (not scroll-adjusted). auto content_rect = rect.translated(-scrollport.x(), -scrollport.y()); auto new_offset = current_offset; if (content_rect.right() > current_offset.x() + scrollport.width()) new_offset.set_x(content_rect.right() - scrollport.width()); else if (content_rect.left() < current_offset.x()) new_offset.set_x(content_rect.left()); if (content_rect.bottom() > current_offset.y() + scrollport.height()) new_offset.set_y(content_rect.bottom() - scrollport.height()); else if (content_rect.top() < current_offset.y()) new_offset.set_y(content_rect.top()); set_scroll_offset(new_offset); } void PaintableBox::set_offset(CSSPixelPoint offset) { m_offset = offset; } void PaintableBox::set_content_size(CSSPixelSize size) { m_content_size = size; if (auto layout_box = as_if(layout_node())) layout_box->did_set_content_size(); } CSSPixelPoint PaintableBox::offset() const { return m_offset; } CSSPixelRect PaintableBox::compute_absolute_rect() const { CSSPixelRect rect { offset(), content_size() }; for (auto const* block = containing_block(); block; block = block->containing_block()) rect.translate_by(block->offset()); return rect; } CSSPixelRect PaintableBox::absolute_rect() const { if (!m_absolute_rect.has_value()) m_absolute_rect = compute_absolute_rect(); return *m_absolute_rect; } CSSPixelRect PaintableBox::absolute_padding_box_rect() const { if (!m_absolute_padding_box_rect.has_value()) { auto absolute_rect = this->absolute_rect(); CSSPixelRect rect; rect.set_x(absolute_rect.x() - box_model().padding.left); rect.set_width(content_width() + box_model().padding.left + box_model().padding.right); rect.set_y(absolute_rect.y() - box_model().padding.top); rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom); m_absolute_padding_box_rect = rect; } return *m_absolute_padding_box_rect; } Optional PaintableBox::absolute_resizer_rect(ChromeMetrics const& metrics) const { if (!has_resizer()) return {}; auto padding_rect = absolute_padding_box_rect(); CSSPixels x = is_chrome_mirrored() ? padding_rect.x() : padding_rect.right() - metrics.resize_gripper_size; CSSPixels y = padding_rect.bottom() - metrics.resize_gripper_size; return CSSPixelRect { x, y, metrics.resize_gripper_size, metrics.resize_gripper_size }; } CSSPixelRect PaintableBox::absolute_border_box_rect() const { if (!m_absolute_border_box_rect.has_value()) { auto padded_rect = this->absolute_padding_box_rect(); CSSPixelRect rect; auto use_collapsing_borders_model = override_borders_data().has_value(); // Implement the collapsing border model https://www.w3.org/TR/CSS22/tables.html#collapsing-borders. auto border_top = use_collapsing_borders_model ? round(box_model().border.top / 2) : box_model().border.top; auto border_bottom = use_collapsing_borders_model ? round(box_model().border.bottom / 2) : box_model().border.bottom; auto border_left = use_collapsing_borders_model ? round(box_model().border.left / 2) : box_model().border.left; auto border_right = use_collapsing_borders_model ? round(box_model().border.right / 2) : box_model().border.right; rect.set_x(padded_rect.x() - border_left); rect.set_width(padded_rect.width() + border_left + border_right); rect.set_y(padded_rect.y() - border_top); rect.set_height(padded_rect.height() + border_top + border_bottom); m_absolute_border_box_rect = rect; } return *m_absolute_border_box_rect; } // https://drafts.csswg.org/css-overflow-4/#overflow-clip-edge CSSPixelRect PaintableBox::overflow_clip_edge_rect() const { // https://drafts.csswg.org/css-overflow-4/#overflow-clip-margin // Values are defined as follows: // '' // Specifies the box edge to use as the overflow clip edge origin, i.e. when the specified offset is zero. // If omitted, defaults to 'padding-box' on non-replaced elements, or 'content-box' on replaced elements. // FIXME: We can't parse this yet so it's always omitted for now. auto overflow_clip_edge = absolute_padding_box_rect(); if (layout_node().is_replaced_box()) { overflow_clip_edge = absolute_rect(); } // '' // The specified offset dictates how much the overflow clip edge is expanded from the specified box edge // Negative values are invalid. Defaults to zero if omitted. overflow_clip_edge.inflate( computed_values().overflow_clip_margin().top().length().absolute_length_to_px(), computed_values().overflow_clip_margin().right().length().absolute_length_to_px(), computed_values().overflow_clip_margin().bottom().length().absolute_length_to_px(), computed_values().overflow_clip_margin().left().length().absolute_length_to_px()); return overflow_clip_edge; } template static CSSPixelRect united_rect_for_continuation_chain(PaintableBox const& start, Callable get_rect) { // Combine the absolute rects of all paintable boxes of all nodes in the continuation chain. Without this, we // calculate the wrong rect for inline nodes that were split because of block elements. Optional result; // FIXME: instead of walking the continuation chain in the layout tree, also keep track of this chain in the // painting tree so we can skip visiting the layout nodes altogether. for (auto const* node = &start.layout_node_with_style_and_box_metrics(); node; node = node->continuation_of_node()) { for (auto const& paintable : node->paintables()) { if (!is(paintable)) continue; auto const& paintable_box = static_cast(paintable); auto paintable_border_box_rect = get_rect(paintable_box); if (!result.has_value()) result = paintable_border_box_rect; else if (!paintable_border_box_rect.is_empty()) result->unite(paintable_border_box_rect); } } return result.value_or({}); } CSSPixelRect PaintableBox::absolute_united_border_box_rect() const { return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) { return paintable_box.absolute_border_box_rect(); }); } CSSPixelRect PaintableBox::absolute_united_content_rect() const { return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) { return paintable_box.absolute_rect(); }); } CSSPixelRect PaintableBox::absolute_united_padding_box_rect() const { return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) { return paintable_box.absolute_padding_box_rect(); }); } Optional PaintableBox::get_clip_rect() const { auto clip = computed_values().clip(); if (clip.is_rect() && layout_node_with_style_and_box_metrics().is_absolutely_positioned()) { auto border_box = absolute_border_box_rect(); return clip.to_rect().resolved(border_box); } return {}; } GC::Ptr PaintableBox::scrollbar(ScrollDirection direction) const { return direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar; } GC::Ref PaintableBox::ensure_scrollbar(ScrollDirection direction) { auto& slot = direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar; if (!slot) slot = Scrollbar::create(heap(), const_cast(*this), direction); return *slot; } bool PaintableBox::could_be_scrolled_by_wheel_event(ScrollDirection direction) const { bool is_horizontal = direction == ScrollDirection::Horizontal; Gfx::Orientation orientation = is_horizontal ? Gfx::Orientation::Horizontal : Gfx::Orientation::Vertical; auto overflow = is_horizontal ? computed_values().overflow_x() : computed_values().overflow_y(); auto scrollable_overflow_rect = this->scrollable_overflow_rect(); if (!scrollable_overflow_rect.has_value()) return false; CSSPixels scrollable_overflow_size = scrollable_overflow_rect->primary_size_for_orientation(orientation); CSSPixels scrollport_size = absolute_padding_box_rect().primary_size_for_orientation(orientation); bool overflow_value_allows_scrolling = overflow == CSS::Overflow::Auto || overflow == CSS::Overflow::Scroll; if ((is_viewport_paintable() && overflow != CSS::Overflow::Hidden) || overflow_value_allows_scrolling) return scrollable_overflow_size > scrollport_size; return false; } bool PaintableBox::could_be_scrolled_by_wheel_event() const { return could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal) || could_be_scrolled_by_wheel_event(ScrollDirection::Vertical); } bool PaintableBox::overflow_property_applies() const { // https://drafts.csswg.org/css-overflow-3/#overflow-control // Overflow properties apply to block containers, flex containers and grid containers. // FIXME: Ideally we would check whether overflow applies positively rather than listing exceptions. However, // not all elements that should support overflow are currently identifiable that way. if (is(*this)) return false; auto const& display = computed_values().display(); if (layout_node().is_inline_node()) return false; if (display.is_ruby_inside()) return false; if (display.is_internal() && !display.is_table_cell() && !display.is_table_caption()) return false; return true; } CSSPixels PaintableBox::available_scrollbar_length(ScrollDirection direction, ChromeMetrics const& metrics) const { bool is_horizontal = direction == ScrollDirection::Horizontal; auto padding_rect = absolute_padding_box_rect(); CSSPixels full_scrollport_length = is_horizontal ? padding_rect.width() : padding_rect.height(); if (has_resizer()) full_scrollport_length -= metrics.resize_gripper_size; else { if (is_horizontal && could_be_scrolled_by_wheel_event(ScrollDirection::Vertical)) full_scrollport_length -= metrics.scroll_gutter_thickness; if (!is_horizontal && could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal)) full_scrollport_length -= metrics.scroll_gutter_thickness; } return full_scrollport_length; } Optional PaintableBox::absolute_scrollbar_rect(ScrollDirection direction, bool with_gutter, ChromeMetrics const& metrics) const { if (!could_be_scrolled_by_wheel_event(direction)) return {}; if (computed_values().scrollbar_width() == CSS::ScrollbarWidth::None) return {}; bool is_horizontal = direction == ScrollDirection::Horizontal; bool adjusting_for_resizer = has_resizer(); CSSPixels rect_thickness = with_gutter ? metrics.scroll_gutter_thickness : metrics.scroll_thumb_thickness_thin + metrics.scroll_thumb_padding_thin; CSSPixelRect scrollbar_rect = absolute_padding_box_rect(); if (is_horizontal) { if (!adjusting_for_resizer && could_be_scrolled_by_wheel_event(ScrollDirection::Vertical)) { scrollbar_rect.set_width(max(CSSPixels { 0 }, scrollbar_rect.width() - metrics.scroll_gutter_thickness)); if (is_chrome_mirrored()) scrollbar_rect.set_x(scrollbar_rect.x() + metrics.scroll_gutter_thickness); } else if (adjusting_for_resizer) { scrollbar_rect.set_width(available_scrollbar_length(ScrollDirection::Horizontal, metrics)); if (is_chrome_mirrored()) scrollbar_rect.set_x(scrollbar_rect.x() + metrics.resize_gripper_size); } scrollbar_rect.set_y(max(CSSPixels { 0 }, scrollbar_rect.bottom() - rect_thickness)); scrollbar_rect.set_height(rect_thickness); } else { if (adjusting_for_resizer) scrollbar_rect.set_height(available_scrollbar_length(ScrollDirection::Vertical, metrics)); if (!is_chrome_mirrored()) scrollbar_rect.set_x(max(CSSPixels { 0 }, scrollbar_rect.right() - rect_thickness)); scrollbar_rect.set_width(rect_thickness); } return scrollbar_rect; } Optional PaintableBox::compute_scrollbar_data(ScrollDirection direction, ChromeMetrics const& metrics, ScrollStateSnapshot const* scroll_state_snapshot) const { bool is_horizontal = direction == ScrollDirection::Horizontal; auto orientation = is_horizontal ? Gfx::Orientation::Horizontal : Gfx::Orientation::Vertical; auto overflow = is_horizontal ? computed_values().overflow_x() : computed_values().overflow_y(); if (overflow != CSS::Overflow::Scroll && !could_be_scrolled_by_wheel_event(direction)) return {}; if (!m_own_scroll_frame_index.value()) return {}; CSSPixelRect scrollable_overflow_rect = this->scrollable_overflow_rect().value(); CSSPixels scrollable_overflow_length = scrollable_overflow_rect.primary_size_for_orientation(orientation); if (scrollable_overflow_length == 0) return {}; auto const& scrollbar = is_horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar; bool with_gutter = scrollbar && scrollbar->is_enlarged(); auto scrollbar_rect = absolute_scrollbar_rect(direction, with_gutter, metrics); if (!scrollbar_rect.has_value()) return {}; CSSPixels thumb_thickness = metrics.scroll_thumb_thickness_thin; CSSPixels thumb_margin = metrics.scroll_thumb_padding_thin; if (with_gutter) { thumb_thickness = metrics.scroll_thumb_thickness; thumb_margin = CSSPixels { (metrics.scroll_gutter_thickness - metrics.scroll_thumb_thickness) / 2.0 }; } CSSPixels scrollbar_length = scrollbar_rect->primary_size_for_orientation(orientation); CSSPixels usable_scrollbar_length = max(CSSPixels { 0 }, scrollbar_length - (2 * thumb_margin)); CSSPixels scrollport_size = absolute_padding_box_rect().primary_size_for_orientation(orientation); CSSPixels min_thumb_length = min(usable_scrollbar_length, metrics.scroll_thumb_min_length); CSSPixels thumb_length = max(usable_scrollbar_length * (scrollport_size / scrollable_overflow_length), min_thumb_length); ScrollbarData scrollbar_data = { .gutter_rect = {}, .thumb_rect = scrollbar_rect.value(), .thumb_travel_to_scroll_ratio = 0 }; scrollbar_data.thumb_rect.set_primary_size_for_orientation(orientation, thumb_length); scrollbar_data.thumb_rect.set_secondary_size_for_orientation(orientation, thumb_thickness); scrollbar_data.thumb_rect.translate_primary_offset_for_orientation(orientation, thumb_margin); if (with_gutter || (!is_horizontal && is_chrome_mirrored())) scrollbar_data.thumb_rect.translate_secondary_offset_for_orientation(orientation, thumb_margin); if (with_gutter) scrollbar_data.gutter_rect = scrollbar_rect.value(); if (scrollable_overflow_length > scrollport_size) scrollbar_data.thumb_travel_to_scroll_ratio = (usable_scrollbar_length - thumb_length) / (scrollable_overflow_length - scrollport_size); if (scroll_state_snapshot) { auto own_offset = scroll_state_snapshot->device_offset_for_index(m_own_scroll_frame_index); auto device_scroll_offset = is_horizontal ? -own_offset.x() : -own_offset.y(); auto device_pixels_per_css_pixel = static_cast(document().page().client().device_pixels_per_css_pixel()); CSSPixels thumb_offset = CSSPixels::nearest_value_for(device_scroll_offset / device_pixels_per_css_pixel) * scrollbar_data.thumb_travel_to_scroll_ratio; scrollbar_data.thumb_rect.translate_primary_offset_for_orientation(orientation, thumb_offset); } return scrollbar_data; } void PaintableBox::paint(DisplayListRecordingContext& context, PaintPhase phase) const { if (!is_visible()) return; auto empty_cells_property_applies = [this]() { return display().is_internal_table() && computed_values().empty_cells() == CSS::EmptyCells::Hide && !has_children(); }; if (phase == PaintPhase::Background && !empty_cells_property_applies()) { paint_backdrop_filter(context); paint_background(context); paint_box_shadow(context); } auto const is_table_with_collapsed_borders = display().is_table_inside() && computed_values().border_collapse() == CSS::BorderCollapse::Collapse; if (!display().is_table_cell() && !is_table_with_collapsed_borders && phase == PaintPhase::Border) { paint_border(context); } if ((display().is_table_inside() || computed_values().border_collapse() == CSS::BorderCollapse::Collapse) && phase == PaintPhase::TableCollapsedBorder) { paint_table_borders(context, *this); } if (phase == PaintPhase::Outline) { auto const& outline_data = this->outline_data(); if (outline_data.has_value()) { auto outline_offset = this->outline_offset(); auto border_radius_data = normalized_border_radii_data(ShrinkRadiiForBorders::No); auto borders_rect = absolute_border_box_rect(); auto outline_offset_x = outline_offset; auto outline_offset_y = outline_offset; // "Both the height and the width of the outside of the shape drawn by the outline should not // become smaller than twice the computed value of the outline-width property to make sure // that an outline can be rendered even with large negative values." // https://www.w3.org/TR/css-ui-4/#outline-offset // So, if the horizontal outline offset is > half the borders_rect's width then we set it to that. // (And the same for y) if ((borders_rect.width() / 2) + outline_offset_x < 0) outline_offset_x = -borders_rect.width() / 2; if ((borders_rect.height() / 2) + outline_offset_y < 0) outline_offset_y = -borders_rect.height() / 2; border_radius_data.inflate(outline_data->top.width + outline_offset_y, outline_data->right.width + outline_offset_x, outline_data->bottom.width + outline_offset_y, outline_data->left.width + outline_offset_x); borders_rect.inflate(outline_data->top.width + outline_offset_y, outline_data->right.width + outline_offset_x, outline_data->bottom.width + outline_offset_y, outline_data->left.width + outline_offset_x); paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(borders_rect), border_radius_data.as_corners(context.device_pixel_converter()), outline_data->to_device_pixels(context)); } } if (phase == PaintPhase::Overlay) { ChromeMetrics const& metrics = context.chrome_metrics(); if ((g_paint_viewport_scrollbars || !is_viewport_paintable()) && computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) { auto scrollbar_colors = computed_values().scrollbar_color(); for (auto direction : { ScrollDirection::Vertical, ScrollDirection::Horizontal }) { auto scrollbar_data = compute_scrollbar_data(direction, metrics); if (!scrollbar_data.has_value()) continue; context.display_list_recorder().paint_scrollbar( m_own_scroll_frame_index, context.rounded_device_rect(scrollbar_data->gutter_rect).to_type(), context.rounded_device_rect(scrollbar_data->thumb_rect).to_type(), scrollbar_data->thumb_travel_to_scroll_ratio.to_double(), scrollbar_colors.thumb_color, scrollbar_colors.track_color, direction == ScrollDirection::Vertical); } } if (auto resizer_rect = absolute_resizer_rect(metrics); resizer_rect.has_value()) { bool bottom_left_resizer = is_chrome_mirrored(); CSSPixels padding = metrics.resize_gripper_padding; CSSPixelRect css_rect = resizer_rect.value() .shrunken(padding, padding) .translated(bottom_left_resizer ? padding / 2 : -padding / 2, -padding / 2); Gfx::IntRect rect = context.rounded_device_rect(css_rect).to_type(); Gfx::Color dark { 0, 0, 0, 100 }; Gfx::Color light { 255, 255, 255, 100 }; auto& recorder = context.display_list_recorder(); auto paint_resizer_line = [&](int step, Gfx::Color color) { Gfx::IntPoint from = { bottom_left_resizer ? rect.left() + step : rect.right() - step, rect.bottom() }; Gfx::IntPoint to = { bottom_left_resizer ? rect.left() : rect.right(), rect.bottom() - step }; recorder.draw_line(from, to, color, 1, Gfx::LineStyle::Solid); }; for (int step = (rect.width() / 3) - 1; step < rect.width(); step += rect.width() / 3) { paint_resizer_line(step, light); paint_resizer_line(step + 1, dark); } } paint_middle_button_scroll_indicator(context); } } void PaintableBox::paint_middle_button_scroll_indicator(DisplayListRecordingContext& context) const { static constexpr Gfx::Color CIRCLE_COLOR = Gfx::Color { Gfx::Color::White }.with_alpha(220); static constexpr Gfx::Color ARROW_COLOR = Gfx::Color::DarkGray; static constexpr auto RADIUS = 16; static constexpr auto ARROW_SIZE = 6; static constexpr auto ARROW_OFFSET = 8; if (!is_viewport_paintable()) return; auto navigable = document().navigable(); if (!navigable) return; auto handler = navigable->event_handler().middle_button_scroll_handler(); if (!handler.has_value()) return; auto& recorder = context.display_list_recorder(); auto device_origin = context.rounded_device_point(handler->origin()).to_type(); Gfx::IntRect circle { device_origin.x() - RADIUS, device_origin.y() - RADIUS, RADIUS * 2, RADIUS * 2 }; recorder.fill_ellipse(circle, CIRCLE_COLOR); recorder.draw_ellipse(circle, ARROW_COLOR, 1); auto paint_arrow = [&](Gfx::FloatPoint p1, Gfx::FloatPoint p2, Gfx::FloatPoint p3) { Gfx::Path path; path.move_to(p1); path.line_to(p2); path.line_to(p3); path.close(); recorder.fill_path({ .path = move(path), .paint_style_or_color = ARROW_COLOR }); }; auto x = static_cast(device_origin.x()); auto y = static_cast(device_origin.y()); // FIXME: We could paint a subset of these arrows depending on which direction the container may be scrolled. paint_arrow({ x, y - ARROW_OFFSET - ARROW_SIZE }, { x - ARROW_SIZE, y - ARROW_OFFSET }, { x + ARROW_SIZE, y - ARROW_OFFSET }); paint_arrow({ x, y + ARROW_OFFSET + ARROW_SIZE }, { x - ARROW_SIZE, y + ARROW_OFFSET }, { x + ARROW_SIZE, y + ARROW_OFFSET }); paint_arrow({ x - ARROW_OFFSET - ARROW_SIZE, y }, { x - ARROW_OFFSET, y - ARROW_SIZE }, { x - ARROW_OFFSET, y + ARROW_SIZE }); paint_arrow({ x + ARROW_OFFSET + ARROW_SIZE, y }, { x + ARROW_OFFSET, y - ARROW_SIZE }, { x + ARROW_OFFSET, y + ARROW_SIZE }); } void PaintableBox::paint_inspector_overlay_internal(DisplayListRecordingContext& context) const { auto content_rect = absolute_united_content_rect(); auto margin_rect = united_rect_for_continuation_chain(*this, [](PaintableBox const& box) { auto margin_box = box.box_model().margin_box(); return CSSPixelRect { box.absolute_x() - margin_box.left, box.absolute_y() - margin_box.top, box.content_width() + margin_box.left + margin_box.right, box.content_height() + margin_box.top + margin_box.bottom, }; }); auto border_rect = absolute_united_border_box_rect(); auto padding_rect = absolute_united_padding_box_rect(); auto paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) { auto device_rect = context.enclosing_device_rect(rect).to_type(); context.display_list_recorder().fill_rect(device_rect, color.with_alpha(100)); context.display_list_recorder().draw_rect(device_rect, color); }; paint_inspector_rect(margin_rect, Color::Yellow); paint_inspector_rect(padding_rect, Color::Cyan); paint_inspector_rect(border_rect, Color::Green); paint_inspector_rect(content_rect, Color::Magenta); auto font = Platform::FontPlugin::the().default_font(12); StringBuilder builder(StringBuilder::Mode::UTF16); builder.append(debug_description()); builder.appendff(" {}x{} @ {},{}", border_rect.width(), border_rect.height(), border_rect.x(), border_rect.y()); auto size_text = builder.to_utf16_string(); auto size_text_rect = border_rect; size_text_rect.set_y(border_rect.y() + border_rect.height()); size_text_rect.set_top(size_text_rect.top()); size_text_rect.set_width(CSSPixels::nearest_value_for(font->width(size_text)) + 4); size_text_rect.set_height(CSSPixels::nearest_value_for(font->pixel_size()) + 4); auto size_text_device_rect = context.enclosing_device_rect(size_text_rect).to_type(); context.display_list_recorder().fill_rect(size_text_device_rect, context.palette().color(Gfx::ColorRole::Tooltip)); context.display_list_recorder().draw_rect(size_text_device_rect, context.palette().threed_shadow1()); context.display_list_recorder().draw_text(size_text_device_rect, size_text, font->with_size(font->point_size() * context.device_pixels_per_css_pixel()), Gfx::TextAlignment::Center, context.palette().color(Gfx::ColorRole::TooltipText)); } void PaintableBox::set_stacking_context(GC::Ref stacking_context) { m_stacking_context = move(stacking_context); } void PaintableBox::invalidate_stacking_context() { m_stacking_context = nullptr; } Optional PaintableBox::effective_z_index() const { // https://drafts.csswg.org/css2/#z-index // Applies to: positioned elements if (is_positioned()) return computed_values().z_index(); return {}; } BordersData PaintableBox::remove_element_kind_from_borders_data(PaintableBox::BordersDataWithElementKind borders_data) { return { .top = borders_data.top.border_data, .right = borders_data.right.border_data, .bottom = borders_data.bottom.border_data, .left = borders_data.left.border_data, }; } void PaintableBox::paint_border(DisplayListRecordingContext& context) const { auto borders_data = m_override_borders_data.has_value() ? remove_element_kind_from_borders_data(m_override_borders_data.value()) : BordersData { .top = box_model().border.top == 0 ? CSS::BorderData() : computed_values().border_top(), .right = box_model().border.right == 0 ? CSS::BorderData() : computed_values().border_right(), .bottom = box_model().border.bottom == 0 ? CSS::BorderData() : computed_values().border_bottom(), .left = box_model().border.left == 0 ? CSS::BorderData() : computed_values().border_left(), }; paint_all_borders(context.display_list_recorder(), context.rounded_device_rect(absolute_border_box_rect()), normalized_border_radii_data().as_corners(context.device_pixel_converter()), borders_data.to_device_pixels(context)); } void PaintableBox::paint_backdrop_filter(DisplayListRecordingContext& context) const { if (!computed_values().backdrop_filter().has_filters()) return; auto resolved = resolve_css_filter(computed_values().backdrop_filter(), *this); auto backdrop_region = context.rounded_device_rect(absolute_border_box_rect()); auto border_radii_data = normalized_border_radii_data(); ScopedCornerRadiusClip corner_clipper { context, backdrop_region, border_radii_data }; if (auto gfx_filter = to_gfx_filter(resolved, context.device_pixels_per_css_pixel()); gfx_filter.has_value()) context.display_list_recorder().apply_backdrop_filter(backdrop_region.to_type(), border_radii_data.as_corners(context.device_pixel_converter()), *gfx_filter); } void PaintableBox::paint_background(DisplayListRecordingContext& context) const { // If the body's background properties were propagated to the root element, do not re-paint the body's background. if (layout_node_with_style_and_box_metrics().is_body() && document().html_element()->should_use_body_background_properties()) return; auto const& computed_values = this->computed_values(); CSSPixelRect background_rect; Color background_color = computed_values.background_color(); auto const* background_layers = &computed_values.background_layers(); // https://drafts.csswg.org/css-backgrounds/#root-background auto is_root = layout_node_with_style_and_box_metrics().is_root_element(); if (is_root) { background_rect = absolute_border_box_rect(); auto& html_element = as(*layout_node_with_style_and_box_metrics().dom_node()); if (html_element.should_use_body_background_properties()) { background_layers = document().background_layers(); background_color = document().background_color(); } } else { background_rect = absolute_padding_box_rect(); } // HACK: If the Box has a border, use the bordered_rect to paint the background. // This way if we have a border-radius there will be no gap between the filling and actual border. if (computed_values.border_top().width != 0 || computed_values.border_right().width != 0 || computed_values.border_bottom().width != 0 || computed_values.border_left().width != 0) background_rect = absolute_border_box_rect(); auto border_radii = normalized_border_radii_data(); ResolvedBackground resolved_background; if (background_layers) resolved_background = resolve_background_layers(*background_layers, *this, background_color, computed_values.background_color_clip(), background_rect, border_radii); if (is_root) { auto canvas_rect = navigable()->viewport_rect(); if (auto overflow_rect = scrollable_overflow_rect(); overflow_rect.has_value()) canvas_rect.unite(overflow_rect.value()); resolved_background.background_rect.unite(canvas_rect); resolved_background.color_box.rect.unite(canvas_rect); } // If the body's background was propagated to the root element, use the body's image-rendering value. auto image_rendering = computed_values.image_rendering(); if (layout_node().is_root_element() && document().html_element() && document().html_element()->should_use_body_background_properties()) { image_rendering = document().background_image_rendering(); } Painting::paint_background(context, *this, image_rendering, resolved_background, border_radii); } void PaintableBox::paint_box_shadow(DisplayListRecordingContext& context) const { auto const& box_shadow_layers = computed_values().box_shadow(); if (box_shadow_layers.is_empty()) return; Vector resolved_box_shadow_data; resolved_box_shadow_data.ensure_capacity(box_shadow_layers.size()); for (auto const& layer : box_shadow_layers) resolved_box_shadow_data.unchecked_append(ShadowData::from_css(layer, layout_node())); auto borders_data = BordersData { .top = computed_values().border_top(), .right = computed_values().border_right(), .bottom = computed_values().border_bottom(), .left = computed_values().border_left(), }; Painting::paint_box_shadow(context, absolute_border_box_rect(), absolute_padding_box_rect(), borders_data, normalized_border_radii_data(), resolved_box_shadow_data); } BorderRadiiData PaintableBox::normalized_border_radii_data(ShrinkRadiiForBorders shrink) const { auto border_radii_data = this->border_radii_data(); if (shrink == ShrinkRadiiForBorders::Yes) border_radii_data.shrink(computed_values().border_top().width, computed_values().border_right().width, computed_values().border_bottom().width, computed_values().border_left().width); return border_radii_data; } Optional PaintableBox::transform_point_to_local(CSSPixelPoint screen_position) const { if (!m_accumulated_visual_context_index.value()) return screen_position; auto pixel_ratio = static_cast(document().page().client().device_pixels_per_css_pixel()); auto const& scroll_state = document().paintable()->scroll_state_snapshot(); auto const& visual_context_tree = document().paintable()->visual_context_tree(); auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_index, screen_position.to_type() * pixel_ratio, scroll_state); if (!result.has_value()) return {}; return (*result / pixel_ratio).to_type(); } Optional PaintableBox::transform_point_to_local_for_descendants(CSSPixelPoint screen_position) const { if (!m_accumulated_visual_context_for_descendants_index.value()) return screen_position; auto pixel_ratio = static_cast(document().page().client().device_pixels_per_css_pixel()); auto const& scroll_state = document().paintable()->scroll_state_snapshot(); auto const& visual_context_tree = document().paintable()->visual_context_tree(); auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_for_descendants_index, screen_position.to_type() * pixel_ratio, scroll_state); if (!result.has_value()) return {}; return (*result / pixel_ratio).to_type(); } CSSPixelRect PaintableBox::transform_rect_to_viewport(CSSPixelRect const& rect) const { if (!m_accumulated_visual_context_index.value()) return rect; auto pixel_ratio = static_cast(document().page().client().device_pixels_per_css_pixel()); auto const& scroll_state = document().paintable()->scroll_state_snapshot(); auto const& visual_context_tree = document().paintable()->visual_context_tree(); auto result = visual_context_tree.transform_rect_to_viewport(m_accumulated_visual_context_index, rect.to_type() * pixel_ratio, scroll_state); return (result * (1.f / pixel_ratio)).to_type(); } CSSPixelPoint PaintableBox::inverse_transform_point(CSSPixelPoint screen_position) const { if (!m_accumulated_visual_context_index.value()) return screen_position; auto pixel_ratio = static_cast(document().page().client().device_pixels_per_css_pixel()); auto const& visual_context_tree = document().paintable()->visual_context_tree(); auto result = visual_context_tree.inverse_transform_point(m_accumulated_visual_context_index, screen_position.to_type() * pixel_ratio); return (result / pixel_ratio).to_type(); } CSSPixelPoint PaintableBox::transform_to_local_coordinates(CSSPixelPoint screen_position) const { return transform_point_to_local(screen_position).value_or(screen_position); } bool PaintableBox::has_resizer() const { // https://drafts.csswg.org/css-ui#resize if (is_viewport_paintable()) return false; // The effect of the resize property on generated content is undefined. // Implementations should not apply the resize property to generated content. if (layout_node().generated_for_pseudo_element().has_value()) return false; auto axes = physical_resize_axes(); return axes.horizontal || axes.vertical; } bool PaintableBox::is_chrome_mirrored() const { auto const& writing_mode = computed_values().writing_mode(); return (writing_mode == CSS::WritingMode::HorizontalTb && computed_values().direction() == CSS::Direction::Rtl) || writing_mode == CSS::WritingMode::VerticalRl || writing_mode == CSS::WritingMode::SidewaysRl; } GC::Ptr PaintableBox::resize_handle() const { return m_resize_handle; } GC::Ref PaintableBox::ensure_resize_handle() { if (!m_resize_handle) m_resize_handle = ResizeHandle::create(heap(), *this); return *m_resize_handle; } bool PaintableBox::handle_mousewheel(Badge, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y) { // if none of the axes we scrolled with can be accepted by this element, don't handle scroll. if ((!wheel_delta_x || !could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal)) && (!wheel_delta_y || !could_be_scrolled_by_wheel_event(ScrollDirection::Vertical))) { return false; } auto scroll_handled = scroll_by(wheel_delta_x, wheel_delta_y); return scroll_handled == ScrollHandled::Yes; } TraversalDecision PaintableBox::hit_test_chrome(CSSPixelPoint adjusted_position, Function const& callback) const { // The vast majority of paintable boxes have no resizer and no scrollable axis. Reject those before constructing // ChromeMetrics or allocating any Scrollbar. auto has_resizer = this->has_resizer(); auto can_scroll_horizontally = could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal); auto can_scroll_vertically = could_be_scrolled_by_wheel_event(ScrollDirection::Vertical); if (!has_resizer && !can_scroll_horizontally && !can_scroll_vertically) return TraversalDecision::Continue; // FIXME: This const_cast is not great, but this method is invoked from overrides of virtual const methods. HitTestResult result { .paintable = const_cast(*this) }; ChromeMetrics metrics = document().page().chrome_metrics(); if (has_resizer && resizer_contains(adjusted_position, metrics)) { result.chrome_widget = const_cast(*this).ensure_resize_handle(); return callback(result); } auto check_scrollbar = [&](ScrollDirection direction) -> TraversalDecision { auto scrollbar = const_cast(*this).ensure_scrollbar(direction); if (scrollbar->contains(adjusted_position, metrics)) { result.chrome_widget = scrollbar; return callback(result); } return TraversalDecision::Continue; }; if (can_scroll_horizontally && check_scrollbar(ScrollDirection::Horizontal) == TraversalDecision::Break) return TraversalDecision::Break; if (can_scroll_vertically && check_scrollbar(ScrollDirection::Vertical) == TraversalDecision::Break) return TraversalDecision::Break; return TraversalDecision::Continue; } bool PaintableBox::resizer_contains(CSSPixelPoint adjusted_position, ChromeMetrics const& metrics) const { auto handle_rect = absolute_resizer_rect(metrics); if (!handle_rect.has_value()) return false; bool bottom_left_resizer = is_chrome_mirrored(); handle_rect->inflate(0, bottom_left_resizer ? 0 : box_model().border.right, box_model().border.bottom, bottom_left_resizer ? box_model().border.left : 0); return handle_rect->contains(adjusted_position); } TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType type, Function const& callback) const { auto const is_visible = computed_values().visibility() == CSS::Visibility::Visible; Optional local_position = transform_point_to_local(position); // Only hit test chrome (scrollbars, etc.) for visible elements. if (is_visible && visible_for_hit_testing()) { if (hit_test_chrome(local_position.value_or(position), callback) == TraversalDecision::Break) return TraversalDecision::Break; } if (is_viewport_paintable()) { auto& viewport_paintable = const_cast(static_cast(*this)); viewport_paintable.build_stacking_context_tree_if_needed(); viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed(); viewport_paintable.refresh_scroll_state(); return stacking_context()->hit_test(position, type, callback); } if (stacking_context()) return TraversalDecision::Continue; if (hit_test_children(position, type, callback) == TraversalDecision::Break) return TraversalDecision::Break; // Hidden elements and elements with pointer-events: none shouldn't be hit. if (!is_visible || !visible_for_hit_testing()) return TraversalDecision::Continue; if (!local_position.has_value()) return TraversalDecision::Continue; auto border_box_rect = absolute_border_box_rect(); if (!border_box_rect.contains(local_position.value())) return TraversalDecision::Continue; if (auto radii = border_radii_data(); radii.has_any_radius()) { if (!radii.contains(local_position.value(), border_box_rect)) return TraversalDecision::Continue; } if (hit_test_continuation(callback) == TraversalDecision::Break) return TraversalDecision::Break; return callback(HitTestResult { .paintable = const_cast(*this) }); } TraversalDecision PaintableBox::hit_test_continuation(Function const& callback) const { // If we're hit testing the "middle" part of a continuation chain, we are dealing with an anonymous box that is // linked to a parent inline node. Since our block element children did not match the hit test, but we did, we // should walk the continuation chain up to the inline parent and return a hit on that instead. auto continuation_node = layout_node_with_style_and_box_metrics().continuation_of_node(); if (!continuation_node || !layout_node().is_anonymous()) return TraversalDecision::Continue; while (continuation_node->continuation_of_node()) continuation_node = continuation_node->continuation_of_node(); auto& paintable = *continuation_node->first_paintable(); if (!paintable.visible_for_hit_testing()) return TraversalDecision::Continue; return callback(HitTestResult { .paintable = paintable }); } Optional PaintableBox::hit_test(CSSPixelPoint position, HitTestType type) const { Optional result; (void)PaintableBox::hit_test(position, type, [&](HitTestResult candidate) { if (!result.has_value() || candidate.vertical_distance.value_or(CSSPixels::max_integer_value) < result->vertical_distance.value_or(CSSPixels::max_integer_value) || candidate.horizontal_distance.value_or(CSSPixels::max_integer_value) < result->horizontal_distance.value_or(CSSPixels::max_integer_value)) { result = move(candidate); } if (result.has_value() && (type == HitTestType::Exact || (result->vertical_distance == 0 && result->horizontal_distance == 0))) return TraversalDecision::Break; return TraversalDecision::Continue; }); return result; } TraversalDecision PaintableBox::hit_test_children(CSSPixelPoint position, HitTestType type, Function const& callback) const { for (auto const* child = last_child(); child; child = child->previous_sibling()) { if (child->is_positioned() && child->computed_values().z_index().value_or(0) == 0) continue; if (child->has_stacking_context()) continue; if (child->hit_test(position, type, callback) == TraversalDecision::Break) return TraversalDecision::Break; } return TraversalDecision::Continue; } void PaintableBox::set_needs_repaint(InvalidateDisplayList should_invalidate_display_list) { if (should_invalidate_display_list == InvalidateDisplayList::Yes) { invalidate_paint_cache(); // Recurse into anonymous child nodes so we properly invalidate nested contents of e.g.