chromium/ios/chrome/browser/ui/omnibox/popup/carousel/omnibox_popup_carousel_control.mm

// Copyright 2022 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/carousel/omnibox_popup_carousel_control.h"

#import "ios/chrome/browser/ui/omnibox/popup/carousel/carousel_item.h"
#import "ios/chrome/browser/ui/omnibox/popup/carousel/carousel_item_menu_provider.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_accessibility_identifier_constants.h"
#import "ios/chrome/browser/ui/omnibox/popup/carousel/omnibox_popup_carousel_cell.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {

/// Size of the view behind the icon.
const CGFloat kBackgroundViewSize = 56.0f;
/// Top, leading and trailing margins of `ImageView`.
const CGFloat kBackgroundViewMargin = 7.0f;
/// Padding between the icon and the label.
const CGFloat kBackgroundLabelSpacing = 10.0f;
/// Size of the view containing the favicon.
const CGFloat kFaviconSize = 32.0f;
/// Size of the favicon placeholder font.
const CGFloat kFaviconPlaceholderFontSize = 22.0f;
/// Leading and trailing margin of the label.
const CGFloat kLabelMargin = 1.0f;
/// Maximum number of lines for the label.
const NSInteger kLabelNumLines = 2;

/// Corner radius of the context menu preview.
const CGFloat kPreviewCornerRadius = 13.0f;
/// Corner radius of the icon's `backgroundView`.
const CGFloat kBackgroundCornerRadius = 13.0f;

/// UILabel displaying text at the bottom of carousel item.
UILabel* CarouselItemLabel() {
  UILabel* label = [[UILabel alloc] init];
  label.translatesAutoresizingMaskIntoConstraints = NO;
  label.textColor = [UIColor colorNamed:kTextSecondaryColor];
  label.numberOfLines = kLabelNumLines;
  label.textAlignment = NSTextAlignmentCenter;
  label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1];
  label.accessibilityIdentifier =
      kOmniboxCarouselControlLabelAccessibilityIdentifier;
  return label;
}

/// UIView for the squircle background of carousel item.
UIView* CarouselItemBackgroundView() {
  UIImageView* imageView = [[UIImageView alloc] init];
  UIImage* backgroundImage = [[UIImage imageNamed:@"ntp_most_visited_tile"]
      imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  imageView.image = backgroundImage;
  imageView.tintColor = [UIColor colorNamed:kGrey100Color];
  imageView.translatesAutoresizingMaskIntoConstraints = NO;
  [NSLayoutConstraint activateConstraints:@[
    [imageView.widthAnchor constraintEqualToConstant:kBackgroundViewSize],
    [imageView.heightAnchor constraintEqualToConstant:kBackgroundViewSize]
  ]];
  return imageView;
}

/// FaviconView containing the icon of carousel item.
FaviconView* CarouselItemFaviconView() {
  FaviconView* faviconView = [[FaviconView alloc] init];
  [faviconView setFont:[UIFont systemFontOfSize:kFaviconPlaceholderFontSize]];
  faviconView.userInteractionEnabled = NO;
  faviconView.translatesAutoresizingMaskIntoConstraints = NO;
  [NSLayoutConstraint activateConstraints:@[
    [faviconView.widthAnchor constraintEqualToConstant:kFaviconSize],
    [faviconView.heightAnchor constraintEqualToConstant:kFaviconSize]
  ]];
  return faviconView;
}

/// CAGradientLayer for `backgroundView` when selected.
CAGradientLayer* CarouselBackgroundGradientLayer() {
  CAGradientLayer* gradientLayer = [[CAGradientLayer alloc] init];
  gradientLayer.startPoint = CGPointMake(0, 0.5);
  gradientLayer.endPoint = CGPointMake(1, 0.5);
  UIColor* gradientStartColor =
      [[UIColor colorNamed:@"omnibox_suggestion_row_highlight_color"]
          colorWithAlphaComponent:0.85];
  UIColor* gradientEndColor =
      [UIColor colorNamed:@"omnibox_suggestion_row_highlight_color"];
  gradientLayer.colors =
      @[ (id)[gradientStartColor CGColor], (id)[gradientEndColor CGColor] ];
  gradientLayer.cornerRadius = kBackgroundCornerRadius;
  gradientLayer.cornerCurve = kCACornerCurveContinuous;
  return gradientLayer;
}

}  // namespace

const CGFloat kOmniboxPopupCarouselControlWidth =
    kBackgroundViewSize + 2 * kBackgroundViewMargin;

@interface OmniboxPopupCarouselControl ()

/// View containing the background.
@property(nonatomic, strong) UIView* backgroundView;
/// View containing the icon.
@property(nonatomic, strong) FaviconView* faviconView;
/// UILabel containing the text.
@property(nonatomic, strong) UILabel* label;
/// Gradient layer for `backgroundView` when selected.
@property(nonatomic, strong) CAGradientLayer* gradientLayer;

@end

@implementation OmniboxPopupCarouselControl

- (instancetype)init {
  self = [super init];
  if (self) {
    _label = CarouselItemLabel();
    _backgroundView = CarouselItemBackgroundView();
    _faviconView = CarouselItemFaviconView();
    _carouselItem = nil;
    _gradientLayer = CarouselBackgroundGradientLayer();
  }
  return self;
}

