chromium/ios/chrome/browser/ui/omnibox/popup/row/actions/actions_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/omnibox/popup/row/actions/actions_view.h"

#import "ios/chrome/browser/ui/omnibox/popup/row/actions/omnibox_popup_actions_row_content_configuration.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/actions/omnibox_popup_actions_row_delegate.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/actions/suggest_action.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 {
/// The scroll view height.
const CGFloat kOmniboxPopupActionsHeight = 44;
///  Space between buttons.
const CGFloat kButtonSpace = 8;
/// The padding between the button's image and title.
const CGFloat kButtonTitleImagePadding = 4;
/// The button's image size.
const CGFloat kIconSize = 13;
/// Actions leading padding.
const CGFloat kLeadingPadding = 61;
}  // namespace

@implementation ActionsView {
  UIScrollView* _actionsScrollView;
  UIStackView* _actionsStackView;
  OmniboxPopupActionsRowContentConfiguration* _configuration;
}

- (instancetype)initWithConfiguration:
    (OmniboxPopupActionsRowContentConfiguration*)configuration {
  self = [super init];
  if (self) {
    _configuration = configuration;

    _actionsScrollView = [[UIScrollView alloc] init];
    _actionsScrollView.translatesAutoresizingMaskIntoConstraints = NO;
    _actionsScrollView.showsHorizontalScrollIndicator = NO;

    _actionsStackView = [[UIStackView alloc] init];
    _actionsStackView.translatesAutoresizingMaskIntoConstraints = NO;
    _actionsStackView.alignment = UIStackViewAlignmentCenter;
    _actionsStackView.axis = UILayoutConstraintAxisHorizontal;
    _actionsStackView.spacing = kButtonSpace;

    [self setupActionsStackView:configuration];

    [_actionsScrollView addSubview:_actionsStackView];
    [self addSubview:_actionsScrollView];
    AddSameConstraints(self, _actionsScrollView);
    AddSameConstraintsWithInsets(
        _actionsStackView, _actionsScrollView.contentLayoutGuide,
        NSDirectionalEdgeInsetsMake(0, kLeadingPadding, 0, 0));

    [NSLayoutConstraint activateConstraints:@[
      [self.heightAnchor
          constraintGreaterThanOrEqualToConstant:kOmniboxPopupActionsHeight],
      [_actionsScrollView.heightAnchor
          constraintEqualToAnchor:_actionsStackView.heightAnchor],
      // Constraint the stackview's width to be able to scroll to semantic
      // leading.
      [_actionsStackView.widthAnchor
          constraintGreaterThanOrEqualToAnchor:_actionsScrollView.widthAnchor
                                      constant:-kLeadingPadding]
    ]];
  }
  return self;
}

- (void)setSemanticContentAttribute:
    (UISemanticContentAttribute)semanticContentAttribute {
  [super setSemanticContentAttribute:semanticContentAttribute];

  // prevent unnecessary semantic update and scrolling.
  if (_actionsScrollView.semanticContentAttribute == semanticContentAttribute) {
    return;
  }

  _actionsScrollView.semanticContentAttribute = semanticContentAttribute;
  _actionsStackView.semanticContentAttribute = semanticContentAttribute;

  [_actionsScrollView layoutIfNeeded];

  BOOL isRTL = [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:
                           semanticContentAttribute] ==
               UIUserInterfaceLayoutDirectionRightToLeft;

  CGFloat contentStartX = 0;

  if (isRTL) {
    contentStartX = MAX(_actionsScrollView.contentSize.width - 1, 0);
  }

  [_actionsScrollView scrollRectToVisible:CGRectMake(contentStartX, 0, 1, 1)
                                 animated:NO];
}

- (void)updateConfiguration:
    (OmniboxPopupActionsRowContentConfiguration*)configuration {
  _configuration = configuration;
  [self setupActionsStackView:configuration];
  [self scrollToHighlightedAction];
}

#pragma mark - Private

// Scrolls to the highlighted action, if there is any.
- (void)scrollToHighlightedAction {
  if (_configuration.highlightedActionIndex == NSNotFound) {
    return;
  }

  // Make sure the scroll view layout is done before computing frames.
  [_actionsScrollView layoutIfNeeded];

  NSUInteger highlightedActionIndex = _configuration.highlightedActionIndex;

  CHECK(_actionsStackView.arrangedSubviews.count > highlightedActionIndex);

  UIButton* highlightedActionButton =
      _actionsStackView.arrangedSubviews[highlightedActionIndex];

  CGRect frameInScrollViewCoordinates =
      [highlightedActionButton convertRect:highlightedActionButton.bounds
                                    toView:_actionsScrollView];
  CGRect frameWithPadding =
      CGRectInset(frameInScrollViewCoordinates, -kLeadingPadding, 0);
  [_actionsScrollView scrollRectToVisible:frameWithPadding animated:NO];
}

