| /* |
| * Copyright (C) 2007, 2008 Rob Buis <buis@kde.org> |
| * Copyright (C) 2007 Nikolas Zimmermann <zimmermann@kde.org> |
| * Copyright (C) 2007 Eric Seidel <eric@webkit.org> |
| * Copyright (C) 2009 Google, Inc. All rights reserved. |
| * Copyright (C) 2009 Dirk Schulze <krit@webkit.org> |
| * Copyright (C) Research In Motion Limited 2009-2010. All rights reserved. |
| * Copyright (C) 2018 Adobe Systems Incorporated. All rights reserved. |
| * Copyright (C) 2020 Apple Inc. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "config.h" |
| #include "SVGRenderSupport.h" |
| |
| #include "ElementAncestorIterator.h" |
| #include "LegacyRenderSVGRoot.h" |
| #include "LegacyRenderSVGShape.h" |
| #include "LegacyRenderSVGTransformableContainer.h" |
| #include "NodeRenderStyle.h" |
| #include "RenderChildIterator.h" |
| #include "RenderElement.h" |
| #include "RenderGeometryMap.h" |
| #include "RenderIterator.h" |
| #include "RenderLayer.h" |
| #include "RenderSVGImage.h" |
| #include "RenderSVGResourceClipper.h" |
| #include "RenderSVGResourceFilter.h" |
| #include "RenderSVGResourceMarker.h" |
| #include "RenderSVGResourceMasker.h" |
| #include "RenderSVGRoot.h" |
| #include "RenderSVGShape.h" |
| #include "RenderSVGText.h" |
| #include "RenderSVGViewportContainer.h" |
| #include "SVGElementTypeHelpers.h" |
| #include "SVGGeometryElement.h" |
| #include "SVGResources.h" |
| #include "SVGResourcesCache.h" |
| #include "TransformState.h" |
| |
| namespace WebCore { |
| |
| LayoutRect SVGRenderSupport::clippedOverflowRectForRepaint(const RenderElement& renderer, const RenderLayerModelObject* repaintContainer) |
| { |
| // Return early for any cases where we don't actually paint |
| if (renderer.style().visibility() != Visibility::Visible && !renderer.enclosingLayer()->hasVisibleContent()) |
| return LayoutRect(); |
| |
| // Pass our local paint rect to computeFloatVisibleRectInContainer() which will |
| // map to parent coords and recurse up the parent chain. |
| FloatRect repaintRect = renderer.repaintRectInLocalCoordinates(); |
| return enclosingLayoutRect(renderer.computeFloatRectForRepaint(repaintRect, repaintContainer)); |
| } |
| |
| std::optional<FloatRect> SVGRenderSupport::computeFloatVisibleRectInContainer(const RenderElement& renderer, const FloatRect& rect, const RenderLayerModelObject* container, RenderObject::VisibleRectContext context) |
| { |
| // Ensure our parent is an SVG object. |
| ASSERT(renderer.parent()); |
| auto& parent = *renderer.parent(); |
| if (!is<SVGElement>(parent.element())) |
| return FloatRect(); |
| |
| FloatRect adjustedRect = rect; |
| adjustedRect.inflate(renderer.style().outlineWidth()); |
| |
| // Translate to coords in our parent renderer, and then call computeFloatVisibleRectInContainer() on our parent. |
| adjustedRect = renderer.localToParentTransform().mapRect(adjustedRect); |
| |
| return parent.computeFloatVisibleRectInContainer(adjustedRect, container, context); |
| } |
| |
| const RenderElement& SVGRenderSupport::localToParentTransform(const RenderElement& renderer, AffineTransform &transform) |
| { |
| ASSERT(renderer.parent()); |
| auto& parent = *renderer.parent(); |
| |
| // At the SVG/HTML boundary (aka LegacyRenderSVGRoot), we apply the localToBorderBoxTransform |
| // to map an element from SVG viewport coordinates to CSS box coordinates. |
| if (is<LegacyRenderSVGRoot>(parent)) |
| transform = downcast<LegacyRenderSVGRoot>(parent).localToBorderBoxTransform() * renderer.localToParentTransform(); |
| else |
| transform = renderer.localToParentTransform(); |
| |
| return parent; |
| } |
| |
| void SVGRenderSupport::mapLocalToContainer(const RenderElement& renderer, const RenderLayerModelObject* ancestorContainer, TransformState& transformState, bool* wasFixed) |
| { |
| AffineTransform transform; |
| auto& parent = localToParentTransform(renderer, transform); |
| |
| transformState.applyTransform(transform); |
| |
| OptionSet<MapCoordinatesMode> mode = UseTransforms; |
| parent.mapLocalToContainer(ancestorContainer, transformState, mode, wasFixed); |
| } |
| |
| const RenderElement* SVGRenderSupport::pushMappingToContainer(const RenderElement& renderer, const RenderLayerModelObject* ancestorToStopAt, RenderGeometryMap& geometryMap) |
| { |
| ASSERT_UNUSED(ancestorToStopAt, ancestorToStopAt != &renderer); |
| |
| AffineTransform transform; |
| auto& parent = localToParentTransform(renderer, transform); |
| |
| geometryMap.push(&renderer, transform); |
| return &parent; |
| } |
| |
| bool SVGRenderSupport::checkForSVGRepaintDuringLayout(const RenderElement& renderer) |
| { |
| if (!renderer.checkForRepaintDuringLayout()) |
| return false; |
| // When a parent container is transformed in SVG, all children will be painted automatically |
| // so we are able to skip redundant repaint checks. |
| auto parent = renderer.parent(); |
| return !(is<LegacyRenderSVGContainer>(parent) && downcast<LegacyRenderSVGContainer>(*parent).didTransformToRootUpdate()); |
| } |
| |
| // Update a bounding box taking into account the validity of the other bounding box. |
| static inline void updateObjectBoundingBox(FloatRect& objectBoundingBox, bool& objectBoundingBoxValid, const RenderObject* other, FloatRect otherBoundingBox) |
| { |
| bool otherValid = is<LegacyRenderSVGContainer>(*other) ? downcast<LegacyRenderSVGContainer>(*other).isObjectBoundingBoxValid() : true; |
| if (!otherValid) |
| return; |
| |
| if (!objectBoundingBoxValid) { |
| objectBoundingBox = otherBoundingBox; |
| objectBoundingBoxValid = true; |
| return; |
| } |
| |
| objectBoundingBox.uniteEvenIfEmpty(otherBoundingBox); |
| } |
| |
| void SVGRenderSupport::computeContainerBoundingBoxes(const RenderElement& container, FloatRect& objectBoundingBox, bool& objectBoundingBoxValid, FloatRect& strokeBoundingBox, FloatRect& repaintBoundingBox) |
| { |
| objectBoundingBox = FloatRect(); |
| objectBoundingBoxValid = false; |
| strokeBoundingBox = FloatRect(); |
| |
| // When computing the strokeBoundingBox, we use the repaintRects of the container's children so that the container's stroke includes |
| // the resources applied to the children (such as clips and filters). This allows filters applied to containers to correctly bound |
| // the children, and also improves inlining of SVG content, as the stroke bound is used in that situation also. |
| for (auto& current : childrenOfType<RenderObject>(container)) { |
| if (current.isSVGHiddenContainer()) |
| continue; |
| |
| // Don't include elements in the union that do not render. |
| if (is<LegacyRenderSVGShape>(current) && downcast<LegacyRenderSVGShape>(current).isRenderingDisabled()) |
| continue; |
| |
| const AffineTransform& transform = current.localToParentTransform(); |
| if (transform.isIdentity()) { |
| updateObjectBoundingBox(objectBoundingBox, objectBoundingBoxValid, ¤t, current.objectBoundingBox()); |
| strokeBoundingBox.unite(current.repaintRectInLocalCoordinates()); |
| } else { |
| updateObjectBoundingBox(objectBoundingBox, objectBoundingBoxValid, ¤t, transform.mapRect(current.objectBoundingBox())); |
| strokeBoundingBox.unite(transform.mapRect(current.repaintRectInLocalCoordinates())); |
| } |
| } |
| |
| repaintBoundingBox = strokeBoundingBox; |
| } |
| |
| bool SVGRenderSupport::paintInfoIntersectsRepaintRect(const FloatRect& localRepaintRect, const AffineTransform& localTransform, const PaintInfo& paintInfo) |
| { |
| if (localTransform.isIdentity()) |
| return localRepaintRect.intersects(paintInfo.rect); |
| |
| return localTransform.mapRect(localRepaintRect).intersects(paintInfo.rect); |
| } |
| |
| LegacyRenderSVGRoot* SVGRenderSupport::findTreeRootObject(RenderElement& start) |
| { |
| return lineageOfType<LegacyRenderSVGRoot>(start).first(); |
| } |
| |
| const LegacyRenderSVGRoot* SVGRenderSupport::findTreeRootObject(const RenderElement& start) |
| { |
| return lineageOfType<LegacyRenderSVGRoot>(start).first(); |
| } |
| |
| static inline void invalidateResourcesOfChildren(RenderElement& renderer) |
| { |
| ASSERT(!renderer.needsLayout()); |
| if (auto* resources = SVGResourcesCache::cachedResourcesForRenderer(renderer)) |
| resources->removeClientFromCache(renderer, false); |
| |
| for (auto& child : childrenOfType<RenderElement>(renderer)) |
| invalidateResourcesOfChildren(child); |
| } |
| |
| static inline bool layoutSizeOfNearestViewportChanged(const RenderElement& renderer) |
| { |
| const RenderElement* start = &renderer; |
| while (start && !start->isSVGRootOrLegacySVGRoot() && !is<RenderSVGViewportContainer>(*start)) |
| start = start->parent(); |
| |
| ASSERT(start); |
| if (is<RenderSVGViewportContainer>(*start)) |
| return downcast<RenderSVGViewportContainer>(*start).isLayoutSizeChanged(); |
| |
| #if ENABLE(LAYER_BASED_SVG_ENGINE) |
| if (is<RenderSVGRoot>(*start)) |
| return downcast<RenderSVGRoot>(*start).isLayoutSizeChanged(); |
| #endif |
| return downcast<LegacyRenderSVGRoot>(*start).isLayoutSizeChanged(); |
| } |
| |
| bool SVGRenderSupport::transformToRootChanged(RenderElement* ancestor) |
| { |
| while (ancestor && !ancestor->isSVGRootOrLegacySVGRoot()) { |
| if (is<LegacyRenderSVGTransformableContainer>(*ancestor)) |
| return downcast<LegacyRenderSVGTransformableContainer>(*ancestor).didTransformToRootUpdate(); |
| if (is<RenderSVGViewportContainer>(*ancestor)) |
| return downcast<RenderSVGViewportContainer>(*ancestor).didTransformToRootUpdate(); |
| ancestor = ancestor->parent(); |
| } |
| |
| return false; |
| } |
| |
| void SVGRenderSupport::layoutDifferentRootIfNeeded(const RenderElement& renderer) |
| { |
| if (auto* resources = SVGResourcesCache::cachedResourcesForRenderer(renderer)) { |
| auto* svgRoot = SVGRenderSupport::findTreeRootObject(renderer); |
| ASSERT(svgRoot); |
| resources->layoutDifferentRootIfNeeded(svgRoot); |
| } |
| } |
| |
| void SVGRenderSupport::layoutChildren(RenderElement& start, bool selfNeedsLayout) |
| { |
| bool layoutSizeChanged = layoutSizeOfNearestViewportChanged(start); |
| bool transformChanged = transformToRootChanged(&start); |
| HashSet<RenderElement*> elementsThatDidNotReceiveLayout; |
| |
| for (auto& child : childrenOfType<RenderObject>(start)) { |
| bool needsLayout = selfNeedsLayout; |
| bool childEverHadLayout = child.everHadLayout(); |
| |
| if (transformChanged) { |
| // If the transform changed we need to update the text metrics (note: this also happens for layoutSizeChanged=true). |
| if (is<RenderSVGText>(child)) |
| downcast<RenderSVGText>(child).setNeedsTextMetricsUpdate(); |
| needsLayout = true; |
| } |
| |
| if (layoutSizeChanged && is<SVGElement>(*child.node())) { |
| // When selfNeedsLayout is false and the layout size changed, we have to check whether this child uses relative lengths |
| auto& element = downcast<SVGElement>(*child.node()); |
| if (element.hasRelativeLengths()) { |
| // When the layout size changed and when using relative values tell the LegacyRenderSVGShape to update its shape object |
| if (is<LegacyRenderSVGShape>(child)) |
| downcast<LegacyRenderSVGShape>(child).setNeedsShapeUpdate(); |
| else if (is<RenderSVGText>(child)) { |
| auto& svgText = downcast<RenderSVGText>(child); |
| svgText.setNeedsTextMetricsUpdate(); |
| svgText.setNeedsPositioningValuesUpdate(); |
| } |
| |
| needsLayout = true; |
| } |
| } |
| |
| if (needsLayout) |
| child.setNeedsLayout(MarkOnlyThis); |
| |
| if (child.needsLayout()) { |
| layoutDifferentRootIfNeeded(downcast<RenderElement>(child)); |
| downcast<RenderElement>(child).layout(); |
| // Renderers are responsible for repainting themselves when changing, except |
| // for the initial paint to avoid potential double-painting caused by non-sensical "old" bounds. |
| // We could handle this in the individual objects, but for now it's easier to have |
| // parent containers call repaint(). (RenderBlock::layout* has similar logic.) |
| if (!childEverHadLayout) |
| child.repaint(); |
| } else if (layoutSizeChanged && is<RenderElement>(child)) |
| elementsThatDidNotReceiveLayout.add(&downcast<RenderElement>(child)); |
| |
| ASSERT(!child.needsLayout()); |
| } |
| |
| if (!layoutSizeChanged) { |
| ASSERT(elementsThatDidNotReceiveLayout.isEmpty()); |
| return; |
| } |
| |
| // If the layout size changed, invalidate all resources of all children that didn't go through the layout() code path. |
| for (auto* element : elementsThatDidNotReceiveLayout) |
| invalidateResourcesOfChildren(*element); |
| } |
| |
| bool SVGRenderSupport::isOverflowHidden(const RenderElement& renderer) |
| { |
| // LegacyRenderSVGRoot should never query for overflow state - it should always clip itself to the initial viewport size. |
| ASSERT(!renderer.isDocumentElementRenderer()); |
| |
| return renderer.style().overflowX() == Overflow::Hidden || renderer.style().overflowX() == Overflow::Scroll; |
| } |
| |
| void SVGRenderSupport::intersectRepaintRectWithResources(const RenderElement& renderer, FloatRect& repaintRect) |
| { |
| auto* resources = SVGResourcesCache::cachedResourcesForRenderer(renderer); |
| if (!resources) |
| return; |
| |
| if (RenderSVGResourceFilter* filter = resources->filter()) |
| repaintRect = filter->resourceBoundingBox(renderer); |
| |
| if (RenderSVGResourceClipper* clipper = resources->clipper()) |
| repaintRect.intersect(clipper->resourceBoundingBox(renderer)); |
| |
| if (RenderSVGResourceMasker* masker = resources->masker()) |
| repaintRect.intersect(masker->resourceBoundingBox(renderer)); |
| } |
| |
| bool SVGRenderSupport::filtersForceContainerLayout(const RenderElement& renderer) |
| { |
| // If any of this container's children need to be laid out, and a filter is applied |
| // to the container, we need to repaint the entire container. |
| if (!renderer.normalChildNeedsLayout()) |
| return false; |
| |
| auto* resources = SVGResourcesCache::cachedResourcesForRenderer(renderer); |
| if (!resources || !resources->filter()) |
| return false; |
| |
| return true; |
| } |
| |
| inline FloatRect clipPathReferenceBox(const RenderElement& renderer, CSSBoxType boxType) |
| { |
| FloatRect referenceBox; |
| switch (boxType) { |
| case CSSBoxType::BorderBox: |
| case CSSBoxType::MarginBox: |
| case CSSBoxType::StrokeBox: |
| // FIXME: strokeBoundingBox() takes dasharray into account but shouldn't. |
| referenceBox = renderer.strokeBoundingBox(); |
| break; |
| case CSSBoxType::ViewBox: |
| if (renderer.element()) { |
| auto viewportSize = SVGLengthContext(downcast<SVGElement>(renderer.element())).viewportSize(); |
| if (viewportSize) |
| referenceBox.setSize(*viewportSize); |
| break; |
| } |
| FALLTHROUGH; |
| case CSSBoxType::ContentBox: |
| case CSSBoxType::FillBox: |
| case CSSBoxType::PaddingBox: |
| case CSSBoxType::BoxMissing: |
| referenceBox = renderer.objectBoundingBox(); |
| break; |
| } |
| return referenceBox; |
| } |
| |
| inline bool isPointInCSSClippingArea(const RenderElement& renderer, const FloatPoint& point) |
| { |
| PathOperation* clipPathOperation = renderer.style().clipPath(); |
| if (is<ShapePathOperation>(clipPathOperation)) { |
| auto& clipPath = downcast<ShapePathOperation>(*clipPathOperation); |
| FloatRect referenceBox = clipPathReferenceBox(renderer, clipPath.referenceBox()); |
| if (!referenceBox.contains(point)) |
| return false; |
| return clipPath.pathForReferenceRect(referenceBox).contains(point, clipPath.windRule()); |
| } |
| if (is<BoxPathOperation>(clipPathOperation)) { |
| auto& clipPath = downcast<BoxPathOperation>(*clipPathOperation); |
| FloatRect referenceBox = clipPathReferenceBox(renderer, clipPath.referenceBox()); |
| if (!referenceBox.contains(point)) |
| return false; |
| return clipPath.pathForReferenceRect(FloatRoundedRect {referenceBox}).contains(point); |
| } |
| |
| return true; |
| } |
| |
| void SVGRenderSupport::clipContextToCSSClippingArea(GraphicsContext& context, const RenderElement& renderer) |
| { |
| PathOperation* clipPathOperation = renderer.style().clipPath(); |
| if (is<ShapePathOperation>(clipPathOperation)) { |
| auto& clipPath = downcast<ShapePathOperation>(*clipPathOperation); |
| auto localToParentTransform = renderer.localToParentTransform(); |
| |
| auto referenceBox = clipPathReferenceBox(renderer, clipPath.referenceBox()); |
| referenceBox = localToParentTransform.mapRect(referenceBox); |
| |
| auto path = clipPath.pathForReferenceRect(referenceBox); |
| path.transform(valueOrDefault(localToParentTransform.inverse())); |
| |
| context.clipPath(path, clipPath.windRule()); |
| } |
| if (is<BoxPathOperation>(clipPathOperation)) { |
| auto& clipPath = downcast<BoxPathOperation>(*clipPathOperation); |
| FloatRect referenceBox = clipPathReferenceBox(renderer, clipPath.referenceBox()); |
| context.clipPath(clipPath.pathForReferenceRect(FloatRoundedRect {referenceBox})); |
| } |
| } |
| |
| bool SVGRenderSupport::pointInClippingArea(const RenderElement& renderer, const FloatPoint& point) |
| { |
| if (SVGHitTestCycleDetectionScope::isVisiting(renderer)) |
| return false; |
| |
| PathOperation* clipPathOperation = renderer.style().clipPath(); |
| if (is<ShapePathOperation>(clipPathOperation) || is<BoxPathOperation>(clipPathOperation)) |
| return isPointInCSSClippingArea(renderer, point); |
| |
| // We just take clippers into account to determine if a point is on the node. The Specification may |
| // change later and we also need to check maskers. |
| auto* resources = SVGResourcesCache::cachedResourcesForRenderer(renderer); |
| if (!resources) |
| return true; |
| |
| if (RenderSVGResourceClipper* clipper = resources->clipper()) |
| return clipper->hitTestClipContent(renderer.objectBoundingBox(), point); |
| |
| return true; |
| } |
| |
| void SVGRenderSupport::applyStrokeStyleToContext(GraphicsContext& context, const RenderStyle& style, const RenderElement& renderer) |
| { |
| Element* element = renderer.element(); |
| if (!is<SVGElement>(element)) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| const SVGRenderStyle& svgStyle = style.svgStyle(); |
| |
| SVGLengthContext lengthContext(downcast<SVGElement>(renderer.element())); |
| context.setStrokeThickness(lengthContext.valueForLength(style.strokeWidth())); |
| context.setLineCap(style.capStyle()); |
| context.setLineJoin(style.joinStyle()); |
| if (style.joinStyle() == LineJoin::Miter) |
| context.setMiterLimit(style.strokeMiterLimit()); |
| |
| const Vector<SVGLengthValue>& dashes = svgStyle.strokeDashArray(); |
| if (dashes.isEmpty()) |
| context.setStrokeStyle(SolidStroke); |
| else { |
| DashArray dashArray; |
| dashArray.reserveInitialCapacity(dashes.size()); |
| bool canSetLineDash = false; |
| float scaleFactor = 1; |
| |
| if (is<SVGGeometryElement>(element)) { |
| ASSERT(renderer.isSVGShapeOrLegacySVGShape()); |
| // FIXME: A value of zero is valid. Need to differentiate this case from being unspecified. |
| if (float pathLength = downcast<SVGGeometryElement>(element)->pathLength()) { |
| if (is<LegacyRenderSVGShape>(renderer)) |
| scaleFactor = downcast<LegacyRenderSVGShape>(renderer).getTotalLength() / pathLength; |
| #if ENABLE(LAYER_BASED_SVG_ENGINE) |
| else if (is<RenderSVGShape>(renderer)) |
| scaleFactor = downcast<RenderSVGShape>(renderer).getTotalLength() / pathLength; |
| #endif |
| } |
| } |
| |
| for (auto& dash : dashes) { |
| dashArray.uncheckedAppend(dash.value(lengthContext) * scaleFactor); |
| if (dashArray.last() > 0) |
| canSetLineDash = true; |
| } |
| |
| if (canSetLineDash) |
| context.setLineDash(dashArray, lengthContext.valueForLength(svgStyle.strokeDashOffset()) * scaleFactor); |
| else |
| context.setStrokeStyle(SolidStroke); |
| } |
| } |
| |
| void SVGRenderSupport::styleChanged(RenderElement& renderer, const RenderStyle* oldStyle) |
| { |
| #if ENABLE(CSS_COMPOSITING) |
| if (renderer.element() && renderer.element()->isSVGElement() && (!oldStyle || renderer.style().hasBlendMode() != oldStyle->hasBlendMode())) |
| SVGRenderSupport::updateMaskedAncestorShouldIsolateBlending(renderer); |
| #else |
| UNUSED_PARAM(renderer); |
| UNUSED_PARAM(oldStyle); |
| #endif |
| } |
| |
| #if ENABLE(CSS_COMPOSITING) |
| |
| bool SVGRenderSupport::isolatesBlending(const RenderStyle& style) |
| { |
| return style.hasPositionedMask() || style.hasFilter() || style.hasBlendMode() || style.opacity() < 1.0f; |
| } |
| |
| void SVGRenderSupport::updateMaskedAncestorShouldIsolateBlending(const RenderElement& renderer) |
| { |
| ASSERT(renderer.element()); |
| ASSERT(renderer.element()->isSVGElement()); |
| |
| for (auto& ancestor : ancestorsOfType<SVGGraphicsElement>(*renderer.element())) { |
| auto* style = ancestor.computedStyle(); |
| if (!style || !isolatesBlending(*style)) |
| continue; |
| if (style->hasPositionedMask()) |
| ancestor.setShouldIsolateBlending(renderer.style().hasBlendMode()); |
| return; |
| } |
| } |
| |
| #endif |
| |
| SVGHitTestCycleDetectionScope::SVGHitTestCycleDetectionScope(const RenderElement& element) |
| { |
| m_element = element; |
| auto result = visitedElements().add(*m_element); |
| ASSERT_UNUSED(result, result.isNewEntry); |
| } |
| |
| SVGHitTestCycleDetectionScope::~SVGHitTestCycleDetectionScope() |
| { |
| bool result = visitedElements().remove(*m_element); |
| ASSERT_UNUSED(result, result); |
| } |
| |
| WeakHashSet<RenderElement>& SVGHitTestCycleDetectionScope::visitedElements() |
| { |
| static NeverDestroyed<WeakHashSet<RenderElement>> s_visitedElements; |
| return s_visitedElements; |
| } |
| |
| bool SVGHitTestCycleDetectionScope::isEmpty() |
| { |
| return visitedElements().computesEmpty(); |
| } |
| |
| bool SVGHitTestCycleDetectionScope::isVisiting(const RenderElement& element) |
| { |
| return visitedElements().contains(element); |
| } |
| |
| } |