Swipe snapshot removed too early (jumps around) on arstechnica and NYT
[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 "UIKitSPI.h"
32 #import "ViewGestureControllerMessages.h"
33 #import "ViewGestureGeometryCollectorMessages.h"
34 #import "ViewSnapshotStore.h"
35 #import "WKBackForwardListItemInternal.h"
36 #import "WKWebViewInternal.h"
37 #import "WebBackForwardList.h"
38 #import "WebPageGroup.h"
39 #import "WebPageMessages.h"
40 #import "WebPageProxy.h"
41 #import "WebProcessProxy.h"
42 #import <UIKit/UIScreenEdgePanGestureRecognizer.h>
43 #import <WebCore/IOSurface.h>
44 #import <WebCore/QuartzCoreSPI.h>
45 #import <wtf/NeverDestroyed.h>
46
47 using namespace WebCore;
48
49 @interface WKSwipeTransitionController : NSObject <_UINavigationInteractiveTransitionBaseDelegate>
50 - (instancetype)initWithViewGestureController:(WebKit::ViewGestureController*)gestureController gestureRecognizerView:(UIView *)gestureRecognizerView;
51 - (void)invalidate;
52 @end
53
54 @interface _UIViewControllerTransitionContext (WKDetails)
55 @property (nonatomic, copy, setter=_setInteractiveUpdateHandler:)  void (^_interactiveUpdateHandler)(BOOL interactionIsOver, CGFloat percentComplete, BOOL transitionCompleted, _UIViewControllerTransitionContext *);
56 @end
57
58 @implementation WKSwipeTransitionController
59 {
60     WebKit::ViewGestureController *_gestureController;
61     RetainPtr<_UINavigationInteractiveTransitionBase> _backTransitionController;
62     RetainPtr<_UINavigationInteractiveTransitionBase> _forwardTransitionController;
63 }
64
65 static const float swipeSnapshotRemovalRenderTreeSizeTargetFraction = 0.5;
66 static const std::chrono::seconds swipeSnapshotRemovalWatchdogDuration = 3_s;
67 static const std::chrono::milliseconds swipeSnapshotRemovalActiveLoadMonitoringInterval = 250_ms;
68
69 // The key in this map is the associated page ID.
70 static HashMap<uint64_t, WebKit::ViewGestureController*>& viewGestureControllersForAllPages()
71 {
72     static NeverDestroyed<HashMap<uint64_t, WebKit::ViewGestureController*>> viewGestureControllers;
73     return viewGestureControllers.get();
74 }
75
76 - (instancetype)initWithViewGestureController:(WebKit::ViewGestureController*)gestureController gestureRecognizerView:(UIView *)gestureRecognizerView
77 {
78     self = [super init];
79     if (self) {
80         _gestureController = gestureController;
81
82         _backTransitionController = adoptNS([_UINavigationInteractiveTransitionBase alloc]);
83         _backTransitionController = [_backTransitionController initWithGestureRecognizerView:gestureRecognizerView animator:nil delegate:self];
84         
85         _forwardTransitionController = adoptNS([_UINavigationInteractiveTransitionBase alloc]);
86         _forwardTransitionController = [_forwardTransitionController initWithGestureRecognizerView:gestureRecognizerView animator:nil delegate:self];
87         [_forwardTransitionController setShouldReverseTranslation:YES];
88     }
89     return self;
90 }
91
92 - (void)invalidate
93 {
94     _gestureController = nullptr;
95 }
96
97 - (WebKit::ViewGestureController::SwipeDirection)directionForTransition:(_UINavigationInteractiveTransitionBase *)transition
98 {
99     return transition == _backTransitionController ? WebKit::ViewGestureController::SwipeDirection::Left : WebKit::ViewGestureController::SwipeDirection::Right;
100 }
101
102 - (void)startInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition
103 {
104     _gestureController->beginSwipeGesture(transition, [self directionForTransition:transition]);
105 }
106
107 - (BOOL)shouldBeginInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition
108 {
109     return _gestureController->canSwipeInDirection([self directionForTransition:transition]);
110 }
111
112 - (BOOL)interactiveTransition:(_UINavigationInteractiveTransitionBase *)transition gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
113 {
114     return [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]];
115 }
116
117 - (BOOL)interactiveTransition:(_UINavigationInteractiveTransitionBase *)transition gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
118 {
119     return YES;
120 }
121
122 - (UIPanGestureRecognizer *)gestureRecognizerForInteractiveTransition:(_UINavigationInteractiveTransitionBase *)transition WithTarget:(id)target action:(SEL)action
123 {
124     UIScreenEdgePanGestureRecognizer *recognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:target action:action];
125     switch ([self directionForTransition:transition]) {
126     case WebKit::ViewGestureController::SwipeDirection::Left:
127         [recognizer setEdges:UIRectEdgeLeft];
128         break;
129     case WebKit::ViewGestureController::SwipeDirection::Right:
130         [recognizer setEdges:UIRectEdgeRight];
131         break;
132     }
133     return [recognizer autorelease];
134 }
135
136 @end
137
138 namespace WebKit {
139
140 ViewGestureController::ViewGestureController(WebPageProxy& webPageProxy)
141     : m_webPageProxy(webPageProxy)
142     , m_swipeWatchdogTimer(RunLoop::main(), this, &ViewGestureController::swipeSnapshotWatchdogTimerFired)
143     , m_swipeActiveLoadMonitoringTimer(RunLoop::main(), this, &ViewGestureController::activeLoadMonitoringTimerFired)
144 {
145     viewGestureControllersForAllPages().add(webPageProxy.pageID(), this);
146 }
147
148 ViewGestureController::~ViewGestureController()
149 {
150     [m_swipeTransitionContext _setTransitionIsInFlight:NO];
151     [m_swipeTransitionContext _setInteractor:nil];
152     [m_swipeTransitionContext _setAnimator:nil];
153     [m_swipeInteractiveTransitionDelegate invalidate];
154     viewGestureControllersForAllPages().remove(m_webPageProxy.pageID());
155 }
156
157 void ViewGestureController::setAlternateBackForwardListSourceView(WKWebView *view)
158 {
159     m_alternateBackForwardListSourceView = view;
160 }
161
162 void ViewGestureController::installSwipeHandler(UIView *gestureRecognizerView, UIView *swipingView)
163 {
164     ASSERT(!m_swipeInteractiveTransitionDelegate);
165     m_swipeInteractiveTransitionDelegate = adoptNS([[WKSwipeTransitionController alloc] initWithViewGestureController:this gestureRecognizerView:gestureRecognizerView]);
166     m_liveSwipeView = swipingView;
167 }
168
169 void ViewGestureController::beginSwipeGesture(_UINavigationInteractiveTransitionBase *transition, SwipeDirection direction)
170 {
171     if (m_activeGestureType != ViewGestureType::None)
172         return;
173
174     m_webPageProxy.recordNavigationSnapshot();
175
176     m_webPageProxyForBackForwardListForCurrentSwipe = m_alternateBackForwardListSourceView.get() ? m_alternateBackForwardListSourceView.get()->_page : &m_webPageProxy;
177     m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureDidBegin();
178
179     auto& backForwardList = m_webPageProxyForBackForwardListForCurrentSwipe->backForwardList();
180
181     // Copy the snapshot from this view to the one that owns the back forward list, so that
182     // swiping forward will have the correct snapshot.
183     if (m_webPageProxyForBackForwardListForCurrentSwipe != &m_webPageProxy)
184         backForwardList.currentItem()->setSnapshot(m_webPageProxy.backForwardList().currentItem()->snapshot());
185
186     RefPtr<WebBackForwardListItem> targetItem = direction == SwipeDirection::Left ? backForwardList.backItem() : backForwardList.forwardItem();
187
188     CGRect liveSwipeViewFrame = [m_liveSwipeView frame];
189
190     RetainPtr<UIViewController> snapshotViewController = adoptNS([[UIViewController alloc] init]);
191     m_snapshotView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]);
192
193     RetainPtr<UIColor> backgroundColor = [UIColor whiteColor];
194     if (ViewSnapshot* snapshot = targetItem->snapshot()) {
195         float deviceScaleFactor = m_webPageProxy.deviceScaleFactor();
196         FloatSize swipeLayerSizeInDeviceCoordinates(liveSwipeViewFrame.size);
197         swipeLayerSizeInDeviceCoordinates.scale(deviceScaleFactor);
198         if (snapshot->hasImage() && snapshot->size() == swipeLayerSizeInDeviceCoordinates && deviceScaleFactor == snapshot->deviceScaleFactor())
199             [m_snapshotView layer].contents = snapshot->asLayerContents();
200         Color coreColor = snapshot->backgroundColor();
201         if (coreColor.isValid())
202             backgroundColor = adoptNS([[UIColor alloc] initWithCGColor:cachedCGColor(coreColor, ColorSpaceDeviceRGB)]);
203     }
204
205     [m_snapshotView setBackgroundColor:backgroundColor.get()];
206     [m_snapshotView layer].contentsGravity = kCAGravityTopLeft;
207     [m_snapshotView layer].contentsScale = m_liveSwipeView.window.screen.scale;
208     [snapshotViewController setView:m_snapshotView.get()];
209
210     m_transitionContainerView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]);
211     m_liveSwipeViewClippingView = adoptNS([[UIView alloc] initWithFrame:liveSwipeViewFrame]);
212     [m_liveSwipeViewClippingView setClipsToBounds:YES];
213
214     [m_liveSwipeView.superview insertSubview:m_transitionContainerView.get() belowSubview:m_liveSwipeView];
215     [m_transitionContainerView addSubview:m_liveSwipeViewClippingView.get()];
216     [m_liveSwipeViewClippingView addSubview:m_liveSwipeView];
217
218     RetainPtr<UIViewController> targettedViewController = adoptNS([[UIViewController alloc] init]);
219     [targettedViewController setView:m_liveSwipeViewClippingView.get()];
220
221     UINavigationControllerOperation transitionOperation = direction == SwipeDirection::Left ? UINavigationControllerOperationPop : UINavigationControllerOperationPush;
222     RetainPtr<_UINavigationParallaxTransition> animationController = adoptNS([[_UINavigationParallaxTransition alloc] initWithCurrentOperation:transitionOperation]);
223
224     m_swipeTransitionContext = adoptNS([[_UIViewControllerOneToOneTransitionContext alloc] init]);
225     [m_swipeTransitionContext _setFromViewController:targettedViewController.get()];
226     [m_swipeTransitionContext _setToViewController:snapshotViewController.get()];
227     [m_swipeTransitionContext _setContainerView:m_transitionContainerView.get()];
228     [m_swipeTransitionContext _setFromStartFrame:liveSwipeViewFrame];
229     [m_swipeTransitionContext _setToEndFrame:liveSwipeViewFrame];
230     [m_swipeTransitionContext _setToStartFrame:CGRectZero];
231     [m_swipeTransitionContext _setFromEndFrame:CGRectZero];
232     [m_swipeTransitionContext _setAnimator:animationController.get()];
233     [m_swipeTransitionContext _setInteractor:transition];
234     [m_swipeTransitionContext _setTransitionIsInFlight:YES];
235     [m_swipeTransitionContext _setInteractiveUpdateHandler:^(BOOL finish, CGFloat percent, BOOL transitionCompleted, _UIViewControllerTransitionContext *) {
236         if (finish)
237             m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureWillEnd(transitionCompleted, *targetItem);
238     }];
239     uint64_t pageID = m_webPageProxy.pageID();
240     [m_swipeTransitionContext _setCompletionHandler:^(_UIViewControllerTransitionContext *context, BOOL didComplete) {
241         auto gestureControllerIter = viewGestureControllersForAllPages().find(pageID);
242         if (gestureControllerIter != viewGestureControllersForAllPages().end())
243             gestureControllerIter->value->endSwipeGesture(targetItem.get(), context, !didComplete);
244     }];
245     [m_swipeTransitionContext _setInteractiveUpdateHandler:^(BOOL, CGFloat, BOOL, _UIViewControllerTransitionContext *) { }];
246
247     [transition setAnimationController:animationController.get()];
248     [transition startInteractiveTransition:m_swipeTransitionContext.get()];
249
250     m_activeGestureType = ViewGestureType::Swipe;
251 }
252
253 bool ViewGestureController::canSwipeInDirection(SwipeDirection direction)
254 {
255     auto& backForwardList = m_alternateBackForwardListSourceView.get() ? m_alternateBackForwardListSourceView.get()->_page->backForwardList() : m_webPageProxy.backForwardList();
256     if (direction == SwipeDirection::Left)
257         return !!backForwardList.backItem();
258     return !!backForwardList.forwardItem();
259 }
260
261 void ViewGestureController::endSwipeGesture(WebBackForwardListItem* targetItem, _UIViewControllerTransitionContext *context, bool cancelled)
262 {
263     [context _setTransitionIsInFlight:NO];
264     [context _setInteractor:nil];
265     [context _setAnimator:nil];
266     
267     [[m_transitionContainerView superview] insertSubview:m_snapshotView.get() aboveSubview:m_transitionContainerView.get()];
268     [[m_transitionContainerView superview] insertSubview:m_liveSwipeView aboveSubview:m_transitionContainerView.get()];
269     [m_liveSwipeViewClippingView removeFromSuperview];
270     m_liveSwipeViewClippingView = nullptr;
271     [m_transitionContainerView removeFromSuperview];
272     m_transitionContainerView = nullptr;
273
274     if (cancelled) {
275         // removeSwipeSnapshot will clear m_webPageProxyForBackForwardListForCurrentSwipe, so hold on to it here.
276         RefPtr<WebPageProxy> webPageProxyForBackForwardListForCurrentSwipe = m_webPageProxyForBackForwardListForCurrentSwipe;
277         removeSwipeSnapshot();
278         webPageProxyForBackForwardListForCurrentSwipe->navigationGestureDidEnd(false, *targetItem);
279         return;
280     }
281
282     m_snapshotRemovalTargetRenderTreeSize = 0;
283     if (ViewSnapshot* snapshot = targetItem->snapshot())
284         m_snapshotRemovalTargetRenderTreeSize = snapshot->renderTreeSize() * swipeSnapshotRemovalRenderTreeSizeTargetFraction;
285
286     m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureDidEnd(true, *targetItem);
287     m_webPageProxyForBackForwardListForCurrentSwipe->goToBackForwardItem(targetItem);
288
289     if (auto drawingArea = m_webPageProxy.drawingArea()) {
290         uint64_t pageID = m_webPageProxy.pageID();
291         uint64_t gesturePendingSnapshotRemoval = m_gesturePendingSnapshotRemoval;
292         drawingArea->dispatchAfterEnsuringDrawing([pageID, gesturePendingSnapshotRemoval] (CallbackBase::Error error) {
293             auto gestureControllerIter = viewGestureControllersForAllPages().find(pageID);
294             if (gestureControllerIter != viewGestureControllersForAllPages().end() && gestureControllerIter->value->m_gesturePendingSnapshotRemoval == gesturePendingSnapshotRemoval)
295                 gestureControllerIter->value->willCommitPostSwipeTransitionLayerTree(error == CallbackBase::Error::None);
296         });
297     } else {
298         removeSwipeSnapshot();
299         return;
300     }
301
302     m_swipeWaitingForRenderTreeSizeThreshold = true;
303     m_swipeWaitingForRepaint = true;
304     m_swipeWaitingForDidFinishLoad = true;
305     m_swipeWaitingForSubresourceLoads = true;
306     m_swipeWaitingForScrollPositionRestoration = true;
307
308     m_swipeWatchdogTimer.startOneShot(swipeSnapshotRemovalWatchdogDuration.count());
309 }
310
311 void ViewGestureController::willCommitPostSwipeTransitionLayerTree(bool successful)
312 {
313     if (m_activeGestureType != ViewGestureType::Swipe)
314         return;
315
316     if (!successful) {
317         removeSwipeSnapshot();
318         return;
319     }
320
321     if (!m_swipeWaitingForRepaint)
322         return;
323
324     m_swipeWaitingForRepaint = false;
325     removeSwipeSnapshotIfReady();
326 }
327
328 void ViewGestureController::setRenderTreeSize(uint64_t renderTreeSize)
329 {
330     if (m_activeGestureType != ViewGestureType::Swipe)
331         return;
332
333     if (!m_swipeWaitingForRenderTreeSizeThreshold)
334         return;
335
336     if (!m_snapshotRemovalTargetRenderTreeSize || renderTreeSize > m_snapshotRemovalTargetRenderTreeSize) {
337         m_swipeWaitingForRenderTreeSizeThreshold = false;
338         removeSwipeSnapshotIfReady();
339     }
340 }
341
342 void ViewGestureController::didRestoreScrollPosition()
343 {
344     if (m_activeGestureType != ViewGestureType::Swipe)
345         return;
346
347     if (!m_swipeWaitingForScrollPositionRestoration)
348         return;
349
350     m_swipeWaitingForScrollPositionRestoration = false;
351     removeSwipeSnapshotIfReady();
352 }
353
354 void ViewGestureController::didFinishLoadForMainFrame()
355 {
356     if (m_activeGestureType != ViewGestureType::Swipe)
357         return;
358
359     if (!m_swipeWaitingForDidFinishLoad)
360         return;
361
362     m_swipeWaitingForDidFinishLoad = false;
363
364     if (m_webPageProxy.pageLoadState().isLoading()) {
365         m_swipeActiveLoadMonitoringTimer.startRepeating(swipeSnapshotRemovalActiveLoadMonitoringInterval);
366         return;
367     }
368
369     m_swipeWaitingForSubresourceLoads = false;
370     removeSwipeSnapshotIfReady();
371 }
372
373 void ViewGestureController::didSameDocumentNavigationForMainFrame(SameDocumentNavigationType type)
374 {
375     if (m_activeGestureType != ViewGestureType::Swipe)
376         return;
377
378     // This is nearly equivalent to didFinishLoad in the same document navigation case.
379     m_swipeWaitingForDidFinishLoad = false;
380
381     if (type != SameDocumentNavigationSessionStateReplace && type != SameDocumentNavigationSessionStatePop)
382         return;
383
384     m_swipeActiveLoadMonitoringTimer.startRepeating(swipeSnapshotRemovalActiveLoadMonitoringInterval);
385 }
386
387 void ViewGestureController::activeLoadMonitoringTimerFired()
388 {
389     if (m_webPageProxy.pageLoadState().isLoading())
390         return;
391
392     m_swipeWaitingForSubresourceLoads = false;
393     removeSwipeSnapshotIfReady();
394 }
395
396 void ViewGestureController::swipeSnapshotWatchdogTimerFired()
397 {
398     removeSwipeSnapshot();
399 }
400
401 void ViewGestureController::removeSwipeSnapshotIfReady()
402 {
403     if (m_swipeWaitingForRenderTreeSizeThreshold || m_swipeWaitingForRepaint || m_swipeWaitingForDidFinishLoad || m_swipeWaitingForSubresourceLoads || m_swipeWaitingForScrollPositionRestoration)
404         return;
405
406     removeSwipeSnapshot();
407 }
408
409 void ViewGestureController::removeSwipeSnapshot()
410 {
411     m_swipeWaitingForRenderTreeSizeThreshold = false;
412     m_swipeWaitingForRepaint = false;
413     m_swipeWaitingForDidFinishLoad = false;
414     m_swipeWaitingForSubresourceLoads = false;
415     m_swipeWaitingForScrollPositionRestoration = false;
416
417     m_swipeWatchdogTimer.stop();
418     m_swipeActiveLoadMonitoringTimer.stop();
419
420     if (m_activeGestureType != ViewGestureType::Swipe)
421         return;
422     
423     ++m_gesturePendingSnapshotRemoval;
424     
425     [m_snapshotView removeFromSuperview];
426     m_snapshotView = nullptr;
427     
428     m_snapshotRemovalTargetRenderTreeSize = 0;
429     m_activeGestureType = ViewGestureType::None;
430
431     m_webPageProxyForBackForwardListForCurrentSwipe->navigationGestureSnapshotWasRemoved();
432     m_webPageProxyForBackForwardListForCurrentSwipe = nullptr;
433
434     m_swipeTransitionContext = nullptr;
435 }
436
437 } // namespace WebKit
438
439 #endif // PLATFORM(IOS)