afd0ae2bfb0cd6c7f3b8548e18ec4a827ca80189
[WebKit-https.git] / Source / WebKit / UIProcess / RemoteLayerTree / ios / ScrollingTreeScrollingNodeDelegateIOS.mm
1 /*
2  * Copyright (C) 2017 Igalia S.L. 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 "ScrollingTreeScrollingNodeDelegateIOS.h"
28
29 #if PLATFORM(IOS_FAMILY) && ENABLE(ASYNC_SCROLLING)
30
31 #import <QuartzCore/QuartzCore.h>
32 #import <UIKit/UIPanGestureRecognizer.h>
33 #import <UIKit/UIScrollView.h>
34 #import <WebCore/ScrollingStateOverflowScrollingNode.h>
35 #import <WebCore/ScrollingTree.h>
36 #import <WebCore/ScrollingTreeFrameScrollingNode.h>
37 #import <WebCore/ScrollingTreeScrollingNode.h>
38 #import <wtf/BlockObjCExceptions.h>
39 #import <wtf/SetForScope.h>
40
41 #if ENABLE(CSS_SCROLL_SNAP)
42 #import <WebCore/AxisScrollSnapOffsets.h>
43 #import <WebCore/ScrollSnapOffsetsInfo.h>
44 #endif
45
46 @implementation WKScrollingNodeScrollViewDelegate
47
48 - (instancetype)initWithScrollingTreeNodeDelegate:(WebKit::ScrollingTreeScrollingNodeDelegateIOS*)delegate
49 {
50     if ((self = [super init]))
51         _scrollingTreeNodeDelegate = delegate;
52
53     return self;
54 }
55
56 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
57 {
58     _scrollingTreeNodeDelegate->scrollViewDidScroll(scrollView.contentOffset, _inUserInteraction);
59 }
60
61 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
62 {
63     _inUserInteraction = YES;
64
65     if (scrollView.panGestureRecognizer.state == UIGestureRecognizerStateBegan)
66         _scrollingTreeNodeDelegate->scrollViewWillStartPanGesture();
67     _scrollingTreeNodeDelegate->scrollWillStart();
68 }
69
70 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
71 {
72 #if ENABLE(POINTER_EVENTS)
73     if (![scrollView isZooming]) {
74         if (auto touchActionData = _scrollingTreeNodeDelegate->touchActionData()) {
75             auto touchActions = touchActionData->touchActions;
76             if (touchActions != WebCore::TouchAction::Auto && touchActions != WebCore::TouchAction::Manipulation) {
77                 bool canPanX = true;
78                 bool canPanY = true;
79                 if (!touchActions.contains(WebCore::TouchAction::PanX)) {
80                     canPanX = false;
81                     targetContentOffset->x = scrollView.contentOffset.x;
82                 }
83                 if (!touchActions.contains(WebCore::TouchAction::PanY)) {
84                     canPanY = false;
85                     targetContentOffset->y = scrollView.contentOffset.y;
86                 }
87             }
88         }
89     }
90 #endif
91
92 #if ENABLE(CSS_SCROLL_SNAP)
93     CGFloat horizontalTarget = targetContentOffset->x;
94     CGFloat verticalTarget = targetContentOffset->y;
95
96     unsigned originalHorizontalSnapPosition = _scrollingTreeNodeDelegate->scrollingNode().currentHorizontalSnapPointIndex();
97     unsigned originalVerticalSnapPosition = _scrollingTreeNodeDelegate->scrollingNode().currentVerticalSnapPointIndex();
98
99     if (!_scrollingTreeNodeDelegate->scrollingNode().horizontalSnapOffsets().isEmpty()) {
100         unsigned index;
101         float potentialSnapPosition = WebCore::closestSnapOffset(_scrollingTreeNodeDelegate->scrollingNode().horizontalSnapOffsets(), _scrollingTreeNodeDelegate->scrollingNode().horizontalSnapOffsetRanges(), horizontalTarget, velocity.x, index);
102         _scrollingTreeNodeDelegate->scrollingNode().setCurrentHorizontalSnapPointIndex(index);
103         if (horizontalTarget >= 0 && horizontalTarget <= scrollView.contentSize.width)
104             targetContentOffset->x = potentialSnapPosition;
105     }
106
107     if (!_scrollingTreeNodeDelegate->scrollingNode().verticalSnapOffsets().isEmpty()) {
108         unsigned index;
109         float potentialSnapPosition = WebCore::closestSnapOffset(_scrollingTreeNodeDelegate->scrollingNode().verticalSnapOffsets(), _scrollingTreeNodeDelegate->scrollingNode().verticalSnapOffsetRanges(), verticalTarget, velocity.y, index);
110         _scrollingTreeNodeDelegate->scrollingNode().setCurrentVerticalSnapPointIndex(index);
111         if (verticalTarget >= 0 && verticalTarget <= scrollView.contentSize.height)
112             targetContentOffset->y = potentialSnapPosition;
113     }
114
115     if (originalHorizontalSnapPosition != _scrollingTreeNodeDelegate->scrollingNode().currentHorizontalSnapPointIndex()
116         || originalVerticalSnapPosition != _scrollingTreeNodeDelegate->scrollingNode().currentVerticalSnapPointIndex()) {
117         _scrollingTreeNodeDelegate->currentSnapPointIndicesDidChange(_scrollingTreeNodeDelegate->scrollingNode().currentHorizontalSnapPointIndex(), _scrollingTreeNodeDelegate->scrollingNode().currentVerticalSnapPointIndex());
118     }
119 #endif
120 }
121
122 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)willDecelerate
123 {
124     if (_inUserInteraction && !willDecelerate) {
125         _inUserInteraction = NO;
126         _scrollingTreeNodeDelegate->scrollViewDidScroll(scrollView.contentOffset, _inUserInteraction);
127         _scrollingTreeNodeDelegate->scrollDidEnd();
128     }
129 }
130
131 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
132 {
133     if (_inUserInteraction) {
134         _inUserInteraction = NO;
135         _scrollingTreeNodeDelegate->scrollViewDidScroll(scrollView.contentOffset, _inUserInteraction);
136         _scrollingTreeNodeDelegate->scrollDidEnd();
137     }
138 }
139
140 #if ENABLE(POINTER_EVENTS)
141 - (CGPoint)_scrollView:(UIScrollView *)scrollView adjustedOffsetForOffset:(CGPoint)offset translation:(CGPoint)translation startPoint:(CGPoint)start locationInView:(CGPoint)locationInView horizontalVelocity:(inout double *)hv verticalVelocity:(inout double *)vv
142 {
143     auto touchActionData = _scrollingTreeNodeDelegate->touchActionData();
144     if (!touchActionData)
145         return offset;
146
147     auto touchActions = touchActionData->touchActions;
148     if (touchActions == WebCore::TouchAction::Auto || touchActions == WebCore::TouchAction::Manipulation)
149         return offset;
150
151     CGPoint adjustedContentOffset = CGPointMake(offset.x, offset.y);
152
153     if (!touchActions.contains(WebCore::TouchAction::PanX))
154         adjustedContentOffset.x = start.x;
155     if (!touchActions.contains(WebCore::TouchAction::PanY))
156         adjustedContentOffset.y = start.y;
157
158     return adjustedContentOffset;
159 }
160 #endif
161
162 @end
163
164 namespace WebKit {
165 using namespace WebCore;
166
167 ScrollingTreeScrollingNodeDelegateIOS::ScrollingTreeScrollingNodeDelegateIOS(ScrollingTreeScrollingNode& scrollingNode)
168     : ScrollingTreeScrollingNodeDelegate(scrollingNode)
169     , m_updatingFromStateNode(false)
170 {
171 }
172
173 ScrollingTreeScrollingNodeDelegateIOS::~ScrollingTreeScrollingNodeDelegateIOS()
174 {
175     BEGIN_BLOCK_OBJC_EXCEPTIONS
176     if (UIScrollView *scrollView = (UIScrollView *)[scrollLayer() delegate]) {
177         ASSERT([scrollView isKindOfClass:[UIScrollView self]]);
178         // The scrollView may have been adopted by another node, so only clear the delegate if it's ours.
179         if (scrollView.delegate == m_scrollViewDelegate.get())
180             scrollView.delegate = nil;
181     }
182     END_BLOCK_OBJC_EXCEPTIONS
183 }
184
185 void ScrollingTreeScrollingNodeDelegateIOS::resetScrollViewDelegate()
186 {
187     BEGIN_BLOCK_OBJC_EXCEPTIONS
188     if (UIScrollView *scrollView = (UIScrollView *)[scrollLayer() delegate]) {
189         ASSERT([scrollView isKindOfClass:[UIScrollView self]]);
190         scrollView.delegate = nil;
191     }
192     END_BLOCK_OBJC_EXCEPTIONS
193 }
194
195 void ScrollingTreeScrollingNodeDelegateIOS::commitStateBeforeChildren(const ScrollingStateScrollingNode& scrollingStateNode)
196 {
197     if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollContainerLayer)) {
198         RetainPtr<CALayer> layer;
199         layer = scrollingStateNode.scrollContainerLayer();
200         if ([[layer delegate] isKindOfClass:[UIScrollView self]])
201             m_scrollLayer = layer;
202     }
203
204     // FIMXE: ScrollContainerLayer should always be the UIScrollView layer.
205     if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrolledContentsLayer)) {
206         RetainPtr<CALayer> layer;
207         layer = scrollingStateNode.scrolledContentsLayer();
208         if ([[layer delegate] isKindOfClass:[UIScrollView self]])
209             m_scrollLayer = layer;
210     }
211 }
212
213 void ScrollingTreeScrollingNodeDelegateIOS::commitStateAfterChildren(const ScrollingStateScrollingNode& scrollingStateNode)
214 {
215     SetForScope<bool> updatingChange(m_updatingFromStateNode, true);
216     if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollContainerLayer)
217         || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrolledContentsLayer)
218         || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::TotalContentsSize)
219         || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ReachableContentsSize)
220         || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollPosition)
221         || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollOrigin)) {
222         BEGIN_BLOCK_OBJC_EXCEPTIONS
223         UIScrollView *scrollView = (UIScrollView *)[scrollLayer() delegate];
224         ASSERT([scrollView isKindOfClass:[UIScrollView self]]);
225
226         if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollContainerLayer)
227             || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrolledContentsLayer)) {
228             if (!m_scrollViewDelegate)
229                 m_scrollViewDelegate = adoptNS([[WKScrollingNodeScrollViewDelegate alloc] initWithScrollingTreeNodeDelegate:this]);
230
231             scrollView.scrollsToTop = NO;
232             scrollView.delegate = m_scrollViewDelegate.get();
233         }
234
235         bool recomputeInsets = scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::TotalContentsSize);
236         if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ReachableContentsSize)) {
237             scrollView.contentSize = scrollingStateNode.reachableContentsSize();
238             recomputeInsets = true;
239         }
240
241         if ((scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollPosition)
242             || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollOrigin))
243             && ![m_scrollViewDelegate _isInUserInteraction]) {
244             scrollView.contentOffset = scrollingStateNode.scrollPosition() + scrollOrigin();
245             recomputeInsets = true;
246         }
247
248         if (recomputeInsets) {
249             UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 0, 0);
250             // With RTL or bottom-to-top scrolling (non-zero origin), we need extra space on the left or top.
251             if (scrollOrigin().x())
252                 insets.left = reachableContentsSize().width() - totalContentsSize().width();
253
254             if (scrollOrigin().y())
255                 insets.top = reachableContentsSize().height() - totalContentsSize().height();
256
257             scrollView.contentInset = insets;
258         }
259
260 #if ENABLE(CSS_SCROLL_SNAP)
261         // FIXME: If only one axis snaps in 2D scrolling, the other axis will decelerate fast as well. Is this what we want?
262         if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::HorizontalSnapOffsets) || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::VerticalSnapOffsets))
263             scrollView.decelerationRate = scrollingNode().horizontalSnapOffsets().size() || scrollingNode().verticalSnapOffsets().size() ? UIScrollViewDecelerationRateFast : UIScrollViewDecelerationRateNormal;
264 #endif
265         END_BLOCK_OBJC_EXCEPTIONS
266     }
267 }
268
269 void ScrollingTreeScrollingNodeDelegateIOS::updateLayersAfterAncestorChange(const ScrollingTreeNode& changedNode, const FloatRect& fixedPositionRect, const FloatSize& cumulativeDelta)
270 {
271     if (!scrollingNode().children())
272         return;
273
274     FloatSize scrollDelta = lastCommittedScrollPosition() - scrollingNode().scrollPosition();
275
276     for (auto& child : *scrollingNode().children())
277         child->updateLayersAfterAncestorChange(changedNode, fixedPositionRect, cumulativeDelta + scrollDelta);
278 }
279
280 FloatPoint ScrollingTreeScrollingNodeDelegateIOS::scrollPosition() const
281 {
282     BEGIN_BLOCK_OBJC_EXCEPTIONS
283     UIScrollView *scrollView = (UIScrollView *)[scrollLayer() delegate];
284     ASSERT([scrollView isKindOfClass:[UIScrollView self]]);
285     return [scrollView contentOffset];
286     END_BLOCK_OBJC_EXCEPTIONS
287 }
288
289 void ScrollingTreeScrollingNodeDelegateIOS::setScrollLayerPosition(const FloatPoint& scrollPosition)
290 {
291     [m_scrollLayer setPosition:CGPointMake(scrollPosition.x() + scrollOrigin().x(), scrollPosition.y() + scrollOrigin().y())];
292     updateChildNodesAfterScroll(scrollPosition);
293 }
294
295 void ScrollingTreeScrollingNodeDelegateIOS::updateChildNodesAfterScroll(const FloatPoint& scrollPosition)
296 {
297     if (!scrollingNode().children())
298         return;
299
300     FloatRect fixedPositionRect;
301     auto* frameNode = scrollingNode().enclosingFrameNodeIncludingSelf();
302     if (frameNode && frameNode->nodeType() == ScrollingNodeType::Subframe)
303         fixedPositionRect = frameNode->fixedPositionRect();
304     else
305         fixedPositionRect = scrollingTree().fixedPositionRect();
306     FloatSize scrollDelta = lastCommittedScrollPosition() - scrollPosition;
307
308     for (auto& child : *scrollingNode().children())
309         child->updateLayersAfterAncestorChange(scrollingNode(), fixedPositionRect, scrollDelta);
310 }
311
312 void ScrollingTreeScrollingNodeDelegateIOS::scrollWillStart() const
313 {
314     scrollingTree().scrollingTreeNodeWillStartScroll();
315 }
316
317 void ScrollingTreeScrollingNodeDelegateIOS::scrollDidEnd() const
318 {
319     scrollingTree().scrollingTreeNodeDidEndScroll();
320 }
321
322 void ScrollingTreeScrollingNodeDelegateIOS::scrollViewWillStartPanGesture() const
323 {
324     scrollingTree().scrollingTreeNodeWillStartPanGesture();
325 }
326
327 void ScrollingTreeScrollingNodeDelegateIOS::scrollViewDidScroll(const FloatPoint& scrollPosition, bool inUserInteraction) const
328 {
329     if (m_updatingFromStateNode)
330         return;
331
332     scrollingTree().scrollPositionChangedViaDelegatedScrolling(scrollingNode().scrollingNodeID(), scrollPosition, inUserInteraction);
333 }
334
335 void ScrollingTreeScrollingNodeDelegateIOS::currentSnapPointIndicesDidChange(unsigned horizontal, unsigned vertical) const
336 {
337     if (m_updatingFromStateNode)
338         return;
339
340     scrollingTree().currentSnapPointIndicesDidChange(scrollingNode().scrollingNodeID(), horizontal, vertical);
341 }
342
343 #if ENABLE(POINTER_EVENTS)
344 Optional<TouchActionData> ScrollingTreeScrollingNodeDelegateIOS::touchActionData() const
345 {
346     return downcast<RemoteScrollingTree>(scrollingTree()).scrollingCoordinatorProxy().touchActionDataForScrollNodeID(scrollingNode().scrollingNodeID());
347 }
348 #endif
349
350 } // namespace WebKit
351
352 #endif // PLATFORM(IOS_FAMILY) && ENABLE(ASYNC_SCROLLING)