// Copyright 2018 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/app_shim_remote_cocoa/render_widget_host_view_cocoa.h"

#include <AppKit/AppKit.h>
#include <Carbon/Carbon.h>  // for <HIToolbox/Events.h>

#include <algorithm>
#include <limits>
#include <optional>
#include <tuple>
#include <utility>

#import "base/apple/foundation_util.h"
#include "base/apple/owned_objc.h"
#include "base/debug/crash_logging.h"
#import "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/input/web_input_event_builders_mac.h"
#include "components/remote_cocoa/app_shim/immersive_mode_controller_cocoa.h"
#include "components/remote_cocoa/app_shim/ns_view_ids.h"
#import "content/browser/cocoa/system_hotkey_helper_mac.h"
#import "content/browser/cocoa/system_hotkey_map.h"
#include "content/browser/renderer_host/render_widget_host_view_mac.h"
#import "content/browser/renderer_host/render_widget_host_view_mac_editcommand_helper.h"
#include "content/common/features.h"
#include "content/public/browser/browser_accessibility_state.h"
#import "content/public/browser/render_widget_host_view_mac_delegate.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "content/public/common/content_features.h"
#include "skia/ext/skia_utils_mac.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/input/input_handler.mojom.h"
#include "ui/accessibility/accessibility_features.h"
#import "ui/accessibility/platform/browser_accessibility_cocoa.h"
#import "ui/accessibility/platform/browser_accessibility_mac.h"
#include "ui/accessibility/platform/browser_accessibility_manager_mac.h"
#import "ui/base/clipboard/clipboard_util_mac.h"
#import "ui/base/cocoa/appkit_utils.h"
#import "ui/base/cocoa/menu_utils.h"
#import "ui/base/cocoa/nsmenu_additions.h"
#import "ui/base/cocoa/nsmenuitem_additions.h"
#include "ui/base/cocoa/remote_accessibility_api.h"
#import "ui/base/cocoa/touch_bar_util.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/platform/platform_event_source.h"
#include "ui/gfx/geometry/clamp_float_geometry.h"
#include "ui/gfx/mac/coordinate_conversion.h"
#include "ui/gfx/native_ui_types.h"

using blink::WebGestureEvent;
using blink::WebInputEvent;
using blink::WebMouseEvent;
using blink::WebMouseWheelEvent;
using blink::WebTouchEvent;
using content::RenderWidgetHostViewMacEditCommandHelper;
using input::NativeWebKeyboardEvent;
using input::WebGestureEventBuilder;
using input::WebMouseEventBuilder;
using input::WebMouseWheelEventBuilder;
using input::WebTouchEventBuilder;
using remote_cocoa::RenderWidgetHostNSViewHostHelper;
using remote_cocoa::mojom::RenderWidgetHostNSViewHost;

namespace {

constexpr NSString* WebAutomaticQuoteSubstitutionEnabled =
    @"WebAutomaticQuoteSubstitutionEnabled";
constexpr NSString* const WebAutomaticDashSubstitutionEnabled =
    @"WebAutomaticDashSubstitutionEnabled";
constexpr NSString* const WebAutomaticTextReplacementEnabled =
    @"WebAutomaticTextReplacementEnabled";

constexpr NSString* const kGoogleJapaneseInputPrefix =
    @"com.google.inputmethod.Japanese.";

// A dummy RenderWidgetHostNSViewHostHelper implementation which no-ops all
// functions.
class DummyHostHelper : public RenderWidgetHostNSViewHostHelper {
 public:
  explicit DummyHostHelper() = default;

  DummyHostHelper(const DummyHostHelper&) = delete;
  DummyHostHelper& operator=(const DummyHostHelper&) = delete;

 private:
  // RenderWidgetHostNSViewHostHelper implementation.
  id GetAccessibilityElement() override { return nil; }
  id GetRootBrowserAccessibilityElement() override { return nil; }
  id GetFocusedBrowserAccessibilityElement() override { return nil; }
  void SetAccessibilityWindow(NSWindow* window) override {}
  void ForwardKeyboardEvent(const input::NativeWebKeyboardEvent& key_event,
                            const ui::LatencyInfo& latency_info) override {}
  void ForwardKeyboardEventWithCommands(
      const input::NativeWebKeyboardEvent& key_event,
      const ui::LatencyInfo& latency_info,
      const std::vector<blink::mojom::EditCommandPtr> commands) override {}
  void RouteOrProcessMouseEvent(
      const blink::WebMouseEvent& web_event) override {}
  void RouteOrProcessTouchEvent(
      const blink::WebTouchEvent& web_event) override {}
  void RouteOrProcessWheelEvent(
      const blink::WebMouseWheelEvent& web_event) override {}
  void ForwardMouseEvent(const blink::WebMouseEvent& web_event) override {}
  void ForwardWheelEvent(const blink::WebMouseWheelEvent& web_event) override {}
  void PinchEvent(blink::WebGestureEvent pinch_event,
                  bool is_synthetically_injected) override {}
  void SmartMagnifyEvent(const blink::WebGestureEvent& web_event) override {}
};

// Touch bar identifier.
NSString* const kWebContentTouchBarId = @"web-content";

constexpr int kWrapAroundDistance = 10000;

// Converts a point in AppKit global screen coordinates (origin at the
// bottom-left of the primary display, as returned by e.g.
// `NSEvent.mouseLocation`) to a Core Graphics global point (origin at
// the top-left of the primary display, as expected by
// `CGWarpMouseCursorPosition`).
CGPoint CGPointFromNSEventScreenPoint(NSPoint p) {
  return CGPointMake(p.x, NSMaxY(NSScreen.screens.firstObject.frame) - p.y);
}

// Whether a keyboard event has been reserved by macOS.
BOOL EventIsReservedBySystem(NSEvent* event) {
  return content::GetSystemHotkeyMap()->IsEventReserved(event);
}

NSEvent* CreateContextMenuKeyDownEvent(NSEvent* source_event) {
  return [NSEvent keyEventWithType:NSEventTypeKeyDown
                          location:source_event.locationInWindow
                     modifierFlags:0
                         timestamp:source_event.timestamp
                      windowNumber:source_event.windowNumber
                           context:nil
                        characters:@""
       charactersIgnoringModifiers:@""
                         isARepeat:source_event.isARepeat
                           keyCode:kVK_ContextualMenu];
}

// Extract underline information from an attributed string. Inspired by
// `extractUnderlines` in
// https://github.com/WebKit/WebKit/blob/main/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm
void ExtractUnderlines(NSAttributedString* string,
                       std::vector<ui::ImeTextSpan>* ime_text_spans) {
  NSUInteger length = string.length;
  NSUInteger i = 0;
  while (i < length) {
    NSRange range;
    NSDictionary* attrs = [string attributesAtIndex:i
                              longestEffectiveRange:&range
                                            inRange:NSMakeRange(i, length - i)];
    if (NSNumber* style_attr = attrs[NSUnderlineStyleAttributeName]) {
      SkColor color = SK_ColorBLACK;
      if (NSColor* color_attr = attrs[NSUnderlineColorAttributeName]) {
        color = skia::NSDeviceColorToSkColor(
            [color_attr colorUsingColorSpace:NSColorSpace.deviceRGBColorSpace]);
      }

      // `NSUnderlineStyle` is the combination of a type enum with a pattern
      // style in the higher bits. Fold anything more complicated than a single
      // unstyled underline down to "thick" rather than "thin".
      ui::ImeTextSpan::Thickness thickness =
          style_attr.intValue > NSUnderlineStyleSingle
              ? ui::ImeTextSpan::Thickness::kThick
              : ui::ImeTextSpan::Thickness::kThin;
      ui::ImeTextSpan ui_ime_text_span = ui::ImeTextSpan(
          ui::ImeTextSpan::Type::kComposition, range.location,
          NSMaxRange(range), thickness, ui::ImeTextSpan::UnderlineStyle::kSolid,
          SK_ColorTRANSPARENT);
      ui_ime_text_span.underline_color = color;
      ime_text_spans->push_back(ui_ime_text_span);
    }
    i = NSMaxRange(range);
  }
}

}  // namespace

// RenderWidgetHostViewCocoa ---------------------------------------------------

// Private methods:
@interface RenderWidgetHostViewCocoa ()

@property(readonly) NSSpellChecker* spellChecker;

@property(getter=isAutomaticTextReplacementEnabled)
    BOOL automaticTextReplacementEnabled;
@property(getter=isAutomaticQuoteSubstitutionEnabled)
    BOOL automaticQuoteSubstitutionEnabled;
@property(getter=isAutomaticDashSubstitutionEnabled)
    BOOL automaticDashSubstitutionEnabled;

// Tracks the window for which the "onWindowDidResignKey" notification was
// deferred.
@property(weak, nonatomic, class) NSWindow* deferredResignKeyWindow;

- (void)keyEvent:(NSEvent*)theEvent wasKeyEquivalent:(BOOL)equiv;
- (void)windowDidChangeScreenOrBackingProperties:(NSNotification*)notification;
- (void)windowChangedGlobalFrame:(NSNotification*)notification;
- (void)windowDidBecomeKey:(NSNotification*)notification;
- (void)windowDidResignKey:(NSNotification*)notification;
- (void)sendViewBoundsInWindowToHost;
- (void)requestTextSubstitutions;
- (void)requestTextSuggestions;
- (void)sendWindowFrameInScreenToHost;
- (bool)hostIsDisconnected;
- (void)invalidateTouchBar;

// NSCandidateListTouchBarItemDelegate implementation
- (void)candidateListTouchBarItem:(NSCandidateListTouchBarItem*)anItem
     endSelectingCandidateAtIndex:(NSInteger)index;
- (void)candidateListTouchBarItem:(NSCandidateListTouchBarItem*)anItem
    changedCandidateListVisibility:(BOOL)isVisible;
@end

namespace {

gfx::PointF GetSanitizedFlippedPoint(NSPoint point, CGFloat height) {
  return gfx::PointF(gfx::ClampFloatGeometry(point.x),
                     gfx::ClampFloatGeometry(height - point.y));
}

}  // namespace

@implementation RenderWidgetHostViewCocoa {
  // Dummy host and host helper that are always valid (see comments below about
  // _host).
  // These need to be declared before |_host| and |_hostHelper| so that it
  // gets destroyed last.
  mojo::Remote<remote_cocoa::mojom::RenderWidgetHostNSViewHost> _dummyHost;
  std::unique_ptr<remote_cocoa::RenderWidgetHostNSViewHostHelper>
      _dummyHostHelper;

  // The communications channel to the RenderWidgetHostViewMac. This pointer is
  // always valid. When the original host disconnects, |_host| is changed to
  // point to |_dummyHost|, to avoid having to preface every dereference with
  // a nullptr check.
  raw_ptr<remote_cocoa::mojom::RenderWidgetHostNSViewHost> _host;

  // A separate host interface for the parts of the interface to
  // RenderWidgetHostViewMac that cannot or should not be forwarded over mojo.
  // This includes events (where the extra translation is unnecessary or loses
  // information) and access to accessibility structures (only present in the
  // browser process).
  raw_ptr<remote_cocoa::RenderWidgetHostNSViewHostHelper> _hostHelper;

  // This ivar is the cocoa delegate of the NSResponder.
  NSObject<RenderWidgetHostViewMacDelegate>* __strong _responderDelegate;
  NSRange _preContextualMenuSelectionRange;
  BOOL _willInvokeContextMenuWithEmptySelection;
  BOOL _textSelectionChangedForContextualMenu;
  BOOL _canBeKeyView;
  BOOL _closeOnDeactivate;
  std::unique_ptr<content::RenderWidgetHostViewMacEditCommandHelper>
      _editCommandHelper;

  // Is YES if there was a mouse-down as yet unbalanced with a mouse-up.
  BOOL _hasOpenMouseDown;

  // The cursor for the page. This is passed up from the renderer.
  NSCursor* __strong _currentCursor;

  // Is YES if the cursor is hidden by key events.
  BOOL _cursorHidden;

  // Controlled by setShowingContextMenu.
  BOOL _showingContextMenu;

  // Controlled by setSupportsAutoFill.
  BOOL _supportsAutoFill;

  // Set during -setFrame to avoid spamming host_ with origin and size
  // changes.
  BOOL _inSetFrame;

  // Variables used by our implementation of the NSTextInput protocol.
  // An input method of Mac calls the methods of this protocol not only to
  // notify an application of its status, but also to retrieve the status of
  // the application. That is, an application cannot control an input method
  // directly.
  // This object keeps the status of a composition of the renderer and returns
  // it when an input method asks for it.
  // We need to implement Objective-C methods for the NSTextInput protocol. On
  // the other hand, we need to implement a C++ method for an IPC-message
  // handler which receives input-method events from the renderer.

  // keyCode value of an NSEvent. This field has a value while we're handling
  // a key down event.
  std::optional<unsigned short> _currentKeyDownCode;

  // Indicates if a reconversion (which means a piece of committed text becomes
  // part of the composition again) is triggered in Japanese IME when Live
  // Conversion is on.
  BOOL _isReconversionTriggered;

  // Indicates if there is any marked text.
  BOOL _hasMarkedText;

  // Indicates if unmarkText is called or not when handling a keyboard
  // event.
  BOOL _unmarkTextCalled;

  // The range of current marked text inside the whole content of the DOM node
  // being edited.
  // TODO(suzhe): This is currently a fake value, as we do not support accessing
  // the whole content yet.
  NSRange _markedRange;

  // The text selection, cached from the RenderWidgetHostView.
  // |_availableText| contains the selected text and is a substring of the
  // full string in the renderer.
  std::u16string _availableText;
  size_t _availableTextOffset;
  NSUInteger _availableTextChangeCounter;
  gfx::Range _textSelectionRange;

  // The composition range, cached from the RenderWidgetHostView. This is only
  // ever updated from the renderer (unlike |_markedRange|, which sometimes but
  // not always coincides with |_compositionRange|).
  gfx::Range _compositionRange;

  // Text to be inserted which was generated by handling a key down event.
  std::u16string _textToBeInserted;

  // Marked text which was generated by handling a key down event.
  std::u16string _markedText;

  // Selected range of |markedText_|.
  NSRange _markedTextSelectedRange;

  // Underline information of the |markedText_|.
  std::vector<ui::ImeTextSpan> _imeTextSpans;

  // Replacement range information received from |setMarkedText:|.
  gfx::Range _setMarkedTextReplacementRange;

  // Indicates if doCommandBySelector method receives any edit command when
  // handling a key down event.
  BOOL _hasEditCommands;

  // Contains edit commands received by the -doCommandBySelector: method when
  // handling a key down event, not including inserting commands, eg. insertTab,
  // etc.
  std::vector<blink::mojom::EditCommandPtr> _editCommands;

  // Whether the previous mouse event was ignored due to hitTest check.
  BOOL _mouseEventWasIgnored;

  // Event monitor for scroll wheel end event.
  id __strong _endWheelMonitor;

  // This is used to indicate if a stylus is currently in the proximity of the
  // tablet.
  bool _isStylusEnteringProximity;
  blink::WebPointerProperties::PointerType _pointerType;

  // The set of key codes from key down events that we haven't seen the matching
  // key up events yet.
  // Used for filtering out non-matching NSEventTypeKeyUp events.
  std::set<unsigned short> _unmatchedKeyDownCodes;

  // The filter used to guide touch events towards a horizontal or vertical
  // orientation.
  content::MouseWheelRailsFilterMac _mouseWheelFilter;

  bool _mouseLocked;
  bool _mouseLockUnacceleratedMovement;
  gfx::PointF _lastMouseScreenPosition;

  // The cursor's screen position (AppKit coordinates) at the moment we
  // entered pointer lock. `std::nullopt` when not locked.
  std::optional<NSPoint> _preLockCursorScreenPosition;

  // When pointer lock re-centers the cursor, the OS generates an artificial
  // mouse movement. Ignore the initial events until the re-centering delta
  // has been fully absorbed to avoid exposing this artificial movement.
  bool _suppressNextLockedMouseMoveForRecenter;

  // The parent accessibility element. This is set only in the browser process.
  id __strong _accessibilityParent;

  uint64_t _popupParentNSViewId;

  bool _keyboardLockActive;
  std::optional<base::flat_set<ui::DomCode>> _lockedKeys;

  NSCandidateListTouchBarItem* __strong _candidateListTouchBarItem;
  NSInteger _textSuggestionsSequenceNumber;
  BOOL _shouldRequestTextSubstitutions;
  BOOL _substitutionWasApplied;
  bool _sonomaAccessibilityRefinementsAreActive;
  std::unique_ptr<content::ScopedAccessibilityMode> _basic_accessibility_mode;
}

