chromium/ios/chrome/browser/ui/omnibox/omnibox_container_view.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/omnibox/omnibox_container_view.h"

#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/fade_truncating_label.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/animation_util.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.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/omnibox/omnibox_constants.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_ui_features.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/pointer_interaction_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/grit/ios_theme_resources.h"
#import "skia/ext/skia_utils_ios.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/gfx/color_palette.h"
#import "ui/gfx/image/image.h"

namespace {

/// Width of the thumbnail.
const CGFloat kThumbnailWidth = 48;
/// Height of the thumbnail.
const CGFloat kThumbnailHeight = 40;
/// Corner radius of the thumbnail image.
const CGFloat kThumbnailImageCornerRadius = 12;
/// Space between the thumbnail image and the omnibox text.
const CGFloat kThumbnailImageTrailingMargin = 10;
/// Space between the leading icon and the thumbnail image.
const CGFloat kThumbnailImageLeadingMargin = 10;

/// Space between the clear button and the edge of the omnibox.
const CGFloat kTextFieldClearButtonTrailingOffset = 4;

/// Clear button inset on all sides.
const CGFloat kClearButtonInset = 4.0f;
/// Clear button image size.
const CGFloat kClearButtonImageSize = 17.0f;
/// Clear button size.
const CGFloat kClearButtonSize = 28.5f;

}  // namespace

#pragma mark - OmniboxContainerView

@interface OmniboxContainerView ()

// Redefined as readwrite.
@property(nonatomic, strong) OmniboxTextFieldIOS* textField;
// Redefined as readwrite.
@property(nonatomic, strong) UIButton* clearButton;

@end

@implementation OmniboxContainerView {
  /// The leading image view. Used for autocomplete icons.
  UIImageView* _leadingImageView;
  /// Thumbnail image used in image search.
  UIImageView* _thumbnailImageView;
  /// UILabel for additional text.
  FadeTruncatingLabel* _additionalTextLabel;
  /// Horizontal stack view containing the `_leadingImageView` ,
  /// `_thumbnailImageView`, `_textScrollView` and `clearButton`.
  UIStackView* _stackView;

  /// Horizontal scroll view containing `_textStackView`.
  UIScrollView* _textScrollView;
  /// Horizontal stack view containing the `textField` and
  /// `_additionalTextLabel` to allow scrolling the additional text.
  UIStackView* _textStackView;
}

#pragma mark - Public

