chromium/ios/chrome/browser/bubble/ui_bundled/gesture_iph/toolbar_swipe_gesture_in_product_help_view.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/toolbar_swipe_gesture_in_product_help_view.h"

#import "base/notreached.h"
#import "base/time/time.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_constants.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_view.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_constants.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_view+subclassing.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// Initial distance between the gesture indicator and the edge of the screen.
const CGFloat kInitialGestureIndicatorToEdgeSpacing = 16.0f;
// The distance that the gesture indicator should move during the animation.
const CGFloat kGestureIndicatorDistanceAnimated = 100.0f;
// The distance between the dismiss button and the bubble.
const CGFloat kDismissButtonDistanceToBubble = 80.0f;
// Number of animation cycles if the view is bidirectional.
const int kBidirectionalAnimationRepeatCount = 4;

}  // namespace

@implementation ToolbarSwipeGestureInProductHelpView {
  // Constraints to position dismiss button.
  NSArray<NSLayoutConstraint*>* _dismissButtonConstraints;
}

@synthesize bidirectional = _bidirectional;
@synthesize animationRepeatCount = _animationRepeatCount;

- (instancetype)initWithBubbleBoundingSize:(CGSize)bubbleBoundingSize
                                 canGoBack:(BOOL)back
                                   forward:(BOOL)forward {
  CHECK(back || forward);
  self = [super initWithText:l10n_util::GetNSString(IDS_IOS_TOOLBAR_SWIPE_IPH)
          bubbleBoundingSize:bubbleBoundingSize
              swipeDirection:(back ^ UseRTLLayout())
                                 ? UISwipeGestureRecognizerDirectionRight
                                 : UISwipeGestureRecognizerDirectionLeft
       voiceOverAnnouncement:nil];
  if (self) {
    _dismissButtonConstraints = [self dismissButtonConstraints];
    if (back && forward) {
      _bidirectional = YES;
      _animationRepeatCount = kBidirectionalAnimationRepeatCount;
    } else {
      _bidirectional = NO;
      _animationRepeatCount = [super animationRepeatCount];
    }
  }
  return self;
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  if (self.traitCollection.verticalSizeClass !=
      previousTraitCollection.verticalSizeClass) {
    [self.bubbleView removeFromSuperview];
    [self setInitialBubbleViewWithDirection:
              (BubbleArrowDirection)0 /* this value is not used */
                               boundingSize:self.bounds.size];
    [NSLayoutConstraint deactivateConstraints:_dismissButtonConstraints];
    _dismissButtonConstraints = [self dismissButtonConstraints];
    [NSLayoutConstraint activateConstraints:_dismissButtonConstraints];
    self.topConstraintForBottomEdgeSwipe.active = [self shouldSwipeBottomEdge];
    self.topConstraintForTopEdgeSwipe.active = ![self shouldSwipeBottomEdge];
  }
  [super traitCollectionDidChange:previousTraitCollection];
}

- (void)setInitialBubbleViewWithDirection:(BubbleArrowDirection)direction
                             boundingSize:(CGSize)boundingSize {
  [super setInitialBubbleViewWithDirection:[self shouldSwipeBottomEdge]
                                               ? BubbleArrowDirectionDown
                                               : BubbleArrowDirectionUp
                              boundingSize:boundingSize];
  [self.bubbleView setArrowHidden:NO animated:NO];
}

- (NSLayoutConstraint*)initialGestureIndicatorMarginConstraint {
  CGFloat margin =
      kInitialGestureIndicatorToEdgeSpacing + kGestureIndicatorRadius;
  switch (self.animatingDirection) {
    case UISwipeGestureRecognizerDirectionUp:
    case UISwipeGestureRecognizerDirectionDown:
      NOTREACHED_IN_MIGRATION();
      return nil;
    case UISwipeGestureRecognizerDirectionLeft:
    case UISwipeGestureRecognizerDirectionRight:
    default:
      BOOL directionIsLeading =
          self.animatingDirection ==
          (UseRTLLayout() ? UISwipeGestureRecognizerDirectionRight
                          : UISwipeGestureRecognizerDirectionLeft);
      if (directionIsLeading) {
        return [self.gestureIndicator.centerXAnchor
            constraintEqualToAnchor:self.trailingAnchor
                           constant:-margin];
      }
      return [self.gestureIndicator.centerXAnchor
          constraintEqualToAnchor:self.leadingAnchor
                         constant:margin];
  }
}

- (NSLayoutConstraint*)initialGestureIndicatorCenterConstraint {
  return [self.gestureIndicator.centerYAnchor
      constraintEqualToAnchor:[self shouldSwipeBottomEdge] ? self.bottomAnchor
                                                           : self.topAnchor];
}

- (NSArray<NSLayoutConstraint*>*)dismissButtonConstraints {
  if ([self shouldSwipeBottomEdge]) {
    return @[
      [self.dismissButton.centerXAnchor
          constraintEqualToAnchor:self.centerXAnchor],
      [self.dismissButton.bottomAnchor
          constraintEqualToAnchor:self.bubbleView.topAnchor
                         constant:-kDismissButtonDistanceToBubble]
    ];
  }
  return [super dismissButtonConstraints];
}

- (CGFloat)gestureIndicatorAnimatedDistance {
  return kGestureIndicatorDistanceAnimated;
}

- (void)animateBubbleSwipeInReverseDrection:(BOOL)reverse {
  // Bubble should not move during the animation.
  return;
}

- (void)handleInstructedSwipeGesture:
    (GestureInProductHelpGestureRecognizer*)gesture {
  // Swipe action happens outside the bounds of the gesture in-product help
  // view.
  return;
}

- (void)handleDirectionChangeToOppositeDirection {
  [self startAnimationAfterDelay:kDurationBetweenBidirectionalCycles];
}

#pragma mark - Private

// Whether the use should swipe the bottom edge, instead of the top.
- (BOOL)shouldSwipeBottomEdge {
  return IsSplitToolbarMode(self.traitCollection);
}

@end