[WK2] Provide a mechanism to grab the back-forward list for gesture navigation purpos...
[WebKit-https.git] / Source / WebKit2 / UIProcess / ios / ViewGestureControllerIOS.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 "ViewGestureController.h"
28
29 #if PLATFORM(IOS)
30
31 #import "ViewGestureControllerMessages.h"
32 #import "ViewGestureGeometryCollectorMessages.h"
33 #import "ViewSnapshotStore.h"
34 #import "WKBackForwardListItemInternal.h"
35 #import "WKWebViewInternal.h"
36 #import "WebBackForwardList.h"
37 #import "WebPageGroup.h"
38 #import "WebPageMessages.h"
39 #import "WebPageProxy.h"
40 #import "WebProcessProxy.h"
41 #import <QuartzCore/QuartzCorePrivate.h>
42 #import <UIKit/UIScreenEdgePanGestureRecognizer.h>
43 #import <UIKit/UIViewControllerTransitioning_Private.h>
44 #import <UIKit/UIWebTouchEventsGestureRecognizer.h>
45 #import <UIKit/_UINavigationInteractiveTransition.h>
46 #import <UIKit/_UINavigationParallaxTransition.h>
47 #import <WebCore/IOSurface.h>
48 #import <wtf/NeverDestroyed.h>
49
50 using namespace WebCore;
51
52 @interface WKSwipeTransitionController : NSObject <_UINavigationInteractiveTransitionBaseDelegate>
53 - (instancetype)initWithViewGestureController:(WebKit::ViewGestureController*)gestureController gestureRecognizerView:(UIView *)gestureRecognizerView;
54 @end
55
56 @interface _UIViewControllerTransitionContext (WKDetails)
57 @property (nonatomic, copy, setter=_setInteractiveUpdateHandler:)  void (^_interactiveUpdateHandler)(BOOL interactionIsOver, CGFloat percentComplete, BOOL transitionCompleted, _UIViewControllerTransitionContext *);
58 @end
59
60 @implementation WKSwipeTransitionController
61 {
62     WebKit::ViewGestureController *_gestureController;
63     RetainPtr<_UINavigationInteractiveTransitionBase> _backTransitionController;
64     RetainPtr<_UINavigationInteractiveTransitionBase> _forwardTransitionController;
65 }
66
67 static const float swipeSnapshotRemovalRenderTreeSizeTargetFraction = 0.5;
68 static const std::chrono::seconds swipeSnapshotRemovalWatchdogDuration = 3_s;
69
70 // The key in this map is the associated page ID.
71 static HashMap<uint64_t, WebKit::ViewGestureController*>& viewGestureControllersForAllPages()
72 {
73     static NeverDestroyed<HashMap<uint64_t, WebKit::ViewGestureController*>> viewGestureControllers;
74     return viewGestureControllers.get();
75 }
76
77 - (instancetype)initWithViewGestureController:(WebKit::ViewGestureController*)gestureController gestureRecognizerView:(UIView *)gestureRecognizerView
78 {
79     self = [super init];
80     if (self) {
81         _gestureController = gestureController;
82
83         _backTransitionController = adoptNS([_UINavigationInteractiveTransitionBase alloc]);
84         _backTransitionController = [_backTransitionController initWithGestureRecognizerView:gestureRecognizerView animator:nil delegate:self];
85         
86         _forwardTransitionController = adoptNS([_UINavigationInteractiveTransitionBase alloc]);
87         _forwardTransitionController = [_forwardTransitionController initWithGestureRecognizerView:gestureRecognizerView animator:nil delegate:self];
88         [_forwardTransitionController setShouldReverseTranslation:YES];
89     }
90     return self;
91 }
92
93 - (WebKit::ViewGestureController::SwipeDirection)directionForTransition:(_UINavigationInteractiveTransitionBase *)transition
94 {
95     return transition == _backTransitionController ? WebKit::ViewGestureController::SwipeDirection::Left : WebKit::ViewGestureController::SwipeDirection::Right;
96 }
97
98 - (void)startInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition
99 {
100     _gestureController->beginSwipeGesture(transition, [self directionForTransition:transition]);
101 }
102
103 - (BOOL)shouldBeginInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition
104 {
105     return _gestureController->canSwipeInDirection([self directionForTransition:transition]);
106 }
107
108 - (BOOL)interactiveTransition:(_UINavigationInteractiveTransitionBase *)transition gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
109 {
110     return [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]];
111 }
112
113 - (BOOL)interactiveTransition:(_UINavigationInteractiveTransitionBase *)transition gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
114 {
115     return YES;
116 }
117
118 - (UIPanGestureRecognizer *)gestureRecognizerForInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition WithTarget:(id)target action:(SEL)action
119 {
120     UIScreenEdgePanGestureRecognizer *recognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:target action:action];
121     switch ([self directionForTransition:transition]) {
122     case WebKit::ViewGestureController::SwipeDirection::Left:
123         [recognizer setEdges:UIRectEdgeLeft];
124         break;
125     case WebKit::ViewGestureController::SwipeDirection::Right:
126         [recognizer setEdges:UIRectEdgeRight];
127         break;
128     }
129     return [recognizer autorelease];
130 }
131
132 @end
133
134 namespace WebKit {
135
136 ViewGestureController::ViewGestureController(WebPageProxy& webPageProxy)
137     : m_webPageProxy(webPageProxy)
138     , m_activeGestureType(ViewGestureType::None)
139     , m_swipeWatchdogTimer(this, &ViewGestureController::swipeSnapshotWatchdogTimerFired)
140     , m_snapshotRemovalTargetRenderTreeSize(0)
141     , m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit(false)
142 {
143     viewGestureControllersForAllPages().add(webPageProxy.pageID(), this);
144 }
145
146 ViewGestureController::~ViewGestureController()
147 {
148     viewGestureControllersForAllPages().remove(m_webPageProxy.pageID());
149 }
150
151 void ViewGestureController::setAlternateBackForwardListSourceView(WKWebView *view)
152 {
153     m_alternateBackForwardListSourceView = view;
154 }
155
156 void ViewGestureController::installSwipeHandler(UIView *gestureRecognizerView, UIView *swipingView)
157 {
158     ASSERT(!m_swipeInteractiveTransitionDelegate);
159     m_swipeInteractiveTransitionDelegate = adoptNS([[WKSwipeTransitionController alloc] initWithViewGestureController:this gestureRecognizerView:gestureRecognizerView]);
160     m_liveSwipeView = swipingView;
161 }
162
163 void ViewGestureController::beginSwipeGesture(_UINavigationInteractiveTransitionBase *transition, SwipeDirection direction)
164 {
165     if (m_activeGestureType != ViewGestureType::None)
166         return;
167
168     m_webPageProxyForBackForwardListForCurrentSwipe = m_alternateBackForwardListSourceView.get() ? m_alternateBackForwardListSourceView.get()->_page : &m_webPageProxy;
169
170     m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureDidBegin();
171
172     m_webPageProxy.recordNavigationSnapshot();
173
174     auto backForwardList = m_webPageProxyForBackForwardListForCurrentSwipe->backForwardList();
175
176     // Copy the snapshot from this view to the one that owns the back forward list, so that
177     // swiping forward will have the correct snapshot.
178     if (m_webPageProxyForBackForwardListForCurrentSwipe != &m_webPageProxy)
179         backForwardList.currentItem()->setSnapshot(m_webPageProxy.backForwardList().currentItem()->snapshot());
180
181     WebBackForwardListItem* targetItem = direction == SwipeDirection::Left ? backForwardList.backItem() : backForwardList.forwardItem();
182
183     CGRect liveSwipeViewFrame = [m_liveSwipeView frame];
184
185     RetainPtr<UIViewController> snapshotViewController = adoptNS([[UIViewController alloc] init]);
186     m_snapshotView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]);
187
188     RetainPtr<UIColor> backgroundColor = [UIColor whiteColor];
189     if (ViewSnapshot* snapshot = targetItem->snapshot()) {
190         float deviceScaleFactor = m_webPageProxy.deviceScaleFactor();
191         FloatSize swipeLayerSizeInDeviceCoordinates(liveSwipeViewFrame.size);
192         swipeLayerSizeInDeviceCoordinates.scale(deviceScaleFactor);
193         if (snapshot->hasImage() && snapshot->size() == swipeLayerSizeInDeviceCoordinates && deviceScaleFactor == snapshot->deviceScaleFactor())
194             [m_snapshotView layer].contents = snapshot->asLayerContents();
195         Color coreColor = snapshot->backgroundColor();
196         if (coreColor.isValid())
197             backgroundColor = adoptNS([[UIColor alloc] initWithCGColor:cachedCGColor(coreColor, ColorSpaceDeviceRGB)]);
198     }
199
200     [m_snapshotView setBackgroundColor:backgroundColor.get()];
201     [m_snapshotView layer].contentsGravity = kCAGravityTopLeft;
202     [m_snapshotView layer].contentsScale = m_liveSwipeView.window.screen.scale;
203     [snapshotViewController setView:m_snapshotView.get()];
204
205     m_transitionContainerView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]);
206     m_liveSwipeViewClippingView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]);
207     [m_liveSwipeViewClippingView setClipsToBounds:YES];
208
209     [m_liveSwipeView.superview insertSubview:m_transitionContainerView.get() belowSubview:m_liveSwipeView];
210     [m_liveSwipeViewClippingView addSubview:m_liveSwipeView];
211     [m_transitionContainerView addSubview:m_liveSwipeViewClippingView.get()];
212
213     RetainPtr<UIViewController> targettedViewController = adoptNS([[UIViewController alloc] init]);
214     [targettedViewController setView:m_liveSwipeViewClippingView.get()];
215
216     UINavigationControllerOperation transitionOperation = direction == SwipeDirection::Left ? UINavigationControllerOperationPop : UINavigationControllerOperationPush;
217     RetainPtr<_UINavigationParallaxTransition> animationController = adoptNS([[_UINavigationParallaxTransition alloc] initWithCurrentOperation:transitionOperation]);
218
219     RetainPtr<_UIViewControllerOneToOneTransitionContext> transitionContext = adoptNS([[_UIViewControllerOneToOneTransitionContext alloc] init]);
220     [transitionContext _setFromViewController:targettedViewController.get()];
221     [transitionContext _setToViewController:snapshotViewController.get()];
222     [transitionContext _setContainerView:m_transitionContainerView.get()];
223     [transitionContext _setFromStartFrame:liveSwipeViewFrame];
224     [transitionContext _setToEndFrame:liveSwipeViewFrame];
225     [transitionContext _setToStartFrame:CGRectZero];
226     [transitionContext _setFromEndFrame:CGRectZero];
227     [transitionContext _setAnimator:animationController.get()];
228     [transitionContext _setInteractor:transition];
229     [transitionContext _setTransitionIsInFlight:YES];
230     [transitionContext _setInteractiveUpdateHandler:^(BOOL finish, CGFloat percent, BOOL transitionCompleted, _UIViewControllerTransitionContext *) {
231         if (finish)
232             m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureWillEnd(transitionCompleted, *targetItem);
233     }];
234     [transitionContext _setCompletionHandler:^(_UIViewControllerTransitionContext *context, BOOL didComplete) { endSwipeGesture(targetItem, context, !didComplete); }];
235     [transitionContext _setInteractiveUpdateHandler:^(BOOL, CGFloat, BOOL, _UIViewControllerTransitionContext *) { }];
236
237     [transition setAnimationController:animationController.get()];
238     [transition startInteractiveTransition:transitionContext.get()];
239
240     m_activeGestureType = ViewGestureType::Swipe;
241 }
242
243 bool ViewGestureController::canSwipeInDirection(SwipeDirection direction)
244 {
245     auto backForwardList = m_alternateBackForwardListSourceView.get() ? m_alternateBackForwardListSourceView.get()->_page->backForwardList() : m_webPageProxy.backForwardList();
246     if (direction == SwipeDirection::Left)
247         return !!backForwardList.backItem();
248     return !!backForwardList.forwardItem();
249 }
250
251 void ViewGestureController::endSwipeGesture(WebBackForwardListItem* targetItem, _UIViewControllerTransitionContext *context, bool cancelled)
252 {
253     [context _setTransitionIsInFlight:NO];
254     [context _setInteractor:nil];
255     [context _setAnimator:nil];
256     
257     [[m_transitionContainerView superview] insertSubview:m_snapshotView.get() aboveSubview:m_transitionContainerView.get()];
258     [[m_transitionContainerView superview] insertSubview:m_liveSwipeView aboveSubview:m_transitionContainerView.get()];
259     [m_liveSwipeViewClippingView removeFromSuperview];
260     m_liveSwipeViewClippingView = nullptr;
261     [m_transitionContainerView removeFromSuperview];
262     m_transitionContainerView = nullptr;
263
264     if (cancelled) {
265         // removeSwipeSnapshot will clear m_webPageProxyForBackForwardListForCurrentSwipe, so hold on to it here.
266         RefPtr<WebPageProxy> webPageProxyForBackForwardListForCurrentSwipe = m_webPageProxyForBackForwardListForCurrentSwipe;
267         removeSwipeSnapshot();
268         webPageProxyForBackForwardListForCurrentSwipe->navigationGestureDidEnd(false, *targetItem);
269         return;
270     }
271
272     m_snapshotRemovalTargetRenderTreeSize = 0;
273     if (ViewSnapshot* snapshot = targetItem->snapshot())
274         m_snapshotRemovalTargetRenderTreeSize = snapshot->renderTreeSize() * swipeSnapshotRemovalRenderTreeSizeTargetFraction;
275
276     // We don't want to replace the current back-forward item's snapshot
277     // like we normally would when going back or forward, because we are
278     // displaying the destination item's snapshot.
279     ViewSnapshotStore::shared().disableSnapshotting();
280
281     m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureDidEnd(true, *targetItem);
282     m_webPageProxyForBackForwardListForCurrentSwipe->goToBackForwardItem(targetItem);
283
284     ViewSnapshotStore::shared().enableSnapshotting();
285
286     uint64_t pageID = m_webPageProxy.pageID();
287     m_webPageProxy.drawingArea()->dispatchAfterEnsuringDrawing([pageID] (CallbackBase::Error error) {
288         auto gestureControllerIter = viewGestureControllersForAllPages().find(pageID);
289         if (gestureControllerIter != viewGestureControllersForAllPages().end())
290             gestureControllerIter->value->willCommitPostSwipeTransitionLayerTree(error == CallbackBase::Error::None);
291     });
292
293     m_swipeWatchdogTimer.startOneShot(swipeSnapshotRemovalWatchdogDuration.count());
294 }
295
296 void ViewGestureController::willCommitPostSwipeTransitionLayerTree(bool successful)
297 {
298     if (m_activeGestureType != ViewGestureType::Swipe)
299         return;
300
301     if (!successful) {
302         removeSwipeSnapshot();
303         return;
304     }
305
306     m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit = true;
307 }
308
309 void ViewGestureController::setRenderTreeSize(uint64_t renderTreeSize)
310 {
311     if (m_activeGestureType != ViewGestureType::Swipe)
312         return;
313
314     if (!m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit)
315         return;
316
317     if (!m_snapshotRemovalTargetRenderTreeSize || renderTreeSize > m_snapshotRemovalTargetRenderTreeSize)
318         removeSwipeSnapshot();
319 }
320
321 void ViewGestureController::swipeSnapshotWatchdogTimerFired(Timer<ViewGestureController>*)
322 {
323     removeSwipeSnapshot();
324 }
325
326 void ViewGestureController::removeSwipeSnapshot()
327 {
328     m_shouldRemoveSnapshotWhenTargetRenderTreeSizeHit = false;
329
330     m_swipeWatchdogTimer.stop();
331
332     if (m_activeGestureType != ViewGestureType::Swipe)
333         return;
334     
335 #if USE(IOSURFACE)
336     if (m_currentSwipeSnapshotSurface)
337         m_currentSwipeSnapshotSurface->setIsVolatile(true);
338     m_currentSwipeSnapshotSurface = nullptr;
339 #endif
340     
341     [m_snapshotView removeFromSuperview];
342     m_snapshotView = nullptr;
343     
344     m_snapshotRemovalTargetRenderTreeSize = 0;
345     m_activeGestureType = ViewGestureType::None;
346
347     m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureSnapshotWasRemoved();
348     m_webPageProxyForBackForwardListForCurrentSwipe = nullptr;
349 }
350
351 } // namespace WebKit
352
353 #endif // PLATFORM(IOS)