- (instancetype)initWithFrame:(CGRect)frame
                    textColor:(UIColor*)textColor
                textFieldTint:(UIColor*)textFieldTint
                     iconTint:(UIColor*)iconTint {
  self = [super initWithFrame:frame];
  if (self) {
    _textField = [[OmniboxTextFieldIOS alloc] initWithFrame:frame
                                                  textColor:textColor
                                                  tintColor:textFieldTint];
    _textField.translatesAutoresizingMaskIntoConstraints = NO;

    // Leading image view.
    _leadingImageView = [[UIImageView alloc] init];
    _leadingImageView.translatesAutoresizingMaskIntoConstraints = NO;
    _leadingImageView.contentMode = UIViewContentModeCenter;
    _leadingImageView.tintColor = iconTint;
    [NSLayoutConstraint activateConstraints:@[
      [_leadingImageView.widthAnchor
          constraintEqualToConstant:kOmniboxLeadingImageSize],
      [_leadingImageView.heightAnchor
          constraintEqualToConstant:kOmniboxLeadingImageSize],
    ]];

    // Stack view.
    _stackView =
        [[UIStackView alloc] initWithArrangedSubviews:@[ _leadingImageView ]];
    _stackView.translatesAutoresizingMaskIntoConstraints = NO;
    _stackView.axis = UILayoutConstraintAxisHorizontal;
    _stackView.alignment = UIStackViewAlignmentCenter;
    _stackView.spacing = 0;
    _stackView.distribution = UIStackViewDistributionFill;
    [self addSubview:_stackView];
    AddSameConstraintsWithInsets(
        _stackView, self,
        NSDirectionalEdgeInsetsMake(0, kOmniboxLeadingImageViewEdgeOffset, 0,
                                    kTextFieldClearButtonTrailingOffset));

    // Thumbnail image view.
    if (base::FeatureList::IsEnabled(kEnableLensOverlay)) {
      _thumbnailImageView = [[UIImageView alloc] init];
      _thumbnailImageView.translatesAutoresizingMaskIntoConstraints = NO;
      _thumbnailImageView.contentMode = UIViewContentModeCenter;
      _thumbnailImageView.backgroundColor = UIColor.clearColor;
      _thumbnailImageView.layer.cornerRadius = kThumbnailImageCornerRadius;
      _thumbnailImageView.clipsToBounds = YES;
      _thumbnailImageView.hidden = YES;
      [NSLayoutConstraint activateConstraints:@[
        [_thumbnailImageView.widthAnchor
            constraintEqualToConstant:kThumbnailWidth],
        [_thumbnailImageView.heightAnchor
            constraintEqualToConstant:kThumbnailHeight],
      ]];
      [_stackView addArrangedSubview:_thumbnailImageView];
      // Spacing between thumbnail and text field.
      [_stackView setCustomSpacing:kThumbnailImageTrailingMargin
                         afterView:_thumbnailImageView];

      // Button to delete the thumbnail.
      _thumbnailButton = [[UIButton alloc] init];
      _thumbnailButton.translatesAutoresizingMaskIntoConstraints = NO;
      _thumbnailButton.backgroundColor = UIColor.clearColor;
      _thumbnailButton.tintColor = UIColor.whiteColor;
      [NSLayoutConstraint activateConstraints:@[
        [_thumbnailButton.widthAnchor
            constraintEqualToConstant:kThumbnailWidth],
        [_thumbnailButton.heightAnchor
            constraintEqualToConstant:kThumbnailHeight],
      ]];
      UIImage* selectedImage = MakeSymbolMonochrome(
          DefaultSymbolWithPointSize(kXMarkSymbol, kSymbolActionPointSize));
      [_thumbnailButton setImage:selectedImage forState:UIControlStateSelected];
      [_thumbnailButton
          setBackgroundImage:ImageWithColor([UIColor.systemBlueColor
                                 colorWithAlphaComponent:0.5])
                    forState:UIControlStateSelected];
      [_thumbnailImageView addSubview:_thumbnailButton];
      AddSameCenterConstraints(_thumbnailButton, _thumbnailImageView);

      _thumbnailImageView.userInteractionEnabled = YES;
    }

    if (IsRichAutocompletionEnabled(RichAutocompletionImplementation::kLabel)) {
      // Text scroll view.
      _textScrollView = [[UIScrollView alloc] init];
      _textScrollView.translatesAutoresizingMaskIntoConstraints = NO;
      _textScrollView.showsHorizontalScrollIndicator = NO;
      _textScrollView.showsVerticalScrollIndicator = NO;
      [_stackView addArrangedSubview:_textScrollView];

      // Additional text.
      _additionalTextLabel = [[FadeTruncatingLabel alloc] init];
      _additionalTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
      _additionalTextLabel.isAccessibilityElement = NO;
      _additionalTextLabel.clipsToBounds = YES;
      _additionalTextLabel.hidden = YES;
      _additionalTextLabel.lineBreakMode = NSLineBreakByClipping;
      _additionalTextLabel.displayAsURL = YES;
      _additionalTextLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];

      // Text stack view.
      _textStackView = [[UIStackView alloc]
          initWithArrangedSubviews:@[ _textField, _additionalTextLabel ]];
      _textStackView.translatesAutoresizingMaskIntoConstraints = NO;
      _textStackView.axis = UILayoutConstraintAxisHorizontal;
      _textStackView.alignment = UIStackViewAlignmentCenter;
      _textStackView.spacing = 0;
      _textStackView.distribution = UIStackViewDistributionFill;
      [_textScrollView addSubview:_textStackView];
      AddSameConstraints(_textScrollView, _textStackView);

      [NSLayoutConstraint activateConstraints:@[
        [_textScrollView.heightAnchor
            constraintEqualToAnchor:_textStackView.heightAnchor],
        // Limit text field width to scroll view width to allow correct handling
        // of the caret by UITextField.
        [_textField.widthAnchor
            constraintLessThanOrEqualToAnchor:_textScrollView.widthAnchor]
      ]];

    } else {  // !IsRichAutocompletionEnabled
      [_stackView addArrangedSubview:_textField];
    }

    // Clear button.
    UIButtonConfiguration* conf =
        [UIButtonConfiguration plainButtonConfiguration];
    conf.image = DefaultSymbolWithPointSize(kXMarkCircleFillSymbol,
                                            kClearButtonImageSize);
    conf.contentInsets =
        NSDirectionalEdgeInsetsMake(kClearButtonInset, kClearButtonInset,
                                    kClearButtonInset, kClearButtonInset);
    _clearButton = [UIButton buttonWithType:UIButtonTypeSystem];
    _clearButton.configuration = conf;
    _clearButton.tintColor = [UIColor colorNamed:kTextfieldPlaceholderColor];
    SetA11yLabelAndUiAutomationName(_clearButton, IDS_IOS_ACCNAME_CLEAR_TEXT,
                                    @"Clear Text");
    _clearButton.pointerInteractionEnabled = YES;
    _clearButton.pointerStyleProvider =
        CreateLiftEffectCirclePointerStyleProvider();
    // Do not use the system clear button. Use a custom view instead.
    _textField.clearButtonMode = UITextFieldViewModeNever;
    if (IsRichAutocompletionEnabled(RichAutocompletionImplementation::kLabel)) {
      [NSLayoutConstraint activateConstraints:@[
        [_clearButton.widthAnchor constraintEqualToConstant:kClearButtonSize],
        [_clearButton.heightAnchor constraintEqualToConstant:kClearButtonSize],
      ]];
      [_stackView addArrangedSubview:_clearButton];
    } else {
      // Note that `rightView` is an incorrect name, it's really a trailing
      // view.
      _textField.rightViewMode = UITextFieldViewModeAlways;
      _textField.rightView = _clearButton;
    }

    // Spacing between image and text field.
    [_stackView setCustomSpacing:kOmniboxTextFieldLeadingOffsetImage
                       afterView:_leadingImageView];

    // Constraints.
    AddSameConstraintsToSides(_textField, _stackView,
                              LayoutSides::kTop | LayoutSides::kBottom);
    if (IsRichAutocompletionEnabled(RichAutocompletionImplementation::kLabel)) {
      AddSameConstraintsToSides(_additionalTextLabel, _stackView,
                                LayoutSides::kTop | LayoutSides::kBottom);

      // Prevents the text field from taking more horizontal space than needed.
      // This allows the additional text to sit flush with the text in
      // `_textField`.
      [_textField setContentHuggingPriority:UILayoutPriorityDefaultHigh + 1
                                    forAxis:UILayoutConstraintAxisHorizontal];
      // Allow the additional text to take more horizontal space.
      [_additionalTextLabel
          setContentHuggingPriority:UILayoutPriorityDefaultLow
                            forAxis:UILayoutConstraintAxisHorizontal];
    }
    [_textField
        setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
                                        forAxis:
                                            UILayoutConstraintAxisHorizontal];
  }
  return self;
}

