blob: ffd7a74b9a83a535d07707cd3bbd30819aa31854 [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 "HTMLIFrameElement.h"
#include "HTMLImageElement.h"
#include "Logging.h"
#include "NodeRenderStyle.h"
#include "Page.h"
#include "RenderDescendantIterator.h"
#include "Settings.h"
namespace WebCore {
static const Seconds maximumDelayForTimers { 400_ms };
static const Seconds maximumDelayForTransitions { 300_ms };
static bool isConsideredHidden(const Element& element)
{
if (!element.renderStyle())
return true;
auto& style = *element.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;
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::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 (!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 (!isConsideredHidden(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 << ").");
adjustObservedState(isConsideredHidden(element) ? Event::EndedTransition : 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 (m_document.activeDOMObjectsAreSuspended())
return;
if (timeout > maximumDelayForTimers || !singleShot)
return;
if (!isObservingDOMTimerScheduling())
return;
if (hasVisibleChangeState())
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::renderTreeUpdateDidStart()
{
if (!m_document.settings().contentChangeObserverEnabled())
return;
if (!isObservingContentChanges())
return;
LOG(ContentObservation, "renderTreeUpdateDidStart: RenderTree update started");
m_isInObservedRenderTreeUpdate = true;
m_elementsWithDestroyedVisibleRenderer.clear();
}
void ContentChangeObserver::renderTreeUpdateDidFinish()
{
if (!m_isInObservedRenderTreeUpdate)
return;
LOG(ContentObservation, "renderTreeUpdateDidStart: RenderTree update finished");
m_isInObservedRenderTreeUpdate = false;
m_elementsWithDestroyedVisibleRenderer.clear();
}
void ContentChangeObserver::stopObservingPendingActivities()
{
setShouldObserveNextStyleRecalc(false);
setShouldObserveDOMTimerScheduling(false);
setShouldObserveTransitions(false);
clearObservedDOMTimers();
clearObservedTransitions();
}
void ContentChangeObserver::reset()
{
stopObservingPendingActivities();
setHasNoChangeState();
setIsBetweenTouchEndAndMouseMoved(false);
m_touchEventIsBeingDispatched = false;
m_isInObservedStyleRecalc = false;
m_isInObservedRenderTreeUpdate = false;
m_observedDomTimerIsBeingExecuted = false;
m_mouseMovedEventIsBeingDispatched = false;
m_contentObservationTimer.stop();
m_elementsWithDestroyedVisibleRenderer.clear();
}
void ContentChangeObserver::didSuspendActiveDOMObjects()
{
LOG(ContentObservation, "didSuspendActiveDOMObjects");
reset();
}
void ContentChangeObserver::willDetachPage()
{
LOG(ContentObservation, "willDetachPage");
reset();
}
void ContentChangeObserver::willDestroyRenderer(const Element& element)
{
if (!m_document.settings().contentChangeObserverEnabled())
return;
if (!m_isInObservedRenderTreeUpdate)
return;
if (hasVisibleChangeState())
return;
LOG_WITH_STREAM(ContentObservation, stream << "willDestroyRenderer element: " << &element);
if (!isConsideredHidden(element))
m_elementsWithDestroyedVisibleRenderer.add(&element);
}
void ContentChangeObserver::contentVisibilityDidChange()
{
LOG(ContentObservation, "contentVisibilityDidChange: visible content change did happen.");
adjustObservedState(Event::ContentVisibilityChanged);
}
void ContentChangeObserver::touchEventDidStart(PlatformEvent::Type eventType)
{
#if ENABLE(TOUCH_EVENTS)
if (!m_document.settings().contentChangeObserverEnabled())
return;
if (eventType != PlatformEvent::Type::TouchStart)
return;
LOG(ContentObservation, "touchEventDidStart: touch start event started.");
m_touchEventIsBeingDispatched = true;
adjustObservedState(Event::StartedTouchStartEventDispatching);
#else
UNUSED_PARAM(eventType);
#endif
}
void ContentChangeObserver::touchEventDidFinish()
{
#if ENABLE(TOUCH_EVENTS)
if (!m_touchEventIsBeingDispatched)
return;
ASSERT(m_document.settings().contentChangeObserverEnabled());
LOG(ContentObservation, "touchEventDidFinish: touch start event finished.");
m_touchEventIsBeingDispatched = false;
adjustObservedState(Event::EndedTouchStartEventDispatching);
#endif
}
void ContentChangeObserver::mouseMovedDidStart()
{
if (!m_document.settings().contentChangeObserverEnabled())
return;
LOG(ContentObservation, "mouseMovedDidStart: mouseMoved started.");
m_mouseMovedEventIsBeingDispatched = true;
adjustObservedState(Event::StartedMouseMovedEventDispatching);
}
void ContentChangeObserver::mouseMovedDidFinish()
{
if (!m_mouseMovedEventIsBeingDispatched)
return;
ASSERT(m_document.settings().contentChangeObserverEnabled());
LOG(ContentObservation, "mouseMovedDidFinish: mouseMoved finished.");
adjustObservedState(Event::EndedMouseMovedEventDispatching);
m_mouseMovedEventIsBeingDispatched = false;
}
void ContentChangeObserver::setShouldObserveNextStyleRecalc(bool shouldObserve)
{
if (shouldObserve)
LOG(ContentObservation, "Wait until next style recalc fires.");
m_isWaitingForStyleRecalc = shouldObserve;
}
bool ContentChangeObserver::hasDeterminateState() const
{
if (hasVisibleChangeState())
return true;
return observedContentChange() == WKContentNoChange && !hasPendingActivity();
}
void ContentChangeObserver::adjustObservedState(Event event)
{
auto resetToStartObserving = [&] {
setHasNoChangeState();
clearObservedDOMTimers();
clearObservedTransitions();
setIsBetweenTouchEndAndMouseMoved(false);
setShouldObserveNextStyleRecalc(false);
setShouldObserveDOMTimerScheduling(false);
setShouldObserveTransitions(false);
ASSERT(!m_isInObservedStyleRecalc);
ASSERT(!m_observedDomTimerIsBeingExecuted);
};
auto adjustStateAndNotifyContentChangeIfNeeded = [&] {
// Demote to "no change" when there's no pending activity anymore.
if (observedContentChange() == WKContentIndeterminateChange && !hasPendingActivity())
setHasNoChangeState();
// Do not notify the client unless we couldn't make the decision synchronously.
if (m_mouseMovedEventIsBeingDispatched) {
LOG(ContentObservation, "adjustStateAndNotifyContentChangeIfNeeded: in mouseMoved call. No need to notify the client.");
return;
}
if (isBetweenTouchEndAndMouseMoved()) {
LOG(ContentObservation, "adjustStateAndNotifyContentChangeIfNeeded: Not reached mouseMoved yet. No need to notify the client.");
return;
}
if (!hasDeterminateState()) {
LOG(ContentObservation, "adjustStateAndNotifyContentChangeIfNeeded: not in a determined state yet.");
return;
}
LOG_WITH_STREAM(ContentObservation, stream << "adjustStateAndNotifyContentChangeIfNeeded: sending observedContentChange ->" << observedContentChange());
ASSERT(m_document.page());
ASSERT(m_document.frame());
m_document.page()->chrome().client().observedContentChange(*m_document.frame());
};
switch (event) {
case Event::StartedTouchStartEventDispatching:
resetToStartObserving();
setShouldObserveDOMTimerScheduling(true);
setShouldObserveTransitions(true);
break;
case Event::EndedTouchStartEventDispatching:
setShouldObserveDOMTimerScheduling(false);
setShouldObserveTransitions(false);
setIsBetweenTouchEndAndMouseMoved(true);
break;
case Event::WillNotProceedWithClick:
reset();
break;
case Event::StartedMouseMovedEventDispatching:
ASSERT(!m_document.hasPendingStyleRecalc());
if (!isBetweenTouchEndAndMouseMoved())
resetToStartObserving();
setIsBetweenTouchEndAndMouseMoved(false);
setShouldObserveDOMTimerScheduling(!hasVisibleChangeState());
setShouldObserveTransitions(!hasVisibleChangeState());
break;
case Event::EndedMouseMovedEventDispatching:
setShouldObserveDOMTimerScheduling(false);
setShouldObserveTransitions(false);
break;
case Event::StartedStyleRecalc:
setShouldObserveNextStyleRecalc(false);
FALLTHROUGH;
case Event::StartedDOMTimerExecution:
ASSERT(isObservationTimeWindowActive() || observedContentChange() == WKContentIndeterminateChange);
break;
case Event::InstalledDOMTimer:
case Event::StartedFixedObservationTimeWindow:
case Event::AddedTransition:
ASSERT(!hasVisibleChangeState());
setHasIndeterminateState();
break;
case Event::EndedDOMTimerExecution:
setShouldObserveNextStyleRecalc(m_document.hasPendingStyleRecalc());
FALLTHROUGH;
case Event::EndedStyleRecalc:
case Event::RemovedDOMTimer:
case Event::CanceledTransition:
if (!isObservationTimeWindowActive())
adjustStateAndNotifyContentChangeIfNeeded();
break;
case Event::EndedTransition:
// 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;
} else
setShouldObserveNextStyleRecalc(true);
break;
case Event::CompletedTransition:
// Set visibility flag on and report visible change synchronously or asynchronously depending whether we are in the middle of style recalc.
contentVisibilityDidChange();
if (m_document.inStyleRecalc())
m_isInObservedStyleRecalc = true;
else if (!isObservationTimeWindowActive())
adjustStateAndNotifyContentChangeIfNeeded();
break;
case Event::EndedFixedObservationTimeWindow:
adjustStateAndNotifyContentChangeIfNeeded();
break;
case Event::ContentVisibilityChanged:
setHasVisibleChangeState();
// Stop pending activities. We don't need to observe them anymore.
stopObservingPendingActivities();
break;
}
}
bool ContentChangeObserver::shouldObserveVisibilityChangeForElement(const Element& element)
{
return isObservingContentChanges() && !hasVisibleChangeState() && !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 = isConsideredHidden(m_element);
}
ContentChangeObserver::StyleChangeScope::~StyleChangeScope()
{
auto changedFromHiddenToVisible = [&] {
return m_wasHidden && !isConsideredHidden(m_element);
};
if (changedFromHiddenToVisible() && isConsideredClickable())
m_contentChangeObserver.contentVisibilityDidChange();
}
bool ContentChangeObserver::StyleChangeScope::isConsideredClickable() const
{
if (m_element.isInUserAgentShadowTree())
return false;
auto& element = const_cast<Element&>(m_element);
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();
}
auto willRespondToMouseClickEvents = element.willRespondToMouseClickEvents();
if (!m_hadRenderer || willRespondToMouseClickEvents)
return willRespondToMouseClickEvents;
// In case when the visible content already had renderers it's not sufficient to check the "newly visible" element only since it might just be the container for the clickable content.
ASSERT(m_element.renderer());
for (auto& descendant : descendantsOfType<RenderElement>(*element.renderer())) {
if (!descendant.element())
continue;
if (descendant.element()->willRespondToMouseClickEvents())
return true;
}
return false;
}
#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();
}
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);
}
ContentChangeObserver::RenderTreeUpdateScope::RenderTreeUpdateScope(Document& document)
: m_contentChangeObserver(document.contentChangeObserver())
{
m_contentChangeObserver.renderTreeUpdateDidStart();
}
ContentChangeObserver::RenderTreeUpdateScope::~RenderTreeUpdateScope()
{
m_contentChangeObserver.renderTreeUpdateDidFinish();
}
}
#endif // PLATFORM(IOS_FAMILY)