chromium/ios/chrome/browser/side_swipe/ui_bundled/side_swipe_mediator.mm

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_mediator.h"

#import <memory>

#import "base/feature_list.h"
#import "base/ios/block_types.h"
#import "base/memory/raw_ptr.h"
#import "base/scoped_observation.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/card_side_swipe_view.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_gesture_recognizer.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_mediator+Testing.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_navigation_view.h"
#import "ios/chrome/browser/side_swipe/ui_bundled/side_swipe_util.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/tabs/ui_bundled/requirements/tab_strip_highlighting.h"
#import "ios/chrome/browser/ui/fullscreen/animated_scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
#import "ios/chrome/browser/ui/fullscreen/scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/ui/toolbar/public/side_swipe_toolbar_interacting.h"
#import "ios/chrome/browser/web/model/page_placeholder_tab_helper.h"
#import "ios/chrome/browser/web/model/web_navigation_util.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ui/base/device_form_factor.h"

NSString* const kSideSwipeWillStartNotification =
    @"kSideSwipeWillStartNotification";
NSString* const kSideSwipeDidStopNotification =
    @"kSideSwipeDidStopNotification";

namespace {

// Swipe starting distance from edge.
const CGFloat kSwipeEdge = 20;

// The distance between touches for a swipe between tabs to begin.
const CGFloat kPanGestureRecognizerThreshold = 25;

// Distance between sections of iPad side swipe.
const CGFloat kIpadTabSwipeDistance = 100;

}  // namespace

@interface SideSwipeMediator () <CRWWebStateObserver,
                                 UIGestureRecognizerDelegate,
                                 WebStateListObserving> {
 @private

  // Side swipe view for tab navigation.
  CardSideSwipeView* _tabSideSwipeView;

  // Side swipe view for page navigation.
  SideSwipeNavigationView* _pageSideSwipeView;

  // YES if the user is currently swiping.
  BOOL _inSwipe;

  // Swipe gesture recognizer.
  SideSwipeGestureRecognizer* _swipeGestureRecognizer;

  SideSwipeGestureRecognizer* _panGestureRecognizer;

  // Used in iPad side swipe gesture, tracks the starting tab index.
  unsigned int _startingTabIndex;

  // If the swipe is for a page change or a tab change.
  SwipeType _swipeType;

  // Bridge to observe the web state from Objective-C.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;

  // Bridge to observe the WebStateList from Objective-C.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;

  // Scoped observer used to track registration of the WebStateObserverBridge.
  std::unique_ptr<base::ScopedObservation<web::WebState, web::WebStateObserver>>
      _scopedWebStateObservation;

  // Curtain over web view while waiting for it to load.
  UIView* _curtain;

  // The disabler that prevents the toolbar from being scrolled away when the
  // side swipe gesture is being recognized.
  std::unique_ptr<ScopedFullscreenDisabler> _fullscreenDisabler;

  // The animated disabler displays the toolbar when a side swipe navigation
  // gesture is being recognized.
  std::unique_ptr<AnimatedScopedFullscreenDisabler> _animatedFullscreenDisabler;
}

// The current active WebState.
@property(nonatomic, readonly) web::WebState* activeWebState;

// The webStateList owned by the current browser.
@property(nonatomic, readonly) WebStateList* webStateList;

// Whether to allow navigating from the leading edge.
@property(nonatomic, assign) BOOL leadingEdgeNavigationEnabled;

// Whether to allow navigating from the trailing edge.
@property(nonatomic, assign) BOOL trailingEdgeNavigationEnabled;

// Handle tab side swipe for iPad.  Change tabs according to swipe distance.
- (void)handleiPadTabSwipe:(SideSwipeGestureRecognizer*)gesture;
// Handle tab side swipe for iPhone. Introduces a CardSideSwipeView to convey
// the tab change.
- (void)handleiPhoneTabSwipe:(SideSwipeGestureRecognizer*)gesture;
// Overlays `curtain_` as a white view to hide the web view while it updates.
// Calls `completionHandler` when the curtain is removed.
- (void)addCurtainWithCompletionHandler:(ProceduralBlock)completionHandler;
// Removes the `curtain_` and calls `completionHandler` when the curtain is
// removed.
- (void)dismissCurtainWithCompletionHandler:(ProceduralBlock)completionHandler;
// Removes the `curtain_` if there was an active swipe, and resets
// `inSwipe_` value.
- (void)dismissCurtain;
@end

