chromium/ios/chrome/browser/ui/search_engine_choice/search_engine_choice_view_controller.mm

// Copyright 2023 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/search_engine_choice/search_engine_choice_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/i18n/rtl.h"
#import "base/strings/sys_string_conversions.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/search_engine_choice/search_engine_choice_constants.h"
#import "ios/chrome/browser/ui/search_engine_choice/search_engine_choice_mutator.h"
#import "ios/chrome/browser/ui/search_engine_choice/search_engine_choice_ui_util.h"
#import "ios/chrome/browser/ui/search_engine_choice/snippet_search_engine_button.h"
#import "ios/chrome/browser/ui/search_engine_choice/snippet_search_engine_element.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/promo_style/constants.h"
#import "ios/chrome/common/ui/promo_style/utils.h"
#import "ios/chrome/common/ui/util/button_util.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/device_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

namespace {

// Space between the Chrome logo and the top of the screen.
constexpr CGFloat kLogoTopMargin = 24.;
// Logo dimensions.
constexpr CGFloat kLogoSize = 40.;
// Margin between the logo and the title.
constexpr CGFloat kLogoTitleMargin = 16.;
// Margin between the title and the subtitle.
constexpr CGFloat kTitleSubtitleMargin = 8.;
// Margin between the subtitle and search engine stack view.
constexpr CGFloat kSubtitleSearchEngineStackMargin = 20.;
// Margin above "Set as Default" button.
// This margin needs to be used for inline and floating buttons, to make sure
// both containers have the same size. Having the same size is required to have
// a smooth transition from inline to floating SetAsDefault button.
constexpr CGFloat kSetAsDefaultButtonTopMargin = 16.;
// Corner radius for the "More" pill button.
constexpr CGFloat kMorePillButtonCornerRadius = 25.;
// Horizontal padding for the "More" pill button.
constexpr CGFloat kMorePillButtonHorizontalPadding = 15.;
// Vertical padding for the "More" pill button.
constexpr CGFloat kMorePillButtonVerticalPadding = 17.;
// The margin between the text and the arrow on the "More" pill button.
constexpr CGFloat kMoreArrowMargin = 4.;
// Animation duration when the floating SetAsDefault button appears.
constexpr NSTimeInterval kFloatingSetAsDefaultAnimationDuration = .3;
// Height of the separator shown in the floating container.
constexpr CGFloat kFloatingContainerSeparatorHeight = 1.;
// Animation duration for the more pill button to move away from the bottom of
// the screen.
constexpr CGFloat kMorePillButtonAnimationDuration = .1;
// URL for the "Learn more" link.
const char* const kLearnMoreURL = "internal://choice-screen-learn-more";

SnippetSearchEngineButton* CreateSnippetSearchEngineButtonWithElement(
    SnippetSearchEngineElement* element) {
  CHECK(element.keyword);
  SnippetSearchEngineButton* button = [[SnippetSearchEngineButton alloc] init];
  button.faviconImage = element.faviconImage;
  button.searchEngineName = element.name;
  button.snippetText = element.snippetDescription;
  button.translatesAutoresizingMaskIntoConstraints = NO;
  button.searchEngineKeyword = element.keyword;
  return button;
}

// Set the tile for a pill button, with its down arrow.
void SetPillButtonTitle(UIButton* pill_button, int string_id) {
  UIFont* font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
  NSDictionary* textAttributes = @{NSFontAttributeName : font};
  NSMutableAttributedString* attributedString =
      [[NSMutableAttributedString alloc]
          initWithString:l10n_util::GetNSString(string_id)
              attributes:textAttributes];
  // Use `ceilf()` when calculating the icon's bounds to ensure the
  // button's content height does not shrink by fractional points, as the
  // attributed string's actual height is slightly smaller than the
  // assigned height.
  NSTextAttachment* attachment = [[NSTextAttachment alloc] init];
  attachment.image = [[UIImage imageNamed:@"read_more_arrow"]
      imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  CGFloat height = ceilf(attributedString.size.height);
  CGFloat capHeight = ceilf(font.capHeight);
  CGFloat horizontalOffset =
      base::i18n::IsRTL() ? -1.f * kMoreArrowMargin : kMoreArrowMargin;
  CGFloat verticalOffset = (capHeight - height) / 2.f;
  attachment.bounds =
      CGRectMake(horizontalOffset, verticalOffset, height, height);
  [attributedString
      appendAttributedString:[NSAttributedString
                                 attributedStringWithAttachment:attachment]];

  UIButtonConfiguration* buttonConfiguration = pill_button.configuration;
  buttonConfiguration.attributedTitle = attributedString;
  pill_button.configuration = buttonConfiguration;
}

// Configures a "Set as Default" button to be enabled or disabled.
void EnableSetAsDefaultButton(UIButton* button, BOOL is_enabled) {
  UIButtonConfiguration* button_configuration = button.configuration;
  if (is_enabled) {
    button_configuration.background.backgroundColor =
        [UIColor colorNamed:kBlueColor];
    button_configuration.baseForegroundColor =
        [UIColor colorNamed:kSolidButtonTextColor];
    button.accessibilityHint = nil;
  } else {
    button_configuration.background.backgroundColor =
        [UIColor colorNamed:kTertiaryBackgroundColor];
    button_configuration.baseForegroundColor =
        [UIColor colorNamed:kDisabledTintColor];
    button.accessibilityHint =
        l10n_util::GetNSString(IDS_SEARCH_ENGINE_CHOICE_DEFAULT_HINT);
  }
  button.configuration = button_configuration;
  button.enabled = is_enabled;
}

// Creates a "Set as Default" button. The button is returned as disabled.
UIButton* CreateSetAsDefaultButton() {
  UIButton* button = PrimaryActionButton(/*pointer_interaction_enabled=*/YES);
  SetConfigurationTitle(
      button, l10n_util::GetNSString(IDS_SEARCH_ENGINE_CHOICE_BUTTON_TITLE));
  button.translatesAutoresizingMaskIntoConstraints = NO;
  // Add semantic group, so the user can skip all the search engine stack view,
  // and jump to the SetAsDefault button, using VoiceOver.
  button.accessibilityContainerType = UIAccessibilityContainerTypeSemanticGroup;
  EnableSetAsDefaultButton(button, /*is_enabled=*/NO);
  return button;
}

// Create a more pill button.
UIButton* CreateMorePillButton() {
  UIButton* morePillButton =
      PrimaryActionButton(/*pointer_interaction_enabled=*/YES);
  morePillButton.layer.cornerRadius = kMorePillButtonCornerRadius;
  morePillButton.layer.masksToBounds = YES;
  UIButtonConfiguration* configuration = morePillButton.configuration;
  configuration.contentInsets = NSDirectionalEdgeInsetsMake(
      kMorePillButtonHorizontalPadding, kMorePillButtonVerticalPadding,
      kMorePillButtonHorizontalPadding, kMorePillButtonVerticalPadding);
  morePillButton.configuration = configuration;
  SetPillButtonTitle(morePillButton, IDS_SEARCH_ENGINE_CHOICE_MORE_BUTTON);
  morePillButton.accessibilityContainerType =
      UIAccessibilityContainerTypeSemanticGroup;
  return morePillButton;
}

// Returns the `y` value from the `localReference` in the coordinator of
// `mainView`.
CGFloat ConvertVerticalCoordonateWithMainViewReference(UIView* mainView,
                                                       UIView* referenceView,
                                                       CGFloat y) {
  CGPoint point = CGPointMake(0, y);
  CGPoint pointWithMainViewReference = [mainView convertPoint:point
                                                     fromView:referenceView];
  return pointWithMainViewReference.y;
}

}  // namespace

@interface SearchEngineChoiceViewController () <UITextViewDelegate>
@end

@implementation SearchEngineChoiceViewController {
  // The view title.
  UILabel* _titleLabel;
  // Scroll view that contains the logo, the title, the subtitle,
  // the search engine list, and the inline SetAsDefault button.
  UIScrollView* _scrollView;
  // Contains the list of search engine buttons.
  UIStackView* _searchEngineStackView;
  // Button floating on top of the scroll view to scroll down to the bottom.
  // If the user already scroll onces to the button, the button will be hidden.
  // By default the title is "More". As soon as the user selects a search engine
  // the title is changed to "Continue" (the button action is the same).
  UIButton* _moreOrContinueButton;
  // Container to display the "Set as Default" button in the scroll view.
  // Related to `_inlineSetAsDefaultButton`. This container is used in
  // the animation to transition to `_floatingSetAsDefaultButtonContainer`.
  // This container needs to have the same size than
  // `_floatingSetAsDefaultButtonContainer`, to have a smooth transition to the
  // floating SetAsDefault button.
  UIView* _inlineSetAsDefaultButtonContainer;
  // Button to confirm the default search engine selection. This button is
  // visually identical to `_floatingSetAsDefaultButton` but it is part of
  // `_inlineSetAsDefaultButtonContainer`.
  UIButton* _inlineSetAsDefaultButton;
  // Container to display the "Set as Default" button on top of the scroll view.
  // Related to `_floatingSetAsDefaultButton`.
  // This container needs to have the same size than
  // `_inlineSetAsDefaultButtonContainer`, to have a smooth transition from the
  // inline SetAsDefault button.
  UIView* _floatingSetAsDefaultButtonContainer;
  // Horizontal separator at the top of `_floatingSetAsDefaultButtonContainer`.
  // It should be visible only when `_floatingSetAsDefaultButtonContainer` is
  // covering `_searchEngineStackView`.
  UIView* _floatingContainerSeparator;
  // Button to confirm the default search engine selection. This button is
  // visually identical to `_inlineSetAsDefaultButton` but it is inside
  // `_floatingSetAsDefaultButtonContainer`.
  UIButton* _floatingSetAsDefaultButton;
  // Whether the choice screen is being displayed for the FRE.
  BOOL _isForFRE;
  // YES, when showing the floating button and hidding the inline button.
  // NO, when showing the inline button and hidding the floating button.
  BOOL _showFloatingSetAsDefaultButton;
  // Contains the selected search engine button.
  SnippetSearchEngineButton* _selectedSearchEngineButton;
  // Whether `-[SearchEngineChoiceViewController viewIsAppearing:]` was called.
  BOOL _viewIsAppearingCalled;
  // Whether the search engine buttons have been loaded in the stack view.
  BOOL _searchEnginesLoaded;
}

@synthesize searchEngines = _searchEngines;

- (instancetype)initWithFirstRunMode:(BOOL)isForFRE {
  self = [super initWithNibName:nil bundle:nil];
  if (self) {
    _isForFRE = isForFRE;
  }
  return self;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  UIView* view = self.view;
  view.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];

