blob: 7ed051a7021b187a15abd85481fc168ae57236cf [file] [log] [blame]
/*
* Copyright (C) 2024-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 "ElementTargetingController.h"
#include "AccessibilityObject.h"
#include "Attr.h"
#include "BitmapImage.h"
#include "Chrome.h"
#include "ChromeClient.h"
#include "ContainerNodeInlines.h"
#include "DOMTokenList.h"
#include "Document.h"
#include "DocumentLoader.h"
#include "ElementAncestorIteratorInlines.h"
#include "ElementChildIteratorInlines.h"
#include "ElementInlines.h"
#include "ElementRareData.h"
#include "ElementTargetingTypes.h"
#include "FloatPoint.h"
#include "FloatRect.h"
#include "FrameSnapshotting.h"
#include "HTMLAnchorElement.h"
#include "HTMLBodyElement.h"
#include "HTMLFrameOwnerElement.h"
#include "HTMLImageElement.h"
#include "HTMLMediaElement.h"
#include "HTMLNames.h"
#include "HitTestRequest.h"
#include "HitTestResult.h"
#include "LocalFrame.h"
#include "LocalFrameView.h"
#include "NamedNodeMap.h"
#include "NodeInlines.h"
#include "NodeList.h"
#include "NodeRenderStyle.h"
#include "Page.h"
#include "PseudoElement.h"
#include "Region.h"
#include "RenderBoxInlines.h"
#include "RenderDescendantIterator.h"
#include "RenderView.h"
#include "ShadowRoot.h"
#include "SimpleRange.h"
#include "StyleImage.h"
#include "StyleURL.h"
#include "TextExtraction.h"
#include "TextIterator.h"
#include "TypedElementDescendantIteratorInlines.h"
#include "VisibilityAdjustment.h"
#include <ranges>
#include <wtf/HashMap.h>
#include <wtf/Scope.h>
#include <wtf/TZoneMallocInlines.h>
#include <wtf/text/MakeString.h>
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(ElementTargetingController);
static constexpr auto maximumNumberOfClasses = 5;
static constexpr auto marginForTrackingAdjustmentRects = 5;
static constexpr auto minimumDistanceToConsiderEdgesEquidistant = 2;
static constexpr auto minimumWidthForNearbyTarget = 2;
static constexpr auto minimumHeightForNearbyTarget = 2;
static constexpr auto minimumLengthForSearchableText = 25;
static constexpr auto maximumLengthForSearchableText = 100;
static constexpr auto selectorBasedVisibilityAdjustmentThrottlingTimeLimit = 10_s;
static constexpr auto selectorBasedVisibilityAdjustmentInterval = 1_s;
static constexpr auto maximumNumberOfAdditionalAdjustments = 20;
static constexpr auto adjustmentClientRectCleanUpDelay = 15_s;
static constexpr auto minimumAreaRatioForElementToCoverViewport = 0.95;
static constexpr auto minimumAreaForInterpolation = 200000;
static constexpr auto maximumAreaForInterpolation = 800000;
static float linearlyInterpolatedViewportRatio(float viewportArea, float minimumValue, float maximumValue)
{
auto areaRatio = (viewportArea - minimumAreaForInterpolation) / (maximumAreaForInterpolation - minimumAreaForInterpolation);
return clampTo(maximumValue - areaRatio * (maximumValue - minimumValue), minimumValue, maximumValue);
}
static float maximumAreaRatioForAbsolutelyPositionedContent(float viewportArea)
{
return linearlyInterpolatedViewportRatio(viewportArea, 0.75, 1);
}
static float maximumAreaRatioForInFlowContent(float viewportArea)
{
return linearlyInterpolatedViewportRatio(viewportArea, 0.5, 1);
}
static float maximumAreaRatioForNearbyTargets(float viewportArea)
{
return linearlyInterpolatedViewportRatio(viewportArea, 0.25, 0.5);
}
static float minimumAreaRatioForInFlowContent(float viewportArea)
{
return linearlyInterpolatedViewportRatio(viewportArea, 0.005, 0.01);
}
static float maximumAreaRatioForTrackingAdjustmentAreas(float viewportArea)
{
return linearlyInterpolatedViewportRatio(viewportArea, 0.25, 0.3);
}
class ClearVisibilityAdjustmentForScope {
WTF_MAKE_NONCOPYABLE(ClearVisibilityAdjustmentForScope);
WTF_MAKE_TZONE_ALLOCATED(ClearVisibilityAdjustmentForScope);
public:
ClearVisibilityAdjustmentForScope(Element& element)
: m_element(element)
, m_adjustmentToRestore(element.visibilityAdjustment())
{
if (m_adjustmentToRestore.isEmpty())
return;
element.setVisibilityAdjustment({ });
element.invalidateStyleAndRenderersForSubtree();
}
ClearVisibilityAdjustmentForScope(ClearVisibilityAdjustmentForScope&& other)
: m_element(WTFMove(other.m_element))
, m_adjustmentToRestore(std::exchange(other.m_adjustmentToRestore, { }))
{
}
~ClearVisibilityAdjustmentForScope()
{
if (m_adjustmentToRestore.isEmpty())
return;
m_element->setVisibilityAdjustment(m_adjustmentToRestore);
m_element->invalidateStyleAndRenderersForSubtree();
}
private:
Ref<Element> m_element;
OptionSet<VisibilityAdjustment> m_adjustmentToRestore;
};
WTF_MAKE_TZONE_ALLOCATED_IMPL(ClearVisibilityAdjustmentForScope);
using ElementSelectorCache = HashMap<Ref<Element>, std::optional<String>>;
ElementTargetingController::ElementTargetingController(Page& page)
: m_page { page }
, m_recentAdjustmentClientRectsCleanUpTimer { *this, &ElementTargetingController::cleanUpAdjustmentClientRects, adjustmentClientRectCleanUpDelay }
, m_selectorBasedVisibilityAdjustmentTimer { *this, &ElementTargetingController::selectorBasedVisibilityAdjustmentTimerFired }
{
}
static inline bool elementAndAncestorsAreOnlyRenderedChildren(const Element& element)
{
CheckedPtr renderer = element.renderer();
if (!renderer)
return false;
for (auto& ancestor : ancestorsOfType<RenderElement>(*renderer)) {
if (ancestor.style().usedVisibility() == Visibility::Hidden)
continue;
unsigned numberOfVisibleChildren = 0;
for (auto& child : childrenOfType<RenderObject>(ancestor)) {
if (CheckedPtr renderElement = dynamicDowncast<RenderElement>(child); renderElement && renderElement->style().usedVisibility() == Visibility::Hidden)
continue;
if (++numberOfVisibleChildren >= 2)
return false;
}
}
return true;
}
static inline bool querySelectorMatchesOneElement(const Element& element, const String& selector)
{
Ref container = [&]() -> ContainerNode& {
if (RefPtr shadowRoot = element.containingShadowRoot())
return *shadowRoot;
return element.document();
}();
auto result = container->querySelectorAll(selector);
if (result.hasException())
return false;
return result.returnValue()->length() == 1 && result.returnValue()->item(0) == &element;
}
struct ChildElementPosition {
size_t index { notFound };
bool firstOfType { false };
bool lastOfType { false };
};
static inline ChildElementPosition findChild(const Element& element, const Element& parent)
{
auto elementTagName = element.tagName();
RefPtr<const Element> firstOfType;
RefPtr<const Element> lastOfType;
size_t index = notFound;
size_t currentChildIndex = 0;
for (auto& child : childrenOfType<Element>(parent)) {
if (&child == &element)
index = currentChildIndex;
if (child.tagName() == elementTagName) {
if (!firstOfType)
firstOfType = child;
lastOfType = child;
}
currentChildIndex++;
}
return { index, &element == firstOfType, &element == lastOfType };
}
static inline String computeIDSelector(const Element& element)
{
if (element.hasID()) {
auto elementID = element.getIdAttribute();
if (auto* matches = element.treeScope().getAllElementsById(elementID); matches && matches->size() == 1)
return makeString('#', elementID);
}
return emptyString();
}
static inline String computeTagAndAttributeSelector(const Element& element, const String& suffix = emptyString())
{
if (!element.hasAttributes())
return emptyString();
static NeverDestroyed<MemoryCompactLookupOnlyRobinHoodHashSet<QualifiedName>> attributesToExclude { std::initializer_list<QualifiedName> {
HTMLNames::classAttr,
HTMLNames::idAttr,
HTMLNames::styleAttr,
HTMLNames::widthAttr,
HTMLNames::heightAttr,
HTMLNames::forAttr,
HTMLNames::aria_labeledbyAttr,
HTMLNames::aria_labelledbyAttr,
HTMLNames::aria_describedbyAttr
} };
static constexpr auto maximumNameLength = 16;
static constexpr auto maximumValueLength = 150;
static constexpr auto maximumValueLengthForExactMatch = 60;
Vector<std::pair<String, String>> attributesToCheck;
auto& attributes = element.attributesMap();
attributesToCheck.reserveInitialCapacity(attributes.length());
for (unsigned i = 0; i < attributes.length(); ++i) {
RefPtr attribute = attributes.item(i);
auto qualifiedName = attribute->qualifiedName();
if (attributesToExclude->contains(qualifiedName))
continue;
auto name = qualifiedName.toString();
if (name.length() > maximumNameLength)
continue;
if (name.startsWith("on"_s))
continue;
auto value = attribute->value();
if (value.length() > maximumValueLength)
continue;
attributesToCheck.append({ WTFMove(name), value.string() });
}
if (attributesToCheck.isEmpty())
return emptyString();
auto tagName = element.tagName();
for (auto [name, value] : attributesToCheck) {
String selector;
if (value.length() > maximumValueLengthForExactMatch) {
value = value.left(maximumValueLengthForExactMatch);
selector = makeString(tagName, '[', name, "^='"_s, value, "']"_s, suffix);
} else if (value.isEmpty())
selector = makeString(tagName, '[', name, ']', suffix);
else
selector = makeString(tagName, '[', name, "='"_s, value, "']"_s, suffix);
if (querySelectorMatchesOneElement(element, selector))
return selector;
}
return emptyString();
}
static inline String computeTagAndClassSelector(Element& element)
{
if (!element.hasClass())
return emptyString();
auto& classList = element.classList();
Vector<String> classes;
classes.reserveInitialCapacity(classList.length());
for (unsigned i = 0; i < std::min<unsigned>(maximumNumberOfClasses, classList.length()); ++i)
classes.append(classList.item(i));
auto selector = makeString(element.tagName(), '.', makeStringByJoining(classes, "."_s));
if (querySelectorMatchesOneElement(element, selector))
return selector;
return emptyString();
}
static String siblingRelativeSelectorRecursive(Element&, ElementSelectorCache&);
static String parentRelativeSelectorRecursive(Element&, ElementSelectorCache&);
static String shortestSelector(const Vector<String>& selectors)
{
auto minLength = std::numeric_limits<size_t>::max();
String shortestSelector;
for (auto& selector : selectors) {
if (selector.length() >= minLength)
continue;
minLength = selector.length();
shortestSelector = selector;
}
return shortestSelector;
}
static String selectorForElementRecursive(Element& element, ElementSelectorCache& cache)
{
if (auto selector = cache.get(element))
return *selector;
Vector<String> selectors;
selectors.reserveInitialCapacity(5);
if (auto selector = computeIDSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (querySelectorMatchesOneElement(element, element.tagName()))
selectors.append(element.tagName());
else if (auto selector = computeTagAndClassSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (auto selector = computeTagAndAttributeSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (auto selector = shortestSelector(selectors); !selector.isEmpty()) {
cache.add(element, selector);
return selector;
}
if (auto selector = parentRelativeSelectorRecursive(element, cache); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (auto selector = siblingRelativeSelectorRecursive(element, cache); !selector.isEmpty())
selectors.append(WTFMove(selector));
auto selector = shortestSelector(selectors);
cache.add(element, selector);
return selector;
}
static String siblingRelativeSelectorRecursive(Element& element, ElementSelectorCache& cache)
{
RefPtr<Element> siblingElement;
for (RefPtr sibling = element.previousSibling(); sibling; sibling = sibling->previousSibling()) {
siblingElement = dynamicDowncast<Element>(sibling);
if (siblingElement)
break;
}
if (!siblingElement)
return emptyString();
if (auto selector = selectorForElementRecursive(*siblingElement, cache); !selector.isEmpty())
return makeString(WTFMove(selector), " + "_s, element.tagName());
return emptyString();
}
static String parentRelativeSelectorRecursive(Element& element, ElementSelectorCache& cache)
{
RefPtr parent = element.parentElement();
if (!parent)
return emptyString();
if (auto selector = selectorForElementRecursive(*parent, cache); !selector.isEmpty()) {
auto selectorPrefix = makeString(WTFMove(selector), " > "_s, element.tagName());
auto [childIndex, firstOfType, lastOfType] = findChild(element, *parent);
if (childIndex == notFound)
return emptyString();
if (firstOfType && lastOfType)
return selectorPrefix;
if (firstOfType)
return makeString(WTFMove(selectorPrefix), ":first-of-type"_s);
if (lastOfType)
return makeString(WTFMove(selectorPrefix), ":last-of-type"_s);
return makeString(WTFMove(selectorPrefix), ":nth-child("_s, childIndex + 1, ')');
}
return emptyString();
}
static String computeHasChildSelector(Element& element)
{
static NeverDestroyed<MemoryCompactLookupOnlyRobinHoodHashSet<QualifiedName>> tagsToCheckForUniqueAttributes { std::initializer_list<QualifiedName> {
HTMLNames::aTag,
HTMLNames::imgTag,
HTMLNames::timeTag,
HTMLNames::pictureTag,
HTMLNames::videoTag,
HTMLNames::articleTag,
HTMLNames::audioTag,
HTMLNames::iframeTag,
HTMLNames::embedTag,
HTMLNames::sourceTag,
HTMLNames::formTag,
HTMLNames::inputTag,
HTMLNames::selectTag,
HTMLNames::buttonTag
} };
String selectorSuffix;
for (auto& child : descendantsOfType<HTMLElement>(element)) {
if (!tagsToCheckForUniqueAttributes->contains(child.tagQName()))
continue;
auto selector = computeTagAndAttributeSelector(child);
if (selector.isEmpty())
continue;
selectorSuffix = makeString(":has("_s, WTFMove(selector), ')');
break;
}
if (selectorSuffix.isEmpty())
return emptyString();
for (auto& ancestor : lineageOfType<HTMLElement>(element)) {
auto selectorWithTag = makeString(ancestor.tagName(), selectorSuffix);
if (querySelectorMatchesOneElement(element, selectorWithTag))
return selectorWithTag;
if (auto selector = computeTagAndAttributeSelector(ancestor, selectorSuffix); !selector.isEmpty())
return selector;
selectorSuffix = makeString(" > "_s, WTFMove(selectorWithTag));
}
return emptyString();
}
// Returns multiple CSS selectors that uniquely match the target element.
static Vector<Vector<String>> selectorsForTarget(Element& element, ElementSelectorCache& cache)
{
if (RefPtr pseudoElement = dynamicDowncast<PseudoElement>(element)) {
RefPtr host = pseudoElement->hostElement();
if (!host)
return { };
auto pseudoSelector = [&]() -> String {
if (element.isBeforePseudoElement())
return "::before"_s;
if (element.isAfterPseudoElement())
return "::after"_s;
return { };
}();
if (pseudoSelector.isEmpty())
return { };
auto selectors = selectorsForTarget(*host, cache);
if (selectors.isEmpty())
return { };
for (auto& selector : selectors.last())
selector = makeString(selector, pseudoSelector);
return selectors;
}
Vector<Vector<String>> selectorsIncludingShadowHost;
if (RefPtr shadowHost = element.shadowHost()) {
selectorsIncludingShadowHost = selectorsForTarget(*shadowHost, cache);
if (selectorsIncludingShadowHost.isEmpty())
return { };
}
Vector<String> selectors;
selectors.reserveInitialCapacity(5);
// First, try to compute a selector using only the target element and its attributes.
if (auto selector = computeIDSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (querySelectorMatchesOneElement(element, element.tagName()))
selectors.append(element.tagName());
else {
if (auto selector = computeTagAndClassSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (auto selector = computeTagAndAttributeSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
}
if (selectors.isEmpty()) {
// Next, fall back to using :has(), with a child that can be uniquely identified.
if (auto selector = computeHasChildSelector(element); !selector.isEmpty())
selectors.append(WTFMove(selector));
}
if (selectors.isEmpty()) {
// Finally, fall back on nth-child or sibling selectors.
if (auto selector = parentRelativeSelectorRecursive(element, cache); !selector.isEmpty())
selectors.append(WTFMove(selector));
if (auto selector = siblingRelativeSelectorRecursive(element, cache); !selector.isEmpty())
selectors.append(WTFMove(selector));
}
std::ranges::sort(selectors, { }, &String::length);
if (!selectors.isEmpty())
cache.add(element, selectors.first());
selectorsIncludingShadowHost.append(WTFMove(selectors));
return selectorsIncludingShadowHost;
}
static inline RectEdges<bool> computeOffsetEdges(const RenderStyle& style)
{
return {
style.top().isSpecified(),
style.right().isSpecified(),
style.bottom().isSpecified(),
style.left().isSpecified()
};
}
static inline Vector<FrameIdentifier> collectChildFrameIdentifiers(const Element& element)
{
Vector<FrameIdentifier> identifiers;
for (auto& owner : descendantsOfType<HTMLFrameOwnerElement>(element)) {
if (RefPtr frame = owner.contentFrame())
identifiers.append(frame->frameID());
}
return identifiers;
}
static FloatRect computeClientRect(const RenderElement& renderer)
{
auto rect = FloatRect { renderer.absoluteBoundingBoxRect() };
renderer.document().convertAbsoluteToClientRect(rect, renderer.style());
return rect;
}
static Vector<Ref<Element>> collectDocumentElementsFromChildFrames(const ContainerNode& container)
{
Vector<Ref<Element>> documentElements;
auto appendElement = [&](const HTMLFrameOwnerElement& owner) {
if (RefPtr contentDocument = owner.contentDocument()) {
if (RefPtr documentElement = contentDocument->documentElement())
documentElements.append(documentElement.releaseNonNull());
}
};
if (RefPtr containerAsFrameOwner = dynamicDowncast<HTMLFrameOwnerElement>(container))
appendElement(*containerAsFrameOwner);
for (auto& descendant : descendantsOfType<HTMLFrameOwnerElement>(container))
appendElement(descendant);
return documentElements;
}
static String searchableTextForTarget(Element& target)
{
auto longestText = emptyString();
size_t longestLength = 0;
TextIterator iterator { makeRangeSelectingNodeContents(target), { TextIteratorBehavior::EmitsTextsWithoutTranscoding } };
for (; !iterator.atEnd(); iterator.advance()) {
auto text = iterator.copyableText().text().toString().trim(isASCIIWhitespace);
if (text.length() <= longestLength)
continue;
longestLength = text.length();
longestText = WTFMove(text);
}
auto documentElements = collectDocumentElementsFromChildFrames(target);
for (auto& documentElement : documentElements) {
if (auto text = searchableTextForTarget(documentElement); text.length() > longestLength) {
longestLength = text.length();
longestText = WTFMove(text);
}
}
if (longestLength >= minimumLengthForSearchableText)
return longestText.left(maximumLengthForSearchableText);
return emptyString();
}
static bool hasAudibleMedia(const Element& element)
{
#if ENABLE(VIDEO)
if (RefPtr media = dynamicDowncast<HTMLMediaElement>(element))
return media->isAudible();
for (Ref media : descendantsOfType<HTMLMediaElement>(element)) {
if (media->isAudible())
return true;
}
for (auto& documentElement : collectDocumentElementsFromChildFrames(element)) {
if (hasAudibleMedia(documentElement))
return true;
}
#else
UNUSED_PARAM(element);
#endif
return false;
}
static URL urlForElement(const Element& element)
{
if (RefPtr anchor = dynamicDowncast<HTMLAnchorElement>(element))
return anchor->href();
if (RefPtr image = dynamicDowncast<HTMLImageElement>(element))
return image->currentURL();
#if ENABLE(VIDEO)
if (RefPtr media = dynamicDowncast<HTMLMediaElement>(element))
return media->currentSrc();
#endif
if (CheckedPtr renderer = element.renderer()) {
if (auto& style = renderer->style(); style.hasBackgroundImage()) {
if (RefPtr image = style.backgroundLayers().first().image().tryStyleImage())
return image->url().resolved;
}
}
return { };
}
static void collectMediaAndLinkURLsRecursive(const Element& element, HashSet<URL>& urls)
{
auto addURLForElement = [&urls](const Element& element) {
if (auto url = urlForElement(element); !url.isEmpty() && !url.protocolIsData() && !url.protocolIsBlob())
urls.add(WTFMove(url));
};
addURLForElement(element);
for (auto& descendant : descendantsOfType<Element>(element)) {
addURLForElement(descendant);
auto frameOwner = dynamicDowncast<HTMLFrameOwnerElement>(descendant);
if (!frameOwner)
continue;
RefPtr contentDocument = frameOwner->contentDocument();
if (!contentDocument)
continue;
RefPtr documentElement = contentDocument->documentElement();
if (!documentElement)
continue;
collectMediaAndLinkURLsRecursive(*documentElement, urls);
}
}
static HashSet<URL> collectMediaAndLinkURLs(const Element& element)
{
HashSet<URL> urls;
collectMediaAndLinkURLsRecursive(element, urls);
return urls;
}
enum class IsNearbyTarget : bool { No, Yes };
static std::optional<TargetedElementInfo> targetedElementInfo(Element& element, IsNearbyTarget isNearbyTarget, ElementSelectorCache& cache, const WeakHashSet<Element, WeakPtrImplWithEventTargetData>& adjustedElements)
{
element.protectedDocument()->updateLayoutIgnorePendingStylesheets();
FloatRect boundsInClientCoordinates;
RectEdges<bool> offsetEdges;
PositionType positionType = PositionType::Static;
{
WeakPtr renderer = element.renderer();
if (!renderer)
return { };
offsetEdges = computeOffsetEdges(renderer->style());
positionType = renderer->style().position();
boundsInClientCoordinates = computeClientRect(*renderer);
}
bool isInVisibilityAdjustmentSubtree = [&] {
for (RefPtr ancestor = element; ancestor; ancestor = ancestor->parentElementInComposedTree()) {
if (adjustedElements.contains(*ancestor))
return true;
}
return false;
}();
auto [renderedText, screenReaderText, hasLargeReplacedDescendant] = TextExtraction::extractRenderedText(element);
return { {
.nodeIdentifier = element.nodeIdentifier(),
.documentIdentifier = element.document().identifier(),
.offsetEdges = offsetEdges,
.renderedText = WTFMove(renderedText),
.searchableText = searchableTextForTarget(element),
.screenReaderText = WTFMove(screenReaderText),
.selectors = selectorsForTarget(element, cache),
.boundsInRootView = element.boundingBoxInRootViewCoordinates(),
.boundsInClientCoordinates = WTFMove(boundsInClientCoordinates),
.positionType = positionType,
.childFrameIdentifiers = collectChildFrameIdentifiers(element),
.mediaAndLinkURLs = collectMediaAndLinkURLs(element),
.isNearbyTarget = isNearbyTarget == IsNearbyTarget::Yes,
.isPseudoElement = element.isPseudoElement(),
.isInShadowTree = element.isInShadowTree(),
.isInVisibilityAdjustmentSubtree = isInVisibilityAdjustmentSubtree,
.hasLargeReplacedDescendant = hasLargeReplacedDescendant,
.hasAudibleMedia = hasAudibleMedia(element)
} };
}
static const HTMLElement* findOnlyMainElement(const HTMLBodyElement& bodyElement)
{
RefPtr<const HTMLElement> onlyMainElement;
for (auto& descendant : descendantsOfType<HTMLElement>(bodyElement)) {
if (!descendant.hasTagName(HTMLNames::mainTag))
continue;
if (onlyMainElement) {
onlyMainElement = nullptr;
break;
}
onlyMainElement = descendant;
}
return onlyMainElement.get();
}
static bool isNavigationalElement(const Element& element)
{
if (element.hasTagName(HTMLNames::navTag))
return true;
auto roleValue = element.attributeWithoutSynchronization(HTMLNames::roleAttr);
return AccessibilityObject::ariaRoleToWebCoreRole(roleValue) == AccessibilityRole::LandmarkNavigation;
}
static bool containsNavigationalElement(const Element& element)
{
if (isNavigationalElement(element))
return true;
for (auto& descendant : descendantsOfType<HTMLElement>(element)) {
if (isNavigationalElement(descendant))
return true;
}
return false;
}
static bool isTargetCandidate(Element& element, const HTMLElement* onlyMainElement, const Element* hitTestedElement = nullptr)
{
if (!element.renderer())
return false;
if (element.isBeforePseudoElement() || element.isAfterPseudoElement()) {
// We don't need to worry about affecting main content if we're only adjusting pseudo elements.
return true;
}
if (&element == element.document().body())
return false;
if (&element == element.document().documentElement())
return false;
if (onlyMainElement && (onlyMainElement == &element || element.contains(*onlyMainElement)))
return false;
if (elementAndAncestorsAreOnlyRenderedChildren(element))
return false;
if (is<HTMLFrameOwnerElement>(hitTestedElement) && containsNavigationalElement(element))
return false;
return true;
}
static inline std::optional<IntRect> inflatedClientRectForAdjustmentRegionTracking(Element& element, float viewportArea)
{
CheckedPtr renderer = element.renderer();
if (!renderer)
return { };
if (!renderer->isOutOfFlowPositioned())
return { };
auto clientRect = computeClientRect(*renderer);
if (clientRect.isEmpty())
return { };
if (clientRect.area() / viewportArea >= maximumAreaRatioForTrackingAdjustmentAreas(viewportArea))
return { };
// Keep track of the client rects of elements we're targeting, until the client
// triggers visibility adjustment for these elements.
auto inflatedClientRect = enclosingIntRect(clientRect);
inflatedClientRect.inflate(marginForTrackingAdjustmentRects);
return { inflatedClientRect };
}
static bool shouldIgnoreExistingVisibilityAdjustments(const TargetedElementRequest& request)
{
return std::holds_alternative<String>(request.data) || std::holds_alternative<TargetedElementSelectors>(request.data);
}
Vector<TargetedElementInfo> ElementTargetingController::findTargets(TargetedElementRequest&& request)
{
Vector<ClearVisibilityAdjustmentForScope> clearVisibilityAdjustmentScopes;
if (shouldIgnoreExistingVisibilityAdjustments(request) && m_adjustedElements.computeSize()) {
for (auto& element : m_adjustedElements)
clearVisibilityAdjustmentScopes.append({ element });
if (RefPtr document = mainDocument())
document->updateLayoutIgnorePendingStylesheets();
}
auto checkViewportAreaRatio = CheckViewportAreaRatio::Yes;
auto [nodes, innerElement] = switchOn(request.data, [this](const String& searchText) {
return findNodes(searchText);
}, [this, &request](const FloatPoint& point) {
return findNodes(point, request.shouldIgnorePointerEventsNone);
}, [this, &checkViewportAreaRatio](const TargetedElementSelectors& selectors) {
checkViewportAreaRatio = CheckViewportAreaRatio::No;
return findNodes(selectors);
});
if (nodes.isEmpty())
return { };
auto includeNearbyElements = request.canIncludeNearbyElements ? IncludeNearbyElements::Yes : IncludeNearbyElements::No;
return extractTargets(WTFMove(nodes), WTFMove(innerElement), checkViewportAreaRatio, includeNearbyElements);
}
void ElementTargetingController::topologicallySortElementsHelper(NodeIdentifier currentElementID, Vector<NodeIdentifier>& depthSortedIDs, HashSet<NodeIdentifier>& processingIDs, HashSet<NodeIdentifier>& unprocessedIDs, const HashMap<NodeIdentifier, HashSet<NodeIdentifier>>& nodeIDToOccludedElementIDs)
{
if (processingIDs.contains(currentElementID)) {
ASSERT_NOT_REACHED();
return;
}
if (!unprocessedIDs.contains(currentElementID))
return;
unprocessedIDs.remove(currentElementID);
processingIDs.add(currentElementID);
for (auto& occludedElementID : nodeIDToOccludedElementIDs.get(currentElementID))
topologicallySortElementsHelper(occludedElementID, depthSortedIDs, processingIDs, unprocessedIDs, nodeIDToOccludedElementIDs);
processingIDs.remove(currentElementID);
depthSortedIDs.append(currentElementID);
}
Vector<NodeIdentifier> ElementTargetingController::topologicallySortElements(const HashMap<NodeIdentifier, HashSet<NodeIdentifier>>& nodeIDToOccludedElementIDs)
{
Vector<NodeIdentifier> depthSortedIDs;
HashSet<NodeIdentifier> processingIDs;
HashSet<NodeIdentifier> unprocessedIDs;
unprocessedIDs.addAll(nodeIDToOccludedElementIDs.keys());
while (!unprocessedIDs.isEmpty() || !processingIDs.isEmpty()) {
if (unprocessedIDs.isEmpty()) {
ASSERT_NOT_REACHED();
break;
}
topologicallySortElementsHelper(*unprocessedIDs.begin(), depthSortedIDs, processingIDs, unprocessedIDs, nodeIDToOccludedElementIDs);
}
depthSortedIDs.reverse();
return depthSortedIDs;
}
Vector<Vector<TargetedElementInfo>> ElementTargetingController::findAllTargets(float hitTestInterval)
{
RefPtr page = m_page.get();
if (!page) {
ASSERT_NOT_REACHED();
return { };
}
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame) {
ASSERT_NOT_REACHED();
return { };
}
RefPtr view = mainFrame->view();
if (!view) {
ASSERT_NOT_REACHED();
return { };
}
const auto viewportRect = view->unobscuredContentRect();
const auto halfHitTestInterval = std::floor(hitTestInterval / 2);
Vector<Vector<TargetedElementInfo>> targetsList;
for (auto x = viewportRect.x() + halfHitTestInterval; x < viewportRect.maxX(); x += hitTestInterval) {
for (auto y = viewportRect.y() + halfHitTestInterval; y < viewportRect.maxY(); y += hitTestInterval) {
auto [nodes, innerElement] = findNodes({ x, y }, true);
if (nodes.isEmpty())
continue;
targetsList.append(extractTargets(WTFMove(nodes), WTFMove(innerElement), CheckViewportAreaRatio::Yes, IncludeNearbyElements::No));
}
}
HashMap<NodeIdentifier, HashSet<NodeIdentifier>> nodeIDToOccludedElementIDs;
HashMap<NodeIdentifier, Vector<TargetedElementInfo>> nodeIDToTargets;
for (auto& targets : targetsList) {
if (targets.isEmpty())
continue;
const auto topElementID = targets.first().nodeIdentifier;
HashSet<NodeIdentifier> occludedElementIDsToInsert;
for (unsigned index = 1; index < targets.size(); ++index)
occludedElementIDsToInsert.add(targets[index].nodeIdentifier);
auto storedTargets = nodeIDToTargets.getOptional(topElementID);
auto storedIDsSet = nodeIDToOccludedElementIDs.getOptional(topElementID);
if (storedTargets && storedIDsSet) {
for (auto& target : targets) {
if (target.nodeIdentifier != topElementID && !storedIDsSet->contains(target.nodeIdentifier))
storedTargets->append(target);
}
nodeIDToTargets.set(topElementID, *storedTargets);
nodeIDToOccludedElementIDs.set(topElementID, storedIDsSet->unionWith(occludedElementIDsToInsert));
} else {
nodeIDToTargets.set(topElementID, targets);
nodeIDToOccludedElementIDs.set(topElementID, occludedElementIDsToInsert);
}
}
return topologicallySortElements(nodeIDToOccludedElementIDs).map([& nodeIDToTargets](const auto& nodeID) {
return nodeIDToTargets.get(nodeID);
});
}
std::pair<Vector<Ref<Node>>, RefPtr<Element>> ElementTargetingController::findNodes(FloatPoint pointInRootView, bool shouldIgnorePointerEventsNone)
{
RefPtr page = m_page.get();
if (!page)
return { };
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame)
return { };
RefPtr document = mainFrame->document();
if (!document)
return { };
RefPtr view = mainFrame->view();
if (!view)
return { };
static constexpr OptionSet defaultHitTestOptions {
HitTestRequest::Type::ReadOnly,
HitTestRequest::Type::DisallowUserAgentShadowContent,
HitTestRequest::Type::CollectMultipleElements,
HitTestRequest::Type::IncludeAllElementsUnderPoint
};
auto hitTestOptions = defaultHitTestOptions;
if (shouldIgnorePointerEventsNone)
hitTestOptions.add(HitTestRequest::Type::IgnoreCSSPointerEventsProperty);
HitTestResult result { LayoutPoint { view->rootViewToContents(pointInRootView) } };
document->hitTest(hitTestOptions, result);
return { copyToVector(result.listBasedTestResult()), result.innerNonSharedElement() };
}
static Element* searchForElementContainingText(ContainerNode& container, const String& searchText)
{
auto remainingRange = makeRangeSelectingNodeContents(container);
while (is_lt(treeOrder(remainingRange.start, remainingRange.end))) {
auto foundRange = findPlainText(remainingRange, searchText, {
FindOption::DoNotRevealSelection,
FindOption::DoNotSetSelection,
});
if (foundRange.collapsed())
break;
RefPtr target = commonInclusiveAncestor<ComposedTree>(foundRange);
if (!target) {
remainingRange.start = foundRange.end;
continue;
}
CheckedPtr renderer = target->renderer();
if (!renderer || renderer->style().isForceHidden()) {
remainingRange.start = foundRange.end;
continue;
}
return ancestorsOfType<Element>(*target).first();
}
auto documentElements = collectDocumentElementsFromChildFrames(container);
for (auto& documentElement : documentElements) {
if (RefPtr target = searchForElementContainingText(documentElement, searchText))
return target.get();
}
return nullptr;
}
std::pair<Vector<Ref<Node>>, RefPtr<Element>> ElementTargetingController::findNodes(const String& searchText)
{
RefPtr document = mainDocument();
if (!document)
return { };
RefPtr documentElement = document->documentElement();
if (!documentElement)
return { };
RefPtr foundElement = searchForElementContainingText(*documentElement, searchText);
if (!foundElement)
return { };
while (!foundElement->document().isTopDocument())
foundElement = foundElement->document().ownerElement();
if (!foundElement) {
ASSERT_NOT_REACHED();
return { };
}
Vector<Ref<Node>> potentialCandidates;
potentialCandidates.append(*foundElement);
for (auto& ancestor : ancestorsOfType<Element>(*foundElement))
potentialCandidates.append(ancestor);
return { WTFMove(potentialCandidates), WTFMove(foundElement) };
}
std::pair<Vector<Ref<Node>>, RefPtr<Element>> ElementTargetingController::findNodes(const TargetedElementSelectors& selectors)
{
auto [foundElement, selectorIncludingPseudo] = findElementFromSelectors(selectors);
if (!foundElement)
return { };
return { { *foundElement }, foundElement };
}
static Vector<Ref<Element>> filterRedundantNearbyTargets(HashSet<Ref<Element>>&& unfilteredNearbyTargets)
{
HashMap<Ref<Element>, bool> shouldKeepCache;
Vector<Ref<Element>> filteredResults;
for (auto& originalTarget : unfilteredNearbyTargets) {
Vector<Ref<Element>> ancestorsOfTarget;
bool shouldKeep = true;
for (auto& ancestor : ancestorsOfType<Element>(originalTarget)) {
if (unfilteredNearbyTargets.contains(ancestor)) {
shouldKeep = false;
break;
}
if (auto entry = shouldKeepCache.find(ancestor); entry != shouldKeepCache.end()) {
shouldKeep = entry->value;
break;
}
ancestorsOfTarget.append(ancestor);
}
for (auto& ancestor : ancestorsOfTarget)
shouldKeepCache.add(ancestor, shouldKeep);
if (shouldKeep)
filteredResults.append(originalTarget);
}
return filteredResults;
}
static IntRect absoluteBoundsForTargetAreaRatio(const Element& element, WeakHashMap<const Element, IntRect, WeakPtrImplWithEventTargetData>& cache)
{
auto absoluteBoundingBoxRect = [&cache](const Element& element) {
auto entry = cache.find(element);
if (entry != cache.end())
return entry->value;
CheckedPtr renderer = element.renderer();
if (!renderer)
return IntRect { };
auto bounds = renderer->absoluteBoundingBoxRect();
cache.set(element, bounds);
return bounds;
};
auto bounds = absoluteBoundingBoxRect(element);
bool hasVisualOverflowX = false;
bool hasVisualOverflowY = false;
if (CheckedPtr style = element.renderStyle()) {
hasVisualOverflowX = style->overflowX() == Overflow::Visible;
hasVisualOverflowY = style->overflowY() == Overflow::Visible;
}
if (!hasVisualOverflowX && !hasVisualOverflowY)
return bounds;
IntRect absoluteBoundsOfChildren;
for (Ref child : childrenOfType<Element>(element))
absoluteBoundsOfChildren.uniteIfNonZero(absoluteBoundingBoxRect(child));
if (absoluteBoundsOfChildren.isEmpty())
return bounds;
auto boundsIncludingChildren = unionRect(bounds, absoluteBoundsOfChildren);
if (hasVisualOverflowX) {
bounds.shiftXEdgeTo(boundsIncludingChildren.x());
bounds.shiftMaxXEdgeTo(boundsIncludingChildren.maxX());
}
if (hasVisualOverflowY) {
bounds.shiftYEdgeTo(boundsIncludingChildren.y());
bounds.shiftMaxYEdgeTo(boundsIncludingChildren.maxY());
}
return bounds;
}
Vector<TargetedElementInfo> ElementTargetingController::extractTargets(Vector<Ref<Node>>&& nodes, RefPtr<Element>&& innerElement, CheckViewportAreaRatio checkViewportAreaRatio, IncludeNearbyElements includeNearbyElements)
{
RefPtr page = m_page.get();
if (!page) {
ASSERT_NOT_REACHED();
return { };
}
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame) {
ASSERT_NOT_REACHED();
return { };
}
RefPtr document = mainFrame->document();
if (!document) {
ASSERT_NOT_REACHED();
return { };
}
RefPtr view = mainFrame->view();
if (!view) {
ASSERT_NOT_REACHED();
return { };
}
RefPtr bodyElement = document->body();
if (!bodyElement) {
ASSERT_NOT_REACHED();
return { };
}
FloatSize viewportSize = view->baseLayoutViewportSize();
auto viewportArea = viewportSize.area();
if (!viewportArea)
return { };
RefPtr onlyMainElement = findOnlyMainElement(*bodyElement);
auto candidates = [&] {
Vector<Ref<Element>> elements;
elements.reserveInitialCapacity(nodes.size());
for (auto& node : nodes) {
if (RefPtr element = dynamicDowncast<Element>(node); element && isTargetCandidate(*element, onlyMainElement.get(), innerElement.get()))
elements.append(element.releaseNonNull());
}
return elements;
}();
auto nearbyTargetAreaRatio = maximumAreaRatioForNearbyTargets(viewportArea);
auto addOutOfFlowTargetClientRectIfNeeded = [&](Element& element) {
if (auto rect = inflatedClientRectForAdjustmentRegionTracking(element, viewportArea))
m_recentAdjustmentClientRects.set(element.nodeIdentifier(), *rect);
};
auto computeViewportAreaRatio = [&](IntRect boundingBox) {
auto area = boundingBox.area<RecordOverflow>();
return area.hasOverflowed() ? std::numeric_limits<float>::max() : area.value() / viewportArea;
};
Vector<Ref<Element>> targets; // The front-most target is last in this list.
Region additionalRegionForNearbyElements;
WeakHashMap<const Element, IntRect, WeakPtrImplWithEventTargetData> absoluteBoundsCache;
// Prioritize parent elements over their children by traversing backwards over the candidates.
// This allows us to target only the top-most container elements that satisfy the criteria.
// While adding targets, we also accumulate additional regions, wherein we should report any
// nearby targets.
while (!candidates.isEmpty()) {
Ref target = candidates.takeLast();
CheckedPtr targetRenderer = target->renderer();
auto targetBoundingBox = view->contentsToRootView(absoluteBoundsForTargetAreaRatio(target, absoluteBoundsCache));
auto targetAreaRatio = computeViewportAreaRatio(targetBoundingBox);
auto hasOneRenderedChild = [](const Element& target) {
CheckedPtr renderer = target.renderer();
if (!renderer)
return false;
CheckedPtr firstChild = renderer->firstChild();
return firstChild && firstChild == renderer->lastChild();
};
bool shouldSkipIrrelevantTarget = [&] {
if (targetAreaRatio < minimumAreaRatioForElementToCoverViewport && !hasOneRenderedChild(target))
return false;
auto& style = targetRenderer->style();
if (auto specifiedZIndexValue = style.specifiedZIndex().tryValue(); specifiedZIndexValue && *specifiedZIndexValue < 0)
return true;
return targetRenderer->isOutOfFlowPositioned()
&& (!style.hasBackground() || style.opacity().isTransparent())
&& targetRenderer->usedPointerEvents() == PointerEvents::None;
}();
if (shouldSkipIrrelevantTarget)
continue;
bool shouldAddTarget = [&] {
if (targetAreaRatio <= 0)
return false;
if (targetRenderer->isFixedPositioned())
return true;
if (targetRenderer->isStickilyPositioned())
return true;
if (!target->firstElementChild())
return true;
return checkViewportAreaRatio == CheckViewportAreaRatio::No
|| (targetRenderer->isAbsolutelyPositioned() && targetAreaRatio < maximumAreaRatioForAbsolutelyPositionedContent(viewportArea))
|| (minimumAreaRatioForInFlowContent(viewportArea) < targetAreaRatio && targetAreaRatio < maximumAreaRatioForInFlowContent(viewportArea));
}();
if (!shouldAddTarget)
continue;
bool checkForNearbyTargets = includeNearbyElements == IncludeNearbyElements::Yes
&& targetRenderer->isOutOfFlowPositioned()
&& targetAreaRatio < nearbyTargetAreaRatio;
if (checkForNearbyTargets && computeViewportAreaRatio(targetBoundingBox) < nearbyTargetAreaRatio)
additionalRegionForNearbyElements.unite(targetBoundingBox);
auto targetEncompassesOtherCandidate = [](Element& target, Element& candidate) {
if (&target == &candidate)
return true;
RefPtr<Element> candidateOrHost;
if (RefPtr pseudo = dynamicDowncast<PseudoElement>(candidate))
candidateOrHost = pseudo->hostElement();
else
candidateOrHost = candidate;
return candidateOrHost && target.isShadowIncludingInclusiveAncestorOf(candidateOrHost.get());
};
candidates.removeAllMatching([&](auto& candidate) {
if (!targetEncompassesOtherCandidate(target, candidate))
return false;
if (checkForNearbyTargets) {
auto boundingBox = candidate->boundingBoxInRootViewCoordinates();
if (computeViewportAreaRatio(boundingBox) < nearbyTargetAreaRatio)
additionalRegionForNearbyElements.unite(boundingBox);
}
return true;
});
targets.append(WTFMove(target));
}
if (targets.isEmpty())
return { };
m_recentAdjustmentClientRectsCleanUpTimer.restart();
ElementSelectorCache cache;
Vector<TargetedElementInfo> results;
results.reserveInitialCapacity(targets.size());
for (auto iterator = targets.rbegin(); iterator != targets.rend(); ++iterator) {
if (auto info = targetedElementInfo(*iterator, IsNearbyTarget::No, cache, m_adjustedElements)) {
results.append(WTFMove(*info));
addOutOfFlowTargetClientRectIfNeeded(*iterator);
}
}
if (additionalRegionForNearbyElements.isEmpty())
return results;
auto nearbyTargets = [&]() -> Vector<Ref<Element>> {
HashSet<Ref<Element>> results;
CheckedPtr bodyRenderer = bodyElement->renderer();
if (!bodyRenderer)
return { };
for (auto& renderer : descendantsOfType<RenderElement>(*bodyRenderer)) {
if (!renderer.isOutOfFlowPositioned())
continue;
RefPtr element = renderer.element();
if (!element)
continue;
bool elementIsAlreadyTargeted = targets.containsIf([&element](auto& target) {
return target->isShadowIncludingInclusiveAncestorOf(element.get());
});
if (elementIsAlreadyTargeted)
continue;
if (results.contains(*element))
continue;
if (nodes.containsIf([&](auto& node) { return node.ptr() == element; }))
continue;
if (!isTargetCandidate(*element, onlyMainElement.get(), innerElement.get()))
continue;
auto boundingBox = element->boundingBoxInRootViewCoordinates();
if (boundingBox.width() <= minimumWidthForNearbyTarget)
continue;
if (boundingBox.height() <= minimumHeightForNearbyTarget)
continue;
if (!additionalRegionForNearbyElements.contains(boundingBox))
continue;
if (computeViewportAreaRatio(boundingBox) > nearbyTargetAreaRatio)
continue;
results.add(element.releaseNonNull());
}
return filterRedundantNearbyTargets(WTFMove(results));
}();
for (auto& element : nearbyTargets) {
if (auto info = targetedElementInfo(element, IsNearbyTarget::Yes, cache, m_adjustedElements)) {
results.append(WTFMove(*info));
addOutOfFlowTargetClientRectIfNeeded(element);
}
}
return results;
}
static inline Element& elementToAdjust(Element& element)
{
if (RefPtr pseudoElement = dynamicDowncast<PseudoElement>(element)) {
if (RefPtr host = pseudoElement->hostElement())
return *host;
}
return element;
}
static inline VisibilityAdjustment adjustmentToApply(Element& element)
{
if (element.isAfterPseudoElement())
return VisibilityAdjustment::AfterPseudo;
if (element.isBeforePseudoElement())
return VisibilityAdjustment::BeforePseudo;
return VisibilityAdjustment::Subtree;
}
struct VisibilityAdjustmentResult {
RefPtr<Element> adjustedElement;
bool invalidateSubtree { false };
};
static inline VisibilityAdjustmentResult adjustVisibilityIfNeeded(Element& element)
{
Ref adjustedElement = elementToAdjust(element);
auto adjustment = adjustmentToApply(element);
auto currentAdjustment = adjustedElement->visibilityAdjustment();
if (currentAdjustment.contains(adjustment))
return { };
adjustedElement->setVisibilityAdjustment(currentAdjustment | adjustment);
return { adjustedElement.ptr(), adjustment == VisibilityAdjustment::Subtree };
}
bool ElementTargetingController::adjustVisibility(Vector<TargetedElementAdjustment>&& adjustments)
{
RefPtr page = m_page.get();
if (!page)
return false;
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame)
return false;
RefPtr frameView = mainFrame->view();
if (!frameView)
return false;
FloatSize viewportSize = frameView->baseLayoutViewportSize();
auto viewportArea = viewportSize.area();
if (!viewportArea)
return false;
Region newAdjustmentRegion;
for (auto& [identifiers, selectors] : adjustments) {
auto [nodeID, documentID] = identifiers;
auto rect = m_recentAdjustmentClientRects.get(nodeID);
if (rect.isEmpty())
continue;
if (RefPtr target = dynamicDowncast<Element>(Node::fromIdentifier(identifiers.first)); target && target->isInVisibilityAdjustmentSubtree()) {
// This target's visibility has already been adjusted; avoid treating it as a new region.
continue;
}
newAdjustmentRegion.unite(rect);
}
m_repeatedAdjustmentClientRegion.unite(intersect(m_adjustmentClientRegion, newAdjustmentRegion));
m_adjustmentClientRegion.unite(newAdjustmentRegion);
Vector<Ref<Element>> elements;
elements.reserveInitialCapacity(adjustments.size());
for (auto& [identifiers, selectors] : adjustments) {
auto [nodeID, documentID] = identifiers;
RefPtr element = dynamicDowncast<Element>(Node::fromIdentifier(nodeID));
if (!element)
continue;
if (element->document().identifier() != documentID)
continue;
elements.append(element.releaseNonNull());
if (m_additionalAdjustmentCount < maximumNumberOfAdditionalAdjustments) {
m_visibilityAdjustmentSelectors.append({ nodeID, WTFMove(selectors) });
m_additionalAdjustmentCount++;
}
}
bool changed = false;
for (auto& element : elements) {
CheckedPtr renderer = element->renderer();
if (!renderer)
continue;
auto [adjustedElement, invalidateSubtree] = adjustVisibilityIfNeeded(element);
if (!adjustedElement)
continue;
changed = true;
if (invalidateSubtree)
adjustedElement->invalidateStyleAndRenderersForSubtree();
else
adjustedElement->invalidateStyle();
m_adjustedElements.add(element);
m_documentsAffectedByVisibilityAdjustment.add(element->document());
}
if (changed)
dispatchVisibilityAdjustmentStateDidChange();
return changed;
}
static void adjustRegionAfterViewportSizeChange(Region& region, FloatSize oldSize, FloatSize newSize)
{
if (region.isEmpty())
return;
bool shouldRebuildRegion = false;
auto adjustedRects = region.rects().map([&](auto rect) {
auto distanceToLeftEdge = std::max<float>(0, rect.x());
auto distanceToTopEdge = std::max<float>(0, rect.y());
auto distanceToRightEdge = std::max<float>(0, oldSize.width() - rect.maxX());
auto distanceToBottomEdge = std::max<float>(0, oldSize.height() - rect.maxY());
float widthDelta = newSize.width() - oldSize.width();
float heightDelta = newSize.height() - oldSize.height();
FloatRect adjustedRect = rect;
if (widthDelta) {
if (std::abs(distanceToLeftEdge - distanceToRightEdge) < minimumDistanceToConsiderEdgesEquidistant)
adjustedRect.inflateX(widthDelta / 2);
else if (distanceToRightEdge < distanceToLeftEdge)
adjustedRect.move(widthDelta, 0);
}
if (heightDelta) {
if (std::abs(distanceToTopEdge - distanceToBottomEdge) < minimumDistanceToConsiderEdgesEquidistant)
adjustedRect.inflateY(heightDelta / 2);
else if (distanceToBottomEdge < distanceToTopEdge)
adjustedRect.move(heightDelta, 0);
}
auto enclosingAdjustedRect = enclosingIntRect(adjustedRect);
if (enclosingAdjustedRect != rect)
shouldRebuildRegion |= true;
return enclosingAdjustedRect;
});
if (!shouldRebuildRegion)
return;
region = { };
for (auto newRect : adjustedRects)
region.unite(newRect);
}
void ElementTargetingController::adjustVisibilityInRepeatedlyTargetedRegions(Document& document)
{
if (RefPtr frame = document.frame(); !frame || !frame->isMainFrame())
return;
RefPtr frameView = document.view();
if (!frameView)
return;
CheckedPtr renderView = document.renderView();
if (!renderView)
return;
RefPtr bodyElement = document.body();
if (!bodyElement)
return;
auto previousViewportSize = std::exchange(m_viewportSizeForVisibilityAdjustment, frameView->baseLayoutViewportSize());
if (previousViewportSize != m_viewportSizeForVisibilityAdjustment) {
adjustRegionAfterViewportSizeChange(m_adjustmentClientRegion, previousViewportSize, m_viewportSizeForVisibilityAdjustment);
adjustRegionAfterViewportSizeChange(m_repeatedAdjustmentClientRegion, previousViewportSize, m_viewportSizeForVisibilityAdjustment);
}
if (RefPtr loader = document.loader(); loader && !m_didCollectInitialAdjustments) {
m_initialVisibilityAdjustmentSelectors = loader->visibilityAdjustmentSelectors();
m_visibilityAdjustmentSelectors.appendVector(m_initialVisibilityAdjustmentSelectors.map([](auto& selectors) -> std::pair<Markable<NodeIdentifier>, TargetedElementSelectors> {
return { std::nullopt, selectors };
}));
m_startTimeForSelectorBasedVisibilityAdjustment = ApproximateTime::now();
m_didCollectInitialAdjustments = true;
}
if (!m_visibilityAdjustmentSelectors.isEmpty()) {
if (ApproximateTime::now() - m_startTimeForSelectorBasedVisibilityAdjustment <= selectorBasedVisibilityAdjustmentThrottlingTimeLimit)
applyVisibilityAdjustmentFromSelectors();
else if (!m_selectorBasedVisibilityAdjustmentTimer.isActive())
m_selectorBasedVisibilityAdjustmentTimer.startOneShot(selectorBasedVisibilityAdjustmentInterval);
}
if (m_repeatedAdjustmentClientRegion.isEmpty())
return;
RefPtr onlyMainElement = findOnlyMainElement(*bodyElement);
auto visibleDocumentRect = frameView->windowToContents(frameView->windowClipRect());
Vector<Ref<Element>> elementsToAdjust;
for (auto& renderer : descendantsOfType<RenderElement>(*renderView)) {
if (!renderer.isOutOfFlowPositioned())
continue;
RefPtr element = renderer.element();
if (!element)
continue;
if (!renderer.isVisibleInDocumentRect(visibleDocumentRect))
continue;
if (!m_repeatedAdjustmentClientRegion.contains(enclosingIntRect(computeClientRect(renderer))))
continue;
if (!isTargetCandidate(*element, onlyMainElement.get()))
continue;
elementsToAdjust.append(element.releaseNonNull());
}
if (elementsToAdjust.isEmpty())
return;
for (auto& element : elementsToAdjust) {
auto [adjustedElement, invalidateSubtree] = adjustVisibilityIfNeeded(element);
if (!adjustedElement)
continue;
if (invalidateSubtree)
adjustedElement->invalidateStyleAndRenderersForSubtree();
else
adjustedElement->invalidateStyle();
m_adjustedElements.add(element);
m_documentsAffectedByVisibilityAdjustment.add(element->document());
}
dispatchVisibilityAdjustmentStateDidChange();
}
static std::pair<String, VisibilityAdjustment> resolveSelectorToQuery(const String& selectorIncludingPseudo)
{
auto components = selectorIncludingPseudo.splitAllowingEmptyEntries("::"_s);
if (components.size() == 1)
return { components.first(), VisibilityAdjustment::Subtree };
if (components.size() == 2) {
auto pseudo = components.last();
if (equalLettersIgnoringASCIICase(pseudo, "after"_s))
return { components.first(), VisibilityAdjustment::AfterPseudo };
if (equalLettersIgnoringASCIICase(pseudo, "before"_s))
return { components.first(), VisibilityAdjustment::BeforePseudo };
}
return { { }, VisibilityAdjustment::Subtree };
}
void ElementTargetingController::applyVisibilityAdjustmentFromSelectors()
{
if (m_visibilityAdjustmentSelectors.isEmpty())
return;
RefPtr page = m_page.get();
if (!page)
return;
RefPtr document = mainDocument();
if (!document)
return;
document->updateLayoutIgnorePendingStylesheets();
auto viewportArea = m_viewportSizeForVisibilityAdjustment.area();
Region adjustmentRegion;
Vector<String> matchingSelectors;
for (auto& [identifier, selectorsForElementIncludingShadowHosts] : m_visibilityAdjustmentSelectors) {
auto [element, selectorIncludingPseudo] = findElementFromSelectors(selectorsForElementIncludingShadowHosts);
if (!element)
continue;
auto [selector, adjustment] = resolveSelectorToQuery(selectorIncludingPseudo);
auto currentAdjustment = element->visibilityAdjustment();
if (currentAdjustment.contains(adjustment))
continue;
element->setVisibilityAdjustment(currentAdjustment | adjustment);
if (adjustment == VisibilityAdjustment::Subtree)
element->invalidateStyleAndRenderersForSubtree();
else
element->invalidateStyle();
m_adjustedElements.add(*element);
m_documentsAffectedByVisibilityAdjustment.add(element->document());
if (auto clientRect = inflatedClientRectForAdjustmentRegionTracking(*element, viewportArea))
adjustmentRegion.unite(*clientRect);
matchingSelectors.append(WTFMove(selectorIncludingPseudo));
}
if (!adjustmentRegion.isEmpty())
m_adjustmentClientRegion.unite(adjustmentRegion);
if (matchingSelectors.isEmpty())
return;
dispatchVisibilityAdjustmentStateDidChange();
page->chrome().client().didAdjustVisibilityWithSelectors(WTFMove(matchingSelectors));
}
ElementTargetingController::FindElementFromSelectorsResult ElementTargetingController::findElementFromSelectors(const TargetedElementSelectors& selectorsForElementIncludingShadowHosts)
{
if (selectorsForElementIncludingShadowHosts.isEmpty())
return { };
RefPtr document = mainDocument();
if (!document)
return { };
Ref<ContainerNode> containerToQuery = *document;
size_t indexOfSelectorToQuery = 0;
for (auto& selectorsToQuery : selectorsForElementIncludingShadowHosts) {
bool isLastTarget = ++indexOfSelectorToQuery == selectorsForElementIncludingShadowHosts.size();
RefPtr<Element> currentTarget;
for (auto& selectorIncludingPseudo : selectorsToQuery) {
auto [selector, adjustment] = resolveSelectorToQuery(selectorIncludingPseudo);
if (selector.isEmpty()) {
// FIXME: Handle the case where the full selector is `::after|before`.
continue;
}
auto queryResult = containerToQuery->querySelector(selector);
if (queryResult.hasException())
continue;
RefPtr element = queryResult.releaseReturnValue();
if (!element)
continue;
CheckedPtr renderer = element->renderer();
if (!renderer)
continue;
if (adjustment == VisibilityAdjustment::AfterPseudo && !element->afterPseudoElement())
continue;
if (adjustment == VisibilityAdjustment::BeforePseudo && !element->beforePseudoElement())
continue;
if (isLastTarget) {
if (computeClientRect(*renderer).isEmpty())
return { };
return { WTFMove(element), selectorIncludingPseudo };
}
currentTarget = WTFMove(element);
break;
}
if (!currentTarget) {
// We failed to resolve the targeted element, or one of its shadow hosts.
break;
}
if (isLastTarget) {
// We resolved the final targeted element.
break;
}
RefPtr nextShadowRoot = currentTarget->shadowRoot();
if (!nextShadowRoot)
break;
// Continue the search underneath the next shadow root.
containerToQuery = nextShadowRoot.releaseNonNull();
}
return { };
}
void ElementTargetingController::reset()
{
m_adjustmentClientRegion = { };
m_repeatedAdjustmentClientRegion = { };
m_viewportSizeForVisibilityAdjustment = { };
m_adjustedElements = { };
m_visibilityAdjustmentSelectors = { };
m_initialVisibilityAdjustmentSelectors = { };
m_didCollectInitialAdjustments = false;
m_additionalAdjustmentCount = 0;
m_selectorBasedVisibilityAdjustmentTimer.stop();
m_startTimeForSelectorBasedVisibilityAdjustment = { };
m_recentAdjustmentClientRectsCleanUpTimer.stop();
cleanUpAdjustmentClientRects();
}
void ElementTargetingController::didChangeMainDocument(Document* newDocument)
{
m_shouldRecomputeAdjustedElements = newDocument && m_documentsAffectedByVisibilityAdjustment.contains(*newDocument);
}
bool ElementTargetingController::resetVisibilityAdjustments(const Vector<TargetedElementIdentifiers>& identifiers)
{
RefPtr page = m_page.get();
if (!page)
return false;
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame)
return false;
RefPtr frameView = mainFrame->view();
if (!frameView)
return false;
RefPtr document = mainFrame->document();
if (!document)
return false;
document->updateLayoutIgnorePendingStylesheets();
HashSet<Ref<Element>> elementsToReset;
if (identifiers.isEmpty()) {
elementsToReset.reserveInitialCapacity(m_adjustedElements.computeSize());
for (auto& element : m_adjustedElements)
elementsToReset.add(element);
m_adjustedElements.clear();
} else {
elementsToReset.reserveInitialCapacity(identifiers.size());
for (auto [nodeID, documentID] : identifiers) {
RefPtr element = dynamicDowncast<Element>(Node::fromIdentifier(nodeID));
if (!element)
continue;
if (element->document().identifier() != documentID)
continue;
if (!m_adjustedElements.remove(*element))
continue;
elementsToReset.add(element.releaseNonNull());
}
}
if (RefPtr loader = document->loader(); loader && !identifiers.isEmpty()) {
m_initialVisibilityAdjustmentSelectors.removeAllMatching([&](auto& selectors) {
auto foundElement = findElementFromSelectors(selectors).element;
return foundElement && elementsToReset.contains(*foundElement);
});
m_visibilityAdjustmentSelectors = m_initialVisibilityAdjustmentSelectors.map([](auto& selectors) -> std::pair<Markable<NodeIdentifier>, TargetedElementSelectors> {
return { std::nullopt, selectors };
});
} else {
// There are no initial adjustments after resetting.
m_visibilityAdjustmentSelectors = { };
m_initialVisibilityAdjustmentSelectors = { };
}
m_additionalAdjustmentCount = 0;
m_didCollectInitialAdjustments = true;
if (elementsToReset.isEmpty())
return false;
bool changed = false;
for (auto& element : elementsToReset) {
Ref adjustedElement = elementToAdjust(element);
auto adjustment = adjustmentToApply(element);
auto currentAdjustment = adjustedElement->visibilityAdjustment();
if (!currentAdjustment.contains(adjustment))
continue;
adjustedElement->setVisibilityAdjustment(currentAdjustment - adjustment);
if (adjustment == VisibilityAdjustment::Subtree)
adjustedElement->invalidateStyleAndRenderersForSubtree();
else
adjustedElement->invalidateStyle();
changed = true;
}
m_viewportSizeForVisibilityAdjustment = frameView->baseLayoutViewportSize();
m_repeatedAdjustmentClientRegion = { };
m_adjustmentClientRegion = { };
if (changed && !m_adjustedElements.isEmptyIgnoringNullReferences()) {
document->updateLayoutIgnorePendingStylesheets();
auto viewportArea = m_viewportSizeForVisibilityAdjustment.area();
for (auto& element : m_adjustedElements) {
if (auto rect = inflatedClientRectForAdjustmentRegionTracking(element, viewportArea))
m_adjustmentClientRegion.unite(*rect);
}
}
if (changed)
dispatchVisibilityAdjustmentStateDidChange();
return changed;
}
uint64_t ElementTargetingController::numberOfVisibilityAdjustmentRects()
{
RefPtr page = m_page.get();
if (!page)
return 0;
if (!page->hasEverSetVisibilityAdjustment() && !m_shouldRecomputeAdjustedElements)
return 0;
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame)
return 0;
RefPtr document = mainFrame->document();
if (!document)
return 0;
document->updateLayoutIgnorePendingStylesheets();
recomputeAdjustedElementsIfNeeded();
Vector<FloatRect> clientRects;
clientRects.reserveInitialCapacity(m_adjustedElements.computeSize());
unsigned numberOfParentedEmptyOrNonRenderedElements = 0;
for (auto& element : m_adjustedElements) {
if (!element.isConnected())
continue;
CheckedPtr renderer = element.renderer();
if (!renderer) {
numberOfParentedEmptyOrNonRenderedElements++;
continue;
}
auto clientRect = computeClientRect(*renderer);
if (clientRect.isEmpty()) {
numberOfParentedEmptyOrNonRenderedElements++;
continue;
}
clientRects.append(clientRect);
}
// Sort by area in descending order so that we don't double-count fully overlapped elements.
std::ranges::sort(clientRects, std::ranges::greater { }, &FloatRect::area);
Region adjustedRegion;
uint64_t numberOfRects = 0;
for (auto rect : clientRects) {
auto enclosingRect = enclosingIntRect(rect);
if (adjustedRegion.contains(enclosingRect))
continue;
numberOfRects++;
adjustedRegion.unite(enclosingRect);
}
return numberOfParentedEmptyOrNonRenderedElements + numberOfRects;
}
void ElementTargetingController::recomputeAdjustedElementsIfNeeded()
{
if (!m_shouldRecomputeAdjustedElements)
return;
m_shouldRecomputeAdjustedElements = false;
RefPtr mainDocument = this->mainDocument();
if (!mainDocument)
return;
RefPtr documentElement = mainDocument->documentElement();
if (!documentElement)
return;
for (Ref element : descendantsOfType<Element>(*documentElement)) {
auto adjustment = element->visibilityAdjustment();
if (adjustment.isEmpty())
continue;
if (adjustment.contains(VisibilityAdjustment::Subtree))
m_adjustedElements.add(element);
if (adjustment.contains(VisibilityAdjustment::AfterPseudo)) {
if (RefPtr afterPseudo = element->afterPseudoElement())
m_adjustedElements.add(*afterPseudo);
}
if (adjustment.contains(VisibilityAdjustment::BeforePseudo)) {
if (RefPtr beforePseudo = element->beforePseudoElement())
m_adjustedElements.add(*beforePseudo);
}
}
}
void ElementTargetingController::cleanUpAdjustmentClientRects()
{
m_recentAdjustmentClientRects = { };
}
void ElementTargetingController::dispatchVisibilityAdjustmentStateDidChange()
{
RefPtr page = m_page.get();
if (!page)
return;
page->forEachDocument([](auto& document) {
document.visibilityAdjustmentStateDidChange();
});
}
RefPtr<Document> ElementTargetingController::mainDocument() const
{
RefPtr page = m_page.get();
if (!page)
return { };
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame)
return { };
return mainFrame->document();
}
void ElementTargetingController::selectorBasedVisibilityAdjustmentTimerFired()
{
applyVisibilityAdjustmentFromSelectors();
}
RefPtr<Image> ElementTargetingController::snapshotIgnoringVisibilityAdjustment(NodeIdentifier nodeID, ScriptExecutionContextIdentifier documentID)
{
RefPtr page = m_page.get();
if (!page)
return { };
RefPtr mainFrame = dynamicDowncast<LocalFrame>(page->mainFrame());
if (!mainFrame)
return { };
RefPtr element = dynamicDowncast<Element>(Node::fromIdentifier(nodeID));
if (!element)
return { };
RefPtr frameView = mainFrame->view();
if (!frameView)
return { };
if (element->document().identifier() != documentID)
return { };
ClearVisibilityAdjustmentForScope clearAdjustmentScope { *element };
element->protectedDocument()->updateLayoutIgnorePendingStylesheets();
CheckedPtr renderer = element->renderer();
if (!renderer)
return { };
if (!renderer->isRenderReplaced() && !renderer->firstChild() && !renderer->style().hasBackgroundImage())
return { };
auto backgroundColor = frameView->baseBackgroundColor();
frameView->setBaseBackgroundColor(Color::transparentBlack);
frameView->setNodeToDraw(element.get());
auto resetPaintingState = makeScopeExit([frameView, backgroundColor]() mutable {
frameView->setBaseBackgroundColor(WTFMove(backgroundColor));
frameView->setNodeToDraw(nullptr);
});
auto snapshotRect = renderer->absoluteBoundingBoxRect();
if (snapshotRect.isEmpty())
return { };
auto buffer = snapshotFrameRect(*mainFrame, snapshotRect, { { }, PixelFormat::BGRA8, DestinationColorSpace::SRGB() });
return BitmapImage::create(ImageBuffer::sinkIntoNativeImage(WTFMove(buffer)));
}
} // namespace WebCore