@implementation SideSwipeMediator

@synthesize inSwipe = _inSwipe;
@synthesize swipeDelegate = _swipeDelegate;
@synthesize toolbarInteractionHandler = _toolbarInteractionHandler;
@synthesize tabStripDelegate = _tabStripDelegate;

- (instancetype)
    initWithFullscreenController:(FullscreenController*)fullscreenController
                    webStateList:(WebStateList*)webStateList {
  self = [super init];
  if (self) {
    _webStateList = webStateList;
    _webStateListObserver = std::make_unique<WebStateListObserverBridge>(self);
    _webStateList->AddObserver(_webStateListObserver.get());
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _scopedWebStateObservation = std::make_unique<
        base::ScopedObservation<web::WebState, web::WebStateObserver>>(
        _webStateObserverBridge.get());
    _fullscreenController = fullscreenController;
    if (self.activeWebState) {
      _scopedWebStateObservation->Observe(self.activeWebState);
    }
  }
  return self;
}

- (void)dealloc {
  // TODO(crbug.com/40276402);
  DUMP_WILL_BE_CHECK(!_fullscreenController);
}

- (void)disconnect {
  if (self.webStateList) {
    self.webStateList->RemoveObserver(_webStateListObserver.get());
  }
  _scopedWebStateObservation.reset();
  _webStateObserverBridge.reset();
  _fullscreenController = nullptr;
}

- (void)addHorizontalGesturesToView:(UIView*)view {
  _swipeGestureRecognizer = [[SideSwipeGestureRecognizer alloc]
      initWithTarget:self
              action:@selector(handleSwipe:)];
  [_swipeGestureRecognizer setMaximumNumberOfTouches:1];
  [_swipeGestureRecognizer setDelegate:self];
  [_swipeGestureRecognizer setSwipeEdge:kSwipeEdge];
  [view addGestureRecognizer:_swipeGestureRecognizer];

  // Add a second gesture recognizer to handle swiping on the toolbar to change
  // tabs.
  _panGestureRecognizer =
      [[SideSwipeGestureRecognizer alloc] initWithTarget:self
                                                  action:@selector(handlePan:)];
  [_panGestureRecognizer setMaximumNumberOfTouches:1];
  [_panGestureRecognizer setSwipeThreshold:kPanGestureRecognizerThreshold];
  [_panGestureRecognizer setDelegate:self];
  [view addGestureRecognizer:_panGestureRecognizer];
}

- (void)animateSwipe:(SwipeType)swipeType
         inDirection:(UISwipeGestureRecognizerDirection)direction {
  if (_inSwipe || [_swipeDelegate preventSideSwipe]) {
    return;
  }
  switch (swipeType) {
    case SwipeType::NONE:
    case SwipeType::CHANGE_TAB:
      NOTREACHED_IN_MIGRATION();
      break;
    case SwipeType::CHANGE_PAGE:
      [self animatePageNavigationInDirection:direction];
      break;
  }
}

- (web::WebState*)activeWebState {
  return self.webStateList ? self.webStateList->GetActiveWebState() : nullptr;
}

- (void)setEnabled:(BOOL)enabled {
  [_swipeGestureRecognizer setEnabled:enabled];
}

- (BOOL)shouldAutorotate {
  return !([_tabSideSwipeView window] || _inSwipe);
}

- (void)handlePan:(SideSwipeGestureRecognizer*)gesture {
  // Do not trigger a CheckForOverRealization here, as it's expected
  // that many WebStates may realize from multiple swipes.
  web::IgnoreOverRealizationCheck();
  if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
    return [self handleiPhoneTabSwipe:gesture];
  } else {
    return [self handleiPadTabSwipe:gesture];
  }
}