  // Add main scroll view with its content view.
  UIView* scrollContentView = [[UIView alloc] init];
  scrollContentView.translatesAutoresizingMaskIntoConstraints = NO;
  _scrollView = [[UIScrollView alloc] init];
  [view addSubview:_scrollView];
  _scrollView.translatesAutoresizingMaskIntoConstraints = NO;
  _scrollView.accessibilityIdentifier = kSearchEngineChoiceScrollViewIdentifier;
  _scrollView.delegate = self;
  _scrollView.contentInsetAdjustmentBehavior =
      UIScrollViewContentInsetAdjustmentNever;
  [_scrollView addSubview:scrollContentView];

  // Add logo image.
  // Need to use a regular png instead of custom symbol to have a better control
  // on the size and the margin of the logo.
#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
  UIImage* logoImage = [UIImage imageNamed:kChromeSearchEngineChoiceIcon];
#else
  UIImage* logoImage = [UIImage imageNamed:kChromiumSearchEngineChoiceIcon];
#endif
  UIImageView* logoImageView = [[UIImageView alloc] initWithImage:logoImage];
  [scrollContentView addSubview:logoImageView];
  logoImageView.translatesAutoresizingMaskIntoConstraints = NO;

  // Add view title.
  _titleLabel = [[UILabel alloc] init];
  // Add semantic group, so the user can skip all the search engine stack view,
  // and jump to the SetAsDefault button, using VoiceOver.
  _titleLabel.accessibilityContainerType =
      UIAccessibilityContainerTypeSemanticGroup;
  [scrollContentView addSubview:_titleLabel];
  [_titleLabel
      setText:l10n_util::GetNSString(IDS_SEARCH_ENGINE_CHOICE_PAGE_TITLE)];
  [_titleLabel setTextColor:[UIColor colorNamed:kSolidBlackColor]];
  UIFontTextStyle textStyle = GetTitleLabelFontTextStyle(self);
  _titleLabel.font = GetFRETitleFont(textStyle);
  _titleLabel.adjustsFontForContentSizeCategory = YES;
  [_titleLabel setTextAlignment:NSTextAlignmentCenter];
  [_titleLabel setNumberOfLines:0];
  [_titleLabel setAccessibilityIdentifier:
                   kSearchEngineChoiceTitleAccessibilityIdentifier];
  _titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader;
  _titleLabel.translatesAutoresizingMaskIntoConstraints = NO;

