chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/grid/group_grid_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/tab_switcher/tab_grid/grid/group_grid_cell.h"

#import <ostream>

#import "base/check.h"
#import "base/check_op.h"
#import "base/debug/dump_without_crashing.h"
#import "base/notreached.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/shared/ui/elements/extended_touch_target_button.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/tab_switcher/tab_grid/grid/grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/group_tab_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_snapshots_view.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"
#import "ui/gfx/ios/uikit_util.h"

namespace {

// The size of symbol icons.
NSInteger kIconSymbolPointSize = 13;

// Offsets the top and bottom snapshot views.
const CGFloat kSnapshotViewLeadingOffset = 4;
const CGFloat kSnapshotViewTrailingOffset = 4;
const CGFloat kSnapShotViewBottomOffset = 4;
const CGFloat kGroupColorViewSize = 18;

}  // namespace

@implementation GroupGridCell {
  // The constraints enabled under accessibility font size.
  NSArray<NSLayoutConstraint*>* _accessibilityConstraints;
  // The constraints enabled under normal font size.
  NSArray<NSLayoutConstraint*>* _nonAccessibilityConstraints;
  // The constraints enabled while showing the close icon.
  NSArray<NSLayoutConstraint*>* _closeIconConstraints;
  // The constraints enabled while showing the selection icon.
  NSArray<NSLayoutConstraint*>* _selectIconConstraints;
  // Header height of the cell.
  NSLayoutConstraint* _topBarHeightConstraint;
  // Visual components of the cell.
  UIView* _topBar;
  UIView* _groupColorView;
  UILabel* _titleLabel;
  UIImageView* _closeIconView;
  UIImageView* _selectIconView;
  // Since the close icon dimensions are smaller than the recommended tap target
  // size, use an overlaid tap target button.
  UIButton* _closeTapTargetButton;
  UIView* _border;

  TabGroupSnapshotsView* _groupSnapshotsView;
}

// `-dequeueReusableCellWithReuseIdentifier:forIndexPath:` calls this method to
// initialize a cell.
- (instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    _state = GridCellStateNotEditing;

    // The background color must be set to avoid the corners behind the rounded
    // layer from showing when dragging and dropping. Unfortunately, using
    // `UIColor.clearColor` here will not remain transparent, so a solid color
    // must be chosen. Using the grid color prevents the corners from showing
    // while it transitions to the presented context menu/dragging state.
    self.backgroundColor = [UIColor colorNamed:kGridBackgroundColor];

    [self setupSelectedBackgroundView];
    UIView* contentView = self.contentView;
    contentView.layer.cornerRadius = kGridCellCornerRadius;
    contentView.layer.masksToBounds = YES;
    [self setupTopBar];
    _groupSnapshotsView = [[TabGroupSnapshotsView alloc]
        initWithTabGroupInfos:nil
                         size:0
                        light:self.theme == GridThemeLight
                         cell:YES];
    _groupSnapshotsView.translatesAutoresizingMaskIntoConstraints = NO;

    _closeTapTargetButton =
        [ExtendedTouchTargetButton buttonWithType:UIButtonTypeCustom];
    _closeTapTargetButton.translatesAutoresizingMaskIntoConstraints = NO;
    [_closeTapTargetButton addTarget:self
                              action:@selector(closeButtonTapped:)
                    forControlEvents:UIControlEventTouchUpInside];
    _closeTapTargetButton.accessibilityIdentifier =
        kGridCellCloseButtonIdentifier;

    [contentView addSubview:_topBar];
    [contentView addSubview:_groupSnapshotsView];
    [contentView addSubview:_closeTapTargetButton];
    _opacity = 1.0;

    self.contentView.backgroundColor =
        [UIColor colorNamed:kSecondaryBackgroundColor];



    _groupSnapshotsView.backgroundColor =
        [UIColor colorNamed:kSecondaryBackgroundColor];
    _topBar.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];
    _titleLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
    _closeIconView.tintColor = [UIColor colorNamed:kCloseButtonColor];

    self.layer.cornerRadius = kGridCellCornerRadius;
    self.layer.shadowColor = [UIColor blackColor].CGColor;
    self.layer.shadowOffset = CGSizeMake(0, 0);
    self.layer.shadowRadius = 4.0f;
    self.layer.shadowOpacity = 0.5f;
    self.layer.masksToBounds = NO;
    _groupSnapshotsView.layer.cornerRadius = kGridCellCornerRadius;
    _groupSnapshotsView.layer.masksToBounds = YES;

    NSArray* constraints = @[
      [_topBar.topAnchor constraintEqualToAnchor:contentView.topAnchor],
      [_topBar.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor],
      [_topBar.trailingAnchor
          constraintEqualToAnchor:contentView.trailingAnchor],
      [_groupSnapshotsView.topAnchor
          constraintEqualToAnchor:_topBar.bottomAnchor],
      [_groupSnapshotsView.leadingAnchor
          constraintEqualToAnchor:contentView.leadingAnchor
                         constant:kSnapshotViewLeadingOffset],
      [_groupSnapshotsView.trailingAnchor
          constraintEqualToAnchor:contentView.trailingAnchor
                         constant:-kSnapshotViewTrailingOffset],
      [_groupSnapshotsView.bottomAnchor
          constraintEqualToAnchor:contentView.bottomAnchor
                         constant:-kSnapShotViewBottomOffset],
      [_closeTapTargetButton.topAnchor
          constraintEqualToAnchor:contentView.topAnchor],
      [_closeTapTargetButton.trailingAnchor
          constraintEqualToAnchor:contentView.trailingAnchor],
      [_closeTapTargetButton.widthAnchor
          constraintEqualToConstant:kGridCellCloseTapTargetWidthHeight],
      [_closeTapTargetButton.heightAnchor
          constraintEqualToConstant:kGridCellCloseTapTargetWidthHeight],
    ];
    [NSLayoutConstraint activateConstraints:constraints];
  }

  if (@available(iOS 17, *)) {
    [self registerForTraitChanges:@[ UITraitPreferredContentSizeCategory.self ]
                       withAction:@selector(updateTopBarSize)];
  }
  return self;
}