@synthesize markedRange = _markedRange;
@synthesize textInputType = _textInputType;
@synthesize textInputFlags = _textInputFlags;
@synthesize spellCheckerForTesting = _spellCheckerForTesting;

// Static storage for the class property deferredResignKeyWindow
static NSWindow* __weak _deferredResignKeyWindow;

+ (void)initialize {
  RenderWidgetHostViewMacEditCommandHelper::AddEditingSelectorsToClass(self);
}

- (instancetype)initWithHost:(RenderWidgetHostNSViewHost*)host
              withHostHelper:(RenderWidgetHostNSViewHostHelper*)hostHelper {
  self = [super initWithFrame:NSZeroRect tracking:YES];
  if (self) {
    // Enable trackpad touches ("direct" touches are touchbar touches).
    self.allowedTouchTypes |= NSTouchTypeMaskIndirect;
    _editCommandHelper =
        std::make_unique<RenderWidgetHostViewMacEditCommandHelper>();

    _host = host;
    _hostHelper = hostHelper;
    _canBeKeyView = YES;
    _supportsAutoFill = YES;
    _isStylusEnteringProximity = false;
    _keyboardLockActive = false;
    _textInputType = ui::TEXT_INPUT_TYPE_NONE;
    _sonomaAccessibilityRefinementsAreActive =
        base::mac::MacOSVersion() >= 14'00'00 &&
        base::FeatureList::IsEnabled(
            features::kSonomaAccessibilityActivationRefinements);
  }
  return self;
}

- (void)dealloc {
  DCHECK([self hostIsDisconnected]);
  [[NSNotificationCenter defaultCenter] removeObserver:self];

  // Update and cache the new input context. Otherwise,
  // [NSTextInputContext currentInputContext] might still hold on to this
  // view's NSTextInputContext even after it's deallocated.
  // See http://crbug.com/684388.
  [[self window] makeFirstResponder:nil];
  [NSApp updateWindows];
}

- (void)sendViewBoundsInWindowToHost {
  TRACE_EVENT0("browser",
               "RenderWidgetHostViewCocoa::sendViewBoundsInWindowToHost");
  if (_inSetFrame)
    return;

  NSRect viewBoundsInView = [self bounds];
  NSWindow* enclosingWindow = [self window];
  if (!enclosingWindow) {
    _host->OnBoundsInWindowChanged(gfx::Rect(viewBoundsInView), false);
    return;
  }

  NSRect viewBoundsInWindow = [self convertRect:viewBoundsInView toView:nil];
  gfx::Rect gfxViewBoundsInWindow(viewBoundsInWindow);
  gfxViewBoundsInWindow.set_y(NSHeight([enclosingWindow frame]) -
                              NSMaxY(viewBoundsInWindow));
  _host->OnBoundsInWindowChanged(gfxViewBoundsInWindow, true);
}

- (NSSpellChecker*)spellChecker {
  if (_spellCheckerForTesting)
    return _spellCheckerForTesting;
  return NSSpellChecker.sharedSpellChecker;
}

- (void)requestTextSubstitutions {
  NSTextCheckingType textCheckingTypes =
      self.allowedTextCheckingTypes & self.enabledTextCheckingTypes;
  if (!textCheckingTypes)
    return;

  NSString* availableText = base::SysUTF16ToNSString(_availableText);

  if (!availableText)
    return;

  auto* textCheckingResults =
      [self.spellChecker checkString:availableText
                               range:NSMakeRange(0, availableText.length)
                               types:textCheckingTypes
                             options:nil
              inSpellDocumentWithTag:0
                         orthography:nullptr
                           wordCount:nullptr];

  NSUInteger cursorLocation = _textSelectionRange.start();
  NSTextCheckingResult* candidateResult;
  for (NSTextCheckingResult* result in textCheckingResults) {
    NSTextCheckingResult* adjustedResult =
        [result resultByAdjustingRangesWithOffset:_availableTextOffset];
    if (!NSLocationInRange(cursorLocation,
                           NSMakeRange(adjustedResult.range.location,
                                       adjustedResult.range.length + 1)))
      continue;
    constexpr NSTextCheckingType textCheckingTypesToReplaceImmediately =
        NSTextCheckingTypeQuote | NSTextCheckingTypeDash;
    if (adjustedResult.resultType & textCheckingTypesToReplaceImmediately) {
      [self insertText:adjustedResult.replacementString
          replacementRange:adjustedResult.range];
      continue;
    }
    candidateResult = adjustedResult;
  }
  if (!candidateResult)
    return;

  NSRect textRectInScreenCoordinates =
      [self firstRectForCharacterRange:candidateResult.range
                           actualRange:nullptr];
  NSRect textRectInWindowCoordinates =
      [self.window convertRectFromScreen:textRectInScreenCoordinates];
  NSRect textRectInViewCoordinates =
      [self convertRect:textRectInWindowCoordinates fromView:nil];

  NSUInteger capturedChangeCounter = _availableTextChangeCounter;

  [self.spellChecker
      showCorrectionIndicatorOfType:NSCorrectionIndicatorTypeDefault
                      primaryString:candidateResult.replacementString
                 alternativeStrings:candidateResult.alternativeStrings
                    forStringInRect:textRectInViewCoordinates
                               view:self
                  completionHandler:^(NSString* acceptedString) {
                    [self didAcceptReplacementString:acceptedString
                               forTextCheckingResult:candidateResult
                                    withChangeNumber:capturedChangeCounter];
                  }];
}

- (void)didAcceptReplacementString:(NSString*)acceptedString
             forTextCheckingResult:(NSTextCheckingResult*)correction
                  withChangeNumber:(NSUInteger)changeNumber {
  // TODO: Keep NSSpellChecker up to date on the user's response via
  // -recordResponse:toCorrection:forWord:language:inSpellDocumentWithTag:.
  // Call it to report whether they initially accepted or rejected the
  // suggestion, but also if they edit, revert, etc. later.

  if (acceptedString == nil)
    return;

  NSRange availableTextRange =
      NSMakeRange(_availableTextOffset, _availableText.length());

  if (NSMaxRange(correction.range) > NSMaxRange(availableTextRange))
    return;

  NSAttributedString* attString = [[NSAttributedString alloc]
      initWithString:base::SysUTF16ToNSString(_availableText)];
  NSRange trailingRange = NSMakeRange(
      NSMaxRange(correction.range),
      NSMaxRange(availableTextRange) - NSMaxRange(correction.range));

  if (trailingRange.length > 0 &&
      trailingRange.location < NSMaxRange(availableTextRange)) {
    NSRange trailingRangeInAvailableText = NSMakeRange(
        trailingRange.location - _availableTextOffset, trailingRange.length);
    NSString* trailingString =
        [attString.string substringWithRange:trailingRangeInAvailableText];
    if ([self.spellChecker preventsAutocorrectionBeforeString:trailingString
                                                     language:nil])
      return;

    // Gather some info in case -doubleClickAtIndex: throws an exception.
    // This change will eventually be reverted.
    NSString* info = [NSString
        stringWithFormat:@"%lu == %lu %lu %@ %@ %@ %@", changeNumber,
                         _availableTextChangeCounter, attString.string.length,
                         NSStringFromRange(availableTextRange),
                         NSStringFromRange(correction.range),
                         NSStringFromRange(trailingRange),
                         NSStringFromRange(trailingRangeInAvailableText)];
    SCOPED_CRASH_KEY_STRING256("RenderWidgetHostViewCocoa", "didAcceptTR",
                               base::SysNSStringToUTF8(info));

    if ([attString doubleClickAtIndex:trailingRangeInAvailableText.location]
            .location < trailingRangeInAvailableText.location)
      return;
  }

  _substitutionWasApplied = YES;
  [self insertText:acceptedString replacementRange:correction.range];
}

- (void)requestTextSuggestions {
  auto* touchBarItem = _candidateListTouchBarItem;
  if (!touchBarItem) {
    return;
  }
  [touchBarItem
      updateWithInsertionPointVisibility:_textSelectionRange.is_empty()];
  if (_textInputType == ui::TEXT_INPUT_TYPE_PASSWORD ||
      _textInputFlags & ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD ||
      _textInputFlags & ui::TEXT_INPUT_FLAG_HAS_BEEN_CUSTOM_PASSWORD) {
    return;
  }
  if (!touchBarItem.candidateListVisible) {
    return;
  }
  if (!_textSelectionRange.IsValid() ||
      _availableTextOffset > _textSelectionRange.GetMin()) {
    return;
  }

  NSRange selectionRange = _textSelectionRange.ToNSRange();
  NSString* selectionText = base::SysUTF16ToNSString(_availableText);
  selectionRange.location -= _availableTextOffset;
  if (NSMaxRange(selectionRange) > selectionText.length) {
    return;
  }

  // TODO: Fetch the spell document tag from the renderer (or equivalent).
  _textSuggestionsSequenceNumber = [self.spellChecker
      requestCandidatesForSelectedRange:selectionRange
                               inString:selectionText
                                  types:NSTextCheckingAllSystemTypes
                                options:nil
                 inSpellDocumentWithTag:0
                      completionHandler:^(
                          NSInteger sequenceNumber,
                          NSArray<NSTextCheckingResult*>* candidates) {
                        dispatch_async(dispatch_get_main_queue(), ^{
                          if (sequenceNumber !=
                              self->_textSuggestionsSequenceNumber) {
                            return;
                          }
                          [touchBarItem setCandidates:candidates
                                     forSelectedRange:selectionRange
                                             inString:selectionText];
                        });
                      }];
}

- (NSTextCheckingType)allowedTextCheckingTypes {
  if (_textInputType == ui::TEXT_INPUT_TYPE_NONE) {
    return 0;
  }
  if (_textInputType == ui::TEXT_INPUT_TYPE_PASSWORD) {
    return 0;
  }
  if (_textInputFlags & ui::TEXT_INPUT_FLAG_AUTOCORRECT_OFF) {
    return 0;
  }
  NSTextCheckingType checkingTypes = NSTextCheckingTypeReplacement;
  if (!(_textInputFlags & ui::TEXT_INPUT_FLAG_SPELLCHECK_OFF)) {
    checkingTypes |= NSTextCheckingTypeQuote | NSTextCheckingTypeDash;
  }
  return checkingTypes;
}

- (NSTextCheckingType)enabledTextCheckingTypes {
  NSTextCheckingType checkingTypes = 0;
  if (self.automaticQuoteSubstitutionEnabled) {
    checkingTypes |= NSTextCheckingTypeQuote;
  }
  if (self.automaticDashSubstitutionEnabled) {
    checkingTypes |= NSTextCheckingTypeDash;
  }
  if (self.automaticTextReplacementEnabled) {
    checkingTypes |= NSTextCheckingTypeReplacement;
  }
  return checkingTypes;
}

- (void)orderFrontSubstitutionsPanel:(id)sender {
  [NSSpellChecker.sharedSpellChecker.substitutionsPanel orderFront:sender];
}

- (bool)canTransformText {
  if (_textInputType == ui::TEXT_INPUT_TYPE_NONE) {
    return NO;
  }
  if (_textInputType == ui::TEXT_INPUT_TYPE_PASSWORD ||
      _textInputFlags & ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD ||
      _textInputFlags & ui::TEXT_INPUT_FLAG_HAS_BEEN_CUSTOM_PASSWORD) {
    return NO;
  }

  return YES;
}

- (void)uppercaseWord:(id)sender {
  NSString *text = base::SysUTF16ToNSString([self selectedText]);
  if (!text)
    return;

  [self insertText:text.localizedUppercaseString
      replacementRange:_textSelectionRange.ToNSRange()];
}

- (void)lowercaseWord:(id)sender {
  NSString *text = base::SysUTF16ToNSString([self selectedText]);
  if (!text)
    return;

  [self insertText:text.localizedLowercaseString
      replacementRange:_textSelectionRange.ToNSRange()];
}

- (void)capitalizeWord:(id)sender {
  NSString *text = base::SysUTF16ToNSString([self selectedText]);
  if (!text)
    return;

  [self insertText:text.localizedCapitalizedString
      replacementRange:_textSelectionRange.ToNSRange()];
}

- (void)setTextSelectionText:(std::u16string)text
                      offset:(size_t)offset
                       range:(gfx::Range)range {
  _availableText = text;
  _availableTextOffset = offset;
  _availableTextChangeCounter++;
  _textSelectionRange = range;
  _substitutionWasApplied = NO;
  [self.spellChecker dismissCorrectionIndicatorForView:self];
  if (_shouldRequestTextSubstitutions && !_substitutionWasApplied &&
      _textSelectionRange.is_empty()) {
    _shouldRequestTextSubstitutions = NO;
    [self requestTextSubstitutions];
  }
  [self requestTextSuggestions];
}

- (void)candidateListTouchBarItem:(NSCandidateListTouchBarItem*)anItem
     endSelectingCandidateAtIndex:(NSInteger)index {
  if (index == NSNotFound)
    return;
  NSTextCheckingResult* selectedResult = anItem.candidates[index];
  NSRange replacementRange = selectedResult.range;
  replacementRange.location += _availableTextOffset;
  [self insertText:selectedResult.replacementString
      replacementRange:replacementRange];
}

- (void)candidateListTouchBarItem:(NSCandidateListTouchBarItem*)anItem
    changedCandidateListVisibility:(BOOL)isVisible {
  [self requestTextSuggestions];
}

- (void)setTextInputType:(ui::TextInputType)textInputType {
  if (_textInputType == textInputType)
    return;
  _textInputType = textInputType;

  [self invalidateTouchBar];
}

- (std::u16string)selectedText {
  gfx::Range textRange(_availableTextOffset,
                       _availableTextOffset + _availableText.size());
  gfx::Range intersectionRange = textRange.Intersect(_textSelectionRange);
  if (intersectionRange.is_empty())
    return std::u16string();
  return _availableText.substr(intersectionRange.start() - _availableTextOffset,
                               intersectionRange.length());
}

- (void)setCompositionRange:(gfx::Range)range {
  _compositionRange = range;
}

