blob: fa774a8991243bd8cbb58c679aa92390df76b018 [file] [log] [blame]
/*
* 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)