#pragma mark - UIView

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];

  if (@available(iOS 17, *)) {
    return;
  }

  BOOL isPreviousAccessibilityCategory =
      UIContentSizeCategoryIsAccessibilityCategory(
          previousTraitCollection.preferredContentSizeCategory);
  BOOL isCurrentAccessibilityCategory =
      UIContentSizeCategoryIsAccessibilityCategory(
          self.traitCollection.preferredContentSizeCategory);
  if (isPreviousAccessibilityCategory ^ isCurrentAccessibilityCategory) {
    [self updateTopBarSize];
  }
}

- (void)didMoveToWindow {
  if (self.theme == GridThemeLight) {
    if (@available(iOS 17, *)) {
      [self updateInterfaceStyleForWindow:self.window];
    }
  }
}

#pragma mark - UICollectionViewCell

- (void)setHighlighted:(BOOL)highlighted {
  // NO-OP to disable highlighting and only allow selection.
}

- (void)prepareForReuse {
  [super prepareForReuse];
  self.title = nil;
  self.titleHidden = NO;
  self.groupColor = nil;
  self.selected = NO;
  self.opacity = 1.0;
  self.hidden = NO;
}

#pragma mark - UIAccessibility

- (BOOL)isAccessibilityElement {
  // This makes the whole cell tappable in VoiceOver rather than the individual
  // title and close button.
  return YES;
}
// TODO(crbug.com/41484563): Add the accessibility custom actions.

#pragma mark - Public

// Updates the theme to either dark or light. Updating is only done if the
// current theme is not the desired theme.
- (void)setTheme:(GridTheme)theme {
  if (_theme == theme) {
    return;
  }

  // The light and dark themes have different colored borders based on the
  // theme, regardless of dark mode, so `overrideUserInterfaceStyle` is not
  // enough here.
  switch (theme) {
    case GridThemeLight:
      if (@available(iOS 17, *)) {
        [self updateInterfaceStyleForWindow:self.window];
      } else {
        self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
      }
      _border.layer.borderColor =
          [UIColor colorNamed:kStaticBlue400Color].CGColor;
      break;
    case GridThemeDark:
      self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
      _border.layer.borderColor = UIColor.whiteColor.CGColor;
      break;
  }

  _theme = theme;
}