- (void)sendWindowFrameInScreenToHost {
  TRACE_EVENT0("browser",
               "RenderWidgetHostViewCocoa::sendWindowFrameInScreenToHost");
  NSWindow* enclosingWindow = [self window];
  if (!enclosingWindow)
    return;
  _host->OnWindowFrameInScreenChanged(
      gfx::ScreenRectFromNSRect([enclosingWindow frame]));
}

- (void)setResponderDelegate:
    (NSObject<RenderWidgetHostViewMacDelegate>*)delegate {
  DCHECK(!_responderDelegate);
  _responderDelegate = delegate;
}

- (void)resetCursorRects {
  if (_currentCursor)
    [self addCursorRect:[self visibleRect] cursor:_currentCursor];
}

- (void)processedGestureScrollEvent:(const blink::WebGestureEvent&)event
                           consumed:(BOOL)consumed {
  [_responderDelegate rendererHandledGestureScrollEvent:event
                                               consumed:consumed];
}

- (void)processedOverscroll:(const ui::DidOverscrollParams&)params {
  [_responderDelegate rendererHandledOverscrollEvent:params];
}

- (BOOL)respondsToSelector:(SEL)selector {
  // Trickiness: this doesn't mean "does this object's superclass respond to
  // this selector" but rather "does the -respondsToSelector impl from the
  // superclass say that this class responds to the selector".
  if ([super respondsToSelector:selector])
    return YES;

  if (_responderDelegate)
    return [_responderDelegate respondsToSelector:selector];

  return NO;
}

- (id)forwardingTargetForSelector:(SEL)selector {
  if ([_responderDelegate respondsToSelector:selector])
    return _responderDelegate;

  return [super forwardingTargetForSelector:selector];
}

- (void)setCanBeKeyView:(BOOL)can {
  _canBeKeyView = can;
}

- (AcceptMouseEvents)acceptsMouseEventsOption {
  // Always-on-top windows, e.g picture-in-picture window, accepts all mouse
  // events even if the window or the application is inactive.
  if (self.window.level > NSNormalWindowLevel) {
    return AcceptMouseEvents::kAlways;
  }

  // By default, only active window accepts mouse events. The embedder may
  // override this to mimic the hover and click behavior of native UIs.
  if (_responderDelegate && [_responderDelegate respondsToSelector:@selector
                                                (acceptsMouseEventsOption)]) {
    return [_responderDelegate acceptsMouseEventsOption];
  }

  // By default, only active window accepts mouse events.
  return AcceptMouseEvents::kWhenInActiveWindow;
}

- (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
  // Enable "click-through" if mouse clicks are accepted in inactive windows.
  return
      [self acceptsMouseEventsOption] > AcceptMouseEvents::kWhenInActiveWindow;
}

- (BOOL)shouldBecomeFirstResponderOnRightClick {
  if (![self acceptsFirstResponder]) {
    return NO;
  }

  if (_responderDelegate &&
      [_responderDelegate respondsToSelector:@selector
                          (shouldBecomeFirstResponderOnRightClick)]) {
    return [_responderDelegate shouldBecomeFirstResponderOnRightClick];
  }

  return NO;
}

- (AcceptTooltipEvents)acceptsTooltipEvents {
  // The embedder may override this behavior to mimic native UI.
  if (_responderDelegate &&
      [_responderDelegate respondsToSelector:@selector(acceptsTooltipEvents)]) {
    return [_responderDelegate acceptsTooltipEvents];
  }

  // By default, only the key window accepts tooltip events.
  return AcceptTooltipEvents::kWhenInKeyWindow;
}

- (void)setCloseOnDeactivate:(BOOL)b {
  _closeOnDeactivate = b;
}

- (void)setHostDisconnected {
  // Set the host to be an abandoned message pipe, and set the hostHelper
  // to forward messages to that host.
  std::ignore = _dummyHost.BindNewPipeAndPassReceiver();
  _dummyHostHelper = std::make_unique<DummyHostHelper>();
  _host = _dummyHost.get();
  _hostHelper = _dummyHostHelper.get();

  // |responderDelegate_| may attempt to access the RenderWidgetHostViewMac
  // through its internal pointers, so detach it here.
  // TODO(ccameron): Force |responderDelegate_| to use the |host_| as well,
  // and the viewGone method to hostGone.
  if (_responderDelegate &&
      [_responderDelegate respondsToSelector:@selector(viewGone:)])
    [_responderDelegate viewGone:self];
  _responderDelegate = nil;
}

- (bool)hostIsDisconnected {
  return _host == (_dummyHost.is_bound() ? _dummyHost.get() : nullptr);
}

- (void)characterPaletteWillOrderFront:(NSNotification*)notification {
  if (!_textSelectionChangedForContextualMenu) {
    return;
  }

  // The user has invoked the emoji palette from the contextual menu of an
  // input field, and the text selection changed with the control-click. This
  // means _preContextualMenuSelectionRange has a zero length, but the current
  // selection has an extent (see setShowingContextMenu:). If the user selects
  // a character from the palette, it will replace the selection, which is not
  // what the user wants. We need to restore the insertion point.
  //
  // If the current selection extends beyond the original insertion point, we
  // want to restore the insertion point to the start of the current selection.
  // Otherwise, place the insertion point at the end of the current selection.
  // We can use moveLeft: / moveRight: to both position the insertion point at
  // either end of the selection and to clear the selection. Note that these
  // methods flip their behavior if we're in RTL, which is what we want.
  //
  // An edge case we won't address is when the insertion point started off in
  // the middle of a word or span of whitespace. A control click in this
  // situation (empirically) creates a selection with the original insertion
  // point somewhere in the middle. We could use moveLeft: to position the
  // insertion point at the start of the range and call moveRight: in a loop to
  // return to our original insertion point, but that seems kind of gross.
  if (NSMaxRange([self selectedRange]) >
      _preContextualMenuSelectionRange.location) {
    [self doCommandBySelector:@selector(moveLeft:)];
  } else {
    [self doCommandBySelector:@selector(moveRight:)];
  }

  _textSelectionChangedForContextualMenu = NO;
}

- (void)setShowingContextMenu:(BOOL)showing {
  // When you control-click in an input field with no existing selection, blink
  // forces the creation of a selection. This is standard Mac behavior,
  // fulfilling the Mac expectation that a selection exists for the contextual
  // menu to act on. This selection change creates a problem if the user
  // invokes the emoji palette from the contextual menu because whatever
  // character they choose will replace the now-selected text. We make a note
  // here of this situation so that we can restore the selection once we're
  // sure the user has invoked the emoji panel.
  if (_willInvokeContextMenuWithEmptySelection) {
    if (!NSEqualRanges(_preContextualMenuSelectionRange,
                       [self selectedRange])) {
      _textSelectionChangedForContextualMenu = YES;
    }
    _willInvokeContextMenuWithEmptySelection = NO;
  }

  _showingContextMenu = showing;

  // Create a fake mouse event to inform the render widget that the mouse
  // left or entered.
  NSWindow* window = [self window];
  int windowNumber = window ? [window windowNumber] : -1;

  // TODO(asvitkine): If the location outside of the event stream doesn't
  // correspond to the current event (due to delayed event processing), then
  // this may result in a cursor flicker if there are later mouse move events
  // in the pipeline. Find a way to use the mouse location from the event that
  // dismissed the context menu.
  NSPoint location = [window mouseLocationOutsideOfEventStream];
  NSTimeInterval eventTime = [[NSApp currentEvent] timestamp];
  NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeMouseMoved
                                      location:location
                                 modifierFlags:0
                                     timestamp:eventTime
                                  windowNumber:windowNumber
                                       context:nil
                                   eventNumber:0
                                    clickCount:0
                                      pressure:0];
  WebMouseEvent webEvent = WebMouseEventBuilder::Build(event, self);
  webEvent.SetModifiers(webEvent.GetModifiers() |
                        WebInputEvent::kRelativeMotionEvent);
  // TODO(crbug.com/40276040): We shouldn't be posting events with null
  // timestamps. However `MessagePumpNSApplication::DoQuit` usage of
  // `otherEventWithType` seems to truncate `NSTimeInterval` to seconds. Which
  // could lead to those generated events be in the past, compared to ones
  // created directly in later parts of the pipeline or in tests.
  if (webEvent.TimeStamp().is_null()) {
    webEvent.SetTimeStamp(ui::EventTimeForNow());
  }
  _hostHelper->ForwardMouseEvent(webEvent);
}

- (void)setSupportsAutoFill:(BOOL)supports {
  _supportsAutoFill = supports;
}

- (BOOL)shouldIgnoreMouseEvent:(NSEvent*)theEvent {
  NSWindow* window = self.window;
  if (theEvent.type == NSEventTypeMouseMoved) {
    AcceptMouseEvents option = [self acceptsMouseEventsOption];

    // If events are accepted only in active window but this window is inactive,
    // ignore this event. This is the default behavior.
    bool inActiveWindow = window.mainWindow || window.keyWindow;
    if (option == AcceptMouseEvents::kWhenInActiveWindow && !inActiveWindow) {
      return YES;
    }

    // If events are accepted in active app but the app in active, ignore this
    // event. This only happens if the content embedder overrides the default
    // behavior.
    bool inActiveApp = NSApplication.sharedApplication.active;
    if (option == AcceptMouseEvents::kWhenInActiveApp && !inActiveApp) {
      return YES;
    }
  }

  NSView* contentView = window.contentView;
  // -hitTest: assumes use of the superview's coordinate system.
  NSPoint pointForHitTestInContentView =
      [contentView.superview convertPoint:theEvent.locationInWindow
                                 fromView:nil];
  NSView* view = [contentView hitTest:pointForHitTestInContentView];
  // Traverse the superview hierarchy as the hitTest will return the frontmost
  // view, such as an NSTextView, while nonWebContentView may be specified by
  // its parent view.
  BOOL hitSelf = NO;
  while (view) {
    if (view == self)
      hitSelf = YES;
    if ([view isKindOfClass:[self class]] && ![view isEqual:self] &&
        !_hasOpenMouseDown) {
      // The cursor is over an overlapping render widget. This check is done by
      // both views so the one that's returned by -hitTest: will end up
      // processing the event.
      // Note that while dragging, we only get events for the render view where
      // drag started, even if mouse is  actually over another view or outside
      // the window. Cocoa does this for us. We should handle these events and
      // not ignore (since there is no other render view to handle them). Thus
      // the |!hasOpenMouseDown_| check above.
      return YES;
    }
    view = [view superview];
  }
  // Ignore events which don't hit test to this subtree (and hit, for example,
  // an overlapping view instead). As discussed above, the mouse may go outside
  // the bounds of the view and keep sending events during a drag.
  return !hitSelf && !_hasOpenMouseDown;
}

