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