chromium/ios/chrome/browser/ui/omnibox/popup/omnibox_popup_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/omnibox/popup/omnibox_popup_presenter.h"

#import "base/time/time.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/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_ui_features.h"
#import "ios/chrome/browser/ui/omnibox/popup/content_providing.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/chrome/grit/ios_theme_resources.h"
#import "ui/base/device_form_factor.h"
#import "ui/gfx/ios/uikit_util.h"

namespace {
const CGFloat kVerticalOffset = 6;
const CGFloat kPopupBottomPaddingTablet = 80;

/// Duration of the fade in animation.
constexpr NSTimeInterval kFadeInAnimationDuration =
    base::Milliseconds(300).InSecondsF();
/// Vertical offset of the suggestions when fading in.
const CGFloat kFadeAnimationVerticalOffset = 12;

}  // namespace

@interface OmniboxPopupPresenter ()
/// Constraint for the bottom anchor of the popup when form factor is phone.
@property(nonatomic, strong) NSLayoutConstraint* bottomConstraintPhone;
/// Constraint for the bottom anchor of the popup when form factor is tablet.
@property(nonatomic, strong) NSLayoutConstraint* bottomConstraintTablet;

@property(nonatomic, weak) id<OmniboxPopupPresenterDelegate> delegate;
@property(nonatomic, weak) UIViewController<ContentProviding>* viewController;
@property(nonatomic, strong) UIView* popupContainerView;
/// Separator for the bottom edge of the popup on iPad.
@property(nonatomic, strong) UIView* bottomSeparator;
/// Top constraint between the popup and it's container. This is used to animate
/// suggestions when focusing the omnibox.
@property(nonatomic, strong) NSLayoutConstraint* popupTopConstraint;

// The layout guide center to use to refer to the omnibox.
@property(nonatomic, strong) LayoutGuideCenter* layoutGuideCenter;
@property(nonatomic, strong) UILayoutGuide* topOmniboxGuide;

@end

@implementation OmniboxPopupPresenter {
  /// Type of the toolbar that contains the omnibox when it's not focused. The
  /// animation of focusing/defocusing the omnibox changes depending on this
  /// position.
  ToolbarType _unfocusedOmniboxToolbarType;
}

- (instancetype)
    initWithPopupPresenterDelegate:(id<OmniboxPopupPresenterDelegate>)delegate
               popupViewController:
                   (UIViewController<ContentProviding>*)viewController
                 layoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter
                         incognito:(BOOL)incognito {
  self = [super init];
  if (self) {
    _delegate = delegate;
    _viewController = viewController;
    _layoutGuideCenter = layoutGuideCenter;

    // Popup uses same colors as the toolbar, so the ToolbarConfiguration is
    // used to get the style.
    ToolbarConfiguration* configuration = [[ToolbarConfiguration alloc]
        initWithStyle:incognito ? ToolbarStyle::kIncognito
                                : ToolbarStyle::kNormal];

    UIView* containerView = [[UIView alloc] init];
    [containerView addSubview:viewController.view];
    _popupContainerView = containerView;

    UIUserInterfaceStyle userInterfaceStyle =
        incognito ? UIUserInterfaceStyleDark : UIUserInterfaceStyleUnspecified;
    // Both the container view and the popup view controller need
    // overrideUserInterfaceStyle set because the overall popup background
    // comes from the container, but overrideUserInterfaceStyle won't
    // propagate from a view to any subviews in a different view controller.
    _popupContainerView.overrideUserInterfaceStyle = userInterfaceStyle;
    viewController.overrideUserInterfaceStyle = userInterfaceStyle;

    if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
      _popupContainerView.backgroundColor =
          [UIColor colorNamed:kPrimaryBackgroundColor];
    } else {
      _popupContainerView.backgroundColor = configuration.backgroundColor;
    }

    _popupContainerView.translatesAutoresizingMaskIntoConstraints = NO;
    viewController.view.translatesAutoresizingMaskIntoConstraints = NO;

    if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
      self.viewController.view.layer.masksToBounds = YES;

      AddSameConstraints(viewController.view, _popupContainerView);
    } else {
      AddSameConstraintsToSides(viewController.view, _popupContainerView,
                                LayoutSides::kLeading | LayoutSides::kTrailing |
                                    LayoutSides::kBottom);
      _popupTopConstraint = [viewController.view.topAnchor
          constraintEqualToAnchor:_popupContainerView.topAnchor];
      _popupTopConstraint.active = YES;

      // Add bottom separator. This will only be visible on iPad where
      // the omnibox doesn't fill the whole screen.
      _bottomSeparator = [[UIView alloc] initWithFrame:CGRectZero];
      _bottomSeparator.translatesAutoresizingMaskIntoConstraints = NO;
      _bottomSeparator.backgroundColor =
          [UIColor colorNamed:kToolbarShadowColor];

      [_popupContainerView addSubview:self.bottomSeparator];

      CGFloat separatorHeight =
          ui::AlignValueToUpperPixel(kToolbarSeparatorHeight);
      [NSLayoutConstraint activateConstraints:@[
        [self.bottomSeparator.heightAnchor
            constraintEqualToConstant:separatorHeight],
        [self.bottomSeparator.leadingAnchor
            constraintEqualToAnchor:_popupContainerView.leadingAnchor],
        [self.bottomSeparator.trailingAnchor
            constraintEqualToAnchor:_popupContainerView.trailingAnchor],
        [self.bottomSeparator.topAnchor
            constraintEqualToAnchor:_popupContainerView.bottomAnchor],
      ]];
    }
  }
  return self;
}

