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