- (void)handleSwipe:(SideSwipeGestureRecognizer*)gesture {
  DCHECK(_swipeType != SwipeType::NONE);
  if (_swipeType == SwipeType::CHANGE_TAB) {
    if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
      return [self handleiPhoneTabSwipe:gesture];
    } else {
      return [self handleiPadTabSwipe:gesture];
    }
  }
  if (_swipeType == SwipeType::CHANGE_PAGE) {
    return [self handleSwipeToNavigate:gesture];
  }
  NOTREACHED_IN_MIGRATION();
}

- (void)handleiPadTabSwipe:(SideSwipeGestureRecognizer*)gesture {
  // Don't handle swipe when there are no tabs.
  int count = self.webStateList->count();
  if (count == 0) {
    return;
  }

  if (gesture.state == UIGestureRecognizerStateBegan) {
    // Disable fullscreen while the side swipe gesture is occurring.
    _fullscreenDisabler =
        std::make_unique<ScopedFullscreenDisabler>(self.fullscreenController);
    SnapshotTabHelper::FromWebState(self.activeWebState)
        ->UpdateSnapshotWithCallback(nil);
    [[NSNotificationCenter defaultCenter]
        postNotificationName:kSideSwipeWillStartNotification
                      object:nil];
    [self.tabStripDelegate setHighlightsSelectedTab:YES];
    _startingTabIndex = self.webStateList->active_index();
  } else if (gesture.state == UIGestureRecognizerStateChanged) {
    // Side swipe for iPad involves changing the selected tab as the swipe moves
    // across the width of the view.  The screen is broken up into
    // `kIpadTabSwipeDistance` / `width` segments, with the current tab in the
    // first section.  The swipe does not wrap edges.
    CGFloat distance = [gesture locationInView:gesture.view].x;
    if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
      distance = gesture.startPoint.x - distance;
    } else {
      distance -= gesture.startPoint.x;
    }

    int indexDelta = std::floor(distance / kIpadTabSwipeDistance);
    // Don't wrap past the first tab.
    if (indexDelta < count) {
      // Flip delta when swiping forward.
      if (IsSwipingForward(gesture.direction)) {
        indexDelta = 0 - indexDelta;
      }

      web::WebState* currentWebState = self.activeWebState;
      int currentIndex = self.webStateList->GetIndexOfWebState(currentWebState);
      DCHECK_GE(currentIndex, 0);
      // Wrap around edges.
      int newIndex = (int)(_startingTabIndex + indexDelta) % count;

      // C99 defines the modulo result as negative if our offset is negative.
      if (newIndex < 0) {
        newIndex += count;
      }

      if (newIndex != currentIndex) {
        [self willActivateWebStateAtIndex:newIndex];
        web::WebState* webState = self.webStateList->GetWebStateAt(newIndex);
        // Toggle overlay preview mode for selected tab.
        PagePlaceholderTabHelper::FromWebState(webState)
            ->AddPlaceholderForNextNavigation();
        self.webStateList->ActivateWebStateAt(newIndex);

        // And disable overlay preview mode for last selected tab.
        PagePlaceholderTabHelper::FromWebState(currentWebState)
            ->CancelPlaceholderForNextNavigation();
      }
    }
  } else {
    if (gesture.state == UIGestureRecognizerStateCancelled) {
      web::WebState* webState =
          self.webStateList->GetWebStateAt(_startingTabIndex);
      PagePlaceholderTabHelper::FromWebState(webState)
          ->CancelPlaceholderForNextNavigation();
      self.webStateList->ActivateWebStateAt(_startingTabIndex);
    }
    PagePlaceholderTabHelper::FromWebState(self.activeWebState)
        ->CancelPlaceholderForNextNavigation();

    // Redisplay the view if it was in overlay preview mode.
    [_swipeDelegate sideSwipeRedisplayTabView];
    [self.tabStripDelegate setHighlightsSelectedTab:NO];
    [[NSNotificationCenter defaultCenter]
        postNotificationName:kSideSwipeDidStopNotification
                      object:nil];

    // Stop disabling fullscreen.
    _fullscreenDisabler = nullptr;
  }
}

