chromium/ios/chrome/browser/ui/popup_menu/public/popup_menu_presenter.mm

// Copyright 2018 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/ui/popup_menu/public/popup_menu_presenter.h"

#import "base/check.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_presenter_delegate.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_view_controller.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_view_controller_delegate.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
const CGFloat kMinHeight = 200;
const CGFloat kMinWidth = 200;
const CGFloat kMaxWidth = 300;
const CGFloat kMaxHeight = 435;
const CGFloat kMinWidthDifference = 50;
const CGFloat kMinHorizontalMargin = 5;
const CGFloat kMinVerticalMargin = 15;
const CGFloat kDamping = 0.85;
}  // namespace

@interface PopupMenuPresenter () <PopupMenuViewControllerDelegate>
@property(nonatomic, strong) PopupMenuViewController* popupViewController;
// Constraints used for the initial positioning of the popup.
@property(nonatomic, strong) NSArray<NSLayoutConstraint*>* initialConstraints;
// Constraints used for the positioning of the popup when presented.
@property(nonatomic, strong) NSArray<NSLayoutConstraint*>* presentedConstraints;

@property(nonatomic, strong)
    NSLayoutConstraint* presentedViewControllerHeightConstraint;
@property(nonatomic, strong)
    NSLayoutConstraint* presentedViewControllerWidthConstraint;
@end

@implementation PopupMenuPresenter

@synthesize baseViewController = _baseViewController;
@synthesize delegate = _delegate;
@synthesize popupViewController = _popupViewController;
@synthesize initialConstraints = _initialConstraints;
@synthesize presentedConstraints = _presentedConstraints;
@synthesize presentedViewController = _presentedViewController;

#pragma mark - Public

- (void)prepareForPresentation {
  DCHECK(self.baseViewController);
  if (self.popupViewController)
    return;

  self.popupViewController = [[PopupMenuViewController alloc] init];
  self.popupViewController.delegate = self;
  [self.presentedViewController.view
      setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
                                      forAxis:UILayoutConstraintAxisHorizontal];

  // Set the frame of the table view to the maximum width to have the label
  // resizing correctly.
  CGRect frame = self.presentedViewController.view.frame;
  frame.size.width = kMaxWidth;
  self.presentedViewController.view.frame = frame;
  // It is necessary to do a first layout pass so the table view can size
  // itself.
  [self.presentedViewController.view setNeedsLayout];
  [self.presentedViewController.view layoutIfNeeded];

  // Set the sizing constraints, in case the UIViewController is using a
  // UIScrollView. The priority needs to be non-required to allow downsizing if
  // needed, and more than UILayoutPriorityDefaultHigh to take precedence on
  // compression resistance.
  self.presentedViewControllerWidthConstraint =
      [self.presentedViewController.view.widthAnchor
          constraintEqualToConstant:0];
  self.presentedViewControllerWidthConstraint.priority =
      UILayoutPriorityDefaultHigh + 1;

  self.presentedViewControllerHeightConstraint =
      [self.presentedViewController.view.heightAnchor
          constraintEqualToConstant:0];
  self.presentedViewControllerHeightConstraint.priority =
      UILayoutPriorityDefaultHigh + 1;

  // Set the constraint constants to their correct intial values.
  [self setPresentedViewControllerConstraintConstants];

  UIView* popup = self.popupViewController.contentContainer;
  [NSLayoutConstraint activateConstraints:@[
    self.presentedViewControllerWidthConstraint,
    self.presentedViewControllerHeightConstraint,
    [popup.heightAnchor constraintLessThanOrEqualToConstant:kMaxHeight],
    [popup.widthAnchor constraintLessThanOrEqualToConstant:kMaxWidth],
    [popup.widthAnchor constraintGreaterThanOrEqualToConstant:kMinWidth],
  ]];
  [self.popupViewController addContent:self.presentedViewController];

  [self.baseViewController addChildViewController:self.popupViewController];
  [self.baseViewController.view addSubview:self.popupViewController.view];
  self.popupViewController.view.frame = self.baseViewController.view.bounds;

  [popup.widthAnchor constraintLessThanOrEqualToAnchor:self.popupViewController
                                                           .view.widthAnchor
                                              constant:-kMinWidthDifference]
      .active = YES;

  UILayoutGuide* layoutGuide = self.layoutGuide;
  self.initialConstraints = @[
    [popup.centerXAnchor constraintEqualToAnchor:layoutGuide.centerXAnchor],
    [popup.centerYAnchor constraintEqualToAnchor:layoutGuide.centerYAnchor],
  ];
  [self setUpPresentedConstraints];

  // Configure the initial state of the animation.
  popup.alpha = 0;
  popup.transform = CGAffineTransformMakeScale(0.1, 0.1);
  [NSLayoutConstraint activateConstraints:self.initialConstraints];
  [self.baseViewController.view layoutIfNeeded];

  [self.popupViewController
      didMoveToParentViewController:self.baseViewController];
}

- (void)presentAnimated:(BOOL)animated {
  [NSLayoutConstraint deactivateConstraints:self.initialConstraints];
  [NSLayoutConstraint activateConstraints:self.presentedConstraints];
  [self
      animate:^{
        self.popupViewController.contentContainer.alpha = 1;
        [self.baseViewController.view layoutIfNeeded];
        self.popupViewController.contentContainer.transform =
            CGAffineTransformIdentity;
      }
      withCompletion:^(BOOL finished) {
        if ([self.delegate
                respondsToSelector:@selector(containedPresenterDidPresent:)]) {
          [self.delegate containedPresenterDidPresent:self];
        }
      }];
}

