[iOS WK2] Page jumps when rubber-banding on azuremagazine.com
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / WKScrollView.mm
1 /*
2  * Copyright (C) 2013, 2014 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 "WKScrollView.h"
28
29 #if PLATFORM(IOS)
30
31 #import "WKWebViewInternal.h"
32 #import <CoreGraphics/CGFloat.h>
33
34 @interface UIScrollView (UIScrollViewInternalHack)
35 - (CGFloat)_rubberBandOffsetForOffset:(CGFloat)newOffset maxOffset:(CGFloat)maxOffset minOffset:(CGFloat)minOffset range:(CGFloat)range outside:(BOOL *)outside;
36 @end
37
38 @interface WKScrollViewDelegateForwarder : NSObject <UIScrollViewDelegate>
39
40 - (instancetype)initWithInternalDelegate:(WKWebView *)internalDelegate externalDelegate:(id <UIScrollViewDelegate>)externalDelegate;
41
42 @end
43
44 @implementation WKScrollViewDelegateForwarder {
45     WKWebView *_internalDelegate;
46     id <UIScrollViewDelegate> _externalDelegate;
47 }
48
49 - (instancetype)initWithInternalDelegate:(WKWebView <UIScrollViewDelegate> *)internalDelegate externalDelegate:(id <UIScrollViewDelegate>)externalDelegate
50 {
51     self = [super init];
52     if (!self)
53         return nil;
54     _internalDelegate = internalDelegate;
55     _externalDelegate = externalDelegate;
56     return self;
57 }
58
59 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
60 {
61     NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
62     if (!signature)
63         signature = [(NSObject *)_internalDelegate methodSignatureForSelector:aSelector];
64     if (!signature)
65         signature = [(NSObject *)_externalDelegate methodSignatureForSelector:aSelector];
66     return signature;
67 }
68
69 - (BOOL)respondsToSelector:(SEL)aSelector
70 {
71     return [super respondsToSelector:aSelector] || [_internalDelegate respondsToSelector:aSelector] || [_externalDelegate respondsToSelector:aSelector];
72 }
73
74 - (void)forwardInvocation:(NSInvocation *)anInvocation
75 {
76     SEL aSelector = [anInvocation selector];
77     BOOL internalDelegateWillRespond = [_internalDelegate respondsToSelector:aSelector];
78     BOOL externalDelegateWillRespond = [_externalDelegate respondsToSelector:aSelector];
79
80     if (internalDelegateWillRespond && externalDelegateWillRespond)
81         [_internalDelegate _willInvokeUIScrollViewDelegateCallback];
82
83     if (internalDelegateWillRespond)
84         [anInvocation invokeWithTarget:_internalDelegate];
85     if (externalDelegateWillRespond)
86         [anInvocation invokeWithTarget:_externalDelegate];
87
88     if (internalDelegateWillRespond && externalDelegateWillRespond)
89         [_internalDelegate _didInvokeUIScrollViewDelegateCallback];
90
91     if (!internalDelegateWillRespond && !externalDelegateWillRespond)
92         [super forwardInvocation:anInvocation];
93 }
94
95 - (id)forwardingTargetForSelector:(SEL)aSelector
96 {
97     BOOL internalDelegateWillRespond = [_internalDelegate respondsToSelector:aSelector];
98     BOOL externalDelegateWillRespond = [_externalDelegate respondsToSelector:aSelector];
99
100     if (internalDelegateWillRespond && !externalDelegateWillRespond)
101         return _internalDelegate;
102     if (externalDelegateWillRespond && !internalDelegateWillRespond)
103         return _externalDelegate;
104     return nil;
105 }
106
107 @end
108
109 @implementation WKScrollView {
110     id <UIScrollViewDelegate> _externalDelegate;
111     WKScrollViewDelegateForwarder *_delegateForwarder;
112 }
113
114 - (void)setInternalDelegate:(WKWebView <UIScrollViewDelegate> *)internalDelegate
115 {
116     if (internalDelegate == _internalDelegate)
117         return;
118     _internalDelegate = internalDelegate;
119     [self _updateDelegate];
120 }
121
122 - (void)setDelegate:(id <UIScrollViewDelegate>)delegate
123 {
124     if (_externalDelegate == delegate)
125         return;
126     _externalDelegate = delegate;
127     [self _updateDelegate];
128 }
129
130 - (id <UIScrollViewDelegate>)delegate
131 {
132     return _externalDelegate;
133 }
134
135 - (void)_updateDelegate
136 {
137     WKScrollViewDelegateForwarder *oldForwarder = _delegateForwarder;
138     _delegateForwarder = nil;
139     if (!_externalDelegate)
140         [super setDelegate:_internalDelegate];
141     else if (!_internalDelegate)
142         [super setDelegate:_externalDelegate];
143     else {
144         _delegateForwarder = [[WKScrollViewDelegateForwarder alloc] initWithInternalDelegate:_internalDelegate externalDelegate:_externalDelegate];
145         [super setDelegate:_delegateForwarder];
146     }
147     [oldForwarder release];
148 }
149
150 - (void)dealloc
151 {
152     [_delegateForwarder release];
153     [super dealloc];
154 }
155
156 static inline bool valuesAreWithinOnePixel(CGFloat a, CGFloat b)
157 {
158     return CGFAbs(a - b) < 1;
159 }
160
161 - (CGFloat)_rubberBandOffsetForOffset:(CGFloat)newOffset maxOffset:(CGFloat)maxOffset minOffset:(CGFloat)minOffset range:(CGFloat)range outside:(BOOL *)outside
162 {
163     UIEdgeInsets contentInsets = self.contentInset;
164     CGSize contentSize = self.contentSize;
165     CGRect bounds = self.bounds;
166
167     CGFloat minimalHorizontalRange = bounds.size.width - contentInsets.left - contentInsets.right;
168     if (contentSize.width < minimalHorizontalRange) {
169         if (valuesAreWithinOnePixel(minOffset, -contentInsets.left)
170             && valuesAreWithinOnePixel(maxOffset, contentSize.width + contentInsets.right - bounds.size.width)
171             && valuesAreWithinOnePixel(range, bounds.size.width)) {
172
173             CGFloat emptyHorizontalMargin = (minimalHorizontalRange - contentSize.width) / 2;
174             minOffset -= emptyHorizontalMargin;
175             maxOffset = minOffset;
176         }
177     }
178
179     CGFloat minimalVerticalRange = bounds.size.height - contentInsets.top - contentInsets.bottom;
180     if (contentSize.height < minimalVerticalRange) {
181         if (valuesAreWithinOnePixel(minOffset, -contentInsets.top)
182             && valuesAreWithinOnePixel(maxOffset, contentSize.height + contentInsets.bottom - bounds.size.height)
183             && valuesAreWithinOnePixel(range, bounds.size.height)) {
184
185             CGFloat emptyVerticalMargin = (minimalVerticalRange - contentSize.height) / 2;
186             minOffset -= emptyVerticalMargin;
187             maxOffset = minOffset;
188         }
189     }
190
191     return [super _rubberBandOffsetForOffset:newOffset maxOffset:maxOffset minOffset:minOffset range:range outside:outside];
192 }
193
194 - (void)setContentInset:(UIEdgeInsets)contentInset
195 {
196     [super setContentInset:contentInset];
197
198     [_internalDelegate _updateVisibleContentRects];
199 }
200
201 // Fetch top/left rubberband amounts (as negative values).
202 - (CGSize)_currentTopLeftRubberbandAmount
203 {
204     UIEdgeInsets edgeInsets = [self contentInset];
205
206     CGSize rubberbandAmount = CGSizeZero;
207
208     CGPoint contentOffset = [self contentOffset];
209     if (contentOffset.x < -edgeInsets.left)
210         rubberbandAmount.width = std::min<CGFloat>(contentOffset.x + -edgeInsets.left, 0);
211
212     if (contentOffset.y < -edgeInsets.top)
213         rubberbandAmount.height = std::min<CGFloat>(contentOffset.y + edgeInsets.top, 0);
214     
215     return rubberbandAmount;
216 }
217
218 - (void)_restoreContentOffsetWithRubberbandAmount:(CGSize)rubberbandAmount
219 {
220     UIEdgeInsets edgeInsets = [self contentInset];
221     CGPoint adjustedOffset = [self contentOffset];
222
223     if (rubberbandAmount.width < 0)
224         adjustedOffset.x = -edgeInsets.left + rubberbandAmount.width;
225
226     if (rubberbandAmount.height < 0)
227         adjustedOffset.y = -edgeInsets.top + rubberbandAmount.height;
228
229     [self setContentOffset:adjustedOffset];
230 }
231
232 - (void)_setContentSizePreservingContentOffsetDuringRubberband:(CGSize)contentSize
233 {
234     CGSize currentContentSize = [self contentSize];
235
236     if (CGSizeEqualToSize(currentContentSize, CGSizeZero) || CGSizeEqualToSize(currentContentSize, contentSize) || self.zoomScale < self.minimumZoomScale) {
237         [self setContentSize:contentSize];
238         return;
239     }
240
241     CGSize rubberbandAmount = [self _currentTopLeftRubberbandAmount];
242
243     [self setContentSize:contentSize];
244
245     if (!CGSizeEqualToSize(rubberbandAmount, CGSizeZero))
246         [self _restoreContentOffsetWithRubberbandAmount:rubberbandAmount];
247 }
248
249 @end
250
251 #endif // PLATFORM(IOS)