- (void)setLeadingImage:(UIImage*)image
    withAccessibilityIdentifier:(NSString*)accessibilityIdentifier {
  _leadingImageView.image = image;
  _leadingImageView.accessibilityIdentifier = accessibilityIdentifier;
}

- (void)setLeadingImageScale:(CGFloat)scaleValue {
  _leadingImageView.transform =
      CGAffineTransformMakeScale(scaleValue, scaleValue);
}

- (void)setThumbnailImage:(UIImage*)image {
  _thumbnailImageView.image = image;
  _thumbnailImageView.hidden = !image;

  if (image) {
    [_stackView setCustomSpacing:kThumbnailImageLeadingMargin
                       afterView:_leadingImageView];
  } else {
    [_stackView setCustomSpacing:kOmniboxTextFieldLeadingOffsetImage
                       afterView:_leadingImageView];
  }
}

- (void)setLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter {
  _layoutGuideCenter = layoutGuideCenter;
  [_layoutGuideCenter referenceView:_leadingImageView
                          underName:kOmniboxLeadingImageGuide];
  [_layoutGuideCenter referenceView:_textField
                          underName:kOmniboxTextFieldGuide];
}

- (void)setClearButtonHidden:(BOOL)isHidden {
  if (IsRichAutocompletionEnabled(RichAutocompletionImplementation::kLabel)) {
    _clearButton.hidden = isHidden;
  } else {
    self.textField.rightViewMode =
        isHidden ? UITextFieldViewModeNever : UITextFieldViewModeAlways;
  }
}

- (void)setSemanticContentAttribute:
    (UISemanticContentAttribute)semanticContentAttribute {
  [super setSemanticContentAttribute:semanticContentAttribute];
  _stackView.semanticContentAttribute = semanticContentAttribute;
}

- (void)updateAdditionalText:(NSString*)additionalText {
  CHECK(IsRichAutocompletionEnabled(RichAutocompletionImplementation::kAny));

  if (IsRichAutocompletionEnabled(
          RichAutocompletionImplementation::kTextField)) {
    // Additional text in text field.
    if (!additionalText) {
      _textField.additionalText = nil;
    } else {
      NSMutableAttributedString* addditionalAttributedText =
          [[NSMutableAttributedString alloc] initWithString:additionalText];
      [addditionalAttributedText
          addAttributes:@{
            NSForegroundColorAttributeName :
                [UIColor colorNamed:kTextSecondaryColor]
          }
                  range:NSMakeRange(0, addditionalAttributedText.length)];
      _textField.additionalText = addditionalAttributedText;
    }
  } else if (IsRichAutocompletionEnabled(
                 RichAutocompletionImplementation::kLabel)) {
    // Additional text in Label.
    _additionalTextLabel.text = additionalText;
    _additionalTextLabel.hidden = !additionalText.length;
    // Update the font here as `_textField` changes font for different dynamic
    // type. TODO(crbug.com/325035406): Refactor dynamic type handling.
    _additionalTextLabel.font = _textField.font;

    // The placeholder text prevents the text field from hugging to a size
    // smaller than `placeholder`. This prevents the additional text from
    // staying flush to the text field (crbug.com/326371877).
    if (_additionalTextLabel.hidden) {
      _textField.placeholder = l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT);
    } else {
      _textField.placeholder = nil;
    }
  }
}

- (void)setOmniboxHasRichInline:(BOOL)omniboxHasRichInline {
  CHECK(IsRichAutocompletionEnabled(
      RichAutocompletionImplementation::kNoAdditionalText));
  _textField.omniboxHasRichInline = omniboxHasRichInline;
}

#pragma mark - TextFieldViewContaining

- (UIView*)textFieldView {
  return self.textField;
}

@end