// Invoked when the active tab is about to be changed.
- (void)willActivateWebStateAtIndex:(int)index {
  if (!self.activeWebState || index == WebStateList::kInvalidIndex) {
    return;
  }
  int currentIndex = self.webStateList->GetIndexOfWebState(self.activeWebState);
  if (currentIndex != index && currentIndex != WebStateList::kInvalidIndex) {
    _engagementTracker->NotifyEvent(
        feature_engagement::events::kIOSSwipeToolbarToChangeTabUsed);
    [self.helpHandler handleToolbarSwipeGesture];
  }
}

- (BOOL)canNavigate:(BOOL)goBack {
  if (!self.activeWebState) {
    return NO;
  }
  if (goBack && self.activeWebState->GetNavigationManager()->CanGoBack()) {
    return YES;
  }
  if (!goBack && self.activeWebState->GetNavigationManager()->CanGoForward()) {
    return YES;
  }
  return NO;
}

// Show swipe to navigate.
- (void)handleSwipeToNavigate:(SideSwipeGestureRecognizer*)gesture {
  if (gesture.state == UIGestureRecognizerStateBegan) {
    // Make sure the Toolbar is visible by disabling Fullscreen.
    _animatedFullscreenDisabler =
        std::make_unique<AnimatedScopedFullscreenDisabler>(
            self.fullscreenController);
    _animatedFullscreenDisabler->StartAnimation();

    _inSwipe = YES;
    [_swipeDelegate updateAccessoryViewsForSideSwipeWithVisibility:NO];
    BOOL goBack = IsSwipingBack(gesture.direction);

    CGRect gestureBounds = gesture.view.bounds;
    CGFloat headerHeight = [_swipeDelegate headerHeightForSideSwipe];
    CGRect navigationFrame =
        CGRectMake(CGRectGetMinX(gestureBounds),
                   CGRectGetMinY(gestureBounds) + headerHeight,
                   CGRectGetWidth(gestureBounds),
                   CGRectGetHeight(gestureBounds) - headerHeight);

    _pageSideSwipeView = [[SideSwipeNavigationView alloc]
        initWithFrame:navigationFrame
        withDirection:gesture.direction
          canNavigate:[self canNavigate:goBack]
                image:[UIImage imageNamed:@"side_swipe_navigation_back"]];
    [_pageSideSwipeView setTargetView:[_swipeDelegate sideSwipeContentView]];

    [gesture.view insertSubview:_pageSideSwipeView
                   belowSubview:[_swipeDelegate topToolbarView]];
  } else if (gesture.state == UIGestureRecognizerStateCancelled ||
             gesture.state == UIGestureRecognizerStateEnded ||
             gesture.state == UIGestureRecognizerStateFailed) {
    // Enable fullscreen functionality after the Toolbar has been shown, and
    // the gesture is over.
    _animatedFullscreenDisabler = nullptr;
  }

  __weak SideSwipeMediator* weakSelf = self;
  [_pageSideSwipeView handleHorizontalPan:gesture
      onOverThresholdCompletion:^{
        [weakSelf handleOverThresholdCompletion:gesture.direction];
      }
      onUnderThresholdCompletion:^{
        [weakSelf handleUnderThresholdCompletion];
      }];
}