- (void)mouseEvent:(NSEvent*)theEvent {
  TRACE_EVENT0("browser", "RenderWidgetHostViewCocoa::mouseEvent");

  _textSelectionChangedForContextualMenu = NO;
  if ((theEvent.type == NSEventTypeLeftMouseDown &&
       (theEvent.modifierFlags & NSEventModifierFlagControl)) ||
      theEvent.type == NSEventTypeRightMouseDown) {
    _preContextualMenuSelectionRange = [self selectedRange];

    if (_preContextualMenuSelectionRange.length == 0) {
      _willInvokeContextMenuWithEmptySelection = YES;
    }
  }

  // By default, a right mouse event does not make the view the first
  // responder. Consequently, the page does not receive focus or blur events.
  // This causes unintuitive behavior for WebUI-based menus that rely on blur
  // events to dismiss themselves. Therefore, we allow the embedder to decide
  // whether to make the view the first responder on a right mouse down.
  if (theEvent.type == NSEventTypeRightMouseDown &&
      [self shouldBecomeFirstResponderOnRightClick] &&
      [self.window firstResponder] != self) {
    [self.window makeFirstResponder:self];
  }

  if (_responderDelegate &&
      [_responderDelegate respondsToSelector:@selector(handleEvent:)]) {
    BOOL handled = [_responderDelegate handleEvent:theEvent];
    if (handled)
      return;
  }
  if (ui::PlatformEventSource::ShouldIgnoreNativePlatformEvents())
    return;

  // Set the pointer type when we are receiving a NSEventTypeMouseEntered event
  // and the following NSEventTypeMouseExited event should have the same pointer
  // type. For NSEventTypeMouseExited and NSEventTypeMouseEntered events, they
  // do not have a subtype. We decide their pointer types by checking if we
  // received a NSEventTypeTabletProximity event.
  NSEventType type = [theEvent type];
  if (type == NSEventTypeMouseEntered || type == NSEventTypeMouseExited) {
    _pointerType = _isStylusEnteringProximity
                       ? _pointerType
                       : blink::WebPointerProperties::PointerType::kMouse;
  } else {
    NSEventSubtype subtype = [theEvent subtype];
    // For other mouse events and touchpad events, the pointer type is mouse.
    if (subtype != NSEventSubtypeTabletPoint &&
        subtype != NSEventSubtypeTabletProximity) {
      _pointerType = blink::WebPointerProperties::PointerType::kMouse;
    } else if (subtype == NSEventSubtypeTabletProximity) {
      _isStylusEnteringProximity = [theEvent isEnteringProximity];
      NSPointingDeviceType deviceType = [theEvent pointingDeviceType];
      // For all tablet events, the pointer type will be pen or eraser.
      _pointerType = deviceType == NSPointingDeviceTypeEraser
                         ? blink::WebPointerProperties::PointerType::kEraser
                         : blink::WebPointerProperties::PointerType::kPen;
    }
  }

  // Because |updateCursor:| changes the current cursor, we have to reset it to
  // the default cursor on mouse exit.
  if (type == NSEventTypeMouseExited) {
    [[NSCursor arrowCursor] set];
  }

  // In macOS immersive fullscreen, the browser UI is held in an AppKit-managed
  // `NSToolbarFullScreenWindow`. When this toolbar auto-hides, the window
  // becomes invisible but remains positioned at the top of the screen.
  // Mouse events pass through it to this view, but a macOS bug causes the OS
  // to forcefully reset the system cursor to the default arrow when the mouse
  // crosses the bottom boundary of that invisible window.
  //
  // To fix this, this code detect spurious cursor resets during mouse move by
  // checking if `currentSystemCursor` diverged from Chrome's `_currentCursor`
  // state. If it did, we forcefully re-apply our cursor.
  //
  // This is safe for overlapping UI (like popups or context menus), which will
  // intercept the mouse event before it reaches here, meaning we won't
  // improperly override their cursors.
  if (type == NSEventTypeMouseMoved || type == NSEventTypeLeftMouseDragged ||
      type == NSEventTypeRightMouseDragged ||
      type == NSEventTypeOtherMouseDragged) {
    if ([self shouldChangeCurrentCursor] &&
        [NSCursor currentSystemCursor] != _currentCursor) {
      [_currentCursor set];
    }
  }

  if ([self shouldIgnoreMouseEvent:theEvent]) {
    // If this is the first such event, send a mouse exit to the host view.
    if (!_mouseEventWasIgnored && !self.hidden) {
      WebMouseEvent exitEvent =
          WebMouseEventBuilder::Build(theEvent, self, _pointerType);
      exitEvent.SetType(WebInputEvent::Type::kMouseLeave);
      exitEvent.button = WebMouseEvent::Button::kNoButton;
      _hostHelper->ForwardMouseEvent(exitEvent);
    }
    _mouseEventWasIgnored = YES;
    [self updateCursor:nil];
    return;
  }

  // After re-centering the cursor for pointer lock, macOS folds the
  // re-centering distance into the delta of the first real mouse-move. Suppress
  // locked move/crossing events until that movement is absorbed so the spurious
  // delta never reaches the DOM. Mirrors Aura's
  // RenderWidgetHostViewEventHandler::ModifyEventMovementAndCoords.
  bool suppressRecenterArtifact = false;
  if (_suppressNextLockedMouseMoveForRecenter) {
    const bool isMovement = type == NSEventTypeMouseMoved ||
                            type == NSEventTypeLeftMouseDragged ||
                            type == NSEventTypeRightMouseDragged ||
                            type == NSEventTypeOtherMouseDragged;
    const bool isCrossing =
        type == NSEventTypeMouseEntered || type == NSEventTypeMouseExited;
    suppressRecenterArtifact = isMovement || isCrossing;
    // The re-centering delta is only present on the first real movement; stop
    // once it's absorbed.
    if (isMovement) {
      _suppressNextLockedMouseMoveForRecenter = NO;
    }
  }

  if (_mouseEventWasIgnored) {
    // If this is the first mouse event after a previous event that was ignored
    // due to the hitTest, send a mouse enter event to the host view.
    WebMouseEvent enterEvent =
        WebMouseEventBuilder::Build(theEvent, self, _pointerType);
    enterEvent.SetType(WebInputEvent::Type::kMouseMove);
    enterEvent.button = WebMouseEvent::Button::kNoButton;
    if (suppressRecenterArtifact) {
      enterEvent.SetModifiers(
          enterEvent.GetModifiers() |
          blink::WebInputEvent::Modifiers::kRelativeMotionEvent);
    }
    _hostHelper->RouteOrProcessMouseEvent(enterEvent);
  }
  _mouseEventWasIgnored = NO;

  // Don't cancel child popups; killing them on a mouse click would prevent the
  // user from positioning the insertion point in the text field spawning the
  // popup. A click outside the text field would cause the text field to drop
  // the focus, and then EditorHostImpl::textFieldDidEndEditing() would cancel
  // the popup anyway, so we're OK.
  if (type == NSEventTypeLeftMouseDown)
    _hasOpenMouseDown = YES;
  else if (type == NSEventTypeLeftMouseUp)
    _hasOpenMouseDown = NO;

  // TODO(suzhe): We should send mouse events to the input method first if it
  // wants to handle them. But it won't work without implementing method
  // - (NSUInteger)characterIndexForPoint:.
  // See: http://code.google.com/p/chromium/issues/detail?id=47141
  // Instead of sending mouse events to the input method first, we now just
  // simply confirm all ongoing composition here.
  if (type == NSEventTypeLeftMouseDown || type == NSEventTypeRightMouseDown ||
      type == NSEventTypeOtherMouseDown) {
    [self finishComposingText];
  }

  if (type == NSEventTypeMouseMoved)
    _cursorHidden = NO;

  bool unacceleratedMovement = _mouseLocked && _mouseLockUnacceleratedMovement;
  WebMouseEvent event = WebMouseEventBuilder::Build(
      theEvent, self, _pointerType, unacceleratedMovement);

  if (_mouseLocked) {
    // When mouse is locked, we keep increasing |_lastMouseScreenPosition|
    // by movement_x/y so that we can still use PositionInScreen to calculate
    // movements in blink. We need to keep |_lastMouseScreenPosition| from
    // getting too large because it will lose some precision. So whenever it
    // exceed the |kWrapAroundDistance|, we start again from the current
    // mouse position (locked position), and also send a synthesized event to
    // update the blink-side status.
    if (std::abs(_lastMouseScreenPosition.x()) > kWrapAroundDistance ||
        std::abs(_lastMouseScreenPosition.y()) > kWrapAroundDistance) {
      NSWindow* window = [self window];
      NSPoint location = [window mouseLocationOutsideOfEventStream];
      int windowNumber = window ? [window windowNumber] : -1;
      NSEvent* nsevent = [NSEvent mouseEventWithType:NSEventTypeMouseMoved
                                            location:location
                                       modifierFlags:[theEvent modifierFlags]
                                           timestamp:[theEvent timestamp]
                                        windowNumber:windowNumber
                                             context:nil
                                         eventNumber:0
                                          clickCount:[theEvent clickCount]
                                            pressure:0];
      WebMouseEvent wrapAroundEvent =
          WebMouseEventBuilder::Build(nsevent, self, _pointerType);
      _lastMouseScreenPosition = wrapAroundEvent.PositionInScreen();
      wrapAroundEvent.SetModifiers(
          event.GetModifiers() |
          blink::WebInputEvent::Modifiers::kRelativeMotionEvent);
      _hostHelper->RouteOrProcessMouseEvent(wrapAroundEvent);
    }
    event.SetPositionInScreen(
        _lastMouseScreenPosition +
        gfx::Vector2dF(event.movement_x, event.movement_y));

    if (suppressRecenterArtifact) {
      event.SetModifiers(event.GetModifiers() |
                         blink::WebInputEvent::Modifiers::kRelativeMotionEvent);
    }
  }

  _lastMouseScreenPosition = event.PositionInScreen();
  _hostHelper->RouteOrProcessMouseEvent(event);
}

- (void)tabletEvent:(NSEvent*)theEvent {
  if ([theEvent type] == NSEventTypeTabletProximity) {
    _isStylusEnteringProximity = [theEvent isEnteringProximity];
    NSPointingDeviceType deviceType = [theEvent pointingDeviceType];
    // For all tablet events, the pointer type will be pen or eraser.
    _pointerType = deviceType == NSPointingDeviceTypeEraser
                       ? blink::WebPointerProperties::PointerType::kEraser
                       : blink::WebPointerProperties::PointerType::kPen;
  }
}

- (void)lockKeyboard:(std::optional<base::flat_set<ui::DomCode>>)keysToLock {
  // TODO(joedow): Integrate System-level keyboard hook into this method.
  _lockedKeys = std::move(keysToLock);
  _keyboardLockActive = true;
}

- (void)unlockKeyboard {
  _keyboardLockActive = false;
  _lockedKeys.reset();
}

- (void)setCursorLocked:(BOOL)locked {
  _mouseLocked = locked;
  if (_mouseLocked) {
    // When the pointer is outside the view we move the cursor to the center
    // of the browser's window so that the browser will receive the pointer
    // events.
    const NSPoint locationInScreen = NSEvent.mouseLocation;
    _preLockCursorScreenPosition = locationInScreen;
    const NSPoint locationInView =
        [self convertPoint:[self.window convertPointFromScreen:locationInScreen]
                  fromView:nil];
    if (!NSPointInRect(locationInView, self.bounds)) {
      const NSPoint centerInWindow = [self
          convertPoint:NSMakePoint(NSMidX(self.bounds), NSMidY(self.bounds))
                toView:nil];
      CGWarpMouseCursorPosition(CGPointFromNSEventScreenPoint(
          [self.window convertPointToScreen:centerInWindow]));

      // `mouseEvent()` suppresses locked events until the move distance has
      // been absorbed.
      _suppressNextLockedMouseMoveForRecenter = YES;
    }

    CGAssociateMouseAndMouseCursorPosition(NO);
    [NSCursor hide];
  } else {
    // Unlock position of mouse cursor and unhide it. Restore the pre-lock
    // cursor position if we had moved it before locking.
    _suppressNextLockedMouseMoveForRecenter = NO;
    if (_preLockCursorScreenPosition) {
      CGWarpMouseCursorPosition(
          CGPointFromNSEventScreenPoint(*_preLockCursorScreenPosition));
      _preLockCursorScreenPosition.reset();
    }
    CGAssociateMouseAndMouseCursorPosition(YES);
    [NSCursor unhide];
  }
}

- (void)setCursorLockedUnacceleratedMovement:(BOOL)unaccelerated {
  _mouseLockUnacceleratedMovement = unaccelerated;
}

// CommandDispatcherTarget implementation:
- (BOOL)isKeyLocked:(NSEvent*)event {
  int keyCode = [event keyCode];
  // Note: We do not want to treat the ESC key as locked as that key is used
  // to exit fullscreen and we don't want to prevent them from exiting.
  ui::DomCode domCode = ui::KeycodeConverter::NativeKeycodeToDomCode(keyCode);
  return _keyboardLockActive && domCode != ui::DomCode::ESCAPE &&
         (!_lockedKeys || _lockedKeys.value().contains(domCode));
}

- (BOOL)performKeyEquivalent:(NSEvent*)theEvent {
  // TODO(bokan): Tracing added temporarily to diagnose crbug.com/1039833.
  TRACE_EVENT0("browser", "RenderWidgetHostViewCocoa::performKeyEquivalent");
  // |performKeyEquivalent:| is sent to all views of a window, not only down the
  // responder chain (cf. "Handling Key Equivalents" in
  // http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/EventOverview/HandlingKeyEvents/HandlingKeyEvents.html
  // ). A |performKeyEquivalent:| may also bubble up from a dialog child window
  // to perform browser commands such as switching tabs. We only want to handle
  // key equivalents if we're first responder in the keyWindow.
  if (![self isKeyTrackingWindowKey] ||
      [[self window] firstResponder] != self) {
    TRACE_EVENT_INSTANT("browser", "NotKeyWindow");
    return NO;
  }

  // If the event is reserved by the system, do not pass it to web content.
  // If the user changes the system hotkey mapping after Chrome has been
  // launched, it is possible that a formerly reserved system hotkey is no
  // longer reserved. The hotkey would have skipped the renderer, but would
  // also have not been handled by the system. If this is the case, immediately
  // return.
  // TODO(erikchen): SystemHotkeyHelperMac should use the File System Events
  // api to monitor changes to system hotkeys. This logic will have to be
  // updated.
  // http://crbug.com/383558.
  if (EventIsReservedBySystem(theEvent)) {
    return NO;
  }

  // Command key combinations are sent via performKeyEquivalent rather than
  // keyDown:. We just forward this on and if WebCore doesn't want to handle
  // it, we let the WebContentsView figure out how to reinject it.
  [self keyEvent:theEvent wasKeyEquivalent:YES];
  return YES;
}

- (BOOL)_wantsKeyDownForEvent:(NSEvent*)event {
  // This is a SPI that AppKit apparently calls after |performKeyEquivalent:|
  // returned NO. If this function returns |YES|, Cocoa sends the event to
  // |keyDown:| instead of doing other things with it. Ctrl-tab will be sent
  // to us instead of doing key view loop control, ctrl-left/right get handled
  // correctly, etc.
  // (However, there are still some keys that Cocoa swallows, e.g. the key
  // equivalent that Cocoa uses for toggling the input language. In this case,
  // that's actually a good thing, though -- see http://crbug.com/26115 .)
  return YES;
}

- (void)contextMenuKeyDown:(NSEvent*)event {
  // Preserve existing Ctrl+Return behavior while typing in inputs.
  if (_textInputType == ui::TEXT_INPUT_TYPE_NONE) {
    NSEvent* context_menu_event = CreateContextMenuKeyDownEvent(event);

    if (context_menu_event) {
      [self keyEvent:context_menu_event wasKeyEquivalent:NO];
      return;
    }
  }

  if (@available(macOS 15.0, *)) {
    [super contextMenuKeyDown:event];
  }
}

- (EventHandled)keyEvent:(NSEvent*)theEvent {
  if (_responderDelegate &&
      [_responderDelegate respondsToSelector:@selector(handleEvent:)]) {
    BOOL handled = [_responderDelegate handleEvent:theEvent];
    if (handled)
      return kEventHandled;
  }

  [self keyEvent:theEvent wasKeyEquivalent:NO];
  return kEventHandled;
}