- (void)setGroupColor:(UIColor*)groupColor {
  if (groupColor) {
    _groupColor = groupColor;
    _groupColorView.backgroundColor = groupColor;
  }
}

- (void)configureWithGroupTabInfos:(NSArray<GroupTabInfo*>*)groupTabInfos
                    totalTabsCount:(NSInteger)totalTabsCount {
  CHECK_LE((int)groupTabInfos.count, totalTabsCount);
  [_groupSnapshotsView
      configureTabGroupSnapshotsViewWithTabGroupInfos:groupTabInfos
                                                 size:totalTabsCount];
}

- (NSArray<UIView*>*)allGroupTabViews {
  return [_groupSnapshotsView allGroupTabViews];
}

- (void)setTabsCount:(NSInteger)tabsCount {
  _tabsCount = tabsCount;
}

- (void)setTitle:(NSString*)title {
  _titleLabel.text = title;
  self.accessibilityLabel = l10n_util::GetNSStringF(
      IDS_IOS_TAB_GROUP_CELL_ACCESSIBILITY_TITLE,
      base::SysNSStringToUTF16(title), base::NumberToString16(_tabsCount));
  _title = [title copy];
}

- (void)setTitleHidden:(BOOL)titleHidden {
  _titleLabel.hidden = titleHidden;
  _titleHidden = titleHidden;
}

- (UIDragPreviewParameters*)dragPreviewParameters {
  UIBezierPath* visiblePath = [UIBezierPath
      bezierPathWithRoundedRect:self.bounds
                   cornerRadius:self.contentView.layer.cornerRadius];
  UIDragPreviewParameters* params = [[UIDragPreviewParameters alloc] init];
  params.visiblePath = visiblePath;
  return params;
}

- (void)setOpacity:(CGFloat)opacity {
  _opacity = opacity;
  self.alpha = opacity;
}

- (void)setAlpha:(CGFloat)alpha {
  // Make sure alpha is synchronized with opacity.
  _opacity = alpha;
  super.alpha = _opacity;
}

#pragma mark - Private

