chromium/ui/views/controls/native/native_view_host_mac.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.

#include "ui/views/controls/native/native_view_host_mac.h"

#import <Cocoa/Cocoa.h>

#include "base/apple/foundation_util.h"
#import "ui/accessibility/platform/ax_platform_node_mac.h"
#include "ui/compositor/layer.h"
#import "ui/views/cocoa/native_widget_mac_ns_window_host.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/widget/widget.h"

namespace views {
namespace {

void EnsureNativeViewHasNoChildWidgets(NSView* native_view) {
  DCHECK(native_view);
  // Mac's NativeViewHost has no support for hosting its own child widgets.
  // This check is probably overly restrictive, since the Widget containing the
  // NativeViewHost _is_ allowed child Widgets. However, we don't know yet
  // whether those child Widgets need to be distinguished from Widgets that code
  // might want to associate with the hosted NSView instead.
  {
    Widget::Widgets child_widgets;
    Widget::GetAllChildWidgets(native_view, &child_widgets);
    CHECK_GE(1u, child_widgets.size());  // 1 (itself) or 0 if detached.
  }
}

// Searches for the first Widget with a kMovedContentNSView reference to
// `native_view`. If found, removes the reference and returns the Widget's
// NSWindow.
NSWindow* RemoveReferenceToMovedContentView(NSView* native_view) {
  for (NSWindow* window in NSApp.windows) {
    Widget* widget =
        views::Widget::GetWidgetForNativeWindow(gfx::NativeWindow(window));
    if (widget == nullptr) {
      continue;
    }

    NSView* moved_content_view =
        (__bridge NSView*)widget->GetNativeWindowProperty(
            views::NativeWidgetMacNSWindowHost::kMovedContentNSView);

    if (moved_content_view == native_view) {
      widget->SetNativeWindowProperty(
          views::NativeWidgetMacNSWindowHost::kMovedContentNSView, nullptr);
      return window;
    }
  }

  return nullptr;
}

}  // namespace

NativeViewHostMac::NativeViewHostMac(NativeViewHost* host) : host_(host) {
  // Ensure that |host_| has its own ui::Layer and that it draws nothing.
  host_->SetPaintToLayer(ui::LAYER_NOT_DRAWN);
}

NativeViewHostMac::~NativeViewHostMac() {
  // The native_view_ is going away, so clear out any reference to it.
  RemoveReferenceToMovedContentView(native_view_);
}

NativeWidgetMacNSWindowHost* NativeViewHostMac::GetNSWindowHost() const {
  return NativeWidgetMacNSWindowHost::GetFromNativeWindow(
      host_->GetWidget()->GetNativeWindow());
}

////////////////////////////////////////////////////////////////////////////////
// NativeViewHostMac, ViewsHostableView::Host implementation:

ui::Layer* NativeViewHostMac::GetUiLayer() const {
  return host_->layer();
}

remote_cocoa::mojom::Application* NativeViewHostMac::GetRemoteCocoaApplication()
    const {
  if (auto* window_host = GetNSWindowHost()) {
    if (auto* application_host = window_host->application_host())
      return application_host->GetApplication();
  }
  return nullptr;
}

uint64_t NativeViewHostMac::GetNSViewId() const {
  auto* window_host = GetNSWindowHost();
  if (window_host)
    return window_host->GetRootViewNSViewId();
  return 0;
}

void NativeViewHostMac::OnHostableViewDestroying() {
  DCHECK(native_view_hostable_);
  host_->NativeViewDestroyed();
  DCHECK(!native_view_hostable_);
}

////////////////////////////////////////////////////////////////////////////////
// NativeViewHostMac, NativeViewHostWrapper implementation:

void NativeViewHostMac::AttachNativeView() {
  DCHECK(host_->native_view());
  DCHECK(!native_view_);
  native_view_ = host_->native_view().GetNativeNSView();
  if ([native_view_ conformsToProtocol:@protocol(ViewsHostable)]) {
    id hostable = native_view_;
    native_view_hostable_ = [hostable viewsHostableView];
  }
  EnsureNativeViewHasNoChildWidgets(native_view_);

  auto* window_host = GetNSWindowHost();
  CHECK(window_host);

  // Save these for later.
  NSWindow* ns_window = [native_view_ window];
  Widget* widget = Widget::GetWidgetForNativeView(host_->native_view());

  // TODO(crbug.com/41442285): This is lifted out the ViewsHostableAttach
  // call below because of crashes being observed in the field.
  NSView* superview =
      window_host->native_widget_mac()->GetNativeView().GetNativeNSView();
  [superview addSubview:native_view_];

  // If adding the native view to another view hierarchy removed it from its
  // host window where it was the contentView (new AppKit behavior as of macOS
  // 13), save a reference to it, otherwise the Views Widget machinery will
  // break.
  if (![ns_window contentView] && widget) {
    widget->SetNativeWindowProperty(
        views::NativeWidgetMacNSWindowHost::kMovedContentNSView,
        (__bridge void*)native_view_);
  }

  if (native_view_hostable_) {
    native_view_hostable_->ViewsHostableAttach(this);
    // Initially set the parent to match the views::Views parent. Note that this
    // may be overridden (e.g, by views::WebView).
    native_view_hostable_->ViewsHostableSetParentAccessible(
        host_->parent()->GetNativeViewAccessible());
  }

  window_host->OnNativeViewHostAttach(host_, native_view_);
}

void NativeViewHostMac::NativeViewDetaching(bool destroyed) {
  // |destroyed| is only true if this class calls host_->NativeViewDestroyed(),
  // which is called if a hosted WebContentsView about to be destroyed (note
  // that its corresponding NSView may still exist).

  // |native_view_| can be nil here if RemovedFromWidget() is called before
  // NativeViewHost::Detach().
  NSView* host_native_view = host_->native_view().GetNativeNSView();
  if (!native_view_) {
    DCHECK(![host_native_view superview]);
    return;
  }

  DCHECK(native_view_ == host_native_view);

  if (native_view_hostable_) {
    native_view_hostable_->ViewsHostableDetach();
    native_view_hostable_ = nullptr;
  } else {
    [native_view_ setHidden:YES];
    [native_view_ removeFromSuperview];
  }

  EnsureNativeViewHasNoChildWidgets(native_view_);
  auto* window_host = GetNSWindowHost();
  // NativeWidgetNSWindowBridge can be null when Widget is closing.
  if (window_host)
    window_host->OnNativeViewHostDetach(host_);

  // If the previous call to AttachNativeView() removed the native_view_ from
  // its window (and it was the window's contentView), remove the reference we
  // created for it in its originating window.
  NSWindow* originating_window =
      RemoveReferenceToMovedContentView(native_view_);

  // Prior to macOS Ventura, AttachNativeView() added native_view_ as a subview
  // of another view tree while leaving it as its originating window's
  // contentView. After removing native_view_ from its superview in this method,
  // it still remained as its window's contentView. With Ventura, the AppKit
  // won't allow a view to live in two separate window view hierarchies at the
  // same time. Restoring it as the contentView of its originating window
  // preserves the pre-Ventura behavior of NativeViewDetaching().
  if ([originating_window contentView] == nil) {
    [originating_window setContentView:native_view_];
  }

  native_view_ = nil;
}

void NativeViewHostMac::AddedToWidget() {
  if (!host_->native_view())
    return;

  AttachNativeView();
  host_->DeprecatedLayoutImmediately();
}

void NativeViewHostMac::RemovedFromWidget() {
  if (!host_->native_view())
    return;

  NativeViewDetaching(false);
}

bool NativeViewHostMac::SetCornerRadii(
    const gfx::RoundedCornersF& corner_radii) {
  ui::Layer* layer = GetUiLayer();
  DCHECK(layer);
  layer->SetRoundedCornerRadius(corner_radii);
  layer->SetIsFastRoundedCorner(true);
  return true;
}

bool NativeViewHostMac::SetCustomMask(std::unique_ptr<ui::LayerOwner> mask) {
  NOTIMPLEMENTED();
  return false;
}

void NativeViewHostMac::SetHitTestTopInset(int top_inset) {
  NOTIMPLEMENTED();
}

int NativeViewHostMac::GetHitTestTopInset() const {
  NOTIMPLEMENTED();
  return 0;
}

void NativeViewHostMac::InstallClip(int x, int y, int w, int h) {
  NOTIMPLEMENTED();
}

bool NativeViewHostMac::HasInstalledClip() {
  return false;
}

void NativeViewHostMac::UninstallClip() {
  NOTIMPLEMENTED();
}

void NativeViewHostMac::ShowWidget(int x,
                                   int y,
                                   int w,
                                   int h,
                                   int native_w,
                                   int native_h) {
  // TODO(crbug.com/41132564): Implement host_->fast_resize().

  if (native_view_hostable_) {
    native_view_hostable_->ViewsHostableSetBounds(gfx::Rect(x, y, w, h));
    native_view_hostable_->ViewsHostableSetVisible(true);
  } else {
    // Coordinates will be from the top left of the parent Widget. The
    // NativeView is already in the same NSWindow, so just flip to get Cocoa
    // coordinates and then convert to the containing view.
    NSRect window_rect = NSMakeRect(
        x, host_->GetWidget()->GetClientAreaBoundsInScreen().height() - y - h,
        w, h);

    // Convert window coordinates to the hosted view's superview, since that's
    // how coordinates of the hosted view's frame is based.
    NSRect container_rect = [[native_view_ superview] convertRect:window_rect
                                                         fromView:nil];
    [native_view_ setFrame:container_rect];
    [native_view_ setHidden:NO];
  }
}

void NativeViewHostMac::HideWidget() {
  if (native_view_hostable_)
    native_view_hostable_->ViewsHostableSetVisible(false);
  else
    [native_view_ setHidden:YES];
}

void NativeViewHostMac::SetFocus() {
  if (native_view_hostable_) {
    native_view_hostable_->ViewsHostableMakeFirstResponder();
  } else {
    if ([native_view_ acceptsFirstResponder])
      [[native_view_ window] makeFirstResponder:native_view_];
  }
}

gfx::NativeView NativeViewHostMac::GetNativeViewContainer() const {
  NOTIMPLEMENTED();
  return nullptr;
}

gfx::NativeViewAccessible NativeViewHostMac::GetNativeViewAccessible() {
  if (native_view_hostable_)
    return native_view_hostable_->ViewsHostableGetAccessibilityElement();
  else
    return native_view_;
}

ui::Cursor NativeViewHostMac::GetCursor(int x, int y) {
  // Intentionally not implemented: Not required on non-aura Mac because macOS
  // will query the native view for the cursor directly. For NativeViewHostMac
  // in practice, macOS will retrieve the cursor that was last set by
  // -[RenderWidgetHostViewCocoa updateCursor:] whenever the pointer is over the
  // hosted view. With some plumbing, NativeViewHostMac could return that same
  // cursor here, but it doesn't achieve anything. The implications of returning
  // null simply mean that the "fallback" cursor on the window itself will be
  // cleared (see -[NativeWidgetMacNSWindow cursorUpdate:]). However, while the
  // pointer is over a RenderWidgetHostViewCocoa, macOS won't ask for the
  // fallback cursor.
  return ui::Cursor();
}

void NativeViewHostMac::SetVisible(bool visible) {
  if (native_view_hostable_)
    native_view_hostable_->ViewsHostableSetVisible(visible);
  else
    [native_view_ setHidden:!visible];
}

void NativeViewHostMac::SetParentAccessible(
    gfx::NativeViewAccessible parent_accessibility_element) {
  if (native_view_hostable_) {
    native_view_hostable_->ViewsHostableSetParentAccessible(
        parent_accessibility_element);
  } else {
    // It is not easy to force a generic NSView to return a different
    // accessibility parent. Fortunately, this interface is only ever used
    // in practice to host a WebContentsView.
  }
}

gfx::NativeViewAccessible NativeViewHostMac::GetParentAccessible() {
  return native_view_hostable_
             ? native_view_hostable_->ViewsHostableGetParentAccessible()
             : nullptr;
}

ui::Layer* NativeViewHostMac::GetUILayer() {
  return host_->layer();
}

// static
NativeViewHostWrapper* NativeViewHostWrapper::CreateWrapper(
    NativeViewHost* host) {
  return new NativeViewHostMac(host);
}

}  // namespace views