2 * Copyright (C) 2017 Igalia S.L. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
27 #import "ScrollingTreeScrollingNodeDelegateIOS.h"
29 #if PLATFORM(IOS_FAMILY) && ENABLE(ASYNC_SCROLLING)
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>
41 #if ENABLE(CSS_SCROLL_SNAP)
42 #import <WebCore/AxisScrollSnapOffsets.h>
43 #import <WebCore/ScrollSnapOffsetsInfo.h>
46 @implementation WKScrollingNodeScrollViewDelegate
48 - (instancetype)initWithScrollingTreeNodeDelegate:(WebKit::ScrollingTreeScrollingNodeDelegateIOS*)delegate
50 if ((self = [super init]))
51 _scrollingTreeNodeDelegate = delegate;
56 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
58 _scrollingTreeNodeDelegate->scrollViewDidScroll(scrollView.contentOffset, _inUserInteraction);
61 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
63 _inUserInteraction = YES;
65 if (scrollView.panGestureRecognizer.state == UIGestureRecognizerStateBegan)
66 _scrollingTreeNodeDelegate->scrollViewWillStartPanGesture();
67 _scrollingTreeNodeDelegate->scrollWillStart();
70 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
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) {
79 if (!touchActions.contains(WebCore::TouchAction::PanX)) {
81 targetContentOffset->x = scrollView.contentOffset.x;
83 if (!touchActions.contains(WebCore::TouchAction::PanY)) {
85 targetContentOffset->y = scrollView.contentOffset.y;
92 #if ENABLE(CSS_SCROLL_SNAP)
93 CGFloat horizontalTarget = targetContentOffset->x;
94 CGFloat verticalTarget = targetContentOffset->y;
96 unsigned originalHorizontalSnapPosition = _scrollingTreeNodeDelegate->scrollingNode().currentHorizontalSnapPointIndex();
97 unsigned originalVerticalSnapPosition = _scrollingTreeNodeDelegate->scrollingNode().currentVerticalSnapPointIndex();
99 if (!_scrollingTreeNodeDelegate->scrollingNode().horizontalSnapOffsets().isEmpty()) {
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;
107 if (!_scrollingTreeNodeDelegate->scrollingNode().verticalSnapOffsets().isEmpty()) {
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;
115 if (originalHorizontalSnapPosition != _scrollingTreeNodeDelegate->scrollingNode().currentHorizontalSnapPointIndex()
116 || originalVerticalSnapPosition != _scrollingTreeNodeDelegate->scrollingNode().currentVerticalSnapPointIndex()) {
117 _scrollingTreeNodeDelegate->currentSnapPointIndicesDidChange(_scrollingTreeNodeDelegate->scrollingNode().currentHorizontalSnapPointIndex(), _scrollingTreeNodeDelegate->scrollingNode().currentVerticalSnapPointIndex());
122 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)willDecelerate
124 if (_inUserInteraction && !willDecelerate) {
125 _inUserInteraction = NO;
126 _scrollingTreeNodeDelegate->scrollViewDidScroll(scrollView.contentOffset, _inUserInteraction);
127 _scrollingTreeNodeDelegate->scrollDidEnd();
131 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
133 if (_inUserInteraction) {
134 _inUserInteraction = NO;
135 _scrollingTreeNodeDelegate->scrollViewDidScroll(scrollView.contentOffset, _inUserInteraction);
136 _scrollingTreeNodeDelegate->scrollDidEnd();
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
143 auto touchActionData = _scrollingTreeNodeDelegate->touchActionData();
144 if (!touchActionData)
147 auto touchActions = touchActionData->touchActions;
148 if (touchActions == WebCore::TouchAction::Auto || touchActions == WebCore::TouchAction::Manipulation)
151 CGPoint adjustedContentOffset = CGPointMake(offset.x, offset.y);
153 if (!touchActions.contains(WebCore::TouchAction::PanX))
154 adjustedContentOffset.x = start.x;
155 if (!touchActions.contains(WebCore::TouchAction::PanY))
156 adjustedContentOffset.y = start.y;
158 return adjustedContentOffset;
165 using namespace WebCore;
167 ScrollingTreeScrollingNodeDelegateIOS::ScrollingTreeScrollingNodeDelegateIOS(ScrollingTreeScrollingNode& scrollingNode)
168 : ScrollingTreeScrollingNodeDelegate(scrollingNode)
169 , m_updatingFromStateNode(false)
173 ScrollingTreeScrollingNodeDelegateIOS::~ScrollingTreeScrollingNodeDelegateIOS()
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;
182 END_BLOCK_OBJC_EXCEPTIONS
185 void ScrollingTreeScrollingNodeDelegateIOS::resetScrollViewDelegate()
187 BEGIN_BLOCK_OBJC_EXCEPTIONS
188 if (UIScrollView *scrollView = (UIScrollView *)[scrollLayer() delegate]) {
189 ASSERT([scrollView isKindOfClass:[UIScrollView self]]);
190 scrollView.delegate = nil;
192 END_BLOCK_OBJC_EXCEPTIONS
195 void ScrollingTreeScrollingNodeDelegateIOS::commitStateBeforeChildren(const ScrollingStateScrollingNode& scrollingStateNode)
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;
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;
213 void ScrollingTreeScrollingNodeDelegateIOS::commitStateAfterChildren(const ScrollingStateScrollingNode& scrollingStateNode)
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]]);
226 if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollContainerLayer)
227 || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrolledContentsLayer)) {
228 if (!m_scrollViewDelegate)
229 m_scrollViewDelegate = adoptNS([[WKScrollingNodeScrollViewDelegate alloc] initWithScrollingTreeNodeDelegate:this]);
231 scrollView.scrollsToTop = NO;
232 scrollView.delegate = m_scrollViewDelegate.get();
235 bool recomputeInsets = scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::TotalContentsSize);
236 if (scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ReachableContentsSize)) {
237 scrollView.contentSize = scrollingStateNode.reachableContentsSize();
238 recomputeInsets = true;
241 if ((scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollPosition)
242 || scrollingStateNode.hasChangedProperty(ScrollingStateScrollingNode::ScrollOrigin))
243 && ![m_scrollViewDelegate _isInUserInteraction]) {
244 scrollView.contentOffset = scrollingStateNode.scrollPosition() + scrollOrigin();
245 recomputeInsets = true;
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();
254 if (scrollOrigin().y())
255 insets.top = reachableContentsSize().height() - totalContentsSize().height();
257 scrollView.contentInset = insets;
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;
265 END_BLOCK_OBJC_EXCEPTIONS
269 void ScrollingTreeScrollingNodeDelegateIOS::updateLayersAfterAncestorChange(const ScrollingTreeNode& changedNode, const FloatRect& fixedPositionRect, const FloatSize& cumulativeDelta)
271 if (!scrollingNode().children())
274 FloatSize scrollDelta = lastCommittedScrollPosition() - scrollingNode().scrollPosition();
276 for (auto& child : *scrollingNode().children())
277 child->updateLayersAfterAncestorChange(changedNode, fixedPositionRect, cumulativeDelta + scrollDelta);
280 FloatPoint ScrollingTreeScrollingNodeDelegateIOS::scrollPosition() const
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
289 void ScrollingTreeScrollingNodeDelegateIOS::setScrollLayerPosition(const FloatPoint& scrollPosition)
291 [m_scrollLayer setPosition:CGPointMake(scrollPosition.x() + scrollOrigin().x(), scrollPosition.y() + scrollOrigin().y())];
292 updateChildNodesAfterScroll(scrollPosition);
295 void ScrollingTreeScrollingNodeDelegateIOS::updateChildNodesAfterScroll(const FloatPoint& scrollPosition)
297 if (!scrollingNode().children())
300 FloatRect fixedPositionRect;
301 auto* frameNode = scrollingNode().enclosingFrameNodeIncludingSelf();
302 if (frameNode && frameNode->nodeType() == ScrollingNodeType::Subframe)
303 fixedPositionRect = frameNode->fixedPositionRect();
305 fixedPositionRect = scrollingTree().fixedPositionRect();
306 FloatSize scrollDelta = lastCommittedScrollPosition() - scrollPosition;
308 for (auto& child : *scrollingNode().children())
309 child->updateLayersAfterAncestorChange(scrollingNode(), fixedPositionRect, scrollDelta);
312 void ScrollingTreeScrollingNodeDelegateIOS::scrollWillStart() const
314 scrollingTree().scrollingTreeNodeWillStartScroll();
317 void ScrollingTreeScrollingNodeDelegateIOS::scrollDidEnd() const
319 scrollingTree().scrollingTreeNodeDidEndScroll();
322 void ScrollingTreeScrollingNodeDelegateIOS::scrollViewWillStartPanGesture() const
324 scrollingTree().scrollingTreeNodeWillStartPanGesture();
327 void ScrollingTreeScrollingNodeDelegateIOS::scrollViewDidScroll(const FloatPoint& scrollPosition, bool inUserInteraction) const
329 if (m_updatingFromStateNode)
332 scrollingTree().scrollPositionChangedViaDelegatedScrolling(scrollingNode().scrollingNodeID(), scrollPosition, inUserInteraction);
335 void ScrollingTreeScrollingNodeDelegateIOS::currentSnapPointIndicesDidChange(unsigned horizontal, unsigned vertical) const
337 if (m_updatingFromStateNode)
340 scrollingTree().currentSnapPointIndicesDidChange(scrollingNode().scrollingNodeID(), horizontal, vertical);
343 #if ENABLE(POINTER_EVENTS)
344 Optional<TouchActionData> ScrollingTreeScrollingNodeDelegateIOS::touchActionData() const
346 return downcast<RemoteScrollingTree>(scrollingTree()).scrollingCoordinatorProxy().touchActionDataForScrollNodeID(scrollingNode().scrollingNodeID());
350 } // namespace WebKit
352 #endif // PLATFORM(IOS_FAMILY) && ENABLE(ASYNC_SCROLLING)