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