chromium/ios/chrome/browser/ui/toolbar/buttons/toolbar_button.mm

// Copyright 2017 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/toolbar/buttons/toolbar_button.h"

#import "base/check.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {
const CGFloat kSpotlightSize = 38;
const CGFloat kSpotlightCornerRadius = 7;
}  // namespace

@interface ToolbarButton () {
  // The image loader used to load `_image` when the button is updated to
  // visible.
  ToolbarButtonImageLoader _imageLoader;
  // The image loader used to load `_IPHHighlightedImage` when the button is
  // updated to visible and highlighted.
  ToolbarButtonImageLoader _IPHHighlightedImageLoader;
}

// The image used for the normal state.
@property(nonatomic, strong) UIImage* image;
// The image used for iphHighlighted state. If this property is not nil, the
// iphHighlighted effect will be replacing the default image with this one,
// instead of using tint color OR `self.spotlightView`.
@property(nonatomic, strong) UIImage* IPHHighlightedImage;
@end

@implementation ToolbarButton

- (instancetype)initWithImageLoader:(ToolbarButtonImageLoader)imageLoader {
  return [self initWithImageLoader:imageLoader IPHHighlightedImageLoader:nil];
}

- (instancetype)initWithImageLoader:(ToolbarButtonImageLoader)imageLoader
          IPHHighlightedImageLoader:
              (ToolbarButtonImageLoader)IPHHighlightedImageLoader {
  self = [[super class] buttonWithType:UIButtonTypeSystem];
  if (self) {
    DCHECK(imageLoader);
    _imageLoader = imageLoader;
    _IPHHighlightedImageLoader = IPHHighlightedImageLoader;

    [self initializeButton];
  }
  return self;
}

#pragma mark - Public Methods

- (void)updateHiddenInCurrentSizeClass {
  BOOL newHiddenValue = YES;

  BOOL isCompactWidth = self.traitCollection.horizontalSizeClass ==
                        UIUserInterfaceSizeClassCompact;
  BOOL isCompactHeight =
      self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
  BOOL isRegularWidth = self.traitCollection.horizontalSizeClass ==
                        UIUserInterfaceSizeClassRegular;
  BOOL isRegularHeight =
      self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular;

  if (isCompactWidth && isCompactHeight) {
    newHiddenValue = !(self.visibilityMask &
                       ToolbarComponentVisibilityCompactWidthCompactHeight);
  } else if (isCompactWidth && isRegularHeight) {
    newHiddenValue = !(self.visibilityMask &
                       ToolbarComponentVisibilityCompactWidthRegularHeight);
  } else if (isRegularWidth && isCompactHeight) {
    newHiddenValue = !(self.visibilityMask &
                       ToolbarComponentVisibilityRegularWidthCompactHeight);
  } else if (isRegularWidth && isRegularHeight) {
    newHiddenValue = !(self.visibilityMask &
                       ToolbarComponentVisibilityRegularWidthRegularHeight);
  }

  if (self.hiddenInCurrentSizeClass != newHiddenValue) {
    self.hiddenInCurrentSizeClass = newHiddenValue;
    [self setHiddenForCurrentStateAndSizeClass];
  }

  [self checkNamedGuide];
  [self checkImageVisibility];
}

- (void)setHiddenInCurrentState:(BOOL)hiddenInCurrentState {
  _hiddenInCurrentState = hiddenInCurrentState;
  [self setHiddenForCurrentStateAndSizeClass];
}

- (void)setIphHighlighted:(BOOL)iphHighlighted {
  if (iphHighlighted == _iphHighlighted)
    return;

  _iphHighlighted = iphHighlighted;
  if ([self canUseIPHHighlightedImage]) {
    [self updateImage];
  } else {
    [self updateTintColor];
    [self updateSpotlightView];
  }
}

- (void)setToolbarConfiguration:(ToolbarConfiguration*)toolbarConfiguration {
  _toolbarConfiguration = toolbarConfiguration;
  if (!toolbarConfiguration)
    return;
  _spotlightView.backgroundColor =
      self.toolbarConfiguration.buttonsIPHHighlightColor;
  [self updateTintColor];
}

