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

#import "base/check.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_toolbar_mutator.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/ui/omnibox/text_field_view_containing.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/ui_util.h"
#import "ios/components/ui_util/dynamic_type_util.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

/// Top padding for the view content.
const CGFloat kViewTopPadding = 19;

/// Width of the back button.
const CGFloat kBackButtonWidth = 44;
/// Size of the back button.
const CGFloat kBackButtonSize = 24;

/// Horizontal inset of the cancel button.
const CGFloat kCancelButtonHorizontalInset = 8;
/// Font size for the cancel button.
const CGFloat kCancelButtonFontSize = 15;

/// Minimum leading and trailing padding for the omnibox container.
const CGFloat kOmniboxContainerHorizontalPadding = 12;
/// Minimum height of the omnibox container.
const CGFloat kOmniboxContainerMinimumHeight = 42;
/// Minimum padding between the top of the view and the top of the web
/// container.
const CGFloat kWebContainerTopPadding = 8;

}  // namespace

@interface LensResultPageViewController ()

/// Web view in `_webViewContainer`.
@property(nonatomic, strong) UIView* webView;

/// Edit view contained in `_omniboxContainer`.
@property(nonatomic, strong) UIView<TextFieldViewContaining>* editView;

/// Whether the back button is available. The back button might be available but
/// hidden when the omnibox is focused.
@property(nonatomic, assign) BOOL canGoBack;

/// Whether the omnibox is currently focused.
@property(nonatomic, assign) BOOL omniboxFocused;

@end

@implementation LensResultPageViewController {
  /// Back button.
  UIButton* _backButton;
  /// Container for the omnibox.
  UIView* _omniboxContainer;
  /// Cancel button.
  UIButton* _cancelButton;
  /// StackView for the `_backButton`, `_omniboxContainer` and `_cancelButton`.
  UIStackView* _horizontalStackView;
  /// Container for the omnibox popup.
  UIButton* _omniboxPopupContainer;
  /// Button to focus the omnibox.
  UIButton* _omniboxTapTarget;
}

