chromium/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm

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

#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"

#import <AppKit/AppKit.h>
#include <Foundation/Foundation.h>
#include <Security/Security.h>
#import <SecurityInterface/SecurityInterface.h>
#import <objc/runtime.h>
#include <stddef.h>
#include <stdint.h>

#include <cmath>
#include <memory>

#include "base/apple/bridging.h"
#import "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#import "components/remote_cocoa/app_shim/NSToolbar+Private.h"
#import "components/remote_cocoa/app_shim/bridged_content_view.h"
#import "components/remote_cocoa/app_shim/browser_native_widget_window_mac.h"
#import "components/remote_cocoa/app_shim/context_menu_runner.h"
#import "components/remote_cocoa/app_shim/mouse_capture.h"
#import "components/remote_cocoa/app_shim/native_widget_mac_frameless_nswindow.h"
#import "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h"
#import "components/remote_cocoa/app_shim/native_widget_mac_overlay_nswindow.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_host_helper.h"
#include "components/remote_cocoa/app_shim/select_file_dialog_bridge.h"
#import "components/remote_cocoa/app_shim/views_nswindow_delegate.h"
#import "components/remote_cocoa/app_shim/window_move_loop.h"
#include "components/remote_cocoa/common/native_widget_ns_window_host.mojom.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/cert/x509_util_apple.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#import "ui/base/cocoa/constrained_window/constrained_window_animation.h"
#include "ui/base/cocoa/cursor_utils.h"
#include "ui/base/cocoa/remote_accessibility_api.h"
#import "ui/base/cocoa/window_size_constants.h"
#include "ui/base/emoji/emoji_panel_helper.h"
#include "ui/base/hit_test.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/ui_base_switches.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/cocoa/cocoa_event_utils.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/size_conversions.h"
#import "ui/gfx/mac/coordinate_conversion.h"
#import "ui/gfx/mac/nswindow_frame_controls.h"

using remote_cocoa::mojom::VisibilityTransition;
using remote_cocoa::mojom::WindowVisibilityState;

namespace {
constexpr auto kUIPaintTimeout = base::Seconds(5);

// Returns the display that the specified window is on.
display::Display GetDisplayForWindow(NSWindow* window) {
  return display::Screen::GetScreen()->GetDisplayNearestWindow(window);
}

}  // namespace

// The NSView that hosts the composited CALayer drawing the UI. It fills the
// window but is not hittable so that accessibility hit tests always go to the
// BridgedContentView.
@interface ViewsCompositorSuperview : NSView
@end

@implementation ViewsCompositorSuperview
- (NSView*)hitTest:(NSPoint)aPoint {
  return nil;
}
@end

// Self-owning animation delegate that starts a hide animation, then calls
// -[NSWindow close] when the animation ends, releasing itself.
@interface ViewsNSWindowCloseAnimator : NSObject <NSAnimationDelegate> {
 @private
  NSWindow* __strong _window;
  NSAnimation* __strong _animation;
}
+ (void)closeWindowWithAnimation:(NSWindow*)window;

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithWindow:(NSWindow*)window NS_UNAVAILABLE;

@end

@implementation ViewsNSWindowCloseAnimator

- (instancetype)initWithWindow:(NSWindow*)window {
  if ((self = [super init])) {
    _window = window;
    _animation = [[ConstrainedWindowAnimationHide alloc] initWithWindow:window];
    [_animation setDelegate:self];
    [_animation setAnimationBlockingMode:NSAnimationNonblocking];
    [_animation startAnimation];
  }
  return self;
}

+ (NSMutableSet<ViewsNSWindowCloseAnimator*>*)allAnimators {
  static NSMutableSet<ViewsNSWindowCloseAnimator*>* set = [NSMutableSet set];
  return set;
}

+ (void)closeWindowWithAnimation:(NSWindow*)window {
  ViewsNSWindowCloseAnimator* animator =
      [[ViewsNSWindowCloseAnimator alloc] initWithWindow:window];
  if (animator) {
    [[ViewsNSWindowCloseAnimator allAnimators] addObject:animator];
  }
}

- (void)animationDidEnd:(NSAnimation*)animation {
  [_window close];
  [_animation setDelegate:nil];
  [[ViewsNSWindowCloseAnimator allAnimators]
      performSelector:@selector(removeObject:)
           withObject:self
           afterDelay:0];
}
@end

// This class overrides NSAnimation methods to invalidate the shadow for each
// frame. It is required because the show animation uses CGSSetWindowWarp()
// which is touchy about the consistency of the points it is given. The show
// animation includes a translate, which fails to apply properly to the window
// shadow, when that shadow is derived from a layer-hosting view. So invalidate
// it. This invalidation is only needed to cater for the translate. It is not
// required if CGSSetWindowWarp() is used in a way that keeps the center point
// of the window stationary (e.g. a scale). It's also not required for the hide
// animation: in that case, the shadow is never invalidated so retains the
// shadow calculated before a translate is applied.
@interface ModalShowAnimationWithLayer
    : ConstrainedWindowAnimationShow <NSAnimationDelegate>
@end

@implementation ModalShowAnimationWithLayer {
  // This is the "real" delegate, but this class acts as the NSAnimationDelegate
  // to avoid a separate object.
  raw_ptr<remote_cocoa::NativeWidgetNSWindowBridge> _bridgedNativeWidget;
}
- (instancetype)initWithBridgedNativeWidget:
    (remote_cocoa::NativeWidgetNSWindowBridge*)widget {
  if ((self = [super initWithWindow:widget->ns_window()])) {
    _bridgedNativeWidget = widget;
    CHECK(_bridgedNativeWidget);
    self.delegate = self;
  }
  return self;
}
- (void)dealloc {
  CHECK(!_bridgedNativeWidget);
}
- (void)animationDidEnd:(NSAnimation*)animation {
  CHECK(_bridgedNativeWidget);
  // The call to `OnShowAnimationComplete()` will immediately reset an owning
  // pointer of this object. Therefore, make sure all the invariants of the
  // `-dealloc` method above are satisfied now by moving the pointer value to be
  // local.
  remote_cocoa::NativeWidgetNSWindowBridge* bridgedNativeWidget =
      _bridgedNativeWidget;
  _bridgedNativeWidget = nullptr;
  bridgedNativeWidget->OnShowAnimationComplete();
  self.delegate = nil;
}
- (void)stopAnimation {
  [super stopAnimation];
  [self.window invalidateShadow];
}
- (void)setCurrentProgress:(NSAnimationProgress)progress {
  [super setCurrentProgress:progress];
  [self.window invalidateShadow];
}
@end

