chromium/ios/chrome/browser/ui/tab_switcher/tab_strip/ui/tab_strip_group_cell.mm

// Copyright 2024 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_strip/ui/tab_strip_group_cell.h"

#import "base/task/sequenced_task_runner.h"
#import "ios/chrome/browser/shared/ui/elements/fade_truncating_label.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_strip/ui/swift_constants_for_objective_c.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_strip/ui/tab_strip_group_stroke_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_mac.h"

namespace {

constexpr CGFloat kTitleContainerVerticalPadding = 4;
constexpr CGFloat kTitleContainerCenterYOffset = -2;
constexpr CGFloat kGroupStrokeViewMinimumWidth = 14;
constexpr double kCollapseUpdateGroupStrokeDelaySeconds = 0.25;
constexpr double kTitleContainerFadeAnimationSeconds = 0.25;

}  // namespace

@implementation TabStripGroupCell {
  FadeTruncatingLabel* _titleLabel;
  UIView* _titleContainer;
  TabStripGroupStrokeView* _groupStrokeView;
  NSLayoutConstraint* _titleContainerHeightConstraint;
}

- (instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    _titleContainer = [self createTitleContainer];
    [self.contentView addSubview:_titleContainer];
    _groupStrokeView = [[TabStripGroupStrokeView alloc] init];
    [self addSubview:_groupStrokeView];
    [self setupConstraints];
    [self updateGroupStroke];
    [self updateAccessibilityValue];
  }
  return self;
}

#pragma mark - TabStripCell

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

#pragma mark - UICollectionViewCell

- (void)prepareForReuse {
  [super prepareForReuse];
  _titleContainer.accessibilityValue = nil;
  _titleContainer.accessibilityLabel = nil;
  _titleLabel.text = nil;
  self.delegate = nil;
  self.titleContainerBackgroundColor = nil;
  self.collapsed = NO;
}

- (void)applyLayoutAttributes:
    (UICollectionViewLayoutAttributes*)layoutAttributes {
  [super applyLayoutAttributes:layoutAttributes];
  // Update the transition state asynchronously to ensure bounds of subviews
  // have been updated accordingly.
  __weak __typeof(self) weakSelf = self;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(^{
        [weakSelf updateTransitionState];
      }));
}

- (void)layoutSubviews {
  [super layoutSubviews];
  [self updateTransitionState];
}

#pragma mark - Setters

- (void)setTitle:(NSString*)title {
  [super setTitle:title];
  _titleContainer.accessibilityLabel = title;
  _titleLabel.text = [title copy];
}

- (void)setTitleContainerBackgroundColor:(UIColor*)color {
  _titleContainerBackgroundColor = color;
  _titleContainer.backgroundColor = color;
}

- (void)setTitleTextColor:(UIColor*)titleTextColor {
  _titleTextColor = titleTextColor;
  _titleLabel.textColor = titleTextColor;
}

- (void)setGroupStrokeColor:(UIColor*)color {
  [super setGroupStrokeColor:color];
  if ([_groupStrokeView.backgroundColor isEqual:color]) {
    return;
  }
  _groupStrokeView.backgroundColor = color;
  [self updateGroupStroke];
}

- (void)setCollapsed:(BOOL)collapsed {
  if (_collapsed == collapsed) {
    return;
  }
  _collapsed = collapsed;
  if (!collapsed) {
    [self updateGroupStroke];
  } else {
    __weak __typeof(self) weakSelf = self;
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, base::BindOnce(^{
          [weakSelf updateGroupStroke];
        }),
        base::Seconds(kCollapseUpdateGroupStrokeDelaySeconds));
  }
  [self updateAccessibilityValue];
}

- (void)setIntersectsLeftEdge:(BOOL)intersectsLeftEdge {
  if (super.intersectsLeftEdge != intersectsLeftEdge) {
    super.intersectsLeftEdge = intersectsLeftEdge;
    [self updateTransitionState];
  }
}

- (void)setIntersectsRightEdge:(BOOL)intersectsRightEdge {
  if (super.intersectsRightEdge != intersectsRightEdge) {
    super.intersectsRightEdge = intersectsRightEdge;
    [self updateTransitionState];
  }
}

#pragma mark - View creation helpers

