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 "WheelEventTestTrigger.h"
32 #include <sys/sysctl.h>
35 #if ENABLE(CSS_SCROLL_SNAP)
36 #include "ScrollSnapAnimatorState.h"
37 #include "ScrollableArea.h"
40 #if ENABLE(RUBBER_BANDING)
42 static NSTimeInterval systemUptime()
44 if ([[NSProcessInfo processInfo] respondsToSelector:@selector(systemUptime)])
45 return [[NSProcessInfo processInfo] systemUptime];
47 // Get how long system has been up. Found by looking getting "boottime" from the kernel.
48 static struct timeval boottime = {0, 0};
49 if (!boottime.tv_sec) {
50 int mib[2] = {CTL_KERN, KERN_BOOTTIME};
51 size_t size = sizeof(boottime);
52 if (-1 == sysctl(mib, 2, &boottime, &size, 0, 0))
56 if (boottime.tv_sec && -1 != gettimeofday(&now, 0)) {
57 struct timeval uptime;
58 timersub(&now, &boottime, &uptime);
59 NSTimeInterval result = uptime.tv_sec + (uptime.tv_usec / 1E+6);
68 static const float scrollVelocityZeroingTimeout = 0.10f;
69 static const float rubberbandDirectionLockStretchRatio = 1;
70 static const float rubberbandMinimumRequiredDeltaBeforeStretch = 10;
72 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
73 static const float snapMagnitudeMax = 25;
74 static const float snapMagnitudeMin = 5;
75 static const float snapThresholdHigh = 1000;
76 static const float snapThresholdLow = 50;
78 static const float inertialScrollPredictionFactor = 16.7;
79 static const float initialToFinalMomentumFactor = 1.0 / 40.0;
81 static const float glideBoostMultiplier = 3.5;
83 static const float maxTargetWheelDelta = 7;
84 static const float minTargetWheelDelta = 3.5;
87 enum class WheelEventStatus {
98 static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
100 return wkNSElasticDeltaForTimeDelta(initialPosition, initialVelocity, elapsedTime);
103 static float elasticDeltaForReboundDelta(float delta)
105 return wkNSElasticDeltaForReboundDelta(delta);
108 static float reboundDeltaForElasticDelta(float delta)
110 return wkNSReboundDeltaForElasticDelta(delta);
113 static float scrollWheelMultiplier()
115 static float multiplier = -1;
116 if (multiplier < 0) {
117 multiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"NSScrollWheelMultiplier"];
124 ScrollController::ScrollController(ScrollControllerClient& client)
126 , m_lastMomentumScrollTimestamp(0)
128 , m_snapRubberbandTimer(RunLoop::current(), this, &ScrollController::snapRubberBandTimerFired)
129 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
130 , m_horizontalScrollSnapTimer(RunLoop::current(), this, &ScrollController::horizontalScrollSnapTimerFired)
131 , m_verticalScrollSnapTimer(RunLoop::current(), this, &ScrollController::verticalScrollSnapTimerFired)
133 , m_inScrollGesture(false)
134 , m_momentumScrollInProgress(false)
135 , m_ignoreMomentumScrolls(false)
136 , m_snapRubberbandTimerIsActive(false)
140 bool ScrollController::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
142 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
143 if (!processWheelEventForScrollSnap(wheelEvent))
146 if (wheelEvent.phase() == PlatformWheelEventPhaseBegan) {
147 // First, check if we should rubber-band at all.
148 if (m_client.pinnedInDirection(FloatSize(-wheelEvent.deltaX(), 0))
149 && !shouldRubberBandInHorizontalDirection(wheelEvent))
152 m_inScrollGesture = true;
153 m_momentumScrollInProgress = false;
154 m_ignoreMomentumScrolls = false;
155 m_lastMomentumScrollTimestamp = 0;
156 m_momentumVelocity = FloatSize();
158 IntSize stretchAmount = m_client.stretchAmount();
159 m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(stretchAmount.width()));
160 m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(stretchAmount.height()));
161 m_overflowScrollDelta = FloatSize();
163 stopSnapRubberbandTimer();
168 if (wheelEvent.phase() == PlatformWheelEventPhaseEnded) {
173 bool isMomentumScrollEvent = (wheelEvent.momentumPhase() != PlatformWheelEventPhaseNone);
174 if (m_ignoreMomentumScrolls && (isMomentumScrollEvent || m_snapRubberbandTimerIsActive)) {
175 if (wheelEvent.momentumPhase() == PlatformWheelEventPhaseEnded) {
176 m_ignoreMomentumScrolls = false;
182 float deltaX = m_overflowScrollDelta.width();
183 float deltaY = m_overflowScrollDelta.height();
185 // Reset overflow values because we may decide to remove delta at various points and put it into overflow.
186 m_overflowScrollDelta = FloatSize();
188 IntSize stretchAmount = m_client.stretchAmount();
189 bool isVerticallyStretched = stretchAmount.height();
190 bool isHorizontallyStretched = stretchAmount.width();
192 float eventCoalescedDeltaX;
193 float eventCoalescedDeltaY;
195 if (isVerticallyStretched || isHorizontallyStretched) {
196 eventCoalescedDeltaX = -wheelEvent.unacceleratedScrollingDeltaX();
197 eventCoalescedDeltaY = -wheelEvent.unacceleratedScrollingDeltaY();
199 eventCoalescedDeltaX = -wheelEvent.deltaX();
200 eventCoalescedDeltaY = -wheelEvent.deltaY();
203 deltaX += eventCoalescedDeltaX;
204 deltaY += eventCoalescedDeltaY;
206 // Slightly prefer scrolling vertically by applying the = case to deltaY
207 if (fabsf(deltaY) >= fabsf(deltaX))
212 bool shouldStretch = false;
214 PlatformWheelEventPhase momentumPhase = wheelEvent.momentumPhase();
216 // If we are starting momentum scrolling then do some setup.
217 if (!m_momentumScrollInProgress && (momentumPhase == PlatformWheelEventPhaseBegan || momentumPhase == PlatformWheelEventPhaseChanged))
218 m_momentumScrollInProgress = true;
220 CFTimeInterval timeDelta = wheelEvent.timestamp() - m_lastMomentumScrollTimestamp;
221 if (m_inScrollGesture || m_momentumScrollInProgress) {
222 if (m_lastMomentumScrollTimestamp && timeDelta > 0 && timeDelta < scrollVelocityZeroingTimeout) {
223 m_momentumVelocity.setWidth(eventCoalescedDeltaX / (float)timeDelta);
224 m_momentumVelocity.setHeight(eventCoalescedDeltaY / (float)timeDelta);
225 m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
227 m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
228 m_momentumVelocity = FloatSize();
231 if (isVerticallyStretched) {
232 if (!isHorizontallyStretched && m_client.pinnedInDirection(FloatSize(deltaX, 0))) {
233 // Stretching only in the vertical.
234 if (deltaY && (fabsf(deltaX / deltaY) < rubberbandDirectionLockStretchRatio))
236 else if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
237 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
240 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
242 } else if (isHorizontallyStretched) {
243 // Stretching only in the horizontal.
244 if (m_client.pinnedInDirection(FloatSize(0, deltaY))) {
245 if (deltaX && (fabsf(deltaY / deltaX) < rubberbandDirectionLockStretchRatio))
247 else if (fabsf(deltaY) < rubberbandMinimumRequiredDeltaBeforeStretch) {
248 m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
251 m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
254 // Not stretching at all yet.
255 if (m_client.pinnedInDirection(FloatSize(deltaX, deltaY))) {
256 if (fabsf(deltaY) >= fabsf(deltaX)) {
257 if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
258 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
261 m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
263 shouldStretch = true;
268 if (deltaX || deltaY) {
269 if (!(shouldStretch || isVerticallyStretched || isHorizontallyStretched)) {
271 deltaY *= scrollWheelMultiplier();
272 m_client.immediateScrollBy(FloatSize(0, deltaY));
275 deltaX *= scrollWheelMultiplier();
276 m_client.immediateScrollBy(FloatSize(deltaX, 0));
279 if (!m_client.allowsHorizontalStretching(wheelEvent)) {
281 eventCoalescedDeltaX = 0;
282 } else if (deltaX && !isHorizontallyStretched && !m_client.pinnedInDirection(FloatSize(deltaX, 0))) {
283 deltaX *= scrollWheelMultiplier();
285 m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(deltaX, 0));
289 if (!m_client.allowsVerticalStretching(wheelEvent)) {
291 eventCoalescedDeltaY = 0;
292 } else if (deltaY && !isVerticallyStretched && !m_client.pinnedInDirection(FloatSize(0, deltaY))) {
293 deltaY *= scrollWheelMultiplier();
295 m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(0, deltaY));
299 IntSize stretchAmount = m_client.stretchAmount();
301 if (m_momentumScrollInProgress) {
302 if ((m_client.pinnedInDirection(FloatSize(eventCoalescedDeltaX, eventCoalescedDeltaY)) || (fabsf(eventCoalescedDeltaX) + fabsf(eventCoalescedDeltaY) <= 0)) && m_lastMomentumScrollTimestamp) {
303 m_ignoreMomentumScrolls = true;
304 m_momentumScrollInProgress = false;
309 m_stretchScrollForce.setWidth(m_stretchScrollForce.width() + deltaX);
310 m_stretchScrollForce.setHeight(m_stretchScrollForce.height() + deltaY);
312 FloatSize dampedDelta(ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.width())), ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.height())));
314 m_client.immediateScrollByWithoutContentEdgeConstraints(dampedDelta - stretchAmount);
318 if (m_momentumScrollInProgress && momentumPhase == PlatformWheelEventPhaseEnded) {
319 m_momentumScrollInProgress = false;
320 m_ignoreMomentumScrolls = false;
321 m_lastMomentumScrollTimestamp = 0;
327 static inline float roundTowardZero(float num)
329 return num > 0 ? ceilf(num - 0.5f) : floorf(num + 0.5f);
332 static inline float roundToDevicePixelTowardZero(float num)
334 float roundedNum = roundf(num);
335 if (fabs(num - roundedNum) < 0.125)
338 return roundTowardZero(num);
341 void ScrollController::snapRubberBandTimerFired()
343 if (!m_momentumScrollInProgress || m_ignoreMomentumScrolls) {
344 CFTimeInterval timeDelta = [NSDate timeIntervalSinceReferenceDate] - m_startTime;
346 if (m_startStretch == FloatSize()) {
347 m_startStretch = m_client.stretchAmount();
348 if (m_startStretch == FloatSize()) {
349 stopSnapRubberbandTimer();
351 m_stretchScrollForce = FloatSize();
353 m_startStretch = FloatSize();
354 m_origOrigin = FloatPoint();
355 m_origVelocity = FloatSize();
359 m_origOrigin = m_client.absoluteScrollPosition() - m_startStretch;
360 m_origVelocity = m_momentumVelocity;
362 // Just like normal scrolling, prefer vertical rubberbanding
363 if (fabsf(m_origVelocity.height()) >= fabsf(m_origVelocity.width()))
364 m_origVelocity.setWidth(0);
366 // Don't rubber-band horizontally if it's not possible to scroll horizontally
367 if (!m_client.canScrollHorizontally())
368 m_origVelocity.setWidth(0);
370 // Don't rubber-band vertically if it's not possible to scroll vertically
371 if (!m_client.canScrollVertically())
372 m_origVelocity.setHeight(0);
375 FloatPoint delta(roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.width(), -m_origVelocity.width(), (float)timeDelta)),
376 roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.height(), -m_origVelocity.height(), (float)timeDelta)));
378 if (fabs(delta.x()) >= 1 || fabs(delta.y()) >= 1) {
379 m_client.immediateScrollByWithoutContentEdgeConstraints(FloatSize(delta.x(), delta.y()) - m_client.stretchAmount());
381 FloatSize newStretch = m_client.stretchAmount();
383 m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(newStretch.width()));
384 m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(newStretch.height()));
386 m_client.adjustScrollPositionToBoundsIfNecessary();
388 stopSnapRubberbandTimer();
389 m_stretchScrollForce = FloatSize();
391 m_startStretch = FloatSize();
392 m_origOrigin = FloatPoint();
393 m_origVelocity = FloatSize();
396 m_startTime = [NSDate timeIntervalSinceReferenceDate];
397 m_startStretch = FloatSize();
398 if (!isRubberBandInProgress())
399 stopSnapRubberbandTimer();
403 bool ScrollController::isRubberBandInProgress() const
405 if (!m_inScrollGesture && !m_momentumScrollInProgress && !m_snapRubberbandTimerIsActive)
408 return !m_client.stretchAmount().isZero();
411 void ScrollController::startSnapRubberbandTimer()
413 m_client.startSnapRubberbandTimer();
414 m_snapRubberbandTimer.startRepeating(1.0 / 60.0);
416 if (auto* trigger = m_client.testTrigger())
417 trigger->deferTestsForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::RubberbandInProgress);
420 void ScrollController::stopSnapRubberbandTimer()
422 m_client.stopSnapRubberbandTimer();
423 m_snapRubberbandTimer.stop();
424 m_snapRubberbandTimerIsActive = false;
426 if (auto* trigger = m_client.testTrigger())
427 trigger->removeTestDeferralForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::RubberbandInProgress);
430 void ScrollController::snapRubberBand()
432 CFTimeInterval timeDelta = systemUptime() - m_lastMomentumScrollTimestamp;
433 if (m_lastMomentumScrollTimestamp && timeDelta >= scrollVelocityZeroingTimeout)
434 m_momentumVelocity = FloatSize();
436 m_inScrollGesture = false;
438 if (m_snapRubberbandTimerIsActive)
441 m_startTime = [NSDate timeIntervalSinceReferenceDate];
442 m_startStretch = FloatSize();
443 m_origOrigin = FloatPoint();
444 m_origVelocity = FloatSize();
446 startSnapRubberbandTimer();
447 m_snapRubberbandTimerIsActive = true;
450 bool ScrollController::shouldRubberBandInHorizontalDirection(const PlatformWheelEvent& wheelEvent)
452 if (wheelEvent.deltaX() > 0)
453 return m_client.shouldRubberBandInDirection(ScrollLeft);
454 if (wheelEvent.deltaX() < 0)
455 return m_client.shouldRubberBandInDirection(ScrollRight);
460 #if ENABLE(CSS_SCROLL_SNAP) && PLATFORM(MAC)
461 ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis)
463 ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
464 ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
466 return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
469 const ScrollSnapAnimatorState& ScrollController::scrollSnapPointState(ScrollEventAxis axis) const
471 ASSERT(axis != ScrollEventAxis::Horizontal || m_horizontalScrollSnapState);
472 ASSERT(axis != ScrollEventAxis::Vertical || m_verticalScrollSnapState);
474 return (axis == ScrollEventAxis::Horizontal) ? *m_horizontalScrollSnapState : *m_verticalScrollSnapState;
477 static inline WheelEventStatus toWheelEventStatus(PlatformWheelEventPhase phase, PlatformWheelEventPhase momentumPhase)
479 if (phase == PlatformWheelEventPhaseNone) {
480 switch (momentumPhase) {
481 case PlatformWheelEventPhaseBegan:
482 return WheelEventStatus::InertialScrollBegin;
484 case PlatformWheelEventPhaseChanged:
485 return WheelEventStatus::InertialScrolling;
487 case PlatformWheelEventPhaseEnded:
488 return WheelEventStatus::InertialScrollEnd;
490 case PlatformWheelEventPhaseNone:
491 return WheelEventStatus::StatelessScrollEvent;
494 return WheelEventStatus::Unknown;
497 if (momentumPhase == PlatformWheelEventPhaseNone) {
499 case PlatformWheelEventPhaseBegan:
500 case PlatformWheelEventPhaseMayBegin:
501 return WheelEventStatus::UserScrollBegin;
503 case PlatformWheelEventPhaseChanged:
504 return WheelEventStatus::UserScrolling;
506 case PlatformWheelEventPhaseEnded:
507 case PlatformWheelEventPhaseCancelled:
508 return WheelEventStatus::UserScrollEnd;
511 return WheelEventStatus::Unknown;
514 return WheelEventStatus::Unknown;
517 void ScrollController::processWheelEventForScrollSnapOnAxis(ScrollEventAxis axis, const PlatformWheelEvent& event)
519 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
521 float wheelDelta = axis == ScrollEventAxis::Horizontal ? -event.deltaX() : -event.deltaY();
522 WheelEventStatus wheelStatus = toWheelEventStatus(event.phase(), event.momentumPhase());
524 switch (wheelStatus) {
525 case WheelEventStatus::UserScrollBegin:
526 case WheelEventStatus::UserScrolling:
527 endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
530 case WheelEventStatus::UserScrollEnd:
531 beginScrollSnapAnimation(axis, ScrollSnapState::Snapping);
534 case WheelEventStatus::InertialScrollBegin:
535 // Begin tracking wheel deltas for glide prediction.
536 endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
537 snapState.pushInitialWheelDelta(wheelDelta);
538 snapState.m_beginTrackingWheelDeltaOffset = m_client.scrollOffsetOnAxis(axis);
541 case WheelEventStatus::InertialScrolling:
542 // This check for DestinationReached ensures that we don't receive another set of momentum events after ending the last glide.
543 if (snapState.m_currentState != ScrollSnapState::Gliding && snapState.m_currentState != ScrollSnapState::DestinationReached) {
544 if (snapState.m_numWheelDeltasTracked < snapState.wheelDeltaWindowSize)
545 snapState.pushInitialWheelDelta(wheelDelta);
547 if (snapState.m_numWheelDeltasTracked == snapState.wheelDeltaWindowSize)
548 beginScrollSnapAnimation(axis, ScrollSnapState::Gliding);
552 case WheelEventStatus::InertialScrollEnd:
553 snapState.clearInitialWheelDeltaWindow();
554 snapState.m_shouldOverrideWheelEvent = false;
557 case WheelEventStatus::StatelessScrollEvent:
558 endScrollSnapAnimation(axis, ScrollSnapState::UserInteraction);
559 snapState.clearInitialWheelDeltaWindow();
560 snapState.m_shouldOverrideWheelEvent = false;
563 case WheelEventStatus::Unknown:
564 ASSERT_NOT_REACHED();
569 bool ScrollController::shouldOverrideWheelEvent(ScrollEventAxis axis, const PlatformWheelEvent& event) const
571 const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
573 return snapState.m_shouldOverrideWheelEvent && toWheelEventStatus(event.phase(), event.momentumPhase()) == WheelEventStatus::InertialScrolling;
576 bool ScrollController::processWheelEventForScrollSnap(const PlatformWheelEvent& wheelEvent)
578 if (m_verticalScrollSnapState) {
579 processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Vertical, wheelEvent);
580 if (shouldOverrideWheelEvent(ScrollEventAxis::Vertical, wheelEvent))
583 if (m_horizontalScrollSnapState) {
584 processWheelEventForScrollSnapOnAxis(ScrollEventAxis::Horizontal, wheelEvent);
585 if (shouldOverrideWheelEvent(ScrollEventAxis::Horizontal, wheelEvent))
592 void ScrollController::updateScrollAnimatorsAndTimers(const ScrollableArea& scrollableArea)
594 // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
595 if (scrollableArea.horizontalSnapOffsets())
596 m_horizontalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, *scrollableArea.horizontalSnapOffsets());
597 else if (m_horizontalScrollSnapState)
598 m_horizontalScrollSnapState = nullptr;
600 if (scrollableArea.verticalSnapOffsets())
601 m_verticalScrollSnapState = std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, *scrollableArea.verticalSnapOffsets());
602 else if (m_verticalScrollSnapState)
603 m_verticalScrollSnapState = nullptr;
606 void ScrollController::updateScrollSnapPoints(ScrollEventAxis axis, const Vector<LayoutUnit>& snapPoints)
608 // FIXME: Currently, scroll snap animators are recreated even though the snap offsets alone can be updated.
609 if (axis == ScrollEventAxis::Horizontal)
610 m_horizontalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Horizontal, snapPoints) : nullptr;
612 if (axis == ScrollEventAxis::Vertical)
613 m_verticalScrollSnapState = !snapPoints.isEmpty() ? std::make_unique<ScrollSnapAnimatorState>(ScrollEventAxis::Vertical, snapPoints) : nullptr;
616 void ScrollController::startScrollSnapTimer(ScrollEventAxis axis)
618 RunLoop::Timer<ScrollController>& scrollSnapTimer = axis == ScrollEventAxis::Horizontal ? m_horizontalScrollSnapTimer : m_verticalScrollSnapTimer;
619 if (!scrollSnapTimer.isActive()) {
620 m_client.startScrollSnapTimer(axis);
621 scrollSnapTimer.startRepeating(1.0 / 60.0);
624 if (!m_horizontalScrollSnapTimer.isActive() && !m_verticalScrollSnapTimer.isActive())
627 if (auto* trigger = m_client.testTrigger())
628 trigger->deferTestsForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
631 void ScrollController::stopScrollSnapTimer(ScrollEventAxis axis)
633 m_client.stopScrollSnapTimer(axis);
634 RunLoop::Timer<ScrollController>& scrollSnapTimer = axis == ScrollEventAxis::Horizontal ? m_horizontalScrollSnapTimer : m_verticalScrollSnapTimer;
635 scrollSnapTimer.stop();
637 if (m_horizontalScrollSnapTimer.isActive() || m_verticalScrollSnapTimer.isActive())
640 if (auto* trigger = m_client.testTrigger())
641 trigger->removeTestDeferralForReason(reinterpret_cast<WheelEventTestTrigger::ScrollableAreaIdentifier>(this), WheelEventTestTrigger::ScrollSnapInProgress);
644 void ScrollController::horizontalScrollSnapTimerFired()
646 scrollSnapAnimationUpdate(ScrollEventAxis::Horizontal);
649 void ScrollController::verticalScrollSnapTimerFired()
651 scrollSnapAnimationUpdate(ScrollEventAxis::Vertical);
654 void ScrollController::scrollSnapAnimationUpdate(ScrollEventAxis axis)
656 if (axis == ScrollEventAxis::Horizontal && !m_horizontalScrollSnapState)
659 if (axis == ScrollEventAxis::Vertical && !m_verticalScrollSnapState)
662 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
663 if (snapState.m_currentState == ScrollSnapState::DestinationReached)
666 ASSERT(snapState.m_currentState == ScrollSnapState::Gliding || snapState.m_currentState == ScrollSnapState::Snapping);
667 float delta = snapState.m_currentState == ScrollSnapState::Snapping ? computeSnapDelta(axis) : computeGlideDelta(axis);
669 m_client.immediateScrollOnAxis(axis, delta);
671 endScrollSnapAnimation(axis, ScrollSnapState::DestinationReached);
674 static inline float projectedInertialScrollDistance(float initialWheelDelta)
676 // FIXME: Experiments with inertial scrolling show a fairly consistent linear relationship between initial wheel delta and total distance scrolled.
677 // In the future, we'll want to find a more accurate way of inertial scroll prediction.
678 return inertialScrollPredictionFactor * initialWheelDelta;
681 void ScrollController::initializeGlideParameters(ScrollEventAxis axis, bool shouldIncreaseInitialWheelDelta)
683 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
685 // FIXME: Glide boost is a hacky way to speed up natural scrolling velocity. We should find a better way to accomplish this.
686 if (shouldIncreaseInitialWheelDelta)
687 snapState.m_glideInitialWheelDelta *= glideBoostMultiplier;
689 // FIXME: There must be a better way to determine a good target delta than multiplying by a factor and clamping to min/max values.
690 float targetFinalWheelDelta = initialToFinalMomentumFactor * (snapState.m_glideInitialWheelDelta < 0 ? -snapState.m_glideInitialWheelDelta : snapState.m_glideInitialWheelDelta);
691 targetFinalWheelDelta = (snapState.m_glideInitialWheelDelta > 0 ? 1 : -1) * std::min(std::max(targetFinalWheelDelta, minTargetWheelDelta), maxTargetWheelDelta);
692 snapState.m_glideMagnitude = (snapState.m_glideInitialWheelDelta + targetFinalWheelDelta) / 2;
693 snapState.m_glidePhaseShift = acos((snapState.m_glideInitialWheelDelta - targetFinalWheelDelta) / (snapState.m_glideInitialWheelDelta + targetFinalWheelDelta));
696 void ScrollController::beginScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
698 ASSERT(newState == ScrollSnapState::Gliding || newState == ScrollSnapState::Snapping);
700 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
702 LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
703 float initialWheelDelta = newState == ScrollSnapState::Gliding ? snapState.averageInitialWheelDelta() : 0;
704 LayoutUnit projectedScrollDestination = newState == ScrollSnapState::Gliding ? snapState.m_beginTrackingWheelDeltaOffset + LayoutUnit(projectedInertialScrollDistance(initialWheelDelta)) : offset;
705 if (snapState.m_snapOffsets.isEmpty())
708 float scaleFactor = m_client.pageScaleFactor();
710 projectedScrollDestination = std::min(std::max(LayoutUnit(projectedScrollDestination / scaleFactor), snapState.m_snapOffsets.first()), snapState.m_snapOffsets.last());
711 snapState.m_initialOffset = offset;
712 snapState.m_targetOffset = scaleFactor * closestSnapOffset<LayoutUnit, float>(snapState.m_snapOffsets, projectedScrollDestination, initialWheelDelta);
713 if (snapState.m_initialOffset == snapState.m_targetOffset)
716 snapState.m_currentState = newState;
717 if (newState == ScrollSnapState::Gliding) {
718 snapState.m_shouldOverrideWheelEvent = true;
719 snapState.m_glideInitialWheelDelta = initialWheelDelta;
720 bool glideRequiresBoost;
721 if (initialWheelDelta > 0)
722 glideRequiresBoost = projectedScrollDestination - offset < snapState.m_targetOffset - projectedScrollDestination;
724 glideRequiresBoost = offset - projectedScrollDestination < projectedScrollDestination - snapState.m_targetOffset;
726 initializeGlideParameters(axis, glideRequiresBoost);
727 snapState.clearInitialWheelDeltaWindow();
729 startScrollSnapTimer(axis);
732 void ScrollController::endScrollSnapAnimation(ScrollEventAxis axis, ScrollSnapState newState)
734 ASSERT(newState == ScrollSnapState::DestinationReached || newState == ScrollSnapState::UserInteraction);
736 ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
738 if (snapState.m_currentState == ScrollSnapState::Gliding)
739 snapState.clearInitialWheelDeltaWindow();
741 snapState.m_currentState = newState;
742 stopScrollSnapTimer(axis);
745 static inline float snapProgress(const LayoutUnit& offset, const ScrollSnapAnimatorState& snapState)
747 const float distanceTraveled = static_cast<float>(offset - snapState.m_initialOffset);
748 const float totalDistance = static_cast<float>(snapState.m_targetOffset - snapState.m_initialOffset);
750 return distanceTraveled / totalDistance;
753 static inline float clampedSnapMagnitude(float thresholdedDistance)
755 return snapMagnitudeMin + (snapMagnitudeMax - snapMagnitudeMin) * (thresholdedDistance - snapThresholdLow) / (snapThresholdHigh - snapThresholdLow);
758 // Computes the amount to scroll by when performing a "snap" operation, i.e. when a user releases the trackpad without flicking. The snap delta
759 // is a function of progress t, where t is equal to DISTANCE_TRAVELED / TOTAL_DISTANCE, DISTANCE_TRAVELED is the distance from the initialOffset
760 // to the current offset, and TOTAL_DISTANCE is the distance from initialOffset to targetOffset. The snapping equation is as follows:
761 // 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
762 // relationship of the distance traveled, clamped by arbitrary min and max values.
763 float ScrollController::computeSnapDelta(ScrollEventAxis axis) const
765 const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
767 LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
768 bool canComputeSnap = (snapState.m_initialOffset <= offset && offset < snapState.m_targetOffset) || (snapState.m_targetOffset < offset && offset <= snapState.m_initialOffset);
769 if (snapState.m_currentState != ScrollSnapState::Snapping || !canComputeSnap)
772 float progress = snapProgress(offset, snapState);
774 // Threshold the distance before computing magnitude, so only distances within a certain range are considered.
775 int sign = snapState.m_initialOffset < snapState.m_targetOffset ? 1 : -1;
776 float thresholdedDistance = std::min(std::max<float>((snapState.m_targetOffset - snapState.m_initialOffset) * sign, snapThresholdLow), snapThresholdHigh);
778 float magnitude = clampedSnapMagnitude(thresholdedDistance);
780 float rawSnapDelta = std::max<float>(1, magnitude * std::sin(piFloat * progress));
781 if ((snapState.m_targetOffset < offset && offset - rawSnapDelta < snapState.m_targetOffset) || (snapState.m_targetOffset > offset && offset + rawSnapDelta > snapState.m_targetOffset))
782 return snapState.m_targetOffset - offset;
784 return sign * rawSnapDelta;
787 static inline float snapGlide(float progress, const ScrollSnapAnimatorState& snapState)
789 // FIXME: We might want to investigate why -m_glidePhaseShift results in the behavior we want.
790 return ceil(snapState.m_glideMagnitude * (1.0f + std::cos(piFloat * progress - snapState.m_glidePhaseShift)));
793 // Computes the amount to scroll by when performing a "glide" operation, i.e. when a user releases the trackpad with an initial velocity. Here,
794 // we want the scroll offset to animate directly to the snap point.
796 // 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
797 // from the initialOffset to the current offset, and (3) TOTAL_DISTANCE is the distance from initialOffset to targetOffset.
799 // The general model of our gliding equation is delta(t) = MAGNITUDE * (1 + cos(PI * t + PHASE_SHIFT)). This was determined after examining the
800 // momentum velocity curve as a function of progress. To compute MAGNITUDE and PHASE_SHIFT, we use initial velocity V0 and the final velocity VF,
801 // both as wheel deltas (pixels per timestep). VF should be a small value (< 10) chosen based on the initial velocity and TOTAL_DISTANCE.
802 // We also enforce the following constraints for the gliding equation:
803 // 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
804 // when the glide velocity is not enough to naturally reach the next snap point, and thus requires a boost (see initializeGlideParameters)
805 // 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
806 // doesn't guarantee that the final velocity will be exactly VF. However, assuming that the initial velocity is much less than TOTAL_DISTANCE,
807 // the last wheel delta will be very close, if not the same, as VF.
808 // For MAGNITUDE = (V0 + VF) / 2 and PHASE_SHIFT = arccos((V0 - VF) / (V0 + VF)), observe that delta(0) and delta(1) evaluate respectively to V0
809 // and VF. Thus, we can express our gliding equation all in terms of V0, VF and t.
810 float ScrollController::computeGlideDelta(ScrollEventAxis axis) const
812 const ScrollSnapAnimatorState& snapState = scrollSnapPointState(axis);
814 LayoutUnit offset = m_client.scrollOffsetOnAxis(axis);
815 bool canComputeGlide = (snapState.m_initialOffset <= offset && offset < snapState.m_targetOffset) || (snapState.m_targetOffset < offset && offset <= snapState.m_initialOffset);
816 if (snapState.m_currentState != ScrollSnapState::Gliding || !canComputeGlide)
819 const float progress = snapProgress(offset, snapState);
820 const float rawGlideDelta = snapGlide(progress, snapState);
822 float glideDelta = snapState.m_initialOffset < snapState.m_targetOffset ? std::max<float>(rawGlideDelta, 1) : std::min<float>(rawGlideDelta, -1);
823 if ((snapState.m_initialOffset < snapState.m_targetOffset && offset + glideDelta > snapState.m_targetOffset) || (snapState.m_initialOffset > snapState.m_targetOffset && offset + glideDelta < snapState.m_targetOffset))
824 return snapState.m_targetOffset - offset;
830 } // namespace WebCore
832 #endif // ENABLE(RUBBER_BANDING)