// Sets up the top bar with icon, title, and close button.
- (void)setupTopBar {
  _topBar = [[UIView alloc] init];
  _topBar.translatesAutoresizingMaskIntoConstraints = NO;

  _groupColorView = [[UIView alloc] init];
  _groupColorView.translatesAutoresizingMaskIntoConstraints = NO;
  _groupColorView.layer.cornerRadius = kGroupColorViewSize / 2;

  _titleLabel = [[UILabel alloc] init];
  _titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
  _titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
  _titleLabel.adjustsFontForContentSizeCategory = YES;

  _closeIconView = [[UIImageView alloc] init];
  _closeIconView.translatesAutoresizingMaskIntoConstraints = NO;
  _closeIconView.contentMode = UIViewContentModeCenter;
  _closeIconView.hidden = [self isInSelectionMode];
  _closeIconView.image =
      DefaultSymbolTemplateWithPointSize(kXMarkSymbol, kIconSymbolPointSize);

  _selectIconView = [[UIImageView alloc] init];
  _selectIconView.translatesAutoresizingMaskIntoConstraints = NO;
  _selectIconView.contentMode = UIViewContentModeScaleAspectFit;
  _selectIconView.hidden = ![self isInSelectionMode];

  _selectIconView.image = [self selectIconImageForCurrentState];

  [_topBar addSubview:_selectIconView];

  [_topBar addSubview:_groupColorView];
  [_topBar addSubview:_titleLabel];
  [_topBar addSubview:_closeIconView];

  _accessibilityConstraints = @[
    [_titleLabel.leadingAnchor
        constraintEqualToAnchor:_topBar.leadingAnchor
                       constant:kGridCellHeaderLeadingInset],
    [_groupColorView.widthAnchor constraintEqualToConstant:0],
    [_groupColorView.heightAnchor constraintEqualToConstant:0],
  ];

  _nonAccessibilityConstraints = @[
    [_groupColorView.heightAnchor
        constraintEqualToConstant:kGroupColorViewSize],
    [_groupColorView.widthAnchor constraintEqualToConstant:kGroupColorViewSize],
    [_groupColorView.leadingAnchor
        constraintEqualToAnchor:_topBar.leadingAnchor
                       constant:kGridCellHeaderLeadingInset],
    [_groupColorView.centerYAnchor
        constraintEqualToAnchor:_topBar.centerYAnchor],
    [_titleLabel.leadingAnchor
        constraintEqualToAnchor:_groupColorView.trailingAnchor
                       constant:kGridCellHeaderLeadingInset],
  ];

  _topBarHeightConstraint =
      [_topBar.heightAnchor constraintEqualToConstant:kGridCellHeaderHeight];

  _closeIconConstraints = @[
    [_titleLabel.trailingAnchor
        constraintEqualToAnchor:_closeIconView.leadingAnchor
                       constant:-kGridCellTitleLabelContentInset],
    [_topBar.topAnchor constraintEqualToAnchor:_closeIconView.centerYAnchor
                                      constant:-kGridCellCloseButtonTopSpacing],
    [_closeIconView.trailingAnchor
        constraintEqualToAnchor:_topBar.trailingAnchor
                       constant:-kGridCellCloseButtonContentInset],
  ];

  _selectIconConstraints = @[
    [_selectIconView.heightAnchor
        constraintEqualToConstant:kGridCellSelectIconSize],
    [_selectIconView.widthAnchor
        constraintEqualToConstant:kGridCellSelectIconSize],
    [_titleLabel.trailingAnchor
        constraintEqualToAnchor:_selectIconView.leadingAnchor
                       constant:-kGridCellTitleLabelContentInset],
    [_topBar.topAnchor constraintEqualToAnchor:_selectIconView.topAnchor
                                      constant:-kGridCellSelectIconTopSpacing],
    [_selectIconView.trailingAnchor
        constraintEqualToAnchor:_topBar.trailingAnchor
                       constant:-kGridCellSelectIconContentInset],

  ];

  [self updateTopBarSize];
  [self configureCloseOrSelectIconConstraints];

  NSArray* constraints = @[
    _topBarHeightConstraint,
    [_titleLabel.centerYAnchor constraintEqualToAnchor:_topBar.centerYAnchor],
  ];

  [NSLayoutConstraint activateConstraints:constraints];
  [_titleLabel
      setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
                                      forAxis:UILayoutConstraintAxisHorizontal];
  [_closeIconView
      setContentCompressionResistancePriority:UILayoutPriorityRequired
                                      forAxis:UILayoutConstraintAxisHorizontal];
  [_closeIconView setContentHuggingPriority:UILayoutPriorityRequired
                                    forAxis:UILayoutConstraintAxisHorizontal];
  [_selectIconView
      setContentCompressionResistancePriority:UILayoutPriorityRequired
                                      forAxis:UILayoutConstraintAxisHorizontal];
  [_selectIconView setContentHuggingPriority:UILayoutPriorityRequired
                                     forAxis:UILayoutConstraintAxisHorizontal];
}

- (UIImage*)selectIconImageForCurrentState {
  if (_state == GridCellStateEditingUnselected) {
    return DefaultSymbolTemplateWithPointSize(kCircleSymbol,
                                              kIconSymbolPointSize);
  }
  return DefaultSymbolTemplateWithPointSize(kCheckmarkCircleFillSymbol,
                                            kIconSymbolPointSize);
}

// Update constraints of top bar when system font size changes. If accessibility
// font size is chosen, the favicon will be hidden, and the title text will be
// shown in two lines.
- (void)updateTopBarSize {
  _topBarHeightConstraint.constant = [self topBarHeight];

  if (UIContentSizeCategoryIsAccessibilityCategory(
          self.traitCollection.preferredContentSizeCategory)) {
    _titleLabel.numberOfLines = 2;
    [NSLayoutConstraint deactivateConstraints:_nonAccessibilityConstraints];
    [NSLayoutConstraint activateConstraints:_accessibilityConstraints];
  } else {
    _titleLabel.numberOfLines = 1;
    [NSLayoutConstraint deactivateConstraints:_accessibilityConstraints];
    [NSLayoutConstraint activateConstraints:_nonAccessibilityConstraints];
  }
}

