/* * Copyright (C) 2013, 2014 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "ViewGestureController.h" #if PLATFORM(MAC) #import "FrameLoadState.h" #import "NativeWebWheelEvent.h" #import "ViewGestureControllerMessages.h" #import "ViewGestureGeometryCollectorMessages.h" #import "ViewSnapshotStore.h" #import "WebBackForwardList.h" #import "WebPageGroup.h" #import "WebPageMessages.h" #import "WebPageProxy.h" #import "WebPreferences.h" #import "WebProcessProxy.h" #import #import #import #import #import using namespace WebCore; #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101000 #define ENABLE_LEGACY_SWIPE_SHADOW_STYLE 1 #else #define ENABLE_LEGACY_SWIPE_SHADOW_STYLE 0 #endif static const double minMagnification = 1; static const double maxMagnification = 3; static const double minElasticMagnification = 0.75; static const double maxElasticMagnification = 4; static const double zoomOutBoost = 1.6; static const double zoomOutResistance = 0.10; static const float smartMagnificationElementPadding = 0.05; static const float smartMagnificationPanScrollThreshold = 100; #if ENABLE(LEGACY_SWIPE_SHADOW_STYLE) static const double swipeOverlayShadowOpacity = 0.66; static const double swipeOverlayShadowRadius = 3; #else static const double swipeOverlayShadowOpacity = 0.06; static const double swipeOverlayDimmingOpacity = 0.12; static const CGFloat swipeOverlayShadowWidth = 81; #endif static const CGFloat minimumHorizontalSwipeDistance = 15; static const float minimumScrollEventRatioForSwipe = 0.5; static const float swipeSnapshotRemovalRenderTreeSizeTargetFraction = 0.5; static const std::chrono::seconds swipeSnapshotRemovalWatchdogDuration = 5_s; static const std::chrono::seconds swipeSnapshotRemovalWatchdogAfterFirstVisuallyNonEmptyLayoutDuration = 3_s; static const std::chrono::milliseconds swipeSnapshotRemovalActiveLoadMonitoringInterval = 250_ms; @interface WKSwipeCancellationTracker : NSObject { @private BOOL _isCancelled; } @property (nonatomic) BOOL isCancelled; @end @implementation WKSwipeCancellationTracker @synthesize isCancelled=_isCancelled; @end namespace WebKit { ViewGestureController::ViewGestureController(WebPageProxy& webPageProxy) : m_webPageProxy(webPageProxy) , m_swipeWatchdogTimer(RunLoop::main(), this, &ViewGestureController::swipeSnapshotWatchdogTimerFired) , m_swipeActiveLoadMonitoringTimer(RunLoop::main(), this, &ViewGestureController::activeLoadMonitoringTimerFired) , m_swipeWatchdogAfterFirstVisuallyNonEmptyLayoutTimer(RunLoop::main(), this, &ViewGestureController::swipeSnapshotWatchdogTimerFired) { m_webPageProxy.process().addMessageReceiver(Messages::ViewGestureController::messageReceiverName(), m_webPageProxy.pageID(), *this); } ViewGestureController::~ViewGestureController() { if (m_swipeCancellationTracker) [m_swipeCancellationTracker setIsCancelled:YES]; if (m_activeGestureType == ViewGestureType::Swipe) removeSwipeSnapshot(); if (m_didMoveSwipeSnapshotCallback) { Block_release(m_didMoveSwipeSnapshotCallback); m_didMoveSwipeSnapshotCallback = nullptr; } m_webPageProxy.process().removeMessageReceiver(Messages::ViewGestureController::messageReceiverName(), m_webPageProxy.pageID()); } static double resistanceForDelta(double deltaScale, double currentScale) { // Zoom out with slight acceleration, until we reach minimum scale. if (deltaScale < 0 && currentScale > minMagnification) return zoomOutBoost; // Zoom in with no acceleration, until we reach maximum scale. if (deltaScale > 0 && currentScale < maxMagnification) return 1; // Outside of the extremes, resist further scaling. double limit = currentScale < minMagnification ? minMagnification : maxMagnification; double scaleDistance = fabs(limit - currentScale); double scalePercent = std::min(std::max(scaleDistance / limit, 0.), 1.); double resistance = zoomOutResistance + scalePercent * (0.01 - zoomOutResistance); return resistance; } FloatPoint ViewGestureController::scaledMagnificationOrigin(FloatPoint origin, double scale) { FloatPoint scaledMagnificationOrigin(origin); scaledMagnificationOrigin.moveBy(m_visibleContentRect.location()); float magnificationOriginScale = 1 - (scale / m_webPageProxy.pageScaleFactor()); scaledMagnificationOrigin.scale(magnificationOriginScale, magnificationOriginScale); return scaledMagnificationOrigin; } void ViewGestureController::didCollectGeometryForMagnificationGesture(FloatRect visibleContentRect, bool frameHandlesMagnificationGesture) { m_activeGestureType = ViewGestureType::Magnification; m_visibleContentRect = visibleContentRect; m_visibleContentRectIsValid = true; m_frameHandlesMagnificationGesture = frameHandlesMagnificationGesture; } void ViewGestureController::handleMagnificationGesture(double scale, FloatPoint origin) { ASSERT(m_activeGestureType == ViewGestureType::None || m_activeGestureType == ViewGestureType::Magnification); if (m_activeGestureType == ViewGestureType::None) { // FIXME: We drop the first frame of the gesture on the floor, because we don't have the visible content bounds yet. m_magnification = m_webPageProxy.pageScaleFactor(); m_webPageProxy.process().send(Messages::ViewGestureGeometryCollector::CollectGeometryForMagnificationGesture(), m_webPageProxy.pageID()); m_lastMagnificationGestureWasSmartMagnification = false; return; } // We're still waiting for the DidCollectGeometry callback. if (!m_visibleContentRectIsValid) return; m_activeGestureType = ViewGestureType::Magnification; double scaleWithResistance = resistanceForDelta(scale, m_magnification) * scale; m_magnification += m_magnification * scaleWithResistance; m_magnification = std::min(std::max(m_magnification, minElasticMagnification), maxElasticMagnification); m_magnificationOrigin = origin; if (m_frameHandlesMagnificationGesture) m_webPageProxy.scalePage(m_magnification, roundedIntPoint(origin)); else m_webPageProxy.drawingArea()->adjustTransientZoom(m_magnification, scaledMagnificationOrigin(origin, m_magnification)); } void ViewGestureController::endMagnificationGesture() { if (m_activeGestureType != ViewGestureType::Magnification) return; double newMagnification = std::min(std::max(m_magnification, minMagnification), maxMagnification); if (m_frameHandlesMagnificationGesture) m_webPageProxy.scalePage(newMagnification, roundedIntPoint(m_magnificationOrigin)); else { if (auto drawingArea = m_webPageProxy.drawingArea()) drawingArea->commitTransientZoom(newMagnification, scaledMagnificationOrigin(m_magnificationOrigin, newMagnification)); } m_activeGestureType = ViewGestureType::None; m_visibleContentRectIsValid = false; } void ViewGestureController::handleSmartMagnificationGesture(FloatPoint origin) { if (m_activeGestureType != ViewGestureType::None) return; m_webPageProxy.process().send(Messages::ViewGestureGeometryCollector::CollectGeometryForSmartMagnificationGesture(origin), m_webPageProxy.pageID()); } static float maximumRectangleComponentDelta(FloatRect a, FloatRect b) { 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())))); } void ViewGestureController::didCollectGeometryForSmartMagnificationGesture(FloatPoint origin, FloatRect renderRect, FloatRect visibleContentRect, bool isReplacedElement, double viewportMinimumScale, double viewportMaximumScale) { double currentScaleFactor = m_webPageProxy.pageScaleFactor(); FloatRect unscaledTargetRect = renderRect; // If there was no usable element under the cursor, we'll scale towards the cursor instead. if (unscaledTargetRect.isEmpty()) unscaledTargetRect.setLocation(origin); unscaledTargetRect.scale(1 / currentScaleFactor); unscaledTargetRect.inflateX(unscaledTargetRect.width() * smartMagnificationElementPadding); unscaledTargetRect.inflateY(unscaledTargetRect.height() * smartMagnificationElementPadding); double targetMagnification = visibleContentRect.width() / unscaledTargetRect.width(); FloatRect unscaledVisibleContentRect = visibleContentRect; unscaledVisibleContentRect.scale(1 / currentScaleFactor); FloatRect viewportConstrainedUnscaledTargetRect = unscaledTargetRect; viewportConstrainedUnscaledTargetRect.intersect(unscaledVisibleContentRect); if (unscaledTargetRect.width() > viewportConstrainedUnscaledTargetRect.width()) viewportConstrainedUnscaledTargetRect.setX(unscaledVisibleContentRect.x() + (origin.x() / currentScaleFactor) - viewportConstrainedUnscaledTargetRect.width() / 2); if (unscaledTargetRect.height() > viewportConstrainedUnscaledTargetRect.height()) viewportConstrainedUnscaledTargetRect.setY(unscaledVisibleContentRect.y() + (origin.y() / currentScaleFactor) - viewportConstrainedUnscaledTargetRect.height() / 2); // For replaced elements like images, we want to fit the whole element // in the view, so scale it down enough to make both dimensions fit if possible. if (isReplacedElement) targetMagnification = std::min(targetMagnification, static_cast(visibleContentRect.height() / viewportConstrainedUnscaledTargetRect.height())); targetMagnification = std::min(std::max(targetMagnification, minMagnification), maxMagnification); // Allow panning between elements via double-tap while magnified, unless the target rect is // similar to the last one or the mouse has not moved, in which case we'll zoom all the way out. if (currentScaleFactor > 1 && m_lastMagnificationGestureWasSmartMagnification) { if (maximumRectangleComponentDelta(m_lastSmartMagnificationUnscaledTargetRect, unscaledTargetRect) < smartMagnificationPanScrollThreshold) targetMagnification = 1; if (m_lastSmartMagnificationOrigin == origin) targetMagnification = 1; } FloatRect targetRect(viewportConstrainedUnscaledTargetRect); targetRect.scale(targetMagnification); FloatPoint targetOrigin(visibleContentRect.center()); targetOrigin.moveBy(-targetRect.center()); m_webPageProxy.drawingArea()->adjustTransientZoom(m_webPageProxy.pageScaleFactor(), scaledMagnificationOrigin(FloatPoint(), m_webPageProxy.pageScaleFactor())); m_webPageProxy.drawingArea()->commitTransientZoom(targetMagnification, targetOrigin); m_lastSmartMagnificationUnscaledTargetRect = unscaledTargetRect; m_lastSmartMagnificationOrigin = origin; m_lastMagnificationGestureWasSmartMagnification = true; } bool ViewGestureController::scrollEventCanBecomeSwipe(NSEvent *event, ViewGestureController::SwipeDirection& potentialSwipeDirection) { if (event.phase != NSEventPhaseBegan) return false; if (!event.hasPreciseScrollingDeltas) return false; if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) return false; if (fabs(event.scrollingDeltaX) <= fabs(event.scrollingDeltaY)) return false; bool isPinnedToLeft = m_shouldIgnorePinnedState || m_webPageProxy.isPinnedToLeftSide(); bool isPinnedToRight = m_shouldIgnorePinnedState || m_webPageProxy.isPinnedToRightSide(); bool willSwipeLeft = event.scrollingDeltaX > 0 && isPinnedToLeft && m_webPageProxy.backForwardList().backItem(); bool willSwipeRight = event.scrollingDeltaX < 0 && isPinnedToRight && m_webPageProxy.backForwardList().forwardItem(); if (!willSwipeLeft && !willSwipeRight) return false; potentialSwipeDirection = willSwipeLeft ? ViewGestureController::SwipeDirection::Back : ViewGestureController::SwipeDirection::Forward; return true; } bool ViewGestureController::deltaIsSufficientToBeginSwipe(NSEvent *event) { if (m_pendingSwipeReason != PendingSwipeReason::InsufficientMagnitude) return false; m_cumulativeDeltaForPendingSwipe += FloatSize(event.scrollingDeltaX, event.scrollingDeltaY); // If the cumulative delta is ever "too vertical", we will stop tracking this // as a potential swipe until we get another "begin" event. if (fabs(m_cumulativeDeltaForPendingSwipe.height()) >= fabs(m_cumulativeDeltaForPendingSwipe.width()) * minimumScrollEventRatioForSwipe) { m_pendingSwipeReason = PendingSwipeReason::None; return false; } if (fabs(m_cumulativeDeltaForPendingSwipe.width()) < minimumHorizontalSwipeDistance) return false; return true; } void ViewGestureController::setDidMoveSwipeSnapshotCallback(void(^callback)(CGRect)) { if (m_didMoveSwipeSnapshotCallback) Block_release(m_didMoveSwipeSnapshotCallback); m_didMoveSwipeSnapshotCallback = Block_copy(callback); } bool ViewGestureController::handleScrollWheelEvent(NSEvent *event) { if (event.phase == NSEventPhaseEnded) { m_cumulativeDeltaForPendingSwipe = FloatSize(); m_pendingSwipeReason = PendingSwipeReason::None; } if (m_pendingSwipeReason == PendingSwipeReason::InsufficientMagnitude) { if (deltaIsSufficientToBeginSwipe(event)) { trackSwipeGesture(event, m_pendingSwipeDirection); return true; } } if (m_activeGestureType != ViewGestureType::None) return false; SwipeDirection direction; if (!scrollEventCanBecomeSwipe(event, direction)) return false; if (!m_shouldIgnorePinnedState && m_webPageProxy.willHandleHorizontalScrollEvents()) { m_pendingSwipeReason = PendingSwipeReason::WebCoreMayScroll; m_pendingSwipeDirection = direction; return false; } if (!deltaIsSufficientToBeginSwipe(event)) { m_pendingSwipeReason = PendingSwipeReason::InsufficientMagnitude; m_pendingSwipeDirection = direction; return true; } trackSwipeGesture(event, direction); return true; } void ViewGestureController::wheelEventWasNotHandledByWebCore(NSEvent *event) { if (m_pendingSwipeReason != PendingSwipeReason::WebCoreMayScroll) return; m_pendingSwipeReason = PendingSwipeReason::None; SwipeDirection direction; if (!scrollEventCanBecomeSwipe(event, direction)) return; if (!deltaIsSufficientToBeginSwipe(event)) { m_pendingSwipeReason = PendingSwipeReason::InsufficientMagnitude; return; } trackSwipeGesture(event, m_pendingSwipeDirection); } void ViewGestureController::trackSwipeGesture(NSEvent *event, SwipeDirection direction) { ASSERT(m_activeGestureType == ViewGestureType::None); m_pendingSwipeReason = PendingSwipeReason::None; m_webPageProxy.recordNavigationSnapshot(); CGFloat maxProgress = (direction == SwipeDirection::Back) ? 1 : 0; CGFloat minProgress = (direction == SwipeDirection::Forward) ? -1 : 0; RefPtr targetItem = (direction == SwipeDirection::Back) ? m_webPageProxy.backForwardList().backItem() : m_webPageProxy.backForwardList().forwardItem(); if (!targetItem) return; __block bool swipeCancelled = false; ASSERT(!m_swipeCancellationTracker); RetainPtr swipeCancellationTracker = adoptNS([[WKSwipeCancellationTracker alloc] init]); m_swipeCancellationTracker = swipeCancellationTracker; [event trackSwipeEventWithOptions:NSEventSwipeTrackingConsumeMouseEvents dampenAmountThresholdMin:minProgress max:maxProgress usingHandler:^(CGFloat progress, NSEventPhase phase, BOOL isComplete, BOOL *stop) { if ([swipeCancellationTracker isCancelled]) { *stop = YES; return; } if (phase == NSEventPhaseBegan) this->beginSwipeGesture(targetItem.get(), direction); CGFloat clampedProgress = std::min(std::max(progress, minProgress), maxProgress); this->handleSwipeGesture(targetItem.get(), clampedProgress, direction); if (phase == NSEventPhaseCancelled) swipeCancelled = true; if (phase == NSEventPhaseEnded || phase == NSEventPhaseCancelled) this->willEndSwipeGesture(*targetItem, swipeCancelled); if (isComplete) this->endSwipeGesture(targetItem.get(), swipeCancelled); }]; } void ViewGestureController::willEndSwipeGesture(WebBackForwardListItem& targetItem, bool cancelled) { m_webPageProxy.navigationGestureWillEnd(!cancelled, targetItem); } FloatRect ViewGestureController::windowRelativeBoundsForCustomSwipeViews() const { FloatRect swipeArea; for (const auto& view : m_customSwipeViews) swipeArea.unite([view convertRect:[view bounds] toView:nil]); swipeArea.setHeight(swipeArea.height() - m_customSwipeViewsTopContentInset); return swipeArea; } static CALayer *leastCommonAncestorLayer(const Vector>& layers) { Vector> liveLayerPathsFromRoot(layers.size()); size_t shortestPathLength = std::numeric_limits::max(); for (size_t layerIndex = 0; layerIndex < layers.size(); layerIndex++) { CALayer *parent = [layers[layerIndex] superlayer]; while (parent) { liveLayerPathsFromRoot[layerIndex].insert(0, parent); shortestPathLength = std::min(shortestPathLength, liveLayerPathsFromRoot[layerIndex].size()); parent = parent.superlayer; } } for (size_t pathIndex = 0; pathIndex < shortestPathLength; pathIndex++) { CALayer *firstPathLayer = liveLayerPathsFromRoot[0][pathIndex]; for (size_t layerIndex = 1; layerIndex < layers.size(); layerIndex++) { if (liveLayerPathsFromRoot[layerIndex][pathIndex] != firstPathLayer) return firstPathLayer; } } return liveLayerPathsFromRoot[0][shortestPathLength]; } CALayer *ViewGestureController::determineSnapshotLayerParent() const { if (m_currentSwipeLiveLayers.size() == 1) return [m_currentSwipeLiveLayers[0] superlayer]; // We insert our snapshot into the first shared superlayer of the custom views' layer, above the frontmost or below the bottommost layer. return leastCommonAncestorLayer(m_currentSwipeLiveLayers); } CALayer *ViewGestureController::determineLayerAdjacentToSnapshotForParent(SwipeDirection direction, CALayer *snapshotLayerParent) const { // If we have custom swiping views, we assume that the views were passed to us in back-to-front z-order. CALayer *layerAdjacentToSnapshot = direction == SwipeDirection::Back ? m_currentSwipeLiveLayers.first().get() : m_currentSwipeLiveLayers.last().get(); if (m_currentSwipeLiveLayers.size() == 1) return layerAdjacentToSnapshot; // 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. while (snapshotLayerParent != layerAdjacentToSnapshot.superlayer) layerAdjacentToSnapshot = layerAdjacentToSnapshot.superlayer; return layerAdjacentToSnapshot; } bool ViewGestureController::shouldUseSnapshotForSize(ViewSnapshot& snapshot, FloatSize swipeLayerSize, float topContentInset) { float deviceScaleFactor = m_webPageProxy.deviceScaleFactor(); if (snapshot.deviceScaleFactor() != deviceScaleFactor) return false; FloatSize unobscuredSwipeLayerSizeInDeviceCoordinates = swipeLayerSize - FloatSize(0, topContentInset); unobscuredSwipeLayerSizeInDeviceCoordinates.scale(deviceScaleFactor); if (snapshot.size() != unobscuredSwipeLayerSizeInDeviceCoordinates) return false; return true; } static bool layerGeometryFlippedToRoot(CALayer *layer) { bool flipped = false; CALayer *parent = layer; while (parent) { if (parent.isGeometryFlipped) flipped = !flipped; parent = parent.superlayer; } return flipped; } void ViewGestureController::applyDebuggingPropertiesToSwipeViews() { CAFilter* filter = [CAFilter filterWithType:kCAFilterColorInvert]; [m_swipeLayer setFilters:@[ filter ]]; [m_swipeLayer setBackgroundColor:[NSColor blueColor].CGColor]; [m_swipeLayer setBorderColor:[NSColor yellowColor].CGColor]; [m_swipeLayer setBorderWidth:4]; [m_swipeSnapshotLayer setBackgroundColor:[NSColor greenColor].CGColor]; [m_swipeSnapshotLayer setBorderColor:[NSColor redColor].CGColor]; [m_swipeSnapshotLayer setBorderWidth:2]; } void ViewGestureController::beginSwipeGesture(WebBackForwardListItem* targetItem, SwipeDirection direction) { ASSERT(m_currentSwipeLiveLayers.isEmpty()); m_webPageProxy.navigationGestureDidBegin(); m_activeGestureType = ViewGestureType::Swipe; m_swipeInProgress = true; CALayer *rootContentLayer = m_webPageProxy.acceleratedCompositingRootLayer(); m_swipeLayer = adoptNS([[CALayer alloc] init]); m_swipeSnapshotLayer = adoptNS([[CALayer alloc] init]); m_currentSwipeCustomViewBounds = windowRelativeBoundsForCustomSwipeViews(); FloatRect swipeArea; float topContentInset = 0; if (!m_customSwipeViews.isEmpty()) { topContentInset = m_customSwipeViewsTopContentInset; swipeArea = m_currentSwipeCustomViewBounds; swipeArea.expand(0, topContentInset); for (const auto& view : m_customSwipeViews) { CALayer *layer = [view layer]; ASSERT(layer); m_currentSwipeLiveLayers.append(layer); } } else { swipeArea = [rootContentLayer convertRect:CGRectMake(0, 0, m_webPageProxy.viewSize().width(), m_webPageProxy.viewSize().height()) toLayer:nil]; topContentInset = m_webPageProxy.topContentInset(); m_currentSwipeLiveLayers.append(rootContentLayer); } CALayer *snapshotLayerParent = determineSnapshotLayerParent(); bool geometryIsFlippedToRoot = layerGeometryFlippedToRoot(snapshotLayerParent); RetainPtr backgroundColor = CGColorGetConstantColor(kCGColorWhite); if (ViewSnapshot* snapshot = targetItem->snapshot()) { if (shouldUseSnapshotForSize(*snapshot, swipeArea.size(), topContentInset)) [m_swipeSnapshotLayer setContents:snapshot->asLayerContents()]; Color coreColor = snapshot->backgroundColor(); if (coreColor.isValid()) backgroundColor = cachedCGColor(coreColor, ColorSpaceDeviceRGB); #if USE_IOSURFACE_VIEW_SNAPSHOTS m_currentSwipeSnapshot = snapshot; #endif } [m_swipeLayer setBackgroundColor:backgroundColor.get()]; [m_swipeLayer setAnchorPoint:CGPointZero]; [m_swipeLayer setFrame:[snapshotLayerParent convertRect:swipeArea fromLayer:nil]]; [m_swipeLayer setName:@"Gesture Swipe Root Layer"]; [m_swipeLayer setGeometryFlipped:geometryIsFlippedToRoot]; [m_swipeLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; float deviceScaleFactor = m_webPageProxy.deviceScaleFactor(); [m_swipeSnapshotLayer setContentsGravity:kCAGravityTopLeft]; [m_swipeSnapshotLayer setContentsScale:deviceScaleFactor]; [m_swipeSnapshotLayer setAnchorPoint:CGPointZero]; [m_swipeSnapshotLayer setFrame:CGRectMake(0, 0, swipeArea.width(), swipeArea.height() - topContentInset)]; [m_swipeSnapshotLayer setName:@"Gesture Swipe Snapshot Layer"]; [m_swipeSnapshotLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; [m_swipeLayer addSublayer:m_swipeSnapshotLayer.get()]; if (m_webPageProxy.preferences().viewGestureDebuggingEnabled()) applyDebuggingPropertiesToSwipeViews(); CALayer *layerAdjacentToSnapshot = determineLayerAdjacentToSnapshotForParent(direction, snapshotLayerParent); if (direction == SwipeDirection::Back) [snapshotLayerParent insertSublayer:m_swipeLayer.get() below:layerAdjacentToSnapshot]; else [snapshotLayerParent insertSublayer:m_swipeLayer.get() above:layerAdjacentToSnapshot]; // We don't know enough about the custom views' hierarchy to apply a shadow. if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap && m_customSwipeViews.isEmpty()) { #if ENABLE(LEGACY_SWIPE_SHADOW_STYLE) if (direction == SwipeDirection::Back) { float topContentInset = m_webPageProxy.topContentInset(); FloatRect shadowRect(FloatPoint(0, topContentInset), m_webPageProxy.viewSize() - FloatSize(0, topContentInset)); RetainPtr shadowPath = adoptCF(CGPathCreateWithRect(shadowRect, 0)); [rootContentLayer setShadowColor:CGColorGetConstantColor(kCGColorBlack)]; [rootContentLayer setShadowOpacity:swipeOverlayShadowOpacity]; [rootContentLayer setShadowRadius:swipeOverlayShadowRadius]; [rootContentLayer setShadowPath:shadowPath.get()]; } else { RetainPtr shadowPath = adoptCF(CGPathCreateWithRect([m_swipeLayer bounds], 0)); [m_swipeLayer setShadowColor:CGColorGetConstantColor(kCGColorBlack)]; [m_swipeLayer setShadowOpacity:swipeOverlayShadowOpacity]; [m_swipeLayer setShadowRadius:swipeOverlayShadowRadius]; [m_swipeLayer setShadowPath:shadowPath.get()]; } #else FloatRect dimmingRect(FloatPoint(), m_webPageProxy.viewSize()); m_swipeDimmingLayer = adoptNS([[CALayer alloc] init]); [m_swipeDimmingLayer setName:@"Gesture Swipe Dimming Layer"]; [m_swipeDimmingLayer setBackgroundColor:[NSColor blackColor].CGColor]; [m_swipeDimmingLayer setOpacity:swipeOverlayDimmingOpacity]; [m_swipeDimmingLayer setAnchorPoint:CGPointZero]; [m_swipeDimmingLayer setFrame:dimmingRect]; [m_swipeDimmingLayer setGeometryFlipped:geometryIsFlippedToRoot]; [m_swipeDimmingLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; FloatRect shadowRect(-swipeOverlayShadowWidth, topContentInset, swipeOverlayShadowWidth, m_webPageProxy.viewSize().height() - topContentInset); m_swipeShadowLayer = adoptNS([[CAGradientLayer alloc] init]); [m_swipeShadowLayer setName:@"Gesture Swipe Shadow Layer"]; [m_swipeShadowLayer setColors:@[ (id)adoptCF(CGColorCreateGenericGray(0, 1.)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.99)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.98)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.95)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.92)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.82)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.71)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.46)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.35)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.25)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.17)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.11)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.07)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.04)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.01)).get(), (id)adoptCF(CGColorCreateGenericGray(0, 0.)).get(), ]]; [m_swipeShadowLayer setLocations:@[ @0, @0.03125, @0.0625, @0.0938, @0.125, @0.1875, @0.25, @0.375, @0.4375, @0.5, @0.5625, @0.625, @0.6875, @0.75, @0.875, @1, ]]; [m_swipeShadowLayer setStartPoint:CGPointMake(1, 0)]; [m_swipeShadowLayer setEndPoint:CGPointMake(0, 0)]; [m_swipeShadowLayer setOpacity:swipeOverlayShadowOpacity]; [m_swipeShadowLayer setAnchorPoint:CGPointZero]; [m_swipeShadowLayer setFrame:shadowRect]; [m_swipeShadowLayer setGeometryFlipped:geometryIsFlippedToRoot]; [m_swipeShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; if (direction == SwipeDirection::Back) [snapshotLayerParent insertSublayer:m_swipeDimmingLayer.get() above:m_swipeLayer.get()]; else [snapshotLayerParent insertSublayer:m_swipeDimmingLayer.get() below:m_swipeLayer.get()]; [snapshotLayerParent insertSublayer:m_swipeShadowLayer.get() above:m_swipeLayer.get()]; #endif } } void ViewGestureController::handleSwipeGesture(WebBackForwardListItem* targetItem, double progress, SwipeDirection direction) { ASSERT(m_activeGestureType == ViewGestureType::Swipe); if (!m_webPageProxy.drawingArea()) return; double width; if (!m_customSwipeViews.isEmpty()) width = m_currentSwipeCustomViewBounds.width(); else width = m_webPageProxy.drawingArea()->size().width(); double swipingLayerOffset = floor(width * progress); #if !ENABLE(LEGACY_SWIPE_SHADOW_STYLE) double dimmingProgress = (direction == SwipeDirection::Back) ? 1 - progress : -progress; dimmingProgress = std::min(1., std::max(dimmingProgress, 0.)); [m_swipeDimmingLayer setOpacity:dimmingProgress * swipeOverlayDimmingOpacity]; double absoluteProgress = fabs(progress); double remainingSwipeDistance = width - fabs(absoluteProgress * width); double shadowFadeDistance = [m_swipeShadowLayer bounds].size.width; if (remainingSwipeDistance < shadowFadeDistance) [m_swipeShadowLayer setOpacity:(remainingSwipeDistance / shadowFadeDistance) * swipeOverlayShadowOpacity]; else [m_swipeShadowLayer setOpacity:swipeOverlayShadowOpacity]; if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap) [m_swipeShadowLayer setTransform:CATransform3DMakeTranslation((direction == SwipeDirection::Back ? 0 : width) + swipingLayerOffset, 0, 0)]; #endif if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap) { if (direction == SwipeDirection::Forward) { [m_swipeLayer setTransform:CATransform3DMakeTranslation(width + swipingLayerOffset, 0, 0)]; didMoveSwipeSnapshotLayer(); } } else if (m_swipeTransitionStyle == SwipeTransitionStyle::Push) [m_swipeLayer setTransform:CATransform3DMakeTranslation((direction == SwipeDirection::Back ? -width : width) + swipingLayerOffset, 0, 0)]; for (const auto& layer : m_currentSwipeLiveLayers) { if (m_swipeTransitionStyle == SwipeTransitionStyle::Overlap) { if (direction == SwipeDirection::Back) [layer setTransform:CATransform3DMakeTranslation(swipingLayerOffset, 0, 0)]; } else if (m_swipeTransitionStyle == SwipeTransitionStyle::Push) [layer setTransform:CATransform3DMakeTranslation(swipingLayerOffset, 0, 0)]; } } void ViewGestureController::didMoveSwipeSnapshotLayer() { if (!m_didMoveSwipeSnapshotCallback) return; m_didMoveSwipeSnapshotCallback(m_webPageProxy.boundsOfLayerInLayerBackedWindowCoordinates(m_swipeLayer.get())); } void ViewGestureController::endSwipeGesture(WebBackForwardListItem* targetItem, bool cancelled) { ASSERT(m_activeGestureType == ViewGestureType::Swipe); m_swipeCancellationTracker = nullptr; m_swipeInProgress = false; CALayer *rootLayer = m_webPageProxy.acceleratedCompositingRootLayer(); [rootLayer setShadowOpacity:0]; [rootLayer setShadowRadius:0]; if (cancelled) { removeSwipeSnapshot(); m_webPageProxy.navigationGestureDidEnd(false, *targetItem); return; } uint64_t renderTreeSize = 0; if (ViewSnapshot* snapshot = targetItem->snapshot()) renderTreeSize = snapshot->renderTreeSize(); m_webPageProxy.process().send(Messages::ViewGestureGeometryCollector::SetRenderTreeSizeNotificationThreshold(renderTreeSize * swipeSnapshotRemovalRenderTreeSizeTargetFraction), m_webPageProxy.pageID()); m_swipeWaitingForVisuallyNonEmptyLayout = true; m_swipeWaitingForRenderTreeSizeThreshold = true; m_webPageProxy.navigationGestureDidEnd(true, *targetItem); m_webPageProxy.goToBackForwardItem(targetItem); // FIXME: Like on iOS, we should ensure that even if one of the timeouts fires, // we never show the old page content, instead showing white (or the snapshot background color). m_swipeWatchdogTimer.startOneShot(swipeSnapshotRemovalWatchdogDuration.count()); if (ViewSnapshot* snapshot = targetItem->snapshot()) m_backgroundColorForCurrentSnapshot = snapshot->backgroundColor(); } void ViewGestureController::didHitRenderTreeSizeThreshold() { if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress) return; m_swipeWaitingForRenderTreeSizeThreshold = false; if (!m_swipeWaitingForVisuallyNonEmptyLayout) { // FIXME: Ideally we would call removeSwipeSnapshotAfterRepaint() here, but sometimes // scroll position isn't done restoring until didFinishLoadForFrame, so we flash the wrong content. } } void ViewGestureController::didFirstVisuallyNonEmptyLayoutForMainFrame() { if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress) return; m_swipeWaitingForVisuallyNonEmptyLayout = false; if (!m_swipeWaitingForRenderTreeSizeThreshold) { // FIXME: Ideally we would call removeSwipeSnapshotAfterRepaint() here, but sometimes // scroll position isn't done restoring until didFinishLoadForFrame, so we flash the wrong content. } else { m_swipeWatchdogAfterFirstVisuallyNonEmptyLayoutTimer.startOneShot(swipeSnapshotRemovalWatchdogAfterFirstVisuallyNonEmptyLayoutDuration.count()); m_swipeWatchdogTimer.stop(); } } void ViewGestureController::mainFrameLoadDidReachTerminalState() { if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress) return; if (m_webPageProxy.pageLoadState().isLoading()) { m_swipeActiveLoadMonitoringTimer.startRepeating(swipeSnapshotRemovalActiveLoadMonitoringInterval); return; } removeSwipeSnapshotAfterRepaint(); } void ViewGestureController::didSameDocumentNavigationForMainFrame(SameDocumentNavigationType type) { if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress) return; if (type != SameDocumentNavigationSessionStateReplace && type != SameDocumentNavigationSessionStatePop) return; m_swipeActiveLoadMonitoringTimer.startRepeating(swipeSnapshotRemovalActiveLoadMonitoringInterval); } void ViewGestureController::activeLoadMonitoringTimerFired() { if (m_webPageProxy.pageLoadState().isLoading()) return; removeSwipeSnapshotAfterRepaint(); } void ViewGestureController::swipeSnapshotWatchdogTimerFired() { removeSwipeSnapshotAfterRepaint(); } void ViewGestureController::removeSwipeSnapshotAfterRepaint() { m_swipeActiveLoadMonitoringTimer.stop(); if (m_activeGestureType != ViewGestureType::Swipe || m_swipeInProgress) return; if (m_swipeWaitingForRepaint) return; m_swipeWaitingForRepaint = true; WebPageProxy* webPageProxy = &m_webPageProxy; m_webPageProxy.forceRepaint(VoidCallback::create([webPageProxy] (CallbackBase::Error error) { webPageProxy->removeNavigationGestureSnapshot(); })); } void ViewGestureController::removeSwipeSnapshot() { m_swipeWaitingForRepaint = false; m_swipeWatchdogTimer.stop(); m_swipeWatchdogAfterFirstVisuallyNonEmptyLayoutTimer.stop(); if (m_activeGestureType != ViewGestureType::Swipe) return; #if USE_IOSURFACE_VIEW_SNAPSHOTS if (m_currentSwipeSnapshot && m_currentSwipeSnapshot->surface()) m_currentSwipeSnapshot->surface()->setIsVolatile(true); m_currentSwipeSnapshot = nullptr; #endif for (const auto& layer : m_currentSwipeLiveLayers) [layer setTransform:CATransform3DIdentity]; [m_swipeSnapshotLayer removeFromSuperlayer]; m_swipeSnapshotLayer = nullptr; [m_swipeLayer removeFromSuperlayer]; m_swipeLayer = nullptr; #if !ENABLE(LEGACY_SWIPE_SHADOW_STYLE) [m_swipeShadowLayer removeFromSuperlayer]; m_swipeShadowLayer = nullptr; [m_swipeDimmingLayer removeFromSuperlayer]; m_swipeDimmingLayer = nullptr; #endif m_currentSwipeLiveLayers.clear(); m_activeGestureType = ViewGestureType::None; m_webPageProxy.navigationGestureSnapshotWasRemoved(); m_backgroundColorForCurrentSnapshot = Color(); } double ViewGestureController::magnification() const { if (m_activeGestureType == ViewGestureType::Magnification) return m_magnification; return m_webPageProxy.pageScaleFactor(); } } // namespace WebKit #endif // !PLATFORM(IOS)