chromium/ios/chrome/browser/ui/settings/cells/inline_promo_cell.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/settings/cells/inline_promo_cell.h"

#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/new_feature_badge_view.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// Spacing of the stack views.
constexpr CGFloat kStackViewSpacing = 16;

// Content inset of the cell.
constexpr CGFloat kCellContentInset = 16;

// Special content inset of the cell for the close button. This is needed to
// have a big enough target area for the close button while having the "x" icon
// aligned with the rest of the UI.
constexpr CGFloat kCellCloseButtonContentInset = 2;

// Special content inset for the bottom of the cell. This is needed to have a
// big enough target area for the more info button without adding more white
// space at the bottom of the cell.
constexpr CGFloat kCellBottomContentInset = 5;

// Height and width of the close button.
constexpr CGFloat kCloseButtonSize = 16;

// Height and width of the close button's target area.
constexpr CGFloat kCloseButtonTargetAreaSize = 44;

// Height and width of the image.
constexpr CGFloat kImageSize = 86;

// Height of the more info button.
constexpr CGFloat kMoreInfoButtonHeight = 44;

// Content inset of the more info button.
constexpr CGFloat kMoreInfoButtonContentInset = 16;

// Height and width of the new feature badge.
constexpr CGFloat kNewFeatureBadgeSize = 20;

// Font size of the new feature badge label.
constexpr CGFloat kNewFeatureFontSize = 10;

// Content inset for the top, leading and bottom anchors of the badged image
// view. Used for the wide layout only.
constexpr CGFloat kBadgedImageViewContentInsetWideLayout = 4;

// Horizontal spacing betwen the stack view and the close button. Used for the
// wide layout only.
constexpr CGFloat kStackViewCloseButtonContentInsetWideLayout = 8;

// Mimimum height of the promo text label. Used so that the more info button
// doesn't take too much space in the vertical stack view. Used for the wide
// layout only.
constexpr CGFloat kPromoTextLabelMinHeightWideLayout = 60;

}  // namespace

@implementation InlinePromoCell {
  // New feature badge that is overlaying part of the promo image view.
  NewFeatureBadgeView* _badgeView;

  // View containing the image view and the new feature badge.
  UIView* _badgedImageView;

  // Vertical stack used to lay out elements both in the narrow and wide
  // layouts.
  UIStackView* _verticalStackView;

  // Horizontal stack used to lay out elements in the wide layout.
  UIStackView* _horizontalStackView;
}

- (instancetype)initWithStyle:(UITableViewCellStyle)style
              reuseIdentifier:(NSString*)reuseIdentifier {
  self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  if (self) {
    _enabled = YES;
    _closeButton = [self createCloseButton];
    _promoImageView = [self createPromoImageView];
    _badgeView = [self createNewFeatureBadgeView];
    _badgedImageView = [self createBadgedImageViewWithImageView:_promoImageView
                                            newFeatureBadgeView:_badgeView];
    _promoTextLabel = [self createPromoTextLabel];
    _moreInfoButton = [self createMoreInfoButton];
    _verticalStackView = [self createVerticalStackView];
    _horizontalStackView = [self createHorizontalStackView];

    [self.contentView addSubview:_closeButton];

    if (_shouldHaveWideLayout) {
      [self setUpCellForWideLayoutWith:self.contentView
                           closeButton:_closeButton
                       badgedImageView:_badgedImageView
                        promoTextLabel:_promoTextLabel
                        moreInfoButton:_moreInfoButton
                     verticalStackView:_verticalStackView
                   horizontalStackView:_horizontalStackView];

    } else {
      [self setUpCellForNarrowLayoutWith:self.contentView
                         badgedImageView:_badgedImageView
                          promoTextLabel:_promoTextLabel
                          moreInfoButton:_moreInfoButton
                       verticalStackView:_verticalStackView];
    }

    [NSLayoutConstraint activateConstraints:@[
      // `_closeButton` constraints.
      [_closeButton.topAnchor
          constraintEqualToAnchor:self.contentView.topAnchor
                         constant:kCellCloseButtonContentInset],
      [_closeButton.trailingAnchor
          constraintEqualToAnchor:self.contentView.trailingAnchor
                         constant:-kCellCloseButtonContentInset],
      [_closeButton.heightAnchor
          constraintEqualToConstant:kCloseButtonTargetAreaSize],
      [_closeButton.widthAnchor
          constraintEqualToConstant:kCloseButtonTargetAreaSize],

      // `_badgedImageView` constraints.
      [_badgedImageView.widthAnchor constraintEqualToConstant:kImageSize],
      [_badgedImageView.heightAnchor constraintEqualToConstant:kImageSize],

      // `_moreInfoButton` constraints.
      [_moreInfoButton.heightAnchor
          constraintGreaterThanOrEqualToConstant:kMoreInfoButtonHeight],
    ]];

    // Make sure the `_closeButton` is not behind a stack view.
    [self.contentView bringSubviewToFront:_closeButton];
  }
  return self;
}