- (void)keyEvent:(NSEvent*)theEvent wasKeyEquivalent:(BOOL)equiv {
  TRACE_EVENT1("browser", "RenderWidgetHostViewCocoa::keyEvent", "WindowNum",
               [[self window] windowNumber]);
  NSEventType eventType = [theEvent type];
  NSEventModifierFlags modifierFlags = [theEvent modifierFlags];
  int keyCode = [theEvent keyCode];

  // If the user changes the system hotkey mapping after Chrome has been
  // launched, then it is possible that a formerly reserved system hotkey is no
  // longer reserved. The hotkey would have skipped the renderer, but would
  // also have not been handled by the system. If this is the case, immediately
  // return.
  // TODO(erikchen): SystemHotkeyHelperMac should use the File System Events
  // api to monitor changes to system hotkeys. This logic will have to be
  // updated.
  // http://crbug.com/383558.
  if (EventIsReservedBySystem(theEvent))
    return;

  if (eventType == NSEventTypeFlagsChanged) {
    // Ignore NSEventTypeFlagsChanged events from the NumLock and Fn keys as
    // Safari does in -[WebHTMLView flagsChanged:] (of "WebHTMLView.mm").
    // Also ignore unsupported |keyCode| (255) generated by Convert, NonConvert
    // and KanaMode from JIS PC keyboard.
    if (!keyCode || keyCode == 10 || keyCode == 63 || keyCode == 255)
      return;
  }

  // Don't cancel child popups; the key events are probably what's triggering
  // the popup in the first place.

  NativeWebKeyboardEvent event((base::apple::OwnedNSEvent(theEvent)));
  ui::LatencyInfo latencyInfo;
  latencyInfo.AddLatencyNumber(ui::INPUT_EVENT_LATENCY_UI_COMPONENT);

  // If KeyboardLock has been requested for this keyCode, then mark the event
  // so it skips the pre-handler and is delivered straight to the website.
  if ([self isKeyLocked:theEvent])
    event.skip_if_unhandled = true;

  // Do not forward key up events unless preceded by a matching key down,
  // otherwise we might get an event from releasing the return key in the
  // omnibox (https://crbug.com/338736) or from closing another window
  // (https://crbug.com/155492).
  if (eventType == NSEventTypeKeyUp) {
    auto numErased = _unmatchedKeyDownCodes.erase(keyCode);
    if (numErased < 1)
      return;
  }

  // Tell the host that we are beginning a keyboard event. This ensures that
  // all event and Ime messages target the same RenderWidgetHost throughout this
  // function call.
  _host->BeginKeyboardEvent();

  bool shouldAutohideCursor = _textInputType != ui::TEXT_INPUT_TYPE_NONE &&
                              eventType == NSEventTypeKeyDown &&
                              !(modifierFlags & NSEventModifierFlagCommand);

  // We only handle key down events and just simply forward other events.
  if (eventType != NSEventTypeKeyDown) {
    _hostHelper->ForwardKeyboardEvent(event, latencyInfo);

    // Possibly autohide the cursor.
    if (shouldAutohideCursor) {
      [NSCursor setHiddenUntilMouseMoves:YES];
      _cursorHidden = YES;
    }

    _host->EndKeyboardEvent();
    return;
  }

  _unmatchedKeyDownCodes.insert(keyCode);

  NS_VALID_UNTIL_END_OF_SCOPE RenderWidgetHostViewCocoa* keepSelfAlive = self;

  // Records the current marked text state, so that we can know if the marked
  // text was deleted or not after handling the key down event.
  BOOL oldHasMarkedText = _hasMarkedText;

  // This method should not be called recursively.
  DCHECK(![self isHandlingKeyDown]);

  // Tells insertText: and doCommandBySelector: that we are handling a key
  // down event.
  _currentKeyDownCode = keyCode;

  // This is to handle an edge case for the "Live Conversion" feature in default
  // Japanese IME. When the feature is on, pressing the left key at the
  // composition boundary will reconvert previously committed text. The text
  // input system will call setMarkedText multiple times to end the current
  // composition and start a new one. In this case we'll need to call
  // ImeSetComposition in setMarkedText instead of here in keyEvent:, otherwise,
  // only the last setMarkedText will be processed.
  ui::DomCode domCode = ui::KeycodeConverter::NativeKeycodeToDomCode(keyCode);
  _isReconversionTriggered =
      _hasMarkedText && domCode == ui::DomCode::ARROW_LEFT &&
      _markedTextSelectedRange.location == 0 && _markedRange.location != 0 &&
      _markedRange.location != NSNotFound;

  // These variables might be set when handling the keyboard event.
  // Clear them here so that we can know whether they have changed afterwards.
  _textToBeInserted.clear();
  _markedText.clear();
  _markedTextSelectedRange = NSMakeRange(NSNotFound, 0);
  _imeTextSpans.clear();
  _setMarkedTextReplacementRange = gfx::Range::InvalidRange();
  _unmarkTextCalled = NO;
  _hasEditCommands = NO;
  _editCommands.clear();

  // Since Mac Eisu Kana keys cannot be handled by interpretKeyEvents to enable/
  // disable an IME, we need to pass the event to processInputKeyBindings.
  // processInputKeyBindings is available at least on 10.11-11.0.
  if (keyCode == kVK_JIS_Eisu || keyCode == kVK_JIS_Kana) {
    if ([NSTextInputContext
            respondsToSelector:@selector(processInputKeyBindings:)]) {
      [NSTextInputContext performSelector:@selector(processInputKeyBindings:)
                               withObject:theEvent];
    }
  } else {
    // Previously, we would just send the event, shortcut or no, to
    // -interpretKeyEvents: below. The problem is that certain keyboard
    // shortcuts now use the Function/World key and in those cases the
    // corresponding shortcut fires but the shortcut event also gets processed
    // as if it were a key press. As a result, with the insertion point
    // in a web text box, after typing something like "Function e" to invoke
    // the Emoji palette, we would wind up in -insertText:replacementRange:.
    // The logic there ([self isHandlingKeyDown] && replacementRange.location ==
    // NSNotFound) would create an invisible placeholder for the character. This
    // invisible placeholder would cause macOS to position the palette at the
    // upper-left corner of the webcontents instead of at the insertion point.
    //
    // For these Function/World events, we want the AppKit to process them
    // as it usually would (i.e. via performKeyEquivalent:). It would be simpler
    // if we could pass all of these keyboard shortcut events along to
    // performKeyEquivalent:, however web pages are allowed to hijack keyboard
    // shortcuts and apparently that's done through interpretKeyEvents:.
    // Applications are not allowed to create these system events (you get a
    // warning if you try and the Function event modifier flag doesn't stick)
    // so it's OK not to allow web pages to do so either.
    //
    // If the event's not a shortcut, send it along to the input method first.
    // We can then decide what should be done according to the input method's
    // feedback.
    bool isASystemShortcutEvent = false;
    if (equiv) {
      bool isAKeyboardShortcutEvent =
          [[NSApp mainMenu] cr_menuItemForKeyEquivalentEvent:theEvent] != nil;

      const NSEventModifierFlags kSystemShortcutModifierFlag =
          NSEventModifierFlagFunction;
      isASystemShortcutEvent = isAKeyboardShortcutEvent &&
                               (ui::cocoa::ModifierMaskForKeyEvent(theEvent) &
                                kSystemShortcutModifierFlag) != 0;
    }

    if (isASystemShortcutEvent) {
      [[NSApp mainMenu] performKeyEquivalent:theEvent];

      // Behavior changed in macOS Sonoma - now it's important we early-out
      // rather than allow the code to reach
      // _hostHelper->ForwardKeyboardEventWithCommands(). Go with the existing
      // behavior for prior versions because we know it works for them.
      if (base::mac::MacOSVersion() >= 14'00'00) {
        _currentKeyDownCode.reset();
        _host->EndKeyboardEvent();
        return;
      }
    } else {
      [self interpretKeyEvents:@[ theEvent ]];
    }
  }

  _currentKeyDownCode.reset();

  // Indicates if we should send the key event and corresponding editor commands
  // after processing the input method result.
  BOOL delayEventUntilAfterImeComposition = NO;

  // To emulate Windows, over-write |event.windowsKeyCode| to VK_PROCESSKEY
  // while an input method is composing or inserting a text.
  // Gmail checks this code in its onkeydown handler to stop auto-completing
  // e-mail addresses while composing a CJK text.
  // If the text to be inserted has only one character, then we don't need this
  // trick, because we'll send the text as a key press event instead.
  if (_hasMarkedText || oldHasMarkedText || _textToBeInserted.length() > 1) {
    NativeWebKeyboardEvent fakeEvent = event;
    fakeEvent.windows_key_code = 0xE5;  // VKEY_PROCESSKEY
    fakeEvent.skip_if_unhandled = true;
    _hostHelper->ForwardKeyboardEvent(fakeEvent, latencyInfo);
    // If this key event was handled by the input method, but
    // -doCommandBySelector: (invoked by the call to -interpretKeyEvents: above)
    // enqueued edit commands, then in order to let webkit handle them
    // correctly, we need to send the real key event and corresponding edit
    // commands after processing the input method result.
    // We shouldn't do this if a new marked text was set by the input method,
    // otherwise the new marked text might be cancelled by webkit.
    if (_hasEditCommands && !_hasMarkedText)
      delayEventUntilAfterImeComposition = YES;
  } else {
    _hostHelper->ForwardKeyboardEventWithCommands(event, latencyInfo,
                                                  std::move(_editCommands));
  }

  // Then send keypress and/or composition related events.
  // If there was a marked text or the text to be inserted is longer than 1
  // character, then we send the text by calling FinishComposingText().
  // Otherwise, if the text to be inserted only contains 1 character, then we
  // can just send a keypress event which is fabricated by changing the type of
  // the keydown event, so that we can retain all necessary information, such
  // as unmodifiedText, etc. And we need to set event.skip_if_unhandled to true
  // to prevent the browser from handling it again. Note that,
  // |textToBeInserted_| is a UTF-16 string, but it's fine to only handle BMP
  // characters here, as we can always insert non-BMP characters as text.
  BOOL textInserted = NO;
  if (_textToBeInserted.length() >
      ((_hasMarkedText || oldHasMarkedText) ? 0u : 1u)) {
    _host->ImeCommitText(_textToBeInserted, gfx::Range::InvalidRange());
    textInserted = YES;
  }

  // Updates or cancels the composition. If some text has been inserted, then
  // we don't need to cancel the composition explicitly.
  if (_hasMarkedText && _markedText.length()) {
    // Sends the updated marked text to the renderer so it can update the
    // composition node in WebKit.
    // When marked text is available, |markedTextSelectedRange_| will be the
    // range being selected inside the marked text.
    if (!_isReconversionTriggered) {
      _host->ImeSetComposition(_markedText, _imeTextSpans,
                               _setMarkedTextReplacementRange,
                               _markedTextSelectedRange.location,
                               NSMaxRange(_markedTextSelectedRange));
    }
  } else if (oldHasMarkedText && !_hasMarkedText && !textInserted) {
    if (_unmarkTextCalled) {
      _host->ImeFinishComposingText();
    } else {
      _host->ImeCancelCompositionFromCocoa();
    }
  }

  _isReconversionTriggered = NO;

  // Clear information from |interpretKeyEvents:|
  _setMarkedTextReplacementRange = gfx::Range::InvalidRange();

  // If the key event was handled by the input method but it also generated some
  // edit commands, then we need to send the real key event and corresponding
  // edit commands here. This usually occurs when the input method wants to
  // finish current composition session but still wants the application to
  // handle the key event. See http://crbug.com/48161 for reference.
  if (delayEventUntilAfterImeComposition) {
    // If |delayEventUntilAfterImeComposition| is YES, then a fake key down
    // event with windowsKeyCode == 0xE5 has already been sent to webkit. So
    // before sending the real key down event, we need to send a fake key up
    // event to balance it.
    NativeWebKeyboardEvent fakeEvent = event;
    fakeEvent.SetType(blink::WebInputEvent::Type::kKeyUp);
    fakeEvent.skip_if_unhandled = true;
    ui::LatencyInfo fakeEventLatencyInfo = latencyInfo;
    _hostHelper->ForwardKeyboardEvent(fakeEvent, fakeEventLatencyInfo);
    _hostHelper->ForwardKeyboardEventWithCommands(event, fakeEventLatencyInfo,
                                                  std::move(_editCommands));
  }

  const NSUInteger kCtrlCmdKeyMask =
      NSEventModifierFlagControl | NSEventModifierFlagCommand;
  // Only send a corresponding key press event if there is no marked text.
  if (!_hasMarkedText) {
    if (!textInserted && _textToBeInserted.length() == 1) {
      // If a single character was inserted, then we just send it as a keypress
      // event.
      event.SetType(blink::WebInputEvent::Type::kChar);
      event.text[0] = _textToBeInserted[0];
      event.text[1] = 0;
      event.skip_if_unhandled = true;
      _hostHelper->ForwardKeyboardEvent(event, latencyInfo);
    } else if ((!textInserted || delayEventUntilAfterImeComposition) &&
               event.text[0] != '\0' &&
               ((modifierFlags & kCtrlCmdKeyMask) ||
                (_hasEditCommands && _editCommands.empty()))) {
      // We don't get insertText: calls if ctrl or cmd is down, or the key event
      // generates an insert command. So synthesize a keypress event for these
      // cases, unless the key event generated any other command.
      event.SetType(blink::WebInputEvent::Type::kChar);
      event.skip_if_unhandled = true;
      _hostHelper->ForwardKeyboardEvent(event, latencyInfo);
    }
  }

  // Possibly autohide the cursor.
  if (shouldAutohideCursor) {
    [NSCursor setHiddenUntilMouseMoves:YES];
    _cursorHidden = YES;
  }

  _host->EndKeyboardEvent();
}

- (BOOL)suppressNextKeyUpForTesting:(int)keyCode {
  return _unmatchedKeyDownCodes.count(keyCode) == 0;
}

- (void)forceTouchEvent:(NSEvent*)theEvent {
  if (ui::ForceClickInvokesQuickLook())
    [self quickLookWithEvent:theEvent];
}

- (void)shortCircuitScrollWheelEvent:(NSEvent*)event {
  if ([event phase] != NSEventPhaseEnded &&
      [event phase] != NSEventPhaseCancelled) {
    return;
  }

  // History-swiping is not possible if the logic reaches this point.
  WebMouseWheelEvent webEvent = WebMouseWheelEventBuilder::Build(event, self);
  webEvent.rails_mode = _mouseWheelFilter.UpdateRailsMode(webEvent);
  _hostHelper->ForwardWheelEvent(webEvent);

  if (_endWheelMonitor) {
    [NSEvent removeMonitor:_endWheelMonitor];
    _endWheelMonitor = nil;
  }
}

- (void)touchesMovedWithEvent:(NSEvent*)event {
  [_responderDelegate touchesMovedWithEvent:event];
}

- (void)touchesBeganWithEvent:(NSEvent*)event {
  [_responderDelegate touchesBeganWithEvent:event];
}

- (void)touchesCancelledWithEvent:(NSEvent*)event {
  [_responderDelegate touchesCancelledWithEvent:event];
}

- (void)touchesEndedWithEvent:(NSEvent*)event {
  [_responderDelegate touchesEndedWithEvent:event];
}

- (void)smartMagnifyWithEvent:(NSEvent*)event {
  const WebGestureEvent& smartMagnifyEvent =
      WebGestureEventBuilder::Build(event, self);
  _hostHelper->SmartMagnifyEvent(smartMagnifyEvent);
}

- (void)showLookUpDictionaryOverlayFromRange:(NSRange)range {
  _host->LookUpDictionaryOverlayFromRange(gfx::Range(range));
}

// This is invoked only on 10.8 or newer when the user taps a word using
// three fingers.
- (void)quickLookWithEvent:(NSEvent*)event {
  NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
  gfx::PointF rootPoint =
      GetSanitizedFlippedPoint(point, NSHeight([self frame]));
  _host->LookUpDictionaryOverlayAtPoint(rootPoint);
}

- (void)scrollWheel:(NSEvent*)event {
  // Consult with the delegate to see if it wants to handle the event. If the
  // delegate has handled the event, it takes priority over the page scrolling.
  if ([_responderDelegate respondsToSelector:@selector(handleEvent:)]) {
    BOOL handled = [_responderDelegate handleEvent:event];
    if (handled)
      return;
  }

  // Use an NSEvent monitor to listen for the wheel-end end. This ensures that
  // the event is received even when the mouse cursor is no longer over the view
  // when the scrolling ends (e.g. if the tab was switched). This is necessary
  // for ending rubber-banding in such cases.
  if (event.phase == NSEventPhaseBegan && !_endWheelMonitor) {
    _endWheelMonitor = [NSEvent
        addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel
                                     handler:^(NSEvent* blockEvent) {
                                       [self shortCircuitScrollWheelEvent:
                                                 blockEvent];
                                       return blockEvent;
                                     }];
  }

  // At this point, the delegate has passed on handling the event itself, so
  // build a mouse wheel event, and pass it on to the renderer for processing.
  WebMouseWheelEvent webEvent = WebMouseWheelEventBuilder::Build(event, self);
  webEvent.rails_mode = _mouseWheelFilter.UpdateRailsMode(webEvent);
  _hostHelper->RouteOrProcessWheelEvent(webEvent);
}

// Called repeatedly during a pinch gesture, with incremental change values.
- (void)magnifyWithEvent:(NSEvent*)event {
  [self magnifyWithEvent:event isSyntheticallyInjected:NO];
}

- (void)magnifyWithEvent:(NSEvent*)event
    isSyntheticallyInjected:(BOOL)injected {
  if (event.phase == NSEventPhaseMayBegin) {
    return;
  }

  WebGestureEvent gestureEvent = WebGestureEventBuilder::Build(event, self);
  _hostHelper->PinchEvent(gestureEvent, injected);
}

// Replace -[self window] with -keyTrackingWindow for any cases involving
// window key status (checking isKeyWindow, observing for
// NSWindowDidBecomeKeyNotification, etc.)
- (NSWindow*)keyTrackingWindow {
  if (remote_cocoa::IsNSToolbarFullScreenWindow(self.window)) {
    // In fullscreen, NSToolbarFullScreenWindow never becomes the key window.
    // If this RWHVCocoa is in the Toolbar, uses the browser window's key
    // status.
    NSWindow* browser_window =
        remote_cocoa::OriginalBrowserWindowFromFullScreenWindow(self.window);
    CHECK(browser_window);
    return browser_window;
  }
  return self.window;
}

- (BOOL)isKeyTrackingWindowKey {
  return self.keyTrackingWindow.isKeyWindow;
}