namespace remote_cocoa {

namespace {

using RankMap = std::map<NSView*, int>;

// Return the content size for a minimum or maximum widget size.
gfx::Size GetClientSizeForWindowSize(NSWindow* window,
                                     const gfx::Size& window_size) {
  NSRect frame_rect =
      NSMakeRect(0, 0, window_size.width(), window_size.height());
  // Note gfx::Size will prevent dimensions going negative. They are allowed to
  // be zero at this point, because Widget::GetMinimumSize() may later increase
  // the size.
  return gfx::Size([window contentRectForFrameRect:frame_rect].size);
}

NSComparisonResult SubviewSorter(__kindof NSView* lhs,
                                 __kindof NSView* rhs,
                                 void* rank_as_void) {
  DCHECK_NE(lhs, rhs);

  if ([lhs isKindOfClass:[ViewsCompositorSuperview class]])
    return NSOrderedAscending;

  const RankMap* rank = static_cast<const RankMap*>(rank_as_void);
  auto left_rank = rank->find(lhs);
  auto right_rank = rank->find(rhs);
  bool left_found = left_rank != rank->end();
  bool right_found = right_rank != rank->end();

  // Sort unassociated views above associated views.
  if (left_found != right_found)
    return left_found ? NSOrderedAscending : NSOrderedDescending;

  if (left_found) {
    return left_rank->second < right_rank->second ? NSOrderedAscending
                                                  : NSOrderedDescending;
  }

  // If both are unassociated, consider that order is not important
  return NSOrderedSame;
}

// Counts windows managed by a NativeWidgetNSWindowBridge instance in the
// |child_windows| array ignoring the windows added by AppKit.
NSUInteger CountBridgedWindows(NSArray* child_windows) {
  NSUInteger count = 0;
  for (NSWindow* child in child_windows) {
    if ([[child delegate] isKindOfClass:[ViewsNSWindowDelegate class]]) {
      ++count;
    }
  }

  return count;
}

std::map<uint64_t, NativeWidgetNSWindowBridge*>& GetIdToWidgetImplMap() {
  static base::NoDestructor<std::map<uint64_t, NativeWidgetNSWindowBridge*>>
      id_map;
  return *id_map;
}

std::map<NSWindow*, std::u16string>& GetPendingWindowTitleMap() {
  static base::NoDestructor<std::map<NSWindow*, std::u16string>> map;
  return *map;
}

}  // namespace

// static
gfx::Size NativeWidgetNSWindowBridge::GetWindowSizeForClientSize(
    NSWindow* window,
    const gfx::Size& content_size) {
  NSRect content_rect =
      NSMakeRect(0, 0, content_size.width(), content_size.height());
  NSRect frame_rect = [window frameRectForContentRect:content_rect];
  return gfx::Size(NSWidth(frame_rect), NSHeight(frame_rect));
}

// static
NativeWidgetNSWindowBridge* NativeWidgetNSWindowBridge::GetFromId(
    uint64_t bridged_native_widget_id) {
  auto found = GetIdToWidgetImplMap().find(bridged_native_widget_id);
  if (found == GetIdToWidgetImplMap().end())
    return nullptr;
  return found->second;
}

// static
NativeWidgetNSWindowBridge* NativeWidgetNSWindowBridge::GetFromNativeWindow(
    gfx::NativeWindow native_window) {
  NSWindow* window = native_window.GetNativeNSWindow();
  if (NativeWidgetMacNSWindow* widget_window =
          base::apple::ObjCCast<NativeWidgetMacNSWindow>(window)) {
    return GetFromId([widget_window bridgedNativeWidgetId]);
  }
  return nullptr;
}

// static
NativeWidgetMacNSWindow* NativeWidgetNSWindowBridge::CreateNSWindow(
    const mojom::CreateWindowParams* params) {
  NativeWidgetMacNSWindow* ns_window;
  switch (params->window_class) {
    case mojom::WindowClass::kDefault:
      ns_window = [[NativeWidgetMacNSWindow alloc]
          initWithContentRect:ui::kWindowSizeDeterminedLater
                    styleMask:params->style_mask
                      backing:NSBackingStoreBuffered
                        defer:NO];
      break;
    case mojom::WindowClass::kBrowser:
      ns_window = [[BrowserNativeWidgetWindow alloc]
          initWithContentRect:ui::kWindowSizeDeterminedLater
                    styleMask:params->style_mask
                      backing:NSBackingStoreBuffered
                        defer:NO];
      break;
    case mojom::WindowClass::kFrameless:
      ns_window = [[NativeWidgetMacFramelessNSWindow alloc]
          initWithContentRect:ui::kWindowSizeDeterminedLater
                    styleMask:params->style_mask
                      backing:NSBackingStoreBuffered
                        defer:NO];
      break;
    case mojom::WindowClass::kOverlay:
      ns_window = [[NativeWidgetMacOverlayNSWindow alloc]
          initWithContentRect:ui::kWindowSizeDeterminedLater
                    styleMask:params->style_mask
                      backing:NSBackingStoreBuffered
                        defer:NO];
      break;
  }
  ns_window.releasedWhenClosed = NO;

  if (params->titlebar_appears_transparent) {
    ns_window.titlebarAppearsTransparent = YES;
  }
  if (params->window_title_hidden) {
    ns_window.titleVisibility = NSWindowTitleHidden;
  }
  if (params->animation_enabled) {
    ns_window.animationBehavior = NSWindowAnimationBehaviorDocumentWindow;
  }

  return ns_window;
}

NativeWidgetNSWindowBridge::NativeWidgetNSWindowBridge(
    uint64_t bridged_native_widget_id,
    NativeWidgetNSWindowHost* host,
    NativeWidgetNSWindowHostHelper* host_helper,
    mojom::TextInputHost* text_input_host)
    : id_(bridged_native_widget_id),
      host_(host),
      host_helper_(host_helper),
      text_input_host_(text_input_host) {
  DCHECK(GetIdToWidgetImplMap().find(id_) == GetIdToWidgetImplMap().end());
  GetIdToWidgetImplMap().insert(std::make_pair(id_, this));
}

NativeWidgetNSWindowBridge::~NativeWidgetNSWindowBridge() {
  SetLocalEventMonitorEnabled(false);
  DCHECK(!key_down_event_monitor_);
  GetPendingWindowTitleMap().erase(window_);
  // The delegate should be cleared already. Note this enforces the precondition
  // that -[NSWindow close] is invoked on the hosted window before the
  // destructor is called.
  DCHECK(![window_ delegate]);
  DCHECK(child_windows_.empty());
  DestroyContentView();
}

void NativeWidgetNSWindowBridge::BindReceiver(
    mojo::PendingAssociatedReceiver<mojom::NativeWidgetNSWindow> receiver,
    base::OnceClosure connection_closed_callback) {
  bridge_mojo_receiver_.Bind(std::move(receiver),
                             ui::WindowResizeHelperMac::Get()->task_runner());
  bridge_mojo_receiver_.set_disconnect_handler(
      std::move(connection_closed_callback));
}

void NativeWidgetNSWindowBridge::SetWindow(NativeWidgetMacNSWindow* window) {
  DCHECK(!window_);
  window_delegate_ =
      [[ViewsNSWindowDelegate alloc] initWithBridgedNativeWidget:this];
  window_ = window;
  window_.bridge = this;
  window_.bridgedNativeWidgetId = id_;
  window_.releasedWhenClosed = NO;
  window_.delegate = window_delegate_;
  ui::CATransactionCoordinator::Get().AddPreCommitObserver(this);
}

void NativeWidgetNSWindowBridge::SetCommandDispatcher(
    NSObject<CommandDispatcherDelegate>* delegate,
    id<UserInterfaceItemCommandHandler> command_handler) {
  window_command_dispatcher_delegate_ = delegate;
  [window_ setCommandDispatcherDelegate:delegate];
  [window_ setCommandHandler:command_handler];
}

void NativeWidgetNSWindowBridge::SetParent(uint64_t new_parent_id) {
  // Remove from the old parent.
  if (parent_) {
    parent_->RemoveChildWindow(this);
    parent_ = nullptr;
  }
  if (!new_parent_id)
    return;

  // It is only valid to have a NativeWidgetMac be the parent of another
  // NativeWidgetMac.
  NativeWidgetNSWindowBridge* new_parent =
      NativeWidgetNSWindowBridge::GetFromId(new_parent_id);
  if (!new_parent) {
    // When the OS tells us a window is closing it is removed from the id map.
    // Since nothing is stopping the browser process from still trying to use
    // that id until the browser process has been informed that the window is
    // gone, it is totally possible to be passed no longer valid ids here.
    return;
  }

  parent_ = new_parent;
  parent_->child_windows_.push_back(this);

  // Widget::ShowInactive() could result in a Space switch when the widget has a
  // parent, and we're calling -orderWindow:relativeTo:. Use Transient
  // collection behaviour to prevent that.
  // https://crbug.com/697829
  [window_ setCollectionBehavior:[window_ collectionBehavior] |
                                 NSWindowCollectionBehaviorTransient];

  if (wants_to_be_visible_)
    parent_->OrderChildren();
}

void NativeWidgetNSWindowBridge::CreateSelectFileDialog(
    mojo::PendingReceiver<mojom::SelectFileDialog> receiver) {
  mojo::MakeSelfOwnedReceiver(
      std::make_unique<remote_cocoa::SelectFileDialogBridge>(window_),
      std::move(receiver));
}

void NativeWidgetNSWindowBridge::ShowCertificateViewer(
    const scoped_refptr<net::X509Certificate>& certificate) {
  NSArray* cert_chain = base::apple::CFToNSOwnershipCast(
      net::x509_util::CreateSecCertificateArrayForX509Certificate(
          certificate.get())
          .release());
  if (!cert_chain) {
    return;
  }

  [[[SFCertificatePanel alloc] init] beginSheetForWindow:window_
                                           modalDelegate:nil
                                          didEndSelector:nil
                                             contextInfo:nil
                                            certificates:cert_chain
                                               showGroup:YES];
}

void NativeWidgetNSWindowBridge::StackAbove(uint64_t sibling_id) {
  NativeWidgetNSWindowBridge* sibling_bridge =
      NativeWidgetNSWindowBridge::GetFromId(sibling_id);
  if (!sibling_bridge) {
    // When the OS tells us a window is closing it is removed from the id map.
    // Since nothing is stopping the browser process from still trying to use
    // that id until the browser process has been informed that the window is
    // gone, it is totally possible to be passed no longer valid ids here.
    return;
  }

  NSInteger sibling = sibling_bridge->ns_window().windowNumber;
  [window_ orderWindowByShuffling:NSWindowAbove relativeTo:sibling];
}

void NativeWidgetNSWindowBridge::StackAtTop() {
  [window_ orderWindowByShuffling:NSWindowAbove relativeTo:0];
}

void NativeWidgetNSWindowBridge::ShowEmojiPanel() {
  ui::ShowEmojiPanel();
}

void NativeWidgetNSWindowBridge::CreateWindow(
    mojom::CreateWindowParamsPtr params) {
  SetWindow(CreateNSWindow(params.get()));
}

void NativeWidgetNSWindowBridge::InitWindow(
    mojom::NativeWidgetNSWindowInitParamsPtr params) {
  modal_type_ = params->modal_type;
  is_translucent_window_ = params->is_translucent;
  pending_restoration_data_ = params->state_restoration_data;

  if (params->is_headless_mode_window)
    headless_mode_window_ = std::make_optional<HeadlessModeWindow>();

  [window_ setIsHeadless:params->is_headless_mode_window];

  // Register for application hide notifications so that visibility can be
  // properly tracked. This is not done in the delegate so that the lifetime is
  // tied to the C++ object, rather than the delegate (which may be reference
  // counted). This is required since the application hides do not send an
  // orderOut: to individual windows. Unhide, however, does send an order
  // message.
  [[NSNotificationCenter defaultCenter]
      addObserver:window_delegate_
         selector:@selector(onWindowOrderChanged:)
             name:NSApplicationDidHideNotification
           object:nil];

  [[NSNotificationCenter defaultCenter]
      addObserver:window_delegate_
         selector:@selector(onSystemColorsChanged:)
             name:NSSystemColorsDidChangeNotification
           object:nil];

  // Validate the window's initial state, otherwise the bridge's initial
  // tracking state will be incorrect.
  DCHECK(![window_ isVisible]);
  DCHECK_EQ(0u, [window_ styleMask] & NSWindowStyleMaskFullScreen);

  // Include "regular" windows without the standard frame in the window cycle.
  // These use NSWindowStyleMaskBorderless so do not get it by default.
  if (params->force_into_collection_cycle) {
    [window_
        setCollectionBehavior:[window_ collectionBehavior] |
                              NSWindowCollectionBehaviorParticipatesInCycle];
  }

  [window_ setHasShadow:params->has_window_server_shadow];

  // Don't allow dragging sheets.
  if (params->modal_type == ui::mojom::ModalType::kWindow) {
    [window_ setMovable:NO];
  }
  [window_ setIsTooltip:params->is_tooltip];
}

void NativeWidgetNSWindowBridge::SetInitialBounds(
    const gfx::Rect& new_bounds,
    const gfx::Size& minimum_content_size) {
  gfx::Rect adjusted_bounds = new_bounds;
  if (new_bounds.IsEmpty()) {
    // If a position is set, but no size, complain. Otherwise, a 1x1 window
    // would appear there, which might be unexpected.
    DCHECK(new_bounds.origin().IsOrigin())
        << "Zero-sized windows not supported on Mac.";

    // Otherwise, bounds is all zeroes. Cocoa will currently have the window at
    // the bottom left of the screen. To support a client calling SetSize() only
    // (and for consistency across platforms) put it at the top-left instead.
    // Read back the current frame: it will be a 1x1 context rect but the frame
    // size also depends on the window style.
    NSRect frame_rect = [window_ frame];
    adjusted_bounds = gfx::Rect(
        gfx::Point(), gfx::Size(NSWidth(frame_rect), NSHeight(frame_rect)));
  }
  SetBounds(adjusted_bounds, minimum_content_size, std::nullopt);
}

void NativeWidgetNSWindowBridge::SetBounds(
    const gfx::Rect& new_bounds,
    const gfx::Size& minimum_content_size,
    const std::optional<gfx::Size>& maximum_content_size) {
  // -[NSWindow contentMinSize] and [NSWindow contentMaxSize] are only checked
  // by Cocoa for user-initiated resizes. This is not what toolkit-views
  // expects, so clamp.
  gfx::Size clamped_content_size =
      GetClientSizeForWindowSize(window_, new_bounds.size());
  clamped_content_size.SetToMax(minimum_content_size);

  if (maximum_content_size.has_value()) {
    clamped_content_size.SetToMin(*maximum_content_size);
  }

  // A contentRect with zero width or height is a banned practice in ChromeMac,
  // due to unpredictable macOS treatment.
  DCHECK(!clamped_content_size.IsEmpty())
      << "Zero-sized windows not supported on Mac";

  if (!window_visible_ && IsWindowModalSheet()) {
    // Window-Modal dialogs (i.e. sheets) are positioned by Cocoa when shown for
    // the first time. They also have no frame, so just update the content size.
    [window_ setContentSize:NSMakeSize(clamped_content_size.width(),
                                       clamped_content_size.height())];
    return;
  }
  gfx::Rect actual_new_bounds(
      new_bounds.origin(),
      GetWindowSizeForClientSize(window_, clamped_content_size));

  NSScreen* previous_screen = [window_ screen];

  [window_ setFrame:gfx::ScreenRectToNSRect(actual_new_bounds)
            display:YES
            animate:NO];

  // If the window has focus but is not on the active space and the window was
  // moved to a different display, re-activate it to switch the space to the
  // active window. (crbug.com/1316543)
  if ([window_ isKeyWindow] && ![window_ isOnActiveSpace] &&
      [window_ screen] != previous_screen) {
    SetVisibilityState(WindowVisibilityState::kShowAndActivateWindow);
  }
}

void NativeWidgetNSWindowBridge::SetSize(
    const gfx::Size& new_size,
    const gfx::Size& minimum_content_size) {
  // Ensure the top-left corner stays in-place (rather than the bottom-left,
  // which -[NSWindow setContentSize:] would do).
  gfx::Rect new_window_bounds = gfx::ScreenRectFromNSRect([window_ frame]);
  new_window_bounds.set_size(new_size);
  SetBounds(new_window_bounds, minimum_content_size, std::nullopt);
}

void NativeWidgetNSWindowBridge::SetSizeAndCenter(
    const gfx::Size& content_size,
    const gfx::Size& minimum_content_size) {
  gfx::Rect new_window_bounds = gfx::ScreenRectFromNSRect([window_ frame]);
  new_window_bounds.set_size(GetWindowSizeForClientSize(window_, content_size));
  SetBounds(new_window_bounds, minimum_content_size, std::nullopt);

  // Note that this is not the precise center of screen, but it is the standard
  // location for windows like dialogs to appear on screen for Mac.
  // TODO(tapted): If there is a parent window, center in that instead.
  [window_ center];
}

void NativeWidgetNSWindowBridge::DestroyContentView() {
  if (!bridged_view_)
    return;
  [bridged_view_ clearView];
  bridged_view_id_mapping_.reset();
  bridged_view_ = nil;
  [window_ setContentView:nil];
}

void NativeWidgetNSWindowBridge::CreateContentView(uint64_t ns_view_id,
                                                   const gfx::Rect& bounds) {
  DCHECK(!bridged_view_);

  bridged_view_ = [[BridgedContentView alloc] initWithBridge:this
                                                      bounds:bounds];
  bridged_view_id_mapping_ =
      std::make_unique<ScopedNSViewIdMapping>(ns_view_id, bridged_view_);

  // Objective C initializers can return nil. However, if |view| is non-NULL
  // this should be treated as an error and caught early.
  CHECK(bridged_view_);

  // Send the accessibility tokens for the NSView now that it exists.
  host_->SetRemoteAccessibilityTokens(
      ui::RemoteAccessibility::GetTokenForLocalElement(window_),
      ui::RemoteAccessibility::GetTokenForLocalElement(bridged_view_));

  // Beware: This view was briefly removed (in favor of a bare CALayer) in
  // https://crrev.com/c/1236675. The ordering of unassociated layers relative
  // to NSView layers is undefined on macOS 10.12 and earlier, so the compositor
  // layer ended up covering up subviews (see https://crbug.com/899499).
  NSView* compositor_view =
      [[ViewsCompositorSuperview alloc] initWithFrame:[bridged_view_ bounds]];
  [compositor_view
      setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
  auto* background_layer = [CALayer layer];
  display_ca_layer_tree_ =
      std::make_unique<ui::DisplayCALayerTree>(background_layer);
  [compositor_view setLayer:background_layer];
  [compositor_view setWantsLayer:YES];
  [bridged_view_ addSubview:compositor_view];

  [bridged_view_ setWantsLayer:YES];
  [window_ setContentView:bridged_view_];
}

void NativeWidgetNSWindowBridge::CloseWindow() {
  if (fullscreen_controller_.HasDeferredWindowClose())
    return;

  // Make a local variable of the window on the stack so that the block can
  // capture a reference to it.
  NSWindow* window = ns_window();

  if (IsWindowModalSheet() && window.sheet) {
    // Sheets can't be closed normally. This starts the sheet closing. Once the
    // sheet has finished animating, it will call the end-sheet block defined
    // when the sheet was displayed. Note it still needs to be asynchronous,
    // since code calling Widget::Close() doesn't expect things to be deleted
    // upon return. Ensure |window| is retained by a block. Note in some cases
    // during teardown, [window sheetParent] may be nil.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(^{
          [NSApp endSheet:window];
        }));
    return;
  }

  // For other modal types, animate the close.
  if (ShouldRunCustomAnimationFor(VisibilityTransition::kHide) &&
      [ns_window() isVisible]) {
    [ViewsNSWindowCloseAnimator closeWindowWithAnimation:window];
    return;
  }

  // Destroy the content view so that it won't call back into |host_| while
  // being torn down.
  DestroyContentView();

  // If the window wants to be visible and has a parent, then the parent may
  // order it back in (in the period between orderOut: and close).
  wants_to_be_visible_ = false;

  // Widget::Close() ensures [Non]ClientView::CanClose() returns true, so there
  // is no need to call the NSWindow or its delegate's -windowShouldClose:
  // implementation in the manner of -[NSWindow performClose:]. But,
  // like -performClose:, first remove the window from AppKit's display
  // list to avoid crashes like http://crbug.com/156101.
  [window orderOut:nil];

  // Defer closing windows until after fullscreen transitions complete.
  fullscreen_controller_.OnWindowWantsToClose();
  if (fullscreen_controller_.HasDeferredWindowClose())
    return;

  // Many tests assume that base::RunLoop().RunUntilIdle() is always sufficient
  // to execute a close. However, in rare cases, -performSelector:..afterDelay:0
  // does not do this. So post a regular task.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
                                                              base::BindOnce(^{
                                                                [window close];
                                                              }));
}

