/*
 * Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2022-2025, Sam Atkins <sam@ladybird.org>
 * Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
 * Copyright (c) 2025-2026, Jelle Raaijmakers <jelle@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Array.h>
#include <AK/GenericShorthands.h>
#include <AK/StdLibExtras.h>
#include <AK/Utf16StringBuilder.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Font/Font.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/ComputedValues.h>
#include <LibWeb/CSS/StyleScope.h>
#include <LibWeb/CSS/StyleValues/BorderImageSliceStyleValue.h>
#include <LibWeb/CSS/StyleValues/ColorSchemeStyleValue.h>
#include <LibWeb/CSS/StyleValues/FilterValueListStyleValue.h>
#include <LibWeb/CSS/StyleValues/ImageStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/SystemColor.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/LocalNavigable.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/TextOffsetMapping.h>
#include <LibWeb/Page/EventHandler.h>
#include <LibWeb/Page/MiddleButtonScrollHandler.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/BackgroundPainting.h>
#include <LibWeb/Painting/BorderRadiusCornerClipper.h>
#include <LibWeb/Painting/ChromeMetrics.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/DisplayListRecordingContext.h>
#include <LibWeb/Painting/FlexboxInspectorOverlay.h>
#include <LibWeb/Painting/GridInspectorOverlay.h>
#include <LibWeb/Painting/HitTestDisplayList.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableWithLines.h>
#include <LibWeb/Painting/ResizeHandle.h>
#include <LibWeb/Painting/SVGGraphicsPaintable.h>
#include <LibWeb/Painting/SVGPaintable.h>
#include <LibWeb/Painting/SVGSVGPaintable.h>
#include <LibWeb/Painting/Scrollbar.h>
#include <LibWeb/Painting/ShadowPainting.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/TableBordersPainting.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/Platform/FontPlugin.h>
#include <LibWeb/SVG/SVGFilterElement.h>

namespace Web::Painting {

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> 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<Paintable> {
        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&>(*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> dom_node)
{
    m_dom_node = dom_node.ptr();
}

GC::Ptr<DOM::Node> Paintable::dom_node()
{
    return m_dom_node.ptr();
}

GC::Ptr<DOM::Node const> Paintable::dom_node() const
{
    return m_dom_node.ptr();
}

GC::Ptr<HTML::LocalNavigable> Paintable::navigable() const
{
    return document().navigable();
}

bool Paintable::has_stacking_context() const
{
    return stacking_context();
}

DOM::Node* HitTestResult::dom_node()
{
    if (dom_node_override)
        return dom_node_override.ptr();

    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
{
    if (dom_node_override)
        return dom_node_override.ptr();

    for (auto const* current = paintable.ptr(); current; current = current->parent()) {
        if (auto node = current->dom_node())
            return node;
    }
    return nullptr;
}

RefPtr<StackingContext> Paintable::enclosing_stacking_context()
{
    for (auto ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
        if (auto stacking_context = ancestor->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<void()> const& callback) const
{
    auto& display_list_recorder = context.display_list_recorder();
    auto previous_visual_context_index = display_list_recorder.accumulated_visual_context();

    auto viewport_paintable = document().paintable();
    VERIFY(viewport_paintable);
    auto& visual_context_tree = const_cast<ViewportPaintable&>(*viewport_paintable).visual_context_tree();
    auto visual_context_index = accumulated_visual_context_index();

    if (visual_context_index != VISUAL_VIEWPORT_NODE_INDEX) {
        Vector<VisualContextIndex> 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; },
                [](AnchorScrollShift 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);
}

CSSPixelPoint Paintable::box_type_agnostic_position() const
{
    return absolute_position();
}

Painting::BorderRadiiData normalize_border_radii_data(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(reference_rect.width()),
            top_left_radius.vertical_radius.to_px(reference_rect.height()) },
        .top_right = { top_right_radius.horizontal_radius.to_px(reference_rect.width()), top_right_radius.vertical_radius.to_px(reference_rect.height()) },
        .bottom_right = { bottom_right_radius.horizontal_radius.to_px(reference_rect.width()), bottom_right_radius.vertical_radius.to_px(reference_rect.height()) },
        .bottom_left = { bottom_left_radius.horizontal_radius.to_px(reference_rect.width()), bottom_left_radius.vertical_radius.to_px(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
{
    return selection_style_for_node(layout_node(), dom_node());
}

Paintable::SelectionStyle Paintable::selection_style_for_node(Layout::Node const& layout_node, GC::Ptr<DOM::Node const> node)
{
    // Selections render in a muted color while the window does not have focus.
    auto navigable = layout_node.document().navigable();
    auto window_is_active = navigable && navigable->is_focused();

    auto default_style_for_color_scheme = [&](CSS::PreferredColorScheme color_scheme, bool use_palette_for_normal_color_scheme = true) {
        auto palette = layout_node.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) {
            auto background = window_is_active ? palette.selection() : palette.inactive_selection();
            return SelectionStyle { CSS::SystemColor::transform_selection_background_color(background) };
        }

        auto background = window_is_active ? CSS::SystemColor::highlight(color_scheme) : CSS::SystemColor::inactive_highlight(color_scheme);
        return SelectionStyle { CSS::SystemColor::transform_selection_background_color(background) };
    };

    // For text nodes, check the parent element since text nodes don't have computed properties.
    if (!node)
        return default_style_for_color_scheme(layout_node.computed_values().color_scheme());

    DOM::Element const* element = as_if<DOM::Element>(*node);
    if (!element)
        element = node->parent_element();
    if (!element)
        return default_style_for_color_scheme(layout_node.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 && !layout_node.document().supported_color_schemes().has_value();
    auto default_style = default_style_for_color_scheme(layout_node.computed_values().color_scheme(), use_palette_for_normal_color_scheme);

    auto style_from_element = [&](DOM::Element const& element) -> Optional<SelectionStyle> {
        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<ShadowData> shadows;
            shadows.ensure_capacity(css_shadows.size());
            for (auto const& shadow : css_shadows)
                shadows.unchecked_append(ShadowData::from_css(shadow));
            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 <input> 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;
    invalidate_paint_cache();
}

void Paintable::scroll_text_offset_into_view(DOM::Text const& text, size_t offset, TextAffinity affinity)
{
    auto scroll_to_cursor = [&](PaintableFragment const& fragment) {
        auto cursor_rect = fragment.range_rect(SelectionState::StartAndEnd, offset, offset);
        for (auto ancestor = fragment.containing_block_paintable(); ancestor; ancestor = ancestor->containing_block()) {
            if (ancestor->has_scrollable_overflow()) {
                ancestor->scroll_into_view(cursor_rect);
                return;
            }
        }
    };

    PaintableFragment const* fallback_fragment = nullptr;
    Layout::TextOffsetMapping mapping { text };
    mapping.for_each_paintable_fragment([&](PaintableFragment const& fragment) {
        switch (fragment.caret_match(offset, affinity)) {
        case PaintableFragment::CaretMatch::None:
            return TraversalDecision::Continue;
        case PaintableFragment::CaretMatch::SoftWrapFallback:
            if (!fallback_fragment)
                fallback_fragment = &fragment;
            return TraversalDecision::Continue;
        case PaintableFragment::CaretMatch::Direct:
            fallback_fragment = nullptr;
            scroll_to_cursor(fragment);
            return TraversalDecision::Break;
        }
        VERIFY_NOT_REACHED();
    });
    if (fallback_fragment)
        scroll_to_cursor(*fallback_fragment);
}

void Paintable::scroll_ancestor_to_offset_into_view(size_t offset)
{
    if (auto const* text = as_if<DOM::Text>(dom_node().ptr()))
        scroll_text_offset_into_view(*text, offset);
}

static bool g_paint_viewport_scrollbars = true;

struct Paintable::CachedPaintData {
    bool has(PaintPhase phase) const
    {
        return m_present_phases[to_underlying(phase)];
    }

    ReadonlyBytes bytes_for(PaintPhase phase) const
    {
        auto const& span = m_phase_spans[to_underlying(phase)];
        return m_command_bytes.span().slice(span.offset, span.size);
    }

    void set(PaintPhase phase, ReadonlyBytes command_bytes)
    {
        auto const phase_index = to_underlying(phase);
        if (m_present_phases[phase_index]) {
            replace(phase, command_bytes);
            return;
        }

        m_phase_spans[phase_index] = append_to(m_command_bytes, command_bytes);
        m_present_phases[phase_index] = true;
    }

    template<typename Callback>
    void for_each_present_phase(Callback callback) const
    {
        for (size_t phase_index = 0; phase_index < paint_phase_count; ++phase_index) {
            if (!m_present_phases[phase_index])
                continue;
            auto phase = static_cast<PaintPhase>(phase_index);
            callback(phase, bytes_for(phase));
        }
    }

private:
    struct Span {
        u32 offset { 0 };
        u32 size { 0 };
    };

    static Span append_to(ByteBuffer& command_buffer, ReadonlyBytes command_bytes)
    {
        auto const offset = command_buffer.size();
        command_buffer.append(command_bytes);
        return { static_cast<u32>(offset), static_cast<u32>(command_bytes.size()) };
    }

    void replace(PaintPhase phase, ReadonlyBytes replacement_bytes)
    {
        ByteBuffer command_buffer;
        Array<Span, paint_phase_count> phase_spans {};

        for (size_t phase_index = 0; phase_index < paint_phase_count; ++phase_index) {
            if (!m_present_phases[phase_index])
                continue;
            auto present_phase = static_cast<PaintPhase>(phase_index);
            auto command_bytes = present_phase == phase ? replacement_bytes : bytes_for(present_phase);
            phase_spans[phase_index] = append_to(command_buffer, command_bytes);
        }

        m_command_bytes = move(command_buffer);
        m_phase_spans = phase_spans;
    }

    ByteBuffer m_command_bytes;
    Array<bool, paint_phase_count> m_present_phases {};
    Array<Span, paint_phase_count> m_phase_spans {};
};

static bool content_size_change_affects_container_queries(Paintable const& paintable_box, CSSPixelSize old_size, CSSPixelSize new_size)
{
    auto const& container_type = paintable_box.computed_values().container_type();
    if (container_type.is_size_container)
        return old_size != new_size;

    if (!container_type.is_inline_size_container)
        return false;

    if (paintable_box.computed_values().writing_mode() == CSS::WritingMode::HorizontalTb)
        return old_size.width() != new_size.width();

    return old_size.height() != new_size.height();
}

static void invalidate_descendant_styles_for_container_query_size_change(Paintable& paintable_box, CSSPixelSize old_size, CSSPixelSize new_size)
{
    if (!content_size_change_affects_container_queries(paintable_box, old_size, new_size))
        return;

    if (auto* element = as_if<DOM::Element>(paintable_box.dom_node().ptr())) {
        element->for_each_shadow_including_descendant([](DOM::Node& node) {
            if (auto* descendant_element = as_if<DOM::Element>(node); descendant_element && descendant_element->style_depends_on_size_container_query())
                descendant_element->set_needs_style_update(true);
            return TraversalDecision::Continue;
        });
    }
}

void set_paint_viewport_scrollbars(bool const enabled)
{
    g_paint_viewport_scrollbars = enabled;
}

bool should_paint_viewport_scrollbars()
{
    return g_paint_viewport_scrollbars;
}

static Gfx::FloatPoint css_point_to_device_point(CSSPixelPoint point, double device_pixels_per_css_pixel)
{
    auto scale = static_cast<float>(device_pixels_per_css_pixel);
    return { point.x().to_float() * scale, point.y().to_float() * scale };
}

static Gfx::FloatSize css_size_to_device_size(CSSPixelSize size, double device_pixels_per_css_pixel)
{
    auto scale = static_cast<float>(device_pixels_per_css_pixel);
    return { size.width().to_float() * scale, size.height().to_float() * scale };
}

static Gfx::FloatRect css_rect_to_device_rect(CSSPixelRect rect, double device_pixels_per_css_pixel)
{
    return { css_point_to_device_point(rect.location(), device_pixels_per_css_pixel), css_size_to_device_size(rect.size(), device_pixels_per_css_pixel) };
}

static Optional<float> css_inset_to_device_inset(Optional<CSSPixels> inset, double device_pixels_per_css_pixel)
{
    if (!inset.has_value())
        return {};
    return inset->to_float() * static_cast<float>(device_pixels_per_css_pixel);
}

static Optional<CompositorScrollNodeKind> scroll_node_kind_for(Paintable const& paintable_box)
{
    if (paintable_box.is_viewport_paintable())
        return CompositorScrollNodeKind::Viewport;
    if (paintable_box.layout_node().generated_for_pseudo_element().has_value())
        return CompositorScrollNodeKind::PseudoElement;
    if (paintable_box.dom_node() && is<DOM::Element>(*paintable_box.dom_node()))
        return CompositorScrollNodeKind::Element;
    return {};
}

static UniqueNodeID scrollable_node_id_for(Paintable const& paintable_box)
{
    if (paintable_box.is_viewport_paintable())
        return paintable_box.document().unique_id();
    if (paintable_box.layout_node().generated_for_pseudo_element().has_value())
        return paintable_box.layout_node().pseudo_element_generator()->unique_id();
    return paintable_box.dom_node()->unique_id();
}

static u8 pseudo_element_type_for(Paintable const& paintable_box)
{
    auto pseudo_element = paintable_box.layout_node().generated_for_pseudo_element();
    if (!pseudo_element.has_value())
        return 0;
    return static_cast<u8>(to_underlying(*pseudo_element));
}

static bool is_nested_navigable_container(Paintable const& paintable_box)
{
    auto node = paintable_box.dom_node();
    return node && node->is_navigable_container() && as<HTML::NavigableContainer const>(*node).content_navigable();
}

static CSSPixelPoint maximum_scroll_offset_for(Paintable const& paintable_box)
{
    CSSPixelPoint max_scroll_offset;
    auto scrollable_overflow_rect = paintable_box.scrollable_overflow_rect();
    if (!scrollable_overflow_rect.has_value())
        return max_scroll_offset;

    auto scrollport_rect = paintable_box.absolute_padding_box_rect();

    max_scroll_offset.set_x(max(CSSPixels(0), scrollable_overflow_rect->width() - scrollport_rect.width()));
    max_scroll_offset.set_y(max(CSSPixels(0), scrollable_overflow_rect->height() - scrollport_rect.height()));
    return max_scroll_offset;
}

static void record_scroll_node(Paintable const& paintable_box, DisplayListRecordingContext& context)
{
    auto scroll_node_kind = scroll_node_kind_for(paintable_box);
    if (!scroll_node_kind.has_value())
        return;

    auto parent_scroll_frame_index = ScrollFrameIndex {};
    if (auto scrollable_ancestor = paintable_box.nearest_scrollable_ancestor())
        parent_scroll_frame_index = scrollable_ancestor->own_scroll_frame_index();

    auto scrollport_rect = paintable_box.is_viewport_paintable()
        ? Gfx::IntRect { {}, context.device_viewport_rect().size().to_type<int>() }
        : context.rounded_device_rect(paintable_box.absolute_padding_box_rect()).to_type<int>();

    auto& recorder = context.display_list_recorder();
    recorder.compositor_scroll_node({
        .document_id = paintable_box.document().unique_id(),
        .scrollable_node_id = scrollable_node_id_for(paintable_box),
        .scroll_frame_index = paintable_box.own_scroll_frame_index(),
        .parent_scroll_frame_index = parent_scroll_frame_index,
        .scrollport_rect = scrollport_rect,
        .max_scroll_offset = css_point_to_device_point(maximum_scroll_offset_for(paintable_box), context.device_pixels_per_css_pixel()),
        .scroll_node_kind = *scroll_node_kind,
        .pseudo_element_type = pseudo_element_type_for(paintable_box),
        .is_viewport = paintable_box.is_viewport_paintable(),
        .can_be_wheel_scrolled_horizontally = paintable_box.could_be_scrolled_by_wheel_event(Paintable::ScrollDirection::Horizontal),
        .can_be_wheel_scrolled_vertically = paintable_box.could_be_scrolled_by_wheel_event(Paintable::ScrollDirection::Vertical),
    });
}

static void record_main_thread_wheel_event_region(Paintable const& paintable_box, DisplayListRecordingContext& context)
{
    auto rect = css_rect_to_device_rect(paintable_box.absolute_united_border_box_rect(), context.device_pixels_per_css_pixel());
    if (rect.is_empty())
        return;

    context.display_list_recorder().compositor_main_thread_wheel_event_region({
        .rect = rect,
    });
}

static Optional<ScrollFrameIndex> wheel_hit_test_target_scroll_frame_index_for(Paintable const& paintable_box)
{
    if (paintable_box.own_scroll_frame_index().value() && paintable_box.could_be_scrolled_by_wheel_event())
        return paintable_box.own_scroll_frame_index();
    if (auto scrollable_ancestor = paintable_box.nearest_scrollable_ancestor())
        return scrollable_ancestor->own_scroll_frame_index();
    if (auto viewport_paintable = paintable_box.document().paintable(); viewport_paintable && viewport_paintable->could_be_scrolled_by_wheel_event())
        return viewport_paintable->own_scroll_frame_index();
    return {};
}

static void record_wheel_hit_test_target(Paintable const& paintable_box, DisplayListRecordingContext& context)
{
    if (!paintable_box.is_visible() || !paintable_box.visible_for_hit_testing())
        return;

    auto rect = css_rect_to_device_rect(paintable_box.absolute_border_box_rect(), context.device_pixels_per_css_pixel());
    if (rect.is_empty())
        return;

    auto target_scroll_frame_index = wheel_hit_test_target_scroll_frame_index_for(paintable_box).value_or({});
    auto corner_radii = paintable_box.border_radii_data().as_corners(context.device_pixel_converter());
    if (corner_radii.has_any_radius()) {
        context.display_list_recorder().compositor_wheel_hit_test_target_with_corner_radii({
            .document_id = paintable_box.document().unique_id(),
            .target_scroll_frame_index = target_scroll_frame_index,
            .rect = rect,
            .corner_radii = corner_radii,
        });
        return;
    }

    context.display_list_recorder().compositor_wheel_hit_test_target({
        .document_id = paintable_box.document().unique_id(),
        .target_scroll_frame_index = target_scroll_frame_index,
        .rect = rect,
    });
}

static bool is_canvas_background_source(Layout::NodeWithStyle const& layout_node)
{
    if (layout_node.is_root_element())
        return true;

    auto const* html_element = layout_node.document().html_element();
    return layout_node.is_body() && html_element && html_element->should_use_body_background_properties();
}

static Color effective_scrollbar_background_color(Paintable const& paintable_box)
{
    auto background_color = paintable_box.document().canvas_background_color();

    Vector<Layout::NodeWithStyle const*> ancestors;
    for (auto const* layout_node = &paintable_box.layout_node(); layout_node; layout_node = layout_node->parent()) {
        if (auto const* layout_node_with_style = as_if<Layout::NodeWithStyle>(layout_node))
            ancestors.append(layout_node_with_style);
    }

    for (auto const* layout_node : ancestors.in_reverse()) {
        auto const& layout_node_with_style = *layout_node;
        if (is_canvas_background_source(layout_node_with_style))
            continue;

        auto color = layout_node_with_style.computed_values().background_color();
        if (color.alpha() == 0)
            continue;

        background_color = background_color.blend(color);
    }

    return background_color;
}

static CSS::ScrollbarColorData automatic_scrollbar_colors(Paintable const& paintable_box)
{
    auto background_color = effective_scrollbar_background_color(paintable_box);
    auto black_thumb = Color(Color::Black).with_alpha(128);
    auto white_thumb = Color(Color::White).with_alpha(128);

    auto black_thumb_contrast = background_color.contrast_ratio(background_color.blend(black_thumb));
    auto white_thumb_contrast = background_color.contrast_ratio(background_color.blend(white_thumb));
    auto thumb_color = black_thumb_contrast >= white_thumb_contrast ? black_thumb : white_thumb;

    return {
        .thumb_color = thumb_color,
        .track_color = thumb_color.with_alpha(25),
        .is_auto = true,
    };
}

static CSS::ScrollbarColorData scrollbar_colors_for_paint(Paintable const& paintable_box)
{
    auto scrollbar_colors = paintable_box.computed_values().scrollbar_color();
    if (!scrollbar_colors.is_auto)
        return scrollbar_colors;

    return automatic_scrollbar_colors(paintable_box);
}

static void record_viewport_scrollbar_state(Paintable const& paintable_box, DisplayListRecordingContext& context)
{
    if (!paintable_box.is_viewport_paintable())
        return;
    if (!paintable_box.document().page().async_scrolling_enabled())
        return;
    if (!should_paint_viewport_scrollbars())
        return;
    if (paintable_box.computed_values().scrollbar_width() == CSS::ScrollbarWidth::None)
        return;

    auto scrollbar_colors = scrollbar_colors_for_paint(paintable_box);
    auto const& metrics = context.chrome_metrics();

    for (auto direction : { Paintable::ScrollDirection::Vertical, Paintable::ScrollDirection::Horizontal }) {
        auto scrollbar_data = paintable_box.compute_scrollbar_data(direction, metrics, nullptr, Paintable::ScrollbarSizing::Regular);
        if (!scrollbar_data.has_value())
            continue;
        auto expanded_scrollbar_data = paintable_box.compute_scrollbar_data(direction, metrics, nullptr, Paintable::ScrollbarSizing::Enlarged);
        VERIFY(expanded_scrollbar_data.has_value());

        auto gutter_rect = context.rounded_device_rect(scrollbar_data->gutter_rect).to_type<int>();
        auto max_scroll_offset = css_point_to_device_point(maximum_scroll_offset_for(paintable_box), context.device_pixels_per_css_pixel());
        auto orientation = direction == Paintable::ScrollDirection::Horizontal ? Gfx::Orientation::Horizontal : Gfx::Orientation::Vertical;

        context.display_list_recorder().compositor_viewport_scrollbar({
            .document_id = paintable_box.document().unique_id(),
            .scroll_frame_index = paintable_box.own_scroll_frame_index(),
            .gutter_rect = gutter_rect,
            .thumb_rect = context.rounded_device_rect(scrollbar_data->thumb_rect).to_type<int>(),
            .expanded_gutter_rect = context.rounded_device_rect(expanded_scrollbar_data->gutter_rect).to_type<int>(),
            .expanded_thumb_rect = context.rounded_device_rect(expanded_scrollbar_data->thumb_rect).to_type<int>(),
            .scroll_size = scrollbar_data->thumb_travel_to_scroll_ratio.to_double(),
            .expanded_scroll_size = expanded_scrollbar_data->thumb_travel_to_scroll_ratio.to_double(),
            .max_scroll_offset = max_scroll_offset.primary_offset_for_orientation(orientation),
            .thumb_color = scrollbar_colors.thumb_color,
            .track_color = scrollbar_colors.track_color,
            .vertical = direction == Paintable::ScrollDirection::Vertical,
        });
    }
}

static void record_blocking_wheel_event_region(Paintable const& paintable_box, DisplayListRecordingContext& context)
{
    if (context.has_blocking_wheel_event_region_covering_viewport())
        return;
    if (!paintable_box.is_visible() || !paintable_box.visible_for_hit_testing())
        return;
    auto node = paintable_box.dom_node();
    if (!node || !node->inside_blocking_wheel_event_handler())
        return;

    auto rect = css_rect_to_device_rect(paintable_box.absolute_united_border_box_rect(), context.device_pixels_per_css_pixel());
    if (rect.is_empty())
        return;

    context.set_has_blocking_wheel_event_listeners(true);
    context.display_list_recorder().compositor_blocking_wheel_event_region({
        .rect = rect,
    });
}

ResolvedCSSFilter resolve_css_filter(CSS::Filter const& computed_filter, Paintable 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<CSS::StyleValue const> 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(Utf16String::from_utf8(fragment_or_error.value()));
                if (!maybe_filter)
                    return;
                if (auto* filter_element = as_if<SVG::SVGFilterElement>(*maybe_filter)) {
                    // Filter primitive lengths are specified in the filtered element's user coordinate system, but the
                    // resulting filter operates in device pixels. Compute the user-unit-to-device-pixel scale so the
                    // filter can convert its lengths accordingly.
                    auto device_pixels_per_css_pixel = paintable_box.document().page().client().device_pixels_per_css_pixel();
                    auto filter_scale = Gfx::FloatPoint { device_pixels_per_css_pixel, device_pixels_per_css_pixel };
                    if (auto const* svg_graphics_paintable = as_if<SVGGraphicsPaintable>(paintable_box)) {
                        auto svg_to_css_pixels = svg_graphics_paintable->computed_transforms().svg_to_css_pixels_transform();
                        filter_scale.scale_by(svg_to_css_pixels.x_scale(), svg_to_css_pixels.y_scale());
                    }
                    result.svg_filter = filter_element->gfx_filter(layout_node, filter_scale);
                    auto bounds = paintable_box.absolute_border_box_rect();
                    if (bounds.is_empty()) {
                        if (auto svg_ancestor = paintable_box.first_ancestor_of_type<SVGSVGPaintable>())
                            result.svg_filter_bounds = svg_ancestor->absolute_rect();
                    }
                    if (!bounds.is_empty())
                        result.svg_filter_bounds = bounds;
                }
            });
    }
    return result;
}

NonnullRefPtr<Paintable> Paintable::create(Layout::Box const& layout_box)
{
    return adopt_ref(*new Paintable(layout_box));
}

NonnullRefPtr<Paintable> Paintable::create(Layout::InlineNode const& layout_box)
{
    return adopt_ref(*new Paintable(layout_box));
}

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(Layout::Box const& layout_box)
    : Paintable(static_cast<Layout::Node const&>(layout_box))
{
}

Paintable::Paintable(Layout::InlineNode const& layout_box)
    : Paintable(static_cast<Layout::Node const&>(layout_box))
{
}

Paintable::~Paintable()
{
    if (has_layout_node())
        invalidate_paint_cache();
}

Layout::NodeWithStyleAndBoxModelMetrics const& Paintable::layout_node_with_style_and_box_metrics() const
{
    return as<Layout::NodeWithStyleAndBoxModelMetrics const>(layout_node());
}

bool Paintable::has_css_transform() const
{
    return layout_node().has_css_transform();
}

void Paintable::acquire_cache_references_for_cached_commands(ReadonlyBytes command_bytes) const
{
    auto& resource_storage = navigable()->display_list_resource_storage();
    auto referenced_resources = resource_storage.collect_referenced_resources(command_bytes);
    if (referenced_resources.is_empty())
        return;
    resource_storage.acquire_cache_references(referenced_resources);
}

void Paintable::release_cache_references_for_cached_commands(ReadonlyBytes command_bytes) const
{
    auto& resource_storage = navigable()->display_list_resource_storage();
    auto referenced_resources = resource_storage.collect_referenced_resources(command_bytes);
    if (referenced_resources.is_empty())
        return;
    resource_storage.release_cache_references(referenced_resources);
}

bool Paintable::has_cached_commands(PaintPhase phase) const
{
    return m_cached_paint_data && m_cached_paint_data->has(phase);
}

ReadonlyBytes Paintable::cached_commands(PaintPhase phase) const
{
    return m_cached_paint_data->bytes_for(phase);
}

void Paintable::invalidate_paint_cache() const
{
    if (!m_cached_paint_data)
        return;

    m_cached_paint_data->for_each_present_phase([&](PaintPhase, ReadonlyBytes command_bytes) {
        release_cache_references_for_cached_commands(command_bytes);
    });
    m_cached_paint_data = nullptr;
}

void Paintable::set_cached_commands(PaintPhase phase, ByteBuffer const& commands) const
{
    if (!m_cached_paint_data)
        m_cached_paint_data = make<CachedPaintData>();

    if (m_cached_paint_data->has(phase))
        release_cache_references_for_cached_commands(m_cached_paint_data->bytes_for(phase));

    auto command_bytes = commands.span();
    acquire_cache_references_for_cached_commands(command_bytes);
    m_cached_paint_data->set(phase, command_bytes);
}

void Paintable::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_containing_line_box_data.clear();
    m_sticky_insets = nullptr;

    invalidate_absolute_geometry_cache(InvalidateDescendantGeometry::No);

    m_enclosing_scroll_frame_index = {};
    m_own_scroll_frame_index = {};
    m_accumulated_visual_context_index = VISUAL_VIEWPORT_NODE_INDEX;
    m_accumulated_visual_context_for_descendants_index = VISUAL_VIEWPORT_NODE_INDEX;
    m_fixed_background_visual_context = {};

    m_used_values_for_grid_template_columns = nullptr;
    m_used_values_for_grid_template_rows = nullptr;
    m_grid_layout_data = nullptr;
    m_flex_layout_data = nullptr;

    invalidate_paint_cache();

    invalidate_stacking_context();
}

CSSPixelPoint Paintable::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::Element>(dom_node().ptr()))
        return element->scroll_offset({});
    return {};
}

Optional<CSSPixelRect> Paintable::absolute_containing_line_box_rect() const
{
    if (!m_containing_line_box_data.has_value())
        return {};

    auto rect = m_containing_line_box_data->rect;
    if (auto containing_block = this->containing_block())
        rect.translate_by(containing_block->absolute_position());
    return rect;
}

Paintable::ScrollHandled Paintable::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::Element>(*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<DOM::EventTarget> 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;
}

Paintable::ScrollHandled Paintable::scroll_by(double delta_x, double delta_y)
{
    return set_scroll_offset(scroll_offset().translated(CSSPixels::nearest_value_for(delta_x), CSSPixels::nearest_value_for(delta_y)));
}

void Paintable::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 Paintable::set_offset(CSSPixelPoint offset)
{
    if (m_offset == offset)
        return;

    m_offset = offset;
    invalidate_absolute_geometry_cache(InvalidateDescendantGeometry::Yes);
}

void Paintable::set_content_size(CSSPixelSize size)
{
    auto old_size = m_content_size;
    m_content_size = size;
    invalidate_absolute_geometry_cache(InvalidateDescendantGeometry::No);
    if (auto layout_box = as_if<Layout::Box>(layout_node()))
        layout_box->did_set_content_size();
    invalidate_descendant_styles_for_container_query_size_change(*this, old_size, size);
}

void Paintable::invalidate_absolute_geometry_cache(InvalidateDescendantGeometry invalidate_descendants)
{
    m_absolute_rect.clear();
    m_absolute_padding_box_rect.clear();
    m_absolute_border_box_rect.clear();

    if (invalidate_descendants == InvalidateDescendantGeometry::No)
        return;

    for_each_child_of_type<Paintable>([](auto& child) {
        child.invalidate_absolute_geometry_cache(InvalidateDescendantGeometry::Yes);
        return IterationDecision::Continue;
    });
}

void Paintable::set_fragmentation_state(FragmentationState fragmentation_state)
{
    switch (fragmentation_state) {
    case FragmentationState::Unfragmented:
        break;
    case FragmentationState::HorizontalStart:
        m_fragment_right_edge_away = true;
        break;
    case FragmentationState::HorizontalMiddle:
        m_fragment_left_edge_away = true;
        m_fragment_right_edge_away = true;
        break;
    case FragmentationState::HorizontalEnd:
        m_fragment_left_edge_away = true;
        break;
    case FragmentationState::VerticalStart:
        m_fragment_bottom_edge_away = true;
        break;
    case FragmentationState::VerticalMiddle:
        m_fragment_top_edge_away = true;
        m_fragment_bottom_edge_away = true;
        break;
    case FragmentationState::VerticalEnd:
        m_fragment_top_edge_away = true;
        break;
    }
}

CSSPixelPoint Paintable::offset() const
{
    return m_offset;
}

CSSPixelRect Paintable::compute_absolute_rect() const
{
    CSSPixelRect rect { offset(), content_size() };
    for (auto block = containing_block(); block; block = block->containing_block())
        rect.translate_by(block->offset());
    return rect;
}

CSSPixelRect Paintable::absolute_rect() const
{
    if (!m_absolute_rect.has_value())
        m_absolute_rect = compute_absolute_rect();
    return *m_absolute_rect;
}

CSSPixelRect Paintable::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<CSSPixelRect> Paintable::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 Paintable::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 = m_fragment_top_edge_away ? 0 : box_model().border.top;
        auto border_bottom = m_fragment_bottom_edge_away ? 0 : box_model().border.bottom;
        auto border_left = m_fragment_left_edge_away ? 0 : box_model().border.left;
        auto border_right = m_fragment_right_edge_away ? 0 : box_model().border.right;
        if (use_collapsing_borders_model) {
            border_top = round(border_top / 2);
            border_bottom = round(border_bottom / 2);
            border_left = round(border_left / 2);
            border_right = round(border_right / 2);
        }
        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 Paintable::overflow_clip_edge_rect() const
{
    // https://drafts.csswg.org/css-overflow-4/#overflow-clip-margin
    // Values are defined as follows:
    // '<visual-box>'
    //     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.
    auto const& overflow_clip_margin = computed_values().overflow_clip_margin();
    auto resolve_box_edge = [&](CSS::OverflowClipMarginSide const& side) -> CSSPixelRect {
        auto box = side.visual_box;
        if (!box.has_value()) {
            if (layout_node().is_replaced_box())
                box = CSS::BackgroundBox::ContentBox;
            else
                box = CSS::BackgroundBox::PaddingBox;
        }
        switch (*box) {
        case CSS::BackgroundBox::ContentBox:
            return absolute_rect();
        case CSS::BackgroundBox::BorderBox:
            return absolute_border_box_rect();
        case CSS::BackgroundBox::PaddingBox:
        default:
            return absolute_padding_box_rect();
        }
    };

    auto overflow_clip_edge = resolve_box_edge(overflow_clip_margin.top);

    // '<length [0,∞]>'
    //     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(
        overflow_clip_margin.top.offset,
        overflow_clip_margin.right.offset,
        overflow_clip_margin.bottom.offset,
        overflow_clip_margin.left.offset);
    return overflow_clip_edge;
}

template<typename Callable>
static CSSPixelRect united_rect_for_all_paintables(Paintable const& start, Callable get_rect)
{
    // Inline nodes can have one paintable per line, so combine all paintables for this layout node.
    Optional<CSSPixelRect> result;

    for (auto const& paintable : start.layout_node().paintables()) {
        auto paintable_border_box_rect = get_rect(*paintable);
        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 Paintable::absolute_united_border_box_rect() const
{
    return united_rect_for_all_paintables(*this, [](auto const& paintable_box) {
        return paintable_box.absolute_border_box_rect();
    });
}

CSSPixelRect Paintable::absolute_united_content_rect() const
{
    return united_rect_for_all_paintables(*this, [](auto const& paintable_box) {
        return paintable_box.absolute_rect();
    });
}

CSSPixelRect Paintable::absolute_united_padding_box_rect() const
{
    return united_rect_for_all_paintables(*this, [](auto const& paintable_box) {
        return paintable_box.absolute_padding_box_rect();
    });
}

Optional<CSSPixelRect> Paintable::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 {};
}

RefPtr<Scrollbar> Paintable::scrollbar(ScrollDirection direction) const
{
    return direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar;
}

NonnullRefPtr<Scrollbar> Paintable::ensure_scrollbar(ScrollDirection direction)
{
    auto& slot = direction == ScrollDirection::Horizontal ? m_horizontal_scrollbar : m_vertical_scrollbar;
    if (!slot)
        slot = Scrollbar::create(const_cast<Paintable&>(*this), direction);
    return *slot;
}

static CSS::Overflow overflow_value_applied_to_viewport_for_wheel_scrolling(DOM::Document const& document, Paintable::ScrollDirection direction)
{
    auto overflow_for_direction = [direction](CSS::ComputedProperties const& computed_properties) {
        return direction == Paintable::ScrollDirection::Horizontal
            ? computed_properties.overflow_x()
            : computed_properties.overflow_y();
    };

    auto* root_element = document.document_element();
    if (!root_element || !root_element->computed_properties())
        return CSS::Overflow::Auto;

    auto* overflow_origin_element = root_element;
    if (root_element->is_html_html_element() && root_element->computed_properties()->contain().is_empty()) {
        auto root_overflow_x = root_element->computed_properties()->overflow_x();
        auto root_overflow_y = root_element->computed_properties()->overflow_y();
        if (root_overflow_x == CSS::Overflow::Visible && root_overflow_y == CSS::Overflow::Visible) {
            auto* body_element = root_element->first_child_of_type<HTML::HTMLBodyElement>();
            if (body_element && body_element->computed_properties() && body_element->computed_properties()->contain().is_empty())
                overflow_origin_element = body_element;
        }
    }

    auto overflow = overflow_for_direction(*overflow_origin_element->computed_properties());
    if (overflow == CSS::Overflow::Visible)
        return CSS::Overflow::Auto;
    if (overflow == CSS::Overflow::Clip)
        return CSS::Overflow::Hidden;
    return overflow;
}

bool Paintable::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();
    if (is_viewport_paintable())
        overflow = overflow_value_applied_to_viewport_for_wheel_scrolling(document(), direction);

    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);

    if (overflow == CSS::Overflow::Auto || overflow == CSS::Overflow::Scroll)
        return scrollable_overflow_size > scrollport_size;

    return false;
}

bool Paintable::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 Paintable::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<SVGPaintable>(*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 Paintable::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<CSSPixelRect> Paintable::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<Paintable::ScrollbarData> Paintable::compute_scrollbar_data(ScrollDirection direction, ChromeMetrics const& metrics, ScrollStateSnapshot const* scroll_state_snapshot, ScrollbarSizing scrollbar_sizing) 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 {};

    auto scrollable_overflow_rect = this->scrollable_overflow_rect();
    if (!scrollable_overflow_rect.has_value())
        return {};

    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 = [&] {
        switch (scrollbar_sizing) {
        case ScrollbarSizing::Current:
            return scrollbar && scrollbar->is_enlarged();
        case ScrollbarSizing::Regular:
            return false;
        case ScrollbarSizing::Enlarged:
            return true;
        }
        VERIFY_NOT_REACHED();
    }();
    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<float>(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 Paintable::record_async_scrolling_metadata(DisplayListRecordingContext& context) const
{
    if (!context.is_recording_async_scrolling_metadata())
        return;

    auto device_pixels_per_css_pixel = context.device_pixels_per_css_pixel();
    auto& recorder = context.display_list_recorder();

    record_wheel_hit_test_target(*this, context);

    record_blocking_wheel_event_region(*this, context);

    if (is_nested_navigable_container(*this)) {
        record_main_thread_wheel_event_region(*this, context);
    } else if (own_scroll_frame_index().value() && could_be_scrolled_by_wheel_event()) {
        record_scroll_node(*this, context);
    }
    record_viewport_scrollbar_state(*this, context);

    auto const& scroll_state = context.async_scrolling_scroll_state();
    auto sticky_frame_index = enclosing_scroll_frame_index();
    if (is_sticky_position() && sticky_frame_index.value()) {
        auto const& frame = scroll_state.frame_at(sticky_frame_index);
        if (frame.is_sticky() && frame.has_sticky_constraints()) {
            auto const& constraints = frame.sticky_constraints();
            auto const& insets = constraints.insets;
            recorder.compositor_sticky_area({
                .document_id = context.async_scrolling_document_id(),
                .scroll_frame_index = sticky_frame_index,
                .parent_scroll_frame_index = frame.parent_index(),
                .nearest_scrolling_ancestor_index = scroll_state.nearest_scrolling_ancestor(sticky_frame_index),
                .position_relative_to_scroll_ancestor = css_point_to_device_point(constraints.position_relative_to_scroll_ancestor, device_pixels_per_css_pixel),
                .border_box_size = css_size_to_device_size(constraints.border_box_size, device_pixels_per_css_pixel),
                .scrollport_size = css_size_to_device_size(constraints.scrollport_size, device_pixels_per_css_pixel),
                .containing_block_region = css_rect_to_device_rect(constraints.containing_block_region, device_pixels_per_css_pixel),
                .needs_parent_offset_adjustment = constraints.needs_parent_offset_adjustment,
                .inset_top = css_inset_to_device_inset(insets.top, device_pixels_per_css_pixel),
                .inset_right = css_inset_to_device_inset(insets.right, device_pixels_per_css_pixel),
                .inset_bottom = css_inset_to_device_inset(insets.bottom, device_pixels_per_css_pixel),
                .inset_left = css_inset_to_device_inset(insets.left, device_pixels_per_css_pixel),
            });
        }
    }
}

void Paintable::record_hit_test_items(DisplayListRecordingContext& context, PaintPhase phase) const
{
    if (phase != PaintPhase::Background && phase != PaintPhase::Overlay)
        return;

    auto* hit_test_display_list = context.hit_test_display_list();
    if (!hit_test_display_list)
        return;

    auto const is_visible = computed_values().visibility() == CSS::Visibility::Visible;
    if (!is_visible || !visible_for_hit_testing())
        return;

    if (phase == PaintPhase::Background) {
        Paintable* target = const_cast<Paintable*>(this);
        if (layout_node().is_anonymous()
            && !layout_node().is_generated_for_pseudo_element()
            && !layout_node().is_list_item_marker_box()) {
            return;
        }

        hit_test_display_list->append_box(*this, *target, absolute_border_box_rect(), accumulated_visual_context_index(), border_radii_data());
        return;
    }

    if (phase != PaintPhase::Overlay)
        return;

    auto& box = const_cast<Paintable&>(*this);
    if (has_resizer())
        hit_test_display_list->append_chrome_widget(*this, box.ensure_resize_handle(), accumulated_visual_context_index());

    if (could_be_scrolled_by_wheel_event(ScrollDirection::Horizontal))
        hit_test_display_list->append_chrome_widget(*this, box.ensure_scrollbar(ScrollDirection::Horizontal), accumulated_visual_context_index());
    if (could_be_scrolled_by_wheel_event(ScrollDirection::Vertical))
        hit_test_display_list->append_chrome_widget(*this, box.ensure_scrollbar(ScrollDirection::Vertical), accumulated_visual_context_index());
}

void Paintable::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 && !document().page().async_scrolling_enabled()) || !is_viewport_paintable())
            && computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) {
            auto scrollbar_colors = scrollbar_colors_for_paint(*this);

            for (auto direction : { ScrollDirection::Vertical, ScrollDirection::Horizontal }) {
                auto scrollbar_data = compute_scrollbar_data(direction, metrics);
                if (!scrollbar_data.has_value())
                    continue;
                auto gutter_rect = context.rounded_device_rect(scrollbar_data->gutter_rect).to_type<int>();
                context.display_list_recorder().paint_scrollbar(
                    m_own_scroll_frame_index,
                    gutter_rect,
                    context.rounded_device_rect(scrollbar_data->thumb_rect).to_type<int>(),
                    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<int>();
            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 Paintable::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<int>();

    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<float>(device_origin.x());
    auto y = static_cast<float>(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 Paintable::paint_inspector_overlay_internal(DisplayListRecordingContext& context) const
{
    auto content_rect = absolute_united_content_rect();
    auto margin_rect = united_rect_for_all_paintables(*this, [](Paintable 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<int>();
        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);

    Utf16StringBuilder builder;
    builder.appendff("{}", debug_description());
    builder.appendff(" {}x{} @ {},{}", border_rect.width(), border_rect.height(), border_rect.x(), border_rect.y());
    auto size_text = builder.to_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<int>();
    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 Paintable::paint_grid_inspector_overlay(DisplayListRecordingContext& context, GridInspectorOverlayOptions const& options) const
{
    if (!m_grid_layout_data)
        return;

    paint_with_inspector_overlay_context(context, [&] {
        auto content_rect = absolute_united_content_rect();
        auto const origin = content_rect.location();
        auto const viewport_rect = document().viewport_rect();
        auto const& color = options.color;
        auto font = Platform::FontPlugin::the().default_font(10);
        auto label_font = font->with_size(font->point_size() * context.device_pixels_per_css_pixel());
        auto label_height = CSSPixels::nearest_value_for(font->pixel_size()) + 4;
        auto label_padding = CSSPixels(4);

        auto paint_rect = [&](CSSPixelRect const& rect, Gfx::Color rect_color) {
            context.display_list_recorder().fill_rect(context.enclosing_device_rect(rect).to_type<int>(), rect_color);
        };

        auto paint_label = [&](CSSPixelPoint top_left, Utf16String const& text) {
            auto label_width = CSSPixels::nearest_value_for(font->width(text)) + label_padding * 2;
            auto label_rect = CSSPixelRect { top_left.x(), top_left.y(), label_width, label_height };
            auto label_device_rect = context.enclosing_device_rect(label_rect).to_type<int>();
            auto label_background = color.with_alpha(235);
            context.display_list_recorder().fill_rect(label_device_rect, label_background);
            context.display_list_recorder().draw_rect(label_device_rect, color.with_alpha(255));
            context.display_list_recorder().draw_text(label_device_rect, text, label_font, Gfx::TextAlignment::Center, label_background.suggested_foreground_color());
        };

        auto paint_centered_label = [&](CSSPixelRect const& rect, Utf16String const& text) {
            auto label_width = CSSPixels::nearest_value_for(font->width(text)) + label_padding * 2;
            paint_label({ rect.center().x() - label_width / 2, rect.center().y() - label_height / 2 }, text);
        };

        auto line_start_for_number = [](Layout::GridLayoutDimension const& dimension, u32 number) -> Optional<CSSPixels> {
            for (auto const& line : dimension.lines) {
                if (line.number == number)
                    return line.start;
            }
            return {};
        };

        auto line_number_label = [](Layout::GridLayoutLine const& line) {
            if (line.negative_number < 0)
                return MUST(String::formatted("{} / {}", line.number, line.negative_number));
            return MUST(String::formatted("{}", line.number));
        };

        auto track_size_label = [](Layout::GridLayoutTrack const& track) {
            return MUST(String::formatted("{:.2f}px", max(0.0, track.breadth.to_double())));
        };

        auto line_color = color.with_alpha(220);
        auto gap_color = color.with_alpha(45);
        auto line_thickness = CSSPixels(1);

        for (auto const& fragment : m_grid_layout_data->fragments) {
            for (auto const& column_line : fragment.columns.lines) {
                auto x = origin.x() + column_line.start;
                auto line_top = options.show_infinite_lines ? viewport_rect.y() : content_rect.y();
                auto line_height = options.show_infinite_lines ? viewport_rect.height() : content_rect.height();

                if (column_line.breadth > 0)
                    paint_rect({ x, line_top, column_line.breadth, line_height }, gap_color);

                paint_rect({ x, line_top, line_thickness, line_height }, line_color);

                if (options.show_line_numbers)
                    paint_label({ x + 2, line_top + 2 }, Utf16String::from_utf8(line_number_label(column_line)));
            }

            for (auto const& row_line : fragment.rows.lines) {
                auto y = origin.y() + row_line.start;
                auto line_left = options.show_infinite_lines ? viewport_rect.x() : content_rect.x();
                auto line_width = options.show_infinite_lines ? viewport_rect.width() : content_rect.width();

                if (row_line.breadth > 0)
                    paint_rect({ line_left, y, line_width, row_line.breadth }, gap_color);

                paint_rect({ line_left, y, line_width, line_thickness }, line_color);

                if (options.show_line_numbers)
                    paint_label({ line_left + 2, y + 2 }, Utf16String::from_utf8(line_number_label(row_line)));
            }

            if (options.show_track_sizes) {
                for (auto const& column_track : fragment.columns.tracks) {
                    auto track_rect = CSSPixelRect {
                        origin.x() + column_track.start,
                        content_rect.y(),
                        column_track.breadth,
                        min(content_rect.height(), label_height + 4),
                    };
                    paint_centered_label(track_rect, Utf16String::from_utf8(track_size_label(column_track)));
                }

                for (auto const& row_track : fragment.rows.tracks) {
                    auto track_rect = CSSPixelRect {
                        content_rect.x(),
                        origin.y() + row_track.start,
                        min(content_rect.width(), CSSPixels(72)),
                        row_track.breadth,
                    };
                    paint_centered_label(track_rect, Utf16String::from_utf8(track_size_label(row_track)));
                }
            }

            if (options.show_area_names) {
                for (auto const& area : fragment.areas) {
                    auto column_start = line_start_for_number(fragment.columns, area.column_start);
                    auto column_end = line_start_for_number(fragment.columns, area.column_end);
                    auto row_start = line_start_for_number(fragment.rows, area.row_start);
                    auto row_end = line_start_for_number(fragment.rows, area.row_end);
                    if (!column_start.has_value() || !column_end.has_value() || !row_start.has_value() || !row_end.has_value())
                        continue;

                    auto area_rect = CSSPixelRect {
                        origin.x() + column_start.value(),
                        origin.y() + row_start.value(),
                        column_end.value() - column_start.value(),
                        row_end.value() - row_start.value(),
                    };
                    paint_rect(area_rect, color.with_alpha(24));

                    auto visible_area_rect = area_rect.intersected(viewport_rect);
                    if (!visible_area_rect.is_empty())
                        paint_centered_label(visible_area_rect, Utf16String::from_utf8(area.name));
                }
            }
        }
    });
}

void Paintable::paint_flexbox_inspector_overlay(DisplayListRecordingContext& context, FlexboxInspectorOverlayOptions const& options) const
{
    if (!m_flex_layout_data)
        return;

    paint_with_inspector_overlay_context(context, [&] {
        auto content_rect = absolute_united_content_rect();
        auto const origin = content_rect.location();
        auto const viewport_rect = document().viewport_rect();
        auto const& color = options.color;
        auto line_color = color.with_alpha(220);
        auto container_fill_color = color.with_alpha(28);
        auto line_fill_color = color.with_alpha(18);
        auto item_fill_color = color.with_alpha(32);
        auto line_thickness = CSSPixels(1);
        auto main_axis_is_horizontal = m_flex_layout_data->flex_direction == CSS::FlexDirection::Row
            || m_flex_layout_data->flex_direction == CSS::FlexDirection::RowReverse;

        auto paint_rect = [&](CSSPixelRect const& rect, Gfx::Color rect_color) {
            auto visible_rect = rect.intersected(viewport_rect);
            if (visible_rect.is_empty())
                return;
            context.display_list_recorder().fill_rect(context.enclosing_device_rect(visible_rect).to_type<int>(), rect_color);
        };

        auto paint_outline = [&](CSSPixelRect const& rect, Gfx::Color rect_color) {
            auto visible_rect = rect.intersected(viewport_rect);
            if (visible_rect.is_empty())
                return;
            context.display_list_recorder().draw_rect(context.enclosing_device_rect(visible_rect).to_type<int>(), rect_color);
        };

        paint_rect(content_rect, container_fill_color);
        paint_outline(content_rect, line_color);

        for (auto const& line : m_flex_layout_data->lines) {
            auto line_rect = main_axis_is_horizontal
                ? CSSPixelRect { content_rect.x(), origin.y() + line.cross_start, content_rect.width(), line.cross_size }
                : CSSPixelRect { origin.x() + line.cross_start, content_rect.y(), line.cross_size, content_rect.height() };

            paint_rect(line_rect, line_fill_color);
            paint_outline(line_rect, line_color);

            for (auto const& item : line.items) {
                auto item_rect = item.rect.translated(origin);
                paint_rect(item_rect, item_fill_color);
                paint_outline(item_rect, line_color);
            }
        }

        // Repaint the container border last so adjacent flex lines and items do not obscure it.
        paint_outline(content_rect, line_color);

        if (main_axis_is_horizontal) {
            for (auto const& line : m_flex_layout_data->lines) {
                auto y = origin.y() + line.cross_start;
                paint_rect({ content_rect.x(), y, content_rect.width(), line_thickness }, line_color);
                paint_rect({ content_rect.x(), y + line.cross_size, content_rect.width(), line_thickness }, line_color);
            }
        } else {
            for (auto const& line : m_flex_layout_data->lines) {
                auto x = origin.x() + line.cross_start;
                paint_rect({ x, content_rect.y(), line_thickness, content_rect.height() }, line_color);
                paint_rect({ x + line.cross_size, content_rect.y(), line_thickness, content_rect.height() }, line_color);
            }
        }
    });
}

void Paintable::set_stacking_context(NonnullRefPtr<StackingContext> stacking_context)
{
    m_stacking_context = move(stacking_context);
}

RefPtr<StackingContext> Paintable::stacking_context()
{
    return m_stacking_context;
}

RefPtr<StackingContext const> Paintable::stacking_context() const
{
    return m_stacking_context;
}

void Paintable::invalidate_stacking_context()
{
    m_stacking_context = nullptr;
}

Optional<int> Paintable::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 Paintable::remove_element_kind_from_borders_data(Paintable::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,
    };
}

enum class BorderImageTrack {
    Start,
    Center,
    End,
};

struct BorderImageAxis {
    CSSPixels start;
    CSSPixels center_start;
    CSSPixels center_end;
    CSSPixels end;

    CSSPixels track_start(BorderImageTrack track) const
    {
        switch (track) {
        case BorderImageTrack::Start:
            return start;
        case BorderImageTrack::Center:
            return center_start;
        case BorderImageTrack::End:
            return center_end;
        }
        VERIFY_NOT_REACHED();
    }

    CSSPixels track_end(BorderImageTrack track) const
    {
        switch (track) {
        case BorderImageTrack::Start:
            return center_start;
        case BorderImageTrack::Center:
            return center_end;
        case BorderImageTrack::End:
            return end;
        }
        VERIFY_NOT_REACHED();
    }

    CSSPixels track_size(BorderImageTrack track) const
    {
        return track_end(track) - track_start(track);
    }
};

struct BorderImageGrid {
    BorderImageAxis columns;
    BorderImageAxis rows;
};

struct ResolvedBorderImageGeometry {
    BorderImageGrid source;
    BorderImageGrid destination;
    PixelBox source_slices;
    PixelBox widths;
};

static Gfx::FloatRect source_rect_for_track(BorderImageGrid const& grid, BorderImageTrack column, BorderImageTrack row)
{
    return {
        grid.columns.track_start(column).to_float(),
        grid.rows.track_start(row).to_float(),
        grid.columns.track_size(column).to_float(),
        grid.rows.track_size(row).to_float(),
    };
}

static CSSPixelRect destination_rect_for_track(BorderImageGrid const& grid, BorderImageTrack column, BorderImageTrack row)
{
    return {
        grid.columns.track_start(column),
        grid.rows.track_start(row),
        grid.columns.track_size(column),
        grid.rows.track_size(row),
    };
}

static ResolvedBorderImageGeometry resolve_border_image_geometry(CSS::BorderImageData const& border_image, Gfx::IntSize source_size, CSSPixelRect border_box_rect, PixelBox border_width)
{
    // https://drafts.csswg.org/css-backgrounds-3/#border-image-slice
    auto resolve_slice = [](CSS::StyleValue const& value, CSSPixels reference_length) {
        if (value.is_percentage())
            return CSSPixels::nearest_value_for(reference_length * value.as_percentage().percentage().as_fraction());
        return CSSPixels(value.as_number().number());
    };

    auto resolve_outset = [](CSS::StyleValue const& value, CSSPixels border_width) {
        if (value.is_number())
            return CSSPixels::nearest_value_for(border_width * value.as_number().number());
        return CSS::Length::from_style_value(value, {}).absolute_length_to_px();
    };

    // https://drafts.csswg.org/css-backgrounds-3/#border-image-width
    auto resolve_width = [](CSS::StyleValue const& value, CSSPixels border_width, CSSPixels reference_length, CSSPixels auto_width) {
        if (value.is_number())
            return CSSPixels::nearest_value_for(border_width * value.as_number().number());
        if (value.is_percentage())
            return CSSPixels::nearest_value_for(reference_length * value.as_percentage().percentage().as_fraction());
        if (value.to_keyword() == CSS::Keyword::Auto)
            return auto_width;
        return CSS::Length::from_style_value(value, {}).absolute_length_to_px();
    };

    auto shrink_opposite_sides_to_fit = [](CSSPixels& first, CSSPixels& second, CSSPixels available) {
        auto total = first + second;
        if (total <= available)
            return;
        auto factor = available / total;
        first = first * factor;
        second = second * factor;
    };

    PixelBox source_slices {
        .top = resolve_slice(border_image.slice.top, source_size.height()),
        .right = resolve_slice(border_image.slice.right, source_size.width()),
        .bottom = resolve_slice(border_image.slice.bottom, source_size.height()),
        .left = resolve_slice(border_image.slice.left, source_size.width()),
    };
    shrink_opposite_sides_to_fit(source_slices.left, source_slices.right, source_size.width());
    shrink_opposite_sides_to_fit(source_slices.top, source_slices.bottom, source_size.height());

    auto const& outset = border_image.outset;
    PixelBox resolved_outset {
        .top = resolve_outset(outset.top, border_width.top),
        .right = resolve_outset(outset.right, border_width.right),
        .bottom = resolve_outset(outset.bottom, border_width.bottom),
        .left = resolve_outset(outset.left, border_width.left),
    };

    auto border_image_rect = border_box_rect;
    border_image_rect.inflate(resolved_outset.top, resolved_outset.right, resolved_outset.bottom, resolved_outset.left);

    // https://drafts.csswg.org/css-backgrounds-3/#border-image-process
    auto const& border_image_width = border_image.width;
    PixelBox widths {
        .top = resolve_width(border_image_width.top, border_width.top, border_image_rect.height(), source_slices.top),
        .right = resolve_width(border_image_width.right, border_width.right, border_image_rect.width(), source_slices.right),
        .bottom = resolve_width(border_image_width.bottom, border_width.bottom, border_image_rect.height(), source_slices.bottom),
        .left = resolve_width(border_image_width.left, border_width.left, border_image_rect.width(), source_slices.left),
    };
    shrink_opposite_sides_to_fit(widths.left, widths.right, border_image_rect.width());
    shrink_opposite_sides_to_fit(widths.top, widths.bottom, border_image_rect.height());

    return {
        .source = {
            .columns = { 0, source_slices.left, CSSPixels(source_size.width()) - source_slices.right, CSSPixels(source_size.width()) },
            .rows = { 0, source_slices.top, CSSPixels(source_size.height()) - source_slices.bottom, CSSPixels(source_size.height()) },
        },
        .destination = {
            .columns = { border_image_rect.left(), border_image_rect.left() + widths.left, border_image_rect.right() - widths.right, border_image_rect.right() },
            .rows = { border_image_rect.top(), border_image_rect.top() + widths.top, border_image_rect.bottom() - widths.bottom, border_image_rect.bottom() },
        },
        .source_slices = source_slices,
        .widths = widths,
    };
}

static Gfx::FloatSize rounded_repeated_border_image_tile_size(Gfx::FloatSize tile_size, Gfx::IntRect const& dest_rect, CSS::BorderImageRepeat repeat_x, CSS::BorderImageRepeat repeat_y)
{
    if (repeat_x == CSS::BorderImageRepeat::Stretch) {
        tile_size.set_width(dest_rect.width());
    } else if (repeat_x == CSS::BorderImageRepeat::Round && tile_size.width() > 0) {
        auto tile_count = max(1, round_to<int>(static_cast<double>(dest_rect.width()) / static_cast<double>(tile_size.width())));
        tile_size.set_width(static_cast<float>(dest_rect.width()) / tile_count);
    }

    if (repeat_y == CSS::BorderImageRepeat::Stretch) {
        tile_size.set_height(dest_rect.height());
    } else if (repeat_y == CSS::BorderImageRepeat::Round && tile_size.height() > 0) {
        auto tile_count = max(1, round_to<int>(static_cast<double>(dest_rect.height()) / static_cast<double>(tile_size.height())));
        tile_size.set_height(static_cast<float>(dest_rect.height()) / tile_count);
    }

    return tile_size;
}

static CSSPixels scale_source_length_to_destination(CSSPixels source_length, CSSPixels source_reference, CSSPixels destination_reference)
{
    if (source_reference <= 0)
        return source_length;
    auto scaled_length = source_length * (destination_reference / source_reference);
    return max(CSSPixels::smallest_positive_value(), scaled_length);
}

static void paint_border_image_slice(DisplayListRecordingContext& context, Gfx::DecodedImageFrame const& source_frame, Gfx::FloatRect const& source_rect, Gfx::IntRect const& dest_rect, Gfx::FloatSize device_tile_size, CSS::ImageRendering image_rendering, CSS::BorderImageRepeat repeat_x, CSS::BorderImageRepeat repeat_y)
{
    if (source_rect.is_empty() || dest_rect.is_empty())
        return;

    device_tile_size = rounded_repeated_border_image_tile_size(device_tile_size, dest_rect, repeat_x, repeat_y);
    if (device_tile_size.is_empty())
        return;

    auto source_size = source_rect.size().to_rounded<int>();

    auto start_x = static_cast<float>(dest_rect.x());
    auto start_y = static_cast<float>(dest_rect.y());
    auto tile_step = device_tile_size;
    Optional<u32> tile_count_x;
    Optional<u32> tile_count_y;

    if (repeat_x == CSS::BorderImageRepeat::Stretch)
        tile_count_x = 1;
    if (repeat_y == CSS::BorderImageRepeat::Stretch)
        tile_count_y = 1;

    if (repeat_x == CSS::BorderImageRepeat::Space || repeat_y == CSS::BorderImageRepeat::Space) {
        auto whole_tiles_x = repeat_x == CSS::BorderImageRepeat::Space ? static_cast<int>(floor(static_cast<float>(dest_rect.width()) / device_tile_size.width())) : 1;
        auto whole_tiles_y = repeat_y == CSS::BorderImageRepeat::Space ? static_cast<int>(floor(static_cast<float>(dest_rect.height()) / device_tile_size.height())) : 1;
        if (whole_tiles_x <= 0 || whole_tiles_y <= 0)
            return;

        auto gap_x = repeat_x == CSS::BorderImageRepeat::Space ? (static_cast<float>(dest_rect.width()) - whole_tiles_x * device_tile_size.width()) / (whole_tiles_x + 1) : 0;
        auto gap_y = repeat_y == CSS::BorderImageRepeat::Space ? (static_cast<float>(dest_rect.height()) - whole_tiles_y * device_tile_size.height()) / (whole_tiles_y + 1) : 0;

        if (repeat_x == CSS::BorderImageRepeat::Space) {
            start_x = dest_rect.x() + gap_x;
            tile_step.set_width(device_tile_size.width() + gap_x);
            tile_count_x = static_cast<u32>(whole_tiles_x);
        } else {
            tile_count_x = 1;
        }

        if (repeat_y == CSS::BorderImageRepeat::Space) {
            start_y = dest_rect.y() + gap_y;
            tile_step.set_height(device_tile_size.height() + gap_y);
            tile_count_y = static_cast<u32>(whole_tiles_y);
        } else {
            tile_count_y = 1;
        }
    } else {
        if (repeat_x == CSS::BorderImageRepeat::Repeat) {
            start_x += (static_cast<float>(dest_rect.width()) - device_tile_size.width()) / 2;
            while (start_x > dest_rect.x())
                start_x -= device_tile_size.width();
        }

        if (repeat_y == CSS::BorderImageRepeat::Repeat) {
            start_y += (static_cast<float>(dest_rect.height()) - device_tile_size.height()) / 2;
            while (start_y > dest_rect.y())
                start_y -= device_tile_size.height();
        }
    }

    Gfx::IntSize tile_size_for_scaling {
        max(1, round_to<int>(device_tile_size.width())),
        max(1, round_to<int>(device_tile_size.height())),
    };
    auto scaling_mode = CSS::to_gfx_scaling_mode(image_rendering, source_size, tile_size_for_scaling);
    context.display_list_recorder().draw_tiled_decoded_image_frame({
        .tile_rect = { start_x, start_y, device_tile_size.width(), device_tile_size.height() },
        .clip_rect = dest_rect,
        .src_rect = source_rect,
        .tile_step = tile_step,
        .frame = source_frame,
        .scaling_mode = scaling_mode,
        .tile_count_x = tile_count_x,
        .tile_count_y = tile_count_y,
    });
}

static bool paint_border_image(DisplayListRecordingContext& context, Paintable const& paintable_box, BordersData const& borders_data)
{
    auto const& border_image = paintable_box.computed_values().border_image();
    if (!border_image.has_value())
        return false;

    // FIXME: Support all abstract image sources here. NodeWithStyle loads and observes gradients and
    // image-set(), but this painting path currently only handles raster images.
    if (!border_image->source->is_image())
        return false;

    auto const& image = border_image->source->as_image();
    auto const& document = paintable_box.document();
    if (!image.is_paintable(document))
        return false;

    auto maybe_frame = image.current_frame(document);
    if (!maybe_frame.has_value())
        return false;

    auto const& frame = *maybe_frame;
    PixelBox border_width {
        .top = borders_data.top.width,
        .right = borders_data.right.width,
        .bottom = borders_data.bottom.width,
        .left = borders_data.left.width,
    };
    auto geometry = resolve_border_image_geometry(*border_image, frame.size(), paintable_box.absolute_border_box_rect(), border_width);

    auto repeat_x = border_image->repeat_x;
    auto repeat_y = border_image->repeat_y;

    auto image_rendering = paintable_box.computed_values().image_rendering();

    // A tile keeps the source region's aspect ratio at the scale forced by the border thickness.
    auto natural_tile_size = [&](BorderImageTrack column, BorderImageTrack row, CSSPixelRect const& destination_rect) -> CSSPixelSize {
        auto source_width = geometry.source.columns.track_size(column);
        auto source_height = geometry.source.rows.track_size(row);
        auto tile_width = destination_rect.width();
        auto tile_height = destination_rect.height();

        if (column == BorderImageTrack::Center && row != BorderImageTrack::Center) {
            tile_width = scale_source_length_to_destination(source_width, source_height, destination_rect.height());
        } else if (row == BorderImageTrack::Center && column != BorderImageTrack::Center) {
            tile_height = scale_source_length_to_destination(source_height, source_width, destination_rect.width());
        } else if (column == BorderImageTrack::Center && row == BorderImageTrack::Center) {
            // The centre fill is scaled horizontally like the top edge and vertically like the left edge.
            tile_width = scale_source_length_to_destination(source_width, geometry.source_slices.top, geometry.widths.top);
            tile_height = scale_source_length_to_destination(source_height, geometry.source_slices.left, geometry.widths.left);
        }
        return { tile_width, tile_height };
    };

    // Only the centre track of an axis honours border-image-repeat; the edges always stretch.
    auto paint_piece = [&](BorderImageTrack column, BorderImageTrack row) {
        auto source_rect = source_rect_for_track(geometry.source, column, row);
        auto destination_rect = destination_rect_for_track(geometry.destination, column, row);
        auto destination_device_rect = context.rounded_device_rect(destination_rect).to_type<int>();
        auto tile_device_size = css_size_to_device_size(natural_tile_size(column, row, destination_rect), context.device_pixels_per_css_pixel());
        auto horizontal_repeat = column == BorderImageTrack::Center ? repeat_x : CSS::BorderImageRepeat::Stretch;
        auto vertical_repeat = row == BorderImageTrack::Center ? repeat_y : CSS::BorderImageRepeat::Stretch;
        paint_border_image_slice(context, frame, source_rect, destination_device_rect, tile_device_size, image_rendering, horizontal_repeat, vertical_repeat);
    };

    for (auto row : { BorderImageTrack::Start, BorderImageTrack::Center, BorderImageTrack::End }) {
        for (auto column : { BorderImageTrack::Start, BorderImageTrack::Center, BorderImageTrack::End }) {
            if (column == BorderImageTrack::Center && row == BorderImageTrack::Center && !border_image->fill)
                continue; // The centre is only painted when the 'fill' keyword is present.
            paint_piece(column, row);
        }
    }

    return true;
}

void Paintable::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 || m_fragment_top_edge_away ? CSS::BorderData() : computed_values().border_top(),
        .right = box_model().border.right == 0 || m_fragment_right_edge_away ? CSS::BorderData() : computed_values().border_right(),
        .bottom = box_model().border.bottom == 0 || m_fragment_bottom_edge_away ? CSS::BorderData() : computed_values().border_bottom(),
        .left = box_model().border.left == 0 || m_fragment_left_edge_away ? CSS::BorderData() : computed_values().border_left(),
    };
    if (paint_border_image(context, *this, borders_data))
        return;
    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 Paintable::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<int>(), border_radii_data.as_corners(context.device_pixel_converter()), *gfx_filter);
}

void Paintable::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<HTML::HTMLHtmlElement>(*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 Paintable::paint_box_shadow(DisplayListRecordingContext& context) const
{
    auto const& box_shadow_layers = computed_values().box_shadow();
    if (box_shadow_layers.is_empty())
        return;
    Vector<Painting::ShadowData> 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));
    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 Paintable::normalized_border_radii_data(ShrinkRadiiForBorders shrink) const
{
    auto border_radii_data = this->border_radii_data();
    if (shrink == ShrinkRadiiForBorders::Yes)
        border_radii_data.shrink(
            m_fragment_top_edge_away ? 0 : computed_values().border_top().width,
            m_fragment_right_edge_away ? 0 : computed_values().border_right().width,
            m_fragment_bottom_edge_away ? 0 : computed_values().border_bottom().width,
            m_fragment_left_edge_away ? 0 : computed_values().border_left().width);

    return border_radii_data;
}

Optional<CSSPixelPoint> Paintable::transform_point_to_local(CSSPixelPoint screen_position) const
{
    auto viewport_paintable = document().paintable();
    if (!viewport_paintable || !viewport_paintable->has_visual_context_tree())
        return screen_position;
    auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
    auto const& scroll_state = viewport_paintable->scroll_state_snapshot();
    auto const& visual_context_tree = viewport_paintable->visual_context_tree();
    auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_index, screen_position.to_type<float>() * pixel_ratio, scroll_state);
    if (!result.has_value())
        return {};
    return (*result / pixel_ratio).to_type<CSSPixels>();
}

Optional<CSSPixelPoint> Paintable::transform_point_to_local_for_descendants(CSSPixelPoint screen_position) const
{
    auto viewport_paintable = document().paintable();
    if (!viewport_paintable || !viewport_paintable->has_visual_context_tree())
        return screen_position;
    auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
    auto const& scroll_state = viewport_paintable->scroll_state_snapshot();
    auto const& visual_context_tree = viewport_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<float>() * pixel_ratio, scroll_state);
    if (!result.has_value())
        return {};
    return (*result / pixel_ratio).to_type<CSSPixels>();
}

CSSPixelRect Paintable::transform_rect_to_viewport(CSSPixelRect const& rect, AccumulatedVisualContextTree::IncludeVisualViewportTransform include_visual_viewport_transform) const
{
    auto viewport_paintable = document().paintable();
    if (!viewport_paintable || !viewport_paintable->has_visual_context_tree())
        return rect;
    auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
    auto const& scroll_state = viewport_paintable->scroll_state_snapshot();
    auto const& visual_context_tree = viewport_paintable->visual_context_tree();
    auto result = visual_context_tree.transform_rect_to_viewport(m_accumulated_visual_context_index, rect.to_type<float>() * pixel_ratio, scroll_state, include_visual_viewport_transform);
    return (result * (1.f / pixel_ratio)).to_type<CSSPixels>();
}

CSSPixelPoint Paintable::inverse_transform_point(CSSPixelPoint screen_position) const
{
    auto viewport_paintable = document().paintable();
    if (!viewport_paintable || !viewport_paintable->has_visual_context_tree())
        return screen_position;
    auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
    auto const& visual_context_tree = viewport_paintable->visual_context_tree();
    auto result = visual_context_tree.inverse_transform_point(m_accumulated_visual_context_index, screen_position.to_type<float>() * pixel_ratio);
    return (result / pixel_ratio).to_type<CSSPixels>();
}

CSSPixelPoint Paintable::transform_to_local_coordinates(CSSPixelPoint screen_position) const
{
    return transform_point_to_local(screen_position).value_or(screen_position);
}

bool Paintable::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 Paintable::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;
}

RefPtr<ResizeHandle> Paintable::resize_handle() const
{
    return m_resize_handle;
}

NonnullRefPtr<ResizeHandle> Paintable::ensure_resize_handle()
{
    if (!m_resize_handle)
        m_resize_handle = ResizeHandle::create(*this);
    return *m_resize_handle;
}

bool Paintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, double wheel_delta_x, double wheel_delta_y)
{
    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 (!can_scroll_horizontally)
        wheel_delta_x = 0;
    if (!can_scroll_vertically)
        wheel_delta_y = 0;

    // if none of the axes we scrolled with can be accepted by this element, don't handle scroll.
    if (wheel_delta_x == 0 && wheel_delta_y == 0)
        return false;

    auto scroll_handled = scroll_by(wheel_delta_x, wheel_delta_y);
    return scroll_handled == ScrollHandled::Yes;
}

bool Paintable::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);
}

void Paintable::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. <button>s.
        for_each_child_of_type<Paintable>([&](auto& child) {
            if (child.layout_node().is_anonymous())
                child.set_needs_repaint(should_invalidate_display_list);
            return IterationDecision::Continue;
        });
    }
    document().set_needs_repaint(Badge<Painting::Paintable> {}, should_invalidate_display_list);
}

// https://www.w3.org/TR/css-transforms-1/#reference-box
CSSPixelRect Paintable::transform_reference_box() const
{
    auto transform_box = computed_values().transform_box();
    // For SVG elements without associated CSS layout box, the used value for content-box is fill-box and for
    // border-box is stroke-box.
    // FIXME: This currently detects any SVG element except the <svg> one. Is that correct?
    //        And is it correct to use `else` below?
    if (is<Painting::SVGPaintable>(*this)) {
        switch (transform_box) {
        case CSS::TransformBox::ContentBox:
            transform_box = CSS::TransformBox::FillBox;
            break;
        case CSS::TransformBox::BorderBox:
            transform_box = CSS::TransformBox::StrokeBox;
            break;
        default:
            break;
        }
    }
    // For elements with associated CSS layout box, the used value for fill-box is content-box and for
    // stroke-box and view-box is border-box.
    else {
        switch (transform_box) {
        case CSS::TransformBox::FillBox:
            transform_box = CSS::TransformBox::ContentBox;
            break;
        case CSS::TransformBox::StrokeBox:
        case CSS::TransformBox::ViewBox:
            transform_box = CSS::TransformBox::BorderBox;
            break;
        default:
            break;
        }
    }

    switch (transform_box) {
    case CSS::TransformBox::ContentBox:
        // Uses the content box as reference box.
        // FIXME: The reference box of a table is the border box of its table wrapper box, not its table box.
        return absolute_rect();
    case CSS::TransformBox::BorderBox:
        // Uses the border box as reference box.
        // FIXME: The reference box of a table is the border box of its table wrapper box, not its table box.
        return absolute_border_box_rect();
    case CSS::TransformBox::FillBox:
        // Uses the object bounding box as reference box.
        // FIXME: For now we're using the content rect as an approximation.
        return absolute_rect();
    case CSS::TransformBox::StrokeBox:
        // Uses the stroke bounding box as reference box.
        // FIXME: For now we're using the border rect as an approximation.
        return absolute_border_box_rect();
    case CSS::TransformBox::ViewBox:
        // Uses the nearest SVG viewport as reference box.
        // FIXME: If a viewBox attribute is specified for the SVG viewport creating element:
        //  - The reference box is positioned at the origin of the coordinate system established by the viewBox attribute.
        //  - The dimension of the reference box is set to the width and height values of the viewBox attribute.
        auto svg_paintable = first_ancestor_of_type<Painting::SVGSVGPaintable>();
        if (!svg_paintable)
            return absolute_border_box_rect();
        return svg_paintable->absolute_rect();
    }
    VERIFY_NOT_REACHED();
}

BorderRadiiData Paintable::border_radii_data() const
{
    auto const& computed_values = this->computed_values();
    if (!computed_values.has_noninitial_border_radii())
        return {};
    CSSPixelRect const border_rect { 0, 0, border_box_width(), border_box_height() };
    auto border_top_left_radius = m_fragment_top_edge_away || m_fragment_left_edge_away ? CSS::BorderRadiusData {} : computed_values.border_top_left_radius();
    auto border_top_right_radius = m_fragment_top_edge_away || m_fragment_right_edge_away ? CSS::BorderRadiusData {} : computed_values.border_top_right_radius();
    auto border_bottom_right_radius = m_fragment_bottom_edge_away || m_fragment_right_edge_away ? CSS::BorderRadiusData {} : computed_values.border_bottom_right_radius();
    auto border_bottom_left_radius = m_fragment_bottom_edge_away || m_fragment_left_edge_away ? CSS::BorderRadiusData {} : computed_values.border_bottom_left_radius();
    return normalize_border_radii_data(border_rect, border_rect,
        border_top_left_radius, border_top_right_radius,
        border_bottom_right_radius, border_bottom_left_radius);
}

Optional<BordersData> Paintable::outline_data() const
{
    auto const& computed_values = this->computed_values();

    // The `auto` outline is the UA focus ring; like native controls, it is only shown while the window has focus.
    if (computed_values.outline_style() == CSS::OutlineStyle::Auto && (!navigable() || !navigable()->is_focused()))
        return {};

    return borders_data_for_outline(layout_node(), computed_values.outline_color(), computed_values.outline_style(), computed_values.outline_width());
}

CSSPixels Paintable::outline_offset() const
{
    return computed_values().outline_offset();
}

ScrollFrameIndex Paintable::nearest_scroll_frame_index() const
{
    if (is_fixed_position())
        return {};
    auto paintable = this->containing_block();
    while (paintable) {
        if (paintable->own_scroll_frame_index().value())
            return paintable->own_scroll_frame_index();
        // Sticky elements need to find a scroll container even through fixed-position ancestors,
        // because they must reference a scrollport for their sticky offset computation.
        if (paintable->is_fixed_position() && !is_sticky_position())
            return {};
        paintable = paintable->containing_block();
    }
    return {};
}

RefPtr<Paintable const> Paintable::nearest_scrollable_ancestor() const
{
    auto paintable = this->containing_block();
    while (paintable) {
        if (paintable->could_be_scrolled_by_wheel_event())
            return paintable;
        if (paintable->is_fixed_position())
            return nullptr;
        paintable = paintable->containing_block();
    }
    return nullptr;
}

Paintable::PhysicalResizeAxes Paintable::physical_resize_axes() const
{
    auto const& computed = computed_values();

    // https://drafts.csswg.org/css-ui/#resize
    if (computed.resize() == CSS::Resize::None)
        return {};

    // 4.1. ... The resize property applies to elements that are scroll containers. UAs may also apply it,
    // regardless of the value of the overflow property, to:
    // - Replaced elements representing images or videos, such as img, video, picture, svg, object, or canvas.
    // - The <iframe> element.
    if (computed.display().is_inline_outside() && computed.display().is_flow_inside())
        return {};

    bool horizontal_writing_mode = computed.writing_mode() == CSS::WritingMode::HorizontalTb;

    return {
        .horizontal = computed.overflow_x() != CSS::Overflow::Visible
            && computed.overflow_x() != CSS::Overflow::Clip
            && (computed.resize() == CSS::Resize::Both
                || computed.resize() == CSS::Resize::Horizontal
                || (computed.resize() == CSS::Resize::Inline && horizontal_writing_mode)
                || (computed.resize() == CSS::Resize::Block && !horizontal_writing_mode)),
        .vertical = computed.overflow_y() != CSS::Overflow::Visible
            && computed.overflow_y() != CSS::Overflow::Clip
            && (computed.resize() == CSS::Resize::Both
                || computed.resize() == CSS::Resize::Vertical
                || (computed.resize() == CSS::Resize::Inline && !horizontal_writing_mode)
                || (computed.resize() == CSS::Resize::Block && horizontal_writing_mode))
    };
}

}