- (void)viewWillMoveToWindow:(NSWindow*)newWindow {
  NSWindow* oldWindow = [self window];
  NSWindow* oldKeyTrackingWindow = [self keyTrackingWindow];

  NSNotificationCenter* notificationCenter =
      [NSNotificationCenter defaultCenter];

  if (oldWindow) {
    [notificationCenter removeObserver:self
                                  name:NSWindowDidChangeScreenNotification
                                object:oldWindow];
    [notificationCenter
        removeObserver:self
                  name:NSWindowDidChangeBackingPropertiesNotification
                object:oldWindow];
    [notificationCenter removeObserver:self
                                  name:NSWindowDidMoveNotification
                                object:oldWindow];
    [notificationCenter removeObserver:self
                                  name:NSWindowDidResizeNotification
                                object:oldWindow];
    [notificationCenter removeObserver:self
                                  name:NSWindowWillCloseNotification
                                object:oldWindow];
    [notificationCenter removeObserver:self
                                  name:@"ChromeWillOrderFrontCharacterPalette"
                                object:nil];
    [notificationCenter removeObserver:self
                                  name:NSApplicationDidResignActiveNotification
                                object:nil];
  }
  if (oldKeyTrackingWindow) {
    [notificationCenter removeObserver:self
                                  name:NSWindowDidBecomeKeyNotification
                                object:oldKeyTrackingWindow];
    [notificationCenter removeObserver:self
                                  name:NSWindowDidResignKeyNotification
                                object:oldKeyTrackingWindow];
  }

  NSWindow* newKeyTrackingWindow =
      remote_cocoa::IsNSToolbarFullScreenWindow(newWindow)
          ? remote_cocoa::OriginalBrowserWindowFromFullScreenWindow(newWindow)
          : newWindow;

  if (newWindow) {
    [notificationCenter
        addObserver:self
           selector:@selector(windowDidChangeScreenOrBackingProperties:)
               name:NSWindowDidChangeScreenNotification
             object:newWindow];
    [notificationCenter
        addObserver:self
           selector:@selector(windowDidChangeScreenOrBackingProperties:)
               name:NSWindowDidChangeBackingPropertiesNotification
             object:newWindow];
    [notificationCenter addObserver:self
                           selector:@selector(windowChangedGlobalFrame:)
                               name:NSWindowDidMoveNotification
                             object:newWindow];
    [notificationCenter addObserver:self
                           selector:@selector(windowChangedGlobalFrame:)
                               name:NSWindowDidResizeNotification
                             object:newWindow];
    [notificationCenter addObserver:self
                           selector:@selector(windowWillClose:)
                               name:NSWindowWillCloseNotification
                             object:newWindow];
    [notificationCenter addObserver:self
                           selector:@selector(characterPaletteWillOrderFront:)
                               name:@"ChromeWillOrderFrontCharacterPalette"
                             object:nil];
    [notificationCenter addObserver:self
                           selector:@selector(applicationDidResignActive:)
                               name:NSApplicationDidResignActiveNotification
                             object:nil];
  }
  if (newKeyTrackingWindow) {
    [notificationCenter addObserver:self
                           selector:@selector(windowDidBecomeKey:)
                               name:NSWindowDidBecomeKeyNotification
                             object:newKeyTrackingWindow];
    [notificationCenter addObserver:self
                           selector:@selector(windowDidResignKey:)
                               name:NSWindowDidResignKeyNotification
                             object:newKeyTrackingWindow];
  }

  _hostHelper->SetAccessibilityWindow(newWindow);
  [self sendWindowFrameInScreenToHost];
}

- (void)updateScreenProperties {
  // This does not require enclosing window to exist, and allowing screen
  // properties change to propagate when it does not ensures that screen infos
  // are properly updated when running headless.
  // See // https://crbug.com/375425824.
  auto* screen = display::Screen::Get();
  const display::ScreenInfos newScreenInfos =
      screen->GetScreenInfosNearestDisplay(
          screen->GetDisplayNearestView(gfx::NativeView(self)).id());
  _host->OnScreenInfosChanged(newScreenInfos);
}

// This will be called when the NSView's NSWindow moves from one NSScreen to
// another, and makes note of the new screen's color space, scale factor, etc.
// It is also called when the current NSScreen's properties change (which is
// redundant with display::DisplayObserver::OnDisplayMetricsChanged).
- (void)windowDidChangeScreenOrBackingProperties:(NSNotification*)notification {
  // Delay calling updateScreenProperties so that display::ScreenMac can
  // update our display::Displays first (if applicable).
  [self performSelector:@selector(updateScreenProperties)
             withObject:nil
             afterDelay:0];
}

- (void)windowChangedGlobalFrame:(NSNotification*)notification {
  [self sendWindowFrameInScreenToHost];
  // Update the view bounds relative to the window, as they may have changed
  // during layout, and we don't explicitly listen for re-layout of parent
  // views.
  [self sendViewBoundsInWindowToHost];
  // Update screen properties in case the window moved to a different display.
  // AppKit only dispatches NSWindowDidChangeScreenNotification when crossing
  // physical NSScreens, which won't fire for virtual displays (e.g. in Headless
  // Mode).
  [self updateScreenProperties];
}

- (void)setFrame:(NSRect)r {
  // Note that -setFrame: calls through -setFrameSize: and -setFrameOrigin. To
  // avoid spamming the host with transiently invalid states, only send one
  // message at the end.
  _inSetFrame = YES;
  [super setFrame:r];
  _inSetFrame = NO;
  [self sendViewBoundsInWindowToHost];
}

- (void)setFrameOrigin:(NSPoint)newOrigin {
  [super setFrameOrigin:newOrigin];
  [self sendViewBoundsInWindowToHost];
}

- (void)setFrameSize:(NSSize)newSize {
  [super setFrameSize:newSize];
  [self sendViewBoundsInWindowToHost];
}

- (BOOL)canBecomeKeyView {
  if ([self hostIsDisconnected]) {
    return NO;
  }

  if (_responderDelegate &&
      [_responderDelegate
          respondsToSelector:@selector(shouldRefuseBecomingKeyView)] &&
      [_responderDelegate shouldRefuseBecomingKeyView]) {
    return NO;
  }

  return _canBeKeyView;
}

- (BOOL)acceptsFirstResponder {
  if ([self hostIsDisconnected])
    return NO;

  return _canBeKeyView;
}

- (void)windowDidBecomeKey:(NSNotification*)notification {
  DCHECK([self keyTrackingWindow]);
  DCHECK_EQ([self keyTrackingWindow], [notification object]);
  [self performDeferredResignKeyWindow];
  if ([_responderDelegate respondsToSelector:@selector(windowDidBecomeKey)])
    [_responderDelegate windowDidBecomeKey];
  if ([self isKeyTrackingWindowKey]) {
    _host->OnWindowIsKeyChanged(true);
  }
}

- (void)windowDidResignKey:(NSNotification*)notification {
  DCHECK([self keyTrackingWindow]);
  DCHECK_EQ([self keyTrackingWindow], [notification object]);

  // If our app is still active and we're still the key window, ignore this
  // message, since it just means that a menu extra (on the "system status bar")
  // was activated; we'll get another |-windowDidResignKey| if we ever really
  // lose key window status.
  if ([NSApp isActive] && ([NSApp keyWindow] == [self keyTrackingWindow])) {
    // Defer processing when the window is still reported as key. This occurs
    // in:
    // 1. Menu extra activation (system status bar items)
    // 2. Window switching via system notifications
    //
    // We cannot immediately determine if this is a transient state or true
    // resignation, so we defer until either another window becomes key or the
    // application resigns active status, at which point we can process
    // correctly.
    RenderWidgetHostViewCocoa.deferredResignKeyWindow = notification.object;
    return;
  }
  RenderWidgetHostViewCocoa.deferredResignKeyWindow = nil;

  _host->OnWindowIsKeyChanged(false);
  if (base::FeatureList::IsEnabled(
          features::kCancelCompositionWhenWindowLosesFocus)) {
    // Cancel any ongoing composition when the window loses focus.
    [self cancelComposition];
  }
}

- (void)windowWillClose:(NSNotification*)notification {
  // Clear deferred window if it's the one being closed. The __weak pointer
  // could remain valid between windowWillClose: and deallocation. If
  // performDeferredResignKeyWindow is called during this gap, it would post
  // NSWindowDidResignKeyNotification for a closing window. Explicit nil
  // prevents this race condition.
  if (RenderWidgetHostViewCocoa.deferredResignKeyWindow &&
      RenderWidgetHostViewCocoa.deferredResignKeyWindow ==
          notification.object) {
    RenderWidgetHostViewCocoa.deferredResignKeyWindow = nil;
  }
}

- (BOOL)becomeFirstResponder {
  if ([self hostIsDisconnected])
    return NO;
  if ([_responderDelegate respondsToSelector:@selector(becomeFirstResponder)])
    [_responderDelegate becomeFirstResponder];

  _host->OnFirstResponderChanged(true);

  // Cancel any ongoing composition text which was left before we lost focus.
  // TODO(suzhe): We should do it in -resignFirstResponder: method, but
  // somehow that method won't be called when switching among different tabs.
  // See http://crbug.com/47209
  [self cancelComposition];

  NSNumber* direction = @([[self window] keyViewSelectionDirection]);
  [[NSNotificationCenter defaultCenter]
      postNotificationName:kViewDidBecomeFirstResponder
                    object:self
                  userInfo:@{kSelectionDirection : direction}];

  return YES;
}

- (BOOL)resignFirstResponder {
  if ([_responderDelegate respondsToSelector:@selector(resignFirstResponder)])
    [_responderDelegate resignFirstResponder];

  _host->OnFirstResponderChanged(false);
  if (_closeOnDeactivate) {
    [self setHidden:YES];
    _host->RequestShutdown();
  }

  // We should cancel any ongoing composition whenever RWH's Blur() method gets
  // called, because in this case, webkit will confirm the ongoing composition
  // internally.
  [self cancelComposition];

  return YES;
}

- (void)applicationDidResignActive:(NSNotification*)notification {
  [self performDeferredResignKeyWindow];
}

- (void)performDeferredResignKeyWindow {
  // It's now time to decide whether the previously deferred
  // "windowDidResignKey" notification should be executed. If so, we repost
  // the notification to allow observers to update accordingly and ensure the
  // web contents enter the correct state.
  if (RenderWidgetHostViewCocoa.deferredResignKeyWindow &&
      RenderWidgetHostViewCocoa.deferredResignKeyWindow != [NSApp keyWindow]) {
    [[NSNotificationCenter defaultCenter]
        postNotificationName:NSWindowDidResignKeyNotification
                      object:RenderWidgetHostViewCocoa.deferredResignKeyWindow];
  }
  RenderWidgetHostViewCocoa.deferredResignKeyWindow = nil;
}

+ (NSWindow*)deferredResignKeyWindow {
  return _deferredResignKeyWindow;
}

+ (void)setDeferredResignKeyWindow:(NSWindow*)window {
  _deferredResignKeyWindow = window;
}

- (BOOL)isAutomaticQuoteSubstitutionEnabled {
  return [NSUserDefaults.standardUserDefaults
      boolForKey:WebAutomaticQuoteSubstitutionEnabled];
}

- (void)setAutomaticQuoteSubstitutionEnabled:(BOOL)enabled {
  [NSUserDefaults.standardUserDefaults
      setBool:enabled
       forKey:WebAutomaticQuoteSubstitutionEnabled];
}

- (void)toggleAutomaticQuoteSubstitution:(id)sender {
  self.automaticQuoteSubstitutionEnabled =
      !self.automaticQuoteSubstitutionEnabled;
}

- (BOOL)isAutomaticDashSubstitutionEnabled {
  return [NSUserDefaults.standardUserDefaults
      boolForKey:WebAutomaticDashSubstitutionEnabled];
}

- (void)setAutomaticDashSubstitutionEnabled:(BOOL)enabled {
  [NSUserDefaults.standardUserDefaults
      setBool:enabled
       forKey:WebAutomaticDashSubstitutionEnabled];
}

- (void)toggleAutomaticDashSubstitution:(id)sender {
  self.automaticDashSubstitutionEnabled =
      !self.automaticDashSubstitutionEnabled;
}

- (BOOL)isAutomaticTextReplacementEnabled {
  if (![NSUserDefaults.standardUserDefaults
          objectForKey:WebAutomaticTextReplacementEnabled]) {
    return NSSpellChecker.automaticTextReplacementEnabled;
  }
  return [NSUserDefaults.standardUserDefaults
      boolForKey:WebAutomaticTextReplacementEnabled];
}

- (void)setAutomaticTextReplacementEnabled:(BOOL)enabled {
  [NSUserDefaults.standardUserDefaults
      setBool:enabled
       forKey:WebAutomaticTextReplacementEnabled];
}

- (void)toggleAutomaticTextReplacement:(id)sender {
  self.automaticTextReplacementEnabled = !self.automaticTextReplacementEnabled;
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  if (item.action == @selector(orderFrontSubstitutionsPanel:))
    return YES;
  if (NSMenuItem* menuItem = base::apple::ObjCCast<NSMenuItem>(item)) {
    if (item.action == @selector(toggleAutomaticQuoteSubstitution:)) {
      menuItem.state = self.automaticQuoteSubstitutionEnabled;
      return !!(self.allowedTextCheckingTypes & NSTextCheckingTypeQuote);
    } else if (item.action == @selector(toggleAutomaticDashSubstitution:)) {
      menuItem.state = self.automaticDashSubstitutionEnabled;
      return !!(self.allowedTextCheckingTypes & NSTextCheckingTypeDash);
    } else if (item.action == @selector(toggleAutomaticTextReplacement:)) {
      menuItem.state = self.automaticTextReplacementEnabled;
      return !!(self.allowedTextCheckingTypes & NSTextCheckingTypeReplacement);
    } else if (item.action == @selector(uppercaseWord:)) {
      return self.canTransformText;
    } else if (item.action == @selector(lowercaseWord:)) {
      return self.canTransformText;
    } else if (item.action == @selector(capitalizeWord:)) {
      return self.canTransformText;
    }
  }

  if (_responderDelegate &&
      [_responderDelegate respondsToSelector:@selector
                          (validateUserInterfaceItem:isValidItem:)]) {
    BOOL valid;
    BOOL known =
        [_responderDelegate validateUserInterfaceItem:item isValidItem:&valid];
    if (known)
      return valid;
  }

  bool isForMainFrame = false;
  _host->SyncIsWidgetForMainFrame(&isForMainFrame);

  SEL action = [item action];

  if (action == @selector(stopSpeaking:)) {
    if (!isForMainFrame) {
      return NO;
    }
    bool isSpeaking = false;
    _host->SyncIsSpeaking(&isSpeaking);
    return isSpeaking;
  }

  if (action == @selector(startSpeaking:))
    return isForMainFrame;

  // For now, these actions are always enabled for render view,
  // this is sub-optimal.
  // TODO(suzhe): Plumb the "can*" methods up from WebCore.
  if (action == @selector(undo:) || action == @selector(redo:) ||
      action == @selector(cut:) || action == @selector(copy:) ||
      action == @selector(centerSelectionInVisibleArea:) ||
      action == @selector(copyToFindPboard:) || action == @selector(paste:) ||
      action == @selector(pasteAndMatchStyle:)) {
    return isForMainFrame;
  }

  return _editCommandHelper->IsMenuItemEnabled(action, self);
}

- (RenderWidgetHostNSViewHost*)renderWidgetHostNSViewHost {
  return _host;
}

- (void)setAccessibilityParentElement:(id)accessibilityParent {
  _accessibilityParent = accessibilityParent;
}

- (void)setPopupParentNSViewId:(uint64_t)viewId {
  _popupParentNSViewId = viewId;
}