void NativeWidgetNSWindowBridge::CloseWindowNow() {
  // NSWindows must be retained until -[NSWindow close] returns.
  NSWindow* __attribute__((objc_precise_lifetime)) window_retain = window_;

  // If there's a bridge at this point, it means there must be a window as well.
  DCHECK(window_);
  [window_ close];
  // Note: |this| will be deleted here.
}

void NativeWidgetNSWindowBridge::SetVisibilityState(
    WindowVisibilityState new_state) {
  // In headless mode the platform window is always hidden, so instead of
  // changing its visibility state just maintain a local flag to track the
  // expected visibility state and lie to the upper layer pretending the
  // window did change its visibility and activation state.
  if (headless_mode_window_) {
    headless_mode_window_->visibility_state =
        new_state != WindowVisibilityState::kHideWindow;
    host_->OnVisibilityChanged(headless_mode_window_->visibility_state);
    if (new_state == WindowVisibilityState::kShowAndActivateWindow) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(
              [](const base::WeakPtr<NativeWidgetNSWindowBridge>& bridge) {
                if (bridge) {
                  bridge->OnWindowKeyStatusChangedTo(/*is_key=*/true);
                }
              },
              factory_.GetWeakPtr()));
    }
    return;
  }

  // During session restore this method gets called from RestoreTabsToBrowser()
  // with new_state = kShowAndActivateWindow. We consume restoration data on our
  // first time through this method so we can use its existence as an
  // indication that session restoration is underway. We'll use this later to
  // decide whether or not to actually honor the WindowVisibilityState change
  // request. This window may live in the dock, for example, in which case we
  // don't really want to kShowAndActivateWindow. Even if the window is on the
  // desktop we still won't want to kShowAndActivateWindow because doing so
  // might trigger a transition to a different space (and we don't want to
  // switch spaces on start-up). When session restore determines the Active
  // window it will also call SetVisibilityState(), on that pass the window
  // can/will be activated.
  bool session_restore_in_progress = false;

  // Restore Cocoa window state.
  if (HasWindowRestorationData()) {
    NSData* restore_ns_data =
        [NSData dataWithBytes:pending_restoration_data_.data()
                       length:pending_restoration_data_.size()];
    NSKeyedUnarchiver* decoder =
        [[NSKeyedUnarchiver alloc] initForReadingFromData:restore_ns_data
                                                    error:nil];
    [window_ restoreStateWithCoder:decoder];
    pending_restoration_data_.clear();

    session_restore_in_progress = true;
  }

  // Ensure that:
  //  - A window with an invisible parent is not made visible.
  //  - A parent changing visibility updates child window visibility.
  //    * But only when changed via this function - ignore changes via the
  //      NSWindow API, or changes propagating out from here.
  wants_to_be_visible_ = new_state != WindowVisibilityState::kHideWindow &&
                         new_state != WindowVisibilityState::kMiniaturizeWindow;

  [show_animation_ stopAnimation];  // If set, calls OnShowAnimationComplete().
  CHECK(!show_animation_);

  if (new_state == WindowVisibilityState::kHideWindow) {
    // Calling -orderOut: on a window with an attached sheet encounters broken
    // AppKit behavior. The sheet effectively becomes "lost".
    // See http://crbug.com/667602. Alternatives: call -setAlphaValue:0 and
    // -setIgnoresMouseEvents:YES on the NSWindow, or dismiss the sheet before
    // hiding.
    //
    // TODO(ellyjones): Sort this entire situation out. This DCHECK doesn't
    // trigger in shipped builds, but it does trigger when the browser exits
    // "abnormally" (not via one of the UI paths to exiting), such as in browser
    // tests, so this breaks a slew of browser tests in MacViews mode. See also
    // https://crbug.com/834926.
    // DCHECK(![window_ attachedSheet]);

    [window_ orderOut:nil];
    DCHECK(!window_visible_);
    return;
  } else if (new_state == WindowVisibilityState::kMiniaturizeWindow) {
    [window_ miniaturize:nil];
    return;
  }

  DCHECK(wants_to_be_visible_);

  if (!ca_transaction_sync_suppressed_)
    ui::CATransactionCoordinator::Get().Synchronize();

  // If the parent (or an ancestor) is hidden, return and wait for it to become
  // visible.
  for (auto* ancestor = parent_.get(); ancestor; ancestor = ancestor->parent_) {
    if (!ancestor->window_visible_)
      return;
  }

  // Don't activate a window during session restore, to avoid switching spaces
  // (or pulling it out of the dock) during startup.
  if (session_restore_in_progress &&
      new_state == WindowVisibilityState::kShowAndActivateWindow) {
    new_state = WindowVisibilityState::kShowInactive;
  }

  if (IsWindowModalSheet()) {
    ShowAsModalSheet();
    return;
  }

  if (parent_)
    parent_->OrderChildren();

  if (new_state == WindowVisibilityState::kShowAndActivateWindow) {
    [window_ makeKeyAndOrderFront:nil];
    [NSApp activateIgnoringOtherApps:YES];
  } else if (new_state == WindowVisibilityState::kShowInactive && !parent_ &&
             ![window_ isMiniaturized]) {
    if ([[NSApp mainWindow] screen] == [window_ screen] ||
        ![[NSApp mainWindow] isKeyWindow]) {
      // When the new window is on the same display as the main window or the
      // main window is inactive, order the window relative to the main window.
      // Avoid making it the front window (with e.g. orderFront:), which can
      // cause a space switch.
      [window_ orderWindow:NSWindowBelow
                relativeTo:NSApp.mainWindow.windowNumber];
    } else {
      // When opening an inactive window on another screen, put the window at
      // the front and trigger a space switch.
      [window_ orderFrontKeepWindowKeyState];
    }
  }

  // For non-sheet modal types, use the constrained window animations to make
  // the window appear.
  if (ShouldRunCustomAnimationFor(VisibilityTransition::kShow)) {
    show_animation_ =
        [[ModalShowAnimationWithLayer alloc] initWithBridgedNativeWidget:this];

    // The default mode is blocking, which would block the UI thread for the
    // duration of the animation, but would keep it smooth. The window also
    // hasn't yet received a frame from the compositor at this stage, so it is
    // fully transparent until the GPU sends a frame swap IPC. For the blocking
    // option, the animation needs to wait until
    // AcceleratedWidgetCALayerParamsUpdated has been called at least once,
    // otherwise it will animate nothing.
    [show_animation_ setAnimationBlockingMode:NSAnimationNonblocking];
    [show_animation_ startAnimation];
  }
}