// Returns a new title label.
- (FadeTruncatingLabel*)createTitleLabel {
  FadeTruncatingLabel* titleLabel = [[FadeTruncatingLabel alloc] init];
  titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
  titleLabel.font = [UIFont systemFontOfSize:TabStripTabItemConstants.fontSize
                                      weight:UIFontWeightMedium];
  titleLabel.textColor = [UIColor colorNamed:kSolidWhiteColor];
  [titleLabel
      setContentCompressionResistancePriority:UILayoutPriorityRequired - 1
                                      forAxis:UILayoutConstraintAxisHorizontal];
  return titleLabel;
}

// Returns a new title container view.
- (UIView*)createTitleContainer {
  UIView* titleContainer = [[UIView alloc] init];
  titleContainer.translatesAutoresizingMaskIntoConstraints = NO;
  titleContainer.layer.masksToBounds = YES;
  titleContainer.isAccessibilityElement = YES;
  titleContainer.layer.cornerRadius =
      TabStripGroupItemConstants.titleContainerHorizontalPadding;
  _titleLabel = [self createTitleLabel];
  [titleContainer addSubview:_titleLabel];
  return titleContainer;
}

#pragma mark - UIAccessibility

- (NSArray*)accessibilityCustomActions {
  int stringID = self.collapsed ? IDS_IOS_TAB_STRIP_TAB_GROUP_EXPAND
                                : IDS_IOS_TAB_STRIP_TAB_GROUP_COLLAPSE;
  return @[ [[UIAccessibilityCustomAction alloc]
      initWithName:l10n_util::GetNSString(stringID)
            target:self
          selector:@selector(collapseOrExpandTapped:)] ];
}

// Selector registered to expand or collapse tab group.
- (void)collapseOrExpandTapped:(id)sender {
  [self.delegate collapseOrExpandTappedForCell:self];
}

#pragma mark - Private