- (void)updatePopupOnFocus:(BOOL)isFocusingOmnibox {
  BOOL popupHasContent = self.viewController.hasContent;
  BOOL popupIsOnscreen = self.popupContainerView.superview != nil;
  if (!popupHasContent && popupIsOnscreen) {
    // If intrinsic size is 0 and popup is onscreen, we want to remove the
    // popup view.
    if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
      self.bottomConstraintPhone.active = NO;
      self.bottomSeparator.hidden = YES;
    }

    [self.viewController willMoveToParentViewController:nil];
    [self.popupContainerView removeFromSuperview];
    [self.viewController removeFromParentViewController];

    self.open = NO;
    [self.delegate popupDidCloseForPresenter:self];
  } else if (popupHasContent && !popupIsOnscreen) {
    // If intrinsic size is nonzero and popup is offscreen, we want to add it.
    UIViewController* parentVC =
        [self.delegate popupParentViewControllerForPresenter:self];
    [parentVC addChildViewController:self.viewController];
    [[self.delegate popupParentViewForPresenter:self]
        addSubview:self.popupContainerView];
    [self.viewController didMoveToParentViewController:parentVC];

    BOOL enableFocusAnimation =
        IsBottomOmniboxAvailable() && isFocusingOmnibox &&
        _unfocusedOmniboxToolbarType == ToolbarType::kSecondary;

    [self initialLayoutAnimated:enableFocusAnimation];

    [self updateBottomConstraints];

    self.open = YES;
    [self.delegate popupDidOpenForPresenter:self];
  }
}

/// With popout omnibox, the popup might be in either of two states:
/// a) regular x regular state, where the popup matches OB width
/// b) compact state, where popup takes whole screen width
/// Therefore, on trait collection change, re-add the popup and recreate the
/// constraints to make sure the correct ones are used.
- (void)updatePopupAfterTraitCollectionChange {
  // Re-add the popup container to break any existing constraints.
  [self.popupContainerView removeFromSuperview];
  [[self.delegate popupParentViewForPresenter:self]
      addSubview:self.popupContainerView];

  // Re-add necessary constraints.
  [self initialLayoutAnimated:NO];
  [self updateBottomConstraints];
}

- (void)updateBottomConstraints {
  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    BOOL showRegularLayout =
        IsRegularXRegularSizeClass(self.popupContainerView.traitCollection);
    self.bottomConstraintPhone.active = !showRegularLayout;
    self.bottomConstraintTablet.active = showRegularLayout;
  } else {
    self.bottomConstraintPhone.active = YES;
    self.bottomSeparator.hidden = NO;
  }
}

#pragma mark - ToolbarOmniboxConsumer

- (void)steadyStateOmniboxMovedToToolbar:(ToolbarType)toolbarType {
  _unfocusedOmniboxToolbarType = toolbarType;
}

#pragma mark - Private

