chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_snapshots_view.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_grid/tab_groups/tab_group_snapshots_view.h"

#import "base/check.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/top_aligned_image_view.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/group_tab_info.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/group_tab_view.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/gradient_view.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
constexpr CGFloat kSpacing = 4;
constexpr CGFloat kFinalViewCornerRadius = 16;
}  // namespace

@implementation TabGroupSnapshotsView {
  BOOL _isLight;
  BOOL _isCell;
  NSArray<GroupTabInfo*>* _tabGroupInfos;
  NSUInteger _tabGroupTabNumber;
  UIStackView* _firstLine;
  UIStackView* _secondLine;
  GroupTabView* _singleView;
}

- (instancetype)initWithTabGroupInfos:(NSArray<GroupTabInfo*>*)tabGroupInfos
                                 size:(NSUInteger)size
                                light:(BOOL)isLight
                                 cell:(BOOL)isCell {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to create a tab group snapshot view outside "
         "the Tab Groups experiment.";
  self = [super initWithFrame:CGRectZero];
  if (self) {
    CHECK_LE([tabGroupInfos count], size);
    self.translatesAutoresizingMaskIntoConstraints = NO;
    _isLight = isLight;
    _isCell = isCell;

    NSMutableArray<GroupTabView*>* finalViews =
        [[NSMutableArray alloc] initWithArray:@[
          [[GroupTabView alloc] initWithIsCell:isCell],
          [[GroupTabView alloc] initWithIsCell:isCell],
          [[GroupTabView alloc] initWithIsCell:isCell],
          [[GroupTabView alloc] initWithIsCell:isCell]
        ]];

    UIStackView* snapshotsView = [self squaredViews:finalViews];

    [self addSubview:snapshotsView];
    AddSameConstraints(snapshotsView, self);

    [self configureTabGroupSnapshotsViewWithTabGroupInfos:tabGroupInfos
                                                     size:size];

    if (!_isCell) {
      _singleView = [[GroupTabView alloc] initWithIsCell:_isCell];
      _singleView.translatesAutoresizingMaskIntoConstraints = NO;
      [_singleView configureWithSnapshot:tabGroupInfos.firstObject.snapshot
                                 favicon:tabGroupInfos.firstObject.favicon];
      [self addSubview:_singleView];
      AddSameConstraints(_singleView, self);
      _singleView.hidden = size > 1;
      _firstLine.hidden = size == 1;
      _secondLine.hidden = size == 1;
    }

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

- (NSArray<GroupTabView*>*)allGroupTabViews {
  return [_firstLine.arrangedSubviews
      arrayByAddingObjectsFromArray:_secondLine.arrangedSubviews];
}

#pragma mark - Private Helpers

// Returns a range computed with `start` index, `length` and the tab group's
// tabs number. To compute the range, it compute if there is element left or
// not. For example, by default we have a range of 3 element, but if there is
// only 4 element in total, then the range will take the last one, but if there
// is 5 element in total, then the range will only take 3 elements.
- (NSRange)computedRangeStartIndex:(NSUInteger)start
          lengthWithoutLastElement:(NSUInteger)length {
  NSUInteger computedNumberOfElement = start + length + 1;
  if (computedNumberOfElement == _tabGroupTabNumber &&
      computedNumberOfElement <= [_tabGroupInfos count]) {
    length += 1;
  }
  return NSMakeRange(start, length);
}

// Returns the list of favicons pictures from the given array of `infos`.
- (NSMutableArray<UIImage*>*)faviconsFromRange:(NSRange)range {
  NSMutableArray<UIImage*>* faviconsSubArray = [[NSMutableArray alloc] init];
  for (GroupTabInfo* info : [_tabGroupInfos subarrayWithRange:range]) {
    if (info.favicon) {
      [faviconsSubArray addObject:info.favicon];
    } else {
      [faviconsSubArray addObject:[[UIImage alloc] init]];
    }
  }
  return faviconsSubArray;
}

// Returns a configured stack view that correspond to a line in the final
// squared view.
- (UIStackView*)lineViews {
  UIStackView* line = [[UIStackView alloc] init];
  line.translatesAutoresizingMaskIntoConstraints = NO;
  line.distribution = UIStackViewDistributionFillEqually;
  line.contentMode = UIViewContentModeScaleAspectFill;
  line.spacing = kSpacing;
  return line;
}

// Returns a stack view that put the views, given in parameters, aligned in
// square.
- (UIStackView*)squaredViews:(NSMutableArray<GroupTabView*>*)views {
  CHECK_EQ([views count], 4u);
  _firstLine = [self lineViews];
  _secondLine = [self lineViews];

  for (NSUInteger i = 0; i < 4; i++) {
    if (i < 2) {
      [_firstLine addArrangedSubview:views[i]];
    } else {
      [_secondLine addArrangedSubview:views[i]];
    }
  }

  UIStackView* completeView = [[UIStackView alloc] init];
  completeView.translatesAutoresizingMaskIntoConstraints = NO;
  completeView.layer.cornerRadius = kFinalViewCornerRadius;
  completeView.spacing = kSpacing;
  completeView.axis = UILayoutConstraintAxisVertical;
  completeView.distribution = UIStackViewDistributionFillEqually;
  completeView.contentMode = UIViewContentModeScaleAspectFill;
  completeView.layer.masksToBounds = YES;
  [completeView addArrangedSubview:_firstLine];
  [completeView addArrangedSubview:_secondLine];

  _secondLine.hidden = [self compactHeight];

  return completeView;
}

// YES if the view is compact.
- (BOOL)compactHeight {
  return self.traitCollection.verticalSizeClass ==
             UIUserInterfaceSizeClassCompact &&
         _isCell;
}

- (void)configureTabGroupSnapshotsViewWithTabGroupInfos:
            (NSArray<GroupTabInfo*>*)tabGroupInfos
                                                   size:(NSUInteger)size {
  _tabGroupInfos = tabGroupInfos;
  _tabGroupTabNumber = size;
  if (!_isCell && (size == 1)) {
    _singleView.hidden = NO;
    _firstLine.hidden = YES;
    _secondLine.hidden = YES;
    [_singleView configureWithSnapshot:_tabGroupInfos.firstObject.snapshot
                               favicon:_tabGroupInfos.firstObject.favicon];
  } else {
    _singleView.hidden = YES;
    _firstLine.hidden = NO;
    _secondLine.hidden = NO;
    [self updateViews];
  }
}

- (void)updateViews {
  NSRange snapshotsViewRange =
      [self computedRangeStartIndex:0
           lengthWithoutLastElement:([self compactHeight]
                                         ? MIN(1, [_tabGroupInfos count])
                                         : MIN(3, [_tabGroupInfos count]))];
  NSRange faviconsViewRange =
      [self computedRangeStartIndex:NSMaxRange(snapshotsViewRange)
           lengthWithoutLastElement:MIN(3, [_tabGroupInfos count] -
                                               NSMaxRange(snapshotsViewRange))];

  _secondLine.hidden = [self compactHeight];

  NSUInteger index = snapshotsViewRange.location;
  for (GroupTabView* view in [self allGroupTabViews]) {
    if (index >= [_tabGroupInfos count]) {
      [view hideAllAttributes];
      continue;
    }

    GroupTabInfo* tabGroupInfo = _tabGroupInfos[index];
    if (index < NSMaxRange(snapshotsViewRange)) {
      [view configureWithSnapshot:tabGroupInfo.snapshot
                          favicon:tabGroupInfo.favicon];
    } else if (index < _tabGroupTabNumber) {
      NSMutableArray<UIImage*>* faviconImages =
          [self faviconsFromRange:faviconsViewRange];
      if (NSMaxRange(faviconsViewRange) < _tabGroupTabNumber) {
        [view configureWithFavicons:faviconImages
                remainingTabsNumber:(_tabGroupTabNumber -
                                     NSMaxRange(faviconsViewRange))];
      } else {
        [view configureWithFavicons:faviconImages];
      }
    }
    ++index;
  }
}

#pragma mark - UITraitEnvironment

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  if (@available(iOS 17, *)) {
    return;
  }
  if (self.traitCollection.verticalSizeClass !=
      previousTraitCollection.verticalSizeClass) {
    [self updateViews];
  }
}

@end