2 * Copyright (C) 2011, 2014-2015 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
27 #include "ScrollController.h"
29 #include "PlatformWheelEvent.h"
30 #include "WebCoreSystemInterface.h"
31 #include <sys/sysctl.h>
34 #if ENABLE(CSS_SCROLL_SNAP)
35 #include "ScrollSnapAnimatorState.h"
36 #include "ScrollableArea.h"
39 #if ENABLE(RUBBER_BANDING)
41 static NSTimeInterval systemUptime()
43 if ([[NSProcessInfo processInfo] respondsToSelector:@selector(systemUptime)])
44 return [[NSProcessInfo processInfo] systemUptime];
46 // Get how long system has been up. Found by looking getting "boottime" from the kernel.
47 static struct timeval boottime = {0, 0};
48 if (!boottime.tv_sec) {
49 int mib[2] = {CTL_KERN, KERN_BOOTTIME};
50 size_t size = sizeof(boottime);
51 if (-1 == sysctl(mib, 2, &boottime, &size, 0, 0))
55 if (boottime.tv_sec && -1 != gettimeofday(&now, 0)) {
56 struct timeval uptime;
57 timersub(&now, &boottime, &uptime);
58 NSTimeInterval result = uptime.tv_sec + (uptime.tv_usec / 1E+6);
67 static const float scrollVelocityZeroingTimeout = 0.10f;
68 static const float rubberbandDirectionLockStretchRatio = 1;
69 static const float rubberbandMinimumRequiredDeltaBeforeStretch = 10;
71 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
72 static const float snapMagnitudeMax = 25;
73 static const float snapMagnitudeMin = 5;
74 static const float snapThresholdHigh = 1000;
75 static const float snapThresholdLow = 50;
77 static const float inertialScrollPredictionFactor = 16.7;
78 static const float initialToFinalMomentumFactor = 1.0 / 40.0;
80 static const float glideBoostMultiplier = 3.5;
82 static const float maxTargetWheelDelta = 7;
83 static const float minTargetWheelDelta = 3.5;
86 enum class WheelEventStatus {
97 static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
99 return wkNSElasticDeltaForTimeDelta(initialPosition, initialVelocity, elapsedTime);
102 static float elasticDeltaForReboundDelta(float delta)
104 return wkNSElasticDeltaForReboundDelta(delta);
107 static float reboundDeltaForElasticDelta(float delta)
109 return wkNSReboundDeltaForElasticDelta(delta);
112 static float scrollWheelMultiplier()
114 static float multiplier = -1;
115 if (multiplier < 0) {
116 multiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"NSScrollWheelMultiplier"];
123 ScrollController::ScrollController(ScrollControllerClient& client)
125 , m_lastMomentumScrollTimestamp(0)
127 , m_snapRubberbandTimer(RunLoop::current(), this, &ScrollController::snapRubberBandTimerFired)
128 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
129 , m_horizontalScrollSnapTimer(RunLoop::current(), this, &ScrollController::horizontalScrollSnapTimerFired)
130 , m_verticalScrollSnapTimer(RunLoop::current(), this, &ScrollController::verticalScrollSnapTimerFired)
132 , m_inScrollGesture(false)
133 , m_momentumScrollInProgress(false)
134 , m_ignoreMomentumScrolls(false)
135 , m_snapRubberbandTimerIsActive(false)
139 bool ScrollController::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
141 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
142 if (!processWheelEventForScrollSnap(wheelEvent))
145 if (wheelEvent.phase() == PlatformWheelEventPhaseBegan) {
146 // First, check if we should rubber-band at all.
147 if (m_client.pinnedInDirection(FloatSize(-wheelEvent.deltaX(), 0))
148 && !shouldRubberBandInHorizontalDirection(wheelEvent))
151 m_inScrollGesture = true;
152 m_momentumScrollInProgress = false;
153 m_ignoreMomentumScrolls = false;
154 m_lastMomentumScrollTimestamp = 0;
155 m_momentumVelocity = FloatSize();
157 IntSize stretchAmount = m_client.stretchAmount();
158 m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(stretchAmount.width()));
159 m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(stretchAmount.height()));
160 m_overflowScrollDelta = FloatSize();
162 stopSnapRubberbandTimer();
167 if (wheelEvent.phase() == PlatformWheelEventPhaseEnded) {
172 bool isMomentumScrollEvent = (wheelEvent.momentumPhase() != PlatformWheelEventPhaseNone);
173 if (m_ignoreMomentumScrolls && (isMomentumScrollEvent || m_snapRubberbandTimerIsActive)) {
174 if (wheelEvent.momentumPhase() == PlatformWheelEventPhaseEnded) {
175 m_ignoreMomentumScrolls = false;
181 float deltaX = m_overflowScrollDelta.width();
182 float deltaY = m_overflowScrollDelta.height();
184 // Reset overflow values because we may decide to remove delta at various points and put it into overflow.
185 m_overflowScrollDelta = FloatSize();
187 IntSize stretchAmount = m_client.stretchAmount();
188 bool isVerticallyStretched = stretchAmount.height();
189 bool isHorizontallyStretched = stretchAmount.width();
191 float eventCoalescedDeltaX;
192 float eventCoalescedDeltaY;
194 if (isVerticallyStretched || isHorizontallyStretched) {
195 eventCoalescedDeltaX = -wheelEvent.unacceleratedScrollingDeltaX();
196 eventCoalescedDeltaY = -wheelEvent.unacceleratedScrollingDeltaY();
198 eventCoalescedDeltaX = -wheelEvent.deltaX();
199 eventCoalescedDeltaY = -wheelEvent.deltaY();
202 deltaX += eventCoalescedDeltaX;
203 deltaY += eventCoalescedDeltaY;
205 // Slightly prefer scrolling vertically by applying the = case to deltaY
206 if (fabsf(deltaY) >= fabsf(deltaX))
211 bool shouldStretch = false;
213 PlatformWheelEventPhase momentumPhase = wheelEvent.momentumPhase();
215 // If we are starting momentum scrolling then do some setup.
216 if (!m_momentumScrollInProgress && (momentumPhase == PlatformWheelEventPhaseBegan || momentumPhase == PlatformWheelEventPhaseChanged))
217 m_momentumScrollInProgress = true;
219 CFTimeInterval timeDelta = wheelEvent.timestamp() - m_lastMomentumScrollTimestamp;
220 if (m_inScrollGesture || m_momentumScrollInProgress) {
221 if (m_lastMomentumScrollTimestamp && timeDelta > 0 && timeDelta < scrollVelocityZeroingTimeout) {
222 m_momentumVelocity.setWidth(eventCoalescedDeltaX / (float)timeDelta);
223 m_momentumVelocity.setHeight(eventCoalescedDeltaY / (float)timeDelta);
224 m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
226 m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
227 m_momentumVelocity = FloatSize();
230 if (isVerticallyStretched) {
231 if (!isHorizontallyStretched && m_client.pinnedInDirection(FloatSize(deltaX, 0))) {
232 // Stretching only in the vertical.
233 if (deltaY && (fabsf(deltaX / deltaY) < rubberbandDirectionLockStretchRatio))
235 else if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
236 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
239 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
241 } else if (isHorizontallyStretched) {
242 // Stretching only in the horizontal.
243 if (m_client.pinnedInDirection(FloatSize(0, deltaY))) {
244 if (deltaX && (fabsf(deltaY / deltaX) < rubberbandDirectionLockStretchRatio))
246 else if (fabsf(deltaY) < rubberbandMinimumRequiredDeltaBeforeStretch) {
247 m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
250 m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
253 // Not stretching at all yet.
254 if (m_client.pinnedInDirection(FloatSize(deltaX, deltaY))) {
255 if (fabsf(deltaY) >= fabsf(deltaX)) {
256 if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
257 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
260 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
262 shouldStretch = true;
267 if (deltaX || deltaY) {
268 if (!(shouldStretch || isVerticallyStretched || isHorizontallyStretched)) {
270 deltaY *= scrollWheelMultiplier();
271 m_client.immediateScrollBy(FloatSize(0, deltaY));
274 deltaX *= scrollWheelMultiplier();
275 m_client.immediateScrollBy(FloatSize(deltaX, 0));
278 if (!m_client.allowsHorizontalStretching(wheelEvent)) {
280 eventCoalescedDeltaX = 0;
281 } else if (deltaX && !isHorizontallyStretched && !m_client.pinnedInDirection(FloatSize(deltaX, 0))) {
282 deltaX *= scrollWheelMultiplier();
284 m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(deltaX, 0));
288 if (!m_client.allowsVerticalStretching(wheelEvent)) {
290 eventCoalescedDeltaY = 0;
291 } else if (deltaY && !isVerticallyStretched && !m_client.pinnedInDirection(FloatSize(0, deltaY))) {
292 deltaY *= scrollWheelMultiplier();
294 m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(0, deltaY));
298 IntSize stretchAmount = m_client.stretchAmount();
300 if (m_momentumScrollInProgress) {
301 if ((m_client.pinnedInDirection(FloatSize(eventCoalescedDeltaX, eventCoalescedDeltaY)) || (fabsf(eventCoalescedDeltaX) + fabsf(eventCoalescedDeltaY) <= 0)) && m_lastMomentumScrollTimestamp) {
302 m_ignoreMomentumScrolls = true;
303 m_momentumScrollInProgress = false;
308 m_stretchScrollForce.setWidth(m_stretchScrollForce.width() + deltaX);
309 m_stretchScrollForce.setHeight(m_stretchScrollForce.height() + deltaY);
311 FloatSize dampedDelta(ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.width())), ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.height())));
313 m_client.immediateScrollByWithoutContentEdgeConstraints(dampedDelta - stretchAmount);
317 if (m_momentumScrollInProgress && momentumPhase == PlatformWheelEventPhaseEnded) {
318 m_momentumScrollInProgress = false;
319 m_ignoreMomentumScrolls = false;
320 m_lastMomentumScrollTimestamp = 0;
326 static inline float roundTowardZero(float num)
328 return num > 0 ? ceilf(num - 0.5f) : floorf(num + 0.5f);
331 static inline float roundToDevicePixelTowardZero(float num)
333 float roundedNum = roundf(num);
334 if (fabs(num - roundedNum) < 0.125)
337 return roundTowardZero(num);
340 void ScrollController::snapRubberBandTimerFired()
342 if (!m_momentumScrollInProgress || m_ignoreMomentumScrolls) {
343 CFTimeInterval timeDelta = [NSDate timeIntervalSinceReferenceDate] - m_startTime;
345 if (m_startStretch == FloatSize()) {
346 m_startStretch = m_client.stretchAmount();
347 if (m_startStretch == FloatSize()) {
348 stopSnapRubberbandTimer();
350 m_stretchScrollForce = FloatSize();
352 m_startStretch = FloatSize();
353 m_origOrigin = FloatPoint();
354 m_origVelocity = FloatSize();
358 m_origOrigin = m_client.absoluteScrollPosition() - m_startStretch;
359 m_origVelocity = m_momentumVelocity;
361 // Just like normal scrolling, prefer vertical rubberbanding
362 if (fabsf(m_origVelocity.height()) >= fabsf(m_origVelocity.width()))
363 m_origVelocity.setWidth(0);
365 // Don't rubber-band horizontally if it's not possible to scroll horizontally
366 if (!m_client.canScrollHorizontally())
367 m_origVelocity.setWidth(0);
369 // Don't rubber-band vertically if it's not possible to scroll vertically
370 if (!m_client.canScrollVertically())
371 m_origVelocity.setHeight(0);
374 FloatPoint delta(roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.width(), -m_origVelocity.width(), (float)timeDelta)),
375 roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.height(), -m_origVelocity.height(), (float)timeDelta)));
377 if (fabs(delta.x()) >= 1 || fabs(delta.y()) >= 1) {
378 m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(delta.x(), delta.y()) - m_client.stretchAmount());
380 FloatSize newStretch = m_client.stretchAmount();
382 m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(newStretch.width()));
383 m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(newStretch.height()));
385 m_client.adjustScrollPositionToBoundsIfNecessary();
387 stopSnapRubberbandTimer();
388 m_stretchScrollForce = FloatSize();
390 m_startStretch = FloatSize();
391 m_origOrigin = FloatPoint();
392 m_origVelocity = FloatSize();
395 m_startTime = [NSDate timeIntervalSinceReferenceDate];
396 m_startStretch = FloatSize();
397 if (!isRubberBandInProgress())
398 stopSnapRubberbandTimer();
402 bool ScrollController::isRubberBandInProgress() const
404 if (!m_inScrollGesture && !m_momentumScrollInProgress && !m_snapRubberbandTimerIsActive)
407 return !m_client.stretchAmount().isZero();
410 void ScrollController::startSnapRubberbandTimer()
412 m_client.startSnapRubberbandTimer();
413 m_snapRubberbandTimer.startRepeating(1.0 / 60.0);
416 void ScrollController::stopSnapRubberbandTimer()
418 m_client.stopSnapRubberbandTimer();
419 m_snapRubberbandTimer.stop();
420 m_snapRubberbandTimerIsActive = false;
423 void ScrollController::snapRubberBand()
425 CFTimeInterval timeDelta = systemUptime() - m_lastMomentumScrollTimestamp;
426 if (m_lastMomentumScrollTimestamp && timeDelta >= scrollVelocityZeroingTimeout)
427 m_momentumVelocity = FloatSize();
429 m_inScrollGesture = false;
431 if (m_snapRubberbandTimerIsActive)
434 m_startTime = [NSDate timeIntervalSinceReferenceDate];
435 m_startStretch = FloatSize();
436 m_origOrigin = FloatPoint();
437 m_origVelocity = FloatSize();
439 startSnapRubberbandTimer();
440 m_snapRubberbandTimerIsActive = true;
443 bool ScrollController::shouldRubberBandInHorizontalDirection(const PlatformWheelEvent& wheelEvent)
445 if (wheelEvent.deltaX() > 0)
446 return m_client.shouldRubberBandInDirection(ScrollLeft);
447 if (wheelEvent.deltaX() < 0)
448 return m_client.shouldRubberBandInDirection(ScrollRight);
453 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
454 ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis)
456 ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
457 ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
459 return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
462 const ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis) const
464 ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
465 ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
467 return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
470 static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase, PlatformWheelEventPhase momentumPhase)
472 if (phase == PlatformWheelEventPhaseNone) {
473 switch (momentumPhase) {
474 case PlatformWheelEventPhaseBegan:
475 return WheelEventStatus::InertialScrollBegin;
477 case PlatformWheelEventPhaseChanged:
478 return WheelEventStatus::InertialScrolling;
480 case PlatformWheelEventPhaseEnded:
481 return WheelEventStatus::InertialScrollEnd;
483 case PlatformWheelEventPhaseNone:
484 return WheelEventStatus::StatelessScrollEvent;
487 return WheelEventStatus::Unknown;
490 if (momentumPhase == PlatformWheelEventPhaseNone) {
492 case PlatformWheelEventPhaseBegan:
493 case PlatformWheelEventPhaseMayBegin:
494 return WheelEventStatus::UserScrollBegin;
496 case PlatformWheelEventPhaseChanged:
497 return WheelEventStatus::UserScrolling;
499 case PlatformWheelEventPhaseEnded:
500 case PlatformWheelEventPhaseCancelled:
501 return WheelEventStatus::UserScrollEnd;
504 return WheelEventStatus::Unknown;
507 return WheelEventStatus::Unknown;
510 void ScrollController::processWheelEventForScrollSnapOnAxis(ScrollEventAxis axis, const PlatformWheelEvent& event)
512 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
514 float wheelDelta = axis == ScrollEventAxis::Horizontal ? -event.deltaX() : -event.deltaY();
515 WheelEventStatus wheelStatus = toWheelEventStatus(event.phase(), event.momentumPhase());
517 switch (wheelStatus) {
518 case WheelEventStatus::UserScrollBegin:
519 case WheelEventStatus::UserScrolling:
520 endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
523 case WheelEventStatus::UserScrollEnd:
524 beginScrollSnapAnimation(axis, ScrollSnapState::Snapping);
527 case WheelEventStatus::InertialScrollBegin:
528 // Begin tracking wheel deltas for glide prediction.
529 endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
530 snapState.pushInitialWheelDelta(wheelDelta);
531 snapState.m_beginTrackingWheelDeltaOffset = m_client.scrollOffsetOnAxis(axis);
534 case WheelEventStatus::InertialScrolling:
535 // This check for DestinationReached ensures that we don't receive another set of momentum events after ending the last glide.
536 if (snapState.m_currentState != ScrollSnapState::Gliding && snapState.m_currentState != ScrollSnapState::DestinationReached) {
537 if (snapState.m_numWheelDeltasTracked < snapState.wheelDeltaWindowSize)
538 snapState.pushInitialWheelDelta(wheelDelta);
540 if (snapState.m_numWheelDeltasTracked == snapState.wheelDeltaWindowSize)
541 beginScrollSnapAnimation(axis, ScrollSnapState::Gliding);
545 case WheelEventStatus::InertialScrollEnd:
546 snapState.clearInitialWheelDeltaWindow();
547 snapState.m_shouldOverrideWheelEvent = false;
550 case WheelEventStatus::StatelessScrollEvent:
551 endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
552 snapState.clearInitialWheelDeltaWindow();
553 snapState.m_shouldOverrideWheelEvent = false;
556 case WheelEventStatus::Unknown:
557 ASSERT_NOT_REACHED();
562 bool ScrollController::shouldOverrideWheelEvent(ScrollEventAxis axis, const PlatformWheelEvent& event) const
564 const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
566 return snapState.m_shouldOverrideWheelEvent && toWheelEventStatus(event.phase(), event.momentumPhase()) == WheelEventStatus::InertialScrolling;
569 bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent& wheelEvent)
571 if (m_verticalScrollSnapState) {
572 processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Vertical, wheelEvent);
573 if (shouldOverrideWheelEvent(ScrollEventAxis::Vertical, wheelEvent))
576 if (m_horizontalScrollSnapState) {
577 processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Horizontal, wheelEvent);
578 if (shouldOverrideWheelEvent(ScrollEventAxis::Horizontal, wheelEvent))
585 void ScrollController::updateScrollAnimatorsAndTimers(const ScrollableArea& scrollableArea)
587 // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
588 if (scrollableArea.horizontalSnapOffsets())
589 m_horizontalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, *scrollableArea.horizontalSnapOffsets());
590 else if (m_horizontalScrollSnapState)
591 m_horizontalScrollSnapState = nullptr;
593 if (scrollableArea.verticalSnapOffsets())
594 m_verticalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, *scrollableArea.verticalSnapOffsets());
595 else if (m_verticalScrollSnapState)
596 m_verticalScrollSnapState = nullptr;
599 void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector<LayoutUnit>& snapPoints)
601 // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
602 if (axis == ScrollEventAxis::Horizontal)
603 m_horizontalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, snapPoints) : nullptr;
605 if (axis == ScrollEventAxis::Vertical)
606 m_verticalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, snapPoints) : nullptr;
609 void ScrollController::startScrollSnapTimer(ScrollEventAxis axis)
611 RunLoop::Timer<ScrollController>& scrollSnapTimer = axis == ScrollEventAxis::Horizontal ? m_horizontalScrollSnapTimer : m_verticalScrollSnapTimer;
612 if (!scrollSnapTimer.isActive()) {
613 m_client.startScrollSnapTimer(axis);
614 scrollSnapTimer.startRepeating(1.0 / 60.0);
618 void ScrollController::stopScrollSnapTimer(ScrollEventAxis axis)
620 m_client.stopScrollSnapTimer(axis);
621 RunLoop::Timer<ScrollController>& scrollSnapTimer = axis == ScrollEventAxis::Horizontal ? m_horizontalScrollSnapTimer : m_verticalScrollSnapTimer;
622 scrollSnapTimer.stop();
625 void ScrollController::horizontalScrollSnapTimerFired()
627 scrollSnapAnimationUpdate(ScrollEventAxis::Horizontal);
630 void ScrollController::verticalScrollSnapTimerFired()
632 scrollSnapAnimationUpdate(ScrollEventAxis::Vertical);
635 void ScrollController::scrollSnapAnimationUpdate(ScrollEventAxis axis)
637 if (axis == ScrollEventAxis::Horizontal && !m_horizontalScrollSnapState)
640 if (axis == ScrollEventAxis::Vertical && !m_verticalScrollSnapState)
643 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
644 if (snapState.m_currentState == ScrollSnapState::DestinationReached)
647 ASSERT(snapState.m_currentState == ScrollSnapState::Gliding || snapState.m_currentState == ScrollSnapState::Snapping);
648 float delta = snapState.m_currentState == ScrollSnapState::Snapping ? computeSnapDelta(axis) : computeGlideDelta(axis);
650 m_client.immediateScrollOnAxis(axis, delta);
652 endScrollSnapAnimation(axis, ScrollSnapState::DestinationReached);
655 static inline float projectedInertialScrollDistance(float initialWheelDelta)
657 // FIXME: Experiments with inertial scrolling show a fairly consistent linear relationship between initial wheel delta and total distance scrolled.
658 // In the future, we'll want to find a more accurate way of inertial scroll prediction.
659 return inertialScrollPredictionFactor * initialWheelDelta;
662 void ScrollController::initializeGlideParameters(ScrollEventAxis axis, bool shouldIncreaseInitialWheelDelta)
664 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
666 // FIXME: Glide boost is a hacky way to speed up natural scrolling velocity. We should find a better way to accomplish this.
667 if (shouldIncreaseInitialWheelDelta)
668 snapState.m_glideInitialWheelDelta *= glideBoostMultiplier;
670 // FIXME: There must be a better way to determine a good target delta than multiplying by a factor and clamping to min/max values.
671 float targetFinalWheelDelta = initialToFinalMomentumFactor * (snapState.m_glideInitialWheelDelta < 0 ? -snapState.m_glideInitialWheelDelta : snapState.m_glideInitialWheelDelta);
672 targetFinalWheelDelta = (snapState.m_glideInitialWheelDelta > 0 ? 1 : -1) * std::min(std::max(targetFinalWheelDelta, minTargetWheelDelta), maxTargetWheelDelta);
673 snapState.m_glideMagnitude = (snapState.m_glideInitialWheelDelta + targetFinalWheelDelta) / 2;
674 snapState.m_glidePhaseShift = acos((snapState.m_glideInitialWheelDelta - targetFinalWheelDelta) / (snapState.m_glideInitialWheelDelta + targetFinalWheelDelta));
677 void ScrollController::beginScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
679 ASSERT(newState == ScrollSnapState::Gliding || newState == ScrollSnapState::Snapping);
681 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
683 LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
684 float initialWheelDelta = newState == ScrollSnapState::Gliding ? snapState.averageInitialWheelDelta() : 0;
685 LayoutUnit projectedScrollDestination = newState == ScrollSnapState::Gliding ? snapState.m_beginTrackingWheelDeltaOffset + LayoutUnit(projectedInertialScrollDistance(initialWheelDelta)) : offset;
686 if (snapState.m_snapOffsets.isEmpty())
689 float scaleFactor = m_client.pageScaleFactor();
691 projectedScrollDestination = std::min(std::max(LayoutUnit(projectedScrollDestination / scaleFactor), snapState.m_snapOffsets.first()), snapState.m_snapOffsets.last());
692 snapState.m_initialOffset = offset;
693 snapState.m_targetOffset = scaleFactor * closestSnapOffset<LayoutUnit, float>(snapState.m_snapOffsets, projectedScrollDestination, initialWheelDelta);
694 if (snapState.m_initialOffset == snapState.m_targetOffset)
697 snapState.m_currentState = newState;
698 if (newState == ScrollSnapState::Gliding) {
699 snapState.m_shouldOverrideWheelEvent = true;
700 snapState.m_glideInitialWheelDelta = initialWheelDelta;
701 bool glideRequiresBoost;
702 if (initialWheelDelta > 0)
703 glideRequiresBoost = projectedScrollDestination - offset < snapState.m_targetOffset - projectedScrollDestination;
705 glideRequiresBoost = offset - projectedScrollDestination < projectedScrollDestination - snapState.m_targetOffset;
707 initializeGlideParameters(axis, glideRequiresBoost);
708 snapState.clearInitialWheelDeltaWindow();
710 startScrollSnapTimer(axis);
713 void ScrollController::endScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
715 ASSERT(newState == ScrollSnapState::DestinationReached || newState == ScrollSnapState::UserInteraction);
717 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
719 if (snapState.m_currentState == ScrollSnapState::Gliding)
720 snapState.clearInitialWheelDeltaWindow();
722 snapState.m_currentState = newState;
723 stopScrollSnapTimer(axis);
726 static inline float snapProgress(const LayoutUnit& offset, const ScrollSnapAnimatorState& snapState)
728 const float distanceTraveled = static_cast<float>(offset - snapState.m_initialOffset);
729 const float totalDistance = static_cast<float>(snapState.m_targetOffset - snapState.m_initialOffset);
731 return distanceTraveled / totalDistance;
734 static inline float clampedSnapMagnitude(float thresholdedDistance)
736 return snapMagnitudeMin + (snapMagnitudeMax - snapMagnitudeMin) * (thresholdedDistance - snapThresholdLow) / (snapThresholdHigh - snapThresholdLow);
739 // Computes the amount to scroll by when performing a "snap" operation, i.e. when a user releases the trackpad without flicking. The snap delta
740 // is a function of progress t, where t is equal to DISTANCE_TRAVELED / TOTAL_DISTANCE, DISTANCE_TRAVELED is the distance from the initialOffset
741 // to the current offset, and TOTAL_DISTANCE is the distance from initialOffset to targetOffset. The snapping equation is as follows:
742 // delta(t) = MAGNITUDE * sin(PI * t). MAGNITUDE indicates the top speed reached near the middle of the animation (t = 0.5), and is a linear
743 // relationship of the distance traveled, clamped by arbitrary min and max values.
744 float ScrollController::computeSnapDelta(ScrollEventAxis axis) const
746 const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
748 LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
749 bool canComputeSnap = (snapState.m_initialOffset <= offset && offset < snapState.m_targetOffset) || (snapState.m_targetOffset < offset && offset <= snapState.m_initialOffset);
750 if (snapState.m_currentState != ScrollSnapState::Snapping || !canComputeSnap)
753 float progress = snapProgress(offset, snapState);
755 // Threshold the distance before computing magnitude, so only distances within a certain range are considered.
756 int sign = snapState.m_initialOffset < snapState.m_targetOffset ? 1 : -1;
757 float thresholdedDistance = std::min(std::max<float>((snapState.m_targetOffset - snapState.m_initialOffset) * sign, snapThresholdLow), snapThresholdHigh);
759 float magnitude = clampedSnapMagnitude(thresholdedDistance);
761 float rawSnapDelta = std::max<float>(1, magnitude * std::sin(piFloat * progress));
762 if ((snapState.m_targetOffset < offset && offset - rawSnapDelta < snapState.m_targetOffset) || (snapState.m_targetOffset > offset && offset + rawSnapDelta > snapState.m_targetOffset))
763 return snapState.m_targetOffset - offset;
765 return sign * rawSnapDelta;
768 static inline float snapGlide(float progress, const ScrollSnapAnimatorState& snapState)
770 // FIXME: We might want to investigate why -m_glidePhaseShift results in the behavior we want.
771 return ceil(snapState.m_glideMagnitude * (1.0f + std::cos(piFloat * progress - snapState.m_glidePhaseShift)));
774 // Computes the amount to scroll by when performing a "glide" operation, i.e. when a user releases the trackpad with an initial velocity. Here,
775 // we want the scroll offset to animate directly to the snap point.
777 // The snap delta is a function of progress t, where: (1) t is equal to DISTANCE_TRAVELED / TOTAL_DISTANCE, (2) DISTANCE_TRAVELED is the distance
778 // from the initialOffset to the current offset, and (3) TOTAL_DISTANCE is the distance from initialOffset to targetOffset.
780 // The general model of our gliding equation is delta(t) = MAGNITUDE * (1 + cos(PI * t + PHASE_SHIFT)). This was determined after examining the
781 // momentum velocity curve as a function of progress. To compute MAGNITUDE and PHASE_SHIFT, we use initial velocity V0 and the final velocity VF,
782 // both as wheel deltas (pixels per timestep). VF should be a small value (< 10) chosen based on the initial velocity and TOTAL_DISTANCE.
783 // We also enforce the following constraints for the gliding equation:
784 // 1. delta(0) = V0, since we want the initial velocity of the gliding animation to match the user's scroll velocity. The exception to this is
785 // when the glide velocity is not enough to naturally reach the next snap point, and thus requires a boost (see initializeGlideParameters)
786 // 2. delta(1) = VF, since at t=1, the animation has completed and we want the last wheel delta to match the final velocity VF. Note that this
787 // doesn't guarantee that the final velocity will be exactly VF. However, assuming that the initial velocity is much less than TOTAL_DISTANCE,
788 // the last wheel delta will be very close, if not the same, as VF.
789 // For MAGNITUDE = (V0 + VF) / 2 and PHASE_SHIFT = arccos((V0 - VF) / (V0 + VF)), observe that delta(0) and delta(1) evaluate respectively to V0
790 // and VF. Thus, we can express our gliding equation all in terms of V0, VF and t.
791 float ScrollController::computeGlideDelta(ScrollEventAxis axis) const
793 const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
795 LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
796 bool canComputeGlide = (snapState.m_initialOffset <= offset && offset < snapState.m_targetOffset) || (snapState.m_targetOffset < offset && offset <= snapState.m_initialOffset);
797 if (snapState.m_currentState != ScrollSnapState::Gliding || !canComputeGlide)
800 const float progress = snapProgress(offset, snapState);
801 const float rawGlideDelta = snapGlide(progress, snapState);
803 float glideDelta = snapState.m_initialOffset < snapState.m_targetOffset ? std::max<float>(rawGlideDelta, 1) : std::min<float>(rawGlideDelta, -1);
804 if ((snapState.m_initialOffset < snapState.m_targetOffset && offset + glideDelta > snapState.m_targetOffset) || (snapState.m_initialOffset > snapState.m_targetOffset && offset + glideDelta < snapState.m_targetOffset))
805 return snapState.m_targetOffset - offset;
811 } // namespace WebCore
813 #endif // ENABLE(RUBBER_BANDING)