- (id)accessibilityHitTest:(NSPoint)point {
  id rootElement = _hostHelper->GetRootBrowserAccessibilityElement();
  if (!rootElement) {
    if (features::IsAccessibilityRemoteUIAppEnabled()) {
      id rwhvElement = _hostHelper->GetAccessibilityElement();
      if (rwhvElement && rwhvElement != self) {
        return [rwhvElement accessibilityHitTest:point];
      }
    }
    return self;
  }

  // Calling accessibilityHitTest on the BrowserAccessibility element will
  // redirect the hit test request to the render side, in
  // RenderAccessibilityImpl::HitTest. This function expects the point passed by
  // parameter to be relative to the main document, not relative to the popup
  // window. In order to satisfy this requirement, we need to keep a reference
  // to the parent NSView of the popup NSView and transform the |point| using
  // that view.
  NSView* popupParentNSView =
      remote_cocoa::GetNSViewFromId(_popupParentNSViewId);

  NSView* view = popupParentNSView ? popupParentNSView : self;

  NSPoint pointInWindow = [view.window convertPointFromScreen:point];
  NSPoint localPoint = [view convertPoint:pointInWindow fromView:nil];
  localPoint.y = NSHeight([view bounds]) - localPoint.y;

  return [rootElement accessibilityHitTest:localPoint];
}

- (id)accessibilityFocusedUIElement {
  return _hostHelper->GetFocusedBrowserAccessibilityElement();
}

// NSAccessibility formal protocol:

- (NSArray*)accessibilityChildren {
  id root = _hostHelper->GetRootBrowserAccessibilityElement();
  if (root)
    return @[ root ];
  return nil;
}

- (NSArray*)accessibilityContents {
  return self.accessibilityChildren;
}

- (BOOL)isAccessibilityElement {
  return _hostHelper->GetRootBrowserAccessibilityElement() != nil;
}

- (id)accessibilityParent {
  if (_accessibilityParent)
    return NSAccessibilityUnignoredAncestor(_accessibilityParent);
  return [super accessibilityParent];
}

- (NSAccessibilityRole)accessibilityRole {
  if (_sonomaAccessibilityRefinementsAreActive) {
    // When an AT asks the application object for its role, we activate
    // nativeAPI accessibility support. If the AT descends into the AX tree
    // and arrives here (the web contents container), activate basic support for
    // all web content in the process so that the AT can descend further into
    // any web content it needs.
    if (!_basic_accessibility_mode) {
      _basic_accessibility_mode =
          content::BrowserAccessibilityState::GetInstance()
              ->CreateScopedModeForProcess(ui::kAXModeBasic |
                                           ui::AXMode::kFromPlatform);
    }
  }

  return NSAccessibilityScrollAreaRole;
}

// Below is our NSTextInputClient implementation.
//
// When WebHTMLView receives a NSEventTypeKeyDown event, WebHTMLView calls the
// following functions to process this event.
//
// [WebHTMLView keyDown] ->
//     EventHandler::keyEvent() ->
//     ...
//     [WebEditorHost handleKeyboardEvent] ->
//     [WebHTMLView _interceptEditingKeyEvent] ->
//     [NSResponder interpretKeyEvents] ->
//     [WebHTMLView insertText] ->
//     Editor::insertText()
//
// Unfortunately, it is hard for Chromium to use this implementation because
// it causes key-typing jank.
// RenderWidgetHostViewMac is running in a browser process. On the other
// hand, Editor and EventHandler are running in a renderer process.
// So, if we used this implementation, a NSEventTypeKeyDown event is dispatched
// to the following functions of Chromium.
//
// [RenderWidgetHostViewMac keyEvent] (browser) ->
//     |Sync IPC (KeyDown)| (*1) ->
//     EventHandler::keyEvent() (renderer) ->
//     ...
//     EditorHostImpl::handleKeyboardEvent() (renderer) ->
//     |Sync IPC| (*2) ->
//     [RenderWidgetHostViewMac _interceptEditingKeyEvent] (browser) ->
//     [self interpretKeyEvents] ->
//     [RenderWidgetHostViewMac insertText] (browser) ->
//     |Async IPC| ->
//     Editor::insertText() (renderer)
//
// (*1) we need to wait until this call finishes since WebHTMLView uses the
// result of EventHandler::keyEvent().
// (*2) we need to wait until this call finishes since WebEditorHost uses
// the result of [WebHTMLView _interceptEditingKeyEvent].
//
// This needs many sync IPC messages sent between a browser and a renderer for
// each key event, which would probably result in key-typing jank.
// To avoid this problem, this implementation processes key events (and input
// method events) totally in a browser process and sends asynchronous input
// events, almost same as KeyboardEvents (and TextEvents) of DOM Level 3, to a
// renderer process.
//
// [RenderWidgetHostViewMac keyEvent] (browser) ->
//     |Async IPC (RawKeyDown)| ->
//     [self interpretKeyEvents] ->
//     [RenderWidgetHostViewMac insertText] (browser) ->
//     |Async IPC (Char)| ->
//     Editor::insertText() (renderer)
//
// Since this implementation doesn't have to wait any IPC calls, this doesn't
// make any key-typing jank. --hbono 7/23/09
//
extern "C" {
extern NSString* NSTextInputReplacementRangeAttributeName;
}

- (NSArray*)validAttributesForMarkedText {
  // This code is just copied from WebKit except renaming variables.
  static NSArray* const kAttributes = @[
    NSUnderlineStyleAttributeName, NSUnderlineColorAttributeName,
    NSMarkedClauseSegmentAttributeName, NSTextInputReplacementRangeAttributeName
  ];
  return kAttributes;
}

- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint {
  DCHECK([self window]);
  // |thePoint| is in screen coordinates, but needs to be converted to WebKit
  // coordinates (upper left origin). Scroll offsets will be taken care of in
  // the renderer.
  thePoint = [self.window convertPointFromScreen:thePoint];
  thePoint = [self convertPoint:thePoint fromView:nil];
  gfx::PointF rootPoint =
      GetSanitizedFlippedPoint(thePoint, NSHeight([self frame]));

  uint32_t index = UINT32_MAX;
  _host->SyncGetCharacterIndexAtPoint(rootPoint, &index);
  // |index| could be blink::kNotFound (-1) and its value is different from
  // NSNotFound so we need to convert it.
  if (index == UINT32_MAX)
    return NSNotFound;
  size_t charIndex = index;
  return NSUInteger(charIndex);
}

- (BOOL)drawsVerticallyForCharacterAtIndex:(NSUInteger)charIndex {
  return !!(_textInputFlags & ui::TEXT_INPUT_FLAG_VERTICAL);
}

- (NSRect)firstRectForCharacterRange:(NSRange)theRange
                         actualRange:(NSRangePointer)actualRange {
  gfx::Rect gfxRect;
  gfx::Range gfxActualRange;
  bool success = false;
  if (actualRange)
    gfxActualRange = gfx::Range::FromPossiblyInvalidNSRange(*actualRange);

  _host->SyncGetFirstRectForRange(
      gfx::Range::FromPossiblyInvalidNSRange(theRange), &gfxRect,
      &gfxActualRange, &success);
  if (!success) {
    // The call to cancelComposition comes from https://crrev.com/350261.
    [self cancelComposition];
    return NSZeroRect;
  }
  if (actualRange)
    *actualRange = gfxActualRange.ToNSRange();

  // The returned rectangle is in WebKit coordinates (upper left origin), so
  // flip the coordinate system.
  NSRect viewFrame = [self frame];
  NSRect rect = gfxRect.ToCGRect();
  rect.origin.y = NSHeight(viewFrame) - NSMaxY(rect);

  // Convert into screen coordinates for return.
  rect = [self convertRect:rect toView:nil];
  rect = [[self window] convertRectToScreen:rect];

  if (_textInputFlags & ui::TEXT_INPUT_FLAG_VERTICAL) {
    // Google Japanese Input doesn't use the result of
    // drawsVerticallyForCharacterAtIndex. So we'd like to ask it to show its
    // horizontal candidate window at the right side of the caret if the text
    // is vertical.
    NSString* inputSourceName =
        [[self inputContext] selectedKeyboardInputSource];
    if ([inputSourceName hasPrefix:kGoogleJapaneseInputPrefix])
      rect.origin.x += rect.size.width;
  }

  return rect;
}

- (NSRange)selectedRange {
  return _textSelectionRange.ToNSRange();
}

- (NSRange)markedRange {
  // An input method calls this method to check if an application really has
  // a text being composed when hasMarkedText call returns true.
  // Returns the range saved in the setMarkedText method so the input method
  // calls the setMarkedText method and we can update the composition node
  // there. (When this method returns an empty range, the input method doesn't
  // call the setMarkedText method.)
  return _hasMarkedText ? _markedRange : NSMakeRange(NSNotFound, 0);
}

- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range
                                               actualRange:
                                                   (NSRangePointer)actualRange {
  // Prepare |actualRange| as if the proposed range is invalid. If it is valid,
  // then |actualRange| will be updated again.
  if (actualRange)
    *actualRange = NSMakeRange(NSNotFound, 0);

  // The caller of this method is allowed to pass nonsensical ranges. These
  // can't even be converted into gfx::Ranges.
  if (range.location == NSNotFound || range.length == 0)
    return nil;
  if (range.length >= std::numeric_limits<NSUInteger>::max() - range.location)
    return nil;

  const gfx::Range requestedRange =
      gfx::Range::FromPossiblyInvalidNSRange(range);
  if (requestedRange.is_reversed())
    return nil;

  gfx::Range expectedRange;
  const std::u16string* expectedText;

  expectedText = &_availableText;
  size_t offset = _availableTextOffset;
  expectedRange = gfx::Range(offset, offset + expectedText->size());

  gfx::Range gfxActualRange = expectedRange.Intersect(requestedRange);
  if (!gfxActualRange.IsValid())
    return nil;
  if (actualRange)
    *actualRange = gfxActualRange.ToNSRange();

  std::u16string string = expectedText->substr(
      gfxActualRange.start() - expectedRange.start(), gfxActualRange.length());
  return [[NSAttributedString alloc]
      initWithString:base::SysUTF16ToNSString(string)];
}

- (NSInteger)conversationIdentifier {
  return reinterpret_cast<NSInteger>(self);
}

// Each RenderWidgetHostViewCocoa has its own input context, but we return
// nil when the caret is in non-editable content or password box to avoid
// making input methods do their work.
//
// We disable input method inside password field as it is normal for macOS
// password input fields to not allow dead keys or non ASCII input methods.
// There is also a privacy risk if the composition candidate window shows your
// password when the user is "composing" inside a password field. See
// https://crbug.com/40759416 for more info.
//
// If AutoFill support has been disabled and we're currently showing a native
// context menu, then we return nil in order to ensure that macOS does NOT add
// any "AutoFill" items (contact, passwords, etc.) to the menu. This logic
// mirrors `ui/views/cocoa/text_input_host.mm`.
- (NSTextInputContext*)inputContext {
  if (_textInputType == ui::TEXT_INPUT_TYPE_NONE ||
      _textInputType == ui::TEXT_INPUT_TYPE_PASSWORD) {
    return nil;
  }

  if (_textInputFlags & ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD ||
      _textInputFlags & ui::TEXT_INPUT_FLAG_HAS_BEEN_CUSTOM_PASSWORD) {
    return nil;
  }

  if (!_supportsAutoFill &&
      ui::GetActiveCocoaMenuAnchorLocation().has_value()) {
    return nil;
  }

  return [super inputContext];
}

- (BOOL)hasMarkedText {
  // An input method calls this function to figure out whether or not an
  // application is really composing a text. If it is composing, it calls
  // the markedRange method, and maybe calls the setMarkedText method.
  // It seems an input method usually calls this function when it is about to
  // cancel an ongoing composition. If an application has a non-empty marked
  // range, it calls the setMarkedText method to delete the range.
  return _hasMarkedText;
}

- (void)unmarkText {
  // Delete the composition node of the renderer and finish an ongoing
  // composition.
  // It seems an input method calls the setMarkedText method and set an empty
  // text when it cancels an ongoing composition, i.e. I have never seen an
  // input method calls this method.
  _hasMarkedText = NO;
  _markedText.clear();
  _markedTextSelectedRange = NSMakeRange(NSNotFound, 0);
  _imeTextSpans.clear();

  // If we are handling a key down event, then FinishComposingText() will be
  // called in keyEvent: method.
  if (![self isHandlingKeyDown]) {
    _host->ImeFinishComposingText();
  } else {
    _unmarkTextCalled = YES;
  }
}

- (void)setMarkedText:(id)string
        selectedRange:(NSRange)newSelRange
     replacementRange:(NSRange)replacementRange {
  // An input method updates the composition string.
  // We send the given text and range to the renderer so it can update the
  // composition node of WebKit.
  // TODO(suzhe): It's hard for us to support replacementRange without accessing
  // the full web content.
  BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
  NSString* imText = isAttributedString ? [string string] : string;
  int length = [imText length];

  // |markedRange_| will get set on a callback from ImeSetComposition().
  _markedTextSelectedRange = newSelRange;
  _markedText = base::SysNSStringToUTF16(imText);

  // Update markedRange/textSelectionRange assuming blink sets composition text
  // as is. We need this because the IME checks markedRange/textSelectionRange
  // before IPC to blink. If markedRange/textSelectionRange is not updated, IME
  // will behave incorrectly, e.g., wrong popup window position or duplicate
  // characters.
  if (length > 0) {
    _hasMarkedText = YES;
    if (replacementRange.location != NSNotFound) {
      // If the replacement range is valid, the range should be replaced with
      // the new text.
      _markedRange = NSMakeRange(replacementRange.location, length);
    } else if (_markedRange.location == NSNotFound) {
      // If no replacement range and no marked range, the current selection
      // should be replaced.
      _markedRange = NSMakeRange(_textSelectionRange.start(), length);
    } else {
      // If no replacement range and the marked range is valid, the current
      // marked text should be replaced.
      _markedRange.length = length;
    }

    if (newSelRange.location != NSNotFound &&
        _markedRange.location <= std::numeric_limits<uint32_t>::max()) {
      CHECK_NE(_markedRange.location, static_cast<NSUInteger>(NSNotFound));
      CHECK_LE(newSelRange.location, std::numeric_limits<uint32_t>::max());
      // `_markedRange.location + NSMaxRange(newSelRange)` can be larger than
      // the maximum uint32_t. See crbug.com/40060200.
      uint32_t new_end = base::saturated_cast<uint32_t>(
          _markedRange.location + NSMaxRange(newSelRange));
      uint32_t new_start = base::saturated_cast<uint32_t>(
          _markedRange.location + newSelRange.location);
      _textSelectionRange = gfx::Range(new_start, new_end);
    }
  } else {
    // An empty text means the composition is about to be cancelled,
    // collapse the selection to the beginning of the current marked range.
    if (_hasMarkedText) {
      CHECK_LE(_markedRange.location, std::numeric_limits<uint32_t>::max())
          << "_markedRange.location is too large.";
      _textSelectionRange =
          gfx::Range(_markedRange.location, _markedRange.location);
    }
    _markedRange = NSMakeRange(NSNotFound, 0);
    _hasMarkedText = NO;
  }

  _imeTextSpans.clear();
  if (isAttributedString) {
    ExtractUnderlines(string, &_imeTextSpans);
  } else {
    // Use a thin black underline by default.
    _imeTextSpans.emplace_back(ui::ImeTextSpan::Type::kComposition, 0, length,
                               ui::ImeTextSpan::Thickness::kThin,
                               ui::ImeTextSpan::UnderlineStyle::kSolid,
                               SK_ColorTRANSPARENT);
  }

  // If we are handling a key down event and the reconversion is not triggered,
  // SetComposition() will be called in keyEvent: method.
  // Input methods of Mac use setMarkedText calls with an empty text to cancel
  // an ongoing composition. So, we should check whether or not the given text
  // is empty to update the input method state. (Our input method backend
  // automatically cancels an ongoing composition when we send an empty text.
  // So, it is OK to send an empty text to the renderer.)
  if ([self isHandlingKeyDown] && !_isReconversionTriggered) {
    _setMarkedTextReplacementRange = gfx::Range(replacementRange);
  } else {
    _host->ImeSetComposition(
        _markedText, _imeTextSpans,
        gfx::Range::FromPossiblyInvalidNSRange(replacementRange),
        newSelRange.location, NSMaxRange(newSelRange));
  }

  [[self inputContext] invalidateCharacterCoordinates];
  [self setNeedsDisplay:YES];
}