  // Add view subtitle.
  NSMutableAttributedString* subtitleText = [[NSMutableAttributedString alloc]
      initWithString:[l10n_util::GetNSString(
                         IDS_SEARCH_ENGINE_CHOICE_PAGE_SUBTITLE)
                         stringByAppendingString:@" "]
          attributes:@{
            NSForegroundColorAttributeName : [UIColor colorNamed:kGrey800Color]
          }];
  NSAttributedString* learnMoreAttributedString =
      [[NSMutableAttributedString alloc]
          initWithString:l10n_util::GetNSString(
                             IDS_SEARCH_ENGINE_CHOICE_PAGE_SUBTITLE_INFO_LINK)
              attributes:@{
                NSForegroundColorAttributeName :
                    [UIColor colorNamed:kBlueColor],
                NSLinkAttributeName : net::NSURLWithGURL(GURL(kLearnMoreURL)),
              }];
  learnMoreAttributedString.accessibilityLabel = l10n_util::GetNSString(
      IDS_SEARCH_ENGINE_CHOICE_PAGE_SUBTITLE_INFO_LINK_A11Y_LABEL);
  [subtitleText appendAttributedString:learnMoreAttributedString];
  UITextView* subtitleTextView = [[UITextView alloc] init];
  [scrollContentView addSubview:subtitleTextView];
  [subtitleTextView setAttributedText:subtitleText];
  [subtitleTextView
      setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]];
  subtitleTextView.backgroundColor = nil;
  subtitleTextView.adjustsFontForContentSizeCategory = YES;
  [subtitleTextView setTextAlignment:NSTextAlignmentCenter];
  subtitleTextView.delegate = self;
  // Disable and hide scrollbar.
  subtitleTextView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0);
  subtitleTextView.scrollEnabled = NO;
  subtitleTextView.showsVerticalScrollIndicator = NO;
  subtitleTextView.showsHorizontalScrollIndicator = NO;
  subtitleTextView.editable = NO;
  subtitleTextView.translatesAutoresizingMaskIntoConstraints = NO;

  // Add stack view for the search engine buttons.
  _searchEngineStackView = [[UIStackView alloc] init];
  // Add semantic group, so the user can skip all the search engine stack view,
  // and jump to the SetAsDefault button, using VoiceOver.
  _searchEngineStackView.accessibilityContainerType =
      UIAccessibilityContainerTypeSemanticGroup;
  _searchEngineStackView.backgroundColor =
      [UIColor colorNamed:kSecondaryBackgroundColor];
  _searchEngineStackView.layer.cornerRadius = 12.;
  _searchEngineStackView.layer.masksToBounds = YES;
  _searchEngineStackView.translatesAutoresizingMaskIntoConstraints = NO;
  _searchEngineStackView.axis = UILayoutConstraintAxisVertical;
  [scrollContentView addSubview:_searchEngineStackView];

  // Add inline "Set as Default" button container.
  _inlineSetAsDefaultButtonContainer = [[UIView alloc] init];
  _inlineSetAsDefaultButtonContainer.translatesAutoresizingMaskIntoConstraints =
      NO;
  [scrollContentView addSubview:_inlineSetAsDefaultButtonContainer];

  // Add inline "Set as Default" button.
  _inlineSetAsDefaultButton = CreateSetAsDefaultButton();
  [_inlineSetAsDefaultButtonContainer addSubview:_inlineSetAsDefaultButton];
  [_inlineSetAsDefaultButton addTarget:self
                                action:@selector(setAsDefaultButtonAction)
                      forControlEvents:UIControlEventTouchUpInside];

  // Add floating "Set as Default" button container.
  _floatingSetAsDefaultButtonContainer = [[UIView alloc] init];
  _floatingSetAsDefaultButtonContainer
      .translatesAutoresizingMaskIntoConstraints = NO;
  [view addSubview:_floatingSetAsDefaultButtonContainer];
  _floatingSetAsDefaultButtonContainer.hidden = YES;
  _floatingSetAsDefaultButtonContainer.backgroundColor = view.backgroundColor;

  // Add separator at the top of the floating container.
  _floatingContainerSeparator = [[UIView alloc] init];
  _floatingContainerSeparator.translatesAutoresizingMaskIntoConstraints = NO;
  [_floatingSetAsDefaultButtonContainer addSubview:_floatingContainerSeparator];
  _floatingContainerSeparator.backgroundColor =
      [UIColor colorNamed:kSeparatorColor];

  // Add floating "Set as Default" button.
  _floatingSetAsDefaultButton = CreateSetAsDefaultButton();
  _floatingSetAsDefaultButton.translatesAutoresizingMaskIntoConstraints = NO;
  [_floatingSetAsDefaultButtonContainer addSubview:_floatingSetAsDefaultButton];
  _floatingSetAsDefaultButton.accessibilityIdentifier =
      kSetAsDefaultSearchEngineIdentifier;
  _floatingSetAsDefaultButtonContainer.accessibilityContainerType =
      UIAccessibilityContainerTypeSemanticGroup;
  [_floatingSetAsDefaultButton addTarget:self
                                  action:@selector(setAsDefaultButtonAction)
                        forControlEvents:UIControlEventTouchUpInside];

  // Add "More" pill button.
  // Needs to be the last element added to the view, so it is always above all
  // other elements.
  _moreOrContinueButton = CreateMorePillButton();
  _moreOrContinueButton.translatesAutoresizingMaskIntoConstraints = NO;
  [view addSubview:_moreOrContinueButton];
  _moreOrContinueButton.accessibilityIdentifier =
      kSearchEngineMoreButtonIdentifier;
  [_moreOrContinueButton addTarget:self
                            action:@selector(moreButtonAction)
                  forControlEvents:UIControlEventTouchUpInside];

  // Create a layout guide to constrain the width of the content, while still
  // allowing the scroll view to take the full screen width.
  UILayoutGuide* widthLayoutGuide = AddPromoStyleWidthLayoutGuide(view);
  // This is the layout guide to compute the bottom margin of the "Set as
  // Default" button.
  UILayoutGuide* buttonBottomMargin = [[UILayoutGuide alloc] init];
  [view addLayoutGuide:buttonBottomMargin];
  // This layout guide is to map `buttonBottomMargin` height into the inline
  // "Set as Default" button container.
  UILayoutGuide* inlineContainerButtonBottomMargin =
      [[UILayoutGuide alloc] init];
  [_inlineSetAsDefaultButtonContainer
      addLayoutGuide:inlineContainerButtonBottomMargin];

  [NSLayoutConstraint activateConstraints:@[
    // Scroll view constraints. It needs to be the full size of the view,
    // so the content is visible in the safe area too.
    [_scrollView.topAnchor constraintEqualToAnchor:view.topAnchor],
    [_scrollView.widthAnchor constraintEqualToAnchor:view.widthAnchor],
    [_scrollView.centerXAnchor constraintEqualToAnchor:view.centerXAnchor],
    [_scrollView.bottomAnchor constraintEqualToAnchor:view.bottomAnchor],

    // Scroll content view constraints.
    [scrollContentView.topAnchor
        constraintEqualToAnchor:_scrollView.contentLayoutGuide.topAnchor],
    [scrollContentView.bottomAnchor
        constraintEqualToAnchor:_scrollView.contentLayoutGuide.bottomAnchor],
    [scrollContentView.heightAnchor
        constraintGreaterThanOrEqualToAnchor:_scrollView.safeAreaLayoutGuide
                                                 .heightAnchor],
    [scrollContentView.centerXAnchor
        constraintEqualToAnchor:_scrollView.centerXAnchor],
    [scrollContentView.widthAnchor
        constraintEqualToAnchor:widthLayoutGuide.widthAnchor],

    // Logo constraints.
    [logoImageView.topAnchor
        constraintEqualToAnchor:scrollContentView.safeAreaLayoutGuide.topAnchor
                       constant:kLogoTopMargin],
    [logoImageView.heightAnchor constraintEqualToConstant:kLogoSize],
    [logoImageView.centerXAnchor
        constraintEqualToAnchor:scrollContentView.centerXAnchor],
    [logoImageView.widthAnchor constraintEqualToConstant:kLogoSize],

    // Title constraints.
    [_titleLabel.topAnchor constraintEqualToAnchor:logoImageView.bottomAnchor
                                          constant:kLogoTitleMargin],
    [_titleLabel.leadingAnchor
        constraintEqualToAnchor:_searchEngineStackView.leadingAnchor],
    [_titleLabel.trailingAnchor
        constraintEqualToAnchor:_searchEngineStackView.trailingAnchor],

    // SubtitleTextView constraints.
    [subtitleTextView.topAnchor constraintEqualToAnchor:_titleLabel.bottomAnchor
                                               constant:kTitleSubtitleMargin],
    [subtitleTextView.leadingAnchor
        constraintEqualToAnchor:_searchEngineStackView.leadingAnchor],
    [subtitleTextView.trailingAnchor
        constraintEqualToAnchor:_searchEngineStackView.trailingAnchor],

    // Search engine stack view constraints.
    [_searchEngineStackView.topAnchor
        constraintEqualToAnchor:subtitleTextView.bottomAnchor
                       constant:kSubtitleSearchEngineStackMargin],
    [_searchEngineStackView.leadingAnchor
        constraintEqualToAnchor:scrollContentView.leadingAnchor],
    [_searchEngineStackView.trailingAnchor
        constraintEqualToAnchor:scrollContentView.trailingAnchor],

    // Button bottom margin constraints.
    [buttonBottomMargin.bottomAnchor constraintEqualToAnchor:view.bottomAnchor],
    [buttonBottomMargin.topAnchor
        constraintLessThanOrEqualToAnchor:view.safeAreaLayoutGuide.bottomAnchor
                                 constant:-kActionsBottomMarginWithSafeArea],
    [buttonBottomMargin.topAnchor
        constraintLessThanOrEqualToAnchor:view.bottomAnchor
                                 constant:-kActionsBottomMarginWithoutSafeArea],

    // _inlineSetAsDefaultButtonContainer constraints.
    [_inlineSetAsDefaultButtonContainer.topAnchor
        constraintGreaterThanOrEqualToAnchor:_searchEngineStackView
                                                 .bottomAnchor],
    [_inlineSetAsDefaultButtonContainer.leadingAnchor
        constraintEqualToAnchor:_searchEngineStackView.leadingAnchor],
    [_inlineSetAsDefaultButtonContainer.trailingAnchor
        constraintEqualToAnchor:_searchEngineStackView.trailingAnchor],
    [_inlineSetAsDefaultButtonContainer.bottomAnchor
        constraintEqualToAnchor:scrollContentView.bottomAnchor],

    // inlineContainerButtonBottomMargin constraints.
    [inlineContainerButtonBottomMargin.bottomAnchor
        constraintEqualToAnchor:_inlineSetAsDefaultButtonContainer
                                    .bottomAnchor],
    [inlineContainerButtonBottomMargin.heightAnchor
        constraintEqualToAnchor:buttonBottomMargin.heightAnchor],

    // _inlineSetAsDefaultButton constraints.
    [_inlineSetAsDefaultButton.topAnchor
        constraintEqualToAnchor:_inlineSetAsDefaultButtonContainer.topAnchor
                       constant:kSetAsDefaultButtonTopMargin],
    [_inlineSetAsDefaultButton.bottomAnchor
        constraintEqualToAnchor:inlineContainerButtonBottomMargin.topAnchor],
    [_inlineSetAsDefaultButton.bottomAnchor
        constraintLessThanOrEqualToAnchor:_inlineSetAsDefaultButtonContainer
                                              .bottomAnchor],
    [_inlineSetAsDefaultButton.widthAnchor
        constraintEqualToAnchor:_searchEngineStackView.widthAnchor],
    [_inlineSetAsDefaultButton.centerXAnchor
        constraintEqualToAnchor:_searchEngineStackView.centerXAnchor],

    // More pill button constraints.
    [_moreOrContinueButton.bottomAnchor
        constraintEqualToAnchor:buttonBottomMargin.topAnchor],
    [_moreOrContinueButton.centerXAnchor
        constraintEqualToAnchor:view.centerXAnchor],

    // _floatingSetAsDefaultButtonContainer constraints.
    [_floatingSetAsDefaultButtonContainer.bottomAnchor
        constraintEqualToAnchor:view.bottomAnchor],
    // It needs to be as large as the screen so the separator can be as large
    // as the screen.
    [_floatingSetAsDefaultButtonContainer.leadingAnchor
        constraintEqualToAnchor:view.leadingAnchor],
    [_floatingSetAsDefaultButtonContainer.trailingAnchor
        constraintEqualToAnchor:view.trailingAnchor],

    // _floatingContainerSeparator constraints.
    [_floatingContainerSeparator.topAnchor
        constraintEqualToAnchor:_floatingSetAsDefaultButtonContainer.topAnchor],
    [_floatingContainerSeparator.leadingAnchor
        constraintEqualToAnchor:_floatingSetAsDefaultButtonContainer
                                    .leadingAnchor],
    [_floatingContainerSeparator.trailingAnchor
        constraintEqualToAnchor:_floatingSetAsDefaultButtonContainer
                                    .trailingAnchor],
    [_floatingContainerSeparator.heightAnchor
        constraintEqualToConstant:kFloatingContainerSeparatorHeight],

    // _floatingSetAsDefaultButton constraints.
    [_floatingSetAsDefaultButton.topAnchor
        constraintEqualToAnchor:_floatingSetAsDefaultButtonContainer.topAnchor
                       constant:kSetAsDefaultButtonTopMargin],
    [_floatingSetAsDefaultButton.bottomAnchor
        constraintEqualToAnchor:buttonBottomMargin.topAnchor],
    [_floatingSetAsDefaultButton.widthAnchor
        constraintEqualToAnchor:_searchEngineStackView.widthAnchor],
    [_floatingSetAsDefaultButton.centerXAnchor
        constraintEqualToAnchor:_searchEngineStackView.centerXAnchor],
  ]];
  // No need to update the more and SetAsDefault buttons. They will be updated
  // when the view will be appearing.
  [self loadSearchEngineButtons];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(accessibilityElementFocusedNotification:)
             name:UIAccessibilityElementFocusedNotification
           object:nil];
}

