blob: a93ddd3e923391f99360bd09be5741a3ff9bbcf3 [file] [log] [blame]
/*
* Copyright (C) 2011-2017 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.
*/
#import "config.h"
#import "ScrollingEffectsController.h"
#import "Logging.h"
#import "PlatformWheelEvent.h"
#import "ScrollAnimationRubberBand.h"
#import "ScrollExtents.h"
#import "ScrollableArea.h"
#import "WheelEventDeltaFilter.h"
#import <pal/spi/mac/NSScrollViewSPI.h>
#import <sys/sysctl.h>
#import <sys/time.h>
#import <wtf/text/TextStream.h>
#if PLATFORM(MAC)
namespace WebCore {
static const Seconds scrollVelocityZeroingTimeout = 100_ms;
static const float rubberbandDirectionLockStretchRatio = 1;
static const float rubberbandMinimumRequiredDeltaBeforeStretch = 10;
static float elasticDeltaForReboundDelta(float delta)
{
return _NSElasticDeltaForReboundDelta(delta);
}
static float reboundDeltaForElasticDelta(float delta)
{
return _NSReboundDeltaForElasticDelta(delta);
}
static float scrollWheelMultiplier()
{
static float multiplier = -1;
if (multiplier < 0) {
multiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"NSScrollWheelMultiplier"];
if (multiplier <= 0)
multiplier = 1;
}
return multiplier;
}
ScrollingEffectsController::~ScrollingEffectsController()
{
ASSERT(m_timersWereStopped);
}
void ScrollingEffectsController::stopAllTimers()
{
if (m_statelessSnapTransitionTimer)
m_statelessSnapTransitionTimer->stop();
#if ASSERT_ENABLED
m_timersWereStopped = true;
#endif
}
static ScrollEventAxis dominantAxisFavoringVertical(FloatSize delta)
{
if (fabsf(delta.height()) >= fabsf(delta.width()))
return ScrollEventAxis::Vertical;
return ScrollEventAxis::Horizontal;
}
static FloatSize deltaAlignedToAxis(FloatSize delta, ScrollEventAxis axis)
{
switch (axis) {
case ScrollEventAxis::Horizontal: return FloatSize { delta.width(), 0 };
case ScrollEventAxis::Vertical: return FloatSize { 0, delta.height() };
}
return { };
}
static FloatSize deltaAlignedToDominantAxis(FloatSize delta)
{
auto dominantAxis = dominantAxisFavoringVertical(delta);
return deltaAlignedToAxis(delta, dominantAxis);
}
static std::optional<BoxSide> affectedSideOnDominantAxis(FloatSize delta)
{
auto dominantAxis = dominantAxisFavoringVertical(delta);
return ScrollableArea::targetSideForScrollDelta(delta, dominantAxis);
}
#if !LOG_DISABLED
static const char* phaseToString(PlatformWheelEventPhase phase)
{
switch (phase) {
case PlatformWheelEventPhase::None: return "none";
case PlatformWheelEventPhase::Began: return "began";
case PlatformWheelEventPhase::Stationary: return "stationary";
case PlatformWheelEventPhase::Changed: return "changed";
case PlatformWheelEventPhase::Ended: return "ended";
case PlatformWheelEventPhase::Cancelled: return "cancelled";
case PlatformWheelEventPhase::MayBegin: return "mayBegin";
}
return "";
}
#endif
static FloatSize adjustedVelocity(FloatSize velocity)
{
auto applyCurve = ^(float originalValue) {
if (!originalValue)
return originalValue;
float value = fabs(originalValue);
float powerLow = 6.7 * pow(value, -.166);
float powerHigh = 36.3 * pow(value, -.392);
const float transitionVelocity = 2000;
auto interpolate = ^(float v0, float v1, float t) {
return (1 - t) * v0 + t * v1;
};
float multiplier = interpolate(powerLow, powerHigh, std::min(value, transitionVelocity) / transitionVelocity);
return copysign(value * multiplier, originalValue);
};
return { applyCurve(velocity.width()), applyCurve(velocity.height()) };
}
bool ScrollingEffectsController::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
{
if (processWheelEventForScrollSnap(wheelEvent))
return true;
if (wheelEvent.phase() == PlatformWheelEventPhase::MayBegin || wheelEvent.phase() == PlatformWheelEventPhase::Cancelled) {
if (momentumScrollingAnimatorEnabled()) {
LOG(ScrollAnimations, "Event (%s, %s): stopping animated scroll", phaseToString(wheelEvent.phase()), phaseToString(wheelEvent.momentumPhase()));
stopAnimatedScroll();
}
return true;
}
if (wheelEvent.phase() == PlatformWheelEventPhase::Began) {
if (momentumScrollingAnimatorEnabled()) {
LOG(ScrollAnimations, "Event (%s, %s): stopping animated scroll", phaseToString(wheelEvent.phase()), phaseToString(wheelEvent.momentumPhase()));
stopAnimatedScroll();
}
// FIXME: Trying to decide if a gesture is horizontal or vertical at the "began" phase is very error-prone.
auto horizontalSide = ScrollableArea::targetSideForScrollDelta(-wheelEvent.delta(), ScrollEventAxis::Horizontal);
if (horizontalSide && m_client.isPinnedOnSide(*horizontalSide) && !shouldRubberBandOnSide(*horizontalSide))
return false;
auto verticalSide = ScrollableArea::targetSideForScrollDelta(-wheelEvent.delta(), ScrollEventAxis::Vertical);
if (verticalSide && m_client.isPinnedOnSide(*verticalSide) && !shouldRubberBandOnSide(*verticalSide))
return false;
m_momentumScrollInProgress = false;
m_ignoreMomentumScrolls = false;
m_lastMomentumScrollTimestamp = { };
m_momentumVelocity = { };
IntSize stretchAmount = m_client.stretchAmount();
m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(stretchAmount.width()));
m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(stretchAmount.height()));
m_unappliedOverscrollDelta = { };
stopRubberBandAnimation();
updateRubberBandingState();
return true;
}
if (wheelEvent.phase() == PlatformWheelEventPhase::Ended) {
// FIXME: This triggers the rubberband timer even when we don't start rubberbanding.
startRubberBandAnimationIfNecessary();
updateRubberBandingState();
return true;
}
#if !LOG_DISABLED
if (wheelEvent.momentumPhase() == PlatformWheelEventPhase::Began)
m_momentumBeganEventTime = wheelEvent.timestamp();
#endif
if (wheelEvent.momentumPhase() == PlatformWheelEventPhase::Ended && momentumScrollingAnimatorEnabled()) {
#if !LOG_DISABLED
auto timeSinceStart = wheelEvent.timestamp() - m_momentumBeganEventTime;
auto distance = m_eventDrivenScrollOffset - m_eventDrivenScrollMomentumStartOffset;
#endif
// FIXME: We can't distinguish between the natural end of a momentum sequence, and a two-finger tap on the trackpad (rdar://85414238),
// so we always have to end the animation here.
LOG(ScrollAnimations, "Event (%s, %s): stopping event scroll duration %.2fms distance %.2f %.2f",
phaseToString(wheelEvent.phase()), phaseToString(wheelEvent.momentumPhase()), timeSinceStart.milliseconds(),
fabs(distance.width()), fabs(distance.height()));
stopAnimatedNonRubberbandingScroll();
}
bool isMomentumScrollEvent = (wheelEvent.momentumPhase() != PlatformWheelEventPhase::None);
if (m_ignoreMomentumScrolls && (isMomentumScrollEvent || m_isAnimatingRubberBand)) {
if (wheelEvent.momentumPhase() == PlatformWheelEventPhase::Ended) {
m_ignoreMomentumScrolls = false;
return true;
}
if (isMomentumScrollEvent && m_ignoreMomentumScrolls)
return true;
return false;
}
IntSize stretchAmount = m_client.stretchAmount();
bool isVerticallyStretched = stretchAmount.height();
bool isHorizontallyStretched = stretchAmount.width();
// Much of this code, including this use of unaccelerated deltas when stretched, is based on AppKit behavior.
auto eventDelta = (isVerticallyStretched || isHorizontallyStretched) ? -wheelEvent.unacceleratedScrollingDelta() : -wheelEvent.delta();
// Reset unapplied overscroll because we may decide to remove delta at various points and put it into this value.
auto delta = std::exchange(m_unappliedOverscrollDelta, { });
delta += eventDelta;
// FIXME: This replicates what WheelEventDeltaFilter does. We should apply that to events in all phases, and remove axis locking here (webkit.org/b/231207).
delta = deltaAlignedToDominantAxis(delta);
auto momentumPhase = wheelEvent.momentumPhase();
if (!m_momentumScrollInProgress && (momentumPhase == PlatformWheelEventPhase::Began || momentumPhase == PlatformWheelEventPhase::Changed)) {
m_momentumScrollInProgress = true;
if (momentumScrollingAnimatorEnabled()) {
startMomentumScrollWithInitialVelocity(m_client.scrollOffset(), -adjustedVelocity(wheelEvent.scrollingVelocity()), -wheelEvent.delta(), [](const FloatPoint& targetOffset) { return targetOffset; });
#if !LOG_DISABLED
m_eventDrivenScrollOffset = m_client.scrollOffset();
m_eventDrivenScrollMomentumStartOffset = m_eventDrivenScrollOffset;
#endif
}
}
if (momentumPhase == PlatformWheelEventPhase::Changed && momentumScrollingAnimatorEnabled()) {
#if !LOG_DISABLED
m_eventDrivenScrollOffset -= wheelEvent.delta();
#endif
LOG(ScrollAnimations, "Event (%s, %s): ignoring - would have scrolled to %.2f,%.2f", phaseToString(wheelEvent.phase()), phaseToString(wheelEvent.momentumPhase()),
m_eventDrivenScrollOffset.x(), m_eventDrivenScrollOffset.y());
return true;
}
bool shouldStretch = false;
auto timeDelta = wheelEvent.timestamp() - m_lastMomentumScrollTimestamp;
if (m_inScrollGesture || m_momentumScrollInProgress) {
if (m_lastMomentumScrollTimestamp && timeDelta > 0_s && timeDelta < scrollVelocityZeroingTimeout) {
m_momentumVelocity = eventDelta / timeDelta.seconds();
m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
} else {
m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
m_momentumVelocity = { };
}
shouldStretch = modifyScrollDeltaForStretching(wheelEvent, delta, isHorizontallyStretched, isVerticallyStretched);
}
bool handled = true;
if (!delta.isZero()) {
if (shouldStretch || isVerticallyStretched || isHorizontallyStretched) {
if (delta.width() && !m_client.allowsHorizontalStretching(wheelEvent))
handled = false;
if (delta.height() && !m_client.allowsVerticalStretching(wheelEvent))
handled = false;
bool canStartAnimation = applyScrollDeltaWithStretching(wheelEvent, delta, isHorizontallyStretched, isVerticallyStretched);
if (m_momentumScrollInProgress) {
if (canStartAnimation && m_lastMomentumScrollTimestamp) {
m_ignoreMomentumScrolls = true;
m_momentumScrollInProgress = false;
startRubberBandAnimationIfNecessary();
}
}
} else {
delta.scale(scrollWheelMultiplier());
m_client.immediateScrollBy(delta);
}
}
if (m_momentumScrollInProgress && momentumPhase == PlatformWheelEventPhase::Ended) {
m_momentumScrollInProgress = false;
m_ignoreMomentumScrolls = false;
m_lastMomentumScrollTimestamp = { };
}
updateRubberBandingState();
return handled;
}
bool ScrollingEffectsController::modifyScrollDeltaForStretching(const PlatformWheelEvent& wheelEvent, FloatSize& delta, bool isHorizontallyStretched, bool isVerticallyStretched)
{
auto affectedSide = affectedSideOnDominantAxis(delta);
if (isVerticallyStretched) {
if (!isHorizontallyStretched && affectedSide && m_client.isPinnedOnSide(*affectedSide)) {
// Stretching only in the vertical.
if (delta.height() && (fabsf(delta.width() / delta.height()) < rubberbandDirectionLockStretchRatio))
delta.setWidth(0);
else if (fabsf(delta.width()) < rubberbandMinimumRequiredDeltaBeforeStretch) {
m_unappliedOverscrollDelta.expand(delta.width(), 0);
delta.setWidth(0);
} else
m_unappliedOverscrollDelta.expand(delta.width(), 0);
}
return false;
}
if (isHorizontallyStretched) {
// Stretching only in the horizontal.
if (affectedSide && m_client.isPinnedOnSide(*affectedSide)) {
if (delta.width() && (fabsf(delta.height() / delta.width()) < rubberbandDirectionLockStretchRatio))
delta.setHeight(0);
else if (fabsf(delta.height()) < rubberbandMinimumRequiredDeltaBeforeStretch) {
m_unappliedOverscrollDelta.expand(0, delta.height());
delta.setHeight(0);
} else
m_unappliedOverscrollDelta.expand(0, delta.height());
}
return false;
}
// Not stretching at all yet.
if (affectedSide && m_client.isPinnedOnSide(*affectedSide)) {
if (fabsf(delta.height()) >= fabsf(delta.width())) {
if (fabsf(delta.width()) < rubberbandMinimumRequiredDeltaBeforeStretch) {
m_unappliedOverscrollDelta.expand(delta.width(), 0);
delta.setWidth(0);
} else
m_unappliedOverscrollDelta.expand(delta.width(), 0);
}
if (!m_client.allowsHorizontalStretching(wheelEvent))
delta.setWidth(0);
if (!m_client.allowsVerticalStretching(wheelEvent))
delta.setHeight(0);
return !delta.isZero();
}
return false;
}
bool ScrollingEffectsController::applyScrollDeltaWithStretching(const PlatformWheelEvent& wheelEvent, FloatSize delta, bool isHorizontallyStretched, bool isVerticallyStretched)
{
auto eventDelta = (isVerticallyStretched || isHorizontallyStretched) ? -wheelEvent.unacceleratedScrollingDelta() : -wheelEvent.delta();
auto affectedSide = affectedSideOnDominantAxis(delta);
FloatSize deltaToScroll;
if (delta.width()) {
if (!m_client.allowsHorizontalStretching(wheelEvent)) {
delta.setWidth(0);
eventDelta.setWidth(0);
} else if (!isHorizontallyStretched && !m_client.isPinnedOnSide(*affectedSide)) {
delta.scale(scrollWheelMultiplier(), 1);
deltaToScroll += FloatSize { delta.width(), 0 };
delta.setWidth(0);
}
}
if (delta.height()) {
if (!m_client.allowsVerticalStretching(wheelEvent)) {
delta.setHeight(0);
eventDelta.setHeight(0);
} else if (!isVerticallyStretched && !m_client.isPinnedOnSide(*affectedSide)) {
delta.scale(1, scrollWheelMultiplier());
deltaToScroll += FloatSize { 0, delta.height() };
delta.setHeight(0);
}
}
if (!deltaToScroll.isZero())
m_client.immediateScrollBy(deltaToScroll, ScrollClamping::Unclamped);
bool canStartAnimation = false;
if (m_momentumScrollInProgress) {
// Compute canStartAnimation, which looks at isPinnedOnSide(), before applying the stretch delta.
auto sideAffectedByEventDelta = affectedSideOnDominantAxis(eventDelta);
canStartAnimation = !sideAffectedByEventDelta || m_client.isPinnedOnSide(*sideAffectedByEventDelta);
}
m_stretchScrollForce += delta;
auto dampedDelta = FloatSize {
ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.width())),
ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.height()))
};
LOG_WITH_STREAM(ScrollAnimations, stream << "ScrollingEffectsController::applyScrollDeltaWithStretching() - stretchScrollForce " << m_stretchScrollForce << " move delta " << delta << " dampedDelta " << dampedDelta);
auto stretchAmount = m_client.stretchAmount();
m_client.immediateScrollBy(dampedDelta - stretchAmount, ScrollClamping::Unclamped);
return canStartAnimation;
}
FloatSize ScrollingEffectsController::wheelDeltaBiasingTowardsVertical(const PlatformWheelEvent& wheelEvent)
{
return deltaAlignedToDominantAxis(wheelEvent.delta());
}
void ScrollingEffectsController::updateRubberBandAnimatingState()
{
if (!m_isAnimatingRubberBand)
return;
if (isScrollSnapInProgress())
return;
if (m_momentumScrollInProgress && !m_ignoreMomentumScrolls) {
LOG_WITH_STREAM(ScrollAnimations, stream << "ScrollingEffectsController::updateRubberBandAnimatingState() - not animating, momentumScrollInProgress " << m_momentumScrollInProgress << " ignoreMomentumScrolls " << m_ignoreMomentumScrolls);
if (!isRubberBandInProgressInternal())
stopRubberBandAnimation();
}
updateRubberBandingState();
}
void ScrollingEffectsController::scrollPositionChanged()
{
updateRubberBandingState();
}
bool ScrollingEffectsController::isUserScrollInProgress() const
{
return m_inScrollGesture;
}
bool ScrollingEffectsController::isRubberBandInProgress() const
{
return m_isRubberBanding;
}
bool ScrollingEffectsController::isScrollSnapInProgress() const
{
if (!usesScrollSnap())
return false;
if (m_inScrollGesture || m_momentumScrollInProgress || m_isAnimatingScrollSnap)
return true;
return false;
}
void ScrollingEffectsController::stopRubberBanding()
{
stopRubberBandAnimation();
updateRubberBandingState();
}
bool ScrollingEffectsController::startRubberBandAnimation(const FloatPoint& targetOffset, const FloatSize& initialVelocity, const FloatSize& initialOverscroll)
{
if (is<ScrollAnimationMomentum>(m_currentAnimation) && m_currentAnimation->isActive()) {
LOG_WITH_STREAM(ScrollAnimations, stream << "ScrollingEffectsController::startRubberBandAnimation() - momentum animation is present");
return false;
}
if (m_currentAnimation)
m_currentAnimation->stop();
m_currentAnimation = makeUnique<ScrollAnimationRubberBand>(*this);
bool started = downcast<ScrollAnimationRubberBand>(*m_currentAnimation).startRubberBandAnimation(targetOffset, initialVelocity, initialOverscroll);
LOG_WITH_STREAM(ScrollAnimations, stream << "ScrollingEffectsController::startRubberBandAnimation() - animation " << *m_currentAnimation << " started " << started);
return started;
}
void ScrollingEffectsController::stopRubberBandAnimation()
{
if (m_isAnimatingRubberBand)
stopAnimatedScroll();
m_stretchScrollForce = { };
}
void ScrollingEffectsController::willStartRubberBandAnimation()
{
m_isAnimatingRubberBand = true;
m_client.willStartRubberBandAnimation();
m_client.deferWheelEventTestCompletionForReason(reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(this), WheelEventTestMonitor::RubberbandInProgress);
}
void ScrollingEffectsController::didStopRubberBandAnimation()
{
m_isAnimatingRubberBand = false;
m_client.didStopRubberBandAnimation();
m_client.removeWheelEventTestCompletionDeferralForReason(reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(this), WheelEventTestMonitor::RubberbandInProgress);
}
void ScrollingEffectsController::startRubberBandAnimationIfNecessary()
{
auto timeDelta = WallTime::now() - m_lastMomentumScrollTimestamp;
if (m_lastMomentumScrollTimestamp && timeDelta >= scrollVelocityZeroingTimeout)
m_momentumVelocity = { };
if (m_isAnimatingRubberBand)
return;
auto extents = m_client.scrollExtents();
auto targetOffset = m_client.scrollOffset();
auto contrainedOffset = targetOffset.constrainedBetween(extents.minimumScrollOffset(), extents.maximumScrollOffset());
bool willOverscroll = targetOffset != contrainedOffset;
auto stretchAmount = m_client.stretchAmount();
if (stretchAmount.isZero() && !willOverscroll)
return;
auto initialVelocity = m_momentumVelocity;
// Just like normal scrolling, prefer vertical rubberbanding
if (fabsf(initialVelocity.height()) >= fabsf(initialVelocity.width()))
initialVelocity.setWidth(0);
// Don't rubber-band horizontally if it's not possible to scroll horizontally
if (!m_client.allowsHorizontalScrolling())
initialVelocity.setWidth(0);
// Don't rubber-band vertically if it's not possible to scroll vertically
if (!m_client.allowsVerticalScrolling())
initialVelocity.setHeight(0);
LOG_WITH_STREAM(ScrollAnimations, stream << "ScrollingEffectsController::startRubberBandAnimationIfNecessary() - rubberBandAnimationRunning " << m_isAnimatingRubberBand << " stretchAmount " << stretchAmount << " contrainedOffset " << contrainedOffset << " initialVelocity " << initialVelocity);
startRubberBandAnimation(contrainedOffset, initialVelocity, stretchAmount);
}
bool ScrollingEffectsController::shouldRubberBandOnSide(BoxSide side) const
{
return m_client.shouldRubberBandOnSide(side);
}
bool ScrollingEffectsController::isRubberBandInProgressInternal() const
{
if (!m_inScrollGesture && !m_momentumScrollInProgress && !m_isAnimatingRubberBand)
return false;
return !m_client.stretchAmount().isZero();
}
void ScrollingEffectsController::updateRubberBandingState()
{
bool isRubberBanding = isRubberBandInProgressInternal();
if (isRubberBanding == m_isRubberBanding)
return;
LOG_WITH_STREAM(ScrollAnimations, stream << "ScrollingEffectsController " << this << " updateRubberBandingState - isRubberBanding " << isRubberBanding);
m_isRubberBanding = isRubberBanding;
if (m_isRubberBanding)
updateRubberBandingEdges(m_client.stretchAmount());
else
m_rubberBandingEdges = { };
m_client.rubberBandingStateChanged(m_isRubberBanding);
}
void ScrollingEffectsController::updateRubberBandingEdges(IntSize clientStretch)
{
m_rubberBandingEdges.setLeft(clientStretch.width() < 0);
m_rubberBandingEdges.setRight(clientStretch.width() > 0);
m_rubberBandingEdges.setTop(clientStretch.height() < 0);
m_rubberBandingEdges.setBottom(clientStretch.height() > 0);
}
enum class WheelEventStatus {
UserScrollBegin,
UserScrolling,
UserScrollEnd,
MomentumScrollBegin,
MomentumScrolling,
MomentumScrollEnd,
StatelessScrollEvent,
Unknown
};
static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase, PlatformWheelEventPhase momentumPhase)
{
if (phase == PlatformWheelEventPhase::None) {
switch (momentumPhase) {
case PlatformWheelEventPhase::Began:
return WheelEventStatus::MomentumScrollBegin;
case PlatformWheelEventPhase::Changed:
return WheelEventStatus::MomentumScrolling;
case PlatformWheelEventPhase::Ended:
return WheelEventStatus::MomentumScrollEnd;
case PlatformWheelEventPhase::None:
return WheelEventStatus::StatelessScrollEvent;
default:
return WheelEventStatus::Unknown;
}
}
if (momentumPhase == PlatformWheelEventPhase::None) {
switch (phase) {
case PlatformWheelEventPhase::Began:
case PlatformWheelEventPhase::MayBegin:
return WheelEventStatus::UserScrollBegin;
case PlatformWheelEventPhase::Changed:
return WheelEventStatus::UserScrolling;
case PlatformWheelEventPhase::Ended:
case PlatformWheelEventPhase::Cancelled:
return WheelEventStatus::UserScrollEnd;
default:
return WheelEventStatus::Unknown;
}
}
return WheelEventStatus::Unknown;
}
#if !LOG_DISABLED
static TextStream& operator<<(TextStream& ts, WheelEventStatus status)
{
switch (status) {
case WheelEventStatus::UserScrollBegin: ts << "UserScrollBegin"; break;
case WheelEventStatus::UserScrolling: ts << "UserScrolling"; break;
case WheelEventStatus::UserScrollEnd: ts << "UserScrollEnd"; break;
case WheelEventStatus::MomentumScrollBegin: ts << "MomentumScrollBegin"; break;
case WheelEventStatus::MomentumScrolling: ts << "MomentumScrolling"; break;
case WheelEventStatus::MomentumScrollEnd: ts << "MomentumScrollEnd"; break;
case WheelEventStatus::StatelessScrollEvent: ts << "StatelessScrollEvent"; break;
case WheelEventStatus::Unknown: ts << "Unknown"; break;
}
return ts;
}
#endif
bool ScrollingEffectsController::shouldOverrideMomentumScrolling() const
{
if (!usesScrollSnap())
return false;
ScrollSnapState scrollSnapState = m_scrollSnapState->currentState();
return scrollSnapState == ScrollSnapState::Gliding || scrollSnapState == ScrollSnapState::DestinationReached;
}
void ScrollingEffectsController::scheduleStatelessScrollSnap()
{
stopScrollSnapAnimation();
if (m_statelessSnapTransitionTimer) {
m_statelessSnapTransitionTimer->stop();
m_statelessSnapTransitionTimer = nullptr;
}
if (!usesScrollSnap())
return;
static const Seconds statelessScrollSnapDelay = 750_ms;
m_statelessSnapTransitionTimer = m_client.createTimer([this] {
statelessSnapTransitionTimerFired();
});
m_statelessSnapTransitionTimer->startOneShot(statelessScrollSnapDelay);
startDeferringWheelEventTestCompletion(WheelEventTestMonitor::ScrollSnapInProgress);
}
void ScrollingEffectsController::statelessSnapTransitionTimerFired()
{
m_statelessSnapTransitionTimer = nullptr;
if (!usesScrollSnap())
return;
if (m_scrollSnapState->transitionToSnapAnimationState(m_client.scrollExtents(), m_client.pageScaleFactor(), m_client.scrollOffset()))
startScrollSnapAnimation();
}
bool ScrollingEffectsController::processWheelEventForScrollSnap(const PlatformWheelEvent& wheelEvent)
{
if (!usesScrollSnap())
return false;
if (m_scrollSnapState->snapOffsetsForAxis(ScrollEventAxis::Horizontal).isEmpty() && m_scrollSnapState->snapOffsetsForAxis(ScrollEventAxis::Vertical).isEmpty())
return false;
auto status = toWheelEventStatus(wheelEvent.phase(), wheelEvent.momentumPhase());
LOG_WITH_STREAM(ScrollSnap, stream << "ScrollingEffectsController " << this << " processWheelEventForScrollSnap: status " << status);
bool isMomentumScrolling = false;
switch (status) {
case WheelEventStatus::UserScrollBegin:
case WheelEventStatus::UserScrolling:
stopScrollSnapAnimation();
m_scrollSnapState->transitionToUserInteractionState();
m_scrollingVelocityForScrollSnap = -wheelEvent.scrollingVelocity();
break;
case WheelEventStatus::UserScrollEnd:
if (m_scrollSnapState->transitionToSnapAnimationState(m_client.scrollExtents(), m_client.pageScaleFactor(), m_client.scrollOffset()))
startScrollSnapAnimation();
break;
case WheelEventStatus::MomentumScrollBegin:
if (m_scrollSnapState->transitionToGlideAnimationState(m_client.scrollExtents(), m_client.pageScaleFactor(), m_client.scrollOffset(), m_scrollingVelocityForScrollSnap, -wheelEvent.delta())) {
startScrollSnapAnimation();
isMomentumScrolling = true;
}
m_scrollingVelocityForScrollSnap = { };
break;
case WheelEventStatus::MomentumScrolling:
case WheelEventStatus::MomentumScrollEnd:
isMomentumScrolling = true;
break;
case WheelEventStatus::StatelessScrollEvent:
m_scrollSnapState->transitionToUserInteractionState();
scheduleStatelessScrollSnap();
break;
case WheelEventStatus::Unknown:
ASSERT_NOT_REACHED();
break;
}
return isMomentumScrolling && shouldOverrideMomentumScrolling();
}
void ScrollingEffectsController::updateGestureInProgressState(const PlatformWheelEvent& wheelEvent)
{
if (wheelEvent.isGestureStart() || wheelEvent.isTransitioningToMomentumScroll())
m_inScrollGesture = true;
else if (wheelEvent.isEndOfNonMomentumScroll() || wheelEvent.isGestureCancel() || wheelEvent.isEndOfMomentumScroll())
m_inScrollGesture = false;
updateRubberBandingState();
}
} // namespace WebCore
#endif // PLATFORM(MAC)