- (void)doCommandBySelector:(SEL)selector {
  // An input method calls this function to dispatch an editing command to be
  // handled by this view.
  if (selector == @selector(noop:))
    return;

  std::string command(base::SysNSStringToUTF8(
      RenderWidgetHostViewMacEditCommandHelper::CommandNameForSelector(
          selector)));

  // If this method is called when handling a key down event, then we need to
  // handle the command in the key event handler. Otherwise we can just handle
  // it here.
  if ([self isHandlingKeyDown]) {
    if ((_textInputFlags & ui::TEXT_INPUT_FLAG_VERTICAL)) {
      // Commands assigned to arrow keys are ignored and Blink handles key down
      // events because macOS doesn't work well with some vertical writing
      // modes. See editing_behavior.cc.
      //
      // The following bindings are affected:
      //                 Left: moveLeft
      //           Shift-Left: moveLeftAndModifySelection
      //          Option-Left: moveWordLeft
      //    Option-Shift-Left: moveWordLeftAndModifySelection
      //                Right: moveRight
      //          Shift-Right: moveRightAndModifySelection
      //         Option-Right: moveWordRight
      //   Option-Shift-Right: moveWordRightAndModifySelection
      //                   Up: moveUp
      //             Shift-Up: moveUpAndModifySelection
      //            Option-Up: moveBackward + moveToBeginningOfParagraph
      //                 Down: moveDown
      //           Shift-Down: moveDownAndModifySelection
      //          Option-Down: moveForward + moveToEndOfParagraph:
      //
      // This doesn't affect Fn + an arrow key, which produces a keyCode for
      // PageUp, PageDown, Home, or End.
      unsigned short keyCode = *_currentKeyDownCode;
      if (keyCode == kVK_LeftArrow || keyCode == kVK_RightArrow ||
          keyCode == kVK_DownArrow || keyCode == kVK_UpArrow) {
        return;
      }
    }
    _hasEditCommands = YES;
    // We ignore commands that insert characters, because this was causing
    // strange behavior (e.g. tab always inserted a tab rather than moving to
    // the next field on the page).
    if (!base::StartsWith(command, "insert",
                          base::CompareCase::INSENSITIVE_ASCII))
      _editCommands.push_back(blink::mojom::EditCommand::New(command, ""));
  } else {
    _host->ExecuteEditCommand(command);
  }
}

- (void)insertText:(id)string replacementRange:(NSRange)replacementRange {
  // An input method has characters to be inserted.
  // Same as Linux, Mac calls this method not only:
  // * when an input method finishes composing text, but also;
  // * when we type an ASCII character (without using input methods).
  // When we aren't using input methods, we should send the given character as
  // a Char event so it is dispatched to an onkeypress() event handler of
  // JavaScript.
  // On the other hand, when we are using input methods, we should send the
  // given characters as an input method event and prevent the characters from
  // being dispatched to onkeypress() event handlers.
  // Text inserting might be initiated by other source instead of keyboard
  // events, such as the Characters dialog. In this case the text should be
  // sent as an input method event as well.
  BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
  NSString* imText = isAttributedString ? [string string] : string;
  if ([self isHandlingKeyDown] && replacementRange.location == NSNotFound) {
    // The user uses keyboard to type in a char without an IME or select a word
    // on the IME. Don't commit the change to the render, because the event is
    // being processed in |keyEvent:|. The commit will happen later after
    // |interpretKeyEvents:| returns.
    _textToBeInserted.append(base::SysNSStringToUTF16(imText));
    _shouldRequestTextSubstitutions = YES;
  } else {
    // Fix the issue that Apple intelligence's writing tools not working. The
    // writing tools bubble will grab the focus from browser after the user
    // clicks replace button in the bubble which causes the replaced text can
    // not be inserted into browser IME since the content's NSView loses focus.
    // Please note that this is a workaround fix and should be removed after the
    // issue is finally fixed by Apple which is tracked via FB16872510.
    NSResponder* firstResponder = [self.window firstResponder];
    if ([firstResponder isKindOfClass:NSClassFromString(@"NSRemoteView")]) {
      NSView* firstResponderView = (NSView*)firstResponder;
      NSView* superView = firstResponderView.superview;
      if ([superView isKindOfClass:NSClassFromString(@"WTWritingToolsView")]) {
        [self becomeFirstResponder];
      }
    }

    // The user uses mouse or touch bar to select a word on the IME.
    gfx::Range replacementGfxRange =
        gfx::Range::FromPossiblyInvalidNSRange(replacementRange);
    _host->ImeCommitText(base::SysNSStringToUTF16(imText), replacementGfxRange);
  }

  if (replacementRange.location == NSNotFound) {
    // Cancel selection after a IME commit by setting a zero-length selection
    // at the end of insertion point.
    // This is required for macOS 10.12+, otherwise the predictive completions
    // of IMEs won't work. See crbug.com/710101.
    int insertEndpoint = _markedRange.location + [imText length];
    _textSelectionRange = gfx::Range(insertEndpoint, insertEndpoint);
    // IMEs read |_availableText| preceding the insertion point as the context
    // for predictive completion. Unfortunately by the moment IME reads the
    // text, Blink likely hasn't finished the commit so the IME will read a
    // wrong context. We hack it by temporarily inserting the committed text
    // into
    // |_availableText|. This variable will ultimately be asynchronously updated
    // by Blink.
    // TODO(crbug.com/40165347): Mac's IME API is synchronous and it plays badly
    // with async APIs between the browser and the renderer. Probably replace
    // the sync |interpretKeyEvents:| with the async
    // |handleEventByInputMethod:|, which is an undocumented API used in
    // Webkit2.
    if (_markedRange.location >= _availableTextOffset &&
        _markedRange.location <= _availableTextOffset + _availableText.length())
      _availableText.insert(_markedRange.location - _availableTextOffset,
                            base::SysNSStringToUTF16(imText));
  }

  // Inserting text will delete all marked text automatically.
  _hasMarkedText = NO;
}

- (void)insertText:(id)string {
  [self insertText:string replacementRange:NSMakeRange(NSNotFound, 0)];
}

- (void)viewDidMoveToWindow {
  // Update the window's frame, the view's bounds, focus, and the display info,
  // as they have not been updated while unattached to a window.
  [self sendWindowFrameInScreenToHost];
  [self sendViewBoundsInWindowToHost];

  if ([self window]) {
    [self updateScreenProperties];
  }

  _host->OnWindowIsKeyChanged([self isKeyTrackingWindowKey]);
  _host->OnFirstResponderChanged([[self window] firstResponder] == self);

  // If we switch windows (or are removed from the view hierarchy), cancel any
  // open mouse-downs.
  if (_hasOpenMouseDown) {
    WebMouseEvent event(WebInputEvent::Type::kMouseUp,
                        WebInputEvent::kNoModifiers, ui::EventTimeForNow());
    event.button = WebMouseEvent::Button::kLeft;
    _hostHelper->ForwardMouseEvent(event);
    _hasOpenMouseDown = NO;
  }
}

- (void)undo:(id)sender {
  _host->Undo();
}

- (void)redo:(id)sender {
  _host->Redo();
}

- (void)cut:(id)sender {
  _host->Cut();
}

- (void)copy:(id)sender {
  _host->Copy();
}

- (void)copyToFindPboard:(id)sender {
  _host->CopyToFindPboard();
}

- (void)centerSelectionInVisibleArea:(id)sender {
  _host->CenterSelection();
}

- (void)paste:(id)sender {
  _host->Paste();
}

- (void)pasteAndMatchStyle:(id)sender {
  _host->PasteAndMatchStyle();
}

- (void)selectAll:(id)sender {
  // editCommandHelper_ adds implementations for most NSResponder methods
  // dynamically. But the renderer side only sends selection results back to
  // the browser if they were triggered by a keyboard event or went through
  // one of the Select methods on RWH. Since selectAll: is called from the
  // menu handler, neither is true.
  // Explicitly call SelectAll() here to make sure the renderer returns
  // selection results.
  _host->SelectAll();
}

- (void)startSpeaking:(id)sender {
  _host->StartSpeaking();
}

- (void)stopSpeaking:(id)sender {
  _host->StopSpeaking();
}

- (void)cancelComposition {
  [NSSpellChecker.sharedSpellChecker dismissCorrectionIndicatorForView:self];

  if (!_hasMarkedText)
    return;

  NSTextInputContext* inputContext = [self inputContext];
  [inputContext discardMarkedText];

  _hasMarkedText = NO;
  // Should not call [self unmarkText] here, because it'll send unnecessary
  // cancel composition IPC message to the renderer.
}

- (void)finishComposingText {
  if (!_hasMarkedText)
    return;

  _host->ImeFinishComposingText();
  [self cancelComposition];
}

// Overriding a NSResponder method to support application services.

- (id)validRequestorForSendType:(NSString*)sendType
                     returnType:(NSString*)returnType {
  UTType* sendUTType = ui::UTTypeForServicesType(sendType);
  UTType* acceptUTType = ui::UTTypeForServicesType(returnType);

  const BOOL canSendText = [sendUTType isEqual:UTTypeUTF8PlainText] &&
                           !_textSelectionRange.is_empty();
  const BOOL canAcceptText = [acceptUTType isEqual:UTTypeUTF8PlainText] &&
                             _textInputType != ui::TEXT_INPUT_TYPE_NONE;

  // This is a valid requestor if the send/accept types can be fulfilled or if
  // they are `nil` (and therefore not the wrong type).
  if ((canSendText && !acceptUTType) || (!sendUTType && canAcceptText) ||
      (canSendText && canAcceptText)) {
    return self;
  }
  return [super validRequestorForSendType:sendType returnType:returnType];
}

- (BOOL)shouldChangeCurrentCursor {
  // |updateCursor:| might be called outside the view bounds. Check the mouse
  // location before setting the cursor. Also, do not set cursor if it's not a
  // key window.
  NSPoint location = [self.window convertPointFromScreen:NSEvent.mouseLocation];
  location = [self convertPoint:location fromView:nil];
  if (![self mouse:location inRect:[self bounds]] ||
      ![self isKeyTrackingWindowKey]) {
    return NO;
  }

  if (_cursorHidden || _showingContextMenu)
    return NO;

  return YES;
}

- (void)updateCursor:(NSCursor*)cursor {
  if (_currentCursor == cursor)
    return;

  _currentCursor = cursor;
  [[self window] invalidateCursorRectsForView:self];

  // NSWindow's invalidateCursorRectsForView: resets cursor rects but does not
  // update the cursor instantly. The cursor is updated when the mouse moves.
  // Update the cursor instantly by setting the current cursor.
  if ([self shouldChangeCurrentCursor])
    [_currentCursor set];
}

- (void)popupWindowWillClose:(NSNotification*)notification {
  [self setHidden:YES];
  _host->RequestShutdown();
}

- (void)invalidateTouchBar {
  // Work around a crash (https://crbug.com/822427).
  [_candidateListTouchBarItem setCandidates:@[]
                           forSelectedRange:NSMakeRange(NSNotFound, 0)
                                   inString:nil];
  _candidateListTouchBarItem = nil;
  self.touchBar = nil;
}

- (NSTouchBar*)makeTouchBar {
  if (_textInputType != ui::TEXT_INPUT_TYPE_NONE) {
    _candidateListTouchBarItem = [[NSCandidateListTouchBarItem alloc]
        initWithIdentifier:NSTouchBarItemIdentifierCandidateList];

    _candidateListTouchBarItem.delegate = self;
    _candidateListTouchBarItem.client = self;
    [self requestTextSuggestions];

    auto* touchBar = [[NSTouchBar alloc] init];
    touchBar.customizationIdentifier = ui::GetTouchBarId(kWebContentTouchBarId);
    touchBar.templateItems = [NSSet setWithObject:_candidateListTouchBarItem];
    bool includeEmojiPicker =
        _textInputType == ui::TEXT_INPUT_TYPE_TEXT ||
        _textInputType == ui::TEXT_INPUT_TYPE_SEARCH ||
        _textInputType == ui::TEXT_INPUT_TYPE_TEXT_AREA ||
        _textInputType == ui::TEXT_INPUT_TYPE_CONTENT_EDITABLE;
    if (includeEmojiPicker) {
      touchBar.defaultItemIdentifiers = @[
        NSTouchBarItemIdentifierCharacterPicker,
        NSTouchBarItemIdentifierCandidateList
      ];
    } else {
      touchBar.defaultItemIdentifiers =
          @[ NSTouchBarItemIdentifierCandidateList ];
    }
    return touchBar;
  }

  return [super makeTouchBar];
}

- (BOOL)isHandlingKeyDown {
  return _currentKeyDownCode.has_value();
}

@end

//
// Supporting application services
//

@interface RenderWidgetHostViewCocoa (
    NSServicesRequests)<NSServicesMenuRequestor>
@end

@implementation RenderWidgetHostViewCocoa (NSServicesRequests)

- (BOOL)writeSelectionToPasteboard:(NSPasteboard*)pboard types:(NSArray*)types {
  NSSet<UTType*>* typeSet = ui::UTTypesForServicesTypeArray(types);

  bool wasAbleToWriteAtLeastOneType = false;

  if ([typeSet containsObject:UTTypeUTF8PlainText] &&
      !_textSelectionRange.is_empty()) {
    NSString* text = base::SysUTF16ToNSString([self selectedText]);
    wasAbleToWriteAtLeastOneType |= [pboard writeObjects:@[ text ]];
  }

  return wasAbleToWriteAtLeastOneType;
}

- (BOOL)readSelectionFromPasteboard:(NSPasteboard*)pboard {
  NSArray* objects = [pboard readObjectsForClasses:@[ [NSString class] ]
                                           options:nil];
  if (!objects.count) {
    return NO;
  }

  // If the user is currently using an IME, confirm the IME input,
  // and then insert the text from the service, the same as TextEdit and Safari.
  [self finishComposingText];

  // It's expected that there only will be one string object on the pasteboard,
  // but if there is more than one, catenate them. This is the same compat
  // technique used by the compatibility call, -[NSPasteboard stringForType:].
  NSString* allTheText = [objects componentsJoinedByString:@"\n"];
  [self insertText:allTheText];
  return YES;
}

// "-webkit-app-region: drag | no-drag" is implemented on Mac by excluding
// regions that are not draggable. (See ControlRegionView in
// native_app_window_cocoa.mm). This requires the render host view to be
// draggable by default.
- (BOOL)mouseDownCanMoveWindow {
  return YES;
}

@end