- (void)viewIsAppearing:(BOOL)animated {
  [super viewIsAppearing:animated];
  // Using -[UIViewController viewWillAppear:] is too early. There is an issue
  // on iPhone, the safe area is not visible yet.
  // Using -[UIViewController viewDidAppear:] is too late. There is an issue on
  // iPad, the More button appears and then disappears.
  [self.view layoutIfNeeded];
  // After the last layout before appearing, now, the views can be updated.
  _viewIsAppearingCalled = YES;
  [self updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:NO];
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
  [self updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:YES];
}

#pragma mark - SearchEngineChoiceTableConsumer

- (void)setSearchEngines:(NSArray<SnippetSearchEngineElement*>*)searchEngines {
  _searchEngines = searchEngines;
  [self loadSearchEngineButtons];
}

#pragma mark - UITraitEnvironment

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  // Reset the title font to make sure that it is
  // properly scaled.
  UIFontTextStyle textStyle = GetTitleLabelFontTextStyle(self);
  _titleLabel.font = GetFRETitleFont(textStyle);
  // Update the SetAsDefault button once the layout changes take effect to have
  // the right measurements to evaluate the scroll position.
  __weak __typeof(self) weakSelf = self;
  dispatch_async(dispatch_get_main_queue(), ^{
    [weakSelf updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:YES];
  });
  // Adjust the inset vertical scroller since floating container size might have
  // be updated.
  [self adjustInsetVerticalScroller];
}

