chromium/content/browser/renderer_host/web_menu_runner_ios.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "content/browser/renderer_host/web_menu_runner_ios.h"

#include "base/strings/sys_string_conversions.h"

@interface UIContextMenuInteraction ()
- (void)_presentMenuAtLocation:(CGPoint)location;
@end

@interface WebMenuRunner () <UIContextMenuInteractionDelegate>
@end

@implementation WebMenuRunner {
  // The UIView in which the popup menu will be displayed.
  UIView* __weak _view;

  // The bounds of the select element from which the menu was triggered.
  CGRect _elementBounds;

  // The index of the selected menu item.
  size_t _selectedIndex;

  // A flag set to YES if a menu item was chosen, or NO if the menu was
  // dismissed without selecting an item.
  BOOL _menuItemWasChosen;

  // The native UIMenu object.
  UIMenu* __strong _menu;

  // Interaction for displaying a popup menu.
  UIContextMenuInteraction* __strong _selectContextMenuInteraction;

  // Delegate to handle menu select/cancel events.
  base::WeakPtr<content::MenuInteractionDelegate> _delegate;
}

- (id)initWithDelegate:(base::WeakPtr<content::MenuInteractionDelegate>)delegate
                 items:(const std::vector<blink::mojom::MenuItemPtr>&)items
          initialIndex:(int)index
              fontSize:(CGFloat)fontSize
          rightAligned:(BOOL)rightAligned {
  if ((self = [super init])) {
    _delegate = delegate;

    DCHECK_GE(index, 0);
    _selectedIndex = static_cast<size_t>(index);

    [self createMenu:items];
  }
  return self;
}

- (void)showMenuInView:(UIView*)view withBounds:(CGRect)bounds {
  _view = view;
  _elementBounds = bounds;

  _selectContextMenuInteraction =
      [[UIContextMenuInteraction alloc] initWithDelegate:self];
  [_view addInteraction:_selectContextMenuInteraction];

  // TODO(crbug.com/40274444): _presentMenuAtLocation is a private API
  // which triggers the ContextMenu immediately at a specified location. By
  // default, the ContextMenu is only triggered on long press or 3D touch. This
  // private API is needed to use because we expect the popup menu to appear
  // immediately when the user touches the <select> element area.
  [_selectContextMenuInteraction _presentMenuAtLocation:_elementBounds.origin];
}

- (void)dealloc {
  [_view removeInteraction:_selectContextMenuInteraction];
}

#pragma mark - UIContextMenuInteractionDelegate

// TODO(crbug.com/40266320): This menu is being shown with unwanted effects.
// Need to find a way to show just the menu without using private API.
- (UIContextMenuConfiguration*)contextMenuInteraction:
                                   (UIContextMenuInteraction*)interaction
                       configurationForMenuAtLocation:(CGPoint)location {
  return [UIContextMenuConfiguration
      configurationWithIdentifier:nil
                  previewProvider:nil
                   actionProvider:^UIMenu* _Nullable(
                       NSArray<UIMenuElement*>* _Nonnull suggestedActions) {
                     return self->_menu;
                   }];
}

- (UITargetedPreview*)contextMenuInteraction:
                          (UIContextMenuInteraction*)interaction
                               configuration:
                                   (UIContextMenuConfiguration*)configuration
       highlightPreviewForItemWithIdentifier:(id<NSCopying>)identifier {
  UIView* snapshotView = [_view resizableSnapshotViewFromRect:_elementBounds
                                           afterScreenUpdates:NO
                                                withCapInsets:UIEdgeInsetsZero];

  UIPreviewTarget* previewTarget = [[UIPreviewTarget alloc]
      initWithContainer:_view
                 center:CGPointMake(CGRectGetMidX(_elementBounds),
                                    CGRectGetMidY(_elementBounds))];

  return
      [[UITargetedPreview alloc] initWithView:snapshotView
                                   parameters:[[UIPreviewParameters alloc] init]
                                       target:previewTarget];
}

- (void)contextMenuInteraction:(UIContextMenuInteraction*)interaction
       willEndForConfiguration:(UIContextMenuConfiguration*)configuration
                      animator:(id<UIContextMenuInteractionAnimating>)animator {
  _menu = nil;
  if (!_delegate) {
    return;
  }

  if (_menuItemWasChosen) {
    _delegate->OnMenuItemSelected(_selectedIndex);
  } else {
    _delegate->OnMenuCanceled();
  }
}

#pragma mark - Private

// Creates the native UIMenu object using the provided list of menu items.
- (void)createMenu:(const std::vector<blink::mojom::MenuItemPtr>&)items {
  NSMutableArray* actions = [NSMutableArray array];

  for (size_t i = 0; i < items.size(); ++i) {
    UIAction* action = [self addItem:items[i] itemIndex:i];
    if (i == _selectedIndex) {
      action.state = UIMenuElementStateOn;
    }
    [actions addObject:action];
  }

  _menu = [UIMenu menuWithTitle:@""
                          image:nil
                     identifier:nil
                        options:UIMenuOptionsDisplayInline
                       children:actions];
}

// Worker function used during initialization.
- (UIAction*)addItem:(const blink::mojom::MenuItemPtr&)item
           itemIndex:(size_t)index {
  NSString* title = base::SysUTF8ToNSString(item->label.value_or(""));
  UIAction* itemAction =
      [UIAction actionWithTitle:title
                          image:nil
                     identifier:nil
                        handler:^(__kindof UIAction* action) {
                          [self menuItemSelected:index];
                        }];

  return itemAction;
}

// A callback for the menu controller object to call when an item is selected
// from the menu. This is not called if the menu is dismissed without a
// selection.
- (void)menuItemSelected:(size_t)index {
  _menuItemWasChosen = YES;
  _selectedIndex = index;
}

@end  // WebMenuRunner