1/*
2 * Copyright (C) 2011 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
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.
12 *
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.
24 */
25
26#include "config.h"
27#include "ScrollElasticityController.h"
28
29#include "PlatformWheelEvent.h"
30#include "WebCoreSystemInterface.h"
31#include <sys/time.h>
32#include <sys/sysctl.h>
33
34#if ENABLE(RUBBER_BANDING)
35
36static NSTimeInterval systemUptime()
37{
38    if ([[NSProcessInfo processInfo] respondsToSelector:@selector(systemUptime)])
39        return [[NSProcessInfo processInfo] systemUptime];
40
41    // Get how long system has been up. Found by looking getting "boottime" from the kernel.
42    static struct timeval boottime = {0, 0};
43    if (!boottime.tv_sec) {
44        int mib[2] = {CTL_KERN, KERN_BOOTTIME};
45        size_t size = sizeof(boottime);
46        if (-1 == sysctl(mib, 2, &boottime, &size, 0, 0))
47            boottime.tv_sec = 0;
48    }
49    struct timeval now;
50    if (boottime.tv_sec && -1 != gettimeofday(&now, 0)) {
51        struct timeval uptime;
52        timersub(&now, &boottime, &uptime);
53        NSTimeInterval result = uptime.tv_sec + (uptime.tv_usec / 1E+6);
54        return result;
55    }
56    return 0;
57}
58
59
60namespace WebCore {
61
62static const float scrollVelocityZeroingTimeout = 0.10f;
63static const float rubberbandDirectionLockStretchRatio = 1;
64static const float rubberbandMinimumRequiredDeltaBeforeStretch = 10;
65
66#if __MAC_OS_X_VERSION_MIN_REQUIRED <= 1070
67static const float rubberbandStiffness = 20;
68static const float rubberbandAmplitude = 0.31f;
69static const float rubberbandPeriod = 1.6f;
70
71static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
72{
73    float amplitude = rubberbandAmplitude;
74    float period = rubberbandPeriod;
75    float criticalDampeningFactor = expf((-elapsedTime * rubberbandStiffness) / period);
76
77    return (initialPosition + (-initialVelocity * elapsedTime * amplitude)) * criticalDampeningFactor;
78}
79
80static float elasticDeltaForReboundDelta(float delta)
81{
82    float stiffness = std::max(rubberbandStiffness, 1.0f);
83    return delta / stiffness;
84}
85
86static float reboundDeltaForElasticDelta(float delta)
87{
88    return delta * rubberbandStiffness;
89}
90#else
91static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
92{
93    return wkNSElasticDeltaForTimeDelta(initialPosition, initialVelocity, elapsedTime);
94}
95
96static float elasticDeltaForReboundDelta(float delta)
97{
98    return wkNSElasticDeltaForReboundDelta(delta);
99}
100
101static float reboundDeltaForElasticDelta(float delta)
102{
103    return wkNSReboundDeltaForElasticDelta(delta);
104}
105#endif
106
107static float scrollWheelMultiplier()
108{
109    static float multiplier = -1;
110    if (multiplier < 0) {
111        multiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"NSScrollWheelMultiplier"];
112        if (multiplier <= 0)
113            multiplier = 1;
114    }
115    return multiplier;
116}
117
118ScrollElasticityController::ScrollElasticityController(ScrollElasticityControllerClient* client)
119    : m_client(client)
120    , m_inScrollGesture(false)
121    , m_momentumScrollInProgress(false)
122    , m_ignoreMomentumScrolls(false)
123    , m_lastMomentumScrollTimestamp(0)
124    , m_startTime(0)
125    , m_snapRubberbandTimerIsActive(false)
126{
127}
128
129bool ScrollElasticityController::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
130{
131    if (wheelEvent.phase() == PlatformWheelEventPhaseBegan) {
132        // First, check if we should rubber-band at all.
133        if (m_client->pinnedInDirection(FloatSize(-wheelEvent.deltaX(), 0)) &&
134            !shouldRubberBandInHorizontalDirection(wheelEvent))
135            return false;
136
137        m_inScrollGesture = true;
138        m_momentumScrollInProgress = false;
139        m_ignoreMomentumScrolls = false;
140        m_lastMomentumScrollTimestamp = 0;
141        m_momentumVelocity = FloatSize();
142
143        IntSize stretchAmount = m_client->stretchAmount();
144        m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(stretchAmount.width()));
145        m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(stretchAmount.height()));
146        m_overflowScrollDelta = FloatSize();
147
148        stopSnapRubberbandTimer();
149
150        return true;
151    }
152
153    if (wheelEvent.phase() == PlatformWheelEventPhaseEnded) {
154        snapRubberBand();
155        return true;
156    }
157
158    bool isMomentumScrollEvent = (wheelEvent.momentumPhase() != PlatformWheelEventPhaseNone);
159    if (m_ignoreMomentumScrolls && (isMomentumScrollEvent || m_snapRubberbandTimerIsActive)) {
160        if (wheelEvent.momentumPhase() == PlatformWheelEventPhaseEnded) {
161            m_ignoreMomentumScrolls = false;
162            return true;
163        }
164        return false;
165    }
166
167    float deltaX = m_overflowScrollDelta.width();
168    float deltaY = m_overflowScrollDelta.height();
169
170    // Reset overflow values because we may decide to remove delta at various points and put it into overflow.
171    m_overflowScrollDelta = FloatSize();
172
173    IntSize stretchAmount = m_client->stretchAmount();
174    bool isVerticallyStretched = stretchAmount.height();
175    bool isHorizontallyStretched = stretchAmount.width();
176
177    float eventCoalescedDeltaX;
178    float eventCoalescedDeltaY;
179
180    if (isVerticallyStretched || isHorizontallyStretched) {
181        eventCoalescedDeltaX = -wheelEvent.unacceleratedScrollingDeltaX();
182        eventCoalescedDeltaY = -wheelEvent.unacceleratedScrollingDeltaY();
183    } else {
184        eventCoalescedDeltaX = -wheelEvent.deltaX();
185        eventCoalescedDeltaY = -wheelEvent.deltaY();
186    }
187
188    deltaX += eventCoalescedDeltaX;
189    deltaY += eventCoalescedDeltaY;
190
191    // Slightly prefer scrolling vertically by applying the = case to deltaY
192    if (fabsf(deltaY) >= fabsf(deltaX))
193        deltaX = 0;
194    else
195        deltaY = 0;
196
197    bool shouldStretch = false;
198
199    PlatformWheelEventPhase momentumPhase = wheelEvent.momentumPhase();
200
201    // If we are starting momentum scrolling then do some setup.
202    if (!m_momentumScrollInProgress && (momentumPhase == PlatformWheelEventPhaseBegan || momentumPhase == PlatformWheelEventPhaseChanged))
203        m_momentumScrollInProgress = true;
204
205    CFTimeInterval timeDelta = wheelEvent.timestamp() - m_lastMomentumScrollTimestamp;
206    if (m_inScrollGesture || m_momentumScrollInProgress) {
207        if (m_lastMomentumScrollTimestamp && timeDelta > 0 && timeDelta < scrollVelocityZeroingTimeout) {
208            m_momentumVelocity.setWidth(eventCoalescedDeltaX / (float)timeDelta);
209            m_momentumVelocity.setHeight(eventCoalescedDeltaY / (float)timeDelta);
210            m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
211        } else {
212            m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
213            m_momentumVelocity = FloatSize();
214        }
215
216        if (isVerticallyStretched) {
217            if (!isHorizontallyStretched && m_client->pinnedInDirection(FloatSize(deltaX, 0))) {
218                // Stretching only in the vertical.
219                if (deltaY != 0 && (fabsf(deltaX / deltaY) < rubberbandDirectionLockStretchRatio))
220                    deltaX = 0;
221                else if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
222                    m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
223                    deltaX = 0;
224                } else
225                    m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
226            }
227        } else if (isHorizontallyStretched) {
228            // Stretching only in the horizontal.
229            if (m_client->pinnedInDirection(FloatSize(0, deltaY))) {
230                if (deltaX != 0 && (fabsf(deltaY / deltaX) < rubberbandDirectionLockStretchRatio))
231                    deltaY = 0;
232                else if (fabsf(deltaY) < rubberbandMinimumRequiredDeltaBeforeStretch) {
233                    m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
234                    deltaY = 0;
235                } else
236                    m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
237            }
238        } else {
239            // Not stretching at all yet.
240            if (m_client->pinnedInDirection(FloatSize(deltaX, deltaY))) {
241                if (fabsf(deltaY) >= fabsf(deltaX)) {
242                    if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
243                        m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
244                        deltaX = 0;
245                    } else
246                        m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
247                }
248                shouldStretch = true;
249            }
250        }
251    }
252
253    if (deltaX != 0 || deltaY != 0) {
254        if (!(shouldStretch || isVerticallyStretched || isHorizontallyStretched)) {
255            if (deltaY != 0) {
256                deltaY *= scrollWheelMultiplier();
257                m_client->immediateScrollBy(FloatSize(0, deltaY));
258            }
259            if (deltaX != 0) {
260                deltaX *= scrollWheelMultiplier();
261                m_client->immediateScrollBy(FloatSize(deltaX, 0));
262            }
263        } else {
264            if (!m_client->allowsHorizontalStretching()) {
265                deltaX = 0;
266                eventCoalescedDeltaX = 0;
267            } else if ((deltaX != 0) && !isHorizontallyStretched && !m_client->pinnedInDirection(FloatSize(deltaX, 0))) {
268                deltaX *= scrollWheelMultiplier();
269
270                m_client->immediateScrollByWithoutContentEdgeConstraints(FloatSize(deltaX, 0));
271                deltaX = 0;
272            }
273
274            if (!m_client->allowsVerticalStretching()) {
275                deltaY = 0;
276                eventCoalescedDeltaY = 0;
277            } else if ((deltaY != 0) && !isVerticallyStretched && !m_client->pinnedInDirection(FloatSize(0, deltaY))) {
278                deltaY *= scrollWheelMultiplier();
279
280                m_client->immediateScrollByWithoutContentEdgeConstraints(FloatSize(0, deltaY));
281                deltaY = 0;
282            }
283
284            IntSize stretchAmount = m_client->stretchAmount();
285
286            if (m_momentumScrollInProgress) {
287                if ((m_client->pinnedInDirection(FloatSize(eventCoalescedDeltaX, eventCoalescedDeltaY)) || (fabsf(eventCoalescedDeltaX) + fabsf(eventCoalescedDeltaY) <= 0)) && m_lastMomentumScrollTimestamp) {
288                    m_ignoreMomentumScrolls = true;
289                    m_momentumScrollInProgress = false;
290                    snapRubberBand();
291                }
292            }
293
294            m_stretchScrollForce.setWidth(m_stretchScrollForce.width() + deltaX);
295            m_stretchScrollForce.setHeight(m_stretchScrollForce.height() + deltaY);
296
297            FloatSize dampedDelta(ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.width())), ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.height())));
298
299            m_client->immediateScrollByWithoutContentEdgeConstraints(dampedDelta - stretchAmount);
300        }
301    }
302
303    if (m_momentumScrollInProgress && momentumPhase == PlatformWheelEventPhaseEnded) {
304        m_momentumScrollInProgress = false;
305        m_ignoreMomentumScrolls = false;
306        m_lastMomentumScrollTimestamp = 0;
307    }
308
309    return true;
310}
311
312static inline float roundTowardZero(float num)
313{
314    return num > 0 ? ceilf(num - 0.5f) : floorf(num + 0.5f);
315}
316
317static inline float roundToDevicePixelTowardZero(float num)
318{
319    float roundedNum = roundf(num);
320    if (fabs(num - roundedNum) < 0.125)
321        num = roundedNum;
322
323    return roundTowardZero(num);
324}
325
326void ScrollElasticityController::snapRubberBandTimerFired()
327{
328    if (!m_momentumScrollInProgress || m_ignoreMomentumScrolls) {
329        CFTimeInterval timeDelta = [NSDate timeIntervalSinceReferenceDate] - m_startTime;
330
331        if (m_startStretch == FloatSize()) {
332            m_startStretch = m_client->stretchAmount();
333            if (m_startStretch == FloatSize()) {
334                stopSnapRubberbandTimer();
335
336                m_stretchScrollForce = FloatSize();
337                m_startTime = 0;
338                m_startStretch = FloatSize();
339                m_origOrigin = FloatPoint();
340                m_origVelocity = FloatSize();
341                return;
342            }
343
344            m_origOrigin = m_client->absoluteScrollPosition() - m_startStretch;
345            m_origVelocity = m_momentumVelocity;
346
347            // Just like normal scrolling, prefer vertical rubberbanding
348            if (fabsf(m_origVelocity.height()) >= fabsf(m_origVelocity.width()))
349                m_origVelocity.setWidth(0);
350
351            // Don't rubber-band horizontally if it's not possible to scroll horizontally
352            if (!m_client->canScrollHorizontally())
353                m_origVelocity.setWidth(0);
354
355            // Don't rubber-band vertically if it's not possible to scroll vertically
356            if (!m_client->canScrollVertically())
357                m_origVelocity.setHeight(0);
358        }
359
360        FloatPoint delta(roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.width(), -m_origVelocity.width(), (float)timeDelta)),
361                         roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.height(), -m_origVelocity.height(), (float)timeDelta)));
362
363        if (fabs(delta.x()) >= 1 || fabs(delta.y()) >= 1) {
364            m_client->immediateScrollByWithoutContentEdgeConstraints(FloatSize(delta.x(), delta.y()) - m_client->stretchAmount());
365
366            FloatSize newStretch = m_client->stretchAmount();
367
368            m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(newStretch.width()));
369            m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(newStretch.height()));
370        } else {
371            m_client->adjustScrollPositionToBoundsIfNecessary();
372
373            stopSnapRubberbandTimer();
374            m_stretchScrollForce = FloatSize();
375            m_startTime = 0;
376            m_startStretch = FloatSize();
377            m_origOrigin = FloatPoint();
378            m_origVelocity = FloatSize();
379        }
380    } else {
381        m_startTime = [NSDate timeIntervalSinceReferenceDate];
382        m_startStretch = FloatSize();
383    }
384}
385
386bool ScrollElasticityController::isRubberBandInProgress() const
387{
388    if (!m_inScrollGesture && !m_momentumScrollInProgress && !m_snapRubberbandTimerIsActive)
389        return false;
390
391    return !m_client->stretchAmount().isZero();
392}
393
394void ScrollElasticityController::stopSnapRubberbandTimer()
395{
396    m_client->stopSnapRubberbandTimer();
397    m_snapRubberbandTimerIsActive = false;
398}
399
400void ScrollElasticityController::snapRubberBand()
401{
402    CFTimeInterval timeDelta = systemUptime() - m_lastMomentumScrollTimestamp;
403    if (m_lastMomentumScrollTimestamp && timeDelta >= scrollVelocityZeroingTimeout)
404        m_momentumVelocity = FloatSize();
405
406    m_inScrollGesture = false;
407
408    if (m_snapRubberbandTimerIsActive)
409        return;
410
411    m_startTime = [NSDate timeIntervalSinceReferenceDate];
412    m_startStretch = FloatSize();
413    m_origOrigin = FloatPoint();
414    m_origVelocity = FloatSize();
415
416    m_client->startSnapRubberbandTimer();
417    m_snapRubberbandTimerIsActive = true;
418}
419
420bool ScrollElasticityController::shouldRubberBandInHorizontalDirection(const PlatformWheelEvent& wheelEvent)
421{
422    if (wheelEvent.deltaX() > 0)
423        return m_client->shouldRubberBandInDirection(ScrollLeft);
424    if (wheelEvent.deltaX() < 0)
425        return m_client->shouldRubberBandInDirection(ScrollRight);
426
427    return true;
428}
429
430} // namespace WebCore
431
432#endif // ENABLE(RUBBER_BANDING)
433