#pragma mark - Private

// Called when the tap on a SnippetSearchEngineButton.
- (void)searchEngineTapAction:(SnippetSearchEngineButton*)button {
  BOOL wasSelectedYet = _selectedSearchEngineButton != nil;
  [self.mutator selectSearchEnginewWithKeyword:button.searchEngineKeyword];
  _selectedSearchEngineButton.checked = NO;
  _selectedSearchEngineButton = button;
  _selectedSearchEngineButton.checked = YES;
  if (wasSelectedYet) {
    return;
  }
  EnableSetAsDefaultButton(_inlineSetAsDefaultButton, /*is_enabled=*/YES);
  EnableSetAsDefaultButton(_floatingSetAsDefaultButton, /*is_enabled=*/YES);
  if (!_moreOrContinueButton) {
    // If the more pill button is not visible, the user already saw the last
    // search engine, and since they selected one, then the "Set as Default"
    // button can appear now.
    [self animateFloatingSetAsDefaultContainer];
  } else {
    // After selecting a search engine, needs to scroll down to see all
    // search engines before tapping on the "Set as Default" button.
    SetPillButtonTitle(_moreOrContinueButton,
                       IDS_SEARCH_ENGINE_CHOICE_CONTINUE_BUTTON);
    _moreOrContinueButton.accessibilityIdentifier =
        kSearchEngineContinueButtonIdentifier;
  }
}