// Animate page navigation.
- (void)animatePageNavigationInDirection:
    (UISwipeGestureRecognizerDirection)direction {
  _inSwipe = YES;
  [_swipeDelegate updateAccessoryViewsForSideSwipeWithVisibility:NO];

  UIView* navigatingView = [_swipeDelegate sideSwipeContentView].superview;
  CGRect navigatingBounds = navigatingView.bounds;
  CGFloat headerHeight = [_swipeDelegate headerHeightForSideSwipe];
  CGRect navigationFrame =
      CGRectMake(CGRectGetMinX(navigatingBounds),
                 CGRectGetMinY(navigatingBounds) + headerHeight,
                 CGRectGetWidth(navigatingBounds),
                 CGRectGetHeight(navigatingBounds) - headerHeight);

  BOOL canNavigate = [self canNavigate:IsSwipingBack(direction)];
  _pageSideSwipeView = [[SideSwipeNavigationView alloc]
      initWithFrame:navigationFrame
      withDirection:direction
        canNavigate:canNavigate
              image:[UIImage imageNamed:@"side_swipe_navigation_back"]];
  [_pageSideSwipeView setTargetView:[_swipeDelegate sideSwipeContentView]];

  [navigatingView insertSubview:_pageSideSwipeView
                   belowSubview:[_swipeDelegate topToolbarView]];

  __weak SideSwipeMediator* weakSelf = self;
  [_pageSideSwipeView
      animateHorizontalPanWithDirection:direction
                      completionHandler:^{
                        if (canNavigate) {
                          [weakSelf handleOverThresholdCompletion:direction];
                        } else {
                          [weakSelf handleUnderThresholdCompletion];
                        }
                      }];
}

- (void)handleOverThresholdCompletion:
    (UISwipeGestureRecognizerDirection)direction {
  web::WebState* webState = self.activeWebState;
  BOOL wantsBack = IsSwipingBack(direction);
  if (webState) {
    if (wantsBack) {
      web_navigation_util::GoBack(webState);
    } else {
      web_navigation_util::GoForward(webState);
    }
    CHECK(self.engagementTracker);
    self.engagementTracker->NotifyEvent(
        feature_engagement::events::kIOSSwipeBackForwardUsed);
  }
  __weak SideSwipeMediator* weakSelf = self;
  // Checking -IsLoading() is likely incorrect, but to narrow the scope of
  // fixes for slim navigation manager, only ignore this state when
  // slim is disabled.  With slim navigation enabled, this false when
  // pages can be served from WKWebView's page cache.
  if (webState) {
    [self addCurtainWithCompletionHandler:^{
      [weakSelf handleCurtainCompletion];
    }];
  } else {
    _inSwipe = NO;
  }
  [_swipeDelegate updateAccessoryViewsForSideSwipeWithVisibility:YES];
}

- (void)handleCurtainCompletion {
  _inSwipe = NO;
}

- (void)handleUnderThresholdCompletion {
  [_swipeDelegate updateAccessoryViewsForSideSwipeWithVisibility:YES];
  _inSwipe = NO;
}

// Show horizontal swipe stack view for iPhone.
- (void)handleiPhoneTabSwipe:(SideSwipeGestureRecognizer*)gesture {
  if (gesture.state == UIGestureRecognizerStateBegan) {
    _inSwipe = YES;

    CGRect frame = [[_swipeDelegate sideSwipeContentView] frame];

    // Add horizontal stack view controller.
    CGFloat headerHeight =
        self.fullscreenController->GetMaxViewportInsets().top;

    if (_tabSideSwipeView) {
      [_tabSideSwipeView setFrame:frame];
      [_tabSideSwipeView setTopMargin:headerHeight];
    } else {
      _tabSideSwipeView =
          [[CardSideSwipeView alloc] initWithFrame:frame
                                         topMargin:headerHeight
                                      webStateList:self.webStateList];
      _tabSideSwipeView.toolbarSnapshotProvider = self.toolbarSnapshotProvider;

      [_tabSideSwipeView setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
                                             UIViewAutoresizingFlexibleHeight];
      [_tabSideSwipeView setDelegate:_swipeDelegate];
      [_tabSideSwipeView setBackgroundColor:[UIColor blackColor]];
    }

    // Ensure that there's an up-to-date snapshot of the current tab.
    if (self.activeWebState) {
      SnapshotTabHelper::FromWebState(self.activeWebState)
          ->UpdateSnapshotWithCallback(nil);
    }

    // Layout tabs with new snapshots in the current orientation.
    [_tabSideSwipeView updateViewsForDirection:gesture.direction];

    // Insert above the toolbar.
    [gesture.view addSubview:_tabSideSwipeView];
  }

  __weak SideSwipeMediator* weakSelf = self;
  [_tabSideSwipeView
        handleHorizontalPan:gesture
      actionBeforeTabSwitch:^(int destinationWebStateIndex) {
        [weakSelf willActivateWebStateAtIndex:destinationWebStateIndex];
      }];
}

