ContentChangeObserver.cpp   [plain text]


/*
 * Copyright (C) 2019 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 "ContentChangeObserver.h"

#if PLATFORM(IOS_FAMILY)
#include "Chrome.h"
#include "ChromeClient.h"
#include "DOMTimer.h"
#include "Document.h"
#include "FullscreenManager.h"
#include "HTMLIFrameElement.h"
#include "HTMLImageElement.h"
#include "Logging.h"
#include "NodeRenderStyle.h"
#include "Page.h"
#include "Quirks.h"
#include "RenderDescendantIterator.h"
#include "Settings.h"

namespace WebCore {

static const Seconds maximumDelayForTimers { 400_ms };
static const Seconds maximumDelayForTransitions { 300_ms };

#if ENABLE(FULLSCREEN_API)
static bool isHiddenBehindFullscreenElement(const Node& descendantCandidate)
{
    // Fullscreen status is propagated on the ancestor document chain all the way to the top document.
    auto& document = descendantCandidate.document();
    auto* topMostFullScreenElement = document.topDocument().fullscreenManager().fullscreenElement();
    if (!topMostFullScreenElement)
        return false;

    // If the document where the node lives does not have an active fullscreen element, it is a sibling/nephew document -> not a descendant.
    auto* fullscreenElement = document.fullscreenManager().fullscreenElement();
    if (!fullscreenElement)
        return true;
    return !descendantCandidate.isDescendantOf(*fullscreenElement);
}
#endif

bool ContentChangeObserver::isVisuallyHidden(const Node& node)
{
    if (!node.renderStyle())
        return true;

    auto& style = *node.renderStyle();
    if (style.display() == DisplayType::None)
        return true;

    if (style.visibility() == Visibility::Hidden)
        return true;

    if (!style.opacity())
        return true;

    auto width = style.logicalWidth();
    auto height = style.logicalHeight();
    if ((width.isFixed() && !width.value()) || (height.isFixed() && !height.value()))
        return true;

    auto top = style.logicalTop();
    auto left = style.logicalLeft();
    // FIXME: This is trying to check if the element is outside of the viewport. This is incorrect for many reasons.
    if (left.isFixed() && width.isFixed() && -left.value() >= width.value())
        return true;
    if (top.isFixed() && height.isFixed() && -top.value() >= height.value())
        return true;

    // It's a common technique used to position content offscreen.
    if (style.hasOutOfFlowPosition() && left.isFixed() && left.value() <= -999)
        return true;

    // FIXME: Check for other cases like zero height with overflow hidden.
    auto maxHeight = style.maxHeight();
    if (maxHeight.isFixed() && !maxHeight.value())
        return true;

    // Special case opacity, because a descendant with non-zero opacity should still be considered hidden when one of its ancetors has opacity: 0;
    // YouTube.com has this setup with the bottom control bar.
    constexpr static unsigned numberOfAncestorsToCheckForOpacity = 4;
    unsigned i = 0;
    for (auto* parent = node.parentNode(); parent && i < numberOfAncestorsToCheckForOpacity; parent = parent->parentNode(), ++i) {
        if (!parent->renderStyle() || !parent->renderStyle()->opacity())
            return true;
    }

#if ENABLE(FULLSCREEN_API)
    if (isHiddenBehindFullscreenElement(node))
        return true;
#endif
    return false;
}

bool ContentChangeObserver::isConsideredVisible(const Node& node)
{
    if (isVisuallyHidden(node))
        return false;

    auto& style = *node.renderStyle();
    auto width = style.logicalWidth();
    // 1px width or height content is not considered visible.
    if (width.isFixed() && width.value() <= 1)
        return false;

    auto height = style.logicalHeight();
    if (height.isFixed() && height.value() <= 1)
        return false;

    return true;
}

enum class ElementHadRenderer { No, Yes };
static bool isConsideredClickable(const Element& candidateElement, ElementHadRenderer hadRenderer)
{
    auto& element = const_cast<Element&>(candidateElement);
    if (element.isInUserAgentShadowTree())
        return false;

    if (is<HTMLIFrameElement>(element))
        return true;

    if (is<HTMLImageElement>(element)) {
        // This is required to avoid HTMLImageElement's touch callout override logic. See rdar://problem/48937767.
        return element.Element::willRespondToMouseClickEvents();
    }

    bool hasRenderer = element.renderer();
    auto willRespondToMouseClickEvents = element.willRespondToMouseClickEvents();
    if (willRespondToMouseClickEvents || !hasRenderer || hadRenderer == ElementHadRenderer::No)
        return willRespondToMouseClickEvents;

    // In case when the content already had renderers it's not sufficient to check the candidate element only since it might just be the container for the clickable content.  
    for (auto& descendant : descendantsOfType<RenderElement>(*element.renderer())) {
        if (!descendant.element())
            continue;
        if (descendant.element()->willRespondToMouseClickEvents())
            return true;
    }
    return false;
}

ContentChangeObserver::ContentChangeObserver(Document& document)
    : m_document(document)
    , m_contentObservationTimer([this] { completeDurationBasedContentObservation(); })
{
}

static void willNotProceedWithClick(Frame& mainFrame)
{
    for (auto* frame = &mainFrame; frame; frame = frame->tree().traverseNext()) {
        if (auto* document = frame->document())
            document->contentChangeObserver().willNotProceedWithClick();
    }
}

void ContentChangeObserver::didCancelPotentialTap(Frame& mainFrame)
{
    LOG(ContentObservation, "didCancelPotentialTap: cancel ongoing content change observing.");
    WebCore::willNotProceedWithClick(mainFrame);
}

void ContentChangeObserver::didRecognizeLongPress(Frame& mainFrame)
{
    LOG(ContentObservation, "didRecognizeLongPress: cancel ongoing content change observing.");
    WebCore::willNotProceedWithClick(mainFrame);
}

void ContentChangeObserver::didPreventDefaultForEvent(Frame& mainFrame)
{
    LOG(ContentObservation, "didPreventDefaultForEvent: cancel ongoing content change observing.");
    WebCore::willNotProceedWithClick(mainFrame);
}

void ContentChangeObserver::startContentObservationForDuration(Seconds duration)
{
    if (!m_document.settings().contentChangeObserverEnabled())
        return;
    ASSERT(!hasVisibleChangeState());
    LOG_WITH_STREAM(ContentObservation, stream << "startContentObservationForDuration: start observing the content for " << duration.milliseconds() << "ms");
    adjustObservedState(Event::StartedFixedObservationTimeWindow);
    m_contentObservationTimer.startOneShot(duration);
}

void ContentChangeObserver::completeDurationBasedContentObservation()
{
    LOG_WITH_STREAM(ContentObservation, stream << "completeDurationBasedContentObservation: complete duration based content observing ");
    adjustObservedState(Event::EndedFixedObservationTimeWindow);
}

void ContentChangeObserver::didAddTransition(const Element& element, const Animation& transition)
{
    if (!m_document.settings().contentChangeObserverEnabled())
        return;
    if (hasVisibleChangeState())
        return;
    if (!isObservingContentChanges())
        return;
    if (!isObservingTransitions())
        return;
    if (!transition.isDurationSet() || !transition.isPropertySet())
        return;
    if (!isObservedPropertyForTransition(transition.property()))
        return;
    auto transitionEnd = Seconds { transition.duration() + std::max<double>(0, transition.isDelaySet() ? transition.delay() : 0) };
    if (transitionEnd > maximumDelayForTransitions)
        return;
    if (!isVisuallyHidden(element))
        return;
    // In case of multiple transitions, the first tranistion wins (and it has to produce a visible content change in order to show up as hover).
    if (m_elementsWithTransition.contains(&element))
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "didAddTransition: transition created on " << &element << " (" << transitionEnd.milliseconds() << "ms).");

    m_elementsWithTransition.add(&element);
    adjustObservedState(Event::AddedTransition);
}

void ContentChangeObserver::didFinishTransition(const Element& element, CSSPropertyID propertyID)
{
    if (!isObservedPropertyForTransition(propertyID))
        return;
    if (!m_elementsWithTransition.take(&element))
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "didFinishTransition: transition finished (" << &element << ").");

    // isConsideredClickable may trigger style update through Node::computeEditability. Let's adjust the state in the next runloop.
    callOnMainThread([weakThis = makeWeakPtr(*this), targetElement = makeWeakPtr(element)] {
        if (!weakThis || !targetElement)
            return;
        if (isVisuallyHidden(*targetElement)) {
            weakThis->adjustObservedState(Event::EndedTransitionButFinalStyleIsNotDefiniteYet);
            return;
        }
        if (isConsideredClickable(*targetElement, ElementHadRenderer::Yes))
            weakThis->elementDidBecomeVisible(*targetElement);
        weakThis->adjustObservedState(Event::CompletedTransition);
    });
}

void ContentChangeObserver::didRemoveTransition(const Element& element, CSSPropertyID propertyID)
{
    if (!isObservedPropertyForTransition(propertyID))
        return;
    if (!m_elementsWithTransition.take(&element))
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "didRemoveTransition: transition got interrupted (" << &element << ").");

    adjustObservedState(Event::CanceledTransition);
}

void ContentChangeObserver::didInstallDOMTimer(const DOMTimer& timer, Seconds timeout, bool singleShot)
{
    if (!m_document.settings().contentChangeObserverEnabled())
        return;
    if (!isObservingContentChanges())
        return;
    if (!isObservingDOMTimerScheduling())
        return;
    if (hasVisibleChangeState())
        return;
    if (m_document.activeDOMObjectsAreSuspended())
        return;
    if (timeout > maximumDelayForTimers || !singleShot)
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "didInstallDOMTimer: register this timer: (" << &timer << ") and observe when it fires.");

    registerDOMTimer(timer);
    adjustObservedState(Event::InstalledDOMTimer);
}

void ContentChangeObserver::didRemoveDOMTimer(const DOMTimer& timer)
{
    if (!containsObservedDOMTimer(timer))
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "removeDOMTimer: remove registered timer (" << &timer << ")");

    unregisterDOMTimer(timer);
    adjustObservedState(Event::RemovedDOMTimer);
}

void ContentChangeObserver::willNotProceedWithClick()
{
    LOG(ContentObservation, "willNotProceedWithClick: click will not happen.");
    adjustObservedState(Event::WillNotProceedWithClick);
}

void ContentChangeObserver::domTimerExecuteDidStart(const DOMTimer& timer)
{
    if (!containsObservedDOMTimer(timer))
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "startObservingDOMTimerExecute: start observing (" << &timer << ") timer callback.");

    m_observedDomTimerIsBeingExecuted = true;
    adjustObservedState(Event::StartedDOMTimerExecution);
}

void ContentChangeObserver::domTimerExecuteDidFinish(const DOMTimer& timer)
{
    if (!m_observedDomTimerIsBeingExecuted)
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "stopObservingDOMTimerExecute: stop observing (" << &timer << ") timer callback.");

    m_observedDomTimerIsBeingExecuted = false;
    unregisterDOMTimer(timer);
    adjustObservedState(Event::EndedDOMTimerExecution);
}

void ContentChangeObserver::styleRecalcDidStart()
{
    if (!isWaitingForStyleRecalc())
        return;
    LOG(ContentObservation, "startObservingStyleRecalc: start observing style recalc.");

    m_isInObservedStyleRecalc = true;
    adjustObservedState(Event::StartedStyleRecalc);
}

void ContentChangeObserver::styleRecalcDidFinish()
{
    if (!m_isInObservedStyleRecalc)
        return;
    LOG(ContentObservation, "stopObservingStyleRecalc: stop observing style recalc");

    m_isInObservedStyleRecalc = false;
    adjustObservedState(Event::EndedStyleRecalc);
}

void ContentChangeObserver::stopObservingPendingActivities()
{
    setShouldObserveNextStyleRecalc(false);
    setShouldObserveDOMTimerSchedulingAndTransitions(false);
    clearObservedDOMTimers();
    clearObservedTransitions();
}

void ContentChangeObserver::stopContentObservation()
{
    reset();
}

void ContentChangeObserver::reset()
{
    stopObservingPendingActivities();
    setHasNoChangeState();

    setTouchEventIsBeingDispatched(false);
    setIsBetweenTouchEndAndMouseMoved(false);
    setMouseMovedEventIsBeingDispatched(false);

    m_isInObservedStyleRecalc = false;
    m_observedDomTimerIsBeingExecuted = false;

    m_visibilityCandidateList.clear();

    m_contentObservationTimer.stop();
    m_elementsWithDestroyedVisibleRenderer.clear();
    resetHiddenTouchTarget();
}

void ContentChangeObserver::didSuspendActiveDOMObjects()
{
    LOG(ContentObservation, "didSuspendActiveDOMObjects");
    reset();
}

void ContentChangeObserver::willDetachPage()
{
    LOG(ContentObservation, "willDetachPage");
    reset();
}

void ContentChangeObserver::rendererWillBeDestroyed(const Element& element)
{ 
    if (!m_document.settings().contentChangeObserverEnabled())
        return;
    if (!isObservingContentChanges())
        return;
    LOG_WITH_STREAM(ContentObservation, stream << "rendererWillBeDestroyed element: " << &element);

    if (!isVisuallyHidden(element))
        m_elementsWithDestroyedVisibleRenderer.add(&element);
    elementDidBecomeHidden(element);
}

void ContentChangeObserver::elementDidBecomeVisible(const Element& element)
{
    LOG_WITH_STREAM(ContentObservation, stream << "elementDidBecomeVisible: element went from hidden to visible: " << &element);
    m_visibilityCandidateList.add(element);
    adjustObservedState(Event::ElementDidBecomeVisible);
}

void ContentChangeObserver::elementDidBecomeHidden(const Element& element)
{
    LOG_WITH_STREAM(ContentObservation, stream << "elementDidBecomeHidden: element went from visible to hidden: " << &element);
    // Candidate element is no longer visible.
    if (!m_visibilityCandidateList.remove(element))
        return;
    ASSERT(hasVisibleChangeState());
    if (m_visibilityCandidateList.computesEmpty())
        setHasIndeterminateState();
}

void ContentChangeObserver::touchEventDidStart(PlatformEvent::Type eventType)
{
#if ENABLE(TOUCH_EVENTS)
    if (!m_document.settings().contentChangeObserverEnabled() || m_document.quirks().shouldDisableContentChangeObserverTouchEventAdjustment())
        return;
    if (eventType != PlatformEvent::Type::TouchStart)
        return;
    LOG(ContentObservation, "touchEventDidStart: touch start event started.");
    setTouchEventIsBeingDispatched(true);
    adjustObservedState(Event::StartedTouchStartEventDispatching);
#else
    UNUSED_PARAM(eventType);
#endif
}

void ContentChangeObserver::touchEventDidFinish()
{
#if ENABLE(TOUCH_EVENTS)
    if (!isTouchEventBeingDispatched())
        return;
    ASSERT(m_document.settings().contentChangeObserverEnabled());
    LOG(ContentObservation, "touchEventDidFinish: touch start event finished.");
    setTouchEventIsBeingDispatched(false);
    adjustObservedState(Event::EndedTouchStartEventDispatching);
#endif
}

void ContentChangeObserver::mouseMovedDidStart()
{
    if (!m_document.settings().contentChangeObserverEnabled())
        return;
    LOG(ContentObservation, "mouseMovedDidStart: mouseMoved started.");
    setMouseMovedEventIsBeingDispatched(true);
    adjustObservedState(Event::StartedMouseMovedEventDispatching);
}

void ContentChangeObserver::mouseMovedDidFinish()
{
    if (!isMouseMovedEventBeingDispatched())
        return;
    ASSERT(m_document.settings().contentChangeObserverEnabled());
    LOG(ContentObservation, "mouseMovedDidFinish: mouseMoved finished.");
    adjustObservedState(Event::EndedMouseMovedEventDispatching);
    setMouseMovedEventIsBeingDispatched(false);
}

void ContentChangeObserver::willNotProceedWithFixedObservationTimeWindow()
{
    ASSERT(!isMouseMovedEventBeingDispatched());
    adjustObservedState(Event::WillNotProceedWithFixedObservationTimeWindow);
}

void ContentChangeObserver::setShouldObserveNextStyleRecalc(bool shouldObserve)
{
    if (shouldObserve)
        LOG(ContentObservation, "Wait until next style recalc fires.");
    m_isWaitingForStyleRecalc = shouldObserve;
}

void ContentChangeObserver::adjustObservedState(Event event)
{
    auto resetToStartObserving = [&] {
        setHasNoChangeState();
        clearObservedDOMTimers();
        clearObservedTransitions();
        setIsBetweenTouchEndAndMouseMoved(false);
        setShouldObserveNextStyleRecalc(false);
        setShouldObserveDOMTimerSchedulingAndTransitions(false);
        ASSERT(!m_isInObservedStyleRecalc);
        ASSERT(!m_observedDomTimerIsBeingExecuted);
    };

    auto notifyClientIfNeeded = [&] {
        if (isTouchEventBeingDispatched()) {
            LOG(ContentObservation, "notifyClientIfNeeded: Touch event is being dispatched. No need to notify the client.");
            return;
        }
        if (isBetweenTouchEndAndMouseMoved()) {
            LOG(ContentObservation, "notifyClientIfNeeded: Not reached mouseMoved yet. No need to notify the client.");
            return;
        }
        if (isMouseMovedEventBeingDispatched()) {
            LOG(ContentObservation, "notifyClientIfNeeded: in mouseMoved call. No need to notify the client.");
            return;
        }
        if (isObservationTimeWindowActive()) {
            LOG(ContentObservation, "notifyClientIfNeeded: Inside the fixed window observation. No need to notify the client.");
            return;
        }

        // The fixed observation window (which is the final step in content observation) is closed and now we check if are still waiting for timers or animations to finish.
        if (hasPendingActivity()) {
            LOG(ContentObservation, "notifyClientIfNeeded: We are still waiting on some events.");
            return;
        }

        // First demote to "no change" because we've got no pending activity anymore.
        if (observedContentChange() == WKContentIndeterminateChange)
            setHasNoChangeState();

        LOG_WITH_STREAM(ContentObservation, stream << "notifyClientIfNeeded: sending observedContentChange ->" << observedContentChange());
        ASSERT(m_document.page());
        ASSERT(m_document.frame());
        m_document.page()->chrome().client().didFinishContentChangeObserving(*m_document.frame(), observedContentChange());
        stopContentObservation();
    };

    // These user initiated events trigger content observation (touchStart and mouseMove). 
    {
        if (event == Event::StartedTouchStartEventDispatching) {
            resetToStartObserving();
            setShouldObserveDOMTimerSchedulingAndTransitions(true);
            return;
        }
        if (event == Event::EndedTouchStartEventDispatching) {
            setShouldObserveDOMTimerSchedulingAndTransitions(false);
            setIsBetweenTouchEndAndMouseMoved(true);
            return;
        }
        if (event == Event::StartedMouseMovedEventDispatching) {
            ASSERT(!m_document.hasPendingStyleRecalc());
            if (!isBetweenTouchEndAndMouseMoved())
                resetToStartObserving();
            setIsBetweenTouchEndAndMouseMoved(false);
            setShouldObserveDOMTimerSchedulingAndTransitions(!hasVisibleChangeState());
            return;
        }
        if (event == Event::EndedMouseMovedEventDispatching) {
            setShouldObserveDOMTimerSchedulingAndTransitions(false);
            return;
        }
    }
    // Fixed window observation starts soon after mouseMove when we don't have a definite answer to whether we should proceed with hover or click.
    {
        if (event == Event::StartedFixedObservationTimeWindow) {
            ASSERT(!hasVisibleChangeState());
            setHasIndeterminateState();
            return;
        }
        if (event == Event::EndedFixedObservationTimeWindow) {
            notifyClientIfNeeded();
            return;
        }
        if (event == Event::WillNotProceedWithFixedObservationTimeWindow) {
            notifyClientIfNeeded();
            return;
        }
    }
    // These events (DOM timer, transition and style recalc) could trigger style changes that are candidates to visibility checking.
    {
        if (event == Event::InstalledDOMTimer || event == Event::AddedTransition) {
            ASSERT(!hasVisibleChangeState());
            setHasIndeterminateState();
            return;
        }
        if (event == Event::RemovedDOMTimer || event == Event::CanceledTransition) {
            notifyClientIfNeeded();
            return;
        }
        if (event == Event::StartedDOMTimerExecution) {
            ASSERT(isObservationTimeWindowActive() || observedContentChange() == WKContentIndeterminateChange);
            return;
        }
        if (event == Event::EndedDOMTimerExecution) {
            if (m_document.hasPendingStyleRecalc()) {
                setShouldObserveNextStyleRecalc(true);
                return;
            }
            notifyClientIfNeeded();
            return;
        }
        if (event == Event::EndedTransitionButFinalStyleIsNotDefiniteYet) {
            // onAnimationEnd can be called while in the middle of resolving the document (synchronously) or
            // asynchronously right before the style update is issued. It also means we don't know whether this animation ends up producing visible content yet. 
            if (m_document.inStyleRecalc()) {
                // We need to start observing this style change synchronously.
                m_isInObservedStyleRecalc = true;
                return;
            }
            setShouldObserveNextStyleRecalc(true);
            return;
        }
        if (event == Event::CompletedTransition) {
            if (m_document.inStyleRecalc()) {
                m_isInObservedStyleRecalc = true;
                return;
            }
            notifyClientIfNeeded();
            return;
        }
        if (event == Event::StartedStyleRecalc) {
            setShouldObserveNextStyleRecalc(false);
            ASSERT(isObservationTimeWindowActive() || observedContentChange() == WKContentIndeterminateChange);
            return;
        }
        if (event == Event::EndedStyleRecalc) {
            notifyClientIfNeeded();
            return;
        }
    }
    // Either the page decided to call preventDefault on the touch action or the tap gesture evolved to some other gesture (long press, double tap). 
    if (event == Event::WillNotProceedWithClick) {
        stopContentObservation();
        return;
    }
    // The page produced an visible change on an actionable content.
    if (event == Event::ElementDidBecomeVisible) {
        setHasVisibleChangeState();
        // Stop pending activities. We don't need to observe them anymore.
        stopObservingPendingActivities();
        return;
    }
}

bool ContentChangeObserver::shouldObserveVisibilityChangeForElement(const Element& element)
{
    return isObservingContentChanges() && !visibleRendererWasDestroyed(element);
}

ContentChangeObserver::StyleChangeScope::StyleChangeScope(Document& document, const Element& element)
    : m_contentChangeObserver(document.contentChangeObserver())
    , m_element(element)
    , m_hadRenderer(element.renderer())
{
    if (m_contentChangeObserver.shouldObserveVisibilityChangeForElement(element))
        m_wasHidden = isVisuallyHidden(m_element);
}

ContentChangeObserver::StyleChangeScope::~StyleChangeScope()
{
    // Do we track this element?
    if (!m_wasHidden.hasValue())
        return;

    if (!isConsideredClickable(m_element, m_hadRenderer ? ElementHadRenderer::Yes : ElementHadRenderer::No))
        return;

    auto wasVisible = !m_wasHidden.value();
    auto isVisible = isConsideredVisible(m_element);
    if (!wasVisible && isVisible)
        m_contentChangeObserver.elementDidBecomeVisible(m_element);
    else if (wasVisible && !isVisible)
        m_contentChangeObserver.elementDidBecomeHidden(m_element);
}

#if ENABLE(TOUCH_EVENTS)
ContentChangeObserver::TouchEventScope::TouchEventScope(Document& document, PlatformEvent::Type eventType)
    : m_contentChangeObserver(document.contentChangeObserver())
{
    m_contentChangeObserver.touchEventDidStart(eventType);
}

ContentChangeObserver::TouchEventScope::~TouchEventScope()
{
    m_contentChangeObserver.touchEventDidFinish();
}
#endif

ContentChangeObserver::MouseMovedScope::MouseMovedScope(Document& document)
    : m_contentChangeObserver(document.contentChangeObserver())
{
    m_contentChangeObserver.mouseMovedDidStart();
}

ContentChangeObserver::MouseMovedScope::~MouseMovedScope()
{
    m_contentChangeObserver.mouseMovedDidFinish();
    m_contentChangeObserver.resetHiddenTouchTarget();
}

ContentChangeObserver::StyleRecalcScope::StyleRecalcScope(Document& document)
    : m_contentChangeObserver(document.contentChangeObserver())
{
    m_contentChangeObserver.styleRecalcDidStart();
}

ContentChangeObserver::StyleRecalcScope::~StyleRecalcScope()
{
    m_contentChangeObserver.styleRecalcDidFinish();
}

ContentChangeObserver::DOMTimerScope::DOMTimerScope(Document* document, const DOMTimer& domTimer)
    : m_contentChangeObserver(document ? &document->contentChangeObserver() : nullptr)
    , m_domTimer(domTimer)
{
    if (m_contentChangeObserver)
        m_contentChangeObserver->domTimerExecuteDidStart(m_domTimer);
}

ContentChangeObserver::DOMTimerScope::~DOMTimerScope()
{
    if (m_contentChangeObserver)
        m_contentChangeObserver->domTimerExecuteDidFinish(m_domTimer);
}

}

#endif // PLATFORM(IOS_FAMILY)