// Animates the floating SetAsDefault button to:
//  1- Fades from grey to blue, to become enabled.
//  2- Appears on the screen by moving from the bottom (if the floating
//     SetAsDefault is not visible yet).
//  3- Scrolls up the scrollview to avoid covering the selected search engine.
- (void)animateFloatingSetAsDefaultContainer {
  CHECK(!_moreOrContinueButton, base::NotFatalUntil::M127);

  // 1- Fades grey color to blue color to have better animation.
  UIButton* fakeButtonForGreyToBlueFading = nil;
  CGPoint inlineButtonOriginInMainView =
      [self.view convertPoint:_inlineSetAsDefaultButton.bounds.origin
                     fromView:_inlineSetAsDefaultButton];
  if (inlineButtonOriginInMainView.y < self.view.bounds.size.height ||
      !_floatingSetAsDefaultButtonContainer.hidden) {
    // When the inline button is visible, a fake Set as Default button is added
    // in the floating container. The fake button is as the same color than
    // the inline button.
    // The fake button is faded out at the same time than the floating container
    // is moved up.
    fakeButtonForGreyToBlueFading = CreateSetAsDefaultButton();
    fakeButtonForGreyToBlueFading.translatesAutoresizingMaskIntoConstraints =
        YES;
    fakeButtonForGreyToBlueFading.frame = _floatingSetAsDefaultButton.frame;
    [_floatingSetAsDefaultButtonContainer
        addSubview:fakeButtonForGreyToBlueFading];
  }
  // Hide the inline SetAsDefault button. It is replace by
  // `fakeButtonForGreyToBlueFading` during the animation.
  _inlineSetAsDefaultButtonContainer.hidden = YES;

  // 2- Sets the starting point of the floating SetAsDefault container to move
  //    up. The starting point should be the position of the inline SetAsDefault
  //    container.
  //    If the floating SetAsDefault container is already visible, there is
  //    nothing to do.
  //    If the inline SetAsDefault container is not visible, the starting point
  //    is the bottom of the view.
  // Rect of the floating container at the end of the animation.
  CGRect animationEndFrame = _floatingSetAsDefaultButtonContainer.frame;
  // Computes and sets the origin of the animation based on the inline
  // container.
  // Rect of the floating container at the beginning of the animation.
  CGRect animationStartFrame = animationEndFrame;
  if (_floatingSetAsDefaultButtonContainer.hidden) {
    // The origin point for the animation should be the origin of the inline
    // container.
    CGPoint animationStartOriginPoint =
        [self.view convertPoint:_inlineSetAsDefaultButtonContainer.bounds.origin
                       fromView:_inlineSetAsDefaultButtonContainer];
    if (animationStartOriginPoint.y > self.view.bounds.size.height) {
      // If the inline container is below the bottom of the view, then
      // the floating container should start at the bottom of the view.
      animationStartOriginPoint.y = self.view.frame.size.height;
    }
    animationStartFrame.origin.y = animationStartOriginPoint.y;
    if (UIAccessibilityPrefersCrossFadeTransitions()) {
      // `_floatingSetAsDefaultButtonContainer` should not appear with a
      // transition, but with a fade in.
      // `animationStartFrame` is unchanged to be able to compute
      // `heightToScrollUp` value.
      _floatingSetAsDefaultButtonContainer.hidden = NO;
      _floatingSetAsDefaultButtonContainer.alpha = 0.;
    } else {
      _floatingSetAsDefaultButtonContainer.frame = animationStartFrame;
    }
    [self makeFloatingSetAsDefaultButtonContainerVisible];
  }

  // 3- At the end of the animation, if the floating SetAsDefault container
  //    will cover the selected search engine, the scroll view needs to move up
  //    as much as the floating SetAsDefault container will move up.
  CGRect selectedButtonRect = _selectedSearchEngineButton.bounds;
  selectedButtonRect = [self.view convertRect:selectedButtonRect
                                     fromView:_selectedSearchEngineButton];
  CGFloat heightToScrollUp = 0.;
  // Tests if the floating SetAsDefault button will cover the selected search
  // engine button, after the animation.
  // If this is true, then scroll view needs to scroll up to compensate
  // the floating SetAsDefault button animation. This value is
  // the begining position height minus the end position height.
  // So the scrollview will move exactly at the same time than the button.
  if (selectedButtonRect.origin.y + selectedButtonRect.size.height >
      animationEndFrame.origin.y) {
    heightToScrollUp =
        animationEndFrame.origin.y - animationStartFrame.origin.y;
  }

  // Animates everything.
  UIView* floatingSetAsDefaultButtonContainer =
      _floatingSetAsDefaultButtonContainer;
  UIScrollView* scrollView = _scrollView;
  [UIView animateWithDuration:kFloatingSetAsDefaultAnimationDuration
      animations:^{
        // 1- Fades in.
        fakeButtonForGreyToBlueFading.alpha = 0;
        // 2- Moves from the bottom or fade in.
        floatingSetAsDefaultButtonContainer.frame = animationEndFrame;
        floatingSetAsDefaultButtonContainer.alpha = 1.;
        // 3- Scrolls up, if needed.
        if (heightToScrollUp) {
          CGPoint contentOffset = scrollView.contentOffset;
          contentOffset.y -= heightToScrollUp;
          scrollView.contentOffset = contentOffset;
        }
      }
      completion:^(BOOL) {
        [fakeButtonForGreyToBlueFading removeFromSuperview];
      }];
}

