2 * Copyright (C) 2013, 2014 Apple Inc. 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 "ViewGestureController.h"
31 #import "FrameLoadState.h"
32 #import "NativeWebWheelEvent.h"
33 #import "ViewGestureControllerMessages.h"
34 #import "ViewGestureGeometryCollectorMessages.h"
35 #import "ViewSnapshotStore.h"
36 #import "WebBackForwardList.h"
37 #import "WebPageGroup.h"
38 #import "WebPageMessages.h"
39 #import "WebPageProxy.h"
40 #import "WebPreferences.h"
41 #import "WebProcessProxy.h"
42 #import <Cocoa/Cocoa.h>
43 #import <WebCore/IOSurface.h>
44 #import <WebCore/QuartzCoreSPI.h>
45 #import <WebCore/WebActionDisablingCALayerDelegate.h>
47 using namespace WebCore;
49 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101000
50 #define ENABLE_LEGACY_SWIPE_SHADOW_STYLE 1
52 #define ENABLE_LEGACY_SWIPE_SHADOW_STYLE 0
55 static const double minMagnification = 1;
56 static const double maxMagnification = 3;
58 static const double minElasticMagnification = 0.75;
59 static const double maxElasticMagnification = 4;
61 static const double zoomOutBoost = 1.6;
62 static const double zoomOutResistance = 0.10;
64 static const float smartMagnificationElementPadding = 0.05;
65 static const float smartMagnificationPanScrollThreshold = 100;
67 #if ENABLE(LEGACY_SWIPE_SHADOW_STYLE)
68 static const double swipeOverlayShadowOpacity = 0.66;
69 static const double swipeOverlayShadowRadius = 3;
71 static const double swipeOverlayShadowOpacity = 0.47;
72 static const double swipeOverlayDimmingOpacity = 0.12;
75 static const CGFloat minimumHorizontalSwipeDistance = 15;
76 static const float minimumScrollEventRatioForSwipe = 0.5;
78 static const float swipeSnapshotRemovalRenderTreeSizeTargetFraction = 0.5;
79 static const std::chrono::seconds swipeSnapshotRemovalWatchdogDuration = 5_s;
80 static const std::chrono::seconds swipeSnapshotRemovalWatchdogAfterFirstVisuallyNonEmptyLayoutDuration = 3_s;
81 static const std::chrono::milliseconds swipeSnapshotRemovalActiveLoadMonitoringInterval = 250_ms;
83 @interface WKSwipeCancellationTracker : NSObject {
88 @property (nonatomic) BOOL isCancelled;
92 @implementation WKSwipeCancellationTracker
93 @synthesize isCancelled=_isCancelled;
98 ViewGestureController::ViewGestureController(WebPageProxy& webPageProxy)
99 : m_webPageProxy(webPageProxy)
100 , m_swipeWatchdogTimer(RunLoop::main(), this, &ViewGestureController::swipeSnapshotWatchdogTimerFired)
101 , m_swipeActiveLoadMonitoringTimer(RunLoop::main(), this, &ViewGestureController::activeLoadMonitoringTimerFired)
102 , m_swipeWatchdogAfterFirstVisuallyNonEmptyLayoutTimer(RunLoop::main(), this, &ViewGestureController::swipeSnapshotWatchdogTimerFired)
104 m_webPageProxy.process().addMessageReceiver(Messages::ViewGestureController::messageReceiverName(), m_webPageProxy.pageID(), *this);
107 ViewGestureController::~ViewGestureController()
109 if (m_swipeCancellationTracker)
110 [m_swipeCancellationTracker setIsCancelled:YES];
112 if (m_activeGestureType == ViewGestureType::Swipe)
113 removeSwipeSnapshot();
115 if (m_didMoveSwipeSnapshotCallback) {
116 Block_release(m_didMoveSwipeSnapshotCallback);
117 m_didMoveSwipeSnapshotCallback = nullptr;
120 m_webPageProxy.process().removeMessageReceiver(Messages::ViewGestureController::messageReceiverName(), m_webPageProxy.pageID());
123 static double resistanceForDelta(double deltaScale, double currentScale)
125 // Zoom out with slight acceleration, until we reach minimum scale.
126 if (deltaScale < 0 && currentScale > minMagnification)
129 // Zoom in with no acceleration, until we reach maximum scale.
130 if (deltaScale > 0 && currentScale < maxMagnification)
133 // Outside of the extremes, resist further scaling.
134 double limit = currentScale < minMagnification ? minMagnification : maxMagnification;
135 double scaleDistance = fabs(limit - currentScale);
136 double scalePercent = std::min(std::max(scaleDistance / limit, 0.), 1.);
137 double resistance = zoomOutResistance + scalePercent * (0.01 - zoomOutResistance);
142 FloatPoint ViewGestureController::scaledMagnificationOrigin(FloatPoint origin, double scale)
144 FloatPoint scaledMagnificationOrigin(origin);
145 scaledMagnificationOrigin.moveBy(m_visibleContentRect.location());
146 float magnificationOriginScale = 1 - (scale / m_webPageProxy.pageScaleFactor());
147 scaledMagnificationOrigin.scale(magnificationOriginScale, magnificationOriginScale);
148 return scaledMagnificationOrigin;
151 void ViewGestureController::didCollectGeometryForMagnificationGesture(FloatRect visibleContentRect, bool frameHandlesMagnificationGesture)
153 m_activeGestureType = ViewGestureType::Magnification;
154 m_visibleContentRect = visibleContentRect;
155 m_visibleContentRectIsValid = true;
156 m_frameHandlesMagnificationGesture = frameHandlesMagnificationGesture;
159 void ViewGestureController::handleMagnificationGesture(double scale, FloatPoint origin)
161 ASSERT(m_activeGestureType == ViewGestureType::None || m_activeGestureType == ViewGestureType::Magnification);
163 if (m_activeGestureType == ViewGestureType::None) {
164 // FIXME: We drop the first frame of the gesture on the floor, because we don't have the visible content bounds yet.
165 m_magnification = m_webPageProxy.pageScaleFactor();
166 m_webPageProxy.process().send(Messages::ViewGestureGeometryCollector::CollectGeometryForMagnificationGesture(), m_webPageProxy.pageID());
167 m_lastMagnificationGestureWasSmartMagnification = false;
172 // We're still waiting for the DidCollectGeometry callback.
173 if (!m_visibleContentRectIsValid)
176 m_activeGestureType = ViewGestureType::Magnification;
178 double scaleWithResistance = resistanceForDelta(scale, m_magnification) * scale;
180 m_magnification += m_magnification * scaleWithResistance;
181 m_magnification = std::min(std::max(m_magnification, minElasticMagnification), maxElasticMagnification);
183 m_magnificationOrigin = origin;
185 if (m_frameHandlesMagnificationGesture)
186 m_webPageProxy.scalePage(m_magnification, roundedIntPoint(origin));
188 m_webPageProxy.drawingArea()->adjustTransientZoom(m_magnification, scaledMagnificationOrigin(origin, m_magnification));
191 void ViewGestureController::endMagnificationGesture()
193 if (m_activeGestureType != ViewGestureType::Magnification)
196 double newMagnification = std::min(std::max(m_magnification, minMagnification), maxMagnification);
198 if (m_frameHandlesMagnificationGesture)
199 m_webPageProxy.scalePage(newMagnification, roundedIntPoint(m_magnificationOrigin));
201 if (auto drawingArea = m_webPageProxy.drawingArea())
202 drawingArea->commitTransientZoom(newMagnification, scaledMagnificationOrigin(m_magnificationOrigin, newMagnification));
205 m_activeGestureType = ViewGestureType::None;
206 m_visibleContentRectIsValid = false;
209 void ViewGestureController::handleSmartMagnificationGesture(FloatPoint origin)
211 if (m_activeGestureType != ViewGestureType::None)
214 m_webPageProxy.process().send(Messages::ViewGestureGeometryCollector::CollectGeometryForSmartMagnificationGesture(origin), m_webPageProxy.pageID());
217 static float maximumRectangleComponentDelta(FloatRect a, FloatRect b)
219 return std::max(fabs(a.x() - b.x()), std::max(fabs(a.y() - b.y()), std::max(fabs(a.width() - b.width()), fabs(a.height() - b.height()))));
222 void ViewGestureController::didCollectGeometryForSmartMagnificationGesture(FloatPoint origin, FloatRect renderRect, FloatRect visibleContentRect, bool isReplacedElement, double viewportMinimumScale, double viewportMaximumScale)
224 double currentScaleFactor = m_webPageProxy.pageScaleFactor();
226 FloatRect unscaledTargetRect = renderRect;
228 // If there was no usable element under the cursor, we'll scale towards the cursor instead.
229 if (unscaledTargetRect.isEmpty())
230 unscaledTargetRect.setLocation(origin);
232 unscaledTargetRect.scale(1 / currentScaleFactor);
233 unscaledTargetRect.inflateX(unscaledTargetRect.width() * smartMagnificationElementPadding);
234 unscaledTargetRect.inflateY(unscaledTargetRect.height() * smartMagnificationElementPadding);
236 double targetMagnification = visibleContentRect.width() / unscaledTargetRect.width();
238 FloatRect unscaledVisibleContentRect = visibleContentRect;
239 unscaledVisibleContentRect.scale(1 / currentScaleFactor);
240 FloatRect viewportConstrainedUnscaledTargetRect = unscaledTargetRect;
241 viewportConstrainedUnscaledTargetRect.intersect(unscaledVisibleContentRect);
243 if (unscaledTargetRect.width() > viewportConstrainedUnscaledTargetRect.width())
244 viewportConstrainedUnscaledTargetRect.setX(unscaledVisibleContentRect.x() + (origin.x() / currentScaleFactor) - viewportConstrainedUnscaledTargetRect.width() / 2);
245 if (unscaledTargetRect.height() > viewportConstrainedUnscaledTargetRect.height())
246 viewportConstrainedUnscaledTargetRect.setY(unscaledVisibleContentRect.y() + (origin.y() / currentScaleFactor) - viewportConstrainedUnscaledTargetRect.height() / 2);
248 // For replaced elements like images, we want to fit the whole element
249 // in the view, so scale it down enough to make both dimensions fit if possible.
250 if (isReplacedElement)
251 targetMagnification = std::min(targetMagnification, static_cast<double>(visibleContentRect.height() / viewportConstrainedUnscaledTargetRect.height()));
253 targetMagnification = std::min(std::max(targetMagnification, minMagnification), maxMagnification);
255 // Allow panning between elements via double-tap while magnified, unless the target rect is
256 // similar to the last one or the mouse has not moved, in which case we'll zoom all the way out.
257 if (currentScaleFactor > 1 && m_lastMagnificationGestureWasSmartMagnification) {
258 if (maximumRectangleComponentDelta(m_lastSmartMagnificationUnscaledTargetRect, unscaledTargetRect) < smartMagnificationPanScrollThreshold)
259 targetMagnification = 1;
261 if (m_lastSmartMagnificationOrigin == origin)
262 targetMagnification = 1;
265 FloatRect targetRect(viewportConstrainedUnscaledTargetRect);
266 targetRect.scale(targetMagnification);
267 FloatPoint targetOrigin(visibleContentRect.center());
268 targetOrigin.moveBy(-targetRect.center());
270 m_webPageProxy.drawingArea()->adjustTransientZoom(m_webPageProxy.pageScaleFactor(), scaledMagnificationOrigin(FloatPoint(), m_webPageProxy.pageScaleFactor()));
271 m_webPageProxy.drawingArea()->commitTransientZoom(targetMagnification, targetOrigin);
273 m_lastSmartMagnificationUnscaledTargetRect = unscaledTargetRect;
274 m_lastSmartMagnificationOrigin = origin;
276 m_lastMagnificationGestureWasSmartMagnification = true;
279 bool ViewGestureController::scrollEventCanBecomeSwipe(NSEvent *event, ViewGestureController::SwipeDirection& potentialSwipeDirection)
281 if (event.phase != NSEventPhaseBegan)
284 if (!event.hasPreciseScrollingDeltas)
287 if (![NSEvent isSwipeTrackingFromScrollEventsEnabled])
290 if (fabs(event.scrollingDeltaX) <= fabs(event.scrollingDeltaY))
293 bool isPinnedToLeft = m_shouldIgnorePinnedState || m_webPageProxy.isPinnedToLeftSide();
294 bool isPinnedToRight = m_shouldIgnorePinnedState || m_webPageProxy.isPinnedToRightSide();
296 bool willSwipeLeft = event.scrollingDeltaX > 0 && isPinnedToLeft && m_webPageProxy.backForwardList().backItem();
297 bool willSwipeRight = event.scrollingDeltaX < 0 && isPinnedToRight && m_webPageProxy.backForwardList().forwardItem();
298 if (!willSwipeLeft && !willSwipeRight)
301 potentialSwipeDirection = willSwipeLeft ? ViewGestureController::SwipeDirection::Back : ViewGestureController::SwipeDirection::Forward;
306 bool ViewGestureController::deltaIsSufficientToBeginSwipe(NSEvent *event)
308 if (m_pendingSwipeReason != PendingSwipeReason::InsufficientMagnitude)
311 m_cumulativeDeltaForPendingSwipe += FloatSize(event.scrollingDeltaX, event.scrollingDeltaY);
313 // If the cumulative delta is ever "too vertical", we will stop tracking this
314 // as a potential swipe until we get another "begin" event.
315 if (fabs(m_cumulativeDeltaForPendingSwipe.height()) >= fabs(m_cumulativeDeltaForPendingSwipe.width()) * minimumScrollEventRatioForSwipe) {
316 m_pendingSwipeReason = PendingSwipeReason::None;
320 if (fabs(m_cumulativeDeltaForPendingSwipe.width()) < minimumHorizontalSwipeDistance)
326 void ViewGestureController::setDidMoveSwipeSnapshotCallback(void(^callback)(CGRect))
328 if (m_didMoveSwipeSnapshotCallback)
329 Block_release(m_didMoveSwipeSnapshotCallback);
330 m_didMoveSwipeSnapshotCallback = Block_copy(callback);
333 bool ViewGestureController::handleScrollWheelEvent(NSEvent *event)
335 if (event.phase == NSEventPhaseEnded) {
336 m_cumulativeDeltaForPendingSwipe = FloatSize();
337 m_pendingSwipeReason = PendingSwipeReason::None;
340 if (m_pendingSwipeReason == PendingSwipeReason::InsufficientMagnitude) {
341 if (deltaIsSufficientToBeginSwipe(event)) {
342 trackSwipeGesture(event, m_pendingSwipeDirection);
347 if (m_activeGestureType != ViewGestureType::None)
350 SwipeDirection direction;
351 if (!scrollEventCanBecomeSwipe(event, direction))
354 if (!m_shouldIgnorePinnedState && m_webPageProxy.willHandleHorizontalScrollEvents()) {
355 m_pendingSwipeReason = PendingSwipeReason::WebCoreMayScroll;
356 m_pendingSwipeDirection = direction;
360 if (!deltaIsSufficientToBeginSwipe(event)) {
361 m_pendingSwipeReason = PendingSwipeReason::InsufficientMagnitude;
362 m_pendingSwipeDirection = direction;
366 trackSwipeGesture(event, direction);
371 void ViewGestureController::wheelEventWasNotHandledByWebCore(NSEvent *event)
373 if (m_pendingSwipeReason != PendingSwipeReason::WebCoreMayScroll)
376 m_pendingSwipeReason = PendingSwipeReason::None;
378 SwipeDirection direction;
379 if (!scrollEventCanBecomeSwipe(event, direction))
382 if (!deltaIsSufficientToBeginSwipe(event)) {
383 m_pendingSwipeReason = PendingSwipeReason::InsufficientMagnitude;
387 trackSwipeGesture(event, m_pendingSwipeDirection);
390 void ViewGestureController::trackSwipeGesture(NSEvent *event, SwipeDirection direction)
392 ASSERT(m_activeGestureType == ViewGestureType::None);
393 m_pendingSwipeReason = PendingSwipeReason::None;
395 m_webPageProxy.recordNavigationSnapshot();
397 CGFloat maxProgress = (direction == SwipeDirection::Back) ? 1 : 0;
398 CGFloat minProgress = (direction == SwipeDirection::Forward) ? -1 : 0;
399 RefPtr<WebBackForwardListItem> targetItem = (direction == SwipeDirection::Back) ? m_webPageProxy.backForwardList().backItem() : m_webPageProxy.backForwardList().forwardItem();
403 __block bool swipeCancelled = false;
405 ASSERT(!m_swipeCancellationTracker);
406 RetainPtr<WKSwipeCancellationTracker> swipeCancellationTracker = adoptNS([[WKSwipeCancellationTracker alloc] init]);
407 m_swipeCancellationTracker = swipeCancellationTracker;
409 [event trackSwipeEventWithOptions:0 dampenAmountThresholdMin:minProgress max:maxProgress usingHandler:^(CGFloat progress, NSEventPhase phase, BOOL isComplete, BOOL *stop) {
410 if ([swipeCancellationTracker isCancelled]) {
414 if (phase == NSEventPhaseBegan)
415 this->beginSwipeGesture(targetItem.get(), direction);
416 CGFloat clampedProgress = std::min(std::max(progress, minProgress), maxProgress);
417 this->handleSwipeGesture(targetItem.get(), clampedProgress, direction);
418 if (phase == NSEventPhaseCancelled)
419 swipeCancelled = true;
420 if (phase == NSEventPhaseEnded || phase == NSEventPhaseCancelled)
421 this->willEndSwipeGesture(*targetItem, swipeCancelled);
423 this->endSwipeGesture(targetItem.get(), swipeCancelled);
427 void ViewGestureController::willEndSwipeGesture(WebBackForwardListItem& targetItem, bool cancelled)
429 m_webPageProxy.navigationGestureWillEnd(!cancelled, targetItem);
432 FloatRect ViewGestureController::windowRelativeBoundsForCustomSwipeViews() const
435 for (const auto& view : m_customSwipeViews)
436 swipeArea.unite([view convertRect:[view bounds] toView:nil]);
437 swipeArea.setHeight(swipeArea.height() - m_customSwipeViewsTopContentInset);
441 static CALayer *leastCommonAncestorLayer(const Vector<RetainPtr<CALayer>>& layers)
443 Vector<Vector<CALayer *>> liveLayerPathsFromRoot(layers.size());
445 size_t shortestPathLength = std::numeric_limits<size_t>::max();
447 for (size_t layerIndex = 0; layerIndex < layers.size(); layerIndex++) {
448 CALayer *parent = [layers[layerIndex] superlayer];
450 liveLayerPathsFromRoot[layerIndex].insert(0, parent);
451 shortestPathLength = std::min(shortestPathLength, liveLayerPathsFromRoot[layerIndex].size());
452 parent = parent.superlayer;
456 for (size_t pathIndex = 0; pathIndex < shortestPathLength; pathIndex++) {
457 CALayer *firstPathLayer = liveLayerPathsFromRoot[0][pathIndex];
458 for (size_t layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
459 if (liveLayerPathsFromRoot[layerIndex][pathIndex] != firstPathLayer)
460 return firstPathLayer;
464 return liveLayerPathsFromRoot[0][shortestPathLength];
467 CALayer *ViewGestureController::determineSnapshotLayerParent() const
469 if (m_currentSwipeLiveLayers.size() == 1)
470 return [m_currentSwipeLiveLayers[0] superlayer];
472 // We insert our snapshot into the first shared superlayer of the custom views' layer, above the frontmost or below the bottommost layer.
473 return leastCommonAncestorLayer(m_currentSwipeLiveLayers);
476 CALayer *ViewGestureController::determineLayerAdjacentToSnapshotForParent(SwipeDirection direction, CALayer *snapshotLayerParent) const
478 // If we have custom swiping views, we assume that the views were passed to us in back-to-front z-order.
479 CALayer *layerAdjacentToSnapshot = direction == SwipeDirection::Back ? m_currentSwipeLiveLayers.first().get() : m_currentSwipeLiveLayers.last().get();
481 if (m_currentSwipeLiveLayers.size() == 1)
482 return layerAdjacentToSnapshot;
484 // If the layers are not all siblings, find the child of the layer we're going to insert the snapshot into which has the frontmost/bottommost layer as a child.
485 while (snapshotLayerParent != layerAdjacentToSnapshot.superlayer)
486 layerAdjacentToSnapshot = layerAdjacentToSnapshot.superlayer;
487 return layerAdjacentToSnapshot;
490 bool ViewGestureController::shouldUseSnapshotForSize(ViewSnapshot& snapshot, FloatSize swipeLayerSize, float topContentInset)
492 float deviceScaleFactor = m_webPageProxy.deviceScaleFactor();
493 if (snapshot.deviceScaleFactor() != deviceScaleFactor)
496 FloatSize unobscuredSwipeLayerSizeInDeviceCoordinates = swipeLayerSize - FloatSize(0, topContentInset);
497 unobscuredSwipeLayerSizeInDeviceCoordinates.scale(deviceScaleFactor);
498 if (snapshot.size() != unobscuredSwipeLayerSizeInDeviceCoordinates)
504 static bool layerGeometryFlippedToRoot(CALayer *layer)
506 bool flipped = false;
507 CALayer *parent = layer;
509 if (parent.isGeometryFlipped)
511 parent = parent.superlayer;
516 void ViewGestureController::applyDebuggingPropertiesToSwipeViews()
518 CAFilter* filter = [CAFilter filterWithType:kCAFilterColorInvert];
519 [m_swipeLayer setFilters:@[ filter ]];
520 [m_swipeLayer setBackgroundColor:[NSColor blueColor].CGColor];
521 [m_swipeLayer setBorderColor:[NSColor yellowColor].CGColor];
522 [m_swipeLayer setBorderWidth:4];
524 [m_swipeSnapshotLayer setBackgroundColor:[NSColor greenColor].CGColor];
525 [m_swipeSnapshotLayer setBorderColor:[NSColor redColor].CGColor];
526 [m_swipeSnapshotLayer setBorderWidth:2];
529 void ViewGestureController::beginSwipeGesture(WebBackForwardListItem* targetItem, SwipeDirection direction)
531 ASSERT(m_currentSwipeLiveLayers.isEmpty());
533 m_webPageProxy.navigationGestureDidBegin();
535 m_activeGestureType = ViewGestureType::Swipe;
536 m_swipeInProgress = true;
538 CALayer *rootContentLayer = m_webPageProxy.acceleratedCompositingRootLayer();
540 m_swipeLayer = adoptNS([[CALayer alloc] init]);
541 m_swipeSnapshotLayer = adoptNS([[CALayer alloc] init]);
542 m_currentSwipeCustomViewBounds = windowRelativeBoundsForCustomSwipeViews();
545 float topContentInset = 0;
546 if (!m_customSwipeViews.isEmpty()) {
547 topContentInset = m_customSwipeViewsTopContentInset;
548 swipeArea = m_currentSwipeCustomViewBounds;
549 swipeArea.expand(0, topContentInset);
551 for (const auto& view : m_customSwipeViews) {
552 CALayer *layer = [view layer];
554 m_currentSwipeLiveLayers.append(layer);
557 swipeArea = FloatRect(FloatPoint(), m_webPageProxy.viewSize());
558 topContentInset = m_webPageProxy.topContentInset();
559 m_currentSwipeLiveLayers.append(rootContentLayer);
562 CALayer *snapshotLayerParent = determineSnapshotLayerParent();
563 bool geometryIsFlippedToRoot = layerGeometryFlippedToRoot(snapshotLayerParent);
565 RetainPtr<CGColorRef> backgroundColor = CGColorGetConstantColor(kCGColorWhite);
566 if (ViewSnapshot* snapshot = targetItem->snapshot()) {
567 if (shouldUseSnapshotForSize(*snapshot, swipeArea.size(), topContentInset))
568 [m_swipeSnapshotLayer setContents:snapshot->asLayerContents()];
570 Color coreColor = snapshot->backgroundColor();
571 if (coreColor.isValid())
572 backgroundColor = cachedCGColor(coreColor, ColorSpaceDeviceRGB);
573 #if USE_IOSURFACE_VIEW_SNAPSHOTS
574 m_currentSwipeSnapshot = snapshot;
578 [m_swipeLayer setBackgroundColor:backgroundColor.get()];
579 [m_swipeLayer setAnchorPoint:CGPointZero];
580 [m_swipeLayer setFrame:swipeArea];
581 [m_swipeLayer setName:@"Gesture Swipe Root Layer"];
582 [m_swipeLayer setGeometryFlipped:geometryIsFlippedToRoot];
583 [m_swipeLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
585 float deviceScaleFactor = m_webPageProxy.deviceScaleFactor();
586 [m_swipeSnapshotLayer setContentsGravity:kCAGravityTopLeft];
587 [m_swipeSnapshotLayer setContentsScale:deviceScaleFactor];
588 [m_swipeSnapshotLayer setAnchorPoint:CGPointZero];
589 [m_swipeSnapshotLayer setFrame:CGRectMake(0, 0, swipeArea.width(), swipeArea.height() - topContentInset)];
590 [m_swipeSnapshotLayer setName:@"Gesture Swipe Snapshot Layer"];
591 [m_swipeSnapshotLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
593 [m_swipeLayer addSublayer:m_swipeSnapshotLayer.get()];
595 if (m_webPageProxy.preferences().viewGestureDebuggingEnabled())
596 applyDebuggingPropertiesToSwipeViews();
599 CALayer *layerAdjacentToSnapshot = determineLayerAdjacentToSnapshotForParent(direction, snapshotLayerParent);
600 if (direction == SwipeDirection::Back)
601 [snapshotLayerParent insertSublayer:m_swipeLayer.get() below:layerAdjacentToSnapshot];
603 [snapshotLayerParent insertSublayer:m_swipeLayer.get() above:layerAdjacentToSnapshot];
605 // We don't know enough about the custom views' hierarchy to apply a shadow.
606 if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap && m_customSwipeViews.isEmpty()) {
607 #if ENABLE(LEGACY_SWIPE_SHADOW_STYLE)
608 if (direction == SwipeDirection::Back) {
609 float topContentInset = m_webPageProxy.topContentInset();
610 FloatRect shadowRect(FloatPoint(0, topContentInset), m_webPageProxy.viewSize() - FloatSize(0, topContentInset));
611 RetainPtr<CGPathRef> shadowPath = adoptCF(CGPathCreateWithRect(shadowRect, 0));
612 [rootContentLayer setShadowColor:CGColorGetConstantColor(kCGColorBlack)];
613 [rootContentLayer setShadowOpacity:swipeOverlayShadowOpacity];
614 [rootContentLayer setShadowRadius:swipeOverlayShadowRadius];
615 [rootContentLayer setShadowPath:shadowPath.get()];
617 RetainPtr<CGPathRef> shadowPath = adoptCF(CGPathCreateWithRect([m_swipeLayer bounds], 0));
618 [m_swipeLayer setShadowColor:CGColorGetConstantColor(kCGColorBlack)];
619 [m_swipeLayer setShadowOpacity:swipeOverlayShadowOpacity];
620 [m_swipeLayer setShadowRadius:swipeOverlayShadowRadius];
621 [m_swipeLayer setShadowPath:shadowPath.get()];
624 FloatRect dimmingRect(FloatPoint(), m_webPageProxy.viewSize());
625 m_swipeDimmingLayer = adoptNS([[CALayer alloc] init]);
626 [m_swipeDimmingLayer setName:@"Gesture Swipe Dimming Layer"];
627 [m_swipeDimmingLayer setBackgroundColor:[NSColor blackColor].CGColor];
628 [m_swipeDimmingLayer setOpacity:swipeOverlayDimmingOpacity];
629 [m_swipeDimmingLayer setAnchorPoint:CGPointZero];
630 [m_swipeDimmingLayer setFrame:dimmingRect];
631 [m_swipeDimmingLayer setGeometryFlipped:geometryIsFlippedToRoot];
632 [m_swipeDimmingLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
634 NSImage *shadowImage = [[NSBundle bundleForClass:[WKSwipeCancellationTracker class]] imageForResource:@"SwipeShadow"];
635 FloatRect shadowRect(-shadowImage.size.width, topContentInset, shadowImage.size.width, m_webPageProxy.viewSize().height() - topContentInset);
636 m_swipeShadowLayer = adoptNS([[CALayer alloc] init]);
637 [m_swipeShadowLayer setName:@"Gesture Swipe Shadow Layer"];
638 [m_swipeShadowLayer setBackgroundColor:[NSColor colorWithPatternImage:shadowImage].CGColor];
639 [m_swipeShadowLayer setContentsScale:deviceScaleFactor];
640 [m_swipeShadowLayer setOpacity:swipeOverlayShadowOpacity];
641 [m_swipeShadowLayer setAnchorPoint:CGPointZero];
642 [m_swipeShadowLayer setFrame:shadowRect];
643 [m_swipeShadowLayer setGeometryFlipped:geometryIsFlippedToRoot];
644 [m_swipeShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
646 if (direction == SwipeDirection::Back)
647 [snapshotLayerParent insertSublayer:m_swipeDimmingLayer.get() above:m_swipeLayer.get()];
649 [snapshotLayerParent insertSublayer:m_swipeDimmingLayer.get() below:m_swipeLayer.get()];
651 [snapshotLayerParent insertSublayer:m_swipeShadowLayer.get() above:m_swipeLayer.get()];
656 void ViewGestureController::handleSwipeGesture(WebBackForwardListItem* targetItem, double progress, SwipeDirection direction)
658 ASSERT(m_activeGestureType == ViewGestureType::Swipe);
660 if (!m_webPageProxy.drawingArea())
664 if (!m_customSwipeViews.isEmpty())
665 width = m_currentSwipeCustomViewBounds.width();
667 width = m_webPageProxy.drawingArea()->size().width();
669 double swipingLayerOffset = floor(width * progress);
671 #if !ENABLE(LEGACY_SWIPE_SHADOW_STYLE)
672 double dimmingProgress = (direction == SwipeDirection::Back) ? 1 - progress : -progress;
673 dimmingProgress = std::min(1., std::max(dimmingProgress, 0.));
674 [m_swipeDimmingLayer setOpacity:dimmingProgress * swipeOverlayDimmingOpacity];
676 double absoluteProgress = fabs(progress);
677 double remainingSwipeDistance = width - fabs(absoluteProgress * width);
678 double shadowFadeDistance = [m_swipeShadowLayer bounds].size.width;
679 if (remainingSwipeDistance < shadowFadeDistance)
680 [m_swipeShadowLayer setOpacity:(remainingSwipeDistance / shadowFadeDistance) * swipeOverlayShadowOpacity];
682 [m_swipeShadowLayer setOpacity:swipeOverlayShadowOpacity];
684 if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap)
685 [m_swipeShadowLayer setTransform:CATransform3DMakeTranslation((direction == SwipeDirection::Back ? 0 : width) + swipingLayerOffset, 0, 0)];
688 if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap) {
689 if (direction == SwipeDirection::Forward) {
690 [m_swipeLayer setTransform:CATransform3DMakeTranslation(width + swipingLayerOffset, 0, 0)];
691 didMoveSwipeSnapshotLayer();
693 } else if (m_swipeTransitionStyle == SwipeTransitionStyle::Push)
694 [m_swipeLayer setTransform:CATransform3DMakeTranslation((direction == SwipeDirection::Back ? -width : width) + swipingLayerOffset, 0, 0)];
696 for (const auto& layer : m_currentSwipeLiveLayers) {
697 if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap) {
698 if (direction == SwipeDirection::Back)
699 [layer setTransform:CATransform3DMakeTranslation(swipingLayerOffset, 0, 0)];
700 } else if (m_swipeTransitionStyle == SwipeTransitionStyle::Push)
701 [layer setTransform:CATransform3DMakeTranslation(swipingLayerOffset, 0, 0)];
705 void ViewGestureController::didMoveSwipeSnapshotLayer()
707 if (!m_didMoveSwipeSnapshotCallback)
710 m_didMoveSwipeSnapshotCallback(m_webPageProxy.boundsOfLayerInLayerBackedWindowCoordinates(m_swipeLayer.get()));
713 void ViewGestureController::endSwipeGesture(WebBackForwardListItem* targetItem, bool cancelled)
715 ASSERT(m_activeGestureType == ViewGestureType::Swipe);
717 m_swipeCancellationTracker = nullptr;
719 m_swipeInProgress = false;
721 CALayer *rootLayer = m_webPageProxy.acceleratedCompositingRootLayer();
723 [rootLayer setShadowOpacity:0];
724 [rootLayer setShadowRadius:0];
727 removeSwipeSnapshot();
728 m_webPageProxy.navigationGestureDidEnd(false, *targetItem);
732 uint64_t renderTreeSize = 0;
733 if (ViewSnapshot* snapshot = targetItem->snapshot())
734 renderTreeSize = snapshot->renderTreeSize();
736 m_webPageProxy.process().send(Messages::ViewGestureGeometryCollector::SetRenderTreeSizeNotificationThreshold(renderTreeSize * swipeSnapshotRemovalRenderTreeSizeTargetFraction), m_webPageProxy.pageID());
738 m_swipeWaitingForVisuallyNonEmptyLayout = true;
739 m_swipeWaitingForRenderTreeSizeThreshold = true;
741 m_webPageProxy.navigationGestureDidEnd(true, *targetItem);
742 m_webPageProxy.goToBackForwardItem(targetItem);
744 // FIXME: Like on iOS, we should ensure that even if one of the timeouts fires,
745 // we never show the old page content, instead showing white (or the snapshot background color).
746 m_swipeWatchdogTimer.startOneShot(swipeSnapshotRemovalWatchdogDuration.count());
748 if (ViewSnapshot* snapshot = targetItem->snapshot())
749 m_backgroundColorForCurrentSnapshot = snapshot->backgroundColor();
752 void ViewGestureController::didHitRenderTreeSizeThreshold()
754 if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress)
757 m_swipeWaitingForRenderTreeSizeThreshold = false;
759 if (!m_swipeWaitingForVisuallyNonEmptyLayout) {
760 // FIXME: Ideally we would call removeSwipeSnapshotAfterRepaint() here, but sometimes
761 // scroll position isn't done restoring until didFinishLoadForFrame, so we flash the wrong content.
765 void ViewGestureController::didFirstVisuallyNonEmptyLayoutForMainFrame()
767 if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress)
770 m_swipeWaitingForVisuallyNonEmptyLayout = false;
772 if (!m_swipeWaitingForRenderTreeSizeThreshold) {
773 // FIXME: Ideally we would call removeSwipeSnapshotAfterRepaint() here, but sometimes
774 // scroll position isn't done restoring until didFinishLoadForFrame, so we flash the wrong content.
776 m_swipeWatchdogAfterFirstVisuallyNonEmptyLayoutTimer.startOneShot(swipeSnapshotRemovalWatchdogAfterFirstVisuallyNonEmptyLayoutDuration.count());
777 m_swipeWatchdogTimer.stop();
781 void ViewGestureController::didFinishLoadForMainFrame()
783 if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress)
786 if (m_webPageProxy.pageLoadState().isLoading()) {
787 m_swipeActiveLoadMonitoringTimer.startRepeating(swipeSnapshotRemovalActiveLoadMonitoringInterval);
791 removeSwipeSnapshotAfterRepaint();
794 void ViewGestureController::didSameDocumentNavigationForMainFrame(SameDocumentNavigationType type)
796 if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress)
799 if (type != SameDocumentNavigationSessionStateReplace && type != SameDocumentNavigationSessionStatePop)
802 m_swipeActiveLoadMonitoringTimer.startRepeating(swipeSnapshotRemovalActiveLoadMonitoringInterval);
805 void ViewGestureController::activeLoadMonitoringTimerFired()
807 if (m_webPageProxy.pageLoadState().isLoading())
810 removeSwipeSnapshotAfterRepaint();
813 void ViewGestureController::swipeSnapshotWatchdogTimerFired()
815 removeSwipeSnapshotAfterRepaint();
818 void ViewGestureController::removeSwipeSnapshotAfterRepaint()
820 m_swipeActiveLoadMonitoringTimer.stop();
822 if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress)
825 if (m_swipeWaitingForRepaint)
828 m_swipeWaitingForRepaint = true;
830 WebPageProxy* webPageProxy = &m_webPageProxy;
831 m_webPageProxy.forceRepaint(VoidCallback::create([webPageProxy] (CallbackBase::Error error) {
832 webPageProxy->removeNavigationGestureSnapshot();
836 void ViewGestureController::removeSwipeSnapshot()
838 m_swipeWaitingForRepaint = false;
840 m_swipeWatchdogTimer.stop();
841 m_swipeWatchdogAfterFirstVisuallyNonEmptyLayoutTimer.stop();
843 if (m_activeGestureType != ViewGestureType::Swipe)
846 #if USE_IOSURFACE_VIEW_SNAPSHOTS
847 if (m_currentSwipeSnapshot && m_currentSwipeSnapshot->surface())
848 m_currentSwipeSnapshot->surface()->setIsVolatile(true);
849 m_currentSwipeSnapshot = nullptr;
852 for (const auto& layer : m_currentSwipeLiveLayers)
853 [layer setTransform:CATransform3DIdentity];
855 [m_swipeSnapshotLayer removeFromSuperlayer];
856 m_swipeSnapshotLayer = nullptr;
858 [m_swipeLayer removeFromSuperlayer];
859 m_swipeLayer = nullptr;
861 #if !ENABLE(LEGACY_SWIPE_SHADOW_STYLE)
862 [m_swipeShadowLayer removeFromSuperlayer];
863 m_swipeShadowLayer = nullptr;
865 [m_swipeDimmingLayer removeFromSuperlayer];
866 m_swipeDimmingLayer = nullptr;
869 m_currentSwipeLiveLayers.clear();
871 m_activeGestureType = ViewGestureType::None;
873 m_webPageProxy.navigationGestureSnapshotWasRemoved();
875 m_backgroundColorForCurrentSnapshot = Color();
878 double ViewGestureController::magnification() const
880 if (m_activeGestureType == ViewGestureType::Magnification)
881 return m_magnification;
883 return m_webPageProxy.pageScaleFactor();
886 } // namespace WebKit
888 #endif // !PLATFORM(IOS)