void NativeWidgetNSWindowBridge::SetTransitionsToAnimate(
    VisibilityTransition transitions) {
  // TODO(tapted): Use scoping to disable native animations at appropriate
  // times as well.
  transitions_to_animate_ = transitions;
}

void NativeWidgetNSWindowBridge::AcquireCapture() {
  if (HasCapture())
    return;
  if (!window_visible_)
    return;  // Capture on hidden windows is disallowed.

  mouse_capture_ = std::make_unique<CocoaMouseCapture>(this);
  host_->OnMouseCaptureActiveChanged(true);

  // Initiating global event capture with addGlobalMonitorForEventsMatchingMask:
  // will reset the mouse cursor to an arrow. Asking the window for an update
  // here will restore what we want. However, it can sometimes cause the cursor
  // to flicker, once, on the initial mouseDown.
  // TODO(tapted): Make this unnecessary by only asking for global mouse capture
  // for the cases that need it (e.g. menus, but not drag and drop).
  [window_ cursorUpdate:[NSApp currentEvent]];
}

void NativeWidgetNSWindowBridge::ReleaseCapture() {
  mouse_capture_.reset();
}

bool NativeWidgetNSWindowBridge::HasCapture() {
  return mouse_capture_ && mouse_capture_->IsActive();
}

void NativeWidgetNSWindowBridge::SetLocalEventMonitorEnabled(bool enabled) {
  if (enabled) {
    // Create the event monitor if it does not exist yet.
    if (key_down_event_monitor_) {
      return;
    }

    base::WeakPtr<NativeWidgetNSWindowBridge> weak_ptr = factory_.GetWeakPtr();

    auto block = ^NSEvent*(NSEvent* event) {
      if (!weak_ptr) {
        return event;
      }

      std::unique_ptr<ui::Event> ui_event =
          ui::EventFromNative(base::apple::OwnedNSEvent(event));
      bool event_handled = false;
      weak_ptr->host_->DispatchMonitorEvent(std::move(ui_event),
                                            &event_handled);
      return event_handled ? nil : event;
    };
    key_down_event_monitor_ =
        [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown
                                              handler:block];
  } else {
    // Destroy the event monitor if it exists.
    if (!key_down_event_monitor_)
      return;

    [NSEvent removeMonitor:key_down_event_monitor_];
    key_down_event_monitor_ = nil;
  }
}

bool NativeWidgetNSWindowBridge::HasWindowRestorationData() {
  return !pending_restoration_data_.empty();
}

