chromium/ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_gesture_recognizer.mm

// Copyright 2024 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/bubble/ui_bundled/gesture_iph/gesture_in_product_help_gesture_recognizer.h"

#import <cmath>

#import "base/i18n/rtl.h"
#import "base/numerics/angle_conversions.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_constants.h"

namespace {

/// Maxmimum angle to the expected direction that should be recognized as a
/// swipe. For example, a swipe-right tilted 30 degrees downwards can still be
/// recognized as a "swipe down", because it's only deviated from "down" 60
/// degrees; however a swipe-right tilted 20 degrees downwards would not be
/// recognized as a "swipe down".
const CGFloat kMaxSwipeAngle = 65;
/// The minimum distance between touches for a swipe to begin.
const CGFloat kDefaultMinSwipeThreshold = 4;
// Swipe starting distance from edge.
const CGFloat kSwipeEdge = 20;

/// Returns the opposite direction to `direction`.
UISwipeGestureRecognizerDirection GetOppositeDirection(
    UISwipeGestureRecognizerDirection direction) {
  switch (direction) {
    case UISwipeGestureRecognizerDirectionUp:
      return UISwipeGestureRecognizerDirectionDown;
    case UISwipeGestureRecognizerDirectionDown:
      return UISwipeGestureRecognizerDirectionUp;
    case UISwipeGestureRecognizerDirectionLeft:
      return UISwipeGestureRecognizerDirectionRight;
    case UISwipeGestureRecognizerDirectionRight:
    default:
      return UISwipeGestureRecognizerDirectionLeft;
  }
}

}  // namespace

@implementation GestureInProductHelpGestureRecognizer {
  /// Expected swipe direction; passed through the initializer.
  UISwipeGestureRecognizerDirection _expectedSwipeDirection;
  /// Starting point of swipe.
  CGPoint _startPoint;
}

- (instancetype)initWithExpectedSwipeDirection:
                    (UISwipeGestureRecognizerDirection)direction
                                        target:(id)target
                                        action:(SEL)action {
  self = [super initWithTarget:target action:action];
  if (self) {
    _expectedSwipeDirection = direction;
    _bidirectional = NO;
    _edgeSwipe = NO;
    _startPoint = CGPointZero;
    _actualSwipeDirection = 0;
    [self setMaximumNumberOfTouches:1];
  }
  return self;
}

/// To quickly avoid interference with other gesture recognizers, fail
/// immediately if the touches aren't at the edge of the touched view.
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
  [super touchesBegan:touches withEvent:event];

  // Don't interrupt gestures in progress.
  if (self.state != UIGestureRecognizerStatePossible) {
    return;
  }

  UITouch* touch = [[event allTouches] anyObject];
  CGPoint location = [touch locationInView:self.view];
  _startPoint = location;
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
  // Revert to normal pan gesture recognizer characteristics after state began.
  if (self.state != UIGestureRecognizerStatePossible) {
    [super touchesMoved:touches withEvent:event];
    return;
  }

  // Only one touch.
  if ([[event allTouches] count] > 1) {
    self.state = UIGestureRecognizerStateFailed;
    return;
  }

  // Avoid recognizing swipe at an angle greater than `kMaxSwipeAngle`.
  UITouch* touch = [[event allTouches] anyObject];
  CGPoint currentPoint = [touch locationInView:self.view];
  CGFloat dx = currentPoint.x - _startPoint.x;
  CGFloat dy = currentPoint.y - _startPoint.y;
  CGFloat degrees = 0;
  CGFloat distance = 0;
  switch (_expectedSwipeDirection) {
    case UISwipeGestureRecognizerDirectionUp:
      distance = std::fabs(dy);
      degrees = base::RadToDeg(std::atan2(std::fabs(dx), -dy));
      break;
    case UISwipeGestureRecognizerDirectionDown:
      distance = std::fabs(dy);
      degrees = base::RadToDeg(std::atan2(std::fabs(dx), dy));
      break;
    case UISwipeGestureRecognizerDirectionLeft:
      distance = std::fabs(dx);
      degrees = base::RadToDeg(std::atan2(std::fabs(dy), -dx));
      break;
    case UISwipeGestureRecognizerDirectionRight:
      distance = std::fabs(dx);
      degrees = base::RadToDeg(std::atan2(std::fabs(dy), dx));
      break;
  }
  if (distance < kDefaultMinSwipeThreshold) {
    return;
  }

  UIGestureRecognizerState state = UIGestureRecognizerStateFailed;
  if (0 <= degrees && degrees < kMaxSwipeAngle) {
    state = UIGestureRecognizerStateBegan;
    _actualSwipeDirection = _expectedSwipeDirection;
  } else if (self.bidirectional &&
             (180 - kMaxSwipeAngle < degrees && degrees <= 180)) {
    state = UIGestureRecognizerStateBegan;
    _actualSwipeDirection = GetOppositeDirection(_expectedSwipeDirection);
  }

  // If `[self isEdgeSwipe]`, determine whether the touch has started
  // from the edge.
  if (state != UIGestureRecognizerStateFailed && [self isEdgeSwipe]) {
    CGFloat startPointFromEdge = CGFLOAT_MAX;
    switch (_actualSwipeDirection) {
      case UISwipeGestureRecognizerDirectionUp:
        startPointFromEdge = CGRectGetHeight(self.view.bounds) - _startPoint.y;
        break;
      case UISwipeGestureRecognizerDirectionDown:
        startPointFromEdge = _startPoint.y;
        break;
      case UISwipeGestureRecognizerDirectionLeft:
        startPointFromEdge = CGRectGetWidth(self.view.bounds) - _startPoint.x;
        break;
      case UISwipeGestureRecognizerDirectionRight:
        startPointFromEdge = _startPoint.x;
        break;
    }
    if (startPointFromEdge > kSwipeEdge) {
      state = UIGestureRecognizerStateFailed;
    }
  }
  self.state = state;
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
  _startPoint = CGPointZero;
  [super touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
  _startPoint = CGPointZero;
  [super touchesCancelled:touches withEvent:event];
}

@end