| /* |
| * 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 |