// Called when the user taps on the SetAsDefault button.
- (void)setAsDefaultButtonAction {
  [self.actionDelegate didTapPrimaryButton];
}

// Called when the user taps on the more/continue pill button.
- (void)moreButtonAction {
  [self animateMorePillButtonAway];
  // Adding 1 to the content offset to make sure the scroll view will reach
  // the bottom of view to trigger the floating SetAsDefault container when
  // `updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:` will be
  // called.
  // See crbug.com/332719699.
  CGPoint bottomOffset = CGPointMake(
      0, _scrollView.contentSize.height - _scrollView.bounds.size.height +
             _scrollView.adjustedContentInset.bottom + 1);
  [_scrollView setContentOffset:bottomOffset animated:YES];
}

// Loads the search engine buttons from `_searchEngines`.
- (void)loadSearchEngineButtons {
  NSString* selectedSearchEngineKeyword =
      _selectedSearchEngineButton.searchEngineKeyword;
  _selectedSearchEngineButton = nil;
  // This set saves the list of search engines that are expanded to keep them
  // expanded after loading the search engine list.
  NSMutableSet<NSString*>* expandedSearchEngineKeyword = [NSMutableSet set];
  for (SnippetSearchEngineButton* oldSearchEngineButton in
           _searchEngineStackView.arrangedSubviews) {
    if (oldSearchEngineButton.snippetButtonState ==
        SnippetButtonState::kExpanded) {
      [expandedSearchEngineKeyword
          addObject:oldSearchEngineButton.searchEngineKeyword];
    }
    [_searchEngineStackView removeArrangedSubview:oldSearchEngineButton];
    [oldSearchEngineButton removeFromSuperview];
  }
  SnippetSearchEngineButton* button = nil;
  for (SnippetSearchEngineElement* element in _searchEngines) {
    button = CreateSnippetSearchEngineButtonWithElement(element);
    button.animatedLayoutView = _scrollView;
    if ([expandedSearchEngineKeyword containsObject:element.keyword]) {
      button.snippetButtonState = SnippetButtonState::kExpanded;
    }
    if ([selectedSearchEngineKeyword isEqualToString:element.keyword]) {
      button.checked = YES;
      _selectedSearchEngineButton = button;
    }
    [button addTarget:self
                  action:@selector(searchEngineTapAction:)
        forControlEvents:UIControlEventTouchUpInside];
    [_searchEngineStackView addArrangedSubview:button];
  }
  // Hide the horizontal seperator for the last button.
  button.horizontalSeparatorHidden = YES;
  _searchEnginesLoaded = YES;
  [self.view layoutIfNeeded];
  [self updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:YES];
}

// Updates views:
// 1- The horizontal separator in the floating SetAsDefault is visible only
//    when the floating SetAsDefault container is on top of the search engine
//    stack view
// 2- If the scroll view reaches the end of the last search engine button
//    for the first time, and hides the more button accordingly.
// 3- If the scroll view reaches the bottom, the inline SetAsDefault container
//    is hidden, and the floating SetAsDefault container is visible.
- (void)updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:
    (BOOL)morePillButtonAnimation {
  // 1- Tests if the stack view is covered by the floating SetAsDefault
  //    container, and makes `_floatingContainerSeparator` visible if it is
  //    the case.
  CGPoint stackViewBottomPoint =
      CGPointMake(_searchEngineStackView.frame.origin.x,
                  _searchEngineStackView.bounds.origin.y +
                      _searchEngineStackView.bounds.size.height);
  stackViewBottomPoint = [self.view convertPoint:stackViewBottomPoint
                                        fromView:_searchEngineStackView];
  _floatingContainerSeparator.hidden =
      stackViewBottomPoint.y <
      _floatingSetAsDefaultButtonContainer.frame.origin.y + 1;
  if (!_viewIsAppearingCalled || _showFloatingSetAsDefaultButton ||
      !self.presentingViewController || !_searchEnginesLoaded) {
    // Don't update the value if the view is not ready to appear.
    // Don't update the value if the bottom was reached at least once.
    // Don't update the value if the view is not presented yet.
    // Don't update the value if the search engines have not been loaded yet.
    return;
  }
  CGFloat scrollPosition =
      _scrollView.contentOffset.y + _scrollView.frame.size.height;

  // 2- Hides `_moreOrContinueButton` if the scroll view reaches the end of
  //    the stack view.
  // The limit to remove the more button is when `_searchEngineStackView` is
  // fully visible.
  CGFloat bottomStackViewLimit = _searchEngineStackView.frame.origin.y +
                                 _searchEngineStackView.frame.size.height;
  if (scrollPosition >= bottomStackViewLimit) {
    if (morePillButtonAnimation) {
      [self animateMorePillButtonAway];
    } else {
      [_moreOrContinueButton removeFromSuperview];
      _moreOrContinueButton = nil;
    }
  }

  // 3- Reveals the floating SetAsDefault container, and hides the inline
  //    SetAsDefault container, if the scroll view reaches the bottom.
  CGFloat scrollLimit =
      _scrollView.contentSize.height + _scrollView.adjustedContentInset.bottom;
  if (scrollPosition >= scrollLimit) {
    // Scroll reached the bottom, the inline SetAsDefault button needs to be
    // hidden, and the floating SetAsDefault button needs to be visible.
    _showFloatingSetAsDefaultButton = YES;
    _inlineSetAsDefaultButtonContainer.hidden = YES;
    [self makeFloatingSetAsDefaultButtonContainerVisible];
  }
}