/// Layouts the popup when it is just added to the view hierarchy.
- (void)initialLayoutAnimated:(BOOL)isAnimated {
  UIView* popup = self.popupContainerView;
  // Creates the constraints if the view is newly added to the view hierarchy.

  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    self.bottomConstraintPhone = [popup.superview.safeAreaLayoutGuide
                                      .bottomAnchor
        constraintGreaterThanOrEqualToAnchor:popup.bottomAnchor
                                    constant:
                                        kPopupBottomPaddingTablet +
                                        kSecondaryToolbarWithoutOmniboxHeight];
  } else {
    self.bottomConstraintPhone = [popup.bottomAnchor
        constraintEqualToAnchor:popup.superview.bottomAnchor];
  }

  // On tablet form factor the popup is padded on the bottom to allow the user
  // to defocus the omnibox.
  self.bottomConstraintTablet = [popup.superview.bottomAnchor
      constraintGreaterThanOrEqualToAnchor:popup.bottomAnchor
                                  constant:kPopupBottomPaddingTablet];

  // Install in the superview the guide tracking the top omnibox.
  if (self.topOmniboxGuide) {
    [[popup superview] removeLayoutGuide:self.topOmniboxGuide];
    self.topOmniboxGuide = nil;
  }
  GuideName* omniboxGuideName =
      [self.delegate omniboxGuideNameForPresenter:self];
  if (omniboxGuideName) {
    self.topOmniboxGuide =
        [self.layoutGuideCenter makeLayoutGuideNamed:omniboxGuideName];
    [[popup superview] addLayoutGuide:self.topOmniboxGuide];
  }

  [self updatePopupLayer];
  [self updateConstraints];

  [[popup superview] layoutIfNeeded];

  if (isAnimated) {
    [self animatePopupOnOmniboxFocus];
  }
}

// Updates the popup's view layer.
- (void)updatePopupLayer {
  if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
    return;
  }

  _popupContainerView.layer.masksToBounds = NO;

  BOOL showRegularLayout =
      IsRegularXRegularSizeClass(self.popupContainerView.traitCollection);

  _popupContainerView.layer.cornerRadius = showRegularLayout ? 16 : 0;
  _popupContainerView.layer.shadowColor = UIColor.blackColor.CGColor;
  _popupContainerView.layer.shadowRadius = 60;
  _popupContainerView.layer.shadowOffset = CGSizeMake(0, 10);
  _popupContainerView.layer.shadowOpacity = 0.2;
  self.viewController.view.layer.cornerRadius = showRegularLayout ? 16 : 0;
}

// Updates and activates the constraints based on the popup's current view state
- (void)updateConstraints {
  UIView* popup = self.popupContainerView;

  NSLayoutConstraint* topConstraint;
  if (self.topOmniboxGuide) {
    // Position the top anchor of the popup relatively to that layout guide.
    topConstraint = [popup.topAnchor
        constraintEqualToAnchor:self.topOmniboxGuide.bottomAnchor
                       constant:kVerticalOffset];
  } else {
    topConstraint = [popup.topAnchor
        constraintEqualToAnchor:[self.delegate popupParentViewForPresenter:self]
                                    .topAnchor];
  }

  NSMutableArray<NSLayoutConstraint*>* constraintsToActivate =
      [NSMutableArray arrayWithObject:topConstraint];

  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET &&
      IsRegularXRegularSizeClass(self.popupContainerView.traitCollection) &&
      self.topOmniboxGuide) {
    NSLayoutConstraint* leadingConstraint = [popup.leadingAnchor
        constraintEqualToAnchor:self.topOmniboxGuide.leadingAnchor
                       constant:-16];
    leadingConstraint.priority = UILayoutPriorityDefaultHigh;

    NSLayoutConstraint* trailingConstraint = [popup.trailingAnchor
        constraintEqualToAnchor:self.topOmniboxGuide.trailingAnchor
                       constant:16];
    trailingConstraint.priority = UILayoutPriorityDefaultHigh;

    NSLayoutConstraint* centerXConstraint = [popup.centerXAnchor
        constraintEqualToAnchor:self.topOmniboxGuide.centerXAnchor];

    [constraintsToActivate addObjectsFromArray:@[
      leadingConstraint, trailingConstraint, centerXConstraint
    ]];
  } else {
    [constraintsToActivate addObjectsFromArray:@[
      [popup.leadingAnchor
          constraintEqualToAnchor:popup.superview.leadingAnchor],
      [popup.trailingAnchor
          constraintEqualToAnchor:popup.superview.trailingAnchor],
    ]];
  }

  [NSLayoutConstraint activateConstraints:constraintsToActivate];
}

/// Animates the popup for omnibox focus.
- (void)animatePopupOnOmniboxFocus {
  __weak __typeof__(self) weakSelf = self;
  self.viewController.view.alpha = 0.0;
  self.popupTopConstraint.constant = kFadeAnimationVerticalOffset;
  [self.popupContainerView.superview layoutIfNeeded];

  auto constraintForVisiblePopup = ^{
    weakSelf.viewController.view.alpha = 1.0;
    weakSelf.popupTopConstraint.constant = 0.0;
    [weakSelf.popupContainerView.superview layoutIfNeeded];
  };

  [UIView animateWithDuration:kFadeInAnimationDuration
                   animations:constraintForVisiblePopup
                   completion:^(BOOL _) {
                     constraintForVisiblePopup();
                   }];
}

@end