Rubber-banding is often not smooth on infinitely scrolling websites
[WebKit-https.git] / Source / WebCore / page / scrolling / mac / ScrollingTreeScrollingNodeMac.mm
1 /*
2  * Copyright (C) 2012 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 #import "config.h"
27 #import "ScrollingTreeScrollingNodeMac.h"
28
29 #if ENABLE(THREADED_SCROLLING)
30
31 #import "FrameView.h"
32 #import "NSScrollerImpDetails.h"
33 #import "PlatformWheelEvent.h"
34 #import "ScrollingCoordinator.h"
35 #import "ScrollingTree.h"
36 #import "ScrollingStateTree.h"
37 #import "Settings.h"
38 #import "TileController.h"
39 #import "WebTileLayer.h"
40
41 #import <QuartzCore/QuartzCore.h>
42 #import <wtf/CurrentTime.h>
43 #import <wtf/Deque.h>
44 #import <wtf/text/StringBuilder.h>
45 #import <wtf/text/CString.h>
46
47 namespace WebCore {
48
49 static void logThreadedScrollingMode(unsigned mainThreadScrollingReasons);
50 static void logWheelEventHandlerCountChanged(unsigned);
51
52
53 PassOwnPtr<ScrollingTreeScrollingNode> ScrollingTreeScrollingNode::create(ScrollingTree* scrollingTree, ScrollingNodeID nodeID)
54 {
55     return adoptPtr(new ScrollingTreeScrollingNodeMac(scrollingTree, nodeID));
56 }
57
58 ScrollingTreeScrollingNodeMac::ScrollingTreeScrollingNodeMac(ScrollingTree* scrollingTree, ScrollingNodeID nodeID)
59     : ScrollingTreeScrollingNode(scrollingTree, nodeID)
60     , m_scrollElasticityController(this)
61     , m_verticalScrollbarPainter(0)
62     , m_horizontalScrollbarPainter(0)
63     , m_lastScrollHadUnfilledPixels(false)
64 {
65 }
66
67 ScrollingTreeScrollingNodeMac::~ScrollingTreeScrollingNodeMac()
68 {
69     if (m_snapRubberbandTimer)
70         CFRunLoopTimerInvalidate(m_snapRubberbandTimer.get());
71 }
72
73 void ScrollingTreeScrollingNodeMac::updateBeforeChildren(ScrollingStateNode* stateNode)
74 {
75     ScrollingTreeScrollingNode::updateBeforeChildren(stateNode);
76     ScrollingStateScrollingNode* scrollingStateNode = toScrollingStateScrollingNode(stateNode);
77
78     if (scrollingStateNode->hasChangedProperty(ScrollingStateNode::ScrollLayer))
79         m_scrollLayer = scrollingStateNode->platformScrollLayer();
80
81     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::CounterScrollingLayer))
82         m_counterScrollingLayer = scrollingStateNode->counterScrollingPlatformLayer();
83
84     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::HeaderLayer))
85         m_headerLayer = scrollingStateNode->headerPlatformLayer();
86
87     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::FooterLayer))
88         m_footerLayer = scrollingStateNode->footerPlatformLayer();
89
90     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::PainterForScrollbar)) {
91         m_verticalScrollbarPainter = scrollingStateNode->verticalScrollbarPainter();
92         m_horizontalScrollbarPainter = scrollingStateNode->horizontalScrollbarPainter();
93     }
94
95     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::ShouldUpdateScrollLayerPositionOnMainThread)) {
96         unsigned mainThreadScrollingReasons = this->shouldUpdateScrollLayerPositionOnMainThread();
97
98         if (mainThreadScrollingReasons) {
99             // We're transitioning to the slow "update scroll layer position on the main thread" mode.
100             // Initialize the probable main thread scroll position with the current scroll layer position.
101             if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::RequestedScrollPosition))
102                 m_probableMainThreadScrollPosition = scrollingStateNode->requestedScrollPosition();
103             else {
104                 CGPoint scrollLayerPosition = m_scrollLayer.get().position;
105                 m_probableMainThreadScrollPosition = IntPoint(-scrollLayerPosition.x, -scrollLayerPosition.y);
106             }
107         }
108
109         if (scrollingTree()->scrollingPerformanceLoggingEnabled())
110             logThreadedScrollingMode(mainThreadScrollingReasons);
111     }
112
113     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::WheelEventHandlerCount)) {
114         if (scrollingTree()->scrollingPerformanceLoggingEnabled())
115             logWheelEventHandlerCountChanged(scrollingStateNode->wheelEventHandlerCount());
116     }
117 }
118
119 void ScrollingTreeScrollingNodeMac::updateAfterChildren(ScrollingStateNode* stateNode)
120 {
121     ScrollingTreeScrollingNode::updateAfterChildren(stateNode);
122
123     ScrollingStateScrollingNode* scrollingStateNode = toScrollingStateScrollingNode(stateNode);
124
125     // Update the scroll position after child nodes have been updated, because they need to have updated their constraints before any scrolling happens.
126     if (scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::RequestedScrollPosition))
127         setScrollPosition(scrollingStateNode->requestedScrollPosition());
128
129     if (scrollingStateNode->hasChangedProperty(ScrollingStateNode::ScrollLayer) || scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::TotalContentsSize) || scrollingStateNode->hasChangedProperty(ScrollingStateScrollingNode::ViewportRect))
130         updateMainFramePinState(scrollPosition());
131 }
132
133 void ScrollingTreeScrollingNodeMac::handleWheelEvent(const PlatformWheelEvent& wheelEvent)
134 {
135     if (!canHaveScrollbars())
136         return;
137
138     m_scrollElasticityController.handleWheelEvent(wheelEvent);
139     scrollingTree()->handleWheelEventPhase(wheelEvent.phase());
140 }
141
142 bool ScrollingTreeScrollingNodeMac::allowsHorizontalStretching()
143 {
144     switch (horizontalScrollElasticity()) {
145     case ScrollElasticityAutomatic:
146         return hasEnabledHorizontalScrollbar() || !hasEnabledVerticalScrollbar();
147     case ScrollElasticityNone:
148         return false;
149     case ScrollElasticityAllowed:
150         return true;
151     }
152
153     ASSERT_NOT_REACHED();
154     return false;
155 }
156
157 bool ScrollingTreeScrollingNodeMac::allowsVerticalStretching()
158 {
159     switch (verticalScrollElasticity()) {
160     case ScrollElasticityAutomatic:
161         return hasEnabledVerticalScrollbar() || !hasEnabledHorizontalScrollbar();
162     case ScrollElasticityNone:
163         return false;
164     case ScrollElasticityAllowed:
165         return true;
166     }
167
168     ASSERT_NOT_REACHED();
169     return false;
170 }
171
172 IntSize ScrollingTreeScrollingNodeMac::stretchAmount()
173 {
174     IntSize stretch;
175
176     if (scrollPosition().y() < minimumScrollPosition().y())
177         stretch.setHeight(scrollPosition().y() - minimumScrollPosition().y());
178     else if (scrollPosition().y() > maximumScrollPosition().y())
179         stretch.setHeight(scrollPosition().y() - maximumScrollPosition().y());
180
181     if (scrollPosition().x() < minimumScrollPosition().x())
182         stretch.setWidth(scrollPosition().x() - minimumScrollPosition().x());
183     else if (scrollPosition().x() > maximumScrollPosition().x())
184         stretch.setWidth(scrollPosition().x() - maximumScrollPosition().x());
185
186     if (scrollingTree()->rootNode() == this) {
187         if (stretch.isZero())
188             scrollingTree()->setMainFrameIsRubberBanding(false);
189         else
190             scrollingTree()->setMainFrameIsRubberBanding(true);
191     }
192
193     return stretch;
194 }
195
196 bool ScrollingTreeScrollingNodeMac::pinnedInDirection(const FloatSize& delta)
197 {
198     FloatSize limitDelta;
199
200     if (fabsf(delta.height()) >= fabsf(delta.width())) {
201         if (delta.height() < 0) {
202             // We are trying to scroll up.  Make sure we are not pinned to the top
203             limitDelta.setHeight(scrollPosition().y() - minimumScrollPosition().y());
204         } else {
205             // We are trying to scroll down.  Make sure we are not pinned to the bottom
206             limitDelta.setHeight(maximumScrollPosition().y() - scrollPosition().y());
207         }
208     } else if (delta.width()) {
209         if (delta.width() < 0) {
210             // We are trying to scroll left.  Make sure we are not pinned to the left
211             limitDelta.setHeight(scrollPosition().x() - minimumScrollPosition().x());
212         } else {
213             // We are trying to scroll right.  Make sure we are not pinned to the right
214             limitDelta.setHeight(maximumScrollPosition().x() - scrollPosition().x());
215         }
216     }
217
218     if ((delta.width() || delta.height()) && (limitDelta.width() < 1 && limitDelta.height() < 1))
219         return true;
220
221     return false;
222 }
223
224 bool ScrollingTreeScrollingNodeMac::canScrollHorizontally()
225 {
226     return hasEnabledHorizontalScrollbar();
227 }
228
229 bool ScrollingTreeScrollingNodeMac::canScrollVertically()
230 {
231     return hasEnabledVerticalScrollbar();
232 }
233
234 bool ScrollingTreeScrollingNodeMac::shouldRubberBandInDirection(ScrollDirection)
235 {
236     return true;
237 }
238
239 IntPoint ScrollingTreeScrollingNodeMac::absoluteScrollPosition()
240 {
241     return scrollPosition();
242 }
243
244 void ScrollingTreeScrollingNodeMac::immediateScrollBy(const FloatSize& offset)
245 {
246     scrollBy(roundedIntSize(offset));
247 }
248
249 void ScrollingTreeScrollingNodeMac::immediateScrollByWithoutContentEdgeConstraints(const FloatSize& offset)
250 {
251     scrollByWithoutContentEdgeConstraints(roundedIntSize(offset));
252 }
253
254 void ScrollingTreeScrollingNodeMac::startSnapRubberbandTimer()
255 {
256     ASSERT(!m_snapRubberbandTimer);
257
258     CFTimeInterval timerInterval = 1.0 / 60.0;
259
260     m_snapRubberbandTimer = adoptCF(CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + timerInterval, timerInterval, 0, 0, ^(CFRunLoopTimerRef) {
261         m_scrollElasticityController.snapRubberBandTimerFired();
262     }));
263     CFRunLoopAddTimer(CFRunLoopGetCurrent(), m_snapRubberbandTimer.get(), kCFRunLoopDefaultMode);
264 }
265
266 void ScrollingTreeScrollingNodeMac::stopSnapRubberbandTimer()
267 {
268     if (!m_snapRubberbandTimer)
269         return;
270
271     scrollingTree()->setMainFrameIsRubberBanding(false);
272
273     // Since the rubberband timer has stopped, totalContentsSizeForRubberBand can be synchronized with totalContentsSize.
274     setTotalContentsSizeForRubberBand(totalContentsSize());
275
276     CFRunLoopTimerInvalidate(m_snapRubberbandTimer.get());
277     m_snapRubberbandTimer = nullptr;
278 }
279
280 IntPoint ScrollingTreeScrollingNodeMac::scrollPosition() const
281 {
282     if (shouldUpdateScrollLayerPositionOnMainThread())
283         return m_probableMainThreadScrollPosition;
284
285     CGPoint scrollLayerPosition = m_scrollLayer.get().position;
286     return IntPoint(-scrollLayerPosition.x + scrollOrigin().x(), -scrollLayerPosition.y + scrollOrigin().y());
287 }
288
289 void ScrollingTreeScrollingNodeMac::setScrollPosition(const IntPoint& scrollPosition)
290 {
291     IntPoint newScrollPosition = scrollPosition;
292     newScrollPosition = newScrollPosition.shrunkTo(maximumScrollPosition());
293     newScrollPosition = newScrollPosition.expandedTo(minimumScrollPosition());
294
295     setScrollPositionWithoutContentEdgeConstraints(newScrollPosition);
296
297     if (scrollingTree()->scrollingPerformanceLoggingEnabled())
298         logExposedUnfilledArea();
299 }
300
301 void ScrollingTreeScrollingNodeMac::setScrollPositionWithoutContentEdgeConstraints(const IntPoint& scrollPosition)
302 {
303     updateMainFramePinState(scrollPosition);
304
305     if (shouldUpdateScrollLayerPositionOnMainThread()) {
306         m_probableMainThreadScrollPosition = scrollPosition;
307         scrollingTree()->updateMainFrameScrollPosition(scrollPosition, SetScrollingLayerPosition);
308         return;
309     }
310
311     setScrollLayerPosition(scrollPosition);
312     scrollingTree()->updateMainFrameScrollPosition(scrollPosition);
313 }
314
315 void ScrollingTreeScrollingNodeMac::setScrollLayerPosition(const IntPoint& position)
316 {
317     ASSERT(!shouldUpdateScrollLayerPositionOnMainThread());
318     m_scrollLayer.get().position = CGPointMake(-position.x() + scrollOrigin().x(), -position.y() + scrollOrigin().y());
319
320     IntPoint scrollOffset = position - toIntSize(scrollOrigin());
321     IntSize scrollOffsetForFixedChildren = FrameView::scrollOffsetForFixedPosition(viewportRect(), totalContentsSize(), scrollOffset, scrollOrigin(), frameScaleFactor(), false, headerHeight(), footerHeight());
322     if (m_counterScrollingLayer)
323         m_counterScrollingLayer.get().position = FloatPoint(scrollOffsetForFixedChildren);
324
325     // Generally the banners should have the same horizontal-position computation as a fixed element. However,
326     // the banners are not affected by the frameScaleFactor(), so if there is currently a non-1 frameScaleFactor()
327     // then we should recompute scrollOffsetForFixedChildren for the banner with a scale factor of 1.
328     float horizontalScrollOffsetForBanner = scrollOffsetForFixedChildren.width();
329     if (frameScaleFactor() != 1)
330         horizontalScrollOffsetForBanner = FrameView::scrollOffsetForFixedPosition(viewportRect(), totalContentsSize(), scrollOffset, scrollOrigin(), 1, false, headerHeight(), footerHeight()).width();
331
332     if (m_headerLayer)
333         m_headerLayer.get().position = FloatPoint(horizontalScrollOffsetForBanner, 0);
334
335     if (m_footerLayer)
336         m_footerLayer.get().position = FloatPoint(horizontalScrollOffsetForBanner, totalContentsSize().height() - footerHeight());
337
338     IntRect viewportRect = this->viewportRect();
339
340     if (m_verticalScrollbarPainter || m_horizontalScrollbarPainter) {
341         [CATransaction begin];
342         [CATransaction lock];
343
344         if (m_verticalScrollbarPainter) {
345             [m_verticalScrollbarPainter setUsePresentationValue:YES];
346             float presentationValue;
347             float overhangAmount;
348             ScrollableArea::computeScrollbarValueAndOverhang(position.y(), totalContentsSize().height(), viewportRect.height(), presentationValue, overhangAmount);
349             [m_verticalScrollbarPainter setPresentationValue:presentationValue];
350         }
351
352         if (m_horizontalScrollbarPainter) {
353             [m_horizontalScrollbarPainter setUsePresentationValue:YES];
354             float presentationValue;
355             float overhangAmount;
356             ScrollableArea::computeScrollbarValueAndOverhang(position.x(), totalContentsSize().width(), viewportRect.width(), presentationValue, overhangAmount);
357             [m_horizontalScrollbarPainter setPresentationValue:presentationValue];
358         }
359         [CATransaction unlock];
360         [CATransaction commit];
361     }
362
363     if (!m_children)
364         return;
365
366     viewportRect.setLocation(IntPoint(scrollOffsetForFixedChildren));
367
368     size_t size = m_children->size();
369     for (size_t i = 0; i < size; ++i)
370         m_children->at(i)->parentScrollPositionDidChange(viewportRect, FloatSize());
371 }
372
373 IntPoint ScrollingTreeScrollingNodeMac::minimumScrollPosition() const
374 {
375     IntPoint position;
376     
377     if (scrollingTree()->rootNode() == this && scrollingTree()->scrollPinningBehavior() == PinToBottom)
378         position.setY(maximumScrollPosition().y());
379
380     return position;
381 }
382
383 IntPoint ScrollingTreeScrollingNodeMac::maximumScrollPosition() const
384 {
385     IntPoint position(totalContentsSizeForRubberBand().width() - viewportRect().width(),
386                       totalContentsSizeForRubberBand().height() - viewportRect().height());
387
388     position.clampNegativeToZero();
389
390     if (scrollingTree()->rootNode() == this && scrollingTree()->scrollPinningBehavior() == PinToTop)
391         position.setY(minimumScrollPosition().y());
392
393     return position;
394 }
395
396 void ScrollingTreeScrollingNodeMac::scrollBy(const IntSize& offset)
397 {
398     setScrollPosition(scrollPosition() + offset);
399 }
400
401 void ScrollingTreeScrollingNodeMac::scrollByWithoutContentEdgeConstraints(const IntSize& offset)
402 {
403     setScrollPositionWithoutContentEdgeConstraints(scrollPosition() + offset);
404 }
405
406 void ScrollingTreeScrollingNodeMac::updateMainFramePinState(const IntPoint& scrollPosition)
407 {
408     bool pinnedToTheLeft = scrollPosition.x() <= minimumScrollPosition().x();
409     bool pinnedToTheRight = scrollPosition.x() >= maximumScrollPosition().x();
410     bool pinnedToTheTop = scrollPosition.y() <= minimumScrollPosition().y();
411     bool pinnedToTheBottom = scrollPosition.y() >= maximumScrollPosition().y();
412
413     scrollingTree()->setMainFramePinState(pinnedToTheLeft, pinnedToTheRight, pinnedToTheTop, pinnedToTheBottom);
414 }
415
416 void ScrollingTreeScrollingNodeMac::logExposedUnfilledArea()
417 {
418     Region paintedVisibleTiles;
419
420     Deque<CALayer*> layerQueue;
421     layerQueue.append(m_scrollLayer.get());
422     WebTileLayerList tiles;
423
424     while (!layerQueue.isEmpty() && tiles.isEmpty()) {
425         CALayer* layer = layerQueue.takeFirst();
426         NSArray* sublayers = [[layer sublayers] copy];
427
428         // If this layer is the parent of a tile, it is the parent of all of the tiles and nothing else.
429         if ([[sublayers objectAtIndex:0] isKindOfClass:[WebTileLayer class]]) {
430             for (CALayer* sublayer in sublayers) {
431                 ASSERT([sublayer isKindOfClass:[WebTileLayer class]]);
432                 tiles.append(static_cast<WebTileLayer*>(sublayer));
433             }
434         } else {
435             for (CALayer* sublayer in sublayers)
436                 layerQueue.append(sublayer);
437         }
438
439         [sublayers release];
440     }
441
442     IntPoint scrollPosition = this->scrollPosition();
443     unsigned unfilledArea = TileController::blankPixelCountForTiles(tiles, viewportRect(), IntPoint(-scrollPosition.x(), -scrollPosition.y()));
444
445     if (unfilledArea || m_lastScrollHadUnfilledPixels)
446         WTFLogAlways("SCROLLING: Exposed tileless area. Time: %f Unfilled Pixels: %u\n", WTF::monotonicallyIncreasingTime(), unfilledArea);
447
448     m_lastScrollHadUnfilledPixels = unfilledArea;
449 }
450
451 static void logThreadedScrollingMode(unsigned mainThreadScrollingReasons)
452 {
453     if (mainThreadScrollingReasons) {
454         StringBuilder reasonsDescription;
455
456         if (mainThreadScrollingReasons & ScrollingCoordinator::ForcedOnMainThread)
457             reasonsDescription.append("forced,");
458         if (mainThreadScrollingReasons & ScrollingCoordinator::HasSlowRepaintObjects)
459             reasonsDescription.append("slow-repaint objects,");
460         if (mainThreadScrollingReasons & ScrollingCoordinator::HasViewportConstrainedObjectsWithoutSupportingFixedLayers)
461             reasonsDescription.append("viewport-constrained objects,");
462         if (mainThreadScrollingReasons & ScrollingCoordinator::HasNonLayerViewportConstrainedObjects)
463             reasonsDescription.append("non-layer viewport-constrained objects,");
464         if (mainThreadScrollingReasons & ScrollingCoordinator::IsImageDocument)
465             reasonsDescription.append("image document,");
466
467         // Strip the trailing comma.
468         String reasonsDescriptionTrimmed = reasonsDescription.toString().left(reasonsDescription.length() - 1);
469
470         WTFLogAlways("SCROLLING: Switching to main-thread scrolling mode. Time: %f Reason(s): %s\n", WTF::monotonicallyIncreasingTime(), reasonsDescriptionTrimmed.ascii().data());
471     } else
472         WTFLogAlways("SCROLLING: Switching to threaded scrolling mode. Time: %f\n", WTF::monotonicallyIncreasingTime());
473 }
474
475 void logWheelEventHandlerCountChanged(unsigned count)
476 {
477     WTFLogAlways("SCROLLING: Wheel event handler count changed. Time: %f Count: %u\n", WTF::monotonicallyIncreasingTime(), count);
478 }
479
480 } // namespace WebCore
481
482 #endif // ENABLE(THREADED_SCROLLING)