// Sets up constraints.
- (void)setupConstraints {
  UIView* contentView = self.contentView;
  AddSameConstraintsToSidesWithInsets(
      _titleContainer, contentView,
      LayoutSides::kLeading | LayoutSides::kTrailing,
      NSDirectionalEdgeInsetsMake(
          0, TabStripGroupItemConstants.titleContainerHorizontalMargin, 0,
          TabStripGroupItemConstants.titleContainerHorizontalMargin));
  [_titleContainer.centerYAnchor
      constraintEqualToAnchor:contentView.centerYAnchor
                     constant:kTitleContainerCenterYOffset]
      .active = YES;
  AddSameCenterConstraints(_titleLabel, _titleContainer);
  NSLayoutConstraint* titleLabelMaxWidthConstraint = [_titleLabel.widthAnchor
      constraintLessThanOrEqualToConstant:TabStripGroupItemConstants
                                              .maxTitleWidth];
  titleLabelMaxWidthConstraint.priority = UILayoutPriorityRequired;
  titleLabelMaxWidthConstraint.active = YES;
  _titleContainerHeightConstraint =
      [_titleContainer.heightAnchor constraintEqualToConstant:0];
  _titleContainerHeightConstraint.active = YES;
  NSLayoutConstraint* groupStrokeViewTitleLabelConstraint =
      [_groupStrokeView.widthAnchor
          constraintEqualToAnchor:_titleLabel.widthAnchor];
  groupStrokeViewTitleLabelConstraint.priority = UILayoutPriorityRequired - 3;
  NSLayoutConstraint* groupStrokeViewTitleContainerConstraint =
      [_groupStrokeView.widthAnchor
          constraintLessThanOrEqualToAnchor:_titleContainer.widthAnchor
                                   constant:
                                       -2 *
                                           TabStripGroupItemConstants
                                               .titleContainerHorizontalPadding -
                                       kGroupStrokeViewMinimumWidth];
  groupStrokeViewTitleContainerConstraint.priority =
      UILayoutPriorityRequired - 2;
  [NSLayoutConstraint activateConstraints:@[
    groupStrokeViewTitleLabelConstraint,
    groupStrokeViewTitleContainerConstraint,
    [_groupStrokeView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
    [_groupStrokeView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
  ]];
}

// Updates the group stroke path, hides the group stroke if necessary.
- (void)updateGroupStroke {
  if (!_groupStrokeView.backgroundColor) {
    _groupStrokeView.hidden = YES;
    return;
  }
  _groupStrokeView.hidden = NO;

  const CGFloat lineWidth =
      TabStripCollectionViewConstants.groupStrokeLineWidth;

  UIBezierPath* leftPath = [UIBezierPath bezierPath];
  CGPoint leftPoint = CGPointZero;
  [leftPath moveToPoint:leftPoint];
  leftPoint.x -= kGroupStrokeViewMinimumWidth / 2;
  [leftPath addLineToPoint:leftPoint];
  leftPoint.y += lineWidth / 2;
  [leftPath addArcWithCenter:leftPoint
                      radius:lineWidth / 2
                  startAngle:M_PI + M_PI_2
                    endAngle:M_PI
                   clockwise:NO];
  leftPoint.x -= lineWidth / 2;
  [_groupStrokeView setLeadingPath:leftPath.CGPath];

  UIBezierPath* rightPath = [UIBezierPath bezierPath];
  CGPoint rightPoint = CGPointZero;
  [rightPath moveToPoint:rightPoint];
  rightPoint.x += kGroupStrokeViewMinimumWidth / 2;
  [rightPath addLineToPoint:rightPoint];
  if (!self.collapsed) {
    // If the group is not collapse, the right end of the stroke should extend
    // to reach the left end of the next tab.
    rightPoint.x += TabStripGroupItemConstants.titleContainerHorizontalMargin;
    rightPoint.x += TabStripTabItemConstants.horizontalSpacing;
    rightPoint.x += lineWidth;
    rightPoint.x += TabStripCollectionViewConstants.groupStrokeExtension;
    [rightPath addLineToPoint:rightPoint];
  }
  rightPoint.y += lineWidth / 2;
  [rightPath addArcWithCenter:rightPoint
                       radius:lineWidth / 2
                   startAngle:M_PI + M_PI_2
                     endAngle:0
                    clockwise:YES];
  [_groupStrokeView setTrailingPath:rightPath.CGPath];
}

// Updates the title alpha value and title container height according to the
// difference between the size of the title and the size of its container.
- (void)updateTransitionState {
  CGFloat horizontalTitlePadding =
      TabStripGroupItemConstants.titleContainerHorizontalPadding;
  CGFloat verticalTitlePadding = kTitleContainerVerticalPadding;
  CGFloat titleContainerWidth = _titleContainer.bounds.size.width;
  CGFloat maxTitleContainerWidth =
      _titleLabel.frame.size.width + 2 * horizontalTitlePadding;
  CGFloat minTitleContainerHeight = 2 * _titleContainer.layer.cornerRadius;
  CGFloat maxTitleContainerHeight =
      _titleLabel.frame.size.height + 2 * verticalTitlePadding;
  CGFloat factor = 0;
  if (maxTitleContainerWidth - 2 * horizontalTitlePadding > 0) {
    factor = (titleContainerWidth - 2 * horizontalTitlePadding) /
             (maxTitleContainerWidth - 2 * horizontalTitlePadding);
  }
  _titleLabel.alpha = factor;
  _titleContainerHeightConstraint.constant =
      (1 - factor) * minTitleContainerHeight + factor * maxTitleContainerHeight;

  // At the end of the group shrinking animation (factor is 0), if the group
  // intersects with the leading or trailing edge, then animate the title
  // container alpha to 0.
  CGFloat titleContainerAlpha = 1;
  if (factor == 0 && (self.intersectsLeftEdge || self.intersectsRightEdge)) {
    titleContainerAlpha = 0;
  }
  UIView* titleContainer = _titleContainer;
  [UIView animateWithDuration:kTitleContainerFadeAnimationSeconds
                   animations:^{
                     titleContainer.alpha = titleContainerAlpha;
                   }];
}

- (void)updateAccessibilityValue {
  // Use the accessibility Value as there is a pause when using the
  // accessibility hint.
  _titleContainer.accessibilityValue = l10n_util::GetNSString(
      self.collapsed ? IDS_IOS_TAB_STRIP_GROUP_CELL_COLLAPSED_VOICE_OVER_VALUE
                     : IDS_IOS_TAB_STRIP_GROUP_CELL_EXPANDED_VOICE_OVER_VALUE);
}

@end