blob: 4d85d617c30098bd2cce973e12913031191c78b7 [file] [log] [blame]
/*
* 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 "Frame.h"
#include "FrameView.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 "LocalizedStrings.h"
#include "Node.h"
#include "NodeList.h"
#include "NodeRenderStyle.h"
#include "OrderIterator.h"
#include "Page.h"
#include "PseudoElement.h"
#include "RenderBox.h"
#include "RenderBoxModelObject.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/StringBuilder.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;
static constexpr UChar bullet = 0x2022;
static constexpr UChar ellipsis = 0x2026;
static constexpr UChar multiplicationSign = 0x00D7;
static constexpr UChar thinSpace = 0x2009;
static constexpr UChar emSpace = 0x2003;
enum class Flip : bool { No, Yes };
static void truncateWithEllipsis(String& string, size_t length)
{
if (string.length() > length)
string = makeString(StringView(string).left(length), ellipsis);
}
static FloatPoint localPointToRootPoint(const FrameView* view, const FloatPoint& point)
{
return view->contentsToRootView(point);
}
static void contentsQuadToCoordinateSystem(const FrameView* mainView, const FrameView* 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)
{
Frame* containingFrame = renderer->document().frame();
if (!containingFrame)
return;
highlight.setDataFromConfig(highlightConfig);
FrameView* containingView = containingFrame->view();
FrameView* mainView = containingFrame->page()->mainFrame().view();
// (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->isSVGRootOrLegacySVGRoot();
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)
{
RenderObject* renderer = node.renderer();
if (!renderer)
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)
{
RenderObject* renderer = node.renderer();
if (!renderer || !is<RenderBox>(renderer))
return;
const ShapeOutsideInfo* shapeOutsideInfo = downcast<RenderBox>(renderer)->shapeOutsideInfo();
if (!shapeOutsideInfo)
return;
Frame* containingFrame = node.document().frame();
if (!containingFrame)
return;
FrameView* containingView = containingFrame->view();
FrameView* mainView = containingFrame->page()->mainFrame().view();
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.apply([&] (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;
FloatSize viewportSize = m_page.mainFrame().view()->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_highlightNode) {
auto nodeRulerExclusion = drawNodeHighlight(context, *m_highlightNode);
rulerExclusion.bounds.unite(nodeRulerExclusion.bounds);
rulerExclusion.titlePath = nodeRulerExclusion.titlePath;
}
for (const InspectorOverlay::Grid& gridOverlay : m_activeGridOverlays) {
if (auto gridHighlightOverlay = buildGridOverlay(gridOverlay))
drawGridOverlay(context, *gridHighlightOverlay);
}
for (const InspectorOverlay::Flex& flexOverlay : m_activeFlexOverlays) {
if (auto flexHighlightOverlay = buildFlexOverlay(flexOverlay))
drawFlexOverlay(context, *flexHighlightOverlay);
}
if (!m_paintRects.isEmpty())
drawPaintRects(context, m_paintRects);
if (m_showRulers || m_showRulersDuringElementSelection)
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;
highlight.type = InspectorOverlay::Highlight::Type::None;
if (m_highlightNode)
buildNodeHighlight(*m_highlightNode, m_nodeHighlightConfig, highlight, coordinateSystem);
else if (m_highlightNodeList) {
highlight.setDataFromConfig(m_nodeHighlightConfig);
for (unsigned i = 0; i < m_highlightNodeList->length(); ++i) {
InspectorOverlay::Highlight nodeHighlight;
buildNodeHighlight(*(m_highlightNodeList->item(i)), m_nodeHighlightConfig, nodeHighlight, coordinateSystem);
if (nodeHighlight.type == InspectorOverlay::Highlight::Type::Node)
highlight.quads.appendVector(nodeHighlight.quads);
}
highlight.type = InspectorOverlay::Highlight::Type::NodeList;
} else if (m_highlightQuad) {
highlight.type = InspectorOverlay::Highlight::Type::Rects;
buildQuadHighlight(*m_highlightQuad, m_quadHighlightConfig, highlight);
}
constexpr bool offsetBoundsByScroll = true;
for (const InspectorOverlay::Grid& gridOverlay : m_activeGridOverlays) {
if (auto gridHighlightOverlay = buildGridOverlay(gridOverlay, offsetBoundsByScroll))
highlight.gridHighlightOverlays.append(*gridHighlightOverlay);
}
for (const InspectorOverlay::Flex& flexOverlay : m_activeFlexOverlays) {
if (auto flexHighlightOverlay = buildFlexOverlay(flexOverlay))
highlight.flexHighlightOverlays.append(*flexHighlightOverlay);
}
}
void InspectorOverlay::hideHighlight()
{
m_highlightNode = nullptr;
m_highlightNodeList = nullptr;
m_highlightQuad = nullptr;
update();
}
void InspectorOverlay::highlightNodeList(RefPtr<NodeList>&& nodes, const InspectorOverlay::Highlight::Config& highlightConfig)
{
m_nodeHighlightConfig = highlightConfig;
m_highlightNodeList = WTFMove(nodes);
m_highlightNode = nullptr;
update();
}
void InspectorOverlay::highlightNode(Node* node, const InspectorOverlay::Highlight::Config& highlightConfig)
{
m_nodeHighlightConfig = highlightConfig;
m_highlightNode = node;
m_highlightNodeList = nullptr;
update();
}
void InspectorOverlay::highlightQuad(std::unique_ptr<FloatQuad> quad, const InspectorOverlay::Highlight::Config& highlightConfig)
{
if (highlightConfig.usePageCoordinates)
*quad -= toIntSize(m_page.mainFrame().view()->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
{
// Don't show the overlay when m_showRulersDuringElementSelection is true, as it's only supposed
// to have an effect when element selection is active (e.g. a node is hovered).
return m_highlightNode || m_highlightNodeList || m_highlightQuad || m_indicating || m_showPaintRects || m_showRulers || m_activeGridOverlays.size() || m_activeFlexOverlays.size();
}
void InspectorOverlay::update()
{
if (!shouldShowOverlay()) {
m_client->hideHighlight();
return;
}
FrameView* view = m_page.mainFrame().view();
if (!view)
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;
IntRect rootRect = m_page.mainFrame().view()->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> 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> 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_showRulersDuringElementSelection)
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_showRulersDuringElementSelection)
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)
{
FrameView* pageView = m_page.mainFrame().view();
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;
FrameView* pageView = m_page.mainFrame().view();
if (!pageView->delegatesScrolling())
scrollOffset = pageView->visibleContentRect().location();
FloatSize viewportSize = pageView->sizeForVisibleContent();
FloatSize contentInset(0, pageView->topContentInset(ScrollView::TopContentInsetType::WebCoreOrPlatformContentInset));
float pageScaleFactor = m_page.pageScaleFactor();
float pageZoomFactor = m_page.mainFrame().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), 0, 0);
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().height() - 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), 0, 0);
font.update(nullptr);
auto viewportRect = pageView->visualViewportRect();
TextRun viewportTextRun(makeString(viewportRect.width() / pageZoomFactor, "px", ' ', multiplicationSign, ' ', viewportRect.height() / pageZoomFactor, "px"));
const float margin = 4;
const float padding = 2;
const float radius = 4;
float fontWidth = font.width(viewportTextRun);
float fontHeight = font.metricsOfPrimaryFont().floatHeight();
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().descent() });
}
}
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 { };
Element* element = effectiveElementForNode(node);
if (!element)
return { };
RenderObject* renderer = node.renderer();
if (!renderer)
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)) {
RenderBoxModelObject* modelObject = downcast<RenderBoxModelObject>(renderer);
elementWidth = String::number(adjustForAbsoluteZoom(roundToInt(modelObject->offsetWidth()), *modelObject));
elementHeight = String::number(adjustForAbsoluteZoom(roundToInt(modelObject->offsetHeight()), *modelObject));
} else {
FrameView* 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 (AccessibilityObject* axObject = axObjectCache->getOrCreate(&node))
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;
}
}
FrameView* pageView = m_page.mainFrame().view();
FloatSize viewportSize = pageView->sizeForVisibleContent();
FloatSize contentInset(0, pageView->topContentInset(ScrollView::TopContentInsetType::WebCoreOrPlatformContentInset));
if (m_showRulers || m_showRulersDuringElementSelection)
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)
{
if (!is<StyledElement>(node))
return { };
auto element = downcast<StyledElement>(node);
auto directionCSSPropertyID = direction == GridTrackSizingDirection::ForColumns ? CSSPropertyID::CSSPropertyGridTemplateColumns : CSSPropertyID::CSSPropertyGridTemplateRows;
RefPtr<CSSValue> cssValue = element->cssomStyle().getPropertyCSSValueInternal(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;
}
}
if (!cssValue || !is<CSSValueList>(cssValue))
return { };
Vector<String> trackSizes;
auto handleValueIgnoringLineNames = [&](const CSSValue& currentValue) {
if (!is<CSSGridLineNamesValue>(currentValue))
trackSizes.append(currentValue.cssText());
};
for (auto& currentValue : downcast<CSSValueList>(*cssValue)) {
if (is<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 : downcast<CSSValueList>(currentValue.get())) {
handleValueIgnoringLineNames(autoRepeatValue);
if (trackSizes.size() >= expectedTrackCount)
break;
}
}
break;
}
if (is<CSSGridIntegerRepeatValue>(currentValue)) {
size_t repetitions = downcast<CSSGridIntegerRepeatValue>(currentValue.get()).repetitions();
for (size_t i = 0; i < repetitions; ++i) {
for (auto& integerRepeatValue : downcast<CSSValueList>(currentValue.get()))
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 (combinedGridLineNames.contains(index)) {
auto names = combinedGridLineNames.take(index);
names.appendVector(newNames);
combinedGridLineNames.add(index, names);
} else
combinedGridLineNames.add(index, newNames);
};
auto orderedGridLineNames = direction == GridTrackSizingDirection::ForColumns ? renderStyle->orderedNamedGridColumnLines() : renderStyle->orderedNamedGridRowLines();
for (auto& [i, names] : orderedGridLineNames)
appendLineNames(i, names);
auto autoRepeatOrderedGridLineNames = direction == GridTrackSizingDirection::ForColumns ? renderStyle->autoRepeatOrderedNamedGridColumnLines() : renderStyle->autoRepeatOrderedNamedGridRowLines();
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) {
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);
FrameView* pageView = m_page.mainFrame().view();
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 { };
float gridStartX = columnPositions[0];
float gridEndX = columnPositions[columnPositions.size() - 1];
float gridStartY = rowPositions[0];
float gridEndY = rowPositions[rowPositions.size() - 1];
Frame* containingFrame = node->document().frame();
if (!containingFrame)
return { };
FrameView* 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);
}
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 && columnLineNames.contains(i)) {
for (auto lineName : columnLineNames.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);
}
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 && rowLineNames.contains(i)) {
for (auto lineName : rowLineNames.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) {
for (auto& gridArea : node->renderStyle()->namedGridArea()) {
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);
Frame* containingFrame = node->document().frame();
if (!containingFrame)
return { };
FrameView* 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 #");
orderNumbers.append(index + 1);
}
if (auto order = renderChild->style().order(); order || hasCustomOrder) {
if (!orderNumbers.isEmpty())
orderNumbers.append('\n');
orderNumbers.append("order: ");
orderNumbers.append(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::fabs(childCrossAxisLeadingEdge - currentLineCrossAxisLeadingEdge) > 1)
populateHighlightForGapOrSpace(childMainAxisLeadingEdge, childMainAxisTrailingEdge, currentLineCrossAxisLeadingEdge, childCrossAxisLeadingEdge, flexHighlightOverlay.spaceBetweenItemsAndCrossAxisSpace);
if (std::fabs(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