- (void)configureCloseOrSelectIconConstraints {
  BOOL showSelectionMode = [self isInSelectionMode] && _selectIconView;

  _closeIconView.hidden = showSelectionMode;
  _selectIconView.hidden = !showSelectionMode;

  if (showSelectionMode) {
    [NSLayoutConstraint deactivateConstraints:_closeIconConstraints];
    [NSLayoutConstraint activateConstraints:_selectIconConstraints];
  } else {
    [NSLayoutConstraint deactivateConstraints:_selectIconConstraints];
    [NSLayoutConstraint activateConstraints:_closeIconConstraints];
  }
}

// Informs whether or not the cell is currently displaying an editing state.
- (BOOL)isInSelectionMode {
  return self.state != GridCellStateNotEditing;
}

- (void)setState:(GridCellState)state {
  if (state == _state) {
    return;
  }

  _state = state;
  // TODO(crbug.com/40942154): Add the accessibility value for selected and
  // unselected states.
  self.accessibilityValue = nil;
  _closeTapTargetButton.enabled = ![self isInSelectionMode];
  _selectIconView.image = [self selectIconImageForCurrentState];

  [self configureCloseOrSelectIconConstraints];
  _border.hidden = [self isInSelectionMode];
}

// Sets up the selection border. The tint color is set when the theme is
// selected.
- (void)setupSelectedBackgroundView {
  self.selectedBackgroundView = [[UIView alloc] init];
  self.selectedBackgroundView.backgroundColor =
      [UIColor colorNamed:kGridBackgroundColor];
  _border = [[UIView alloc] init];
  _border.hidden = [self isInSelectionMode];
  _border.translatesAutoresizingMaskIntoConstraints = NO;
  _border.backgroundColor = [UIColor colorNamed:kGridBackgroundColor];
  _border.layer.cornerRadius = kGridCellCornerRadius +
                               kGridCellSelectionRingGapWidth +
                               kGridCellSelectionRingTintWidth;
  _border.layer.borderWidth = kGridCellSelectionRingTintWidth;
  [self.selectedBackgroundView addSubview:_border];
  [NSLayoutConstraint activateConstraints:@[
    [_border.topAnchor
        constraintEqualToAnchor:self.selectedBackgroundView.topAnchor
                       constant:-kGridCellSelectionRingTintWidth -
                                kGridCellSelectionRingGapWidth],
    [_border.leadingAnchor
        constraintEqualToAnchor:self.selectedBackgroundView.leadingAnchor
                       constant:-kGridCellSelectionRingTintWidth -
                                kGridCellSelectionRingGapWidth],
    [_border.trailingAnchor
        constraintEqualToAnchor:self.selectedBackgroundView.trailingAnchor
                       constant:kGridCellSelectionRingTintWidth +
                                kGridCellSelectionRingGapWidth],
    [_border.bottomAnchor
        constraintEqualToAnchor:self.selectedBackgroundView.bottomAnchor
                       constant:kGridCellSelectionRingTintWidth +
                                kGridCellSelectionRingGapWidth]
  ]];
}

// Selector registered to the close button.
- (void)closeButtonTapped:(id)sender {
  [self.delegate closeButtonTappedForGroupCell:self];
}

// Returns the height of top bar in grid cell. The value depends on whether
// accessibility font size is chosen.
- (CGFloat)topBarHeight {
  return UIContentSizeCategoryIsAccessibilityCategory(
             self.traitCollection.preferredContentSizeCategory)
             ? kGridCellHeaderAccessibilityHeight
             : kGridCellHeaderHeight;
}

// If window is not nil, register for updates to its interface style updates and
// set the user interface style to be the same as the window.
- (void)updateInterfaceStyleForWindow:(UIWindow*)window {
  if (!window) {
    return;
  }
  if (@available(iOS 17, *)) {
    [self.window.windowScene
        registerForTraitChanges:@[ UITraitUserInterfaceStyle.self ]
                     withTarget:self
                         action:@selector(interfaceStyleChangedForWindow:
                                                         traitCollection:)];
    self.overrideUserInterfaceStyle =
        self.window.windowScene.traitCollection.userInterfaceStyle;
  }
}

// Callback for the observation of the user interface style trait of the window
// scene.
- (void)interfaceStyleChangedForWindow:(UIView*)window
                       traitCollection:(UITraitCollection*)traitCollection {
  self.overrideUserInterfaceStyle =
      self.window.windowScene.traitCollection.userInterfaceStyle;
}

@end