bool NativeWidgetNSWindowBridge::RunMoveLoop(const gfx::Vector2d& drag_offset) {
  // https://crbug.com/876493
  CHECK(!HasCapture());
  // Does some *other* widget have capture?
  CHECK(!CocoaMouseCapture::GetGlobalCaptureWindow());
  CHECK(!window_move_loop_);

  // RunMoveLoop caller is responsible for updating the window to be under the
  // mouse, but it does this using possibly outdated coordinate from the mouse
  // event, and mouse is very likely moved beyond that point.

  // Compensate for mouse drift by shifting the initial mouse position we pass
  // to CocoaWindowMoveLoop, so as it handles incoming move events the window's
  // top left corner will be |drag_offset| from the current mouse position.

  const gfx::Rect frame = gfx::ScreenRectFromNSRect([window_ frame]);
  const gfx::Point mouse_in_screen(frame.x() + drag_offset.x(),
                                   frame.y() + drag_offset.y());
  window_move_loop_ = std::make_unique<CocoaWindowMoveLoop>(
      this, gfx::ScreenPointToNSPoint(mouse_in_screen));

  return window_move_loop_->Run();

  // |this| may be destroyed during the RunLoop, causing it to exit early.
  // Even if that doesn't happen, CocoaWindowMoveLoop will clean itself up by
  // calling EndMoveLoop(). So window_move_loop_ will always be null before the
  // function returns. But don't DCHECK since |this| might not be valid.
}

void NativeWidgetNSWindowBridge::EndMoveLoop() {
  DCHECK(window_move_loop_);
  window_move_loop_->End();
  window_move_loop_.reset();
}

void NativeWidgetNSWindowBridge::SetCursor(NSCursor* cursor) {
  [window_delegate_ setCursor:cursor];
}

void NativeWidgetNSWindowBridge::SetCursor(const ui::Cursor& cursor) {
  SetCursor(ui::GetNativeCursor(cursor));
}

void NativeWidgetNSWindowBridge::EnableImmersiveFullscreen(
    uint64_t fullscreen_overlay_widget_id,
    uint64_t tab_widget_id) {
  NativeWidgetNSWindowBridge* tab_widget_bridge = GetFromId(tab_widget_id);
  if (tab_widget_bridge) {
    NSWindow* tab_window = tab_widget_bridge->ns_window();
    immersive_mode_controller_ =
        std::make_unique<ImmersiveModeTabbedControllerCocoa>(
            ns_window(), GetFromId(fullscreen_overlay_widget_id)->ns_window(),
            tab_window);
  } else {
    immersive_mode_controller_ = std::make_unique<ImmersiveModeControllerCocoa>(
        ns_window(), GetFromId(fullscreen_overlay_widget_id)->ns_window());
  }
  immersive_mode_controller_->Init();

  // It is possible for the fullscreen transition to complete before the
  // immersive mode controller is created. Mark the transition as complete as
  // needed here.
  if (!fullscreen_controller_.IsInFullscreenTransition() &&
      fullscreen_controller_.GetTargetFullscreenState()) {
    immersive_mode_controller_->FullscreenTransitionCompleted();
  }

  // Reveal locks can outlive immersive_mode_controller_, re-establish any
  // outstanding locks.
  for (int i = 0; i < immersive_fullscreen_reveal_lock_count_; ++i) {
    immersive_mode_controller_->RevealLock();
  }
}

void NativeWidgetNSWindowBridge::DisableImmersiveFullscreen() {
  immersive_mode_controller_.reset();
}

void NativeWidgetNSWindowBridge::UpdateToolbarVisibility(
    remote_cocoa::mojom::ToolbarVisibilityStyle style) {
  if (immersive_mode_controller_) {
    immersive_mode_controller_->UpdateToolbarVisibility(style);
  }
}

void NativeWidgetNSWindowBridge::OnTopContainerViewBoundsChanged(
    const gfx::Rect& bounds) {
  if (immersive_mode_controller_) {
    immersive_mode_controller_->OnTopViewBoundsChanged(bounds);
  }
}

void NativeWidgetNSWindowBridge::ImmersiveFullscreenRevealLock() {
  ++immersive_fullscreen_reveal_lock_count_;
  if (immersive_mode_controller_) {
    immersive_mode_controller_->RevealLock();
  }
}

void NativeWidgetNSWindowBridge::ImmersiveFullscreenRevealUnlock() {
  --immersive_fullscreen_reveal_lock_count_;
  DCHECK(immersive_fullscreen_reveal_lock_count_ >= 0);
  if (immersive_mode_controller_) {
    immersive_mode_controller_->RevealUnlock();
  }
}

bool NativeWidgetNSWindowBridge::ShouldUseCustomTitlebarHeightForFullscreen()
    const {
  return immersive_mode_controller_ &&
         immersive_mode_controller_->is_initialized() &&
         immersive_mode_controller_->IsTabbed() &&
         !immersive_mode_controller_->IsContentFullscreen();
}

void NativeWidgetNSWindowBridge::OnImmersiveFullscreenToolbarRevealChanged(
    bool is_revealed) {
  host_->OnImmersiveFullscreenToolbarRevealChanged(is_revealed);
}

void NativeWidgetNSWindowBridge::OnImmersiveFullscreenMenuBarRevealChanged(
    float reveal_amount) {
  host_->OnImmersiveFullscreenMenuBarRevealChanged(reveal_amount);
}

void NativeWidgetNSWindowBridge::OnAutohidingMenuBarHeightChanged(
    int menu_bar_height) {
  host_->OnAutohidingMenuBarHeightChanged(menu_bar_height);
}

void NativeWidgetNSWindowBridge::SetCanGoBack(bool can_go_back) {
  can_go_back_ = can_go_back;
}

void NativeWidgetNSWindowBridge::SetCanGoForward(bool can_go_forward) {
  can_go_forward_ = can_go_forward;
}

void NativeWidgetNSWindowBridge::DisplayContextMenu(
    mojom::ContextMenuPtr menu,
    mojo::PendingRemote<mojom::MenuHost> host,
    mojo::PendingReceiver<mojom::Menu> receiver) {
  ContextMenuRunner runner(std::move(host), std::move(receiver));
  NSView* target_view = GetNSViewFromId(menu->target_view_id);
  runner.ShowMenu(std::move(menu), GetWindow(), target_view);
}

void NativeWidgetNSWindowBridge::SetAllowScreenshots(bool allow) {
  [ns_window()
      setSharingType:allow ? NSWindowSharingReadOnly : NSWindowSharingNone];
}

void NativeWidgetNSWindowBridge::OnWindowWillClose() {
  fullscreen_controller_.OnWindowWillClose();
  // Immersive full screen needs to be disabled synchronously when the window
  // is closing. So disable it right away, rather than waiting for the browser
  // process to signal us to disable immersive fullscreen after being informed
  // of the window closing.
  DisableImmersiveFullscreen();

  [window_ setCommandHandler:nil];
  [window_ setCommandDispatcherDelegate:nil];

  ui::CATransactionCoordinator::Get().RemovePreCommitObserver(this);
  host_->OnWindowWillClose();

  // Ensure NativeWidgetNSWindowBridge does not have capture, otherwise
  // OnMouseCaptureLost() may reference a deleted |host_| when called via
  // ~CocoaMouseCapture() upon the destruction of |mouse_capture_|. See
  // https://crbug.com/622201. Also we do this before setting the delegate to
  // nil, because this may lead to callbacks to bridge which rely on a valid
  // delegate.
  ReleaseCapture();

  if (parent_) {
    parent_->RemoveChildWindow(this);
    parent_ = nullptr;
  }
  [[NSNotificationCenter defaultCenter] removeObserver:window_delegate_];

  [show_animation_ stopAnimation];  // If set, calls OnShowAnimationComplete().
  CHECK(!show_animation_);

  [window_ setDelegate:nil];
  [window_ setBridge:nullptr];

  // Ensure that |this| cannot be reached by its id while it is being destroyed.
  size_t erased = GetIdToWidgetImplMap().erase(id_);
  DCHECK_EQ(1u, erased);

  RemoveOrDestroyChildren();
  DCHECK(child_windows_.empty());

  host_->OnWindowHasClosed();
  // Note: |this| and its host will be deleted here.
}

void NativeWidgetNSWindowBridge::OnSizeChanged() {
  UpdateWindowGeometry();
}

void NativeWidgetNSWindowBridge::OnPositionChanged() {
  UpdateWindowGeometry();
}

void NativeWidgetNSWindowBridge::OnVisibilityChanged() {
  const bool window_visible = [window_ isVisible];
  if (window_visible_ == window_visible)
    return;

  window_visible_ = window_visible;

  // If arriving via SetVisible(), |wants_to_be_visible_| should already be set.
  // If made visible externally (e.g. Cmd+H), just roll with it. Don't try (yet)
  // to distinguish being *hidden* externally from being hidden by a parent
  // window - we might not need that.
  if (window_visible_) {
    wants_to_be_visible_ = true;
    if (parent_ && !window_visible_)
      parent_->OrderChildren();
  } else {
    ReleaseCapture();  // Capture on hidden windows is not permitted.

    // When becoming invisible, remove the entry in any parent's childWindow
    // list. Cocoa's childWindow management breaks down when child windows are
    // hidden.
    if (parent_)
      [parent_->ns_window() removeChildWindow:window_];
  }

  // Showing a translucent window after hiding it should trigger shadow
  // invalidation.
  if (window_visible && ![window_ isOpaque])
    invalidate_shadow_on_frame_swap_ = true;

  NotifyVisibilityChangeDown();
  host_->OnVisibilityChanged(window_visible_);
}

