ScrollController.mm   [plain text]


/*
 * Copyright (C) 2011, 2014-2015 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 "ScrollController.h"

#include "LayoutSize.h"
#include "PlatformWheelEvent.h"
#include "WebCoreSystemInterface.h"
#include "WheelEventTestTrigger.h"
#include <sys/sysctl.h>
#include <sys/time.h>
#include <wtf/CurrentTime.h>

#if ENABLE(CSS_SCROLL_SNAP)
#include "ScrollSnapAnimatorState.h"
#include "ScrollableArea.h"
#endif

#if ENABLE(RUBBER_BANDING) || ENABLE(CSS_SCROLL_SNAP)

#if PLATFORM(MAC)
static NSTimeInterval systemUptime()
{
    if ([[NSProcessInfo processInfo] respondsToSelector:@selector(systemUptime)])
        return [[NSProcessInfo processInfo] systemUptime];

    // Get how long system has been up. Found by looking getting "boottime" from the kernel.
    static struct timeval boottime = {0, 0};
    if (!boottime.tv_sec) {
        int mib[2] = {CTL_KERN, KERN_BOOTTIME};
        size_t size = sizeof(boottime);
        if (-1 == sysctl(mib, 2, &boottime, &size, 0, 0))
            boottime.tv_sec = 0;
    }
    struct timeval now;
    if (boottime.tv_sec && -1 != gettimeofday(&now, 0)) {
        struct timeval uptime;
        timersub(&now, &boottime, &uptime);
        NSTimeInterval result = uptime.tv_sec + (uptime.tv_usec / 1E+6);
        return result;
    }
    return 0;
}
#endif

namespace WebCore {

#if ENABLE(RUBBER_BANDING)
static const float scrollVelocityZeroingTimeout = 0.10f;
static const float rubberbandDirectionLockStretchRatio = 1;
static const float rubberbandMinimumRequiredDeltaBeforeStretch = 10;
#endif

#if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
static const float inertialScrollPredictionFactor = 16.7;
static const double statelessScrollSnapDelay = 0.5;
#endif

#if PLATFORM(MAC)
enum class WheelEventStatus {
    UserScrollBegin,
    UserScrolling,
    UserScrollEnd,
    InertialScrollBegin,
    InertialScrolling,
    InertialScrollEnd,
    StatelessScrollEvent,
    Unknown
};

static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
{
    return wkNSElasticDeltaForTimeDelta(initialPosition, initialVelocity, elapsedTime);
}

static float elasticDeltaForReboundDelta(float delta)
{
    return wkNSElasticDeltaForReboundDelta(delta);
}

static float reboundDeltaForElasticDelta(float delta)
{
    return wkNSReboundDeltaForElasticDelta(delta);
}

static float scrollWheelMultiplier()
{
    static float multiplier = -1;
    if (multiplier < 0) {
        multiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"NSScrollWheelMultiplier"];
        if (multiplier <= 0)
            multiplier = 1;
    }
    return multiplier;
}
#endif

ScrollController::ScrollController(ScrollControllerClient& client)
    : m_client(client)
#if ENABLE(RUBBER_BANDING)
    , m_snapRubberbandTimer(RunLoop::current(), this, &ScrollController::snapRubberBandTimerFired)
#endif
#if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
    , m_scrollSnapTimer(RunLoop::current(), this, &ScrollController::scrollSnapTimerFired)
#endif
{
}

#if PLATFORM(MAC)
bool ScrollController::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
{
#if ENABLE(CSS_SCROLL_SNAP)
    if (!processWheelEventForScrollSnap(wheelEvent))
        return false;
#endif
    if (wheelEvent.phase() == PlatformWheelEventPhaseBegan) {
        // First, check if we should rubber-band at all.
        if (m_client.pinnedInDirection(FloatSize(-wheelEvent.deltaX(), 0))
            && !shouldRubberBandInHorizontalDirection(wheelEvent))
            return false;

        m_inScrollGesture = true;
        m_momentumScrollInProgress = false;
        m_ignoreMomentumScrolls = false;
        m_lastMomentumScrollTimestamp = 0;
        m_momentumVelocity = FloatSize();

        IntSize stretchAmount = m_client.stretchAmount();
        m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(stretchAmount.width()));
        m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(stretchAmount.height()));
        m_overflowScrollDelta = FloatSize();

        stopSnapRubberbandTimer();

        return true;
    }

    if (wheelEvent.phase() == PlatformWheelEventPhaseEnded) {
        snapRubberBand();
        return true;
    }

    bool isMomentumScrollEvent = (wheelEvent.momentumPhase() != PlatformWheelEventPhaseNone);
    if (m_ignoreMomentumScrolls && (isMomentumScrollEvent || m_snapRubberbandTimerIsActive)) {
        if (wheelEvent.momentumPhase() == PlatformWheelEventPhaseEnded) {
            m_ignoreMomentumScrolls = false;
            return true;
        }
        return false;
    }

    float deltaX = m_overflowScrollDelta.width();
    float deltaY = m_overflowScrollDelta.height();

    // Reset overflow values because we may decide to remove delta at various points and put it into overflow.
    m_overflowScrollDelta = FloatSize();

    IntSize stretchAmount = m_client.stretchAmount();
    bool isVerticallyStretched = stretchAmount.height();
    bool isHorizontallyStretched = stretchAmount.width();

    float eventCoalescedDeltaX;
    float eventCoalescedDeltaY;

    if (isVerticallyStretched || isHorizontallyStretched) {
        eventCoalescedDeltaX = -wheelEvent.unacceleratedScrollingDeltaX();
        eventCoalescedDeltaY = -wheelEvent.unacceleratedScrollingDeltaY();
    } else {
        eventCoalescedDeltaX = -wheelEvent.deltaX();
        eventCoalescedDeltaY = -wheelEvent.deltaY();
    }

    deltaX += eventCoalescedDeltaX;
    deltaY += eventCoalescedDeltaY;

    // Slightly prefer scrolling vertically by applying the = case to deltaY
    if (fabsf(deltaY) >= fabsf(deltaX))
        deltaX = 0;
    else
        deltaY = 0;

    bool shouldStretch = false;

    PlatformWheelEventPhase momentumPhase = wheelEvent.momentumPhase();

    // If we are starting momentum scrolling then do some setup.
    if (!m_momentumScrollInProgress && (momentumPhase == PlatformWheelEventPhaseBegan || momentumPhase == PlatformWheelEventPhaseChanged))
        m_momentumScrollInProgress = true;

    CFTimeInterval timeDelta = wheelEvent.timestamp() - m_lastMomentumScrollTimestamp;
    if (m_inScrollGesture || m_momentumScrollInProgress) {
        if (m_lastMomentumScrollTimestamp && timeDelta > 0 && timeDelta < scrollVelocityZeroingTimeout) {
            m_momentumVelocity.setWidth(eventCoalescedDeltaX / (float)timeDelta);
            m_momentumVelocity.setHeight(eventCoalescedDeltaY / (float)timeDelta);
            m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
        } else {
            m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
            m_momentumVelocity = FloatSize();
        }

        if (isVerticallyStretched) {
            if (!isHorizontallyStretched && m_client.pinnedInDirection(FloatSize(deltaX, 0))) {
                // Stretching only in the vertical.
                if (deltaY && (fabsf(deltaX / deltaY) < rubberbandDirectionLockStretchRatio))
                    deltaX = 0;
                else if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
                    m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
                    deltaX = 0;
                } else
                    m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
            }
        } else if (isHorizontallyStretched) {
            // Stretching only in the horizontal.
            if (m_client.pinnedInDirection(FloatSize(0, deltaY))) {
                if (deltaX && (fabsf(deltaY / deltaX) < rubberbandDirectionLockStretchRatio))
                    deltaY = 0;
                else if (fabsf(deltaY) < rubberbandMinimumRequiredDeltaBeforeStretch) {
                    m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
                    deltaY = 0;
                } else
                    m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
            }
        } else {
            // Not stretching at all yet.
            if (m_client.pinnedInDirection(FloatSize(deltaX, deltaY))) {
                if (fabsf(deltaY) >= fabsf(deltaX)) {
                    if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
                        m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
                        deltaX = 0;
                    } else
                        m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
                }
                shouldStretch = true;
            }
        }
    }

    if (deltaX || deltaY) {
        if (!(shouldStretch || isVerticallyStretched || isHorizontallyStretched)) {
            if (deltaY) {
                deltaY *= scrollWheelMultiplier();
                m_client.immediateScrollBy(FloatSize(0, deltaY));
            }
            if (deltaX) {
                deltaX *= scrollWheelMultiplier();
                m_client.immediateScrollBy(FloatSize(deltaX, 0));
            }
        } else {
            if (!m_client.allowsHorizontalStretching(wheelEvent)) {
                deltaX = 0;
                eventCoalescedDeltaX = 0;
            } else if (deltaX && !isHorizontallyStretched && !m_client.pinnedInDirection(FloatSize(deltaX, 0))) {
                deltaX *= scrollWheelMultiplier();

                m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(deltaX, 0));
                deltaX = 0;
            }

            if (!m_client.allowsVerticalStretching(wheelEvent)) {
                deltaY = 0;
                eventCoalescedDeltaY = 0;
            } else if (deltaY && !isVerticallyStretched && !m_client.pinnedInDirection(FloatSize(0, deltaY))) {
                deltaY *= scrollWheelMultiplier();

                m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(0, deltaY));
                deltaY = 0;
            }

            IntSize stretchAmount = m_client.stretchAmount();

            if (m_momentumScrollInProgress) {
                if ((m_client.pinnedInDirection(FloatSize(eventCoalescedDeltaX, eventCoalescedDeltaY)) || (fabsf(eventCoalescedDeltaX) + fabsf(eventCoalescedDeltaY) <= 0)) && m_lastMomentumScrollTimestamp) {
                    m_ignoreMomentumScrolls = true;
                    m_momentumScrollInProgress = false;
                    snapRubberBand();
                }
            }

            m_stretchScrollForce.setWidth(m_stretchScrollForce.width() + deltaX);
            m_stretchScrollForce.setHeight(m_stretchScrollForce.height() + deltaY);

            FloatSize dampedDelta(ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.width())), ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.height())));

            m_client.immediateScrollByWithoutContentEdgeConstraints(dampedDelta - stretchAmount);
        }
    }

    if (m_momentumScrollInProgress && momentumPhase == PlatformWheelEventPhaseEnded) {
        m_momentumScrollInProgress = false;
        m_ignoreMomentumScrolls = false;
        m_lastMomentumScrollTimestamp = 0;
    }

    return true;
}
#endif

#if ENABLE(RUBBER_BANDING)
static inline float roundTowardZero(float num)
{
    return num > 0 ? ceilf(num - 0.5f) : floorf(num + 0.5f);
}

static inline float roundToDevicePixelTowardZero(float num)
{
    float roundedNum = roundf(num);
    if (fabs(num - roundedNum) < 0.125)
        num = roundedNum;

    return roundTowardZero(num);
}

void ScrollController::snapRubberBandTimerFired()
{
    if (isScrollSnapInProgress())
        return;
    
    if (!m_momentumScrollInProgress || m_ignoreMomentumScrolls) {
        CFTimeInterval timeDelta = [NSDate timeIntervalSinceReferenceDate] - m_startTime;

        if (m_startStretch == FloatSize()) {
            m_startStretch = m_client.stretchAmount();
            if (m_startStretch == FloatSize()) {
                stopSnapRubberbandTimer();

                m_stretchScrollForce = FloatSize();
                m_startTime = 0;
                m_startStretch = FloatSize();
                m_origOrigin = FloatPoint();
                m_origVelocity = FloatSize();
                return;
            }

            m_origOrigin = m_client.absoluteScrollPosition() - m_startStretch;
            m_origVelocity = m_momentumVelocity;

            // Just like normal scrolling, prefer vertical rubberbanding
            if (fabsf(m_origVelocity.height()) >= fabsf(m_origVelocity.width()))
                m_origVelocity.setWidth(0);

            // Don't rubber-band horizontally if it's not possible to scroll horizontally
            if (!m_client.canScrollHorizontally())
                m_origVelocity.setWidth(0);

            // Don't rubber-band vertically if it's not possible to scroll vertically
            if (!m_client.canScrollVertically())
                m_origVelocity.setHeight(0);
        }

        FloatPoint delta(roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.width(), -m_origVelocity.width(), (float)timeDelta)),
            roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.height(), -m_origVelocity.height(), (float)timeDelta)));

        if (fabs(delta.x()) >= 1 || fabs(delta.y()) >= 1) {
            m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(delta.x(), delta.y()) - m_client.stretchAmount());

            FloatSize newStretch = m_client.stretchAmount();

            m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(newStretch.width()));
            m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(newStretch.height()));
        } else {
            m_client.adjustScrollPositionToBoundsIfNecessary();

            stopSnapRubberbandTimer();
            m_stretchScrollForce = FloatSize();
            m_startTime = 0;
            m_startStretch = FloatSize();
            m_origOrigin = FloatPoint();
            m_origVelocity = FloatSize();
        }
    } else {
        m_startTime = [NSDate timeIntervalSinceReferenceDate];
        m_startStretch = FloatSize();
        if (!isRubberBandInProgress())
            stopSnapRubberbandTimer();
    }
}
#endif

bool ScrollController::isRubberBandInProgress() const
{
#if ENABLE(RUBBER_BANDING) && PLATFORM(MAC)
    if (!m_inScrollGesture && !m_momentumScrollInProgress && !m_snapRubberbandTimerIsActive)
        return false;

    return !m_client.stretchAmount().isZero();
#else
    return false;
#endif
}

bool ScrollController::isScrollSnapInProgress() const
{
#if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
    if (m_inScrollGesture || m_momentumScrollInProgress || m_scrollSnapTimer.isActive())
        return true;
#endif
    return false;
}

#if ENABLE(RUBBER_BANDING)
void ScrollController::startSnapRubberbandTimer()
{
    m_client.startSnapRubberbandTimer();
    m_snapRubberbandTimer.startRepeating(1.0 / 60.0);

    m_client.deferTestsForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::RubberbandInProgress);
}

void ScrollController::stopSnapRubberbandTimer()
{
    m_client.stopSnapRubberbandTimer();
    m_snapRubberbandTimer.stop();
    m_snapRubberbandTimerIsActive = false;
    
    m_client.removeTestDeferralForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::RubberbandInProgress);
}

void ScrollController::snapRubberBand()
{
    CFTimeInterval timeDelta = systemUptime() - m_lastMomentumScrollTimestamp;
    if (m_lastMomentumScrollTimestamp && timeDelta >= scrollVelocityZeroingTimeout)
        m_momentumVelocity = FloatSize();

    m_inScrollGesture = false;

    if (m_snapRubberbandTimerIsActive)
        return;

    m_startTime = [NSDate timeIntervalSinceReferenceDate];
    m_startStretch = FloatSize();
    m_origOrigin = FloatPoint();
    m_origVelocity = FloatSize();

    startSnapRubberbandTimer();
    m_snapRubberbandTimerIsActive = true;
}

bool ScrollController::shouldRubberBandInHorizontalDirection(const PlatformWheelEvent& wheelEvent)
{
    if (wheelEvent.deltaX() > 0)
        return m_client.shouldRubberBandInDirection(ScrollLeft);
    if (wheelEvent.deltaX() < 0)
        return m_client.shouldRubberBandInDirection(ScrollRight);

    return true;
}
#endif

#if ENABLE(CSS_SCROLL_SNAP)
ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis)
{
    ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
    ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);

    return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
}

const ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis) const
{
    ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
    ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
    
    return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
}

#if PLATFORM(MAC)
static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase, PlatformWheelEventPhase momentumPhase)
{
    if (phase == PlatformWheelEventPhaseNone) {
        switch (momentumPhase) {
        case PlatformWheelEventPhaseBegan:
            return WheelEventStatus::InertialScrollBegin;
                
        case PlatformWheelEventPhaseChanged:
            return WheelEventStatus::InertialScrolling;
                
        case PlatformWheelEventPhaseEnded:
            return WheelEventStatus::InertialScrollEnd;

        case PlatformWheelEventPhaseNone:
            return WheelEventStatus::StatelessScrollEvent;

        default:
            return WheelEventStatus::Unknown;
        }
    }
    if (momentumPhase == PlatformWheelEventPhaseNone) {
        switch (phase) {
        case PlatformWheelEventPhaseBegan:
        case PlatformWheelEventPhaseMayBegin:
            return WheelEventStatus::UserScrollBegin;
                
        case PlatformWheelEventPhaseChanged:
            return WheelEventStatus::UserScrolling;
                
        case PlatformWheelEventPhaseEnded:
        case PlatformWheelEventPhaseCancelled:
            return WheelEventStatus::UserScrollEnd;
                
        default:
            return WheelEventStatus::Unknown;
        }
    }
    return WheelEventStatus::Unknown;
}

void ScrollController::processWheelEventForScrollSnapOnAxis(ScrollEventAxis axis, const PlatformWheelEvent& event)
{
    ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);

    float wheelDelta = axis == ScrollEventAxis::Horizontal ? -event.deltaX() : -event.deltaY();
    WheelEventStatus wheelStatus = toWheelEventStatus(event.phase(), event.momentumPhase());
    
    switch (wheelStatus) {
    case WheelEventStatus::UserScrollBegin:
    case WheelEventStatus::UserScrolling:
        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
        break;
            
    case WheelEventStatus::UserScrollEnd:
        beginScrollSnapAnimation(axis, ScrollSnapState::Snapping);
        break;
        
    case WheelEventStatus::InertialScrollBegin:
        // Begin tracking wheel deltas for glide prediction.
        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
        snapState.pushInitialWheelDelta(wheelDelta);
        snapState.m_beginTrackingWheelDeltaOffset = m_client.scrollOffsetOnAxis(axis);
        break;
            
    case WheelEventStatus::InertialScrolling:
        // This check for DestinationReached ensures that we don't receive another set of momentum events after ending the last glide.
        if (snapState.m_currentState != ScrollSnapState::Gliding && snapState.m_currentState != ScrollSnapState::DestinationReached) {
            if (snapState.m_numWheelDeltasTracked < snapState.wheelDeltaWindowSize && wheelDelta)
                snapState.pushInitialWheelDelta(wheelDelta);
            
            if ((snapState.m_numWheelDeltasTracked == snapState.wheelDeltaWindowSize) && snapState.averageInitialWheelDelta())
                beginScrollSnapAnimation(axis, ScrollSnapState::Gliding);
        }
        break;
        
    case WheelEventStatus::InertialScrollEnd:
        snapState.clearInitialWheelDeltaWindow();
        snapState.m_shouldOverrideWheelEvent = false;
        break;

    case WheelEventStatus::StatelessScrollEvent:
        endScrollSnapAnimation(ScrollSnapState::UserInteraction);
        snapState.clearInitialWheelDeltaWindow();
        snapState.m_shouldOverrideWheelEvent = false;
        m_scrollSnapTimer.startOneShot(statelessScrollSnapDelay);
        if (axis == ScrollEventAxis::Horizontal)
            m_expectingHorizontalStatelessScrollSnap = true;
        else
            m_expectingVerticalStatelessScrollSnap = true;
        break;

    case WheelEventStatus::Unknown:
        ASSERT_NOT_REACHED();
        break;
    }
}

bool ScrollController::shouldOverrideWheelEvent(ScrollEventAxis axis, const PlatformWheelEvent& event) const
{
    const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);

    return snapState.m_shouldOverrideWheelEvent && toWheelEventStatus(event.phase(), event.momentumPhase()) == WheelEventStatus::InertialScrolling;
}

bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent& wheelEvent)
{
    bool shouldAllowWheelEventToPropagate = true;
    if (m_verticalScrollSnapState) {
        processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Vertical, wheelEvent);
        shouldAllowWheelEventToPropagate &= !shouldOverrideWheelEvent(ScrollEventAxis::Vertical, wheelEvent);
    }
    if (m_horizontalScrollSnapState) {
        processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Horizontal, wheelEvent);
        shouldAllowWheelEventToPropagate &= !shouldOverrideWheelEvent(ScrollEventAxis::Horizontal, wheelEvent);
    }
    return shouldAllowWheelEventToPropagate;
}
#endif

void ScrollController::updateScrollSnapState(const ScrollableArea& scrollableArea)
{
    // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
    if (scrollableArea.horizontalSnapOffsets())
        m_horizontalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, *scrollableArea.horizontalSnapOffsets());
    else if (m_horizontalScrollSnapState)
        m_horizontalScrollSnapState = nullptr;

    if (scrollableArea.verticalSnapOffsets())
        m_verticalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, *scrollableArea.verticalSnapOffsets());
    else if (m_verticalScrollSnapState)
        m_verticalScrollSnapState = nullptr;
}

void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector<LayoutUnit>& snapPoints)
{
    // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
    if (axis == ScrollEventAxis::Horizontal)
        m_horizontalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, snapPoints) : nullptr;

    if (axis == ScrollEventAxis::Vertical)
        m_verticalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, snapPoints) : nullptr;
}

#if PLATFORM(MAC)
void ScrollController::startScrollSnapTimer()
{
    if (!m_scrollSnapTimer.isActive()) {
        m_client.startScrollSnapTimer();
        m_scrollSnapTimer.startRepeating(1.0 / 60.0);
    }

    if (!m_scrollSnapTimer.isActive())
        return;

    m_client.deferTestsForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
}

void ScrollController::stopScrollSnapTimer()
{
    m_client.stopScrollSnapTimer();
    m_scrollSnapTimer.stop();
    
    if (m_scrollSnapTimer.isActive())
        return;

    m_client.removeTestDeferralForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
}

void ScrollController::scrollSnapTimerFired()
{
    if (m_expectingHorizontalStatelessScrollSnap || m_expectingVerticalStatelessScrollSnap) {
        if (m_expectingHorizontalStatelessScrollSnap)
            beginScrollSnapAnimation(ScrollEventAxis::Horizontal, ScrollSnapState::Snapping);
        if (m_expectingVerticalStatelessScrollSnap)
            beginScrollSnapAnimation(ScrollEventAxis::Vertical, ScrollSnapState::Snapping);
        return;
    }
        
    bool snapOnHorizontalAxis = isSnappingOnAxis(ScrollEventAxis::Horizontal);
    bool snapOnVerticalAxis = isSnappingOnAxis(ScrollEventAxis::Vertical);
    if (snapOnHorizontalAxis && !m_horizontalScrollSnapState->canReachTargetWithCurrentInitialScrollDelta()) {
        m_horizontalScrollSnapState->m_currentState = ScrollSnapState::DestinationReached;
        snapOnHorizontalAxis = false;
    }
    if (snapOnVerticalAxis && !m_verticalScrollSnapState->canReachTargetWithCurrentInitialScrollDelta()) {
        m_verticalScrollSnapState->m_currentState = ScrollSnapState::DestinationReached;
        snapOnVerticalAxis = false;
    }
    if (!snapOnHorizontalAxis && !snapOnVerticalAxis) {
        endScrollSnapAnimation(ScrollSnapState::DestinationReached);
        return;
    }
    
    double currentTime = monotonicallyIncreasingTime();
    if (m_scrollSnapCurveState->shouldCompleteSnapAnimationImmediatelyAtTime(currentTime)) {
        float finalHorizontalDelta = 0;
        float finalVerticalDelta = 0;
        if (snapOnHorizontalAxis)
            finalHorizontalDelta = m_horizontalScrollSnapState->m_targetOffset - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
        if (snapOnVerticalAxis)
            finalVerticalDelta = m_verticalScrollSnapState->m_targetOffset - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);

        if (finalHorizontalDelta || finalVerticalDelta)
            m_client.immediateScrollBy(FloatSize(finalHorizontalDelta, finalVerticalDelta));

        endScrollSnapAnimation(ScrollSnapState::DestinationReached);
        return;
    }
    
    float animationProgress = m_scrollSnapCurveState->animationProgressAtTime(currentTime);
    float horizontalDelta = 0;
    float verticalDelta = 0;
    if (m_scrollSnapCurveState->shouldAnimateDirectlyToSnapPoint) {
        if (snapOnHorizontalAxis)
            horizontalDelta = m_horizontalScrollSnapState->interpolatedOffsetAtProgress(animationProgress) - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
        if (snapOnVerticalAxis)
            verticalDelta = m_verticalScrollSnapState->interpolatedOffsetAtProgress(animationProgress) - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);

    } else {
        FloatPoint interpolatedPoint = m_scrollSnapCurveState->interpolatedPositionAtProgress(animationProgress);
        horizontalDelta = interpolatedPoint.x() - m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal);
        verticalDelta = interpolatedPoint.y() - m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical);
    }
    
    if (horizontalDelta || verticalDelta)
        m_client.immediateScrollBy(FloatSize(horizontalDelta, verticalDelta));
}

static inline float projectedInertialScrollDistance(float initialWheelDelta)
{
    // FIXME: Experiments with inertial scrolling show a fairly consistent linear relationship between initial wheel delta and total distance scrolled.
    // In the future, we'll want to find a more accurate way of inertial scroll prediction.
    return inertialScrollPredictionFactor * initialWheelDelta;
}
#endif

unsigned ScrollController::activeScrollSnapIndexForAxis(ScrollEventAxis axis) const
{
    if ((axis == ScrollEventAxis::Horizontal) && !m_horizontalScrollSnapState)
        return 0;
    if ((axis == ScrollEventAxis::Vertical) && !m_verticalScrollSnapState)
        return 0;
    
    const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
    return snapState.m_activeSnapIndex;
}

void ScrollController::setActiveScrollSnapIndexForAxis(ScrollEventAxis axis, unsigned index)
{
    auto* snapState = (axis == ScrollEventAxis::Horizontal) ? m_horizontalScrollSnapState.get() : m_verticalScrollSnapState.get();
    if (!snapState)
        return;

    snapState->m_activeSnapIndex = index;
}

void ScrollController::setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis axis, int offset)
{
    float scaleFactor = m_client.pageScaleFactor();
    ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
    
    LayoutUnit clampedOffset = std::min(std::max(LayoutUnit(offset / scaleFactor), snapState.m_snapOffsets.first()), snapState.m_snapOffsets.last());

    unsigned activeIndex = 0;
    (void)closestSnapOffset<LayoutUnit, float>(snapState.m_snapOffsets, clampedOffset, 0, activeIndex);

    if (activeIndex == snapState.m_activeSnapIndex)
        return;

    m_activeScrollSnapIndexDidChange = true;
    snapState.m_activeSnapIndex = activeIndex;
}

void ScrollController::setActiveScrollSnapIndicesForOffset(int x, int y)
{
    if (m_horizontalScrollSnapState)
        setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Horizontal, x);
    if (m_verticalScrollSnapState)
        setNearestScrollSnapIndexForAxisAndOffset(ScrollEventAxis::Vertical, y);
}

#if PLATFORM(MAC)
void ScrollController::beginScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
{
    ASSERT(newState == ScrollSnapState::Gliding || newState == ScrollSnapState::Snapping);
    if (m_expectingHorizontalStatelessScrollSnap || m_expectingVerticalStatelessScrollSnap) {
        m_expectingHorizontalStatelessScrollSnap = false;
        m_expectingVerticalStatelessScrollSnap = false;
        stopScrollSnapTimer();
    }
    ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);

    LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
    float initialWheelDelta = newState == ScrollSnapState::Gliding ? snapState.averageInitialWheelDelta() : 0;
    LayoutUnit scaledProjectedScrollDestination = newState == ScrollSnapState::Gliding ? snapState.m_beginTrackingWheelDeltaOffset + LayoutUnit(projectedInertialScrollDistance(initialWheelDelta)) : offset;
    if (snapState.m_snapOffsets.isEmpty())
        return;

    float scaleFactor = m_client.pageScaleFactor();
    LayoutUnit originalProjectedScrollDestination = scaledProjectedScrollDestination / scaleFactor;
    
    LayoutUnit clampedScrollDestination = std::min(std::max(originalProjectedScrollDestination, snapState.m_snapOffsets.first()), snapState.m_snapOffsets.last());
    snapState.m_initialOffset = offset;
    m_activeScrollSnapIndexDidChange = false;
    snapState.m_targetOffset = scaleFactor * closestSnapOffset<LayoutUnit, float>(snapState.m_snapOffsets, clampedScrollDestination, initialWheelDelta, snapState.m_activeSnapIndex);
    if (snapState.m_initialOffset == snapState.m_targetOffset)
        return;

    LayoutUnit scrollExtent = (axis == ScrollEventAxis::Horizontal) ? m_client.scrollExtent().width() : m_client.scrollExtent().height();
    LayoutUnit projectedScrollDestination = clampedScrollDestination;
    if (originalProjectedScrollDestination < 0 || originalProjectedScrollDestination > scrollExtent)
        projectedScrollDestination = originalProjectedScrollDestination;
    
    m_activeScrollSnapIndexDidChange = true;
    snapState.m_currentState = newState;
    if (newState == ScrollSnapState::Gliding) {
        // Check if the other scroll axis needs to animate to the nearest snap point.
        snapState.m_initialScrollDelta = initialWheelDelta;
        snapState.m_shouldOverrideWheelEvent = true;
        snapState.clearInitialWheelDeltaWindow();
        ScrollEventAxis otherAxis = axis == ScrollEventAxis::Horizontal ? ScrollEventAxis::Vertical : ScrollEventAxis::Horizontal;
        if ((otherAxis == ScrollEventAxis::Horizontal && m_horizontalScrollSnapState && m_horizontalScrollSnapState->m_currentState == ScrollSnapState::UserInteraction)
            || (otherAxis == ScrollEventAxis::Vertical && m_verticalScrollSnapState && m_verticalScrollSnapState->m_currentState == ScrollSnapState::UserInteraction)) {
            
            ScrollSnapAnimatorState& otherState = scrollSnapPointState(otherAxis);
            if (!otherState.averageInitialWheelDelta()) {
                float offsetOnOtherAxis = m_client.scrollOffsetOnAxis(otherAxis);
                float snapOffsetForOtherAxis = scaleFactor * closestSnapOffset<LayoutUnit, float>(otherState.m_snapOffsets, offsetOnOtherAxis, 0, otherState.m_activeSnapIndex);
                if (offsetOnOtherAxis != snapOffsetForOtherAxis) {
                    otherState.m_initialOffset = offsetOnOtherAxis;
                    otherState.m_targetOffset = snapOffsetForOtherAxis;
                    otherState.m_initialScrollDelta = 0;
                    otherState.m_currentState = ScrollSnapState::Gliding;
                }
            }
        }
        
    } else {
        snapState.m_initialScrollDelta = initialWheelDelta;
    }
    initializeScrollSnapAnimationParameters();
    startScrollSnapTimer();
}

void ScrollController::endScrollSnapAnimation(ScrollSnapState newState)
{
    ASSERT(newState == ScrollSnapState::DestinationReached || newState == ScrollSnapState::UserInteraction);
    if (m_horizontalScrollSnapState)
        m_horizontalScrollSnapState->m_currentState = newState;

    if (m_verticalScrollSnapState)
        m_verticalScrollSnapState->m_currentState = newState;

    stopScrollSnapTimer();
}

void ScrollController::initializeScrollSnapAnimationParameters()
{
    if (!m_scrollSnapCurveState)
        m_scrollSnapCurveState = std::make_unique<ScrollSnapAnimationCurveState>();
    
    bool isSnappingOnHorizontalAxis = isSnappingOnAxis(ScrollEventAxis::Horizontal);
    bool isSnappingOnVerticalAxis = isSnappingOnAxis(ScrollEventAxis::Vertical);
    FloatSize initialVector(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState->m_initialOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal),
        isSnappingOnVerticalAxis ? m_verticalScrollSnapState->m_initialOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical));
    FloatSize targetVector(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState->m_targetOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Horizontal),
        isSnappingOnVerticalAxis ? m_verticalScrollSnapState->m_targetOffset : m_client.scrollOffsetOnAxis(ScrollEventAxis::Vertical));
    FloatSize initialDelta(isSnappingOnHorizontalAxis ? m_horizontalScrollSnapState->m_initialScrollDelta : 0,
        isSnappingOnVerticalAxis ? m_verticalScrollSnapState->m_initialScrollDelta : 0);

    // Animate directly by default. This flag will be changed as necessary if interpolation is possible.
    m_scrollSnapCurveState->shouldAnimateDirectlyToSnapPoint = true;
    m_scrollSnapCurveState->initializeSnapProgressCurve(initialVector, targetVector, initialDelta);
    if (isSnappingOnHorizontalAxis && isSnappingOnVerticalAxis)
        m_scrollSnapCurveState->initializeInterpolationCoefficientsIfNecessary(initialVector, targetVector, initialDelta);
}
    
bool ScrollController::isSnappingOnAxis(ScrollEventAxis axis) const
{
    if (axis == ScrollEventAxis::Horizontal)
        return m_horizontalScrollSnapState && m_horizontalScrollSnapState->isSnapping();

    return m_verticalScrollSnapState && m_verticalScrollSnapState->isSnapping();
}
    
#endif
#endif

} // namespace WebCore

#endif // ENABLE(RUBBER_BANDING)