#pragma mark - Setters

- (void)setEnabled:(BOOL)enabled {
  if (_enabled == enabled) {
    return;
  }
  _enabled = enabled;

  if (enabled) {
    [_badgeView setBadgeColor:[UIColor colorNamed:kBlue600Color]];
    _promoTextLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
  } else {
    [_badgeView setBadgeColor:[UIColor colorNamed:kTextSecondaryColor]];
    _promoTextLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
  }
  _moreInfoButton.enabled = enabled;
  _closeButton.enabled = enabled;
}

- (void)setShouldHaveWideLayout:(BOOL)shouldHaveWideLayout {
  if (_shouldHaveWideLayout == shouldHaveWideLayout) {
    return;
  }

  _shouldHaveWideLayout = shouldHaveWideLayout;
  [self configureCellForLayoutChangeWith:shouldHaveWideLayout
                             closeButton:_closeButton
                         badgedImageView:_badgedImageView
                          promoTextLabel:_promoTextLabel
                          moreInfoButton:_moreInfoButton
                       verticalStackView:_verticalStackView
                     horizontalStackView:_horizontalStackView];
}

#pragma mark - Private

// Creates and configures the promo's close button.
- (UIButton*)createCloseButton {
  UIButton* closeButton = [UIButton buttonWithType:UIButtonTypeSystem];
  closeButton.translatesAutoresizingMaskIntoConstraints = NO;
  closeButton.accessibilityLabel = l10n_util::GetNSString(IDS_IOS_ICON_CLOSE);

  UIImageSymbolConfiguration* symbolConfiguration = [UIImageSymbolConfiguration
      configurationWithPointSize:kCloseButtonSize
                          weight:UIImageSymbolWeightSemibold
                           scale:UIImageSymbolScaleMedium];

  UIButtonConfiguration* buttonConfiguration =
      [UIButtonConfiguration plainButtonConfiguration];
  buttonConfiguration.image =
      DefaultSymbolWithConfiguration(kXMarkSymbol, symbolConfiguration);
  buttonConfiguration.baseForegroundColor =
      [UIColor colorNamed:kTextTertiaryColor];
  closeButton.configuration = buttonConfiguration;

  return closeButton;
}

// Creates and configures the promo's image view.
- (UIImageView*)createPromoImageView {
  UIImageView* promoImageView = [[UIImageView alloc] init];
  promoImageView.translatesAutoresizingMaskIntoConstraints = NO;
  promoImageView.contentMode = UIViewContentModeScaleAspectFit;

  return promoImageView;
}

// Creates and configures the new feature badge view.
- (NewFeatureBadgeView*)createNewFeatureBadgeView {
  NewFeatureBadgeView* badgeView =
      [[NewFeatureBadgeView alloc] initWithBadgeSize:kNewFeatureBadgeSize
                                            fontSize:kNewFeatureFontSize];
  badgeView.translatesAutoresizingMaskIntoConstraints = NO;
  badgeView.accessibilityElementsHidden = YES;

  return badgeView;
}