void NativeWidgetNSWindowBridge::OnSystemColorsChanged() {
  host_->OnWindowNativeThemeChanged();
}

void NativeWidgetNSWindowBridge::OnScreenOrBackingPropertiesChanged() {
  UpdateWindowDisplay();
}

void NativeWidgetNSWindowBridge::OnWindowKeyStatusChangedTo(bool is_key) {
  host_->OnWindowKeyStatusChanged(
      is_key, [window_ contentView] == [window_ firstResponder],
      [NSApp isFullKeyboardAccessEnabled]);
  // If the window just became key, this is a good chance to add child windows
  // from when the window wasn't on the current space.
  if (is_key)
    OrderChildren();
}

void NativeWidgetNSWindowBridge::SetSizeConstraints(const gfx::Size& min_size,
                                                    const gfx::Size& max_size,
                                                    bool is_resizable,
                                                    bool is_maximizable) {
  // Don't modify the size constraints or fullscreen collection behavior while
  // in fullscreen or during a transition.
  if (!fullscreen_controller_.CanResize())
    return;

  bool shows_resize_controls =
      is_resizable && (min_size.IsEmpty() || min_size != max_size);
  bool shows_fullscreen_controls = is_resizable && is_maximizable;

  gfx::ApplyNSWindowSizeConstraints(window_, min_size, max_size,
                                    shows_resize_controls,
                                    shows_fullscreen_controls);
}

void NativeWidgetNSWindowBridge::OnShowAnimationComplete() {
  show_animation_ = nil;
}

void NativeWidgetNSWindowBridge::InitCompositorView(
    InitCompositorViewCallback callback) {
  // Use the regular window background for window modal sheets. The layer will
  // still paint over most of it, but the native -[NSApp beginSheet:] animation
  // blocks the UI thread, so there's no way to invalidate the shadow to match
  // the composited layer. This assumes the native window shape is a good match
  // for the composited NonClientFrameView, which should be the case since the
  // native shape is what's most appropriate for displaying sheets on Mac.
  if (is_translucent_window_ && !IsWindowModalSheet()) {
    [window_ setOpaque:NO];
    [window_ setBackgroundColor:[NSColor clearColor]];

    // Don't block waiting for the initial frame of completely transparent
    // windows. This allows us to avoid blocking on the UI thread e.g, while
    // typing in the omnibox. Note window modal sheets _must_ wait: there is no
    // way for a frame to arrive during AppKit's sheet animation.
    // https://crbug.com/712268
    ca_transaction_sync_suppressed_ = true;
  } else {
    DCHECK(!ca_transaction_sync_suppressed_);
  }

  // Send the initial window geometry and screen properties. Any future changes
  // will be forwarded.
  UpdateWindowDisplay();
  UpdateWindowGeometry();

  // Inform the browser of the CGWindowID for this NSWindow.
  std::move(callback).Run([window_ windowNumber]);
}

void NativeWidgetNSWindowBridge::SortSubviews(
    const std::vector<uint64_t>& attached_subview_ids) {
  // Ignore layer manipulation during a Close(). This can be reached during the
  // orderOut: in Close(), which notifies visibility changes to Views.
  if (!bridged_view_)
    return;
  RankMap rank;
  for (uint64_t subview_id : attached_subview_ids) {
    if (NSView* subview = remote_cocoa::GetNSViewFromId(subview_id))
      rank[subview] = rank.size() + 1;
  }
  [bridged_view_ sortSubviewsUsingFunction:&SubviewSorter context:&rank];
}

void NativeWidgetNSWindowBridge::SetAnimationEnabled(bool animate) {
  [window_
      setAnimationBehavior:(animate ? NSWindowAnimationBehaviorDocumentWindow
                                    : NSWindowAnimationBehaviorNone)];
}

bool NativeWidgetNSWindowBridge::ShouldRunCustomAnimationFor(
    VisibilityTransition transition) const {
  // The logic around this needs to change if new transition types are set.
  // E.g. it would be nice to distinguish "hide" from "close". Mac currently
  // treats "hide" only as "close". Hide (e.g. Cmd+h) should not animate on Mac.
  if (transitions_to_animate_ != transition &&
      transitions_to_animate_ != VisibilityTransition::kBoth) {
    return false;
  }

  // Custom animations are only used for tab-modals.
  bool widget_is_modal = false;
  host_->GetWidgetIsModal(&widget_is_modal);
  if (!widget_is_modal)
    return false;

  // Note this also checks the native animation property. Clearing that will
  // also disable custom animations to ensure that the views::Widget API
  // behaves consistently.
  if ([window_ animationBehavior] == NSWindowAnimationBehaviorNone)
    return false;

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDisableModalAnimations)) {
    return false;
  }

  return true;
}

bool NativeWidgetNSWindowBridge::RedispatchKeyEvent(NSEvent* event) {
  return [[window_ commandDispatcher] redispatchKeyEvent:event];
}

NSWindow* NativeWidgetNSWindowBridge::ns_window() {
  return window_;
}

DragDropClient* NativeWidgetNSWindowBridge::drag_drop_client() {
  return host_helper_->GetDragDropClient();
}

////////////////////////////////////////////////////////////////////////////////
// NativeWidgetNSWindowBridge, display::DisplayObserver:

void NativeWidgetNSWindowBridge::OnDisplayAdded(
    const display::Display& display) {
  UpdateWindowDisplay();
}

void NativeWidgetNSWindowBridge::OnDisplaysRemoved(
    const display::Displays& removed_displays) {
  UpdateWindowDisplay();
}

void NativeWidgetNSWindowBridge::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t metrics) {
  UpdateWindowDisplay();
}

////////////////////////////////////////////////////////////////////////////////
// NativeWidgetNSWindowBridge, NativeWidgetNSWindowFullscreenController::Client:

void NativeWidgetNSWindowBridge::FullscreenControllerTransitionStart(
    bool is_target_fullscreen) {
  host_->OnWindowFullscreenTransitionStart(is_target_fullscreen);
  if (!is_target_fullscreen) {
    // Immersive full screen needs to be disabled synchronously during the
    // fullscreen transition. So disable it right away, rather than waiting for
    // the browser process to signal us to disable immersive fullscreen after
    // being informed of the start of the transition.
    DisableImmersiveFullscreen();
  }
}

void NativeWidgetNSWindowBridge::FullscreenControllerTransitionComplete(
    bool is_fullscreen) {
  DCHECK(!fullscreen_controller_.IsInFullscreenTransition());
  UpdateWindowGeometry();
  UpdateWindowDisplay();

  // Add any children that were skipped during the fullscreen transition.
  OrderChildren();

  host_->OnWindowFullscreenTransitionComplete(is_fullscreen);
  if (is_fullscreen && immersive_mode_controller_) {
    immersive_mode_controller_->FullscreenTransitionCompleted();
  }
}

void NativeWidgetNSWindowBridge::FullscreenControllerSetFrame(
    const gfx::Rect& frame,
    bool animate,
    base::OnceCallback<void()> completion_callback) {
  NSRect ns_frame = gfx::ScreenRectToNSRect(frame);
  base::TimeDelta transition_time = base::Seconds(0);
  if (animate)
    transition_time = base::Seconds([window_ animationResizeTime:ns_frame]);

  __block base::OnceCallback<void()> complete = std::move(completion_callback);
  [NSAnimationContext
      runAnimationGroup:^(NSAnimationContext* context) {
        [context setDuration:transition_time.InSecondsF()];
        [[window_ animator] setFrame:ns_frame display:YES animate:animate];
      }
      completionHandler:^{
        std::move(complete).Run();
      }];
}

void NativeWidgetNSWindowBridge::FullscreenControllerToggleFullscreen() {
  // AppKit implicitly makes the fullscreen window visible, so avoid going
  // fullscreen in headless mode. Instead, toggle the expected fullscreen state
  // and fake the relevant callbacks for the fullscreen controller to
  // believe the fullscreen state was toggled.
  if (headless_mode_window_) {
    headless_mode_window_->fullscreen_state =
        !headless_mode_window_->fullscreen_state;
    if (headless_mode_window_->fullscreen_state) {
      fullscreen_controller_.OnWindowWillEnterFullscreen();
      fullscreen_controller_.OnWindowDidEnterFullscreen();
    } else {
      fullscreen_controller_.OnWindowWillExitFullscreen();
      fullscreen_controller_.OnWindowDidExitFullscreen();
    }
    return;
  }

  bool is_key_window = [window_ isKeyWindow];
  [window_ toggleFullScreen:nil];
  // Ensure the transitioning window maintains focus.
  // When a key window moves to a different space, AppKit will focus a
  // different window on the previously focused space to become key, which can
  // break cross-display fullscreen transitions by losing focus of the
  // transitioning window (crbug.com/1338659) or changing the z-order of
  // windows on the previous space. Making the window key here seems to
  // alleviate those apparent defects (crbug.com/1392542).
  if (is_key_window)
    [window_ makeKeyAndOrderFront:nil];
}

