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
66static float elasticDeltaForTimeDelta(float initialPosition, float initialVelocity, float elapsedTime)
67{
68    return wkNSElasticDeltaForTimeDelta(initialPosition, initialVelocity, elapsedTime);
69}
70
71static float elasticDeltaForReboundDelta(float delta)
72{
73    return wkNSElasticDeltaForReboundDelta(delta);
74}
75
76static float reboundDeltaForElasticDelta(float delta)
77{
78    return wkNSReboundDeltaForElasticDelta(delta);
79}
80
81static float scrollWheelMultiplier()
82{
83    static float multiplier = -1;
84    if (multiplier < 0) {
85        multiplier = [[NSUserDefaults standardUserDefaults] floatForKey:@"NSScrollWheelMultiplier"];
86        if (multiplier <= 0)
87            multiplier = 1;
88    }
89    return multiplier;
90}
91
92ScrollElasticityController::ScrollElasticityController(ScrollElasticityControllerClient* client)
93    : m_client(client)
94    , m_inScrollGesture(false)
95    , m_momentumScrollInProgress(false)
96    , m_ignoreMomentumScrolls(false)
97    , m_lastMomentumScrollTimestamp(0)
98    , m_startTime(0)
99    , m_snapRubberbandTimerIsActive(false)
100{
101}
102
103bool ScrollElasticityController::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
104{
105    if (wheelEvent.phase() == PlatformWheelEventPhaseBegan) {
106        // First, check if we should rubber-band at all.
107        if (m_client->pinnedInDirection(FloatSize(-wheelEvent.deltaX(), 0)) &&
108            !shouldRubberBandInHorizontalDirection(wheelEvent))
109            return false;
110
111        m_inScrollGesture = true;
112        m_momentumScrollInProgress = false;
113        m_ignoreMomentumScrolls = false;
114        m_lastMomentumScrollTimestamp = 0;
115        m_momentumVelocity = FloatSize();
116
117        IntSize stretchAmount = m_client->stretchAmount();
118        m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(stretchAmount.width()));
119        m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(stretchAmount.height()));
120        m_overflowScrollDelta = FloatSize();
121
122        stopSnapRubberbandTimer();
123
124        return true;
125    }
126
127    if (wheelEvent.phase() == PlatformWheelEventPhaseEnded) {
128        snapRubberBand();
129        return true;
130    }
131
132    bool isMomentumScrollEvent = (wheelEvent.momentumPhase() != PlatformWheelEventPhaseNone);
133    if (m_ignoreMomentumScrolls && (isMomentumScrollEvent || m_snapRubberbandTimerIsActive)) {
134        if (wheelEvent.momentumPhase() == PlatformWheelEventPhaseEnded) {
135            m_ignoreMomentumScrolls = false;
136            return true;
137        }
138        return false;
139    }
140
141    float deltaX = m_overflowScrollDelta.width();
142    float deltaY = m_overflowScrollDelta.height();
143
144    // Reset overflow values because we may decide to remove delta at various points and put it into overflow.
145    m_overflowScrollDelta = FloatSize();
146
147    IntSize stretchAmount = m_client->stretchAmount();
148    bool isVerticallyStretched = stretchAmount.height();
149    bool isHorizontallyStretched = stretchAmount.width();
150
151    float eventCoalescedDeltaX;
152    float eventCoalescedDeltaY;
153
154    if (isVerticallyStretched || isHorizontallyStretched) {
155        eventCoalescedDeltaX = -wheelEvent.unacceleratedScrollingDeltaX();
156        eventCoalescedDeltaY = -wheelEvent.unacceleratedScrollingDeltaY();
157    } else {
158        eventCoalescedDeltaX = -wheelEvent.deltaX();
159        eventCoalescedDeltaY = -wheelEvent.deltaY();
160    }
161
162    deltaX += eventCoalescedDeltaX;
163    deltaY += eventCoalescedDeltaY;
164
165    // Slightly prefer scrolling vertically by applying the = case to deltaY
166    if (fabsf(deltaY) >= fabsf(deltaX))
167        deltaX = 0;
168    else
169        deltaY = 0;
170
171    bool shouldStretch = false;
172
173    PlatformWheelEventPhase momentumPhase = wheelEvent.momentumPhase();
174
175    // If we are starting momentum scrolling then do some setup.
176    if (!m_momentumScrollInProgress && (momentumPhase == PlatformWheelEventPhaseBegan || momentumPhase == PlatformWheelEventPhaseChanged))
177        m_momentumScrollInProgress = true;
178
179    CFTimeInterval timeDelta = wheelEvent.timestamp() - m_lastMomentumScrollTimestamp;
180    if (m_inScrollGesture || m_momentumScrollInProgress) {
181        if (m_lastMomentumScrollTimestamp && timeDelta > 0 && timeDelta < scrollVelocityZeroingTimeout) {
182            m_momentumVelocity.setWidth(eventCoalescedDeltaX / (float)timeDelta);
183            m_momentumVelocity.setHeight(eventCoalescedDeltaY / (float)timeDelta);
184            m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
185        } else {
186            m_lastMomentumScrollTimestamp = wheelEvent.timestamp();
187            m_momentumVelocity = FloatSize();
188        }
189
190        if (isVerticallyStretched) {
191            if (!isHorizontallyStretched && m_client->pinnedInDirection(FloatSize(deltaX, 0))) {
192                // Stretching only in the vertical.
193                if (deltaY != 0 && (fabsf(deltaX / deltaY) < rubberbandDirectionLockStretchRatio))
194                    deltaX = 0;
195                else if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
196                    m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
197                    deltaX = 0;
198                } else
199                    m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
200            }
201        } else if (isHorizontallyStretched) {
202            // Stretching only in the horizontal.
203            if (m_client->pinnedInDirection(FloatSize(0, deltaY))) {
204                if (deltaX != 0 && (fabsf(deltaY / deltaX) < rubberbandDirectionLockStretchRatio))
205                    deltaY = 0;
206                else if (fabsf(deltaY) < rubberbandMinimumRequiredDeltaBeforeStretch) {
207                    m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
208                    deltaY = 0;
209                } else
210                    m_overflowScrollDelta.setHeight(m_overflowScrollDelta.height() + deltaY);
211            }
212        } else {
213            // Not stretching at all yet.
214            if (m_client->pinnedInDirection(FloatSize(deltaX, deltaY))) {
215                if (fabsf(deltaY) >= fabsf(deltaX)) {
216                    if (fabsf(deltaX) < rubberbandMinimumRequiredDeltaBeforeStretch) {
217                        m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
218                        deltaX = 0;
219                    } else
220                        m_overflowScrollDelta.setWidth(m_overflowScrollDelta.width() + deltaX);
221                }
222                shouldStretch = true;
223            }
224        }
225    }
226
227    if (deltaX != 0 || deltaY != 0) {
228        if (!(shouldStretch || isVerticallyStretched || isHorizontallyStretched)) {
229            if (deltaY != 0) {
230                deltaY *= scrollWheelMultiplier();
231                m_client->immediateScrollBy(FloatSize(0, deltaY));
232            }
233            if (deltaX != 0) {
234                deltaX *= scrollWheelMultiplier();
235                m_client->immediateScrollBy(FloatSize(deltaX, 0));
236            }
237        } else {
238            if (!m_client->allowsHorizontalStretching()) {
239                deltaX = 0;
240                eventCoalescedDeltaX = 0;
241            } else if ((deltaX != 0) && !isHorizontallyStretched && !m_client->pinnedInDirection(FloatSize(deltaX, 0))) {
242                deltaX *= scrollWheelMultiplier();
243
244                m_client->immediateScrollByWithoutContentEdgeConstraints(FloatSize(deltaX, 0));
245                deltaX = 0;
246            }
247
248            if (!m_client->allowsVerticalStretching()) {
249                deltaY = 0;
250                eventCoalescedDeltaY = 0;
251            } else if ((deltaY != 0) && !isVerticallyStretched && !m_client->pinnedInDirection(FloatSize(0, deltaY))) {
252                deltaY *= scrollWheelMultiplier();
253
254                m_client->immediateScrollByWithoutContentEdgeConstraints(FloatSize(0, deltaY));
255                deltaY = 0;
256            }
257
258            IntSize stretchAmount = m_client->stretchAmount();
259
260            if (m_momentumScrollInProgress) {
261                if ((m_client->pinnedInDirection(FloatSize(eventCoalescedDeltaX, eventCoalescedDeltaY)) || (fabsf(eventCoalescedDeltaX) + fabsf(eventCoalescedDeltaY) <= 0)) && m_lastMomentumScrollTimestamp) {
262                    m_ignoreMomentumScrolls = true;
263                    m_momentumScrollInProgress = false;
264                    snapRubberBand();
265                }
266            }
267
268            m_stretchScrollForce.setWidth(m_stretchScrollForce.width() + deltaX);
269            m_stretchScrollForce.setHeight(m_stretchScrollForce.height() + deltaY);
270
271            FloatSize dampedDelta(ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.width())), ceilf(elasticDeltaForReboundDelta(m_stretchScrollForce.height())));
272
273            m_client->immediateScrollByWithoutContentEdgeConstraints(dampedDelta - stretchAmount);
274        }
275    }
276
277    if (m_momentumScrollInProgress && momentumPhase == PlatformWheelEventPhaseEnded) {
278        m_momentumScrollInProgress = false;
279        m_ignoreMomentumScrolls = false;
280        m_lastMomentumScrollTimestamp = 0;
281    }
282
283    return true;
284}
285
286static inline float roundTowardZero(float num)
287{
288    return num > 0 ? ceilf(num - 0.5f) : floorf(num + 0.5f);
289}
290
291static inline float roundToDevicePixelTowardZero(float num)
292{
293    float roundedNum = roundf(num);
294    if (fabs(num - roundedNum) < 0.125)
295        num = roundedNum;
296
297    return roundTowardZero(num);
298}
299
300void ScrollElasticityController::snapRubberBandTimerFired()
301{
302    if (!m_momentumScrollInProgress || m_ignoreMomentumScrolls) {
303        CFTimeInterval timeDelta = [NSDate timeIntervalSinceReferenceDate] - m_startTime;
304
305        if (m_startStretch == FloatSize()) {
306            m_startStretch = m_client->stretchAmount();
307            if (m_startStretch == FloatSize()) {
308                stopSnapRubberbandTimer();
309
310                m_stretchScrollForce = FloatSize();
311                m_startTime = 0;
312                m_startStretch = FloatSize();
313                m_origOrigin = FloatPoint();
314                m_origVelocity = FloatSize();
315                return;
316            }
317
318            m_origOrigin = m_client->absoluteScrollPosition() - m_startStretch;
319            m_origVelocity = m_momentumVelocity;
320
321            // Just like normal scrolling, prefer vertical rubberbanding
322            if (fabsf(m_origVelocity.height()) >= fabsf(m_origVelocity.width()))
323                m_origVelocity.setWidth(0);
324
325            // Don't rubber-band horizontally if it's not possible to scroll horizontally
326            if (!m_client->canScrollHorizontally())
327                m_origVelocity.setWidth(0);
328
329            // Don't rubber-band vertically if it's not possible to scroll vertically
330            if (!m_client->canScrollVertically())
331                m_origVelocity.setHeight(0);
332        }
333
334        FloatPoint delta(roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.width(), -m_origVelocity.width(), (float)timeDelta)),
335                         roundToDevicePixelTowardZero(elasticDeltaForTimeDelta(m_startStretch.height(), -m_origVelocity.height(), (float)timeDelta)));
336
337        if (fabs(delta.x()) >= 1 || fabs(delta.y()) >= 1) {
338            m_client->immediateScrollByWithoutContentEdgeConstraints(FloatSize(delta.x(), delta.y()) - m_client->stretchAmount());
339
340            FloatSize newStretch = m_client->stretchAmount();
341
342            m_stretchScrollForce.setWidth(reboundDeltaForElasticDelta(newStretch.width()));
343            m_stretchScrollForce.setHeight(reboundDeltaForElasticDelta(newStretch.height()));
344        } else {
345            m_client->adjustScrollPositionToBoundsIfNecessary();
346
347            stopSnapRubberbandTimer();
348            m_stretchScrollForce = FloatSize();
349            m_startTime = 0;
350            m_startStretch = FloatSize();
351            m_origOrigin = FloatPoint();
352            m_origVelocity = FloatSize();
353        }
354    } else {
355        m_startTime = [NSDate timeIntervalSinceReferenceDate];
356        m_startStretch = FloatSize();
357    }
358}
359
360bool ScrollElasticityController::isRubberBandInProgress() const
361{
362    if (!m_inScrollGesture && !m_momentumScrollInProgress && !m_snapRubberbandTimerIsActive)
363        return false;
364
365    return !m_client->stretchAmount().isZero();
366}
367
368void ScrollElasticityController::stopSnapRubberbandTimer()
369{
370    m_client->stopSnapRubberbandTimer();
371    m_snapRubberbandTimerIsActive = false;
372}
373
374void ScrollElasticityController::snapRubberBand()
375{
376    CFTimeInterval timeDelta = systemUptime() - m_lastMomentumScrollTimestamp;
377    if (m_lastMomentumScrollTimestamp && timeDelta >= scrollVelocityZeroingTimeout)
378        m_momentumVelocity = FloatSize();
379
380    m_inScrollGesture = false;
381
382    if (m_snapRubberbandTimerIsActive)
383        return;
384
385    m_startTime = [NSDate timeIntervalSinceReferenceDate];
386    m_startStretch = FloatSize();
387    m_origOrigin = FloatPoint();
388    m_origVelocity = FloatSize();
389
390    m_client->startSnapRubberbandTimer();
391    m_snapRubberbandTimerIsActive = true;
392}
393
394bool ScrollElasticityController::shouldRubberBandInHorizontalDirection(const PlatformWheelEvent& wheelEvent)
395{
396    if (wheelEvent.deltaX() > 0)
397        return m_client->shouldRubberBandInDirection(ScrollLeft);
398    if (wheelEvent.deltaX() < 0)
399        return m_client->shouldRubberBandInDirection(ScrollRight);
400
401    return true;
402}
403
404} // namespace WebCore
405
406#endif // ENABLE(RUBBER_BANDING)
407