- (void)setSelected:(BOOL)selected {
  [super setSelected:selected];
  if (selected) {
    [self.delegate carouselControlDidBecomeFocused:self];
    [self.backgroundView.layer addSublayer:self.gradientLayer];
  } else {
    [self.gradientLayer removeFromSuperlayer];
  }
}

- (void)layoutSubviews {
  self.gradientLayer.frame = self.backgroundView.bounds;
  [super layoutSubviews];
}

- (void)addSubviews {
  // Rounds corners in a Squircle.
  self.layer.cornerCurve = kCACornerCurveContinuous;
  self.layer.cornerRadius = kPreviewCornerRadius;
  [self
      addInteraction:[[UIContextMenuInteraction alloc] initWithDelegate:self]];
  self.translatesAutoresizingMaskIntoConstraints = NO;
  [self addSubview:_label];
  [self addSubview:_backgroundView];
  [self addSubview:_faviconView];

  AddSameCenterConstraints(_backgroundView, _faviconView);
  [NSLayoutConstraint activateConstraints:@[
    [_backgroundView.topAnchor constraintEqualToAnchor:self.topAnchor
                                              constant:kBackgroundViewMargin],
    [_backgroundView.leadingAnchor
        constraintEqualToAnchor:self.leadingAnchor
                       constant:kBackgroundViewMargin],
    [self.trailingAnchor constraintEqualToAnchor:_backgroundView.trailingAnchor
                                        constant:kBackgroundViewMargin],

    [_label.topAnchor constraintEqualToAnchor:_backgroundView.bottomAnchor
                                     constant:kBackgroundLabelSpacing],
    [_label.leadingAnchor constraintEqualToAnchor:self.leadingAnchor
                                         constant:kLabelMargin],
    [self.trailingAnchor constraintEqualToAnchor:_label.trailingAnchor
                                        constant:kLabelMargin],
    [self.bottomAnchor constraintEqualToAnchor:_label.bottomAnchor
                                      constant:kBackgroundViewMargin]
  ]];
  [self setNeedsLayout];
  [self layoutIfNeeded];
}

#pragma mark - Public methods

- (void)setCarouselItem:(CarouselItem*)carouselItem {
  if (self.subviews.count == 0) {
    [self addSubviews];
  }
  _carouselItem = carouselItem;
  [self.faviconView configureWithAttributes:carouselItem.faviconAttributes];
  self.label.text = carouselItem.title;
  self.accessibilityLabel = carouselItem.title;
}

#pragma mark - UIContextMenuInteractionDelegate

- (UIContextMenuConfiguration*)contextMenuInteraction:
                                   (UIContextMenuInteraction*)interaction
                       configurationForMenuAtLocation:(CGPoint)location {
  return [self.menuProvider
      contextMenuConfigurationForCarouselItem:self.carouselItem
                                     fromView:self];
}

- (void)contextMenuInteraction:(UIContextMenuInteraction*)interaction
    willDisplayMenuForConfiguration:(UIContextMenuConfiguration*)configuration
                           animator:
                               (id<UIContextMenuInteractionAnimating>)animator {
  if (self.isSelected) {
    __weak CAGradientLayer* weakHighlightLayer = self.gradientLayer;
    [animator addAnimations:^{
      [weakHighlightLayer removeFromSuperlayer];
    }];
  }
  [super contextMenuInteraction:interaction
      willDisplayMenuForConfiguration:configuration
                             animator:animator];
}

- (void)contextMenuInteraction:(UIContextMenuInteraction*)interaction
       willEndForConfiguration:(UIContextMenuConfiguration*)configuration
                      animator:(id<UIContextMenuInteractionAnimating>)animator {
  if (self.isSelected) {
    __weak __typeof(self) weakSelf = self;
    [animator addAnimations:^{
      [weakSelf.backgroundView.layer addSublayer:weakSelf.gradientLayer];
    }];
  }
  [super contextMenuInteraction:interaction
        willEndForConfiguration:configuration
                       animator:animator];
}

- (UITargetedPreview*)contextMenuInteraction:
                          (UIContextMenuInteraction*)interaction
                               configuration:
                                   (UIContextMenuConfiguration*)configuration
       highlightPreviewForItemWithIdentifier:(id<NSCopying>)identifier {
  UIPreviewParameters* previewParameters = [[UIPreviewParameters alloc] init];
  previewParameters.backgroundColor =
      [UIColor colorNamed:kGroupedSecondaryBackgroundColor];
  previewParameters.visiblePath =
      [UIBezierPath bezierPathWithRoundedRect:interaction.view.bounds
                                 cornerRadius:kPreviewCornerRadius];
  return [[UITargetedPreview alloc] initWithView:self
                                      parameters:previewParameters];
}

#pragma mark - UIAccessibility

- (void)accessibilityElementDidBecomeFocused {
  // Element is focused by VoiceOver, informs its delegate so it can make it
  // visible, in case it's hidden in the scroll view.
  [self.delegate carouselControlDidBecomeFocused:self];
}

- (UIAccessibilityTraits)accessibilityTraits {
  return UIAccessibilityTraitButton | [super accessibilityTraits];
}

/// Custom actions for a cell configured with this item.
- (NSArray<UIAccessibilityCustomAction*>*)accessibilityCustomActions {
  return
      [self.menuProvider accessibilityActionsForCarouselItem:self.carouselItem
                                                    fromView:self];
}

@end