void NativeWidgetNSWindowBridge::FullscreenControllerCloseWindow() {
  [window_ close];
}

int64_t NativeWidgetNSWindowBridge::FullscreenControllerGetDisplayId() const {
  return GetDisplayForWindow(window_).id();
}

gfx::Rect NativeWidgetNSWindowBridge::FullscreenControllerGetFrameForDisplay(
    int64_t display_id) const {
  display::Display display;
  if (display::Screen::GetScreen()->GetDisplayWithDisplayId(display_id,
                                                            &display)) {
    // Use the current window size to avoid unexpected window resizes on
    // subsequent cross-screen window drag and drops; see crbug.com/1338664
    return gfx::Rect(display.work_area().origin(),
                     FullscreenControllerGetFrame().size());
  }
  return gfx::Rect();
}

gfx::Rect NativeWidgetNSWindowBridge::FullscreenControllerGetFrame() const {
  return gfx::ScreenRectFromNSRect([window_ frame]);
}

////////////////////////////////////////////////////////////////////////////////
// NativeWidgetNSWindowBridge, ui::CATransactionObserver

bool NativeWidgetNSWindowBridge::ShouldWaitInPreCommit() {
  if (!window_visible_)
    return false;
  if (ca_transaction_sync_suppressed_)
    return false;
  if (!bridged_view_)
    return false;
  if (content_dip_size_.IsEmpty())
    return false;
  // Suppress synchronous CA transactions during AppKit fullscreen transition
  // since there is no need for updates during such transition.
  // Re-layout and re-paint will be done after the transition. See
  // https://crbug.com/875707 for potential problems if we don't suppress.
  if (fullscreen_controller_.IsInFullscreenTransition())
    return false;
  return content_dip_size_ != compositor_frame_dip_size_;
}

base::TimeDelta NativeWidgetNSWindowBridge::PreCommitTimeout() {
  return kUIPaintTimeout;
}

////////////////////////////////////////////////////////////////////////////////
// NativeWidgetNSWindowBridge, CocoaMouseCaptureDelegate:

bool NativeWidgetNSWindowBridge::PostCapturedEvent(NSEvent* event) {
  [bridged_view_ processCapturedMouseEvent:event];
  return true;
}

void NativeWidgetNSWindowBridge::OnMouseCaptureLost() {
  host_->OnMouseCaptureActiveChanged(false);
}

NSWindow* NativeWidgetNSWindowBridge::GetWindow() const {
  return window_;
}

////////////////////////////////////////////////////////////////////////////////
// TODO(ccameron): Update class names to:
// NativeWidgetNSWindowBridge, NativeWidgetNSWindowBridge:

void NativeWidgetNSWindowBridge::SetVisibleOnAllSpaces(bool always_visible) {
  gfx::SetNSWindowVisibleOnAllWorkspaces(window_, always_visible);
}

void NativeWidgetNSWindowBridge::SetZoomed(bool zoomed) {
  const bool window_zoomed = [window_ isZoomed];
  if (window_zoomed == zoomed)
    return;
  [window_ performZoom:nil];
}

void NativeWidgetNSWindowBridge::EnterFullscreen(int64_t target_display_id) {
  // Going fullscreen implicitly makes the window visible. AppKit does this.
  // That is, -[NSWindow isVisible] is always true after a call to -[NSWindow
  // toggleFullScreen:]. Unfortunately, this change happens after AppKit calls
  // -[NSWindowDelegate windowWillEnterFullScreen:], and AppKit doesn't send
  // an orderWindow message. So intercepting the implicit change is hard.
  // Luckily, to trigger externally, the window typically needs to be visible
  // in the first place. So we can just ensure the window is visible here
  // instead of relying on AppKit to do it, and not worry that
  // OnVisibilityChanged() won't be called for externally triggered fullscreen
  // requests.
  if (!window_visible_)
    SetVisibilityState(WindowVisibilityState::kShowInactive);

  // Enable fullscreen collection behavior because:
  // 1: -[NSWindow toggleFullscreen:] would otherwise be ignored,
  // 2: the fullscreen button must be enabled so the user can leave
  // fullscreen. This will be reset when a transition out of fullscreen
  // completes.
  gfx::SetNSWindowCanFullscreen(window_, true);

  fullscreen_controller_.EnterFullscreen(target_display_id);
}

void NativeWidgetNSWindowBridge::ExitFullscreen() {
  fullscreen_controller_.ExitFullscreen();
}

// TODO(https://crbug.com/357082344): Do not set
// `NSWindowCollectionBehaviorPrimary` if the window does not already have this
// flag set by `SetCanAppearInExistingFullscreenSpaces(true)`
void NativeWidgetNSWindowBridge::SetCanAppearInExistingFullscreenSpaces(
    bool can_appear_in_existing_fullscreen_spaces) {
  NSWindowCollectionBehavior collectionBehavior = window_.collectionBehavior;
  if (can_appear_in_existing_fullscreen_spaces) {
    if (@available(macOS 13.0, *)) {
      collectionBehavior &= ~NSWindowCollectionBehaviorPrimary;
    }
    collectionBehavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
    collectionBehavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
  } else {
    if (@available(macOS 13.0, *)) {
      collectionBehavior |= NSWindowCollectionBehaviorPrimary;
    }
    collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
    collectionBehavior &= ~NSWindowCollectionBehaviorFullScreenAuxiliary;
  }
  window_.collectionBehavior = collectionBehavior;
}

void NativeWidgetNSWindowBridge::SetMiniaturized(bool miniaturized) {
  // In headless mode the platform window is always hidden and WebKit
  // will not deminiaturize hidden windows. So instead of changing the window
  // miniaturization state just lie to the upper layer pretending the window did
  // change its state. We don't need to keep track of the requested state here
  // because the host will do this.
  if (headless_mode_window_) {
    host_->OnWindowMiniaturizedChanged(miniaturized);
    return;
  }

  if (miniaturized) {
    // Calling performMiniaturize: will momentarily highlight the button, but
    // AppKit will reject it if there is no miniaturize button.
    if ([window_ styleMask] & NSWindowStyleMaskMiniaturizable)
      [window_ performMiniaturize:nil];
    else
      [window_ miniaturize:nil];
  } else {
    [window_ deminiaturize:nil];
  }
}

void NativeWidgetNSWindowBridge::SetOpacity(float opacity) {
  [window_ setAlphaValue:opacity];
}

void NativeWidgetNSWindowBridge::SetWindowLevel(int32_t level) {
  [window_ setLevel:level];
  [bridged_view_ updateCursorTrackingArea];

  // Windows that have a higher window level than NSNormalWindowLevel default to
  // NSWindowCollectionBehaviorTransient. Set the value explicitly here to match
  // normal windows.
  NSWindowCollectionBehavior behavior =
      [window_ collectionBehavior] | NSWindowCollectionBehaviorManaged;
  [window_ setCollectionBehavior:behavior];
}

void NativeWidgetNSWindowBridge::SetAspectRatio(
    const gfx::SizeF& aspect_ratio,
    const gfx::Size& excluded_margin) {
  DCHECK(!aspect_ratio.IsEmpty());
  [window_delegate_ setAspectRatio:aspect_ratio.width() / aspect_ratio.height()
                    excludedMargin:excluded_margin];
}

void NativeWidgetNSWindowBridge::SetCALayerParams(
    const gfx::CALayerParams& ca_layer_params) {
  // Ignore frames arriving "late" for an old size. A frame at the new size
  // should arrive soon.
  // TODO(danakj): We should avoid lossy conversions to integer DIPs.
  gfx::Size frame_dip_size = gfx::ToFlooredSize(gfx::ConvertSizeToDips(
      ca_layer_params.pixel_size, ca_layer_params.scale_factor));
  if (content_dip_size_ != frame_dip_size)
    return;
  compositor_frame_dip_size_ = frame_dip_size;

  // Update the DisplayCALayerTree with the most recent CALayerParams, to make
  // the content display on-screen.
  display_ca_layer_tree_->UpdateCALayerTree(ca_layer_params);

  if (ca_transaction_sync_suppressed_)
    ca_transaction_sync_suppressed_ = false;

  if (invalidate_shadow_on_frame_swap_) {
    invalidate_shadow_on_frame_swap_ = false;
    [window_ invalidateShadow];
  }
}

void NativeWidgetNSWindowBridge::SetIgnoresMouseEvents(
    bool ignores_mouse_events) {
  [window_ setIgnoresMouseEvents:ignores_mouse_events];
}

void NativeWidgetNSWindowBridge::MakeFirstResponder() {
  [window_ makeFirstResponder:bridged_view_];
}

