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
156static 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)
252