// Makes the floating container visible.
- (void)makeFloatingSetAsDefaultButtonContainerVisible {
  _floatingSetAsDefaultButtonContainer.hidden = NO;
  [self adjustInsetVerticalScroller];
}

// Adjust the vertical scroller inset if the floating container is visible.
- (void)adjustInsetVerticalScroller {
  if (_floatingSetAsDefaultButtonContainer.hidden) {
    return;
  }
  // The bottom inset should not include the safe area height.
  CGFloat bottomInset = _floatingSetAsDefaultButtonContainer.frame.size.height -
                        _scrollView.adjustedContentInset.bottom;
  _scrollView.verticalScrollIndicatorInsets =
      UIEdgeInsetsMake(0, 0, bottomInset, 0);
}

// Animate the more pill button to disappear to the bottom of the screen.
- (void)animateMorePillButtonAway {
  if (!_moreOrContinueButton) {
    return;
  }
  UIButton* button = _moreOrContinueButton;
  _moreOrContinueButton = nil;
  CGAffineTransform transform = button.transform;
  CGFloat translateDistance =
      CGRectGetMaxY(self.view.bounds) - CGRectGetMinY(button.frame);
  transform = CGAffineTransformTranslate(transform, 0, translateDistance);
  [UIView animateWithDuration:kMorePillButtonAnimationDuration
      animations:^{
        if (UIAccessibilityPrefersCrossFadeTransitions()) {
          button.alpha = 0;
        } else {
          button.transform = transform;
        }
      }
      completion:^(BOOL finished) {
        [button removeFromSuperview];
      }];
}

// Scrolls automatically `_scrollView` to make sure the search engine button
// is always fully visible and not hidden by
// `_floatingSetAsDefaultButtonContainer`.
- (void)accessibilityElementFocusedNotification:(NSNotification*)notification {
  CHECK([notification.name
      isEqualToString:UIAccessibilityElementFocusedNotification])
      << base::SysNSStringToUTF8(notification.name);
  id focusedElement = notification.userInfo[UIAccessibilityFocusedElementKey];
  if (!focusedElement ||
      ![focusedElement isKindOfClass:SnippetSearchEngineButton.class] ||
      _floatingSetAsDefaultButtonContainer.hidden) {
    return;
  }
  SnippetSearchEngineButton* searchEngineButton =
      base::apple::ObjCCast<SnippetSearchEngineButton>(focusedElement);
  // Get the bottom of `searchEngineButton` in the reference of `self.view`.
  CGFloat searchEngineButtonBottom =
      ConvertVerticalCoordonateWithMainViewReference(
          self.view, searchEngineButton,
          CGRectGetMaxY(searchEngineButton.bounds));
  // Get the top of `floatingSetAsDefaultContainerTop` in the reference of
  // `self.view`.
  CGFloat floatingSetAsDefaultContainerTop =
      ConvertVerticalCoordonateWithMainViewReference(
          self.view, _floatingSetAsDefaultButtonContainer,
          CGRectGetMinY(_floatingSetAsDefaultButtonContainer.bounds));
  if (searchEngineButtonBottom <= floatingSetAsDefaultContainerTop) {
    // The bottom of `searchEngineButton` is visible, no need to scroll.
    return;
  }
  // `_scrollView` should go down to reveal the bottom of `searchEngineButton`.
  CGFloat distanceToScrollDown =
      searchEngineButtonBottom - floatingSetAsDefaultContainerTop;
  // Get the top of `searchEngineButton` in the reference of `self.view`.
  CGFloat searchEngineButtonTop =
      ConvertVerticalCoordonateWithMainViewReference(
          self.view, searchEngineButton,
          CGRectGetMinY(searchEngineButton.bounds));
  if (searchEngineButtonTop - distanceToScrollDown < 0) {
    // If the distance to scroll will hide the top of `searchEngineButton`,
    // the scroll distance should be reduced to make sure at the top is visible.
    distanceToScrollDown += searchEngineButtonTop - distanceToScrollDown;
  }
  // Update the scroll position.
  CGPoint contentOffset = _scrollView.contentOffset;
  contentOffset.y += distanceToScrollDown;
  _scrollView.contentOffset = contentOffset;
}

#pragma mark - UITextViewDelegate

- (BOOL)textView:(UITextView*)textView
    shouldInteractWithURL:(NSURL*)URL
                  inRange:(NSRange)characterRange
              interaction:(UITextItemInteraction)interaction {
  [self.actionDelegate showLearnMore];
  return NO;
}

- (void)textViewDidChangeSelection:(UITextView*)textView {
  // Always force the `selectedTextRange` to `nil` to prevent users from
  // selecting text. Setting the `selectable` property to `NO` doesn't help
  // since it makes links inside the text view untappable.
  textView.selectedTextRange = nil;
}

#pragma mark - UIContentContainer

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:
           (id<UIViewControllerTransitionCoordinator>)coordinator {
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  __weak __typeof(self) weakSelf = self;
  [coordinator
      animateAlongsideTransition:^(
          id<UIViewControllerTransitionCoordinatorContext> context) {
        // Recompute if the user reached the bottom, once the animation is done.
        // This needs be done at the beginning of the transition to have a
        // smooth transition.
        [weakSelf
            updateViewsBasedOnScrollPositionWithMorePillButtonAnimation:YES];
      }
                      completion:nil];
}

@end