summaryrefslogtreecommitdiffstats
path: root/Source/WebCore/platform/ScrollAnimatorWin.cpp
blob: 47a00cb193c2055869bc309a508c22d3c19bc110 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/*
 * Copyright (c) 2010, Google Inc. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#if ENABLE(SMOOTH_SCROLLING)

#include "ScrollAnimatorWin.h"

#include "FloatPoint.h"
#include "ScrollableArea.h"
#include "ScrollbarTheme.h"
#include <algorithm>
#include <wtf/CurrentTime.h>
#include <wtf/PassOwnPtr.h>

namespace WebCore {

PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea)
{
    return adoptPtr(new ScrollAnimatorWin(scrollableArea));
}

const double ScrollAnimatorWin::animationTimerDelay = 0.01;

ScrollAnimatorWin::PerAxisData::PerAxisData(ScrollAnimatorWin* parent, float* currentPos)
    : m_currentPos(currentPos)
    , m_desiredPos(0)
    , m_currentVelocity(0)
    , m_desiredVelocity(0)
    , m_lastAnimationTime(0)
    , m_animationTimer(parent, &ScrollAnimatorWin::animationTimerFired)
{
}


ScrollAnimatorWin::ScrollAnimatorWin(ScrollableArea* scrollableArea)
    : ScrollAnimator(scrollableArea)
    , m_horizontalData(this, &m_currentPosX)
    , m_verticalData(this, &m_currentPosY)
{
}

ScrollAnimatorWin::~ScrollAnimatorWin()
{
    stopAnimationTimerIfNeeded(&m_horizontalData);
    stopAnimationTimerIfNeeded(&m_verticalData);
}

bool ScrollAnimatorWin::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier)
{
    // Don't animate jumping to the beginning or end of the document.
    if (granularity == ScrollByDocument)
        return ScrollAnimator::scroll(orientation, granularity, step, multiplier);

    // This is an animatable scroll.  Calculate the scroll delta.
    PerAxisData* data = (orientation == VerticalScrollbar) ? &m_verticalData : &m_horizontalData;
    float newPos = std::max(std::min(data->m_desiredPos + (step * multiplier), static_cast<float>(m_scrollableArea->scrollSize(orientation))), 0.0f);
    if (newPos == data->m_desiredPos)
        return false;
    data->m_desiredPos = newPos;

    // Calculate the animation velocity.
    if (*data->m_currentPos == data->m_desiredPos)
        return false;
    bool alreadyAnimating = data->m_animationTimer.isActive();
    // There are a number of different sources of scroll requests.  We want to
    // make both keyboard and wheel-generated scroll requests (which can come at
    // unpredictable rates) and autoscrolling from holding down the mouse button
    // on a scrollbar part (where the request rate can be obtained from the
    // scrollbar theme) feel smooth, responsive, and similar.
    //
    // When autoscrolling, the scrollbar's autoscroll timer will call us to
    // increment the desired position by |step| (with |multiplier| == 1) every
    // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() seconds.  If we set
    // the desired velocity to exactly this rate, smooth scrolling will neither
    // race ahead (and then have to slow down) nor increasingly lag behind, but
    // will be smooth and synchronized.
    //
    // Note that because of the acceleration period, the current position in
    // this case would lag the desired one by a small, constant amount (see
    // comments on animateScroll()); the exact amount is given by
    //   lag = |step| - v(0.5tA + tD)
    // Where
    //   v = The steady-state velocity,
    //       |step| / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay()
    //   tA = accelerationTime()
    //   tD = The time we pretend has already passed when starting to scroll,
    //        |animationTimerDelay|
    //
    // This lag provides some buffer against timer jitter so we're less likely
    // to hit the desired position and stop (and thus have to re-accelerate,
    // causing a visible hitch) while waiting for the next autoscroll increment.
    //
    // Thus, for autoscroll-timer-triggered requests, the ideal steady-state
    // distance to travel in each time interval is:
    //   float animationStep = step;
    // Note that when we're not already animating, this is exactly the same as
    // the distance to the target position.  We'll return to that in a moment.
    //
    // For keyboard and wheel scrolls, we don't know when the next increment
    // will be requested.  If we set the target velocity based on how far away
    // from the target position we are, then for keyboard/wheel events that come
    // faster than the autoscroll delay, we'll asymptotically approach the
    // velocity needed to stay smoothly in sync with the user's actions; for
    // events that come slower, we'll scroll one increment and then pause until
    // the next event fires.
    float animationStep = fabs(newPos - *data->m_currentPos);
    // If a key is held down (or the wheel continually spun), then once we have
    // reached a velocity close to the steady-state velocity, we're likely to
    // hit the desired position at around the same time we'd expect the next
    // increment to occur -- bad because it leads to hitching as described above
    // (if autoscroll-based requests didn't result in a small amount of constant
    // lag).  So if we're called again while already animating, we want to trim
    // the animationStep slightly to maintain lag like what's described above.
    // (I say "maintain" since we'll already be lagged due to the acceleration
    // during the first scroll period.)
    //
    // Remember that trimming won't cause us to fall steadily further behind
    // here, because the further behind we are, the larger the base step value
    // above.  Given the scrolling algorithm in animateScroll(), the practical
    // effect will actually be that, assuming a constant trim factor, we'll lag
    // by a constant amount depending on the rate at which increments occur
    // compared to the autoscroll timer delay.  The exact lag is given by
    //   lag = |step| * ((r / k) - 1)
    // Where
    //   r = The ratio of the autoscroll repeat delay,
    //       ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(), to the
    //       key/wheel repeat delay (i.e. > 1 when keys repeat faster)
    //   k = The velocity trim constant given below
    //
    // We want to choose the trim factor such that for calls that come at the
    // autoscroll timer rate, we'll wind up with the same lag as in the
    // "perfect" case described above (or, to put it another way, we'll end up
    // with |animationStep| == |step| * |multiplier| despite the actual distance
    // calculated above being larger than that).  This will result in "perfect"
    // behavior for autoscrolling without having to special-case it.
    if (alreadyAnimating)
        animationStep /= (2.0 - ((1.0 / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay()) * (0.5 * accelerationTime() + animationTimerDelay)));
    // The result of all this is that single keypresses or wheel flicks will
    // scroll in the same time period as single presses of scrollbar elements;
    // holding the mouse down on a scrollbar part will scroll as fast as
    // possible without hitching; and other repeated scroll events will also
    // scroll with the same time lag as holding down the mouse on a scrollbar
    // part.
    data->m_desiredVelocity = animationStep / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay();

    // If we're not already scrolling, start.
    if (!alreadyAnimating)
        animateScroll(data);
    return true;
}

void ScrollAnimatorWin::scrollToOffsetWithoutAnimation(const FloatPoint& offset)
{
    stopAnimationTimerIfNeeded(&m_horizontalData);
    stopAnimationTimerIfNeeded(&m_verticalData);

    *m_horizontalData.m_currentPos = offset.x();
    m_horizontalData.m_desiredPos = offset.x();
    m_horizontalData.m_currentVelocity = 0;
    m_horizontalData.m_desiredVelocity = 0;

    *m_verticalData.m_currentPos = offset.y();
    m_verticalData.m_desiredPos = offset.y();
    m_verticalData.m_currentVelocity = 0;
    m_verticalData.m_desiredVelocity = 0;

    notityPositionChanged();
}

double ScrollAnimatorWin::accelerationTime()
{
    // We elect to use ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() as
    // the length of time we'll take to accelerate from 0 to our target
    // velocity.  Choosing a larger value would produce a more pronounced
    // acceleration effect.
    return ScrollbarTheme::nativeTheme()->autoscrollTimerDelay();
}

void ScrollAnimatorWin::animationTimerFired(Timer<ScrollAnimatorWin>* timer)
{
    animateScroll((timer == &m_horizontalData.m_animationTimer) ? &m_horizontalData : &m_verticalData);
}

void ScrollAnimatorWin::stopAnimationTimerIfNeeded(PerAxisData* data)
{
    if (data->m_animationTimer.isActive())
        data->m_animationTimer.stop();
}

void ScrollAnimatorWin::animateScroll(PerAxisData* data)
{
    // Note on smooth scrolling perf versus non-smooth scrolling perf:
    // The total time to perform a complete scroll is given by
    //   t = t0 + 0.5tA - tD + tS
    // Where
    //   t0 = The time to perform the scroll without smooth scrolling
    //   tA = The acceleration time,
    //        ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() (see below)
    //   tD = |animationTimerDelay|
    //   tS = A value less than or equal to the time required to perform a
    //        single scroll increment, i.e. the work done due to calling
    //        client()->valueChanged() (~0 for simple pages, larger for complex
    //        pages).
    //
    // Because tA and tD are fairly small, the total lag (as users perceive it)
    // is negligible for simple pages and roughly tS for complex pages.  Without
    // knowing in advance how large tS is it's hard to do better than this.
    // Perhaps we could try to remember previous values and forward-compensate.


    // We want to update the scroll position based on the time it's been since
    // our last update.  This may be longer than our ideal time, especially if
    // the page is complex or the system is slow.
    //
    // To avoid feeling laggy, if we've just started smooth scrolling we pretend
    // we've already accelerated for one ideal interval, so that we'll scroll at
    // least some distance immediately.
    double lastScrollInterval = data->m_currentVelocity ? (WTF::currentTime() - data->m_lastAnimationTime) : animationTimerDelay;

    // Figure out how far we've actually traveled and update our current
    // velocity.
    float distanceTraveled;
    if (data->m_currentVelocity < data->m_desiredVelocity) {
        // We accelerate at a constant rate until we reach the desired velocity.
        float accelerationRate = data->m_desiredVelocity / accelerationTime();

        // Figure out whether contant acceleration has caused us to reach our
        // target velocity.
        float potentialVelocityChange = accelerationRate * lastScrollInterval;
        float potentialNewVelocity = data->m_currentVelocity + potentialVelocityChange;
        if (potentialNewVelocity > data->m_desiredVelocity) {
            // We reached the target velocity at some point between our last
            // update and now.  The distance traveled can be calculated in two
            // pieces: the distance traveled while accelerating, and the
            // distance traveled after reaching the target velocity.
            float actualVelocityChange = data->m_desiredVelocity - data->m_currentVelocity;
            float accelerationInterval = actualVelocityChange / accelerationRate;
            // The distance traveled under constant acceleration is the area
            // under a line segment with a constant rising slope.  Break this
            // into a triangular portion atop a rectangular portion and sum.
            distanceTraveled = ((data->m_currentVelocity + (actualVelocityChange / 2)) * accelerationInterval);
            // The distance traveled at the target velocity is simply
            // (target velocity) * (remaining time after accelerating).
            distanceTraveled += (data->m_desiredVelocity * (lastScrollInterval - accelerationInterval));
            data->m_currentVelocity = data->m_desiredVelocity;
        } else {
            // Constant acceleration through the entire time interval.
            distanceTraveled = (data->m_currentVelocity + (potentialVelocityChange / 2)) * lastScrollInterval;
            data->m_currentVelocity = potentialNewVelocity;
        }
    } else {
        // We've already reached the target velocity, so the distance we've
        // traveled is simply (current velocity) * (elapsed time).
        distanceTraveled = data->m_currentVelocity * lastScrollInterval;
        // If our desired velocity has decreased, drop the current velocity too.
        data->m_currentVelocity = data->m_desiredVelocity;
    }

    // Now update the scroll position based on the distance traveled.
    if (distanceTraveled >= fabs(data->m_desiredPos - *data->m_currentPos)) {
        // We've traveled far enough to reach the desired position.  Stop smooth
        // scrolling.
        *data->m_currentPos = data->m_desiredPos;
        data->m_currentVelocity = 0;
        data->m_desiredVelocity = 0;
    } else {
        // Not yet at the target position.  Travel towards it and set up the
        // next update.
        if (*data->m_currentPos > data->m_desiredPos)
            distanceTraveled = -distanceTraveled;
        *data->m_currentPos += distanceTraveled;
        data->m_animationTimer.startOneShot(animationTimerDelay);
        data->m_lastAnimationTime = WTF::currentTime();
    }

    notityPositionChanged();
}

} // namespace WebCore

#endif // ENABLE(SMOOTH_SCROLLING)