- (void)hasBlueDot:(BOOL)hasBlueDot {
  if (_hasBlueDot == hasBlueDot) {
    return;
  }

  _hasBlueDot = hasBlueDot;

  // TODO(crbug.com/338249447): Add or remove the blue dot.
}

#pragma mark - Accessors

- (UIView*)spotlightView {
  // Lazy load spotlightView to improve startup latency.
  if (!_spotlightView) {
    [self createSpotlightViewIfNeeded];
  }
  return _spotlightView;
}

- (UIImage*)image {
  // Lazy load image to improve startup latency.
  if (!_image) {
    _image = _imageLoader();
  }
  return _image;
}

- (UIImage*)IPHHighlightedImage {
  // Lazy load IPHHighlightedImage to improve startup latency.
  if (!_IPHHighlightedImage && _IPHHighlightedImageLoader) {
    _IPHHighlightedImage = _IPHHighlightedImageLoader();
  }
  return _IPHHighlightedImage;
}

#pragma mark - Private

- (void)initializeButton {
  self.translatesAutoresizingMaskIntoConstraints = NO;

  __weak __typeof(self) weakSelf = self;
  CustomHighlightableButtonHighlightHandler handler = ^(BOOL highlighted) {
    [weakSelf setIphHighlighted:highlighted];
  };
  [self setCustomHighlightHandler:handler];
}

// Creates spotlightView if not done yet.
- (void)createSpotlightViewIfNeeded {
  if (_spotlightView) {
    return;
  }
  UIView* spotlightView = [[UIView alloc] init];
  spotlightView.translatesAutoresizingMaskIntoConstraints = NO;
  spotlightView.hidden = YES;
  spotlightView.userInteractionEnabled = NO;
  spotlightView.layer.cornerRadius = kSpotlightCornerRadius;
  spotlightView.backgroundColor =
      self.toolbarConfiguration.buttonsIPHHighlightColor;
  // Make sure that the spotlightView is below the image to avoid changing the
  // color of the image.
  [self insertSubview:spotlightView belowSubview:self.imageView];
  AddSameCenterConstraints(self, spotlightView);
  [spotlightView.widthAnchor constraintEqualToConstant:kSpotlightSize].active =
      YES;
  [spotlightView.heightAnchor constraintEqualToConstant:kSpotlightSize].active =
      YES;
  _spotlightView = spotlightView;
}

// Checks if the button should be visible based on its hiddenInCurrentSizeClass
// and hiddenInCurrentState properties, then updates its visibility accordingly.
- (void)setHiddenForCurrentStateAndSizeClass {
  self.hidden = self.hiddenInCurrentState || self.hiddenInCurrentSizeClass;

  [self checkNamedGuide];
  [self checkImageVisibility];
}

// Checks whether the named guide associated with this button, if there is one,
// should be updated.
- (void)checkNamedGuide {
  if (!self.hidden && self.guideName) {
    [self.layoutGuideCenter referenceView:self underName:self.guideName];
  }
}

// Checks whether the image is set when the button visibility is changed, if the
// button is visible and the image is not set, update the image.
- (void)checkImageVisibility {
  // Use `self.currentImage` to check whether the image is set,
  // `self.imageView.image` is a costly call from the Instruments measurement.
  if (!self.hidden && !self.currentImage) {
    [self updateImage];
  }
}

// Updates the spotlight view's appearance according to the current state.
- (void)updateSpotlightView {
  self.spotlightView.hidden = !self.iphHighlighted;
}

- (void)updateImage {
  if (_iphHighlighted && [self canUseIPHHighlightedImage]) {
    [self setImage:self.IPHHighlightedImage forState:UIControlStateNormal];
  } else {
    [self setImage:self.image forState:UIControlStateNormal];
  }
}

// Updates the tint color according to the current state.
- (void)updateTintColor {
  self.tintColor =
      (self.iphHighlighted)
          ? self.toolbarConfiguration.buttonsTintColorIPHHighlighted
          : self.toolbarConfiguration.buttonsTintColor;
}

// Whether there is an IPH highlighted image can be used.
- (BOOL)canUseIPHHighlightedImage {
  return _IPHHighlightedImageLoader != nil;
}

@end