// Setup the actions buttons with the given configuration actions and adds them
// to the actions stack view.
- (void)setupActionsStackView:
    (OmniboxPopupActionsRowContentConfiguration*)configuration {
  // Clear out previous arranged buttons.
  for (UIView* view in _actionsStackView.arrangedSubviews) {
    [view removeFromSuperview];
  }

  for (NSUInteger i = 0; i < configuration.actions.count; i++) {
    SuggestAction* action = configuration.actions[i];

    BOOL isActionHighlighted = configuration.highlightedActionIndex == i;

    UIButton* actionButton =
        [[self class] actionButtonWithSuggestAction:action
                                        highlighted:isActionHighlighted];

    [_actionsStackView addArrangedSubview:actionButton];

    if (action.type == omnibox::ActionInfo_ActionType_CALL) {
      [actionButton addTarget:self
                       action:@selector(callButtonTapped)
             forControlEvents:UIControlEventTouchUpInside];
      actionButton.accessibilityLabel = l10n_util::GetNSString(
          IDS_IOS_CALL_OMNIBOX_ACTION_ACCESSIBILITY_LABEL);
    } else if (action.type == omnibox::ActionInfo_ActionType_DIRECTIONS) {
      [actionButton addTarget:self
                       action:@selector(directionsButtonTapped)
             forControlEvents:UIControlEventTouchUpInside];
      actionButton.accessibilityLabel = l10n_util::GetNSString(
          IDS_IOS_DIRECTIONS_OMNIBOX_ACTION_ACCESSIBILITY_LABEL);
    } else if (action.type == omnibox::ActionInfo_ActionType_REVIEWS) {
      [actionButton addTarget:self
                       action:@selector(reviewsButtonTapped)
             forControlEvents:UIControlEventTouchUpInside];
      actionButton.accessibilityLabel = l10n_util::GetNSString(
          IDS_IOS_REVIEWS_OMNIBOX_ACTION_ACCESSIBILITY_LABEL);
    }
  }

  // add a spacer to the stack view in order to fill the available stack view
  // width.
  UIView* spacerView = [[UIView alloc] init];
  spacerView.translatesAutoresizingMaskIntoConstraints = NO;
  [spacerView
      setContentCompressionResistancePriority:0
                                      forAxis:UILayoutConstraintAxisHorizontal];
  [_actionsStackView addArrangedSubview:spacerView];
}

// Creates a suggestion action button and setup its configutation attributes
// (eg. Call action button).
+ (UIButton*)actionButtonWithSuggestAction:(SuggestAction*)suggestAction
                               highlighted:(BOOL)highlighted {
  UIButton* button = [[UIButton alloc] init];

  [button
      setContentCompressionResistancePriority:UILayoutPriorityRequired
                                      forAxis:UILayoutConstraintAxisHorizontal];
  [button
      setContentCompressionResistancePriority:UILayoutPriorityRequired
                                      forAxis:UILayoutConstraintAxisVertical];

  button.accessibilityIdentifier =
      [suggestAction.class accessibilityIdentifierWithType:suggestAction.type
                                               highlighted:highlighted];

  NSAttributedString* attributedTitle = [[NSAttributedString alloc]
      initWithString:suggestAction.title
          attributes:@{
            NSFontAttributeName :
                [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]
          }];

  UIButtonConfiguration* configuration =
      highlighted ? [UIButtonConfiguration filledButtonConfiguration]
                  : [UIButtonConfiguration plainButtonConfiguration];
  configuration.imagePadding = kButtonTitleImagePadding;
  configuration.attributedTitle = attributedTitle;
  configuration.image = [SuggestAction imageIconForAction:suggestAction
                                                     size:kIconSize];
  if (!highlighted) {
    UIBackgroundConfiguration* backgroundConfig =
        [UIBackgroundConfiguration clearConfiguration];
    backgroundConfig.backgroundColor =
        [UIColor colorNamed:kTertiaryBackgroundColor];
    configuration.background = backgroundConfig;
  }

  button.configuration = configuration;

  [button sizeToFit];
  button.layer.cornerRadius = button.frame.size.height / 2;
  button.layer.masksToBounds = YES;

  return button;
}

/// Handles tap on call button
- (void)callButtonTapped {
  SuggestAction* callAction =
      [self actionWithType:omnibox::ActionInfo_ActionType_CALL];
  [_configuration.delegate
      omniboxPopupRowActionSelectedWithConfiguration:_configuration
                                              action:callAction];
}

/// Handles tap on directions button
- (void)directionsButtonTapped {
  SuggestAction* directionsAction =
      [self actionWithType:omnibox::ActionInfo_ActionType_DIRECTIONS];
  [_configuration.delegate
      omniboxPopupRowActionSelectedWithConfiguration:_configuration
                                              action:directionsAction];
}

/// Handles tap on reviews button
- (void)reviewsButtonTapped {
  SuggestAction* reviewsAction =
      [self actionWithType:omnibox::ActionInfo_ActionType_REVIEWS];
  [_configuration.delegate
      omniboxPopupRowActionSelectedWithConfiguration:_configuration
                                              action:reviewsAction];
}

/// Returns the action that corresponds to the given type.
- (SuggestAction*)actionWithType:(omnibox::ActionInfo::ActionType)actionType {
  for (SuggestAction* action in _configuration.actions) {
    if (action.type == actionType) {
      return action;
    }
  }
  return nil;
}

@end