- (instancetype)init {
  self = [super init];
  if (self) {
    _webViewContainer = [[UIView alloc] init];
    _omniboxPopupContainer = [[UIButton alloc] init];

    // Initialize `setEditView` dependencies as it can be called before
    // `viewDidLoad`.
    _omniboxContainer = [[UIView alloc] init];
    _omniboxTapTarget = [[UIButton alloc] init];
    [_omniboxContainer addSubview:_omniboxTapTarget];
  }
  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];

  self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor];

  CHECK(self.webViewContainer, kLensOverlayNotFatalUntil);
  // Webview container.
  self.webViewContainer.translatesAutoresizingMaskIntoConstraints = NO;
  [self.view addSubview:self.webViewContainer];

  // Omnibox popup container.
  _omniboxPopupContainer.translatesAutoresizingMaskIntoConstraints = NO;
  _omniboxPopupContainer.hidden = YES;
  _omniboxPopupContainer.layer.zPosition = 1;
  _omniboxPopupContainer.clipsToBounds = YES;
  [_omniboxPopupContainer addTarget:self
                             action:@selector(didTapOmniboxPopupContainer:)
                   forControlEvents:UIControlEventTouchUpInside];
  [self.view addSubview:_omniboxPopupContainer];

  // Back Button.
  _backButton = [UIButton buttonWithType:UIButtonTypeSystem];
  _backButton.translatesAutoresizingMaskIntoConstraints = NO;
  _backButton.hidden = YES;
  UIImage* image =
      DefaultSymbolWithPointSize(kChevronBackwardSymbol, kBackButtonSize);
  [_backButton setImage:image forState:UIControlStateNormal];
  [_backButton addTarget:self
                  action:@selector(didTapBackButton:)
        forControlEvents:UIControlEventTouchUpInside];

  // Omnibox container.
  _omniboxContainer.translatesAutoresizingMaskIntoConstraints = NO;
  _omniboxContainer.backgroundColor = [UIColor colorNamed:kGrey200Color];
  _omniboxContainer.layer.cornerRadius = 21;
  [_omniboxContainer
      setContentHuggingPriority:UILayoutPriorityDefaultLow
                        forAxis:UILayoutConstraintAxisHorizontal];

  // Omnibox tap target.
  _omniboxTapTarget.translatesAutoresizingMaskIntoConstraints = NO;
  _omniboxTapTarget.backgroundColor = UIColor.clearColor;
  [_omniboxTapTarget addTarget:self
                        action:@selector(didTapOmniboxTapTarget:)
              forControlEvents:UIControlEventTouchUpInside];
  AddSameConstraints(_omniboxContainer, _omniboxTapTarget);

  // Cancel button.
  _cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
  _cancelButton.translatesAutoresizingMaskIntoConstraints = NO;
  _cancelButton.tintColor = [UIColor colorNamed:kBlueColor];
  [_cancelButton
      setContentCompressionResistancePriority:UILayoutPriorityRequired
                                      forAxis:UILayoutConstraintAxisHorizontal];
  [_cancelButton setContentHuggingPriority:UILayoutPriorityRequired
                                   forAxis:UILayoutConstraintAxisHorizontal];
  UIButtonConfiguration* buttonConfiguration =
      [UIButtonConfiguration plainButtonConfiguration];
  buttonConfiguration.contentInsets = NSDirectionalEdgeInsetsMake(
      0, kCancelButtonHorizontalInset, 0, kCancelButtonHorizontalInset);
  UIFont* font = [UIFont systemFontOfSize:kCancelButtonFontSize];
  NSDictionary* attributes = @{NSFontAttributeName : font};
  NSMutableAttributedString* attributedString =
      [[NSMutableAttributedString alloc]
          initWithString:l10n_util::GetNSString(IDS_CANCEL)
              attributes:attributes];
  buttonConfiguration.attributedTitle = attributedString;
  _cancelButton.configuration = buttonConfiguration;
  _cancelButton.hidden = YES;
  [_cancelButton addTarget:self
                    action:@selector(didTapCancelButton:)
          forControlEvents:UIControlEventTouchUpInside];

  // Horizontal stack view.
  _horizontalStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
    _backButton, _omniboxContainer, _cancelButton
  ]];
  _horizontalStackView.translatesAutoresizingMaskIntoConstraints = NO;
  _horizontalStackView.axis = UILayoutConstraintAxisHorizontal;
  _horizontalStackView.distribution = UIStackViewDistributionFill;
  [self.view addSubview:_horizontalStackView];

  NSLayoutConstraint* omniboxLeadingConstraint =
      [_omniboxContainer.leadingAnchor
          constraintEqualToAnchor:self.view.leadingAnchor
                         constant:kOmniboxContainerHorizontalPadding];
  omniboxLeadingConstraint.priority = UILayoutPriorityDefaultHigh;

  [NSLayoutConstraint activateConstraints:@[
    [_horizontalStackView.topAnchor
        constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor
                       constant:kViewTopPadding],
    [_backButton.widthAnchor constraintEqualToConstant:kBackButtonWidth],
    [_horizontalStackView.heightAnchor
        constraintGreaterThanOrEqualToConstant:kOmniboxContainerMinimumHeight],
    [_backButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
    omniboxLeadingConstraint,
    [self.view.trailingAnchor
        constraintEqualToAnchor:_horizontalStackView.trailingAnchor
                       constant:kOmniboxContainerHorizontalPadding],
    [_webViewContainer.topAnchor
        constraintEqualToAnchor:_horizontalStackView.bottomAnchor
                       constant:kWebContainerTopPadding],
    [_omniboxPopupContainer.topAnchor
        constraintEqualToAnchor:_horizontalStackView.bottomAnchor],
  ]];
  AddSameConstraintsToSides(
      self.webViewContainer, self.view,
      LayoutSides::kLeading | LayoutSides::kBottom | LayoutSides::kTrailing);
  AddSameConstraintsToSides(
      _omniboxPopupContainer, self.view,
      LayoutSides::kLeading | LayoutSides::kBottom | LayoutSides::kTrailing);
}