- (void)addCurtainWithCompletionHandler:(ProceduralBlock)completionHandler {
  if (!_curtain) {
    _curtain = [[UIView alloc]
        initWithFrame:[_swipeDelegate sideSwipeContentView].bounds];
    [_curtain setBackgroundColor:[UIColor whiteColor]];
  }
  [[_swipeDelegate sideSwipeContentView] addSubview:_curtain];

  // Fallback in case load takes a while. 3 seconds is a balance between how
  // long it can take a web view to clear the previous page image, and what
  // feels like to 'too long' to see the curtain.
  [self performSelector:@selector(dismissCurtainWithCompletionHandler:)
             withObject:[completionHandler copy]
             afterDelay:3];
}

- (void)resetContentView {
  if (!_inSwipe) {
    return;
  }
  CGRect frame = [_swipeDelegate sideSwipeContentView].frame;
  frame.origin.x = 0;
  [_swipeDelegate sideSwipeContentView].frame = frame;
}

- (void)dismissCurtainWithCompletionHandler:(ProceduralBlock)completionHandler {
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
  [_curtain removeFromSuperview];
  _curtain = nil;
  completionHandler();
}

- (void)dismissCurtain {
  if (!_inSwipe) {
    return;
  }
  __weak SideSwipeMediator* weakSelf = self;
  [self dismissCurtainWithCompletionHandler:^{
    [weakSelf handleCurtainCompletion];
  }];
}

- (void)updateNavigationEdgeSwipeForWebState:(web::WebState*)webState {
  if (!webState) {
    return;
  }

  // With slim nav enabled, disable SideSwipeMediator's edge swipe for a
  // typical navigation.  Continue to use SideSwipeMediator when on, before,
  // or after a native page.
  self.leadingEdgeNavigationEnabled = NO;
  self.trailingEdgeNavigationEnabled = NO;

  web::NavigationItem* item =
      webState->GetNavigationManager()->GetVisibleItem();
  if (UseNativeSwipe(item)) {
    self.leadingEdgeNavigationEnabled = YES;
    self.trailingEdgeNavigationEnabled = YES;
  }

  // If the previous page is an NTP, enable leading edge swipe.
  std::vector<web::NavigationItem*> backItems =
      webState->GetNavigationManager()->GetBackwardItems();
  if (backItems.size() > 0 && UseNativeSwipe(backItems[0])) {
    self.leadingEdgeNavigationEnabled = YES;
  }

  // If the next page is an NTP, enable trailing edge swipe.
  std::vector<web::NavigationItem*> forwardItems =
      webState->GetNavigationManager()->GetForwardItems();
  if (forwardItems.size() > 0 && UseNativeSwipe(forwardItems[0])) {
    self.trailingEdgeNavigationEnabled = YES;
  }
}

#pragma mark - CRWWebStateObserver Methods

- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
  [self dismissCurtain];
}

- (void)webState:(web::WebState*)webState
    didFinishNavigation:(web::NavigationContext*)navigation {
  [self updateNavigationEdgeSwipeForWebState:webState];
}

#pragma mark - UIGestureRecognizerDelegate Methods

