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