- (void)setEditView:(UIView<TextFieldViewContaining>*)editView {
  CHECK(!_editView, kLensOverlayNotFatalUntil);
  CHECK(editView, kLensOverlayNotFatalUntil);
  CHECK(_omniboxContainer, kLensOverlayNotFatalUntil);
  _editView = editView;
  _editView.translatesAutoresizingMaskIntoConstraints = NO;
  [_omniboxContainer insertSubview:_editView belowSubview:_omniboxTapTarget];
  AddSameConstraints(_editView, _omniboxContainer);
}

#pragma mark - UIResponder

- (BOOL)canBecomeFirstResponder {
  // Capture key command close when the omnibox is focused to defocus the
  // omnibox instead of closing the overlay.
  return !_omniboxPopupContainer.hidden;
}

- (NSArray<UIKeyCommand*>*)keyCommands {
  return @[ UIKeyCommand.cr_close ];
}

- (void)keyCommand_close {
  [self.toolbarMutator defocusOmnibox];
}

#pragma mark - LensResultPageConsumer

- (void)setWebView:(UIView*)webView {
  if (_webView == webView) {
    return;
  }

  if (_webView.superview == self.webViewContainer) {
    [_webView removeFromSuperview];
  }
  _webView = webView;

  _webView.translatesAutoresizingMaskIntoConstraints = NO;
  if (!_webView || !self.webViewContainer) {
    return;
  }

  [self.webViewContainer addSubview:_webView];
  AddSameConstraints(_webView, self.webViewContainer);
}

- (void)setBackgroundColor:(UIColor*)backgroundColor {
  self.view.backgroundColor = backgroundColor;
}

#pragma mark - OmniboxPopupPresenterDelegate

- (UIView*)popupParentViewForPresenter:(OmniboxPopupPresenter*)presenter {
  return _omniboxPopupContainer;
}

- (UIViewController*)popupParentViewControllerForPresenter:
    (OmniboxPopupPresenter*)presenter {
  return self;
}

- (GuideName*)omniboxGuideNameForPresenter:(OmniboxPopupPresenter*)presenter {
  return nil;
}

- (void)popupDidOpenForPresenter:(OmniboxPopupPresenter*)presenter {
}

- (void)popupDidCloseForPresenter:(OmniboxPopupPresenter*)presenter {
}

#pragma mark - LensToolbarConsumer

- (void)setOmniboxFocused:(BOOL)isFocused {
  _omniboxFocused = isFocused;
  [self updateBackButtonVisibility];

  // Visible when omnibox is focused.
  _cancelButton.hidden = !isFocused;
  _omniboxPopupContainer.hidden = !isFocused;

  // Hidden when omnibox is focused.
  _omniboxTapTarget.hidden = isFocused;
}

- (void)setCanGoBack:(BOOL)canGoBack {
  _canGoBack = canGoBack;
  [self updateBackButtonVisibility];
}

#pragma mark - Private

/// Handles back button taps.
- (void)didTapBackButton:(UIView*)button {
  [self.toolbarMutator goBack];
}

/// Handles omnibox tap target taps.
- (void)didTapOmniboxTapTarget:(UIView*)view {
  [self.toolbarMutator focusOmnibox];
}

/// Handles omnibox popup container taps, acting like a typing shield.
- (void)didTapOmniboxPopupContainer:(UIView*)view {
  [self.toolbarMutator defocusOmnibox];
}

/// Handles cancel button taps.
- (void)didTapCancelButton:(UIView*)button {
  [self.toolbarMutator defocusOmnibox];
}

- (void)updateBackButtonVisibility {
  _backButton.hidden = self.omniboxFocused || !self.canGoBack;
}

@end