// Gestures should only be recognized within `contentArea_` or the toolbar.
- (BOOL)gestureRecognizerShouldBegin:(SideSwipeGestureRecognizer*)gesture {
  if (_inSwipe) {
    return NO;
  }

  if ([_swipeDelegate preventSideSwipe]) {
    return NO;
  }

  if (IsContextualPanelEnabled()) {
    // Don't handle gesture if it's meant for the Contextual Panel Entrypoint
    // (gesture began in its frame) and that entrypoint is currently large.
    // `contextualPanelEntrypointView` is nil if the entrypoint is not currently
    // large, which means the gesture won't be blocked here.
    UIView* contextualPanelEntrypointView = [self.layoutGuideCenter
        referencedViewUnderName:kContextualPanelLargeEntrypointGuide];
    CGPoint touchLocationInEntrypointViewCoordinates =
        [contextualPanelEntrypointView convertPoint:[gesture locationInView:nil]
                                           fromView:nil];
    BOOL tapInsideContextualPanelEntrypointContainer =
        [contextualPanelEntrypointView
            pointInside:touchLocationInEntrypointViewCoordinates
              withEvent:nil];

    if (tapInsideContextualPanelEntrypointContainer) {
      return NO;
    }
  }

  CGPoint location = [gesture locationInView:gesture.view];

  // Since the toolbar and the contentView can overlap, check the toolbar frame
  // first, and confirm the right gesture recognizer is firing.
  if ([self.toolbarInteractionHandler
          isInsideToolbar:[gesture.view convertPoint:location toView:nil]]) {
    if (![gesture isEqual:_panGestureRecognizer]) {
      return NO;
    }

    return [_swipeDelegate canBeginToolbarSwipe];
  }

  // Otherwise, only allow contentView touches with `swipeGestureRecognizer_`.
  // The content view frame is inset by -1 because CGRectContainsPoint does
  // include points on the max X and Y edges, which will happen frequently with
  // edge swipes from the right side.
  CGRect contentViewFrame =
      CGRectInset([[_swipeDelegate sideSwipeContentView] frame], -1, -1);
  if (CGRectContainsPoint(contentViewFrame, location)) {
    if (![gesture isEqual:_swipeGestureRecognizer]) {
      return NO;
    }

    if (gesture.direction == UISwipeGestureRecognizerDirectionRight &&
        !self.leadingEdgeNavigationEnabled) {
      return NO;
    }

    if (gesture.direction == UISwipeGestureRecognizerDirectionLeft &&
        !self.trailingEdgeNavigationEnabled) {
      return NO;
    }
    _swipeType = SwipeType::CHANGE_PAGE;
    return YES;
  }
  return NO;
}

// Always return yes, as this swipe should work with various recognizers,
// including UITextTapRecognizer, UILongPressGestureRecognizer,
// UIScrollViewPanGestureRecognizer and others.
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
        (UIGestureRecognizer*)otherGestureRecognizer {
  return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    shouldBeRequiredToFailByGestureRecognizer:
        (UIGestureRecognizer*)otherGestureRecognizer {
  // Take precedence over a pan gesture recognizer so that moving up and
  // down while swiping doesn't trigger overscroll actions.
  if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
    return YES;
  }
  // Take precedence over a WKWebView side swipe gesture.
  if ([otherGestureRecognizer
          isKindOfClass:[UIScreenEdgePanGestureRecognizer class]]) {
    return YES;
  }
  return NO;
}

#pragma mark - WebStateListObserving Methods

- (void)didChangeWebStateList:(WebStateList*)webStateList
                       change:(const WebStateListChange&)change
                       status:(const WebStateListStatus&)status {
  if (!status.active_web_state_change()) {
    return;
  }

  // If there is any an ongoing swipe for the old webState, cancel it and
  // dismiss the curtain.
  [self dismissCurtain];
  // Toggling the gesture's enabled state off and on will effectively cancel
  // the gesture recognizer.
  [_swipeGestureRecognizer setEnabled:NO];
  [_swipeGestureRecognizer setEnabled:YES];
  // Track the new active WebState for navigation events. Also remove the old if
  // there was one.
  if (status.old_active_web_state) {
    _scopedWebStateObservation->Reset();
  }
  if (status.new_active_web_state) {
    _scopedWebStateObservation->Observe(status.new_active_web_state);
  }

  [self updateNavigationEdgeSwipeForWebState:status.new_active_web_state];
}

@end