// Creates and configures the promo's text label.
- (UILabel*)createPromoTextLabel {
  UILabel* promoTextLabel = [[UILabel alloc] init];
  promoTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
  promoTextLabel.isAccessibilityElement = YES;
  promoTextLabel.textAlignment = NSTextAlignmentCenter;
  promoTextLabel.font =
      [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
  promoTextLabel.adjustsFontForContentSizeCategory = YES;
  promoTextLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
  promoTextLabel.numberOfLines = 0;

  return promoTextLabel;
}

// Creates and configures the more info button.
- (UIButton*)createMoreInfoButton {
  UIButton* moreInfoButton = [UIButton buttonWithType:UIButtonTypeSystem];
  moreInfoButton.translatesAutoresizingMaskIntoConstraints = NO;
  moreInfoButton.contentHorizontalAlignment =
      UIControlContentHorizontalAlignmentCenter;

  UIButtonConfiguration* buttonConfiguration =
      [UIButtonConfiguration plainButtonConfiguration];
  buttonConfiguration.baseForegroundColor = [UIColor colorNamed:kBlue600Color];
  buttonConfiguration.contentInsets = NSDirectionalEdgeInsetsMake(
      kMoreInfoButtonContentInset, kMoreInfoButtonContentInset,
      kMoreInfoButtonContentInset, kMoreInfoButtonContentInset);
  buttonConfiguration.titleLineBreakMode = NSLineBreakByWordWrapping;
  buttonConfiguration.titleAlignment =
      UIButtonConfigurationTitleAlignmentCenter;
  moreInfoButton.configuration = buttonConfiguration;

  return moreInfoButton;
}

// Creates and configures the view composed of the image view and the new
// feature badge.
- (UIView*)createBadgedImageViewWithImageView:(UIImageView*)imageView
                          newFeatureBadgeView:(NewFeatureBadgeView*)badgeView {
  UIView* view = [[UIView alloc] init];
  view.translatesAutoresizingMaskIntoConstraints = NO;

  [view addSubview:imageView];
  [view addSubview:badgeView];

  [NSLayoutConstraint activateConstraints:@[
    // `imageView` constraints.
    [imageView.topAnchor constraintEqualToAnchor:view.topAnchor],
    [imageView.bottomAnchor constraintEqualToAnchor:view.bottomAnchor],
    [imageView.leadingAnchor constraintEqualToAnchor:view.leadingAnchor],
    [imageView.trailingAnchor constraintEqualToAnchor:view.trailingAnchor],
    [imageView.widthAnchor constraintEqualToConstant:kImageSize],
    [imageView.heightAnchor constraintEqualToConstant:kImageSize],

    // `badgeView` constraints.
    [badgeView.widthAnchor constraintEqualToConstant:kNewFeatureBadgeSize],
    [badgeView.heightAnchor constraintEqualToConstant:kNewFeatureBadgeSize],
    [badgeView.topAnchor constraintEqualToAnchor:view.topAnchor],
    [badgeView.leadingAnchor constraintEqualToAnchor:view.leadingAnchor],
  ]];

  return view;
}

// Creates and configures the vertical stack view.
- (UIStackView*)createVerticalStackView {
  UIStackView* verticalStackView = [[UIStackView alloc] init];
  verticalStackView.translatesAutoresizingMaskIntoConstraints = NO;
  verticalStackView.axis = UILayoutConstraintAxisVertical;
  verticalStackView.alignment = UIStackViewAlignmentCenter;
  verticalStackView.spacing = kStackViewSpacing;

  return verticalStackView;
}

// Creates and configures the horizontal stack view.
- (UIStackView*)createHorizontalStackView {
  UIStackView* horizontalStackView = [[UIStackView alloc] init];
  horizontalStackView.translatesAutoresizingMaskIntoConstraints = NO;
  horizontalStackView.axis = UILayoutConstraintAxisHorizontal;
  horizontalStackView.alignment = UIStackViewAlignmentCenter;
  horizontalStackView.spacing = kStackViewSpacing;

  return horizontalStackView;
}

// Sets up the subviews for the cell's narrow layout.
- (void)setUpCellForNarrowLayoutWith:(UIView*)contentView
                     badgedImageView:(UIView*)badgedImageView
                      promoTextLabel:(UILabel*)promoTextLabel
                      moreInfoButton:(UIButton*)moreInfoButton
                   verticalStackView:(UIStackView*)verticalStackView {
  [verticalStackView addArrangedSubview:badgedImageView];
  [verticalStackView addArrangedSubview:promoTextLabel];
  [verticalStackView addArrangedSubview:moreInfoButton];

  [contentView addSubview:verticalStackView];

  [NSLayoutConstraint activateConstraints:@[
    // `verticalStackView` constraints.
    [verticalStackView.topAnchor constraintEqualToAnchor:contentView.topAnchor
                                                constant:kCellContentInset],
    [verticalStackView.bottomAnchor
        constraintEqualToAnchor:contentView.bottomAnchor
                       constant:-kCellContentInset],
    [verticalStackView.leadingAnchor
        constraintEqualToAnchor:contentView.leadingAnchor
                       constant:kCellContentInset],
    [verticalStackView.trailingAnchor
        constraintEqualToAnchor:contentView.trailingAnchor
                       constant:-kCellContentInset],

    // `badgedImageView` constraints.
    [badgedImageView.topAnchor
        constraintEqualToAnchor:verticalStackView.topAnchor],

    // `promoTextLabel` constraints.
    [promoTextLabel.leadingAnchor
        constraintEqualToAnchor:verticalStackView.leadingAnchor],
    [promoTextLabel.trailingAnchor
        constraintEqualToAnchor:verticalStackView.trailingAnchor],

    // `moreInfoButton` constraints.
    [moreInfoButton.leadingAnchor
        constraintEqualToAnchor:verticalStackView.leadingAnchor],
    [moreInfoButton.trailingAnchor
        constraintEqualToAnchor:verticalStackView.trailingAnchor],
    [moreInfoButton.bottomAnchor
        constraintEqualToAnchor:contentView.bottomAnchor
                       constant:-kCellBottomContentInset],
  ]];
}

// Sets up the subviews for the cell's wide layout.
- (void)setUpCellForWideLayoutWith:(UIView*)contentView
                       closeButton:(UIButton*)closeButton
                   badgedImageView:(UIView*)badgedImageView
                    promoTextLabel:(UILabel*)promoTextLabel
                    moreInfoButton:(UIButton*)moreInfoButton
                 verticalStackView:(UIStackView*)verticalStackView
               horizontalStackView:(UIStackView*)horizontalStackView {
  [verticalStackView addArrangedSubview:promoTextLabel];
  [verticalStackView addArrangedSubview:moreInfoButton];

  [horizontalStackView addArrangedSubview:badgedImageView];
  [horizontalStackView addArrangedSubview:verticalStackView];

  [contentView addSubview:horizontalStackView];

  [NSLayoutConstraint activateConstraints:@[

    // `horizontalStackView` constraints.
    [horizontalStackView.topAnchor
        constraintEqualToAnchor:self.contentView.topAnchor
                       constant:kCellContentInset],
    [horizontalStackView.bottomAnchor
        constraintEqualToAnchor:self.contentView.bottomAnchor
                       constant:-kCellContentInset],
    [horizontalStackView.leadingAnchor
        constraintEqualToAnchor:self.contentView.leadingAnchor
                       constant:kCellContentInset],
    [horizontalStackView.trailingAnchor
        constraintEqualToAnchor:closeButton.leadingAnchor
                       constant:-kStackViewCloseButtonContentInsetWideLayout],

    // `badgedImageView` constraints.
    [badgedImageView.topAnchor
        constraintGreaterThanOrEqualToAnchor:horizontalStackView.topAnchor
                                    constant:
                                        kBadgedImageViewContentInsetWideLayout],
    [badgedImageView.bottomAnchor
        constraintLessThanOrEqualToAnchor:horizontalStackView.bottomAnchor
                                 constant:
                                     -kBadgedImageViewContentInsetWideLayout],
    [badgedImageView.leadingAnchor
        constraintEqualToAnchor:horizontalStackView.leadingAnchor
                       constant:kBadgedImageViewContentInsetWideLayout],

    // `promoTextLabel` constraints.
    [promoTextLabel.leadingAnchor
        constraintEqualToAnchor:verticalStackView.leadingAnchor],
    [promoTextLabel.trailingAnchor
        constraintEqualToAnchor:verticalStackView.trailingAnchor],
    [promoTextLabel.heightAnchor constraintGreaterThanOrEqualToConstant:
                                     kPromoTextLabelMinHeightWideLayout],

    // `moreInfoButton` constraints.
    [moreInfoButton.leadingAnchor
        constraintEqualToAnchor:verticalStackView.leadingAnchor],
    [moreInfoButton.trailingAnchor
        constraintEqualToAnchor:verticalStackView.trailingAnchor],
    [moreInfoButton.bottomAnchor
        constraintEqualToAnchor:contentView.bottomAnchor
                       constant:-kCellBottomContentInset],
  ]];
}

// Configure elements with properties that depend on the type of layout.
- (void)configureElementsForLayoutChangeWith:(UILabel*)promoTextLabel
                              moreInfoButton:(UIButton*)moreInfoButton
                           verticalStackView:(UIStackView*)verticalStackView {
  CGFloat moreInfoButtonLeftEdgeInset = 0;

  if (self.shouldHaveWideLayout) {
    promoTextLabel.textAlignment = NSTextAlignmentNatural;
    moreInfoButton.contentHorizontalAlignment =
        UIControlContentHorizontalAlignmentLeft;
    verticalStackView.alignment = UIStackViewAlignmentFill;
    verticalStackView.spacing = 0;
  } else {
    promoTextLabel.textAlignment = NSTextAlignmentCenter;
    moreInfoButton.contentHorizontalAlignment =
        UIControlContentHorizontalAlignmentCenter;
    moreInfoButtonLeftEdgeInset = kMoreInfoButtonContentInset;
    verticalStackView.alignment = UIStackViewAlignmentCenter;
    verticalStackView.spacing = kStackViewSpacing;
  }

  UIButtonConfiguration* buttonConfiguration = moreInfoButton.configuration;
  buttonConfiguration.contentInsets = NSDirectionalEdgeInsetsMake(
      kMoreInfoButtonContentInset, moreInfoButtonLeftEdgeInset,
      kMoreInfoButtonContentInset, kMoreInfoButtonContentInset);
  moreInfoButton.configuration = buttonConfiguration;
}

// Configure elements according to the expected layout (narrow or wide).
- (void)configureCellForLayoutChangeWith:(BOOL)shouldHaveWideLayout
                             closeButton:(UIButton*)closeButton
                         badgedImageView:(UIView*)badgedImageView
                          promoTextLabel:(UILabel*)promoTextLabel
                          moreInfoButton:(UIButton*)moreInfoButton
                       verticalStackView:(UIStackView*)verticalStackView
                     horizontalStackView:(UIStackView*)horizontalStackView {
  // Remove subviews to reset their interdependent constraints.
  [badgedImageView removeFromSuperview];
  [promoTextLabel removeFromSuperview];
  [moreInfoButton removeFromSuperview];
  [verticalStackView removeFromSuperview];
  [horizontalStackView removeFromSuperview];

  [self configureElementsForLayoutChangeWith:promoTextLabel
                              moreInfoButton:moreInfoButton
                           verticalStackView:verticalStackView];

  if (shouldHaveWideLayout) {
    [self setUpCellForWideLayoutWith:self.contentView
                         closeButton:closeButton
                     badgedImageView:badgedImageView
                      promoTextLabel:promoTextLabel
                      moreInfoButton:moreInfoButton
                   verticalStackView:verticalStackView
                 horizontalStackView:horizontalStackView];

  } else {
    [self setUpCellForNarrowLayoutWith:self.contentView
                       badgedImageView:badgedImageView
                        promoTextLabel:promoTextLabel
                        moreInfoButton:moreInfoButton
                     verticalStackView:verticalStackView];
  }

  // Make sure the `closeButton` is not behind a stack view.
  [self.contentView bringSubviewToFront:closeButton];
}

@end