void NativeWidgetNSWindowBridge::SetWindowTitle(const std::u16string& title) {
  // Delay setting the window title until after any menu tracking is complete.
  if (NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) {
    // Install one run loop trigger to handle all the pending titles.
    if (GetPendingWindowTitleMap().empty()) {
      CFRunLoopPerformBlock(
          [NSRunLoop.currentRunLoop getCFRunLoop], kCFRunLoopDefaultMode, ^{
            for (const auto& pending_title : GetPendingWindowTitleMap()) {
              pending_title.first.title =
                  base::SysUTF16ToNSString(pending_title.second);
            }

            GetPendingWindowTitleMap().clear();
          });
    }

    GetPendingWindowTitleMap()[window_] = title;
  } else {
    window_.title = base::SysUTF16ToNSString(title);

    // In case there is an unfired run loop trigger, erase any pending title so
    // that the new title now being set doesn't get smashed.
    GetPendingWindowTitleMap().erase(window_);
  }
}

void NativeWidgetNSWindowBridge::ClearTouchBar() {
  [bridged_view_ setTouchBar:nil];
}

void NativeWidgetNSWindowBridge::UpdateTooltip() {
  NSPoint nspoint = [window_ convertPointFromScreen:NSEvent.mouseLocation];
  // Note: flip in the view's frame, which matches the window's contentRect.
  gfx::Point point(nspoint.x, NSHeight([bridged_view_ frame]) - nspoint.y);
  [bridged_view_ updateTooltipIfRequiredAt:point];
}

bool NativeWidgetNSWindowBridge::NeedsUpdateWindows() {
  return [bridged_view_ needsUpdateWindows];
}

void NativeWidgetNSWindowBridge::RedispatchKeyEvent(
    const std::vector<uint8_t>& native_event_data) {
  RedispatchKeyEvent(ui::EventFromData(native_event_data));
}

////////////////////////////////////////////////////////////////////////////////
// NativeWidgetNSWindowBridge, former BridgedNativeWidgetOwner:

void NativeWidgetNSWindowBridge::RemoveChildWindow(
    NativeWidgetNSWindowBridge* child) {
  auto location = base::ranges::find(child_windows_, child);
  DCHECK(location != child_windows_.end());
  child_windows_.erase(location);

  // Note the child is sometimes removed already by AppKit. This depends on OS
  // version, and possibly some unpredictable reference counting. Removing it
  // here should be safe regardless.
  [window_ removeChildWindow:child->window_];
}

////////////////////////////////////////////////////////////////////////////////
// NativeWidgetNSWindowBridge, private:

void NativeWidgetNSWindowBridge::OrderChildren() {
  // Adding a child to a window that isn't visible on the active space will
  // switch to that space (https://crbug.com/783521, https://crbug.com/798792).
  // Bail here (and call OrderChildren() in a few places) to defer adding
  // children until the window is visible.
  NSWindow* window = window_;
  if (!window.isVisible || !window.isOnActiveSpace)
    return;
  for (auto* child : child_windows_) {
    if (!child->wants_to_be_visible())
      continue;
    NSWindow* child_window = child->window_;
    if (child->IsWindowModalSheet()) {
      if (!child->window_visible_)
        child->ShowAsModalSheet();
      // Sheets don't need a parentWindow set, and setting one causes graphical
      // glitches (http://crbug.com/605098).
    } else {
      if (child_window.parentWindow == window)
        continue;
      if (immersive_mode_controller_ &&
          immersive_mode_controller_->overlay_window() == child_window) {
        continue;
      }
      [window addChildWindow:child_window ordered:NSWindowAbove];
    }
  }
}

void NativeWidgetNSWindowBridge::RemoveOrDestroyChildren() {
  // TODO(tapted): Implement unowned child windows if required.
  while (!child_windows_.empty()) {
    // The NSWindow can only be destroyed after -[NSWindow close] is complete.
    // Retain the window, otherwise the reference count can reach zero when the
    // child calls back into RemoveChildWindow() via its OnWindowWillClose().
    NSWindow* __attribute__((objc_precise_lifetime)) child =
        child_windows_.back()->ns_window();
    [child close];
  }
}

void NativeWidgetNSWindowBridge::CheckAndNotifyZoomedStateChanged() {
  const bool window_zoomed = [window_ isZoomed];
  if (window_zoomed_ == window_zoomed)
    return;

  window_zoomed_ = window_zoomed;

  // Notify that the window's zoomed state has changed.
  host_->OnWindowZoomedChanged(window_zoomed_);
}

void NativeWidgetNSWindowBridge::NotifyVisibilityChangeDown() {
  // Child windows sometimes like to close themselves in response to visibility
  // changes. That's supported, but only with the asynchronous Widget::Close().
  // Perform a heuristic to detect child removal that would break these loops.
  const size_t child_count = child_windows_.size();
  if (!window_visible_) {
    for (NativeWidgetNSWindowBridge* child : child_windows_) {
      if (child->window_visible_) {
        [child->ns_window() orderOut:nil];
      }
      DCHECK(!child->window_visible_);
      CHECK_EQ(child_count, child_windows_.size());
    }
    // The orderOut calls above should result in a call to OnVisibilityChanged()
    // in each child. There, children will remove themselves from the NSWindow
    // childWindow list as well as propagate NotifyVisibilityChangeDown() calls
    // to any children of their own. However this is only true for windows
    // managed by the NativeWidgetNSWindowBridge i.e. windows which have
    // ViewsNSWindowDelegate as the delegate.
    DCHECK_EQ(0u, CountBridgedWindows([window_ childWindows]));
    return;
  }

  OrderChildren();
}

void NativeWidgetNSWindowBridge::UpdateWindowGeometry() {
  gfx::Rect window_in_screen = gfx::ScreenRectFromNSRect([window_ frame]);
  gfx::Rect content_in_screen = gfx::ScreenRectFromNSRect(
      [window_ contentRectForFrameRect:[window_ frame]]);
  bool content_resized = content_dip_size_ != content_in_screen.size();
  content_dip_size_ = content_in_screen.size();

  host_->OnWindowGeometryChanged(window_in_screen, content_in_screen);

  CheckAndNotifyZoomedStateChanged();

  if (content_resized && !ca_transaction_sync_suppressed_)
    ui::CATransactionCoordinator::Get().Synchronize();

  // For a translucent window, the shadow calculation needs to be carried out
  // after the frame from the compositor arrives.
  if (content_resized && ![window_ isOpaque])
    invalidate_shadow_on_frame_swap_ = true;
}

void NativeWidgetNSWindowBridge::UpdateWindowDisplay() {
  if (fullscreen_controller_.IsInFullscreenTransition())
    return;

  host_->OnWindowDisplayChanged(GetDisplayForWindow(window_));
}

bool NativeWidgetNSWindowBridge::IsWindowModalSheet() const {
  return parent_ && modal_type_ == ui::mojom::ModalType::kWindow;
}

void NativeWidgetNSWindowBridge::ShowAsModalSheet() {
  // -[NSWindow beginSheet:completionHandler:] will block the UI thread while
  // the animation runs. So that it doesn't animate a fully transparent window,
  // first wait for a frame. The first step is to pretend that the window is
  // already visible.
  window_visible_ = true;
  host_->OnVisibilityChanged(window_visible_);

  NSWindow* parent_window = parent_->ns_window();
  if (NativeWidgetMacNSWindow* parent_widget_window =
          base::apple::ObjCCast<NativeWidgetMacNSWindow>(parent_window)) {
    parent_window = [parent_widget_window preferredSheetParent];
  }
  DCHECK(parent_window);
  NSWindow* __weak weak_window = window_;

  // Don't show a sheet twice. If a sheet is shown twice but endSheet: only
  // once it will leave a dangling blank sheet. This happened when the browser
  // is restored from minimization.
  if (parent_window.attachedSheet == window_) {
    return;
  }

  auto begin_sheet_closure = base::BindOnce(^{
    [parent_window beginSheet:window_
            completionHandler:^(NSModalResponse return_code) {
              // This class, NativeWidgetNSWindowBridge, clears the window's
              // delegate as an indication of its death, in which case this
              // completion handler will no-op. This is necessary to handle
              // AppKit invoking this selector via a posted task. See
              // https://crbug.com/851376.
              NSWindow* window = weak_window;
              if (!window.delegate) {
                return;
              }
              // Make sure to mark ourselves as not wanting to be visible.
              // Otherwise if during the orderOut call our parent becomes the
              // key window, it would try to show us as a new modal sheet.
              wants_to_be_visible_ = false;
              [window orderOut:nil];
              OnWindowWillClose();
            }];
  });

  if (host_helper_->MustPostTaskToRunModalSheetAnimation()) {
    // This function is called via mojo when using remote cocoa. Inside the
    // nested run loop, we will wait for a message providing the correctly-sized
    // frame for the new sheet. This message will not be processed until we
    // return from handling this message, because it will coming on the same
    // pipe. Avoid the resulting hang by posting a task to show the modal
    // sheet (which will be executed on a fresh stack, which will not block
    // the message).
    // https://crbug.com/1234509
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(begin_sheet_closure));
  } else {
    std::move(begin_sheet_closure).Run();
  }
}

bool NativeWidgetNSWindowBridge::window_visible() const {
  // In headless mode the platform window is always hidden, so instead of
  // returning the actual platform window visibility state tracked by
  // OnVisibilityChanged() callback, return the expected visibility state
  // maintained by SetVisibilityState() call.
  return headless_mode_window_ ? headless_mode_window_->visibility_state
                               : window_visible_;
}

}  // namespace remote_cocoa