blob: 436278290d0631e27b439029ca13bb91bb690728 [file] [log] [blame]
/*
* Copyright (C) 2022-2025 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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 "InteractionRegion.h"
#include "AccessibilityObject.h"
#include "BorderShape.h"
#include "Document.h"
#include "ElementAncestorIteratorInlines.h"
#include "ElementInlines.h"
#include "ElementRuleCollector.h"
#include "FloatSizeHash.h"
#include "FrameSnapshotting.h"
#include "GeometryUtilities.h"
#include "HTMLAnchorElement.h"
#include "HTMLAttachmentElement.h"
#include "HTMLButtonElement.h"
#include "HTMLFieldSetElement.h"
#include "HTMLFormControlElement.h"
#include "HTMLInputElement.h"
#include "HTMLLabelElement.h"
#include "HitTestResult.h"
#include "LayoutShape.h"
#include "LegacyRenderSVGShape.h"
#include "LegacyRenderSVGShapeInlines.h"
#include "LocalFrame.h"
#include "LocalFrameView.h"
#include "NativeImage.h"
#include "NodeList.h"
#include "Page.h"
#include "PathOperation.h"
#include "PlatformMouseEvent.h"
#include "PseudoClassChangeInvalidation.h"
#include "RenderAncestorIterator.h"
#include "RenderBoxInlines.h"
#include "RenderImage.h"
#include "RenderLayer.h"
#include "RenderLayerBacking.h"
#include "RenderVideo.h"
#include "SVGSVGElement.h"
#include "SimpleRange.h"
#include "SliderThumbElement.h"
#include "StyleResolver.h"
#include "TextIterator.h"
#include <wtf/NeverDestroyed.h>
#include <wtf/text/MakeString.h>
#if ENABLE(FORM_CONTROL_REFRESH)
#include "PathCG.h"
#include "RenderThemeCocoa.h"
#endif
namespace WebCore {
InteractionRegion::~InteractionRegion() = default;
class InteractionRegionPathCache {
public:
static InteractionRegionPathCache& singleton();
std::optional<Path> get(const Image&, const FloatSize&);
void add(const Image&, const FloatSize&, Path);
void clear();
private:
friend class NeverDestroyed<InteractionRegionPathCache>;
InteractionRegionPathCache() = default;
WeakHashMap<const Image, HashMap<FloatSize, Path>> m_imageCache;
};
InteractionRegionPathCache& InteractionRegionPathCache::singleton()
{
static NeverDestroyed<InteractionRegionPathCache> cache;
return cache;
}
std::optional<Path> InteractionRegionPathCache::get(const Image& image, const FloatSize& size)
{
if (auto cacheBySize = m_imageCache.getOptional(image))
return cacheBySize->getOptional(size);
return std::nullopt;
}
void InteractionRegionPathCache::add(const Image& image, const FloatSize& size, Path path)
{
m_imageCache.ensure(image, [] {
return HashMap<FloatSize, Path>();
}).iterator->value.add(size, path);
}
void InteractionRegionPathCache::clear()
{
m_imageCache.clear();
}
void InteractionRegion::clearCache()
{
InteractionRegionPathCache::singleton().clear();
}
static bool hasInteractiveCursorType(Element& element)
{
auto* renderer = element.renderer();
auto* style = renderer ? &renderer->style() : nullptr;
auto cursorType = style ? style->cursorType() : CursorType::Auto;
if (cursorType == CursorType::Auto && element.enclosingLinkEventParentOrSelf())
cursorType = CursorType::Pointer;
return cursorType == CursorType::Move
|| cursorType == CursorType::Pointer
|| cursorType == CursorType::Text
|| cursorType == CursorType::VerticalText;
}
static bool shouldAllowElement(const Element& element)
{
if (is<HTMLFieldSetElement>(element))
return false;
if (auto* input = dynamicDowncast<HTMLInputElement>(element)) {
if (input->isDisabledFormControl())
return false;
// Do not allow regions for the <input type='range'>, because we make one for the thumb.
if (input->isRangeControl())
return false;
// Do not allow regions for the <input type='file'>, because we make one for the button.
if (input->isFileUpload())
return false;
}
return true;
}
static bool shouldAllowAccessibilityRoleAsPointerCursorReplacement(const Element& element)
{
switch (AccessibilityObject::ariaRoleToWebCoreRole(element.attributeWithoutSynchronization(HTMLNames::roleAttr))) {
case AccessibilityRole::Button:
case AccessibilityRole::Checkbox:
case AccessibilityRole::Link:
case AccessibilityRole::ListBoxOption:
case AccessibilityRole::MenuItem:
case AccessibilityRole::MenuItemCheckbox:
case AccessibilityRole::MenuItemRadio:
case AccessibilityRole::PopUpButton:
case AccessibilityRole::RadioButton:
case AccessibilityRole::Switch:
case AccessibilityRole::TextField:
case AccessibilityRole::ToggleButton:
return true;
default:
return false;
}
}
static bool elementMatchesHoverRules(Element& element)
{
bool foundHoverRules = false;
bool initialValue = element.isUserActionElement() && element.document().userActionElements().isHovered(element);
for (auto key : Style::makePseudoClassInvalidationKeys(CSSSelector::PseudoClass::Hover, element)) {
auto& ruleSets = element.styleResolver().ruleSets();
auto* invalidationRuleSets = ruleSets.pseudoClassInvalidationRuleSets(key);
if (!invalidationRuleSets)
continue;
for (auto& invalidationRuleSet : *invalidationRuleSets) {
element.document().userActionElements().setHovered(element, invalidationRuleSet.isNegation == Style::IsNegation::No);
Style::ElementRuleCollector ruleCollector(element, *invalidationRuleSet.ruleSet, nullptr);
ruleCollector.setMode(SelectorChecker::Mode::CollectingRulesIgnoringVirtualPseudoElements);
if (ruleCollector.matchesAnyAuthorRules()) {
foundHoverRules = true;
break;
}
}
if (foundHoverRules)
break;
}
element.document().userActionElements().setHovered(element, initialValue);
return foundHoverRules;
}
static bool shouldAllowNonInteractiveCursorForElement(const Element& element)
{
#if ENABLE(ATTACHMENT_ELEMENT)
if (is<HTMLAttachmentElement>(element))
return true;
#endif
if (RefPtr textElement = dynamicDowncast<HTMLTextFormControlElement>(element))
return !textElement->focused() || !textElement->lastChangeWasUserEdit() || textElement->value()->isEmpty();
if (is<HTMLFormControlElement>(element))
return true;
if (is<SliderThumbElement>(element))
return true;
if (is<HTMLAnchorElement>(element))
return true;
if (shouldAllowAccessibilityRoleAsPointerCursorReplacement(element))
return true;
return false;
}
static bool shouldGetOcclusion(const RenderElement& renderer)
{
if (auto* renderLayerModelObject = dynamicDowncast<RenderBox>(renderer)) {
if (renderLayerModelObject->hasLayer() && renderLayerModelObject->layer()->isComposited())
return false;
}
if (renderer.style().specifiedZIndex() > 0)
return true;
if (renderer.isFixedPositioned())
return true;
return false;
}
static bool hasTransparentContainerStyle(const RenderStyle& style)
{
return !style.hasBackground()
&& !style.hasOutline()
&& !style.hasBoxShadow()
&& !style.hasClipPath()
&& !style.hasExplicitlySetBorderRadius()
// No visible borders or borders that do not create a complete box.
&& (!style.hasVisibleBorder()
|| !(style.borderTopWidth() && style.borderRightWidth() && style.borderBottomWidth() && style.borderLeftWidth()));
}
static bool canTweakShapeForStyle(const RenderStyle& style)
{
if (!hasTransparentContainerStyle(style))
return false;
switch (style.usedAppearance()) {
case StyleAppearance::TextField:
case StyleAppearance::TextArea:
return false;
default:
return true;
}
}
static bool colorIsChallengingToHighlight(const Color& color)
{
constexpr double luminanceThreshold = 0.01;
return color.isValid()
&& ((color.luminance() < luminanceThreshold || std::abs(color.luminance() - 1) < luminanceThreshold));
}
static bool styleIsChallengingToHighlight(const RenderStyle& style)
{
auto fillPaintType = style.fill().type;
if (fillPaintType == Style::SVGPaintType::None) {
auto strokePaintType = style.stroke().type;
if (strokePaintType != Style::SVGPaintType::RGBColor && strokePaintType != Style::SVGPaintType::CurrentColor)
return false;
return colorIsChallengingToHighlight(style.colorResolvingCurrentColor(style.stroke().color));
}
if (fillPaintType != Style::SVGPaintType::RGBColor && fillPaintType != Style::SVGPaintType::CurrentColor)
return false;
return colorIsChallengingToHighlight(style.colorResolvingCurrentColor(style.fill().color));
}
static bool isGuardContainer(const Element& element)
{
bool isButton = is<HTMLButtonElement>(element);
bool isLink = element.isLink();
if (!isButton && !isLink)
return false;
if (!element.firstElementChild()
|| element.firstElementChild() != element.lastElementChild())
return false;
if (!element.renderer())
return false;
auto& renderer = *element.renderer();
return hasTransparentContainerStyle(renderer.style());
}
static FloatSize boundingSize(const RenderObject& renderer, const std::optional<AffineTransform>& transform)
{
Vector<LayoutRect> rects;
renderer.boundingRects(rects, LayoutPoint());
if (!rects.size())
return FloatSize();
FloatSize size = unionRect(rects).size();
if (transform)
size.scale(transform->xScale(), transform->yScale());
return size;
}
static bool cachedImageIsPhoto(const CachedImage& cachedImage)
{
if (cachedImage.errorOccurred())
return false;
auto* image = cachedImage.image();
if (!image || !image->isBitmapImage())
return false;
if (image->nativeImage() && image->nativeImage()->hasAlpha())
return false;
return true;
}
static RefPtr<Image> findIconImage(const RenderObject& renderer)
{
if (const auto& renderImage = dynamicDowncast<RenderImage>(renderer)) {
if (!renderImage->cachedImage() || renderImage->cachedImage()->errorOccurred())
return nullptr;
auto* image = renderImage->cachedImage()->imageForRenderer(renderImage);
if (!image)
return nullptr;
if (image->isSVGImageForContainer()
|| (image->isBitmapImage() && image->nativeImage() && image->nativeImage()->hasAlpha()))
return image;
}
return nullptr;
}
static std::optional<std::pair<Ref<SVGSVGElement>, Ref<SVGGraphicsElement>>> findSVGClipElements(const RenderObject& renderer)
{
if (const auto& renderShape = dynamicDowncast<LegacyRenderSVGShape>(renderer)) {
Ref shapeElement = renderShape->graphicsElement();
if (auto* owner = shapeElement->ownerSVGElement()) {
Ref svgSVGElement = *owner;
return std::make_pair(svgSVGElement, shapeElement);
}
}
return std::nullopt;
}
#if ENABLE(INTERACTION_REGION_TEXT_CONTENT)
static String interactionRegionTextContentForNode(Node& node)
{
if (auto nodeRange = makeRangeSelectingNode(node))
return plainText(*nodeRange);
return { };
}
#endif
std::optional<InteractionRegion> interactionRegionForRenderedRegion(RenderObject& regionRenderer, const FloatRect& bounds, const FloatSize& clipOffset, const std::optional<AffineTransform>& transform)
{
if (bounds.isEmpty())
return std::nullopt;
if (!regionRenderer.node())
return std::nullopt;
auto originalElement = dynamicDowncast<Element>(regionRenderer.node());
if (originalElement && originalElement->isPseudoElement())
return std::nullopt;
auto matchedElement = originalElement;
if (!matchedElement)
matchedElement = regionRenderer.node()->parentElement();
if (!matchedElement)
return std::nullopt;
bool isLabelable = [&] {
auto* htmlElement = dynamicDowncast<HTMLElement>(matchedElement);
return htmlElement && htmlElement->isLabelable();
}();
for (Node* node = matchedElement; node; node = node->parentInComposedTree()) {
auto* element = dynamicDowncast<Element>(node);
if (!element)
continue;
bool matchedButton = is<HTMLButtonElement>(*element);
bool matchedLabel = isLabelable && is<HTMLLabelElement>(*element);
bool matchedLink = element->isLink();
if (matchedButton || matchedLabel || matchedLink) {
matchedElement = element;
break;
}
}
if (!shouldAllowElement(*matchedElement))
return std::nullopt;
if (!matchedElement->renderer())
return std::nullopt;
auto& renderer = *matchedElement->renderer();
if (renderer.usedPointerEvents() == PointerEvents::None)
return std::nullopt;
bool isOriginalMatch = matchedElement == originalElement;
// FIXME: Consider also allowing elements that only receive touch events.
bool hasListener = renderer.style().eventListenerRegionTypes().contains(EventListenerRegionType::MouseClick);
bool hasPointer = hasInteractiveCursorType(*matchedElement) || shouldAllowNonInteractiveCursorForElement(*matchedElement);
RefPtr localMainFrame = dynamicDowncast<LocalFrame>(regionRenderer.document().frame()->mainFrame());
if (!localMainFrame) {
ASSERT_NOT_REACHED();
return std::nullopt;
}
RefPtr pageView = localMainFrame->view();
if (!pageView) {
ASSERT_NOT_REACHED();
return std::nullopt;
}
auto viewportSize = FloatSize(pageView->baseLayoutViewportSize());
auto viewportArea = viewportSize.area();
bool isTooBigForInteraction = bounds.area() > viewportArea / 3;
bool isTooBigForOcclusion = bounds.area() > viewportArea * 3;
auto nodeIdentifier = matchedElement->nodeIdentifier();
if (!hasPointer) {
if (auto* labelElement = dynamicDowncast<HTMLLabelElement>(matchedElement)) {
// Could be a `<label for="...">` or a label with a descendant.
// In cases where both elements get a region we want to group them by the same `nodeIdentifier`.
auto associatedElement = labelElement->control();
if (associatedElement && !associatedElement->isDisabledFormControl()) {
hasPointer = true;
nodeIdentifier = associatedElement->nodeIdentifier();
}
}
}
bool detectedHoverRules = false;
if (!hasPointer) {
// The hover check can be expensive (it may end up doing selector matching), so we only run it on some elements.
bool hasVisibleBoxDecorations = renderer.hasVisibleBoxDecorations();
bool nonScrollable = [&] {
auto* box = dynamicDowncast<RenderBox>(renderer);
return !box || (!box->hasScrollableOverflowX() && !box->hasScrollableOverflowY());
}();
if (hasVisibleBoxDecorations && nonScrollable)
detectedHoverRules = elementMatchesHoverRules(*matchedElement);
}
if (!hasListener || !(hasPointer || detectedHoverRules) || isTooBigForInteraction) {
if (isOriginalMatch && shouldGetOcclusion(renderer) && !isTooBigForOcclusion) {
return { {
InteractionRegion::Type::Occlusion,
nodeIdentifier,
bounds
} };
}
return std::nullopt;
}
bool isInlineNonBlock = renderer.isInline() && !renderer.isBlockLevelReplacedOrAtomicInline();
bool isPhoto = false;
float minimumContentHintArea = 200 * 200;
bool needsContentHint = bounds.area() > minimumContentHintArea;
if (needsContentHint) {
if (auto* renderImage = dynamicDowncast<RenderImage>(regionRenderer)) {
isPhoto = [&]() -> bool {
#if ENABLE(VIDEO)
if (is<RenderVideo>(renderImage))
return true;
#endif
if (!renderImage->cachedImage())
return false;
return cachedImageIsPhoto(*renderImage->cachedImage());
}();
} else if (regionRenderer.style().hasBackgroundImage()) {
isPhoto = [&]() -> bool {
auto* backgroundImage = regionRenderer.style().backgroundLayers().image();
if (!backgroundImage || !backgroundImage->cachedImage())
return false;
return cachedImageIsPhoto(*backgroundImage->cachedImage());
}();
}
}
bool matchedElementIsGuardContainer = isGuardContainer(*matchedElement);
if (isOriginalMatch && matchedElementIsGuardContainer) {
return { {
InteractionRegion::Type::Guard,
nodeIdentifier,
bounds
} };
}
// The parent will get its own InteractionRegion.
if (!isOriginalMatch && !matchedElementIsGuardContainer && !isPhoto && !isInlineNonBlock && !renderer.style().isDisplayTableOrTablePart())
return std::nullopt;
// FIXME: Consider allowing rotation / skew - rdar://127499446.
bool hasRotationOrShear = false;
if (transform)
hasRotationOrShear = transform->isRotateOrShear();
RefPtr<Image> iconImage;
std::optional<std::pair<Ref<SVGSVGElement>, Ref<SVGGraphicsElement>>> svgClipElements;
if (!hasRotationOrShear && !needsContentHint)
iconImage = findIconImage(regionRenderer);
if (!hasRotationOrShear && !iconImage)
svgClipElements = findSVGClipElements(regionRenderer);
auto rect = bounds;
float cornerRadius = 0;
OptionSet<InteractionRegion::CornerMask> maskedCorners { };
std::optional<Path> clipPath = std::nullopt;
auto& style = regionRenderer.style();
CheckedPtr<const RenderBox> regionRendererBox;
if (auto basicShapePath = style.clipPath().tryBasicShape(); !hasRotationOrShear && originalElement && basicShapePath) {
auto size = boundingSize(regionRenderer, transform);
auto path = Style::tryPath(*basicShapePath, TransformOperationData(FloatRect(FloatPoint(), size)));
if (path && !clipOffset.isZero())
path->translate(clipOffset);
clipPath = path;
} else if (iconImage && originalElement) {
auto size = boundingSize(regionRenderer, transform);
auto generateAndCachePath = [&] {
LayoutRect imageRect(FloatPoint(), size);
Ref shape = LayoutShape::createRasterShape(iconImage.get(), 0, imageRect, imageRect, WritingMode(), 0);
LayoutShape::DisplayPaths paths;
shape->buildDisplayPaths(paths);
auto path = paths.shape;
InteractionRegionPathCache::singleton().add(*iconImage.get(), size, path);
return path;
};
auto cachedPath = InteractionRegionPathCache::singleton().get(*iconImage.get(), size);
auto path = cachedPath ? *cachedPath : generateAndCachePath();
if (!clipOffset.isZero())
path.translate(clipOffset);
clipPath = path;
} else if (svgClipElements) {
auto& [svgSVGElement, shapeElement] = *svgClipElements;
auto path = shapeElement->toClipPath();
FloatSize size = svgSVGElement->currentViewportSizeExcludingZoom();
auto viewBoxTransform = svgSVGElement->viewBoxToViewTransform(size.width(), size.height());
auto shapeBoundingBox = shapeElement->getBBox(SVGLocatable::DisallowStyleUpdate);
path.transform(viewBoxTransform);
shapeBoundingBox = viewBoxTransform.mapRect(shapeBoundingBox);
constexpr float smallShapeDimension = 30;
bool shouldFallbackToContainerRegion = shapeBoundingBox.size().minDimension() < smallShapeDimension
&& styleIsChallengingToHighlight(style)
&& matchedElementIsGuardContainer;
// Bail out, we'll convert the guard container to Interaction.
if (shouldFallbackToContainerRegion)
return std::nullopt;
path.translate(FloatSize(-shapeBoundingBox.x(), -shapeBoundingBox.y()));
if (!clipOffset.isZero())
path.translate(clipOffset);
clipPath = path;
} else if ((regionRendererBox = dynamicDowncast<RenderBox>(regionRenderer))) {
auto borderShape = BorderShape::shapeForBorderRect(regionRendererBox->style(), regionRendererBox->borderBoxRect());
auto borderRadii = borderShape.radii();
auto minRadius = borderRadii.minimumRadius();
auto maxRadius = borderRadii.maximumRadius();
bool needsClipPath = false;
auto checkIfClipPathNeeded = [&](auto radius) {
if (radius != minRadius && radius != maxRadius)
needsClipPath = true;
};
checkIfClipPathNeeded(borderRadii.topLeft().minDimension());
checkIfClipPathNeeded(borderRadii.topRight().minDimension());
checkIfClipPathNeeded(borderRadii.bottomLeft().minDimension());
checkIfClipPathNeeded(borderRadii.bottomRight().minDimension());
if (minRadius == maxRadius)
cornerRadius = minRadius;
else if (!minRadius && !needsClipPath) {
cornerRadius = maxRadius;
if (borderRadii.topLeft().minDimension() == maxRadius)
maskedCorners.add(InteractionRegion::CornerMask::MinXMinYCorner);
if (borderRadii.topRight().minDimension() == maxRadius)
maskedCorners.add(InteractionRegion::CornerMask::MaxXMinYCorner);
if (borderRadii.bottomLeft().minDimension() == maxRadius)
maskedCorners.add(InteractionRegion::CornerMask::MinXMaxYCorner);
if (borderRadii.bottomRight().minDimension() == maxRadius)
maskedCorners.add(InteractionRegion::CornerMask::MaxXMaxYCorner);
} else
clipPath = borderShape.pathForOuterShape(regionRendererBox->document().deviceScaleFactor());
}
bool canTweakShape = !isPhoto
&& !clipPath
&& canTweakShapeForStyle(style);
auto adjustForTheme = false;
auto useContinuousCorners = false;
#if ENABLE(FORM_CONTROL_REFRESH)
// Certain form controls with native appearance need to use a modified interaction region in order to
// be shaped correctly. Check for them and use the correpsonding adjusted corner style, size, and radius,
// if available.
// FIXME: <rdar://154930959> The region for native textareas still needs to be adjusted.
if (!regionRendererBox)
regionRendererBox = dynamicDowncast<RenderBox>(regionRenderer);
adjustForTheme = regionRendererBox
&& regionRendererBox->settings().formControlRefreshEnabled()
&& !style.hasTransformRelatedProperty();
if (adjustForTheme) {
// We only need the bounding path for the region if a clip path exists so that we can compute
// the intersecting path and use it instead.
const auto needsPath = clipPath ? ShouldComputePath::Yes : ShouldComputePath::No;
if (auto themeShape = RenderThemeCocoa::shapeForInteractionRegion(*regionRendererBox, regionRendererBox->borderBoxRect(), needsPath)) {
if (themeShape->cornerType == CornerType::Continuous)
useContinuousCorners = true;
cornerRadius = themeShape->cornerRadius;
maskedCorners = { };
if (auto themePath = themeShape->path) {
// A clip path has already been set. Find the intersecting path between the existing
// clip path and the bounding path for the adjusted interaction region for the control.
Path adjustedPath = *themePath;
if (!clipOffset.isZero())
adjustedPath.translate(clipOffset);
RetainPtr intersectingPath = adoptCF(CGPathCreateCopyByIntersectingPath(adjustedPath.platformPath(), clipPath->platformPath(), false));
clipPath = { PathCG::create(adoptCF(CGPathCreateMutableCopy(intersectingPath.get()))) };
// No need for continuous corners if we're already going to clip.
useContinuousCorners = false;
}
// Expand the interaction region by the width of the CSS border, if necessary.
const auto rectOffset = RenderThemeCocoa::inflateRectForInteractionRegion(regionRenderer, rect);
if (clipPath && !rectOffset.isZero())
clipPath->translate(rectOffset);
} else
adjustForTheme = false;
}
#endif
if (canTweakShape && !adjustForTheme) {
// We can safely tweak the bounds and radius without causing visual mismatch.
cornerRadius = std::max<float>(cornerRadius, regionRenderer.document().settings().interactionRegionMinimumCornerRadius());
if (isInlineNonBlock)
rect.inflate(regionRenderer.document().settings().interactionRegionInlinePadding());
}
return { {
InteractionRegion::Type::Interaction,
nodeIdentifier,
rect,
cornerRadius,
maskedCorners,
isPhoto ? InteractionRegion::ContentHint::Photo : InteractionRegion::ContentHint::Default,
clipPath,
useContinuousCorners,
#if ENABLE(INTERACTION_REGION_TEXT_CONTENT)
interactionRegionTextContentForNode(*regionRenderer.node())
#endif
} };
}
TextStream& operator<<(TextStream& ts, const InteractionRegion& interactionRegion)
{
auto regionName = interactionRegion.type == InteractionRegion::Type::Interaction
? "interaction"_s
: (interactionRegion.type == InteractionRegion::Type::Occlusion ? "occlusion"_s : "guard"_s);
ts.dumpProperty(regionName, interactionRegion.rectInLayerCoordinates);
if (interactionRegion.contentHint != InteractionRegion::ContentHint::Default)
ts.dumpProperty("content hint"_s, "photo"_s);
auto radius = interactionRegion.cornerRadius;
if (radius > 0) {
if (interactionRegion.maskedCorners.isEmpty()) {
ts.dumpProperty("cornerRadius"_s, radius);
auto useContinuousCorners = interactionRegion.useContinuousCorners;
if (useContinuousCorners)
ts.dumpProperty("useContinuousCorners"_s, true);
} else {
auto mask = interactionRegion.maskedCorners;
ts.dumpProperty("cornerRadius"_s, makeString(
mask.contains(InteractionRegion::CornerMask::MinXMinYCorner) ? radius : 0, ' ',
mask.contains(InteractionRegion::CornerMask::MaxXMinYCorner) ? radius : 0, ' ',
mask.contains(InteractionRegion::CornerMask::MaxXMaxYCorner) ? radius : 0, ' ',
mask.contains(InteractionRegion::CornerMask::MinXMaxYCorner) ? radius : 0
));
}
}
if (interactionRegion.clipPath)
ts.dumpProperty("clipPath"_s, interactionRegion.clipPath.value());
#if ENABLE(INTERACTION_REGION_TEXT_CONTENT)
if (!interactionRegion.text.isEmpty())
ts.dumpProperty("text"_s, interactionRegion.text);
#endif
return ts;
}
}