/*
 * Copyright (C) 2011 Google Inc. All rights reserved.
 * Copyright (C) 2019-2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "InspectorOverlay.h"

#include "AXObjectCache.h"
#include "AccessibilityObject.h"
#include "CSSGridAutoRepeatValue.h"
#include "CSSGridIntegerRepeatValue.h"
#include "CSSGridLineNamesValue.h"
#include "CSSStyleDeclaration.h"
#include "DOMCSSNamespace.h"
#include "DOMTokenList.h"
#include "ElementInlines.h"
#include "FloatLine.h"
#include "FloatPoint.h"
#include "FloatRoundedRect.h"
#include "FloatSize.h"
#include "FontCascade.h"
#include "FontCascadeDescription.h"
#include "GraphicsContext.h"
#include "GridArea.h"
#include "GridPositionsResolver.h"
#include "InspectorClient.h"
#include "InspectorController.h"
#include "InspectorDOMAgent.h"
#include "IntPoint.h"
#include "IntRect.h"
#include "IntSize.h"
#include "LocalFrame.h"
#include "LocalFrameView.h"
#include "LocalizedStrings.h"
#include "Node.h"
#include "NodeList.h"
#include "NodeRenderStyle.h"
#include "OrderIterator.h"
#include "Page.h"
#include "PseudoElement.h"
#include "RenderBoxInlines.h"
#include "RenderBoxModelObject.h"
#include "RenderElementInlines.h"
#include "RenderFlexibleBox.h"
#include "RenderGrid.h"
#include "RenderInline.h"
#include "RenderObject.h"
#include "Settings.h"
#include "StyleGridData.h"
#include "StyleResolver.h"
#include "TextDirection.h"
#include <wtf/MathExtras.h>
#include <wtf/text/MakeString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/unicode/CharacterNames.h>

namespace WebCore {

using namespace Inspector;

static constexpr float rulerSize = 15;
static constexpr float rulerLabelSize = 13;
static constexpr float rulerStepIncrement = 50;
static constexpr float rulerStepLength = 8;
static constexpr float rulerSubStepIncrement = 5;
static constexpr float rulerSubStepLength = 5;

enum class Flip : bool { No, Yes };

static bool isInNodeList(const Node& node, const NodeList& list)
{
    for (unsigned i = 0; i < list.length(); ++i) {
        if (&node == list.item(i))
            return true;
    }
    return false;
}

static void truncateWithEllipsis(String& string, size_t length)
{
    if (string.length() > length)
        string = makeString(StringView(string).left(length), horizontalEllipsis);
}

static FloatPoint localPointToRootPoint(const FrameView* view, const FloatPoint& point)
{
    return view->contentsToRootView(point);
}

static void contentsQuadToCoordinateSystem(const FrameView* mainView, const LocalFrameView* view, FloatQuad& quad, InspectorOverlay::CoordinateSystem coordinateSystem)
{
    quad.setP1(localPointToRootPoint(view, quad.p1()));
    quad.setP2(localPointToRootPoint(view, quad.p2()));
    quad.setP3(localPointToRootPoint(view, quad.p3()));
    quad.setP4(localPointToRootPoint(view, quad.p4()));

    if (coordinateSystem == InspectorOverlay::CoordinateSystem::View)
        quad += toIntSize(mainView->scrollPosition());
}

static Element* effectiveElementForNode(Node& node)
{
    if (!is<Element>(node) || !node.document().frame())
        return nullptr;

    Element* element = nullptr;
    if (is<PseudoElement>(node)) {
        if (Element* hostElement = downcast<PseudoElement>(node).hostElement())
            element = hostElement;
    } else
        element = &downcast<Element>(node);

    return element;
}

static void buildRendererHighlight(RenderObject* renderer, const InspectorOverlay::Highlight::Config& highlightConfig, InspectorOverlay::Highlight& highlight, InspectorOverlay::CoordinateSystem coordinateSystem)
{
    auto* containingFrame = renderer->document().frame();
    if (!containingFrame)
        return;

    highlight.setDataFromConfig(highlightConfig);
    auto* containingView = containingFrame->view();
    auto* mainView = containingFrame->page()->mainFrame().virtualView();

    // (Legacy)RenderSVGRoot should be highlighted through the isBox() code path, all other SVG elements should just dump their absoluteQuads().
    bool isSVGRenderer = renderer->node() && renderer->node()->isSVGElement() && !renderer->isRenderOrLegacyRenderSVGRoot();

    if (isSVGRenderer) {
        highlight.type = InspectorOverlay::Highlight::Type::Rects;
        renderer->absoluteQuads(highlight.quads);
        for (auto& quad : highlight.quads)
            contentsQuadToCoordinateSystem(mainView, containingView, quad, coordinateSystem);
    } else if (is<RenderBox>(*renderer) || is<RenderInline>(*renderer)) {
        LayoutRect contentBox;
        LayoutRect paddingBox;
        LayoutRect borderBox;
        LayoutRect marginBox;

        if (is<RenderBox>(*renderer)) {
            auto& renderBox = downcast<RenderBox>(*renderer);

            LayoutBoxExtent margins(renderBox.marginTop(), renderBox.marginRight(), renderBox.marginBottom(), renderBox.marginLeft());
            paddingBox = renderBox.clientBoxRect();
            contentBox = LayoutRect(paddingBox.x() + renderBox.paddingLeft(), paddingBox.y() + renderBox.paddingTop(),
                paddingBox.width() - renderBox.paddingLeft() - renderBox.paddingRight(), paddingBox.height() - renderBox.paddingTop() - renderBox.paddingBottom());
            borderBox = LayoutRect(paddingBox.x() - renderBox.borderLeft(), paddingBox.y() - renderBox.borderTop(),
                paddingBox.width() + renderBox.borderLeft() + renderBox.borderRight(), paddingBox.height() + renderBox.borderTop() + renderBox.borderBottom());
            marginBox = LayoutRect(borderBox.x() - margins.left(), borderBox.y() - margins.top(),
                borderBox.width() + margins.left() + margins.right(), borderBox.height() + margins.top() + margins.bottom());
        } else {
            auto& renderInline = downcast<RenderInline>(*renderer);

            // RenderInline's bounding box includes padding and borders, excludes margins.
            borderBox = renderInline.linesBoundingBox();
            paddingBox = LayoutRect(borderBox.x() + renderInline.borderLeft(), borderBox.y() + renderInline.borderTop(),
                borderBox.width() - renderInline.borderLeft() - renderInline.borderRight(), borderBox.height() - renderInline.borderTop() - renderInline.borderBottom());
            contentBox = LayoutRect(paddingBox.x() + renderInline.paddingLeft(), paddingBox.y() + renderInline.paddingTop(),
                paddingBox.width() - renderInline.paddingLeft() - renderInline.paddingRight(), paddingBox.height() - renderInline.paddingTop() - renderInline.paddingBottom());
            // Ignore marginTop and marginBottom for inlines.
            marginBox = LayoutRect(borderBox.x() - renderInline.marginLeft(), borderBox.y(),
                borderBox.width() + renderInline.horizontalMarginExtent(), borderBox.height());
        }

        FloatQuad absContentQuad = renderer->localToAbsoluteQuad(FloatRect(contentBox));
        FloatQuad absPaddingQuad = renderer->localToAbsoluteQuad(FloatRect(paddingBox));
        FloatQuad absBorderQuad = renderer->localToAbsoluteQuad(FloatRect(borderBox));
        FloatQuad absMarginQuad = renderer->localToAbsoluteQuad(FloatRect(marginBox));

        contentsQuadToCoordinateSystem(mainView, containingView, absContentQuad, coordinateSystem);
        contentsQuadToCoordinateSystem(mainView, containingView, absPaddingQuad, coordinateSystem);
        contentsQuadToCoordinateSystem(mainView, containingView, absBorderQuad, coordinateSystem);
        contentsQuadToCoordinateSystem(mainView, containingView, absMarginQuad, coordinateSystem);

        highlight.type = InspectorOverlay::Highlight::Type::Node;
        highlight.quads.append(absMarginQuad);
        highlight.quads.append(absBorderQuad);
        highlight.quads.append(absPaddingQuad);
        highlight.quads.append(absContentQuad);
    }
}

static void buildNodeHighlight(Node& node, const InspectorOverlay::Highlight::Config& highlightConfig, InspectorOverlay::Highlight& highlight, InspectorOverlay::CoordinateSystem coordinateSystem)
{
    auto* renderer = node.renderer();
    if (!renderer || renderer->isSkippedContent())
        return;

    buildRendererHighlight(renderer, highlightConfig, highlight, coordinateSystem);
}

static void buildQuadHighlight(const FloatQuad& quad, const InspectorOverlay::Highlight::Config& highlightConfig, InspectorOverlay::Highlight& highlight)
{
    highlight.setDataFromConfig(highlightConfig);
    highlight.type = InspectorOverlay::Highlight::Type::Rects;
    highlight.quads.append(quad);
}

static Path quadToPath(const FloatQuad& quad)
{
    Path path;
    path.moveTo(quad.p1());
    path.addLineTo(quad.p2());
    path.addLineTo(quad.p3());
    path.addLineTo(quad.p4());
    path.closeSubpath();

    return path;
}

static Path quadToPath(const FloatQuad& quad, InspectorOverlay::Highlight::Bounds& bounds)
{
    auto path = quadToPath(quad);
    bounds.unite(path.boundingRect());

    return path;
}

static void drawOutlinedQuadWithClip(GraphicsContext& context, const FloatQuad& quad, const FloatQuad& clipQuad, const Color& fillColor, InspectorOverlay::Highlight::Bounds& bounds)
{
    GraphicsContextStateSaver stateSaver(context);

    context.setFillColor(fillColor);
    context.setStrokeThickness(0);
    context.fillPath(quadToPath(quad, bounds));

    context.setCompositeOperation(CompositeOperator::DestinationOut);
    context.setFillColor(Color::red);
    context.fillPath(quadToPath(clipQuad, bounds));
}

static void drawOutlinedQuad(GraphicsContext& context, const FloatQuad& quad, const Color& fillColor, const Color& outlineColor, InspectorOverlay::Highlight::Bounds& bounds)
{
    Path path = quadToPath(quad, bounds);

    GraphicsContextStateSaver stateSaver(context);

    context.setStrokeThickness(2);

    context.clipPath(path);

    context.setFillColor(fillColor);
    context.fillPath(path);

    context.setStrokeColor(outlineColor);
    context.strokePath(path);
}

static void drawFragmentHighlight(GraphicsContext& context, Node& node, const InspectorOverlay::Highlight::Config& highlightConfig, InspectorOverlay::Highlight::Bounds& bounds)
{
    InspectorOverlay::Highlight highlight;
    buildNodeHighlight(node, highlightConfig, highlight, InspectorOverlay::CoordinateSystem::Document);

    FloatQuad marginQuad;
    FloatQuad borderQuad;
    FloatQuad paddingQuad;
    FloatQuad contentQuad;

    size_t size = highlight.quads.size();
    if (size >= 1)
        marginQuad = highlight.quads[0];
    if (size >= 2)
        borderQuad = highlight.quads[1];
    if (size >= 3)
        paddingQuad = highlight.quads[2];
    if (size >= 4)
        contentQuad = highlight.quads[3];

    if (!marginQuad.boundingBoxIsEmpty() && marginQuad != borderQuad && highlight.marginColor.isVisible())
        drawOutlinedQuadWithClip(context, marginQuad, borderQuad, highlight.marginColor, bounds);

    if (!borderQuad.boundingBoxIsEmpty() && borderQuad != paddingQuad && highlight.borderColor.isVisible())
        drawOutlinedQuadWithClip(context, borderQuad, paddingQuad, highlight.borderColor, bounds);

    if (!paddingQuad.boundingBoxIsEmpty() && paddingQuad != contentQuad && highlight.paddingColor.isVisible())
        drawOutlinedQuadWithClip(context, paddingQuad, contentQuad, highlight.paddingColor, bounds);

    if (!contentQuad.boundingBoxIsEmpty() && (highlight.contentColor.isVisible() || highlight.contentOutlineColor.isVisible()))
        drawOutlinedQuad(context, contentQuad, highlight.contentColor, highlight.contentOutlineColor, bounds);
}

static void drawShapeHighlight(GraphicsContext& context, Node& node, InspectorOverlay::Highlight::Bounds& bounds)
{
    auto* renderer = node.renderer();
    if (!renderer || renderer->isSkippedContent() || !is<RenderBox>(renderer))
        return;

    const ShapeOutsideInfo* shapeOutsideInfo = downcast<RenderBox>(renderer)->shapeOutsideInfo();
    if (!shapeOutsideInfo)
        return;

    auto* containingFrame = node.document().frame();
    if (!containingFrame)
        return;

    auto* containingView = containingFrame->view();
    auto* mainView = containingFrame->page()->mainFrame().virtualView();

    static constexpr auto shapeHighlightColor = SRGBA<uint8_t> { 96, 82, 127, 204 };

    Shape::DisplayPaths paths;
    shapeOutsideInfo->computedShape().buildDisplayPaths(paths);

    if (paths.shape.isEmpty()) {
        LayoutRect shapeBounds = shapeOutsideInfo->computedShapePhysicalBoundingBox();
        FloatQuad shapeQuad = renderer->localToAbsoluteQuad(FloatRect(shapeBounds));
        contentsQuadToCoordinateSystem(mainView, containingView, shapeQuad, InspectorOverlay::CoordinateSystem::Document);
        drawOutlinedQuad(context, shapeQuad, shapeHighlightColor, Color::transparentBlack, bounds);
        return;
    }

    const auto mapPoints = [&] (const Path& path) {
        Path newPath;
        path.applyElements([&] (const PathElement& pathElement) {
            const auto localToRoot = [&] (size_t index) {
                const FloatPoint& point = pathElement.points[index];
                return localPointToRootPoint(containingView, renderer->localToAbsolute(shapeOutsideInfo->shapeToRendererPoint(point)));
            };

            switch (pathElement.type) {
            case PathElement::Type::MoveToPoint:
                newPath.moveTo(localToRoot(0));
                break;

            case PathElement::Type::AddLineToPoint:
                newPath.addLineTo(localToRoot(0));
                break;

            case PathElement::Type::AddCurveToPoint:
                newPath.addBezierCurveTo(localToRoot(0), localToRoot(1), localToRoot(2));
                break;

            case PathElement::Type::AddQuadCurveToPoint:
                newPath.addQuadCurveTo(localToRoot(0), localToRoot(1));
                break;

            case PathElement::Type::CloseSubpath:
                newPath.closeSubpath();
                break;
            }
        });
        return newPath;
    };

    if (paths.marginShape.length()) {
        Path marginPath = mapPoints(paths.marginShape);
        bounds.unite(marginPath.boundingRect());

        GraphicsContextStateSaver stateSaver(context);

        constexpr auto shapeMarginHighlightColor = SRGBA<uint8_t> { 96, 82, 127, 153 };
        context.setFillColor(shapeMarginHighlightColor);
        context.fillPath(marginPath);
    }

    Path shapePath = mapPoints(paths.shape);
    bounds.unite(shapePath.boundingRect());

    GraphicsContextStateSaver stateSaver(context);

    context.setFillColor(shapeHighlightColor);
    context.fillPath(shapePath);
}

InspectorOverlay::InspectorOverlay(Page& page, InspectorClient* client)
    : m_page(page)
    , m_client(client)
    , m_paintRectUpdateTimer(*this, &InspectorOverlay::updatePaintRectsTimerFired)
{
}

InspectorOverlay::~InspectorOverlay() = default;

void InspectorOverlay::paint(GraphicsContext& context)
{
    if (!shouldShowOverlay())
        return;

    auto viewportSize = m_page.mainFrame().virtualView()->sizeForVisibleContent();

    context.clearRect({ FloatPoint::zero(), viewportSize });

    GraphicsContextStateSaver stateSaver(context);

    if (m_indicating) {
        GraphicsContextStateSaver stateSaver(context);

        constexpr auto indicatingColor = SRGBA<uint8_t> { 111, 168, 220, 168 };
        context.setFillColor(indicatingColor);
        context.fillRect({ FloatPoint::zero(), viewportSize });
    }

    RulerExclusion rulerExclusion;

    if (m_highlightQuad) {
        auto quadRulerExclusion = drawQuadHighlight(context, *m_highlightQuad);
        rulerExclusion.bounds.unite(quadRulerExclusion.bounds);
    }

    if (m_highlightNodeList) {
        for (unsigned i = 0; i < m_highlightNodeList->length(); ++i) {
            if (auto* node = m_highlightNodeList->item(i)) {
                auto nodeRulerExclusion = drawNodeHighlight(context, *node);
                rulerExclusion.bounds.unite(nodeRulerExclusion.bounds);

                if (m_nodeGridOverlayConfig) {
                    if (auto gridHighlightOverlay = buildGridOverlay({ *node, *m_nodeGridOverlayConfig }))
                        drawGridOverlay(context, *gridHighlightOverlay);
                }

                if (m_nodeFlexOverlayConfig) {
                    if (auto flexHighlightOverlay = buildFlexOverlay({ *node, *m_nodeFlexOverlayConfig }))
                        drawFlexOverlay(context, *flexHighlightOverlay);
                }
            }
        }
    }

    if (m_highlightNode) {
        auto nodeRulerExclusion = drawNodeHighlight(context, *m_highlightNode);
        rulerExclusion.bounds.unite(nodeRulerExclusion.bounds);
        rulerExclusion.titlePath = nodeRulerExclusion.titlePath;

        if (m_nodeGridOverlayConfig) {
            if (auto gridHighlightOverlay = buildGridOverlay({ *m_highlightNode, *m_nodeGridOverlayConfig }))
                drawGridOverlay(context, *gridHighlightOverlay);
        }

        if (m_nodeFlexOverlayConfig) {
            if (auto flexHighlightOverlay = buildFlexOverlay({ *m_highlightNode, *m_nodeFlexOverlayConfig }))
                drawFlexOverlay(context, *flexHighlightOverlay);
        }
    }

    for (const InspectorOverlay::Grid& gridOverlay : m_activeGridOverlays) {
        if (m_nodeGridOverlayConfig && gridOverlay.gridNode) {
            if (m_highlightNodeList && isInNodeList(*gridOverlay.gridNode, *m_highlightNodeList))
                continue;
            if (gridOverlay.gridNode == m_highlightNode.get())
                continue;
        }

        if (auto gridHighlightOverlay = buildGridOverlay(gridOverlay))
            drawGridOverlay(context, *gridHighlightOverlay);
    }

    for (const InspectorOverlay::Flex& flexOverlay : m_activeFlexOverlays) {
        if (m_nodeFlexOverlayConfig && flexOverlay.flexNode) {
            if (m_highlightNodeList && isInNodeList(*flexOverlay.flexNode, *m_highlightNodeList))
                continue;
            if (flexOverlay.flexNode == m_highlightNode.get())
                continue;
        }

        if (auto flexHighlightOverlay = buildFlexOverlay(flexOverlay))
            drawFlexOverlay(context, *flexHighlightOverlay);
    }

    if (!m_paintRects.isEmpty())
        drawPaintRects(context, m_paintRects);

    if (m_showRulers || m_showRulersForNodeHighlight)
        drawRulers(context, rulerExclusion);
}

void InspectorOverlay::getHighlight(InspectorOverlay::Highlight& highlight, InspectorOverlay::CoordinateSystem coordinateSystem)
{
    if (!m_highlightNode && !m_highlightQuad && !m_highlightNodeList && !m_activeGridOverlays.size() && !m_activeFlexOverlays.size())
        return;

    constexpr bool offsetBoundsByScroll = true;

    highlight.type = InspectorOverlay::Highlight::Type::None;
    if (m_highlightNode) {
        buildNodeHighlight(*m_highlightNode, m_nodeHighlightConfig, highlight, coordinateSystem);

        if (m_nodeGridOverlayConfig) {
            if (auto gridHighlightOverlay = buildGridOverlay({ *m_highlightNode, *m_nodeGridOverlayConfig }, offsetBoundsByScroll))
                highlight.gridHighlightOverlays.append(*gridHighlightOverlay);
        }

        if (m_nodeFlexOverlayConfig) {
            if (auto flexHighlightOverlay = buildFlexOverlay({ *m_highlightNode, *m_nodeFlexOverlayConfig }))
                highlight.flexHighlightOverlays.append(*flexHighlightOverlay);
        }
    } else if (m_highlightNodeList) {
        highlight.setDataFromConfig(m_nodeHighlightConfig);
        for (unsigned i = 0; i < m_highlightNodeList->length(); ++i) {
            auto* node = m_highlightNodeList->item(i);

            InspectorOverlay::Highlight nodeHighlight;
            buildNodeHighlight(*node, m_nodeHighlightConfig, nodeHighlight, coordinateSystem);
            if (nodeHighlight.type == InspectorOverlay::Highlight::Type::Node)
                highlight.quads.appendVector(nodeHighlight.quads);

            if (m_nodeGridOverlayConfig) {
                if (auto gridHighlightOverlay = buildGridOverlay({ *node, *m_nodeGridOverlayConfig }, offsetBoundsByScroll))
                    highlight.gridHighlightOverlays.append(*gridHighlightOverlay);
            }

            if (m_nodeFlexOverlayConfig) {
                if (auto flexHighlightOverlay = buildFlexOverlay({ *node, *m_nodeFlexOverlayConfig }))
                    highlight.flexHighlightOverlays.append(*flexHighlightOverlay);
            }
        }
        highlight.type = InspectorOverlay::Highlight::Type::NodeList;
    } else if (m_highlightQuad) {
        highlight.type = InspectorOverlay::Highlight::Type::Rects;
        buildQuadHighlight(*m_highlightQuad, m_quadHighlightConfig, highlight);
    }

    for (const InspectorOverlay::Grid& gridOverlay : m_activeGridOverlays) {
        if (m_nodeGridOverlayConfig && gridOverlay.gridNode) {
            if (m_highlightNodeList && isInNodeList(*gridOverlay.gridNode, *m_highlightNodeList))
                continue;
            if (gridOverlay.gridNode == m_highlightNode.get())
                continue;
        }

        if (auto gridHighlightOverlay = buildGridOverlay(gridOverlay, offsetBoundsByScroll))
            highlight.gridHighlightOverlays.append(*gridHighlightOverlay);
    }

    for (const InspectorOverlay::Flex& flexOverlay : m_activeFlexOverlays) {
        if (m_nodeFlexOverlayConfig && flexOverlay.flexNode) {
            if (m_highlightNodeList && isInNodeList(*flexOverlay.flexNode, *m_highlightNodeList))
                continue;
            if (flexOverlay.flexNode == m_highlightNode.get())
                continue;
        }

        if (auto flexHighlightOverlay = buildFlexOverlay(flexOverlay))
            highlight.flexHighlightOverlays.append(*flexHighlightOverlay);
    }
}

void InspectorOverlay::hideHighlight()
{
    m_highlightNode = nullptr;
    m_highlightNodeList = nullptr;
    m_nodeHighlightConfig = { };
    m_nodeGridOverlayConfig = std::nullopt;
    m_nodeFlexOverlayConfig = std::nullopt;
    m_showRulersForNodeHighlight = false;

    m_highlightQuad = nullptr;
    m_quadHighlightConfig = { };

    update();
}

void InspectorOverlay::highlightNodeList(RefPtr<NodeList>&& nodes, const InspectorOverlay::Highlight::Config& highlightConfig, const std::optional<Grid::Config>& gridOverlayConfig, const std::optional<Flex::Config>& flexOverlayConfig, bool showRulers)
{
    m_highlightNode = nullptr;
    m_highlightNodeList = WTFMove(nodes);
    m_nodeHighlightConfig = highlightConfig;
    m_nodeGridOverlayConfig = gridOverlayConfig;
    m_nodeFlexOverlayConfig = flexOverlayConfig;
    m_showRulersForNodeHighlight = showRulers;
    update();
}

void InspectorOverlay::highlightNode(Node* node, const InspectorOverlay::Highlight::Config& highlightConfig, const std::optional<Grid::Config>& gridOverlayConfig, const std::optional<Flex::Config>& flexOverlayConfig, bool showRulers)
{
    m_highlightNode = node;
    m_highlightNodeList = nullptr;
    m_nodeHighlightConfig = highlightConfig;
    m_nodeGridOverlayConfig = gridOverlayConfig;
    m_nodeFlexOverlayConfig = flexOverlayConfig;
    m_showRulersForNodeHighlight = showRulers;
    update();
}

void InspectorOverlay::highlightQuad(std::unique_ptr<FloatQuad> quad, const InspectorOverlay::Highlight::Config& highlightConfig)
{
    if (highlightConfig.usePageCoordinates)
        *quad -= toIntSize(m_page.mainFrame().virtualView()->scrollPosition());

    m_quadHighlightConfig = highlightConfig;
    m_highlightQuad = WTFMove(quad);
    update();
}

Node* InspectorOverlay::highlightedNode() const
{
    return m_highlightNode.get();
}

void InspectorOverlay::didSetSearchingForNode(bool enabled)
{
    m_client->didSetSearchingForNode(enabled);
}

void InspectorOverlay::setIndicating(bool indicating)
{
    if (m_indicating == indicating)
        return;

    m_indicating = indicating;

    update();
}

bool InspectorOverlay::shouldShowOverlay() const
{
    return m_highlightNode
        || m_highlightNodeList
        || m_highlightQuad
        || m_activeGridOverlays.size()
        || m_activeFlexOverlays.size()
        || m_indicating
        || m_showPaintRects
        || m_showRulers;
}

void InspectorOverlay::update()
{
    if (!shouldShowOverlay()) {
        m_client->hideHighlight();
        return;
    }

    if (!m_page.mainFrame().virtualView())
        return;

    m_client->highlight();
}

void InspectorOverlay::setShowPaintRects(bool showPaintRects)
{
    if (m_showPaintRects == showPaintRects)
        return;

    m_showPaintRects = showPaintRects;
    if (!m_showPaintRects) {
        m_paintRects.clear();
        m_paintRectUpdateTimer.stop();
        update();
    }
}

void InspectorOverlay::showPaintRect(const FloatRect& rect)
{
    if (!m_showPaintRects)
        return;

    auto rootRect = m_page.mainFrame().virtualView()->contentsToRootView(enclosingIntRect(rect));

    const auto removeDelay = 250_ms;

    MonotonicTime removeTime = MonotonicTime::now() + removeDelay;
    m_paintRects.append(TimeRectPair(removeTime, rootRect));

    if (!m_paintRectUpdateTimer.isActive()) {
        const Seconds paintRectsUpdateInterval { 32_ms };
        m_paintRectUpdateTimer.startRepeating(paintRectsUpdateInterval);
    }

    update();
}

void InspectorOverlay::setShowRulers(bool showRulers)
{
    if (m_showRulers == showRulers)
        return;

    m_showRulers = showRulers;

    update();
}

bool InspectorOverlay::removeGridOverlayForNode(Node& node)
{
    // Try to remove `node`. Also clear any grid overlays whose WeakPtr<Node, WeakPtrImplWithEventTargetData> has been cleared.
    return m_activeGridOverlays.removeAllMatching([&] (const InspectorOverlay::Grid& gridOverlay) {
        return !gridOverlay.gridNode || gridOverlay.gridNode.get() == &node;
    });
}

ErrorStringOr<void> InspectorOverlay::setGridOverlayForNode(Node& node, const InspectorOverlay::Grid::Config& gridOverlayConfig)
{
    RenderObject* renderer = node.renderer();
    if (!is<RenderGrid>(renderer))
        return makeUnexpected("Node does not initiate a grid context"_s);

    removeGridOverlayForNode(node);

    m_activeGridOverlays.append({ node, gridOverlayConfig });

    update();

    return { };
}

ErrorStringOr<void> InspectorOverlay::clearGridOverlayForNode(Node& node)
{
    if (!removeGridOverlayForNode(node))
        return makeUnexpected("No grid overlay exists for the node, so cannot clear."_s);

    update();

    return { };
}

void InspectorOverlay::clearAllGridOverlays()
{
    m_activeGridOverlays.clear();

    update();
}

bool InspectorOverlay::removeFlexOverlayForNode(Node& node)
{
    // Try to remove `node`. Also clear any grid overlays whose WeakPtr<Node, WeakPtrImplWithEventTargetData> has been cleared.
    return m_activeFlexOverlays.removeAllMatching([&] (const InspectorOverlay::Flex& flexOverlay) {
        return !flexOverlay.flexNode || flexOverlay.flexNode.get() == &node;
    });
}

ErrorStringOr<void> InspectorOverlay::setFlexOverlayForNode(Node& node, const InspectorOverlay::Flex::Config& flexOverlayConfig)
{
    if (!is<RenderFlexibleBox>(node.renderer()))
        return makeUnexpected("Node does not initiate a flex context"_s);

    removeFlexOverlayForNode(node);

    m_activeFlexOverlays.append({ node, flexOverlayConfig });

    update();

    return { };
}

ErrorStringOr<void> InspectorOverlay::clearFlexOverlayForNode(Node& node)
{
    if (!removeFlexOverlayForNode(node))
        return makeUnexpected("No flex overlay exists for the node, so cannot clear."_s);

    update();

    return { };
}

void InspectorOverlay::clearAllFlexOverlays()
{
    m_activeFlexOverlays.clear();

    update();
}

void InspectorOverlay::updatePaintRectsTimerFired()
{
    MonotonicTime now = MonotonicTime::now();
    bool rectsChanged = false;
    while (!m_paintRects.isEmpty() && m_paintRects.first().first < now) {
        m_paintRects.removeFirst();
        rectsChanged = true;
    }

    if (m_paintRects.isEmpty())
        m_paintRectUpdateTimer.stop();

    if (rectsChanged)
        update();
}

InspectorOverlay::RulerExclusion InspectorOverlay::drawNodeHighlight(GraphicsContext& context, Node& node)
{
    RulerExclusion rulerExclusion;

    drawFragmentHighlight(context, node, m_nodeHighlightConfig, rulerExclusion.bounds);

    if (m_nodeHighlightConfig.showInfo)
        drawShapeHighlight(context, node, rulerExclusion.bounds);

    if (m_showRulers || m_showRulersForNodeHighlight)
        drawBounds(context, rulerExclusion.bounds);

    // Ensure that the title information is drawn after the bounds.
    if (m_nodeHighlightConfig.showInfo)
        rulerExclusion.titlePath = drawElementTitle(context, node, rulerExclusion.bounds);

    // Note: since grid overlays may cover the entire viewport with little lines, grid overlay bounds
    // are not considered as part of the combined bounds used as the ruler exclusion area.
    
    return rulerExclusion;
}

InspectorOverlay::RulerExclusion InspectorOverlay::drawQuadHighlight(GraphicsContext& context, const FloatQuad& quad)
{
    RulerExclusion rulerExclusion;

    InspectorOverlay::Highlight highlight;
    buildQuadHighlight(quad, m_quadHighlightConfig, highlight);

    if (highlight.quads.size() >= 1) {
        drawOutlinedQuad(context, highlight.quads[0], highlight.contentColor, highlight.contentOutlineColor, rulerExclusion.bounds);

        if (m_showRulers || m_showRulersForNodeHighlight)
            drawBounds(context, rulerExclusion.bounds);
    }

    return rulerExclusion;
}

void InspectorOverlay::drawPaintRects(GraphicsContext& context, const Deque<TimeRectPair>& paintRects)
{
    GraphicsContextStateSaver stateSaver(context);

    constexpr auto paintRectsColor = Color::red.colorWithAlphaByte(128);
    context.setFillColor(paintRectsColor);

    for (const TimeRectPair& pair : paintRects)
        context.fillRect(pair.second);
}

void InspectorOverlay::drawBounds(GraphicsContext& context, const InspectorOverlay::Highlight::Bounds& bounds)
{
    Ref mainFrame = m_page.mainFrame();
    RefPtr pageView = mainFrame->virtualView();
    FloatSize viewportSize = pageView->sizeForVisibleContent();
    FloatSize contentInset(0, pageView->topContentInset(ScrollView::TopContentInsetType::WebCoreOrPlatformContentInset));

    Path path;

    if (bounds.y() > contentInset.height()) {
        path.moveTo({ bounds.x(), bounds.y() });
        path.addLineTo({ bounds.x(), contentInset.height() });

        path.moveTo({ bounds.maxX(), bounds.y() });
        path.addLineTo({ bounds.maxX(), contentInset.height() });
    }

    if (bounds.maxY() < viewportSize.height()) {
        path.moveTo({ bounds.x(), viewportSize.height() });
        path.addLineTo({ bounds.x(), bounds.maxY() });

        path.moveTo({ bounds.maxX(), viewportSize.height() });
        path.addLineTo({ bounds.maxX(), bounds.maxY() });
    }

    if (bounds.x() > contentInset.width()) {
        path.moveTo({ bounds.x(), bounds.y() });
        path.addLineTo({ contentInset.width(), bounds.y() });

        path.moveTo({ bounds.x(), bounds.maxY() });
        path.addLineTo({ contentInset.width(), bounds.maxY() });
    }

    if (bounds.maxX() < viewportSize.width()) {
        path.moveTo({ bounds.maxX(), bounds.y() });
        path.addLineTo({ viewportSize.width(), bounds.y() });

        path.moveTo({ bounds.maxX(), bounds.maxY() });
        path.addLineTo({ viewportSize.width(), bounds.maxY() });
    }

    GraphicsContextStateSaver stateSaver(context);

    context.setStrokeThickness(1);

    constexpr auto boundsColor = Color::red.colorWithAlphaByte(153);
    context.setStrokeColor(boundsColor);

    context.strokePath(path);
}

void InspectorOverlay::drawRulers(GraphicsContext& context, const InspectorOverlay::RulerExclusion& rulerExclusion)
{
    constexpr auto rulerBackgroundColor = Color::white.colorWithAlphaByte(153);
    constexpr auto lightRulerColor = Color::black.colorWithAlphaByte(51);
    constexpr auto darkRulerColor = Color::black.colorWithAlphaByte(128);

    IntPoint scrollOffset;
    auto* localMainFrame = dynamicDowncast<LocalFrame>(m_page.mainFrame());
    if (!localMainFrame)
        return;

    auto* pageView = localMainFrame->view();
    if (!pageView->delegatesScrollingToNativeView())
        scrollOffset = pageView->visibleContentRect().location();

    FloatSize viewportSize = pageView->sizeForVisibleContent();
    FloatSize contentInset(0, pageView->topContentInset(ScrollView::TopContentInsetType::WebCoreOrPlatformContentInset));
    float pageScaleFactor = m_page.pageScaleFactor();
    float pageZoomFactor = localMainFrame->pageZoomFactor();

    float pageFactor = pageZoomFactor * pageScaleFactor;
    float scrollX = scrollOffset.x() * pageScaleFactor;
    float scrollY = scrollOffset.y() * pageScaleFactor;

    const auto zoom = [&] (float value) -> float {
        return value * pageFactor;
    };

    const auto unzoom = [&] (float value) -> float {
        return value / pageFactor;
    };

    const auto multipleBelow = [&] (float value, float step) -> float {
        return value - std::fmod(value, step);
    };

    float width = viewportSize.width() / pageFactor;
    float height = viewportSize.height() / pageFactor;
    float minX = unzoom(scrollX);
    float minY = unzoom(scrollY);
    float maxX = minX + width;
    float maxY = minY + height;

    bool drawTopEdge = true;
    bool drawLeftEdge = true;

    // Determine which side (top/bottom and left/right) to draw the rulers.
    {
        FloatRect topEdge(contentInset.width(), contentInset.height(), zoom(width) - contentInset.width(), rulerSize);
        FloatRect bottomEdge(contentInset.width(), zoom(height) - rulerSize, zoom(width) - contentInset.width(), rulerSize);
        drawTopEdge = !rulerExclusion.bounds.intersects(topEdge) || rulerExclusion.bounds.intersects(bottomEdge);

        FloatRect rightEdge(zoom(width) - rulerSize, contentInset.height(), rulerSize, zoom(height) - contentInset.height());
        FloatRect leftEdge(contentInset.width(), contentInset.height(), rulerSize, zoom(height) - contentInset.height());
        drawLeftEdge = !rulerExclusion.bounds.intersects(leftEdge) || rulerExclusion.bounds.intersects(rightEdge);
    }

    float cornerX = drawLeftEdge ? contentInset.width() : zoom(width) - rulerSize;
    float cornerY = drawTopEdge ? contentInset.height() : zoom(height) - rulerSize;

    // Draw backgrounds.
    {
        GraphicsContextStateSaver backgroundStateSaver(context);

        context.setFillColor(rulerBackgroundColor);

        context.fillRect({ cornerX, cornerY, rulerSize, rulerSize });

        if (drawLeftEdge)
            context.fillRect({ cornerX + rulerSize, cornerY, zoom(width) - cornerX - rulerSize, rulerSize });
        else
            context.fillRect({ contentInset.width(), cornerY, cornerX - contentInset.width(), rulerSize });

        if (drawTopEdge)
            context.fillRect({ cornerX, cornerY + rulerSize, rulerSize, zoom(height) - cornerY - rulerSize });  
        else
            context.fillRect({ cornerX, contentInset.height(), rulerSize, cornerY - contentInset.height() });
    }

    // Draw lines.
    {
        FontCascadeDescription fontDescription;
        fontDescription.setOneFamily(AtomString { m_page.settings().sansSerifFontFamily() });
        fontDescription.setComputedSize(10);

        FontCascade font(WTFMove(fontDescription));
        font.update(nullptr);

        GraphicsContextStateSaver lineStateSaver(context);

        context.setFillColor(darkRulerColor);
        context.setStrokeThickness(1);

        // Draw horizontal ruler.
        {
            GraphicsContextStateSaver horizontalRulerStateSaver(context);

            context.translate(contentInset.width() - scrollX + 0.5f, cornerY - scrollY);

            for (float x = multipleBelow(minX, rulerSubStepIncrement); x < maxX; x += rulerSubStepIncrement) {
                if (!x && !scrollX)
                    continue;

                Path path;
                path.moveTo({ zoom(x), drawTopEdge ? scrollY : scrollY + rulerSize });

                float lineLength = 0.0f;
                if (std::fmod(x, rulerStepIncrement)) {
                    lineLength = rulerSubStepLength;
                    context.setStrokeColor(lightRulerColor);
                } else {
                    lineLength = std::fmod(x, rulerStepIncrement * 2) ? rulerSubStepLength : rulerStepLength;
                    context.setStrokeColor(darkRulerColor);
                }
                path.addLineTo({ zoom(x), scrollY + (drawTopEdge ? lineLength : rulerSize - lineLength) });

                context.strokePath(path);
            }

            // Draw labels.
            for (float x = multipleBelow(minX, rulerStepIncrement * 2); x < maxX; x += rulerStepIncrement * 2) {
                if (!x && !scrollX)
                    continue;

                GraphicsContextStateSaver verticalLabelStateSaver(context);
                context.translate(zoom(x) + 0.5f, scrollY);
                context.drawText(font, TextRun(String::number(x)), { 2, drawTopEdge ? rulerLabelSize : rulerLabelSize - rulerSize + font.metricsOfPrimaryFont().intHeight() - 1.0f });
            }
        }

        // Draw vertical ruler.
        {
            GraphicsContextStateSaver veritcalRulerStateSaver(context);

            context.translate(cornerX - scrollX, contentInset.height() - scrollY + 0.5f);

            for (float y = multipleBelow(minY, rulerSubStepIncrement); y < maxY; y += rulerSubStepIncrement) {
                if (!y && !scrollY)
                    continue;

                Path path;
                path.moveTo({ drawLeftEdge ? scrollX : scrollX + rulerSize, zoom(y) });

                float lineLength = 0.0f;
                if (std::fmod(y, rulerStepIncrement)) {
                    lineLength = rulerSubStepLength;
                    context.setStrokeColor(lightRulerColor);
                } else {
                    lineLength = std::fmod(y, rulerStepIncrement * 2) ? rulerSubStepLength : rulerStepLength;
                    context.setStrokeColor(darkRulerColor);
                }
                path.addLineTo({ scrollX + (drawLeftEdge ? lineLength : rulerSize - lineLength), zoom(y) });

                context.strokePath(path);
            }

            // Draw labels.
            for (float y = multipleBelow(minY, rulerStepIncrement * 2); y < maxY; y += rulerStepIncrement * 2) {
                if (!y && !scrollY)
                    continue;

                GraphicsContextStateSaver horizontalLabelStateSaver(context);
                context.translate(scrollX, zoom(y) + 0.5f);
                context.rotate(drawLeftEdge ? -piOverTwoFloat : piOverTwoFloat);
                context.drawText(font, TextRun(String::number(y)), { 2, drawLeftEdge ? rulerLabelSize : rulerLabelSize - rulerSize });
            }
        }
    }

    // Draw viewport size.
    {
        FontCascadeDescription fontDescription;
        fontDescription.setOneFamily(AtomString { m_page.settings().sansSerifFontFamily() });
        fontDescription.setComputedSize(12);

        FontCascade font(WTFMove(fontDescription));
        font.update(nullptr);

        auto viewportRect = pageView->visualViewportRect();
        TextRun viewportTextRun(makeString(viewportRect.width() / pageZoomFactor, "px"_s, ' ', multiplicationSign, ' ', viewportRect.height() / pageZoomFactor, "px"_s));

        const float margin = 4;
        const float padding = 2;
        const float radius = 4;
        float fontWidth = font.width(viewportTextRun);
        float fontHeight = font.metricsOfPrimaryFont().height();
        FloatRect viewportTextRect(margin, margin, (padding * 2.0f) + fontWidth, (padding * 2.0f) + fontHeight);
        const auto viewportTextRectCenter = viewportTextRect.center();

        GraphicsContextStateSaver viewportSizeStateSaver(context);

        float leftTranslateX = rulerSize;
        float rightTranslateX = 0.0f - (margin * 2.0f) - (padding * 2.0f) - fontWidth;
        float translateX = cornerX + (drawLeftEdge ? leftTranslateX : rightTranslateX);

        float topTranslateY = rulerSize;
        float bottomTranslateY = 0.0f - (margin * 2.0f) - (padding * 2.0f) - fontHeight;
        float translateY = cornerY + (drawTopEdge ? topTranslateY : bottomTranslateY);

        FloatPoint translate(translateX, translateY);
        if (rulerExclusion.titlePath.contains(viewportTextRectCenter + translate)) {
            // Try the opposite horizontal side.
            float oppositeTranslateX = drawLeftEdge ? zoom(width) + rightTranslateX : contentInset.width() + leftTranslateX;
            translate.setX(oppositeTranslateX);

            if (rulerExclusion.titlePath.contains(viewportTextRectCenter + translate)) {
                translate.setX(translateX);

                // Try the opposite vertical side.
                float oppositeTranslateY = drawTopEdge ? zoom(height) + bottomTranslateY : contentInset.height() + topTranslateY;
                translate.setY(oppositeTranslateY);

                if (rulerExclusion.titlePath.contains(viewportTextRectCenter + translate)) {
                    // Try the opposite corner.
                    translate.setX(oppositeTranslateX);
                }
            }
        }
        context.translate(translate);

        context.fillRoundedRect(FloatRoundedRect(viewportTextRect, FloatRoundedRect::Radii(radius)), rulerBackgroundColor);

        context.setFillColor(Color::black);
        context.drawText(font, viewportTextRun, { margin +  padding, margin + padding + fontHeight - font.metricsOfPrimaryFont().intDescent() });
    }
}

static bool rendererIsFlexboxItem(RenderObject& renderer)
{
    if (auto* parentFlexRenderer = dynamicDowncast<RenderFlexibleBox>(renderer.parent()))
        return !parentFlexRenderer->orderIterator().shouldSkipChild(renderer);

    return false;
}

static bool rendererIsGridItem(RenderObject& renderer)
{
    if (is<RenderGrid>(renderer.parent()))
        return !renderer.isOutOfFlowPositioned() && !renderer.isExcludedFromNormalLayout();

    return false;
}

Path InspectorOverlay::drawElementTitle(GraphicsContext& context, Node& node, const InspectorOverlay::Highlight::Bounds& bounds)
{
    if (bounds.isEmpty())
        return { };

    auto* element = effectiveElementForNode(node);
    if (!element)
        return { };

    auto* renderer = node.renderer();
    if (!renderer || renderer->isSkippedContent())
        return { };

    String elementTagName = element->nodeName();
    if (!element->document().isXHTMLDocument())
        elementTagName = elementTagName.convertToASCIILowercase();

    String elementIDValue;
    if (element->hasID())
        elementIDValue = makeString('#', DOMCSSNamespace::escape(element->getIdAttribute()));

    String elementClassValue;
    if (element->hasClass()) {
        StringBuilder builder;
        DOMTokenList& classList = element->classList();
        for (size_t i = 0; i < classList.length(); ++i) {
            builder.append('.');
            builder.append(DOMCSSNamespace::escape(classList.item(i)));
        }

        elementClassValue = builder.toString();
        truncateWithEllipsis(elementClassValue, 50);
    }

    String elementPseudoType;
    if (node.isBeforePseudoElement())
        elementPseudoType = "::before"_s;
    else if (node.isAfterPseudoElement())
        elementPseudoType = "::after"_s;

    String elementWidth;
    String elementHeight;
    if (is<RenderBoxModelObject>(renderer)) {
        auto* modelObject = downcast<RenderBoxModelObject>(renderer);
        elementWidth = String::number(adjustForAbsoluteZoom(roundToInt(modelObject->offsetWidth()), *modelObject));
        elementHeight = String::number(adjustForAbsoluteZoom(roundToInt(modelObject->offsetHeight()), *modelObject));
    } else {
        auto* containingView = node.document().frame()->view();
        IntRect boundingBox = snappedIntRect(containingView->contentsToRootView(renderer->absoluteBoundingBoxRect()));
        elementWidth = String::number(boundingBox.width());
        elementHeight = String::number(boundingBox.height());
    }

    Vector<String> layoutContextBubbleStrings;
    
    if (rendererIsFlexboxItem(*renderer))
        layoutContextBubbleStrings.append(WEB_UI_STRING_KEY("Flex Item", "Flex Item (Inspector Element Selection)", "Inspector element selection tooltip text for items inside a Flexbox Container."));
    else if (rendererIsGridItem(*renderer))
        layoutContextBubbleStrings.append(WEB_UI_STRING_KEY("Grid Item", "Grid Item (Inspector Element Selection)", "Inspector element selection tooltip text for items inside a Grid Container."));

    if (is<RenderFlexibleBox>(renderer))
        layoutContextBubbleStrings.append(WEB_UI_STRING_KEY("Flex", "Flex (Inspector Element Selection)", "Inspector element selection tooltip text for Flexbox containers."));
    else if (is<RenderGrid>(renderer))
        layoutContextBubbleStrings.append(WEB_UI_STRING_KEY("Grid", "Grid (Inspector Element Selection)", "Inspector element selection tooltip text for Grid containers."));

    // Need to enable AX to get the computed role.
    if (!WebCore::AXObjectCache::accessibilityEnabled())
        WebCore::AXObjectCache::enableAccessibility();

    String elementRole;
    if (AXObjectCache* axObjectCache = node.document().axObjectCache()) {
        if (auto* axObject = axObjectCache->getOrCreate(node); axObject && !axObject->accessibilityIsIgnored())
            elementRole = axObject->computedRoleString();
    }

    constexpr auto elementTitleTagColor = SRGBA<uint8_t> { 136, 18, 128 }; // Keep this in sync with XMLViewer.css (.tag)
    constexpr auto elementTitleAttributeValueColor = SRGBA<uint8_t> { 26, 26, 166 }; // Keep this in sync with XMLViewer.css (.attribute-value)
    constexpr auto elementTitleAttributeNameColor = SRGBA<uint8_t> { 153, 69, 0 }; // Keep this in sync with XMLViewer.css (.attribute-name)
    constexpr auto elementTitleRoleBubbleColor = SRGBA<uint8_t> { 170, 13, 145, 48 };
    constexpr auto elementTitleLayoutBubbleColor = Color::gray.colorWithAlphaByte(64);

    Vector<InspectorOverlayLabel::Content> labelContents = {
        { elementTagName, elementTitleTagColor },
        { elementIDValue, elementTitleAttributeValueColor },
        { elementClassValue, elementTitleAttributeNameColor },
        { elementPseudoType, elementTitleTagColor },
        { makeString(emSpace, elementWidth), Color::black },
        { makeString("px "_s, multiplicationSign, " "_s), Color::darkGray },
        { elementHeight, Color::black },
        { "px"_s, Color::darkGray },
    };

    if (!elementRole.isEmpty() || !layoutContextBubbleStrings.isEmpty()) {
        labelContents.append({ "\n"_s, Color::black });

        if (!elementRole.isEmpty())
            labelContents.append({ makeString("Role: "_s, elementRole), Color::black, { InspectorOverlayLabel::Content::Decoration::Type::Bordered, elementTitleRoleBubbleColor } });

        auto isFirstBubble = elementRole.isEmpty();
        for (auto& layoutContextBubbleString : layoutContextBubbleStrings) {
            if (!isFirstBubble)
                labelContents.append({ "  "_s, Color::black });
            labelContents.append({ layoutContextBubbleString, Color::black, { InspectorOverlayLabel::Content::Decoration::Type::Bordered, elementTitleLayoutBubbleColor } });
            isFirstBubble = false;
        }
    }

    Ref mainFrame = m_page.mainFrame();
    RefPtr pageView = mainFrame->virtualView();
    FloatSize viewportSize = pageView->sizeForVisibleContent();
    FloatSize contentInset(0, pageView->topContentInset(ScrollView::TopContentInsetType::WebCoreOrPlatformContentInset));
    if (m_showRulers || m_showRulersForNodeHighlight)
        contentInset.expand(rulerSize, rulerSize);

    auto expectedLabelSize = InspectorOverlayLabel::expectedSize(labelContents, InspectorOverlayLabel::Arrow::Direction::Up);
    auto boundsCenterX = bounds.center().x();

    float labelX;
    InspectorOverlayLabel::Arrow::Alignment arrowAlignment;
    if (boundsCenterX + (expectedLabelSize.width() / 2) < viewportSize.width()
        && boundsCenterX - (expectedLabelSize.width() / 2) > contentInset.width()) {
        labelX = bounds.x() + (bounds.width() / 2);
        arrowAlignment = InspectorOverlayLabel::Arrow::Alignment::Middle;
    } else if (bounds.x() < contentInset.width()) {
        labelX = fmax(contentInset.width(), boundsCenterX);
        arrowAlignment = InspectorOverlayLabel::Arrow::Alignment::Leading;
    } else if (bounds.maxX() > viewportSize.width()) {
        labelX = fmin(viewportSize.width(), boundsCenterX);
        arrowAlignment = InspectorOverlayLabel::Arrow::Alignment::Trailing;
    } else {
        labelX = boundsCenterX;
        arrowAlignment = boundsCenterX < (viewportSize.width() / 2) ? InspectorOverlayLabel::Arrow::Alignment::Leading : InspectorOverlayLabel::Arrow::Alignment::Trailing;
    }

    float anchorTop = bounds.y();
    float anchorBottom = bounds.maxY();

    float labelY;
    InspectorOverlayLabel::Arrow::Direction arrowDirection;
    if (anchorTop > viewportSize.height()) {
        labelY = viewportSize.height();
        arrowDirection = InspectorOverlayLabel::Arrow::Direction::Down;
    } else if (anchorBottom < contentInset.height()) {
        labelY = contentInset.height();
        arrowDirection = InspectorOverlayLabel::Arrow::Direction::Up;
    } else if (anchorTop - expectedLabelSize.height() > contentInset.height()) {
        labelY = anchorTop;
        arrowDirection = InspectorOverlayLabel::Arrow::Direction::Down;
    } else if (anchorBottom + expectedLabelSize.height() < viewportSize.height()) {
        labelY = anchorBottom;
        arrowDirection = InspectorOverlayLabel::Arrow::Direction::Up;
    } else {
        labelY = contentInset.height() + expectedLabelSize.height();
        arrowDirection = InspectorOverlayLabel::Arrow::Direction::Down;
    }

    constexpr auto elementTitleBackgroundColor = SRGBA<uint8_t> { 255, 255, 194 };
    constexpr auto elementTitleBorderColor = Color::darkGray;

    GraphicsContextStateSaver stateSaver(context);
    context.setStrokeThickness(1);
    context.setStrokeColor(elementTitleBorderColor);

    InspectorOverlayLabel label = { WTFMove(labelContents), { labelX, labelY }, elementTitleBackgroundColor, { arrowDirection, arrowAlignment } };
    return label.draw(context);
}

static void drawLayoutPattern(GraphicsContext& context, const FloatQuad& quad, int hatchSpacing, Flip flip)
{
    GraphicsContextStateSaver saver(context);
    context.clipPath(quadToPath(quad));

    Path hatchPath;

    auto boundingBox = quad.enclosingBoundingBox();

    auto correctedLineForPoints = [&](const FloatPoint& start, const FloatPoint& end) {
        return (flip == Flip::Yes) ? FloatLine(end, start) : FloatLine(start, end);
    };

    auto topSide = correctedLineForPoints(boundingBox.minXMinYCorner(), boundingBox.maxXMinYCorner());
    auto leftSide = correctedLineForPoints(boundingBox.minXMinYCorner(), boundingBox.minXMaxYCorner());

    // The opposite axis' length is used to determine how far to draw a hatch line in both dimensions, which keeps the lines at a 45deg angle.
    if (topSide.length() > leftSide.length()) {
        auto bottomSide = correctedLineForPoints(boundingBox.minXMaxYCorner(), boundingBox.maxXMaxYCorner());
        // Move across the relative top of the area, starting left of `0, 0` to ensure that the tail of the previous hatch line is drawn while scrolling.
        for (float x = -leftSide.length(); x < topSide.length(); x += hatchSpacing) {
            hatchPath.moveTo(topSide.pointAtAbsoluteDistance(x));
            hatchPath.addLineTo(bottomSide.pointAtAbsoluteDistance(x + leftSide.length()));
        }
    } else {
        auto rightSide = correctedLineForPoints(boundingBox.maxXMinYCorner(), boundingBox.maxXMaxYCorner());
        // Move down the relative left side of the area, starting above `0, 0` to ensure that the tail of the previous hatch line is drawn while scrolling.
        for (float y = -topSide.length(); y < leftSide.length(); y += hatchSpacing) {
            hatchPath.moveTo(leftSide.pointAtAbsoluteDistance(y));
            hatchPath.addLineTo(rightSide.pointAtAbsoluteDistance(y + topSide.length()));
        }
    }

    context.strokePath(hatchPath);
}

static void drawLayoutStippling(GraphicsContext& context, const FloatQuad& quad, float density)
{
    GraphicsContextStateSaver saver(context);
    context.setStrokeThickness(1);
    context.setStrokeStyle(StrokeStyle::DashedStroke);
    context.setLineDash({ 1, density }, 1);

    drawLayoutPattern(context, quad, density, Flip::No);
}

static void drawLayoutHatching(GraphicsContext& context, const FloatQuad& quad, Flip flip = Flip::No)
{
    GraphicsContextStateSaver saver(context);
    context.setStrokeThickness(0.5);
    context.setStrokeStyle(StrokeStyle::DashedStroke);
    context.setLineDash({ 2, 2 }, 2);

    constexpr auto defaultLayoutHatchSpacing = 12;
    drawLayoutPattern(context, quad, defaultLayoutHatchSpacing, flip);
}

void InspectorOverlay::drawGridOverlay(GraphicsContext& context, const InspectorOverlay::Highlight::GridHighlightOverlay& gridOverlay)
{
    constexpr auto translucentLabelBackgroundColor = Color::white.colorWithAlphaByte(230);

    GraphicsContextStateSaver saver(context);
    context.setStrokeThickness(1);
    context.setStrokeColor(gridOverlay.color);

    Path gridLinesPath;
    for (auto gridLine : gridOverlay.gridLines) {
        gridLinesPath.moveTo(gridLine.start());
        gridLinesPath.addLineTo(gridLine.end());
    }
    context.strokePath(gridLinesPath);

    for (auto gapQuad : gridOverlay.gaps)
        drawLayoutHatching(context, gapQuad);

    context.setStrokeThickness(3);
    for (auto area : gridOverlay.areas)
        context.strokePath(quadToPath(area.quad));

    // Draw labels on top of all other lines.
    context.setStrokeThickness(1);
    for (auto area : gridOverlay.areas)
        InspectorOverlayLabel(area.name, area.quad.center(), translucentLabelBackgroundColor, { InspectorOverlayLabel::Arrow::Direction::None, InspectorOverlayLabel::Arrow::Alignment::None }).draw(context, area.quad.boundingBox().width());

    for (auto label : gridOverlay.labels)
        label.draw(context);
}

static Vector<String> authoredGridTrackSizes(Node* node, GridTrackSizingDirection direction, unsigned expectedTrackCount)
{
    auto* element = dynamicDowncast<StyledElement>(node);
    if (!element)
        return { };

    auto directionCSSPropertyID = direction == GridTrackSizingDirection::ForColumns ? CSSPropertyID::CSSPropertyGridTemplateColumns : CSSPropertyID::CSSPropertyGridTemplateRows;
    RefPtr<CSSValue> cssValue;
    if (auto* inlineStyle = element->inlineStyle())
        cssValue = inlineStyle->getPropertyCSSValue(directionCSSPropertyID);

    if (!cssValue) {
        auto styleRules = element->styleResolver().styleRulesForElement(element);
        styleRules.reverse();
        for (auto styleRule : styleRules) {
            ASSERT(styleRule);
            if (!styleRule)
                continue;
            cssValue = styleRule->properties().getPropertyCSSValue(directionCSSPropertyID);
            if (cssValue)
                break;
        }
    }

    auto* cssValueList = dynamicDowncast<CSSValueList>(cssValue.get());
    if (!cssValueList)
        return { };
    
    Vector<String> trackSizes;
    
    auto handleValueIgnoringLineNames = [&](const CSSValue& currentValue) {
        if (!is<CSSGridLineNamesValue>(currentValue))
            trackSizes.append(currentValue.cssText());
    };

    for (auto& currentValue : *cssValueList) {
        if (auto* cssGridAutoRepeatValue = dynamicDowncast<CSSGridAutoRepeatValue>(currentValue)) {
            // Auto-repeated values will be looped through until no more values were used in layout based on the expected track count.
            while (trackSizes.size() < expectedTrackCount) {
                for (auto& autoRepeatValue : *cssGridAutoRepeatValue) {
                    handleValueIgnoringLineNames(autoRepeatValue);
                    if (trackSizes.size() >= expectedTrackCount)
                        break;
                }
            }
            break;
        }

        if (auto* cssGridIntegerRepeatValue = dynamicDowncast<CSSGridIntegerRepeatValue>(currentValue)) {
            size_t repetitions = cssGridIntegerRepeatValue->repetitions();
            for (size_t i = 0; i < repetitions; ++i) {
                for (auto& integerRepeatValue : *cssGridIntegerRepeatValue)
                    handleValueIgnoringLineNames(integerRepeatValue);
            }
            continue;
        }

        handleValueIgnoringLineNames(currentValue);
    }
    
    return trackSizes;
}

static OrderedNamedGridLinesMap gridLineNames(const RenderStyle* renderStyle, GridTrackSizingDirection direction, unsigned expectedLineCount)
{
    if (!renderStyle)
        return { };
    
    OrderedNamedGridLinesMap combinedGridLineNames;
    auto appendLineNames = [&](unsigned index, const Vector<String>& newNames) {
        if (auto result = combinedGridLineNames.map.add(index, newNames); !result.isNewEntry)
            result.iterator->value.appendVector(newNames);
    };

    auto orderedGridLineNames = direction == GridTrackSizingDirection::ForColumns ? renderStyle->orderedNamedGridColumnLines() : renderStyle->orderedNamedGridRowLines();
    for (auto& [i, names] : orderedGridLineNames.map)
        appendLineNames(i, names);

    auto& autoRepeatOrderedGridLineNames = (direction == GridTrackSizingDirection::ForColumns ? renderStyle->autoRepeatOrderedNamedGridColumnLines() : renderStyle->autoRepeatOrderedNamedGridRowLines()).map;
    auto autoRepeatInsertionPoint = direction == GridTrackSizingDirection::ForColumns ? renderStyle->gridAutoRepeatColumnsInsertionPoint() : renderStyle->gridAutoRepeatRowsInsertionPoint();
    unsigned autoRepeatIndex = 0;
    while (autoRepeatOrderedGridLineNames.size() && autoRepeatIndex < expectedLineCount - autoRepeatInsertionPoint) {
        auto names = autoRepeatOrderedGridLineNames.get(autoRepeatIndex % autoRepeatOrderedGridLineNames.size());
        auto lineIndex = autoRepeatIndex + autoRepeatInsertionPoint;
        appendLineNames(lineIndex, names);
        ++autoRepeatIndex;
    }

    auto implicitGridLineNames = direction == GridTrackSizingDirection::ForColumns ? renderStyle->implicitNamedGridColumnLines() : renderStyle->implicitNamedGridRowLines();
    for (auto& [name, indexes] : implicitGridLineNames.map) {
        for (auto i : indexes)
            appendLineNames(i, {name});
    }
    
    return combinedGridLineNames;
}

std::optional<InspectorOverlay::Highlight::GridHighlightOverlay> InspectorOverlay::buildGridOverlay(const InspectorOverlay::Grid& gridOverlay, bool offsetBoundsByScroll)
{
    // If the node WeakPtr has been cleared, then the node is gone and there's nothing to draw.
    if (!gridOverlay.gridNode) {
        m_activeGridOverlays.removeAllMatching([&] (const InspectorOverlay::Grid& gridOverlay) {
            return !gridOverlay.gridNode;
        });
        return { };
    }
    
    // Always re-check because the node's renderer may have changed since being added.
    // If renderer is no longer a grid, then remove the grid overlay for the node.
    Node* node = gridOverlay.gridNode.get();
    auto renderer = node->renderer();
    if (!is<RenderGrid>(renderer)) {
        removeGridOverlayForNode(*node);
        return { };
    }

    constexpr auto translucentLabelBackgroundColor = Color::white.colorWithAlphaByte(230);

    RefPtr pageView = m_page.mainFrame().virtualView();
    if (!pageView)
        return { };
    FloatRect viewportBounds = { { 0, 0 }, pageView->sizeForVisibleContent() };

    auto scrollPosition = pageView->scrollPosition();
    if (offsetBoundsByScroll)
        viewportBounds.setLocation(scrollPosition);
    
    auto& renderGrid = *downcast<RenderGrid>(renderer);
    auto columnPositions = renderGrid.columnPositions();
    auto rowPositions = renderGrid.rowPositions();
    if (!columnPositions.size() || !rowPositions.size())
        return { };

    LayoutUnit masonryContentSize = renderGrid.masonryContentSize();

    // There are no actual rows or columns in the masonry axis of a masonry layout.
    // But we can borrow the concept to draw the two lines at the start and end of the masonry axis.
    if (renderGrid.areMasonryRows()) {
        auto firstRowPosition = rowPositions[0];
        auto lastRowPosition = rowPositions[0] + masonryContentSize;
        rowPositions = Vector<LayoutUnit> { firstRowPosition, lastRowPosition };
    }

    if (renderGrid.areMasonryColumns()) {
        auto firstColumnPosition = columnPositions[0];
        auto lastColumnPosition = columnPositions[0] + masonryContentSize;
        columnPositions = Vector<LayoutUnit> { firstColumnPosition, lastColumnPosition };
    }

    float gridStartX = columnPositions[0];
    float gridEndX = columnPositions[columnPositions.size() - 1];
    float gridStartY = rowPositions[0];
    float gridEndY = rowPositions[rowPositions.size() - 1];

    auto* containingFrame = node->document().frame();
    if (!containingFrame)
        return { };
    auto* containingView = containingFrame->view();

    auto computedStyle = node->computedStyle();
    if (!computedStyle)
        return { };

    auto isHorizontalWritingMode = computedStyle->isHorizontalWritingMode();
    auto isDirectionFlipped = !computedStyle->isLeftToRightDirection();
    auto isWritingModeFlipped = computedStyle->isFlippedBlocksWritingMode();
    auto contentBox = renderGrid.absoluteBoundingBoxRectIgnoringTransforms();

    auto columnLineAt = [&](float x) -> FloatLine {
        FloatPoint startPoint;
        FloatPoint endPoint;
        if (isHorizontalWritingMode) {
            startPoint = { isDirectionFlipped ? contentBox.width() - x : x, isWritingModeFlipped ? contentBox.height() - gridStartY : gridStartY };
            endPoint = { isDirectionFlipped ? contentBox.width() - x : x, isWritingModeFlipped ? contentBox.height() - gridEndY : gridEndY };
        } else {
            startPoint = { isWritingModeFlipped ? contentBox.width() - gridStartY : gridStartY, isDirectionFlipped ? contentBox.height() - x : x };
            endPoint = { isWritingModeFlipped ? contentBox.width() - gridEndY : gridEndY, isDirectionFlipped ? contentBox.height() - x : x };
        }
        return {
            localPointToRootPoint(containingView, renderGrid.localToContainerPoint(startPoint, nullptr)),
            localPointToRootPoint(containingView, renderGrid.localToContainerPoint(endPoint, nullptr)),
        };
    };
    auto rowLineAt = [&](float y) -> FloatLine {
        FloatPoint startPoint;
        FloatPoint endPoint;
        if (isHorizontalWritingMode) {
            startPoint = { isDirectionFlipped ? contentBox.width() - gridStartX : gridStartX, isWritingModeFlipped ? contentBox.height() - y : y };
            endPoint = { isDirectionFlipped ? contentBox.width() - gridEndX : gridEndX, isWritingModeFlipped ? contentBox.height() - y : y };
        } else {
            startPoint = { isWritingModeFlipped ? contentBox.width() - y : y, isDirectionFlipped ? contentBox.height() - gridStartX : gridStartX };
            endPoint = { isWritingModeFlipped ? contentBox.width() - y : y, isDirectionFlipped ? contentBox.height() - gridEndX : gridEndX };
        }
        return {
            localPointToRootPoint(containingView, renderGrid.localToContainerPoint(startPoint, nullptr)),
            localPointToRootPoint(containingView, renderGrid.localToContainerPoint(endPoint, nullptr)),
        };
    };

    auto correctedArrowDirection = [&](InspectorOverlayLabel::Arrow::Direction direction, GridTrackSizingDirection sizingDirection) -> InspectorOverlayLabel::Arrow::Direction {
        if ((sizingDirection == GridTrackSizingDirection::ForColumns && isWritingModeFlipped) || (sizingDirection == GridTrackSizingDirection::ForRows && isDirectionFlipped)) {
            switch (direction) {
            case InspectorOverlayLabel::Arrow::Direction::Down:
                direction = InspectorOverlayLabel::Arrow::Direction::Up;
                break;
            case InspectorOverlayLabel::Arrow::Direction::Up:
                direction = InspectorOverlayLabel::Arrow::Direction::Down;
                break;
            case InspectorOverlayLabel::Arrow::Direction::Left:
                direction = InspectorOverlayLabel::Arrow::Direction::Right;
                break;
            case InspectorOverlayLabel::Arrow::Direction::Right:
                direction = InspectorOverlayLabel::Arrow::Direction::Left;
                break;
            case InspectorOverlayLabel::Arrow::Direction::None:
                break;
            }
        }

        if (!isHorizontalWritingMode) {
            switch (direction) {
            case InspectorOverlayLabel::Arrow::Direction::Down:
                direction = InspectorOverlayLabel::Arrow::Direction::Right;
                break;
            case InspectorOverlayLabel::Arrow::Direction::Up:
                direction = InspectorOverlayLabel::Arrow::Direction::Left;
                break;
            case InspectorOverlayLabel::Arrow::Direction::Left:
                direction = InspectorOverlayLabel::Arrow::Direction::Up;
                break;
            case InspectorOverlayLabel::Arrow::Direction::Right:
                direction = InspectorOverlayLabel::Arrow::Direction::Down;
                break;
            case InspectorOverlayLabel::Arrow::Direction::None:
                break;
            }
        }

        return direction;
    };

    auto correctedArrowAlignment = [&](InspectorOverlayLabel::Arrow::Alignment alignment, GridTrackSizingDirection sizingDirection) -> InspectorOverlayLabel::Arrow::Alignment {
        if ((sizingDirection == GridTrackSizingDirection::ForRows && isWritingModeFlipped) || (sizingDirection == GridTrackSizingDirection::ForColumns && isDirectionFlipped)) {
            if (alignment == InspectorOverlayLabel::Arrow::Alignment::Leading)
                return InspectorOverlayLabel::Arrow::Alignment::Trailing;
            if (alignment == InspectorOverlayLabel::Arrow::Alignment::Trailing)
                return InspectorOverlayLabel::Arrow::Alignment::Leading;
        }

        return alignment;
    };

    InspectorOverlay::Highlight::GridHighlightOverlay gridHighlightOverlay;
    gridHighlightOverlay.color = gridOverlay.config.gridColor;

    // Draw columns and rows.
    auto columnWidths = renderGrid.trackSizesForComputedStyle(GridTrackSizingDirection::ForColumns);
    auto columnLineNames = gridLineNames(node->renderStyle(), GridTrackSizingDirection::ForColumns, columnPositions.size());
    auto authoredTrackColumnSizes = authoredGridTrackSizes(node, GridTrackSizingDirection::ForColumns, columnWidths.size());
    FloatLine previousColumnEndLine;
    for (unsigned i = 0; i < columnPositions.size(); ++i) {
        auto columnStartLine = columnLineAt(columnPositions[i]);

        if (gridOverlay.config.showExtendedGridLines) {
            auto extendedLine = columnStartLine.extendedToBounds(viewportBounds);
            gridHighlightOverlay.gridLines.append(extendedLine);
        } else {
            gridHighlightOverlay.gridLines.append(columnStartLine);
        }

        // Draw only the bounding lines of the masonry axis.
        if (renderGrid.areMasonryColumns())
            continue;
        
        FloatLine gapLabelLine = columnStartLine;
        if (i) {
            gridHighlightOverlay.gaps.append({ previousColumnEndLine.start(), columnStartLine.start(), columnStartLine.end(), previousColumnEndLine.end() });
            FloatLine lineBetweenColumnTops = { columnStartLine.start(), previousColumnEndLine.start() };
            FloatLine lineBetweenColumnBottoms = { columnStartLine.end(), previousColumnEndLine.end() };
            gapLabelLine = { lineBetweenColumnTops.pointAtRelativeDistance(0.5), lineBetweenColumnBottoms.pointAtRelativeDistance(0.5) };
        }

        if (i < columnWidths.size() && i < columnPositions.size()) {
            auto width = columnWidths[i];
            auto columnEndLine = columnLineAt(columnPositions[i] + width);

            if (gridOverlay.config.showExtendedGridLines) {
                auto extendedLine = columnEndLine.extendedToBounds(viewportBounds);
                gridHighlightOverlay.gridLines.append(extendedLine);
            } else {
                gridHighlightOverlay.gridLines.append(columnEndLine);
            }
            previousColumnEndLine = columnEndLine;
            
            if (gridOverlay.config.showTrackSizes) {
                auto authoredTrackSize = i < authoredTrackColumnSizes.size() ? authoredTrackColumnSizes[i] : "auto"_s;
                FloatLine trackTopLine = { columnStartLine.start(), columnEndLine.start() };
                gridHighlightOverlay.labels.append({ authoredTrackSize, trackTopLine.pointAtRelativeDistance(0.5), translucentLabelBackgroundColor, { correctedArrowDirection(InspectorOverlayLabel::Arrow::Direction::Up, GridTrackSizingDirection::ForColumns), InspectorOverlayLabel::Arrow::Alignment::Middle } });
            }
        } else
            previousColumnEndLine = columnStartLine;

        StringBuilder lineLabel;
        if (gridOverlay.config.showLineNumbers) {
            lineLabel.append(i + 1);
            if (i <= authoredTrackColumnSizes.size())
                lineLabel.append(emSpace, -static_cast<int>(authoredTrackColumnSizes.size() - i + 1));
        }
        if (gridOverlay.config.showLineNames) {
            for (auto lineName : columnLineNames.map.get(i)) {
                if (!lineLabel.isEmpty())
                    lineLabel.append(thinSpace, bullet, thinSpace);
                lineLabel.append(lineName);
            }
        }

        if (!lineLabel.isEmpty()) {
            auto text = lineLabel.toString();
            auto arrowDirection = correctedArrowDirection(InspectorOverlayLabel::Arrow::Direction::Down, GridTrackSizingDirection::ForColumns);
            auto arrowAlignment = correctedArrowAlignment(InspectorOverlayLabel::Arrow::Alignment::Middle, GridTrackSizingDirection::ForColumns);

            if (!i)
                arrowAlignment = correctedArrowAlignment(InspectorOverlayLabel::Arrow::Alignment::Leading, GridTrackSizingDirection::ForColumns);
            else if (i == columnPositions.size() - 1)
                arrowAlignment = correctedArrowAlignment(InspectorOverlayLabel::Arrow::Alignment::Trailing, GridTrackSizingDirection::ForColumns);

            auto expectedLabelSize = InspectorOverlayLabel::expectedSize(text, arrowDirection);
            auto gapLabelPosition = gapLabelLine.start();

            // The area under the window's toolbar is drawable, but not meaningfully visible, so we must account for that space.
            auto topEdgeInset = pageView->topContentInset(ScrollView::TopContentInsetType::WebCoreOrPlatformContentInset);
            if (gapLabelLine.start().y() - expectedLabelSize.height() - topEdgeInset + scrollPosition.y() - viewportBounds.y() < 0) {
                arrowDirection = correctedArrowDirection(InspectorOverlayLabel::Arrow::Direction::Up, GridTrackSizingDirection::ForColumns);

                // Special case for the first column to make sure the label will be out of the way of the first row's label.
                // The label heights will be the same, as they use the same font, so moving down by this label's size will
                // create enough space for this special circumstance.
                if (!i)
                    gapLabelPosition = gapLabelLine.pointAtAbsoluteDistance(expectedLabelSize.height());
            }

            gridHighlightOverlay.labels.append({ text, gapLabelPosition, translucentLabelBackgroundColor, { arrowDirection, arrowAlignment } });
        }
    }

    auto rowHeights = renderGrid.trackSizesForComputedStyle(GridTrackSizingDirection::ForRows);
    auto rowLineNames = gridLineNames(node->renderStyle(), GridTrackSizingDirection::ForRows, rowPositions.size());
    auto authoredTrackRowSizes = authoredGridTrackSizes(node, GridTrackSizingDirection::ForRows, rowHeights.size());
    FloatLine previousRowEndLine;
    for (unsigned i = 0; i < rowPositions.size(); ++i) {
        auto rowStartLine = rowLineAt(rowPositions[i]);

        if (gridOverlay.config.showExtendedGridLines) {
            auto extendedLine = rowStartLine.extendedToBounds(viewportBounds);
            gridHighlightOverlay.gridLines.append(extendedLine);
        } else {
            gridHighlightOverlay.gridLines.append(rowStartLine);
        }

        // Draw only the bounding lines of the masonry axis.
        if (renderGrid.areMasonryRows())
            continue;

        FloatPoint gapLabelPosition = rowStartLine.start();
        if (i) {
            FloatLine lineBetweenRowStarts = { rowStartLine.start(), previousRowEndLine.start() };
            gridHighlightOverlay.gaps.append({ previousRowEndLine.start(), previousRowEndLine.end(), rowStartLine.end(), rowStartLine.start() });
            gapLabelPosition = lineBetweenRowStarts.pointAtRelativeDistance(0.5);
        }

        if (i < rowHeights.size() && i < rowPositions.size()) {
            auto height = rowHeights[i];
            auto rowEndLine = rowLineAt(rowPositions[i] + height);

            if (gridOverlay.config.showExtendedGridLines) {
                auto extendedLine = rowEndLine.extendedToBounds(viewportBounds);
                gridHighlightOverlay.gridLines.append(extendedLine);
            } else {
                gridHighlightOverlay.gridLines.append(rowEndLine);
            }
            previousRowEndLine = rowEndLine;

            if (gridOverlay.config.showTrackSizes) {
                auto authoredTrackSize = i < authoredTrackRowSizes.size() ? authoredTrackRowSizes[i] : "auto"_s;
                FloatLine trackLeftLine = { rowStartLine.start(), rowEndLine.start() };
                gridHighlightOverlay.labels.append({ authoredTrackSize, trackLeftLine.pointAtRelativeDistance(0.5), translucentLabelBackgroundColor, { correctedArrowDirection(InspectorOverlayLabel::Arrow::Direction::Left, GridTrackSizingDirection::ForRows), InspectorOverlayLabel::Arrow::Alignment::Middle } });
            }
        } else
            previousRowEndLine = rowStartLine;

        StringBuilder lineLabel;
        if (gridOverlay.config.showLineNumbers) {
            lineLabel.append(i + 1);
            if (i <= authoredTrackRowSizes.size())
                lineLabel.append(emSpace, -static_cast<int>(authoredTrackRowSizes.size() - i + 1));
        }
        if (gridOverlay.config.showLineNames) {
            for (auto lineName : rowLineNames.map.get(i)) {
                if (!lineLabel.isEmpty())
                    lineLabel.append(thinSpace, bullet, thinSpace);
                lineLabel.append(lineName);
            }
        }

        if (!lineLabel.isEmpty()) {
            auto text = lineLabel.toString();
            auto arrowDirection = correctedArrowDirection(InspectorOverlayLabel::Arrow::Direction::Right, GridTrackSizingDirection::ForRows);
            auto arrowAlignment = correctedArrowAlignment(InspectorOverlayLabel::Arrow::Alignment::Middle, GridTrackSizingDirection::ForRows);

            if (!i)
                arrowAlignment = correctedArrowAlignment(InspectorOverlayLabel::Arrow::Alignment::Leading, GridTrackSizingDirection::ForRows);
            else if (i == rowPositions.size() - 1)
                arrowAlignment = correctedArrowAlignment(InspectorOverlayLabel::Arrow::Alignment::Trailing, GridTrackSizingDirection::ForRows);

            auto expectedLabelSize = InspectorOverlayLabel::expectedSize(text, arrowDirection);
            if (gapLabelPosition.x() - expectedLabelSize.width() + scrollPosition.x() - viewportBounds.x() < 0)
                arrowDirection = correctedArrowDirection(InspectorOverlayLabel::Arrow::Direction::Left, GridTrackSizingDirection::ForRows);

            gridHighlightOverlay.labels.append({ text, gapLabelPosition, translucentLabelBackgroundColor, { arrowDirection, arrowAlignment } });
        }
    }

    if (gridOverlay.config.showAreaNames && !renderGrid.isMasonry()) {
        for (auto& gridArea : node->renderStyle()->namedGridArea().map) {
            auto& name = gridArea.key;
            auto& area = gridArea.value;

            // Named grid areas will always be rectangular per the CSS Grid specification.
            auto columnStartLine = columnLineAt(columnPositions[area.columns.startLine()]);
            auto columnEndLine = columnLineAt(columnPositions[area.columns.endLine() - 1] + columnWidths[area.columns.endLine() - 1]);
            auto rowStartLine = rowLineAt(rowPositions[area.rows.startLine()]);
            auto rowEndLine = rowLineAt(rowPositions[area.rows.endLine() - 1] + rowHeights[area.rows.endLine() - 1]);

            std::optional<FloatPoint> topLeft = columnStartLine.intersectionWith(rowStartLine);
            std::optional<FloatPoint> topRight = columnEndLine.intersectionWith(rowStartLine);
            std::optional<FloatPoint> bottomRight = columnEndLine.intersectionWith(rowEndLine);
            std::optional<FloatPoint> bottomLeft = columnStartLine.intersectionWith(rowEndLine);

            // If any two lines are coincident with each other, they will not have an intersection, which can occur with extreme `transform: perspective(...)` values.
            if (!topLeft || !topRight || !bottomRight || !bottomLeft)
                continue;

            InspectorOverlay::Highlight::GridHighlightOverlay::Area highlightOverlayArea;
            highlightOverlayArea.name = name;
            highlightOverlayArea.quad = { *topLeft, *topRight, *bottomRight, *bottomLeft };
            gridHighlightOverlay.areas.append(highlightOverlayArea);
        }
    }

    return { gridHighlightOverlay };
}

void InspectorOverlay::drawFlexOverlay(GraphicsContext& context, const InspectorOverlay::Highlight::FlexHighlightOverlay& flexHighlightOverlay)
{
    GraphicsContextStateSaver saver(context);
    context.setStrokeThickness(1);
    context.setStrokeColor(flexHighlightOverlay.color);
    context.strokePath(quadToPath(flexHighlightOverlay.containerBounds));

    for (const auto& bounds : flexHighlightOverlay.itemBounds)
        context.strokePath(quadToPath(bounds));

    for (const auto& mainAxisGap : flexHighlightOverlay.mainAxisGaps) {
        context.strokePath(quadToPath(mainAxisGap));
        drawLayoutHatching(context, mainAxisGap);
    }

    {
        GraphicsContextStateSaver mainAxisSpaceContextSaver(context);
        context.setAlpha(0.5);

        constexpr auto mainAxisSpaceDensity = 3;
        for (auto mainAxisSpaceBetweenItemAndGap : flexHighlightOverlay.mainAxisSpaceBetweenItemsAndGaps)
            drawLayoutStippling(context, mainAxisSpaceBetweenItemAndGap, mainAxisSpaceDensity);
    }

    for (const auto& crossAxisGap : flexHighlightOverlay.crossAxisGaps) {
        context.strokePath(quadToPath(crossAxisGap));
        drawLayoutHatching(context, crossAxisGap, Flip::Yes);
    }

    context.setAlpha(0.7);
    constexpr auto spaceBetweenItemsAndCrossAxisSpaceStipplingDensity = 6;
    for (const auto& crossAxisSpaceBetweenItemAndGap : flexHighlightOverlay.spaceBetweenItemsAndCrossAxisSpace)
        drawLayoutStippling(context, crossAxisSpaceBetweenItemAndGap, spaceBetweenItemsAndCrossAxisSpaceStipplingDensity);

    for (auto label : flexHighlightOverlay.labels)
        label.draw(context);
}

std::optional<InspectorOverlay::Highlight::FlexHighlightOverlay> InspectorOverlay::buildFlexOverlay(const InspectorOverlay::Flex& flexOverlay)
{
    // If the node WeakPtr has been cleared, then the node is gone and there's nothing to draw.
    if (!flexOverlay.flexNode) {
        m_activeFlexOverlays.removeAllMatching([&] (const InspectorOverlay::Flex& flexOverlay) {
            return !flexOverlay.flexNode;
        });
        return { };
    }

    // Always re-check because the node's renderer may have changed since being added.
    // If renderer is no longer a flex, then remove the flex overlay for the node.
    Node* node = flexOverlay.flexNode.get();
    auto renderer = node->renderer();
    if (!is<RenderFlexibleBox>(renderer)) {
        removeFlexOverlayForNode(*node);
        return { };
    }

    auto& renderFlex = *downcast<RenderFlexibleBox>(renderer);

    auto itemsAtStartOfLine = m_page.inspectorController().ensureDOMAgent().flexibleBoxRendererCachedItemsAtStartOfLine(renderFlex);

    auto* containingFrame = node->document().frame();
    if (!containingFrame)
        return { };
    auto* containingView = containingFrame->view();

    auto computedStyle = node->computedStyle();
    if (!computedStyle)
        return { };

    auto wasRowDirection = !computedStyle->isColumnFlexDirection();
    auto isFlippedBlocksWritingMode = computedStyle->isFlippedBlocksWritingMode();
    auto isRightToLeftDirection = computedStyle->direction() == TextDirection::RTL;

    auto isRowDirection = wasRowDirection ^ !computedStyle->isHorizontalWritingMode();
    auto isMainAxisDirectionReversed = computedStyle->isReverseFlexDirection() ^ (wasRowDirection ? isRightToLeftDirection : isFlippedBlocksWritingMode);
    auto isCrossAxisDirectionReversed = (computedStyle->flexWrap() == FlexWrap::Reverse) ^ (wasRowDirection ? isFlippedBlocksWritingMode : isRightToLeftDirection);

    auto localQuadToRootQuad = [&](const FloatQuad& quad) {
        return FloatQuad(
            localPointToRootPoint(containingView, quad.p1()),
            localPointToRootPoint(containingView, quad.p2()),
            localPointToRootPoint(containingView, quad.p3()),
            localPointToRootPoint(containingView, quad.p4())
        );
    };

    auto childQuadToRootQuad = [&](const FloatQuad& quad) {
        return FloatQuad(
            localPointToRootPoint(containingView, renderFlex.localToContainerPoint(quad.p1(), nullptr)),
            localPointToRootPoint(containingView, renderFlex.localToContainerPoint(quad.p2(), nullptr)),
            localPointToRootPoint(containingView, renderFlex.localToContainerPoint(quad.p3(), nullptr)),
            localPointToRootPoint(containingView, renderFlex.localToContainerPoint(quad.p4(), nullptr))
        );
    };

    auto correctedMainAxisLeadingEdge = [&](const LayoutRect& rect) {
        if (isRowDirection)
            return isMainAxisDirectionReversed ? rect.maxX() : rect.x();
        return isMainAxisDirectionReversed ? rect.maxY() : rect.y();
    };

    auto correctedMainAxisTrailingEdge = [&](const LayoutRect& rect) {
        if (isRowDirection)
            return isMainAxisDirectionReversed ? rect.x() : rect.maxX();
        return isMainAxisDirectionReversed ? rect.y() : rect.maxY();
    };

    auto correctedCrossAxisLeadingEdge = [&](const LayoutRect& rect) {
        if (isRowDirection)
            return isCrossAxisDirectionReversed ? rect.maxY() : rect.y();
        return isCrossAxisDirectionReversed ? rect.maxX() : rect.x();
    };

    auto correctedCrossAxisTrailingEdge = [&](const LayoutRect& rect) {
        if (isRowDirection)
            return isCrossAxisDirectionReversed ? rect.y() : rect.maxY();
        return isCrossAxisDirectionReversed ? rect.x() : rect.maxX();
    };

    auto correctedCrossAxisMin = [&](float a, float b) {
        return isCrossAxisDirectionReversed ? std::fmax(a, b) : std::fmin(a, b);
    };

    auto correctedCrossAxisMax = [&](float a, float b) {
        return isCrossAxisDirectionReversed ? std::fmin(a, b) : std::fmax(a, b);
    };

    auto correctedPoint = [&](float mainAxisLocation, float crossAxisLocation) {
        return isRowDirection ? FloatPoint(mainAxisLocation, crossAxisLocation) : FloatPoint(crossAxisLocation, mainAxisLocation);
    };

    auto populateHighlightForGapOrSpace = [&](float fromMainAxisEdge, float toMainAxisEdge, float fromCrossAxisEdge, float toCrossAxisEdge, Vector<FloatQuad>& gapsSet) {
        gapsSet.append(childQuadToRootQuad({
            correctedPoint(fromMainAxisEdge, fromCrossAxisEdge),
            correctedPoint(toMainAxisEdge, fromCrossAxisEdge),
            correctedPoint(toMainAxisEdge, toCrossAxisEdge),
            correctedPoint(fromMainAxisEdge, toCrossAxisEdge),
        }));
    };

    InspectorOverlay::Highlight::FlexHighlightOverlay flexHighlightOverlay;
    flexHighlightOverlay.color = flexOverlay.config.flexColor;
    flexHighlightOverlay.containerBounds = localQuadToRootQuad(renderFlex.absoluteContentQuad());

    float computedMainAxisGap = renderFlex.computeGap(RenderFlexibleBox::GapType::BetweenItems).toFloat();
    float computedCrossAxisGap = renderFlex.computeGap(RenderFlexibleBox::GapType::BetweenLines).toFloat();

    // For reasoning about the edges of the flex container, use the untransformed content rect moved to the origin of the
    // inner top-left corner of padding, which is the same relative coordinate space that each item's `frameRect()` will be in.
    auto containerRect = renderFlex.absoluteContentBox();
    containerRect.setLocation({ renderFlex.paddingLeft() + renderFlex.borderLeft(), renderFlex.paddingTop() + renderFlex.borderTop() });

    float containerMainAxisLeadingEdge = correctedMainAxisLeadingEdge(containerRect);
    float containerMainAxisTrailingEdge = correctedMainAxisTrailingEdge(containerRect);

    Vector<LayoutRect> currentLineChildrenRects;
    float currentLineCrossAxisLeadingEdge = isCrossAxisDirectionReversed ? 0.0f : std::numeric_limits<float>::max();
    float currentLineCrossAxisTrailingEdge = isCrossAxisDirectionReversed ? std::numeric_limits<float>::max() : 0.0f;
    float previousLineCrossAxisTrailingEdge = correctedCrossAxisLeadingEdge(containerRect);

    Vector<RenderBox*> renderChildrenInFlexOrder;
    Vector<RenderObject*> renderChildrenInDOMOrder;
    bool hasCustomOrder = false;

    auto childOrderIterator = renderFlex.orderIterator();
    for (RenderBox* renderChild = childOrderIterator.first(); renderChild; renderChild = childOrderIterator.next()) {
        if (childOrderIterator.shouldSkipChild(*renderChild))
            continue;
        renderChildrenInFlexOrder.append(renderChild);
    }

    if (flexOverlay.config.showOrderNumbers) {
        for (auto* child = node->firstChild(); child; child = child->nextSibling()) {
            if (auto* renderer = child->renderer()) {
                if (!renderChildrenInFlexOrder.contains(renderer))
                    continue;

                renderChildrenInDOMOrder.append(renderer);

                if (renderer->style().order())
                    hasCustomOrder = true;
            }
        }
    }

    size_t currentChildIndex = 0;
    for (auto* renderChild : renderChildrenInFlexOrder) {
        // Build bounds for each child and collect children on the same logical line.
        {
            auto childRect = renderChild->frameRect();
            renderFlex.flipForWritingMode(childRect);
            childRect.expand(renderChild->marginBox());

            auto itemBounds = childQuadToRootQuad({ childRect });
            flexHighlightOverlay.itemBounds.append(itemBounds);

            if (flexOverlay.config.showOrderNumbers) {
                StringBuilder orderNumbers;

                if (auto index = renderChildrenInDOMOrder.find(renderChild); index != notFound)
                    orderNumbers.append("Item #"_s, index + 1);

                if (auto order = renderChild->style().order(); order || hasCustomOrder) {
                    if (!orderNumbers.isEmpty())
                        orderNumbers.append('\n');
                    orderNumbers.append("order: "_s, order);
                }

                if (!orderNumbers.isEmpty())
                    flexHighlightOverlay.labels.append({ orderNumbers.toString(), itemBounds.center(), Color::white.colorWithAlphaByte(230), { InspectorOverlayLabel::Arrow::Direction::None, InspectorOverlayLabel::Arrow::Alignment::None } });
            }

            currentLineCrossAxisLeadingEdge = correctedCrossAxisMin(currentLineCrossAxisLeadingEdge, correctedCrossAxisLeadingEdge(childRect));
            currentLineCrossAxisTrailingEdge = correctedCrossAxisMax(currentLineCrossAxisTrailingEdge, correctedCrossAxisTrailingEdge(childRect));

            currentLineChildrenRects.append(WTFMove(childRect));
            ++currentChildIndex;
        }

        // The remaining work can only be done once we have collected all of the children on the current line.
        if (!itemsAtStartOfLine.contains(currentChildIndex))
            continue;

        float previousChildMainAxisTrailingEdge = correctedMainAxisLeadingEdge(containerRect);
        for (const auto& childRect : currentLineChildrenRects) {
            auto childMainAxisLeadingEdge = correctedMainAxisLeadingEdge(childRect);
            auto childMainAxisTrailingEdge = correctedMainAxisTrailingEdge(childRect);
            auto childCrossAxisLeadingEdge = correctedCrossAxisLeadingEdge(childRect);
            auto childCrossAxisTrailingEdge = correctedCrossAxisTrailingEdge(childRect);

            // Build bounds for space between the current item and the cross-axis space.
            if (std::abs(childCrossAxisLeadingEdge - currentLineCrossAxisLeadingEdge) > 1)
                populateHighlightForGapOrSpace(childMainAxisLeadingEdge, childMainAxisTrailingEdge, currentLineCrossAxisLeadingEdge, childCrossAxisLeadingEdge, flexHighlightOverlay.spaceBetweenItemsAndCrossAxisSpace);
            if (std::abs(childCrossAxisTrailingEdge - currentLineCrossAxisTrailingEdge) > 1)
                populateHighlightForGapOrSpace(childMainAxisLeadingEdge, childMainAxisTrailingEdge, currentLineCrossAxisTrailingEdge, childCrossAxisTrailingEdge, flexHighlightOverlay.spaceBetweenItemsAndCrossAxisSpace);

            // Build bounds for gaps and space between the current item and previous item (or container edge).
            if (computedMainAxisGap && previousChildMainAxisTrailingEdge != correctedMainAxisLeadingEdge(containerRect)) {
                // Regardless of flipped axises, we need to do the below calculations from left to right or top to bottom.
                float startEdge = std::fmin(previousChildMainAxisTrailingEdge, childMainAxisLeadingEdge);
                float endEdge = std::fmax(previousChildMainAxisTrailingEdge, childMainAxisLeadingEdge);

                float spaceBetweenEdgeAndGap = (endEdge - startEdge - computedMainAxisGap) / 2;

                populateHighlightForGapOrSpace(startEdge, startEdge + spaceBetweenEdgeAndGap, currentLineCrossAxisLeadingEdge, currentLineCrossAxisTrailingEdge, flexHighlightOverlay.mainAxisSpaceBetweenItemsAndGaps);
                populateHighlightForGapOrSpace(startEdge + spaceBetweenEdgeAndGap, endEdge - spaceBetweenEdgeAndGap, currentLineCrossAxisLeadingEdge, currentLineCrossAxisTrailingEdge, flexHighlightOverlay.mainAxisGaps);
                populateHighlightForGapOrSpace(endEdge - spaceBetweenEdgeAndGap, endEdge, currentLineCrossAxisLeadingEdge, currentLineCrossAxisTrailingEdge, flexHighlightOverlay.mainAxisSpaceBetweenItemsAndGaps);
            } else
                populateHighlightForGapOrSpace(previousChildMainAxisTrailingEdge, childMainAxisLeadingEdge, currentLineCrossAxisLeadingEdge, currentLineCrossAxisTrailingEdge, flexHighlightOverlay.mainAxisSpaceBetweenItemsAndGaps);

            previousChildMainAxisTrailingEdge = childMainAxisTrailingEdge;
        }
        populateHighlightForGapOrSpace(previousChildMainAxisTrailingEdge, containerMainAxisTrailingEdge, currentLineCrossAxisLeadingEdge, currentLineCrossAxisTrailingEdge, flexHighlightOverlay.mainAxisSpaceBetweenItemsAndGaps);

        // Build gaps between the current line and the previous line.
        if (computedCrossAxisGap && previousLineCrossAxisTrailingEdge != correctedCrossAxisLeadingEdge(containerRect)) {
            // Regardless of flipped axises, we need to do the below calculations from left to right or top to bottom.
            float startEdge = std::fmin(previousLineCrossAxisTrailingEdge, currentLineCrossAxisLeadingEdge);
            float endEdge = std::fmax(previousLineCrossAxisTrailingEdge, currentLineCrossAxisLeadingEdge);

            float spaceBetweenEdgeAndGap = (endEdge - startEdge - computedCrossAxisGap) / 2;

            populateHighlightForGapOrSpace(containerMainAxisLeadingEdge, containerMainAxisTrailingEdge, startEdge + spaceBetweenEdgeAndGap, endEdge - spaceBetweenEdgeAndGap, flexHighlightOverlay.crossAxisGaps);
        }

        previousLineCrossAxisTrailingEdge = currentLineCrossAxisTrailingEdge;

        currentLineChildrenRects.clear();
        currentLineCrossAxisLeadingEdge = isCrossAxisDirectionReversed ? 0.0f : std::numeric_limits<float>::max();
        currentLineCrossAxisTrailingEdge = isCrossAxisDirectionReversed ? std::numeric_limits<float>::max() : 0.0f;
    }

    return { flexHighlightOverlay };
}

} // namespace WebCore