- (void)dismissAnimated:(BOOL)animated {
  [self.popupViewController willMoveToParentViewController:nil];
  // Notify the presented view controller that it will be removed to prevent it
  // from triggering unnecessary layout passes, which might lead to a hang. See
  // crbug.com/1126618.
  [self.presentedViewController willMoveToParentViewController:nil];
  [NSLayoutConstraint deactivateConstraints:self.presentedConstraints];
  [NSLayoutConstraint activateConstraints:self.initialConstraints];
  auto completion = ^(BOOL finished) {
    [self.popupViewController.view removeFromSuperview];
    [self.popupViewController removeFromParentViewController];
    self.popupViewController = nil;
    if ([self.delegate
            respondsToSelector:@selector(containedPresenterDidDismiss:)]) {
      [self.delegate containedPresenterDidDismiss:self];
    }
  };
  if (animated) {
    [self
               animate:^{
                 self.popupViewController.contentContainer.alpha = 0;
                 [self.baseViewController.view layoutIfNeeded];
                 self.popupViewController.contentContainer.transform =
                     CGAffineTransformMakeScale(0.1, 0.1);
               }
        withCompletion:completion];
  } else {
    completion(YES);
  }
}

#pragma mark - Private

// Animate the `animations` then execute `completion`.
- (void)animate:(void (^)(void))animation
    withCompletion:(void (^)(BOOL finished))completion {
  [UIView animateWithDuration:kMaterialDuration1
                        delay:0
       usingSpringWithDamping:kDamping
        initialSpringVelocity:0
                      options:UIViewAnimationOptionBeginFromCurrentState
                   animations:animation
                   completion:completion];
}

// Sets `presentedConstraints` up, such as they are positioning the popup
// relatively to `layoutGuide`. The popup is positioned closest to the layout
// guide, by default it is presented below the layout guide, aligned on its
// leading edge. However, it is respecting the safe area bounds.
- (void)setUpPresentedConstraints {
  UIView* parentView = self.baseViewController.view;
  UIView* container = self.popupViewController.contentContainer;

  UILayoutGuide* layoutGuide = self.layoutGuide;
  CGRect guideFrame =
      [self.popupViewController.view convertRect:layoutGuide.layoutFrame
                                        fromView:layoutGuide.owningView];

  NSLayoutConstraint* verticalPositioning = nil;
  if (CGRectGetMaxY(guideFrame) + kMinHeight >
      CGRectGetHeight(parentView.frame)) {
    // Display above.
    verticalPositioning =
        [container.bottomAnchor constraintEqualToAnchor:layoutGuide.topAnchor];
  } else {
    // Display below.
    verticalPositioning =
        [container.topAnchor constraintEqualToAnchor:layoutGuide.bottomAnchor];
  }

  NSLayoutConstraint* center = [container.centerXAnchor
      constraintEqualToAnchor:layoutGuide.centerXAnchor];
  center.priority = UILayoutPriorityDefaultHigh;

  id<LayoutGuideProvider> safeArea = parentView.safeAreaLayoutGuide;
  self.presentedConstraints = @[
    center,
    verticalPositioning,
    [container.leadingAnchor
        constraintGreaterThanOrEqualToAnchor:safeArea.leadingAnchor
                                    constant:kMinHorizontalMargin],
    [container.trailingAnchor
        constraintLessThanOrEqualToAnchor:safeArea.trailingAnchor
                                 constant:-kMinHorizontalMargin],
    [container.bottomAnchor
        constraintLessThanOrEqualToAnchor:safeArea.bottomAnchor
                                 constant:-kMinVerticalMargin],
    [container.topAnchor
        constraintGreaterThanOrEqualToAnchor:safeArea.topAnchor
                                    constant:kMinVerticalMargin],
  ];
}

// Updates the constants for the constraints constraining the presented view
// controller's height and width.
- (void)setPresentedViewControllerConstraintConstants {
  CGSize fittingSize = [self.presentedViewController.view
      sizeThatFits:CGSizeMake(kMaxWidth, kMaxHeight)];
  // Use preferredSize if it is set.
  CGSize preferredSize = self.presentedViewController.preferredContentSize;
  CGFloat width = fittingSize.width;
  CGFloat height = fittingSize.height;
  if (!CGSizeEqualToSize(preferredSize, CGSizeZero)) {
    width = preferredSize.width;
    height = preferredSize.height;
  }
  self.presentedViewControllerHeightConstraint.constant = height;
  self.presentedViewControllerWidthConstraint.constant = width;
}

#pragma mark - PopupMenuViewControllerDelegate

- (void)popupMenuViewControllerWillDismiss:
    (PopupMenuViewController*)viewController {
  [self.delegate popupMenuPresenterWillDismiss:self];
}

- (void)containedViewControllerContentSizeChangedForPopupMenuViewController:
    (PopupMenuViewController*)viewController {
  // Set the frame of the table view to the maximum width to have the label
  // resizing correctly.
  CGRect frame = self.presentedViewController.view.frame;
  frame.size.width = kMaxWidth;
  self.presentedViewController.view.frame = frame;
  // It is necessary to do a first layout pass so the table view can size
  // itself.
  [self.presentedViewController.view setNeedsLayout];
  [self.presentedViewController.view layoutIfNeeded];

  [self setPresentedViewControllerConstraintConstants];
}

@end