chromium/components/exo/shell_surface_base.cc

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

#include "components/exo/shell_surface_base.h"

#include <stdint.h>

#include "ash/display/screen_orientation_controller.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/metrics/login_unlock_throughput_recorder.h"
#include "ash/public/cpp/rounded_corner_utils.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/resize_shadow_controller.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/traced_value.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_pin_type.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "chromeos/ui/frame/frame_utils.h"
#include "components/app_restore/app_restore_utils.h"
#include "components/app_restore/window_properties.h"
#include "components/exo/custom_window_state_delegate.h"
#include "components/exo/security_delegate.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/window_properties.h"
#include "components/exo/wm_helper.h"
#include "components/viz/host/host_frame_sink_manager.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/client/focus_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window_observer.h"
#include "ui/aura/window_occlusion_tracker.h"
#include "ui/aura/window_targeter.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/class_property.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/compositor/layer.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/client_view.h"
#include "ui/wm/core/shadow_controller.h"
#include "ui/wm/core/shadow_types.h"
#include "ui/wm/core/window_animations.h"
#include "ui/wm/core/window_properties.h"
#include "ui/wm/core/window_util.h"

namespace exo {
namespace {

// The accelerator keys used to close ShellSurfaces.
const struct {
  ui::KeyboardCode keycode;
  int modifiers;
} kCloseWindowAccelerators[] = {
    {ui::VKEY_W, ui::EF_CONTROL_DOWN},
    {ui::VKEY_W, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN},
    {ui::VKEY_F4, ui::EF_ALT_DOWN}};

class ShellSurfaceWidget : public views::Widget {
 public:
  ShellSurfaceWidget() = default;

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

  // Overridden from views::Widget:
  void OnKeyEvent(ui::KeyEvent* event) override {
    if (GetFocusManager()->GetFocusedView() &&
        GetFocusManager()->GetFocusedView()->GetWidget() != this) {
      // If the focus is on the overlay widget, dispatch the key event normally.
      views::Widget::OnKeyEvent(event);
    } else if (GetFocusManager()->ProcessAccelerator(ui::Accelerator(*event))) {
      // Otherwise handle only accelerators. Do not call Widget::OnKeyEvent that
      // eats focus management keys (like the tab key) as well.
      event->SetHandled();
    }
  }
};

class CustomFrameView : public ash::NonClientFrameViewAsh {
 public:
  using ShapeRects = std::vector<gfx::Rect>;

  CustomFrameView(views::Widget* widget,
                  ShellSurfaceBase* shell_surface,
                  bool enabled)
      : NonClientFrameViewAsh(widget), shell_surface_(shell_surface) {
    SetFrameEnabled(enabled);
    if (!enabled)
      NonClientFrameViewAsh::SetShouldPaintHeader(false);
  }

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

  ~CustomFrameView() override = default;

  // Overridden from ash::NonClientFrameViewAsh:
  void SetShouldPaintHeader(bool paint) override {
    if (GetFrameEnabled()) {
      NonClientFrameViewAsh::SetShouldPaintHeader(paint);
      return;
    }
  }

  // Overridden from views::NonClientFrameView:
  gfx::Rect GetBoundsForClientView() const override {
    if (GetFrameEnabled())
      return ash::NonClientFrameViewAsh::GetBoundsForClientView();
    return bounds();
  }

  // Overridden from views::NonClientFrameView:
  void UpdateWindowRoundedCorners() override {
    if (!chromeos::features::IsRoundedWindowsEnabled() && GetFrameEnabled()) {
      header_view_->SetHeaderCornerRadius(
          chromeos::GetFrameCornerRadius(frame()->GetNativeWindow()));
    }

    if (!GetWidget()) {
      return;
    }

    aura::Window* window = GetWidget()->GetNativeWindow();
    const ash::WindowState* window_state = ash::WindowState::Get(window);
    std::optional<gfx::RoundedCornersF> window_radii =
        shell_surface_->window_corners_radii();
    std::optional<gfx::RoundedCornersF> shadow_radii =
        shell_surface_->shadow_corner_radii();

    int corner_radius = -1;
    if (window_state->IsPip()) {
      corner_radius = chromeos::kPipRoundedCornerRadius;
    } else if (window_radii || shadow_radii) {
      gfx::RoundedCornersF radii;

      // Certain clients (such as Lacros, for instance) handle window rounding
      // on the client side. These clients do not specify window_radii. However,
      // they do specify shadow radii which matches window radii.
      // For such clients, use shadow radii to specify
      // `aura::client::kWindowCornerRadiusKey` since it is used to round
      // various server side decorations.
      radii =
          window_radii.value_or(shadow_radii.value_or(gfx::RoundedCornersF()));

      // TODO(crbug.com/40256581): Support variable window radii.
      corner_radius = radii.upper_left();
    }

    // Various window decorations are rounded using `kWindowCornerRadiusKey`
    // property.
    window->SetProperty(aura::client::kWindowCornerRadiusKey, corner_radius);

    // If window_radii is null, skip rounding the window.
    if (!window_radii) {
      return;
    }

    if (GetFrameEnabled()) {
      header_view_->SetHeaderCornerRadius(corner_radius);
    }

    GetWidget()->client_view()->UpdateWindowRoundedCorners(corner_radius);
  }

  gfx::Rect GetWindowBoundsForClientBounds(
      const gfx::Rect& client_bounds) const override {
    if (GetFrameEnabled()) {
      return ash::NonClientFrameViewAsh::GetWindowBoundsForClientBounds(
          client_bounds);
    }
    return client_bounds;
  }
  int NonClientHitTest(const gfx::Point& point) override {
    if (GetFrameEnabled() || shell_surface_->server_side_resize()) {
      return ash::NonClientFrameViewAsh::NonClientHitTest(point);
    }
    return GetWidget()->client_view()->NonClientHitTest(point);
  }
  void GetWindowMask(const gfx::Size& size, SkPath* window_mask) override {
    if (GetFrameEnabled())
      return ash::NonClientFrameViewAsh::GetWindowMask(size, window_mask);
  }
  void ResetWindowControls() override {
    if (GetFrameEnabled())
      return ash::NonClientFrameViewAsh::ResetWindowControls();
  }
  void UpdateWindowIcon() override {
    if (GetFrameEnabled())
      return ash::NonClientFrameViewAsh::ResetWindowControls();
  }
  void UpdateWindowTitle() override {
    if (GetFrameEnabled())
      return ash::NonClientFrameViewAsh::UpdateWindowTitle();
  }
  void SizeConstraintsChanged() override {
    if (GetFrameEnabled())
      return ash::NonClientFrameViewAsh::SizeConstraintsChanged();
  }
  gfx::Size GetMinimumSize() const override {
    gfx::Size minimum_size = shell_surface_->GetMinimumSize();
    if (GetFrameEnabled()) {
      return ash::NonClientFrameViewAsh::GetWindowBoundsForClientBounds(
                 gfx::Rect(minimum_size))
          .size();
    }
    return minimum_size;
  }
  gfx::Size GetMaximumSize() const override {
    gfx::Size maximum_size = shell_surface_->GetMaximumSize();
    if (GetFrameEnabled() && !maximum_size.IsEmpty()) {
      return ash::NonClientFrameViewAsh::GetWindowBoundsForClientBounds(
                 gfx::Rect(maximum_size))
          .size();
    }
    return maximum_size;
  }

 private:
  const raw_ptr<ShellSurfaceBase> shell_surface_;
};

class CustomClientView : public views::ClientView {
 public:
  CustomClientView(views::Widget* widget, ShellSurfaceBase* shell_surface)
      : views::ClientView(widget, shell_surface),
        shell_surface_(shell_surface) {}

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

  ~CustomClientView() override = default;

  // ClientView:
  void UpdateWindowRoundedCorners(int corner_radius) override {
    DCHECK(GetWidget());
    const CustomFrameView* custom_frame_view = static_cast<CustomFrameView*>(
        GetWidget()->non_client_view()->frame_view());

    // In the typical scenario with frame enabled, we round:
    //   * Upper corners of the frame.
    //   * Lower corners of the client view.
    // But when the frame is overlapped with the client view, for upper corners,
    // both the top (frame) and the bottom (client view) views need to be
    // rounded.
    const bool should_round_client_view_upper_corner =
        !custom_frame_view->GetFrameEnabled() ||
        custom_frame_view->GetFrameOverlapped();

    const float corner_radius_f = corner_radius;
    const gfx::RoundedCornersF root_surface_radii = {
        should_round_client_view_upper_corner ? corner_radius_f : 0,
        should_round_client_view_upper_corner ? corner_radius_f : 0,
        corner_radius_f, corner_radius_f};

    const Surface* root_surface = shell_surface_->root_surface();

    const gfx::RectF bounds =
        root_surface_radii.IsEmpty()
            ? gfx::RectF()
            : gfx::RectF(root_surface->surface_hierarchy_content_bounds());
    shell_surface_->ApplyRoundedCornersToSurfaceTree(bounds,
                                                     root_surface_radii);
  }

 private:
  raw_ptr<ShellSurfaceBase> shell_surface_;
};

class CustomWindowTargeter : public aura::WindowTargeter {
 public:
  explicit CustomWindowTargeter(ShellSurfaceBase* shell_surface)
      : shell_surface_(shell_surface), widget_(shell_surface->GetWidget()) {}

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

  ~CustomWindowTargeter() override = default;

  // Overridden from aura::WindowTargeter:
  bool EventLocationInsideBounds(aura::Window* window,
                                 const ui::LocatedEvent& event) const override {
    gfx::Point local_point =
        ConvertEventLocationToWindowCoordinates(window, event);

    if (shell_surface_->shape_dp() &&
        !shell_surface_->shape_dp()->Contains(local_point)) {
      return false;
    }

    if (IsInResizeHandle(window, event, local_point))
      return true;

    Surface* surface = GetShellRootSurface(window);
    if (!surface)
      return false;

    int component =
        widget_->non_client_view()
            ? widget_->non_client_view()->NonClientHitTest(local_point)
            : HTNOWHERE;
    if (component != HTNOWHERE && component != HTCLIENT &&
        component != HTBORDER) {
      return true;
    }

    aura::Window::ConvertPointToTarget(window, surface->window(), &local_point);
    return surface->HitTest(local_point);
  }

 private:
  bool IsInResizeHandle(aura::Window* window,
                        const ui::LocatedEvent& event,
                        const gfx::Point& local_point) const {
    if (window != widget_->GetNativeWindow() ||
        !widget_->widget_delegate()->CanResize()) {
      return false;
    }

    if (!shell_surface_->server_side_resize())
      return false;

    ui::EventTarget* parent =
        static_cast<ui::EventTarget*>(window)->GetParentTarget();
    if (parent) {
      aura::WindowTargeter* parent_targeter =
          static_cast<aura::WindowTargeter*>(parent->GetEventTargeter());

      if (parent_targeter) {
        gfx::Rect mouse_rect;
        gfx::Rect touch_rect;

        if (parent_targeter->GetHitTestRects(window, &mouse_rect,
                                             &touch_rect)) {
          const gfx::Vector2d offset = -window->bounds().OffsetFromOrigin();
          mouse_rect.Offset(offset);
          touch_rect.Offset(offset);
          if (event.IsTouchEvent() || event.IsGestureEvent()
                  ? touch_rect.Contains(local_point)
                  : mouse_rect.Contains(local_point)) {
            return true;
          }
        }
      }
    }
    return false;
  }

  raw_ptr<ShellSurfaceBase> shell_surface_;
  const raw_ptr<views::Widget, DanglingUntriaged> widget_;
};

void CloseAllShellSurfaceTransientChildren(aura::Window* window) {
  // Deleting a window may delete other transient children. Remove other shell
  // surface bases first so they don't get deleted.
  auto list = wm::GetTransientChildren(window);
  for (size_t i = 0; i < list.size(); ++i) {
    if (GetShellSurfaceBaseForWindow(list[i]))
      wm::RemoveTransientChild(window, list[i]);
  }
}

int shell_id = 0;

void ShowSnapPreview(aura::Window* window,
                     chromeos::SnapDirection snap_direction) {
  chromeos::SnapController::Get()->ShowSnapPreview(
      window, snap_direction,
      /*allow_haptic_feedback=*/false);
}

void CommitSnap(aura::Window* window,
                chromeos::SnapDirection snap_direction,
                float snap_ratio) {
  chromeos::SnapController::Get()->CommitSnap(
      window, snap_direction, snap_ratio,
      chromeos::SnapController::SnapRequestSource::
          kFromLacrosSnapButtonOrWindowLayoutMenu);
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase, public:

ShellSurfaceBase::ShellSurfaceBase(Surface* surface,
                                   const gfx::Point& origin,
                                   bool can_minimize,
                                   int container)
    : SurfaceTreeHost(base::StringPrintf("ExoShellSurfaceHost-%d", shell_id)),
      origin_(origin),
      container_(container),
      can_minimize_(can_minimize) {
  WMHelper::GetInstance()->AddActivationObserver(this);
  WMHelper::GetInstance()->AddTooltipObserver(this);
  surface->AddSurfaceObserver(this);
  SetRootSurface(surface);
  host_window()->Show();
  set_owned_by_client();

  SetCanMinimize(can_minimize_);
  SetCanMaximize(ash::desks_util::IsDeskContainerId(container_));
  SetCanFullscreen(ash::desks_util::IsDeskContainerId(container_));
  SetCanResize(true);
  SetShowTitle(false);
  GetViewAccessibility().SetRole(ax::mojom::Role::kClient);
}

ShellSurfaceBase::~ShellSurfaceBase() {
  // For overlapped frames, a relationship is created between the host window
  // layer and the frame header layer. We need to notify frame header to remove
  // the relationship before host window to destroyed.
  if (frame_overlapped()) {
    auto* frame_header = chromeos::FrameHeader::Get(widget_);
    frame_header->RemoveLayerBeneath();
  }

  // If the surface was TrustedPinned, we have to unpin first as this might have
  // locked down some system functions.
  if (current_pinned_state_ == chromeos::WindowPinType::kTrustedPinned) {
    pending_pinned_state_ = chromeos::WindowPinType::kNone;
    UpdatePinned();
  }

  // Close the overlay in case the window is deleted by the server.
  overlay_widget_.reset();

  // Remove activation observer before hiding widget to prevent it from
  // casuing the configure callback to be called.
  WMHelper::GetInstance()->RemoveActivationObserver(this);

  // Client is gone by now, so don't call callbacks.
  close_callback_.Reset();
  pre_close_callback_.Reset();
  surface_destroyed_callback_.Reset();

  if (widget_) {
    if (has_grab_) {
      widget_->ReleaseCapture();
      WMHelper::GetInstance()->GetCaptureClient()->RemoveObserver(this);
    }
    widget_->GetNativeWindow()->RemoveObserver(this);
    widget_->RemoveObserver(this);
    // Remove transient children which are shell surfaces so they are not
    // automatically destroyed.
    CloseAllShellSurfaceTransientChildren(widget_->GetNativeWindow());
    if (widget_->IsVisible())
      widget_->Hide();
    widget_->CloseNow();
  }
  if (parent_)
    parent_->RemoveObserver(this);
  if (root_surface())
    root_surface()->RemoveSurfaceObserver(this);
  WMHelper::GetInstance()->RemoveTooltipObserver(this);
  CHECK(!views::WidgetObserver::IsInObserverList());
}

void ShellSurfaceBase::Activate() {
  TRACE_EVENT0("exo", "ShellSurfaceBase::Activate");

  if (!widget_ || pending_show_widget_) {
    initially_activated_ = true;
    return;
  }

  if (widget_->IsActive())
    return;

  widget_->Activate();
}

void ShellSurfaceBase::Deactivate() {
  TRACE_EVENT0("exo", "ShellSurfaceBase::Deactivate");

  if (!widget_ || pending_show_widget_) {
    initially_activated_ = false;
    return;
  }

  if (!widget_->IsActive())
    return;

  widget_->Deactivate();
}

void ShellSurfaceBase::SetTitle(const std::u16string& title) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetTitle", "title",
               base::UTF16ToUTF8(title));
  WidgetDelegate::SetTitle(title);

  aura::Env::GetInstance()
      ->context_factory()
      ->GetHostFrameSinkManager()
      ->SetFrameSinkDebugLabel(host_window()->GetFrameSinkId(),
                               base::UTF16ToUTF8(title));
}

void ShellSurfaceBase::SetIcon(const gfx::ImageSkia& icon) {
  TRACE_EVENT0("exo", "ShellSurfaceBase::SetIcon");
  WidgetDelegate::SetIcon(ui::ImageModel::FromImageSkia(icon));
}

void ShellSurfaceBase::SetSystemModal(bool system_modal) {
  if (system_modal == system_modal_)
    return;

  bool non_system_modal_window_was_active =
      !system_modal_ && widget_ && widget_->IsActive();

  system_modal_ = system_modal;

  if (widget_) {
    UpdateSystemModal();
    // Deactivate to give the focus back to normal windows.
    if (!system_modal_ && !non_system_modal_window_was_active_) {
      widget_->Deactivate();
    }
  }

  non_system_modal_window_was_active_ = non_system_modal_window_was_active;
}

void ShellSurfaceBase::SetTopInset(int height) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetTopInset", "height", height);
  pending_top_inset_height_ = height;
}

void ShellSurfaceBase::SetWindowCornersRadii(
    const gfx::RoundedCornersF& radii) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetWindowCornerRadii", "radii",
               radii.ToString());
  pending_window_corners_radii_dp_ = radii;
}

void ShellSurfaceBase::SetShadowCornersRadii(
    const gfx::RoundedCornersF& radii) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetShadowCornersRadii", "shadow_radii",
               radii.ToString());
  pending_shadow_corners_radii_dp_ = radii;
}

void ShellSurfaceBase::SetBoundsForShadows(
    const std::optional<gfx::Rect>& shadow_bounds) {
  if (shadow_bounds_ != shadow_bounds) {
    // Set normal shadow bounds.
    shadow_bounds_ = shadow_bounds;
    shadow_bounds_changed_ = true;
    if (widget_ && shadow_bounds) {
      // Set resize shadow bounds and origin.
      const gfx::Rect bounds = shadow_bounds.value();
      const gfx::Point absolute_origin =
          widget_->GetNativeWindow()->bounds().origin();
      const gfx::Rect absolute_bounds =
          gfx::Rect(absolute_origin.x(), absolute_origin.y(), bounds.width(),
                    bounds.height());
      ash::Shell::Get()
          ->resize_shadow_controller()
          ->UpdateResizeShadowBoundsOfWindow(widget_->GetNativeWindow(),
                                             absolute_bounds);
    }
  }
}

void ShellSurfaceBase::UpdateSystemModal() {
  DCHECK(widget_);
  DCHECK_EQ(container_, ash::kShellWindowId_SystemModalContainer);
  widget_->GetNativeWindow()->SetProperty(
      aura::client::kModalKey, system_modal_ ? ui::mojom::ModalType::kSystem
                                             : ui::mojom::ModalType::kNone);
}

void ShellSurfaceBase::UpdateShape() {
  auto* widget_window = widget_->GetNativeWindow();
  if (!widget_window || !widget_window->layer()) {
    return;
  }

  if (!shape_dp_.has_value()) {
    widget_window->layer()->SetAlphaShape(nullptr);
    return;
  }

  // TODO(crbug.com/40276217): The current implementation of window shape must
  // only be used on frameless windows with shadows disabled, otherwise we risk
  // the layer bounds not matching the bounds of the root surface. This needs to
  // be updated such that the shape is applied to the root surface's geometry.
  DCHECK_EQ(frame_type_, SurfaceFrameType::NONE);

  auto shape_rects_dp = std::make_unique<ui::Layer::ShapeRects>();
  for (gfx::Rect rect : shape_dp_.value()) {
    shape_rects_dp->push_back(std::move(rect));
  }

  widget_window->layer()->SetAlphaShape(std::move(shape_rects_dp));
}

void ShellSurfaceBase::SetApplicationId(const char* application_id) {
  // Store the value in |application_id_| in case the window does not exist yet.
  if (application_id)
    application_id_ = std::string(application_id);
  else
    application_id_.reset();

  if (widget_ && widget_->GetNativeWindow()) {
    SetShellApplicationId(widget_->GetNativeWindow(), application_id_);
    WMHelper::AppPropertyResolver::Params params;
    if (application_id_)
      params.app_id = *application_id_;
    if (startup_id_)
      params.startup_id = *startup_id_;
    ui::PropertyHandler& property_handler = *widget_->GetNativeWindow();
    WMHelper::GetInstance()->PopulateAppProperties(params, property_handler);
  }
  if (application_id_) {
    GetViewAccessibility().SetChildTreeNodeAppId(*application_id_);
  } else {
    GetViewAccessibility().RemoveChildTreeNodeAppId();
  }
  this->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged,
                                 /* send_native_event */ false);
}

void ShellSurfaceBase::SetStartupId(const char* startup_id) {
  // Store the value in |startup_id_| in case the window does not exist yet.
  if (startup_id)
    startup_id_ = std::string(startup_id);
  else
    startup_id_.reset();

  if (widget_ && widget_->GetNativeWindow())
    SetShellStartupId(widget_->GetNativeWindow(), startup_id_);
}

void ShellSurfaceBase::SetUseImmersiveForFullscreen(bool value) {
  // Store the value in case the window doesn't exist yet.
  immersive_implied_by_fullscreen_ = value;

  if (widget_ && widget_->GetNativeWindow())
    SetShellUseImmersiveForFullscreen(widget_->GetNativeWindow(), value);
}

void ShellSurfaceBase::ShowSnapPreviewToPrimary() {
  ShowSnapPreview(widget_->GetNativeWindow(),
                  chromeos::SnapDirection::kPrimary);
}

void ShellSurfaceBase::ShowSnapPreviewToSecondary() {
  ShowSnapPreview(widget_->GetNativeWindow(),
                  chromeos::SnapDirection::kSecondary);
}

void ShellSurfaceBase::HideSnapPreview() {
  ShowSnapPreview(widget_->GetNativeWindow(), chromeos::SnapDirection::kNone);
}

void ShellSurfaceBase::SetSnapPrimary(float snap_ratio) {
  CommitSnap(widget_->GetNativeWindow(), chromeos::SnapDirection::kPrimary,
             snap_ratio);
}

void ShellSurfaceBase::SetSnapSecondary(float snap_ratio) {
  CommitSnap(widget_->GetNativeWindow(), chromeos::SnapDirection::kSecondary,
             snap_ratio);
}

void ShellSurfaceBase::UnsetSnap() {
  if (widget_ && widget_->GetNativeWindow()) {
    CommitSnap(widget_->GetNativeWindow(), chromeos::SnapDirection::kNone,
               chromeos::kDefaultSnapRatio);
  }
}

void ShellSurfaceBase::SetCanGoBack() {
  if (widget_)
    widget_->GetNativeWindow()->SetProperty(ash::kMinimizeOnBackKey, false);
}

void ShellSurfaceBase::UnsetCanGoBack() {
  if (widget_)
    widget_->GetNativeWindow()->SetProperty(ash::kMinimizeOnBackKey, true);
}

void ShellSurfaceBase::SetPip() {
  if (!widget_) {
    pending_pip_ = true;
    return;
  }
  pending_pip_ = false;

  // Set all the necessary window properties and window state.
  auto* window = widget_->GetNativeWindow();
  window->SetProperty(ash::kWindowPipTypeKey, true);
  window->SetProperty(aura::client::kZOrderingKey,
                      ui::ZOrderLevel::kFloatingWindow);

  if (initial_bounds_)
    return;
  // If no initial bounds is specified, pip windows should start in the bottom
  // right corner of the screen so move |window| to the bottom right of the
  // work area and let the pip positioner move it within the work area.
  auto display = display::Screen::GetScreen()->GetDisplayNearestWindow(window);
  gfx::Size window_size = window->bounds().size();
  window->SetBoundsInScreen(
      gfx::Rect(display.work_area().bottom_right(), window_size), display);
}

void ShellSurfaceBase::UnsetPip() {
  // Ash does not implement restoring the pip state. Additionally it does not
  // make sense for browser pip window to unset pip since the browser(lacros)
  // creates a separate window for a pip and once pip is not needed,
  // the window is destroyed rather than restoring it to some other state.
  // However, ClientControlledShellSurface(Arc++), has a concept of restoring
  // from pip state and implements UnsetPip.
  NOTIMPLEMENTED();
}

void ShellSurfaceBase::SetFloatToLocation(
    chromeos::FloatStartLocation float_start_location) {
  chromeos::FloatControllerBase::Get()->SetFloat(widget_->GetNativeWindow(),
                                                 float_start_location);
}

void ShellSurfaceBase::MoveToDesk(int desk_index) {
  if (widget_) {
    ash::DesksController::Get()->SendToDeskAtIndex(widget_->GetNativeWindow(),
                                                   desk_index);
  }
}

void ShellSurfaceBase::SetVisibleOnAllWorkspaces() {
  if (widget_)
    widget_->SetVisibleOnAllWorkspaces(true);
}

void ShellSurfaceBase::SetInitialWorkspace(const char* initial_workspace) {
  if (initial_workspace)
    initial_workspace_ = std::string(initial_workspace);
  else
    initial_workspace_.reset();
}

void ShellSurfaceBase::Pin(bool trusted) {
  pending_pinned_state_ = trusted ? chromeos::WindowPinType::kTrustedPinned
                                  : chromeos::WindowPinType::kPinned;
  UpdatePinned();
}

void ShellSurfaceBase::Unpin() {
  // Only need to do something when we have to set a pinned mode.
  if (pending_pinned_state_ == chromeos::WindowPinType::kNone)
    return;

  // Remove any pending pin states which might not have been applied yet.
  pending_pinned_state_ = chromeos::WindowPinType::kNone;
  UpdatePinned();
}

void ShellSurfaceBase::UpdatePinned() {
  if (!widget_) {
    // It is possible to get here before the widget has actually been created.
    // The state will be set once the widget gets created.
    return;
  }
  if (current_pinned_state_ != pending_pinned_state_) {
    auto* window = widget_->GetNativeWindow();
    if (pending_pinned_state_ == chromeos::WindowPinType::kNone) {
      ash::WindowState::Get(window)->Restore();
    } else {
      bool trusted_pinned =
          pending_pinned_state_ == chromeos::WindowPinType::kTrustedPinned;
      ash::window_util::PinWindow(window,
                                  /*trusted=*/trusted_pinned);
    }

    current_pinned_state_ = pending_pinned_state_;
  }
}

void ShellSurfaceBase::UpdateTopInset() {
  if (!widget_) {
    // It is possible to get here before the widget has actually been created.
    // The state will be set once the widget gets created.
    return;
  }

  // Apply new top inset height.
  if (pending_top_inset_height_ != top_inset_height_) {
    widget_->GetNativeWindow()->SetProperty(aura::client::kTopViewInset,
                                            pending_top_inset_height_);
    top_inset_height_ = pending_top_inset_height_;
  }
}

void ShellSurfaceBase::SetChildAxTreeId(ui::AXTreeID child_ax_tree_id) {
  GetViewAccessibility().SetChildTreeID(child_ax_tree_id);
  this->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged, false);
}

void ShellSurfaceBase::SetGeometry(const gfx::Rect& geometry) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetGeometry", "geometry",
               geometry.ToString());

  if (geometry.IsEmpty()) {
    DLOG(WARNING) << "Surface geometry must be non-empty";
    return;
  }
  pending_geometry_ = geometry;
}

void ShellSurfaceBase::SetWindowBounds(const gfx::Rect& bounds) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetWindowBounds", "bounds",
               bounds.ToString());
  if (!widget_) {
    initial_bounds_.emplace(bounds);
    return;
  }

  SecurityDelegate* security = GetSecurityDelegate();
  if (!security) {
    return;
  }

  switch (security->CanSetBounds(widget_->GetNativeWindow())) {
    // Disallowed by default.
    case SecurityDelegate::SetBoundsPolicy::IGNORE:
      break;

    // For selected clients (Borealis) expand the requested bounds to include
    // the decorations, if any.
    //
    // TODO(crbug.com/1261321, b/268395213): Instead, tell clients how large the
    // decorations are, so they can make better decisions.
    case SecurityDelegate::SetBoundsPolicy::ADJUST_IF_DECORATED:
      if (widget_->non_client_view()) {
        gfx::Rect expanded_bounds{
            widget_->non_client_view()->GetWindowBoundsForClientBounds(bounds)};

        // If this expansion pushes the title bar offscreen, push it back
        // onscreen while preserving requested X coordinate, width, and height.
        gfx::Rect work_area = display::Screen::GetScreen()
                                  ->GetDisplayMatching(bounds)
                                  .work_area();
        if (!work_area.IsEmpty() && expanded_bounds.y() < work_area.y()) {
          expanded_bounds.Offset(0, work_area.y() - expanded_bounds.y());
        }
        widget_->SetBounds(expanded_bounds);
      } else {
        // No decorations, so no adjustment needed.
        widget_->SetBounds(bounds);
      }
      break;

    // Other clients (Lacros) may set bounds, but it's a bug to do so for
    // decorated windows. The chosen way to detect such bugs is a DCHECK.
    //
    // TODO(crbug.com/1261321, b/268395213): Instead, tell clients how large the
    // decorations are, so they can make better decisions.
    case SecurityDelegate::SetBoundsPolicy::DCHECK_IF_DECORATED:
      DCHECK(!frame_enabled());
      widget_->SetBounds(bounds);
      break;
  }
}

void ShellSurfaceBase::SetRestoreInfo(int32_t restore_session_id,
                                      int32_t restore_window_id) {
  // TODO(crbug.com/1327490): Rename restore info variables.
  // Restore information must be set before widget is created.
  DCHECK(!widget_);
  restore_session_id_.emplace(restore_session_id);
  restore_window_id_.emplace(restore_window_id);
  ash::LoginUnlockThroughputRecorder* throughput_recorder =
      ash::Shell::Get()->login_unlock_throughput_recorder();
  throughput_recorder->OnRestoredWindowCreated(restore_window_id);
}

void ShellSurfaceBase::SetRestoreInfoWithWindowIdSource(
    int32_t restore_session_id,
    const std::string& restore_window_id_source) {
  restore_session_id_.emplace(restore_session_id);
  if (!restore_window_id_source.empty())
    restore_window_id_source_.emplace(restore_window_id_source);
}

void ShellSurfaceBase::UnsetFloat() {
  chromeos::FloatControllerBase::Get()->UnsetFloat(widget_->GetNativeWindow());
}

void ShellSurfaceBase::SetDisplay(int64_t display_id) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetDisplay", "display_id", display_id);

  pending_display_id_ = display_id;
}

void ShellSurfaceBase::SetOrigin(const gfx::Point& origin) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetOrigin", "origin",
               origin.ToString());

  origin_ = origin;
}

void ShellSurfaceBase::SetActivatable(bool activatable) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetActivatable", "activatable",
               activatable);

  activatable_ = activatable;
}

void ShellSurfaceBase::SetContainer(int container) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetContainer", "container", container);
  SetContainerInternal(container);
}

void ShellSurfaceBase::SetMaximumSize(const gfx::Size& size) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetMaximumSize", "size",
               size.ToString());
  pending_maximum_size_ = size;
}

void ShellSurfaceBase::SetMinimumSize(const gfx::Size& size) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetMinimumSize", "size",
               size.ToString());

  pending_minimum_size_ = size;
}

void ShellSurfaceBase::SetAspectRatio(const gfx::SizeF& aspect_ratio) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetAspectRatio", "aspect_ratio",
               aspect_ratio.ToString());

  pending_aspect_ratio_ = aspect_ratio;
}

void ShellSurfaceBase::SetCanMinimize(bool can_minimize) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetCanMinimize", "can_minimize",
               can_minimize);

  can_minimize_ = can_minimize;
  WidgetDelegate::SetCanMinimize(!parent_ && can_minimize_);
}

void ShellSurfaceBase::SetPersistable(bool persistable) {
  // This should be called before the widget is created.
  DCHECK(!widget_);

  persistable_ = persistable;
}

void ShellSurfaceBase::SetMenu() {
  is_menu_ = true;
}

void ShellSurfaceBase::DisableMovement() {
  movement_disabled_ = true;
  SetCanResize(false);

  if (widget_)
    widget_->set_movement_disabled(true);
}

void ShellSurfaceBase::UpdateResizability() {
  SetCanResize(CalculateCanResize());
  auto max_size = GetMaximumSize();
  bool max_size_resizability_only = false;
  if (widget_ && widget_->GetNativeWindow()) {
    max_size_resizability_only = widget_->GetNativeWindow()->GetProperty(
        kMaximumSizeForResizabilityOnly);
  }

  // Allow maximizing if the max size is bigger than 32k resolution.
  SetCanMaximize(CanResize() && !parent_ &&
                 ash::desks_util::IsDeskContainerId(container_) &&
                 (max_size.IsEmpty() || max_size_resizability_only));
}

void ShellSurfaceBase::RebindRootSurface(Surface* root_surface,
                                         bool can_minimize,
                                         int container) {
  can_minimize_ = can_minimize;
  container_ = container;
  this->root_surface()->RemoveSurfaceObserver(this);
  root_surface->AddSurfaceObserver(this);
  SetRootSurface(root_surface);
  host_window()->Show();

  // Re-apply window properties to the new root surface.
  auto* window = widget_ ? widget_->GetNativeWindow() : nullptr;
  if (window) {
    // Int properties.
    for (auto* const key :
         {aura::client::kSkipImeProcessing, chromeos::kFrameRestoreLookKey,
          ash::kFrameRateThrottleKey}) {
      if (base::Contains(window->GetAllPropertyKeys(), key)) {
        OnWindowPropertyChanged(window, key,
                                /*old_value(unused)=*/0);
      }
    }
    // Boolean property.
    if (base::Contains(window->GetAllPropertyKeys(),
                       aura::client::kWindowWorkspaceKey)) {
      OnWindowPropertyChanged(window, aura::client::kWindowWorkspaceKey,
                              /*old_value(unused)=*/0);
    }
    if (window->HasFocus())
      root_surface->window()->Focus();
  }

  SetCanMinimize(can_minimize_);
  SetCanMaximize(ash::desks_util::IsDeskContainerId(container_));
  SetCanFullscreen(ash::desks_util::IsDeskContainerId(container_));
  SetCanResize(true);
  SetShowTitle(false);
}

std::unique_ptr<base::trace_event::TracedValue>
ShellSurfaceBase::AsTracedValue() const {
  std::unique_ptr<base::trace_event::TracedValue> value(
      new base::trace_event::TracedValue());
  value->SetString("title", base::UTF16ToUTF8(GetWindowTitle()));
  if (GetWidget() && GetWidget()->GetNativeWindow()) {
    const std::string* application_id =
        GetShellApplicationId(GetWidget()->GetNativeWindow());

    if (application_id)
      value->SetString("application_id", *application_id);

    const std::string* startup_id =
        GetShellStartupId(GetWidget()->GetNativeWindow());

    if (startup_id)
      value->SetString("startup_id", *startup_id);
  }
  return value;
}

void ShellSurfaceBase::AddOverlay(OverlayParams&& overlay_params) {
  DCHECK(widget_);
  DCHECK(!overlay_widget_);
  overlay_overlaps_frame_ = overlay_params.overlaps_frame;
  overlay_can_resize_ = std::move(overlay_params.can_resize);

  views::Widget::InitParams params(
      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
      views::Widget::InitParams::TYPE_CONTROL);
  params.parent = widget_->GetNativeWindow();
  if (overlay_params.translucent)
    params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;

  if (overlay_params.focusable)
    params.activatable = views::Widget::InitParams::Activatable::kYes;

  params.delegate = new views::WidgetDelegate();
  params.delegate->SetOwnedByWidget(true);
  params.delegate->SetContentsView(std::move(overlay_params.contents_view));
  params.name = "Overlay";

  overlay_widget_ = std::make_unique<views::Widget>();
  overlay_widget_->Init(std::move(params));
  overlay_widget_->GetNativeWindow()->SetEventTargeter(
      std::make_unique<aura::WindowTargeter>());

  if (overlay_params.corners_radii) {
    ui::Layer* layer = overlay_widget_->GetLayer();
    const gfx::RoundedCornersF& radii = overlay_params.corners_radii.value();
    layer->SetRoundedCornerRadius(radii);
    layer->SetIsFastRoundedCorner(/*enable=*/!radii.IsEmpty());
  }

  overlay_widget_->Show();

  // Setup Focus Traversal.
  overlay_widget_->SetFocusTraversableParentView(this);
  overlay_widget_->SetFocusTraversableParent(
      GetWidget()->GetFocusTraversable());
  SetFocusTraversesOut(true);

  skip_ime_processing_ = GetWidget()->GetNativeWindow()->GetProperty(
      aura::client::kSkipImeProcessing);
  if (skip_ime_processing_) {
    GetWidget()->GetNativeWindow()->SetProperty(
        aura::client::kSkipImeProcessing, false);
  }

  set_bounds_is_dirty(true);
  UpdateWidgetBounds();
  UpdateResizability();
}

void ShellSurfaceBase::RemoveOverlay() {
  overlay_widget_.reset();
  SetFocusTraversesOut(false);
  if (skip_ime_processing_) {
    GetWidget()->GetNativeWindow()->SetProperty(
        aura::client::kSkipImeProcessing, true);
  }
  UpdateResizability();
}

////////////////////////////////////////////////////////////////////////////////
// SurfaceDelegate overrides:

void ShellSurfaceBase::OnSurfaceCommit() {
  // Pause occlusion tracking since we will update a bunch of window properties.
  aura::WindowOcclusionTracker::ScopedPause pause_occlusion;

  // SetShadowBounds requires synchronizing shadow bounds with the next frame,
  // so submit the next frame to a new surface and let the host window use the
  // new surface.
  if (shadow_bounds_changed_) {
    AllocateLocalSurfaceId();
  }

  const gfx::Rect old_content_bounds =
      root_surface()->surface_hierarchy_content_bounds();

  root_surface()->CommitSurfaceHierarchy(false);

  set_bounds_is_dirty(bounds_is_dirty() ||
                      old_content_bounds !=
                          root_surface()->surface_hierarchy_content_bounds());

  if (!OnPreWidgetCommit())
    return;

  WillCommit();

  CommitWidget();
  OnPostWidgetCommit();
  SubmitCompositorFrame();
}

bool ShellSurfaceBase::IsInputEnabled(Surface*) const {
  return true;
}

void ShellSurfaceBase::OnSetFrame(SurfaceFrameType frame_type) {
  if (!IsFrameDecorationSupported(frame_type)) {
    DLOG(WARNING)
        << "popup does not support frame decoration other than NONE/SHADOW.";
    return;
  }

  bool frame_type_changed = frame_type_ != frame_type;

  // aura-shell's set_frame, when used with xdg-shell, works iff the frame type
  // or frame colors were specified before firsrt buffer commit. If these are
  // not specified, the widget's layer is set to 'NOT_DRAWN' and the frame can't
  // be drawn. `ClientControlledShellSurface` is not affected.
  if (frame_type_changed && widget_ &&
      widget_->GetNativeWindow()->layer()->type() == ui::LAYER_NOT_DRAWN) {
    if (frame_type != SurfaceFrameType::NONE &&
        frame_type != SurfaceFrameType::SHADOW) {
      DLOG(FATAL)
          << "A shell surface with NOT_DRAWN layer can't support visible frame";
      return;
    }
  }
  bool frame_was_disabled = !frame_enabled();

  frame_type_ = frame_type;
  switch (frame_type) {
    case SurfaceFrameType::NONE:
      shadow_bounds_.reset();
      break;
    case SurfaceFrameType::NORMAL:
    case SurfaceFrameType::AUTOHIDE:
    case SurfaceFrameType::OVERLAY:
    case SurfaceFrameType::OVERLAP:
    case SurfaceFrameType::SHADOW:
      // Initialize the shadow if it didn't exist. Do not reset if
      // the frame type just switched from another enabled type or
      // there is a pending shadow_bounds_ change to avoid overriding
      // a shadow bounds which have been changed and not yet committed.
      if (frame_type_changed &&
          (!shadow_bounds_ || (frame_was_disabled && !shadow_bounds_changed_)))
        shadow_bounds_ = gfx::Rect();
      break;
  }
  if (!widget_)
    return;

  // Override redirect window and popup can request NONE/SHADOW. The shadow
  // will be updated in next commit.
  if (widget_->non_client_view()) {
    CustomFrameView* frame_view =
        static_cast<CustomFrameView*>(widget_->non_client_view()->frame_view());
    if (frame_view->GetFrameEnabled() == frame_enabled() &&
        frame_view->GetFrameOverlapped() == frame_overlapped()) {
      return;
    }

    frame_view->SetFrameEnabled(frame_enabled());
    frame_view->SetShouldPaintHeader(frame_enabled());

    frame_view->SetFrameOverlapped(frame_overlapped());

    auto* frame_header = chromeos::FrameHeader::Get(widget_);
    if (frame_overlapped()) {
      frame_header->AddLayerBeneath(host_window());
    } else {
      frame_header->RemoveLayerBeneath();
    }
  }

  widget_->GetRootView()->DeprecatedLayoutImmediately();
  // TODO(oshima): We probably should wait applying these if the
  // window is animating.
  set_bounds_is_dirty(true);
  UpdateWidgetBounds();
  UpdateHostWindowOrigin();
}

void ShellSurfaceBase::OnSetFrameColors(SkColor active_color,
                                        SkColor inactive_color) {
  has_frame_colors_ = true;
  active_frame_color_ = SkColorSetA(active_color, SK_AlphaOPAQUE);
  inactive_frame_color_ = SkColorSetA(inactive_color, SK_AlphaOPAQUE);
  if (widget_) {
    // Set kTrackDefaultFrameColors to false to prevent clobbering the active
    // and inactive frame colors during theme changes.
    widget_->GetNativeWindow()->SetProperty(chromeos::kTrackDefaultFrameColors,
                                            false);
    widget_->GetNativeWindow()->SetProperty(chromeos::kFrameActiveColorKey,
                                            active_frame_color_);
    widget_->GetNativeWindow()->SetProperty(chromeos::kFrameInactiveColorKey,
                                            inactive_frame_color_);
  }
}

void ShellSurfaceBase::OnSetStartupId(const char* startup_id) {
  SetStartupId(startup_id);
}

void ShellSurfaceBase::OnSetApplicationId(const char* application_id) {
  SetApplicationId(application_id);
}

void ShellSurfaceBase::OnActivationRequested() {
  RequestActivation();
}

void ShellSurfaceBase::RequestActivation() {
  if (!IsReady()) {
    initially_activated_ = true;
    return;
  }
  if (widget_ && GetSecurityDelegate() &&
      GetSecurityDelegate()->CanSelfActivate(widget_->GetNativeWindow())) {
    this->Activate();
  }
}

void ShellSurfaceBase::RequestDeactivation() {
  if (!IsReady()) {
    initially_activated_ = false;
    return;
  }
  if (widget_ && GetSecurityDelegate() && IsReady() &&
      GetSecurityDelegate()->CanSelfActivate(widget_->GetNativeWindow())) {
    this->Deactivate();
  }
}

void ShellSurfaceBase::OnSetServerStartResize() {
  server_side_resize_ = true;
}

bool ShellSurfaceBase::IsReady() const {
  return widget_ && !pending_show_widget_;
}

////////////////////////////////////////////////////////////////////////////////
// SurfaceObserver overrides:

void ShellSurfaceBase::OnSurfaceDestroying(Surface* surface) {
  DCHECK_EQ(root_surface(), surface);
  surface->RemoveSurfaceObserver(this);
  SetRootSurface(nullptr);

  overlay_widget_.reset();

  // Hide widget before surface is destroyed. This allows hide animations to
  // run using the current surface contents.
  if (widget_) {
    // Remove transient children which are shell surfaces so they are not
    // automatically hidden.
    CloseAllShellSurfaceTransientChildren(widget_->GetNativeWindow());
    widget_->Hide();
  }

  // Note: In its use in the Wayland server implementation, the surface
  // destroyed callback may destroy the ShellSurface instance. This call needs
  // to be last so that the instance can be destroyed.
  if (!surface_destroyed_callback_.is_null())
    std::move(surface_destroyed_callback_).Run();
}

////////////////////////////////////////////////////////////////////////////////
// views::WidgetDelegate overrides:

bool ShellSurfaceBase::OnCloseRequested(
    views::Widget::ClosedReason close_reason) {
  if (!pre_close_callback_.is_null())
    pre_close_callback_.Run();
  // Closing the shell surface is a potentially asynchronous operation, so we
  // will defer actually closing the Widget right now, and come back and call
  // CloseNow() when the callback completes and the shell surface is destroyed
  // (see ~ShellSurfaceBase()).
  if (!close_callback_.is_null())
    close_callback_.Run();
  return false;
}

void ShellSurfaceBase::WindowClosing() {
  SetEnabled(false);
  if (widget_)
    widget_->RemoveObserver(this);
  widget_ = nullptr;
}

views::Widget* ShellSurfaceBase::GetWidget() {
  return widget_;
}

const views::Widget* ShellSurfaceBase::GetWidget() const {
  return widget_;
}

views::View* ShellSurfaceBase::GetContentsView() {
  return this;
}

views::ClientView* ShellSurfaceBase::CreateClientView(views::Widget* widget) {
  return new CustomClientView(widget, this);
}

std::unique_ptr<views::NonClientFrameView>
ShellSurfaceBase::CreateNonClientFrameView(views::Widget* widget) {
  return CreateNonClientFrameViewInternal(widget);
}

bool ShellSurfaceBase::ShouldSaveWindowPlacement() const {
  return !is_popup_ && !movement_disabled_;
}

bool ShellSurfaceBase::WidgetHasHitTestMask() const {
  return true;
}

void ShellSurfaceBase::GetWidgetHitTestMask(SkPath* mask) const {
  // If a window shape is applied set the hit test mask to the boundary path
  // of the masked region.
  if (shape_dp_) {
    shape_dp_->GetBoundaryPath(mask);
    return;
  }

  if (!HasHitTestRegion()) {
    return;
  }
  GetHitTestMask(mask);

  const float scale = GetScale();

  // `mask` should be in the Widget's coordinates, but the above
  // GetHitTestMask() call returns the mask in the root_surface's coordinates.
  // We need to offset the difference.
  auto widget_bounds = widget_->GetWindowBoundsInScreen().origin();
  auto root_surface_bounds =
      root_surface()->window()->GetBoundsInScreen().origin();
  auto offset = root_surface_bounds - widget_bounds.OffsetFromOrigin();

  SkMatrix matrix;
  matrix.setScaleTranslate(
      SkFloatToScalar(1.0f / scale), SkFloatToScalar(1.0f / scale),
      SkIntToScalar(offset.x()), SkIntToScalar(offset.y()));
  mask->transform(matrix);
}

void ShellSurfaceBase::OnCaptureChanged(aura::Window* lost_capture,
                                        aura::Window* gained_capture) {
  if (lost_capture != widget_->GetNativeWindow() || !is_popup_)
    return;

  // If the capture mode is active, do not close the popup to include it in a
  // screenshot.
  if (!views::ViewsDelegate::GetInstance()
           ->ShouldCloseMenuIfMouseCaptureLost()) {
    return;
  }

  WMHelper::GetInstance()->GetCaptureClient()->RemoveObserver(this);

  // Fast return for a simple case: if `lost_capture` is the parent of
  // `gained_capture`, do nothing.
  aura::Window* gained_capture_parent =
      gained_capture ? wm::GetTransientParent(gained_capture) : nullptr;
  if (lost_capture == gained_capture_parent)
    return;

  if (!gained_capture) {
    // If `gained_capture` is nullptr, find the closest ancestor of
    // `lost_capture` that is a popup with grab.
    for (aura::Window* next = wm::GetTransientParent(lost_capture);
         next != nullptr; next = wm::GetTransientParent(next)) {
      if (IsPopupWithGrab(next)) {
        gained_capture = next;
        break;
      }
    }
    // Give capture to the new `gained_capture`.
    if (gained_capture) {
      ShellSurfaceBase* parent_shell_surface =
          GetShellSurfaceBaseForWindow(gained_capture);
      parent_shell_surface->StartCapture();
    }
  }

  // Close any popup that satisfies the following conditions:
  // 1) it has grab, and it is not `gained_capture` or any of its ancestors; or
  // 2) descendants of any popup satisfying (1).
  //
  // Imagine there are the following popups:
  //
  //    popup_e
  //   (no grab)
  //       |
  //    popup_d
  // (has grab; lost_capture)
  //       |
  //    popup_c       popup_b
  //   (no grab) (has grab; gained_capture)
  //         \         /
  //          \       /
  //           popup_a
  //
  // In this case, popup_e and popup_d are the ones to close, in the order
  // from leaf to root.

  // Please note that `gained_capture_ancestors` also includes `gained_capture`.
  base::flat_set<aura::Window*> gained_capture_ancestors;
  for (aura::Window* next = gained_capture; next != nullptr;
       next = wm::GetTransientParent(next)) {
    gained_capture_ancestors.insert(next);
  }

  // BFS to collect all transient windows. The boolean field indicates whether
  // the corresponding window is a popup to be closed.
  std::vector<std::pair<aura::Window*, bool>> all;

  auto is_close_candidate_with_popup_grab =
      [&gained_capture_ancestors](aura::Window* window) {
        return IsPopupWithGrab(window) &&
               !base::Contains(gained_capture_ancestors, window);
      };

  aura::Window* root = wm::GetTransientRoot(lost_capture);
  all.emplace_back(root, is_close_candidate_with_popup_grab(root));

  // Use index instead of iterator because the vector grows during the
  // iteration.
  for (size_t i = 0; i < all.size(); ++i) {
    const std::vector<raw_ptr<aura::Window, VectorExperimental>>& children =
        wm::GetTransientChildren(all[i].first);
    for (aura::Window* child : children) {
      const bool to_close =
          all[i].second || is_close_candidate_with_popup_grab(child);
      all.emplace_back(child, to_close);
    }
  }

  // Traverse backwards so that popups are closed in the direction from leaf to
  // root.
  for (auto iter = all.rbegin(); iter != all.rend(); ++iter) {
    if (!iter->second)
      continue;

    ShellSurfaceBase* shell_surface =
        exo::GetShellSurfaceBaseForWindow(iter->first);
    DCHECK(shell_surface);
    shell_surface->widget_->Close();
  }
}

////////////////////////////////////////////////////////////////////////////////
// views::WidgetObserver overrides:

void ShellSurfaceBase::OnWidgetClosing(views::Widget* widget) {
  DCHECK(widget_ == widget);
  // To force the widget to close we first disconnect this shell surface from
  // its underlying surface, by asserting to it that the surface destroyed
  // itself. After that, it is safe to call CloseNow() on the widget.
  //
  // TODO(crbug.com/40651062): This only closes the aura/exo pieces, but we
  // should go one level deeper and destroy the wayland stuff. Some options:
  //  - Invoke xkill under-the-hood, which will only work for x11 and won't
  //    work if the container itself is stuck.
  //  - Close the wl connection to the client (i.e. wlkill) this is
  //    problematic with X11 as all of xwayland shares the same client.
  //  - Transitively kill all the wl_resources rooted at this window's
  //    wl_surface, which is not really supported in wayland.
  surface_destroyed_callback_.Reset();
  OnSurfaceDestroying(root_surface());
}

////////////////////////////////////////////////////////////////////////////////
// views::Views overrides:

gfx::Size ShellSurfaceBase::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  if (!geometry_.IsEmpty())
    return geometry_.size();

  // The root surface's content bounds should be used instead of the host window
  // bounds because the host window bounds are not updated until the widget is
  // committed, meaning that if we need to calculate the preferred size before
  // then (e.g. in OnPreWidgetCommit()), then we need to use the root surface's
  // to ensure that we're using the correct bounds' size.
  return root_surface()->surface_hierarchy_content_bounds().size();
}

gfx::Size ShellSurfaceBase::GetMinimumSize() const {
  return requested_minimum_size_.IsEmpty() ? gfx::Size(1, 1)
                                           : requested_minimum_size_;
}

gfx::Size ShellSurfaceBase::GetMaximumSize() const {
  // On ChromeOS, non empty maximum size will make the window
  // non maximizable.
  gfx::Size maximum_size = requested_maximum_size_;
  // Make sure that the max size is already equal to or greater than the min
  // size if set.
  if (!requested_minimum_size_.IsEmpty() &&
      !requested_maximum_size_.IsEmpty()) {
    maximum_size.SetToMax(requested_minimum_size_);
  }
  return maximum_size;
}

views::FocusTraversable* ShellSurfaceBase::GetFocusTraversable() {
  return overlay_widget_.get();
}

////////////////////////////////////////////////////////////////////////////////
// aura::WindowObserver overrides:

void ShellSurfaceBase::OnWindowDestroying(aura::Window* window) {
  surface_destroyed_callback_.Reset();
  if (!close_callback_.is_null()) {
    close_callback_.Run();
  }

  if (window == parent_)
    SetParentInternal(nullptr);
  window->RemoveObserver(this);
  if (IsShellSurfaceWindow(window) && root_surface()) {
    root_surface()->ThrottleFrameRate(false);
  }
}

void ShellSurfaceBase::OnWindowPropertyChanged(aura::Window* window,
                                               const void* key,
                                               intptr_t old_value) {
  if (!IsShellSurfaceWindow(window) || !root_surface()) {
    return;
  }

  if (key == aura::client::kSkipImeProcessing) {
    SetSkipImeProcessingToDescendentSurfaces(
        window, window->GetProperty(aura::client::kSkipImeProcessing));
  } else if (key == chromeos::kFrameRestoreLookKey) {
    root_surface()->SetFrameLocked(
        window->GetProperty(chromeos::kFrameRestoreLookKey));
  } else if (key == aura::client::kWindowWorkspaceKey) {
    root_surface()->OnDeskChanged(GetWindowDeskStateChanged(window));
  } else if (key == ash::kFrameRateThrottleKey) {
    root_surface()->ThrottleFrameRate(
        window->GetProperty(ash::kFrameRateThrottleKey));
  }
}

void ShellSurfaceBase::OnWindowAddedToRootWindow(aura::Window* window) {
  if (!IsShellSurfaceWindow(window)) {
    return;
  }
  UpdateDisplayOnTree();
}

void ShellSurfaceBase::OnWindowParentChanged(aura::Window* window,
                                             aura::Window* parent) {
  if (!IsShellSurfaceWindow(window) || !root_surface()) {
    return;
  }
  root_surface()->OnDeskChanged(GetWindowDeskStateChanged(window));
}

////////////////////////////////////////////////////////////////////////////////
// wm::ActivationChangeObserver overrides:

void ShellSurfaceBase::OnWindowActivated(ActivationReason reason,
                                         aura::Window* gained_active,
                                         aura::Window* lost_active) {
  if (!widget_)
    return;

  if (overlay_widget_ && overlay_widget_->widget_delegate()->CanActivate()) {
    aura::client::FocusClient* client =
        aura::client::GetFocusClient(widget_->GetNativeWindow());
    client->ResetFocusWithinActiveWindow(overlay_widget_->GetNativeWindow());
  }

  if (gained_active == widget_->GetNativeWindow() ||
      lost_active == widget_->GetNativeWindow()) {
    DCHECK(gained_active != widget_->GetNativeWindow() || CanActivate());
    UpdateShadow();
  }
}

////////////////////////////////////////////////////////////////////////////////
// wm::TooltipObserver overrides:

void ShellSurfaceBase::OnTooltipShown(aura::Window* target,
                                      const std::u16string& text,
                                      const gfx::Rect& bounds) {
  if (root_surface()) {
    root_surface()->OnTooltipShown(text, bounds);
  }
}

void ShellSurfaceBase::OnTooltipHidden(aura::Window* target) {
  if (root_surface()) {
    root_surface()->OnTooltipHidden();
  }
}

// Returns true if surface is currently being resized.
bool ShellSurfaceBase::IsDragged() const {
  if (in_extended_drag_)
    return true;

  if (!widget_)
    return false;

  ash::WindowState* window_state =
      ash::WindowState::Get(widget_->GetNativeWindow());
  return window_state ? window_state->is_dragged() : false;
}

////////////////////////////////////////////////////////////////////////////////
// ui::AcceleratorTarget overrides:

bool ShellSurfaceBase::AcceleratorPressed(const ui::Accelerator& accelerator) {
  for (const auto& entry : kCloseWindowAccelerators) {
    if (ui::Accelerator(entry.keycode, entry.modifiers) == accelerator) {
      OnCloseRequested(views::Widget::ClosedReason::kUnspecified);
      return true;
    }
  }
  return views::View::AcceleratorPressed(accelerator);
}

////////////////////////////////////////////////////////////////////////////////
// SurfaceTreeHost:
void ShellSurfaceBase::SetRootSurface(Surface* root_surface) {
  SurfaceTreeHost::SetRootSurface(root_surface);
  if (widget_) {
    SetShellRootSurface(widget_->GetNativeWindow(), root_surface);
  }
}

float ShellSurfaceBase::GetPendingScaleFactor() const {
  if (!host_window()->parent() && !HasDoubleBufferedPendingScaleFactor()) {
    // Before the initial commit, `host_window()` has not been a descendant of
    // the root window yet so we need to fetch the scale factor directly from
    // the pending target display.
    display::Display display;
    if (display::Screen::GetScreen()->GetDisplayWithDisplayId(
            pending_display_id_, &display)) {
      return display.device_scale_factor();
    }
  }
  return SurfaceTreeHost::GetPendingScaleFactor();
}

////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase, protected:

void ShellSurfaceBase::CreateShellSurfaceWidget(
    ui::WindowShowState show_state) {
  DCHECK(GetEnabled());
  DCHECK(!widget_);

  // Sommelier sets the null application id for override redirect windows,
  // which controls its bounds by itself.
  bool emulate_x11_override_redirect =
      !is_popup_ && parent_ && ash::desks_util::IsDeskContainerId(container_) &&
      !application_id_.has_value();

  if (emulate_x11_override_redirect) {
    // override redirect is used for menu, tooltips etc, which should be placed
    // above normal windows, but below lock screen. Specify the container here
    // to avoid using parent_ in params.parent.
    SetContainerInternal(ash::kShellWindowId_ShelfBubbleContainer);
    // X11 override redirect should not be activatable.
    activatable_ = false;
    DisableMovement();
  }

  if (system_modal_)
    SetModalType(ui::mojom::ModalType::kSystem);

  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET);
  params.type = (emulate_x11_override_redirect || is_menu_)
                    ? views::Widget::InitParams::TYPE_MENU
                    : (is_popup_ ? views::Widget::InitParams::TYPE_POPUP
                                 : views::Widget::InitParams::TYPE_WINDOW);

  params.delegate = this;
  params.shadow_type = views::Widget::InitParams::ShadowType::kNone;
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.show_state = show_state;

  if (initial_z_order_.has_value())
    params.z_order = initial_z_order_.value();

  if (initial_workspace_.has_value()) {
    const std::string kToggleVisibleOnAllWorkspacesValue = "-1";
    if (initial_workspace_ == kToggleVisibleOnAllWorkspacesValue) {
      // If |initial_workspace_| is -1, window is visible on all workspaces.
      params.visible_on_all_workspaces = true;
    } else {
      params.workspace = initial_workspace_.value();
    }
  }

  // Make shell surface a transient child if |parent_| has been set and
  // container_ isn't specified.
  aura::Window* root_window = ash::Shell::GetRootWindowForNewWindows();
  if (ash::desks_util::IsDeskContainerId(container_)) {
    DCHECK_EQ(ash::desks_util::GetActiveDeskContainerId(), container_);
    if (parent_)
      params.parent = parent_;
    else
      params.context = root_window;
  } else {
    params.parent = ash::Shell::GetContainer(root_window, container_);
  }
  if (initial_bounds_)
    params.bounds = *initial_bounds_;
  else
    params.bounds = gfx::Rect(origin_, gfx::Size());

  // This is called before CommitWidget:
  if (pending_display_id_ != display::kInvalidDisplayId) {
    params.display_id = pending_display_id_;
  }

  params.name = base::StringPrintf("ExoShellSurface-%d", shell_id++);

  WMHelper::AppPropertyResolver::Params property_resolver_params;
  if (application_id_)
    property_resolver_params.app_id = *application_id_;
  if (startup_id_)
    property_resolver_params.startup_id = *startup_id_;
  property_resolver_params.for_creation = true;
  WMHelper::GetInstance()->PopulateAppProperties(
      property_resolver_params, params.init_properties_container);

  SetShellApplicationId(&params.init_properties_container, application_id_);
  SetShellRootSurface(&params.init_properties_container, root_surface());
  SetShellStartupId(&params.init_properties_container, startup_id_);

  bool activatable = activatable_;

  // ShellSurfaces in system modal container are only activatable if input
  // region is non-empty. See OnCommitSurface() for more details.
  if (container_ == ash::kShellWindowId_SystemModalContainer)
    activatable &= HasHitTestRegion();
  // Transient child needs to have an application id to be activatable.
  if (parent_)
    activatable &= application_id_.has_value();
  params.activatable = activatable
                           ? views::Widget::InitParams::Activatable::kYes
                           : views::Widget::InitParams::Activatable::kNo;
  if (restore_session_id_) {
    params.init_properties_container.SetProperty(app_restore::kWindowIdKey,
                                                 *restore_session_id_);
  }
  if (restore_window_id_) {
    params.init_properties_container.SetProperty(
        app_restore::kRestoreWindowIdKey, *restore_window_id_);
  }
  if (restore_window_id_source_) {
    params.init_properties_container.SetProperty(
        app_restore::kRestoreWindowIdKey,
        app_restore::FetchRestoreWindowId(*restore_window_id_source_));
    params.init_properties_container.SetProperty(
        app_restore::kAppIdKey, restore_window_id_source_.value());
  }

  params.init_properties_container.SetProperty(wm::kPersistableKey,
                                               persistable_);

  // Restore `params` to those of the saved `restore_window_id_`.
  app_restore::ModifyWidgetParams(params.init_properties_container.GetProperty(
                                      app_restore::kRestoreWindowIdKey),
                                  &params);

  // If app restore specifies the initial bounds, set `initial_bounds_` to it so
  // that shell surface knows the initial bounds is set.
  if (!params.bounds.IsEmpty() && !initial_bounds_)
    initial_bounds_.emplace(params.bounds);

  OverrideInitParams(&params);

  // Note: NativeWidget owns this widget.
  widget_ = new ShellSurfaceWidget;
  widget_->Init(std::move(params));
  widget_->AddObserver(this);

  // As setting the pinned mode may have come in earlier we apply it now.
  UpdatePinned();

  UpdateTopInset();

  aura::Window* window = widget_->GetNativeWindow();
  {
    // AddChild involves propagating a non-1 device_scale_factor to
    // `host_window()`. Changing device_scale_factor this way does not send
    // configure events. So suppress allocation of its LocalSurfaceId.
    viz::ScopedSurfaceIdAllocator scoped_suppression =
        host_window()->GetSurfaceIdAllocator(base::NullCallback());
    window->AddChild(host_window());
  }
  window->SetEventTargetingPolicy(
      aura::EventTargetingPolicy::kTargetAndDescendants);
  if (is_menu_) {
    // Sets menu config id to kGroupingPropertyKey if the window is menu.
    window->SetNativeWindowProperty(
        views::TooltipManager::kGroupingPropertyKey,
        reinterpret_cast<void*>(views::MenuConfig::kMenuControllerGroupingId));
  }
  InstallCustomWindowTargeter();

  // TODO(fangzhoug): Consider performing the first shell_surface configure here
  // s.t. there's no gap between the first configure to the point we start
  // observing states of the window. crbug.com/1505583
  // Start tracking changes to window bounds and window state.
  window->AddObserver(this);
  ash::WindowState* window_state = ash::WindowState::Get(window);
  // Skip initializing window state when `window_state` is null.
  // This happesn when the window type is popup.
  if (window_state) {
    InitializeWindowState(window_state);
  }

  SetShellUseImmersiveForFullscreen(window, immersive_implied_by_fullscreen_);

  // Fade visibility animations for non-activatable windows.
  if (!CanActivate()) {
    wm::SetWindowVisibilityAnimationType(
        window, wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
  }

  // Register close window accelerators.
  views::FocusManager* focus_manager = widget_->GetFocusManager();
  for (const auto& entry : kCloseWindowAccelerators) {
    focus_manager->RegisterAccelerator(
        ui::Accelerator(entry.keycode, entry.modifiers),
        ui::AcceleratorManager::kNormalPriority, this);
  }

  // Show widget next time Commit() is called.
  pending_show_widget_ = true;

  UpdateDisplayOnTree();

  if (frame_type_ != SurfaceFrameType::NONE)
    OnSetFrame(frame_type_);

  if (pending_pip_)
    SetPip();

  root_surface()->OnDeskChanged(GetWindowDeskStateChanged(window));
  root_surface()->OnFullscreenStateChanged(widget_->IsFullscreen());

  WMHelper::GetInstance()->NotifyExoWindowCreated(widget_->GetNativeWindow());
}

bool ShellSurfaceBase::IsShellSurfaceWindow(const aura::Window* window) const {
  return widget_ && window == widget_->GetNativeWindow();
}

ShellSurfaceBase::OverlayParams::OverlayParams(
    std::unique_ptr<views::View> overlay)
    : contents_view(std::move(overlay)) {}
ShellSurfaceBase::OverlayParams::~OverlayParams() = default;

bool ShellSurfaceBase::IsResizing() const {
  ash::WindowState* window_state =
      ash::WindowState::Get(widget_->GetNativeWindow());
  if (!window_state || !window_state->is_dragged())
    return false;
  return window_state->drag_details() &&
         (window_state->drag_details()->bounds_change &
          ash::WindowResizer::kBoundsChange_Resizes);
}

gfx::Rect ShellSurfaceBase::ComputeAdjustedBounds(
    const gfx::Rect& bounds) const {
  return bounds;
}

void ShellSurfaceBase::UpdateWidgetBounds() {
  DCHECK(widget_);
  std::optional<gfx::Rect> bounds = GetWidgetBounds();
  if (!bounds) {
    return;
  }

  ash::WindowState* window_state =
      ash::WindowState::Get(widget_->GetNativeWindow());
  gfx::Rect adjusted_bounds = ComputeAdjustedBounds(*bounds);

  bool should_update_widget_bounds = bounds_is_dirty() ||
                                     adjusted_bounds != *bounds ||
                                     (window_state && window_state->IsPip());

  set_bounds_is_dirty(false);

  if (!should_update_widget_bounds) {
    return;
  }

  if (overlay_widget_) {
    gfx::Rect content_bounds(adjusted_bounds.size());
    int height = 0;
    if (!overlay_overlaps_frame_ && frame_enabled()) {
      auto* frame_view = static_cast<const ash::NonClientFrameViewAsh*>(
          widget_->non_client_view()->frame_view());
      height = frame_view->NonClientTopBorderHeight();
    }
    content_bounds.set_height(content_bounds.height() - height);
    content_bounds.set_y(height);
    overlay_widget_->SetBounds(content_bounds);
  }

  aura::Window* window = widget_->GetNativeWindow();
  // Return early if the shell is currently managing the bounds of the widget.
  if (window_state && !window_state->allow_set_bounds_direct()) {
    // 1) When a window is either maximized/fullscreen/pinned.
    if (window_state->IsMaximizedOrFullscreenOrPinned())
      return;
    // 2) When a window is snapped.
    if (window_state->IsSnapped())
      return;
    // 3) When a window is being interactively resized.
    if (IsResizing())
      return;
    // 4) When a window's bounds are being animated.
    if (window->layer()->GetAnimator()->IsAnimatingProperty(
            ui::LayerAnimationElement::BOUNDS))
      return;
  }

  SetWidgetBounds(adjusted_bounds, adjusted_bounds != *bounds);
}

void ShellSurfaceBase::UpdateHostWindowOrigin() {
  // There's an animation happening on cloned layers, `host_window()` layer may
  // be "ahead" of client's commit_target_layer. Do not update its origin,
  // instead, rely on SurfaceLayer stretching until the client catches up.
  if (GetCommitTargetLayer() != host_window()->layer()) {
    return;
  }
  gfx::Point origin = GetClientViewBounds().origin();

  origin += GetSurfaceOrigin().OffsetFromOrigin();
  // As `origin` is in DP here, it eventually needs to be converted to pixels.
  // We need to make subpixel adjustment so the `origin` in pixel coordinates
  // will align exactly to a pixel boundary. Here, we calculate the closest
  // pixel boundary by converting to pixels and rounding the original DP value.
  // Note that this shouldn't take `scaled_root_origin` into account as its
  // original value is in pixels and it needs a different type of subpixel
  // adjustment (i.e. preserving the original pixel distance between two
  // points).
  const gfx::Vector2dF surface_origin_subpixel_offset =
      ScaleVector2d(ToRoundedVector2d(ScaleVector2d(origin.OffsetFromOrigin(),
                                                    GetScaleFactor())),
                    1.f / GetScaleFactor()) -
      origin.OffsetFromOrigin();

  const gfx::Vector2dF root_surface_origin_dp = ScaleVector2d(
      root_surface_origin_pixel().OffsetFromOrigin(), 1.f / GetScaleFactor());
  origin -= ToFlooredVector2d(root_surface_origin_dp);
  // Subpixel offset used to adjust the offset of `root_origin` so it will
  // exactly match the original value in pixels.
  const gfx::Vector2dF root_surface_origin_subpixel_offset =
      ToFlooredVector2d(root_surface_origin_dp) - root_surface_origin_dp;

  // Two offsets can be simply added together because
  // `surface_origin_subpixel_offset` is used for shifting the origin on a pixel
  // boundary while `root_surface_origin_subpixel_offset` just ensures that the
  // root surface origin stays the same value in pixel while scrolling when a
  // sub surface moves (e.g. by scrolling) but the actual value it's preserving
  // doesn't matter.
  host_window()->layer()->SetSubpixelPositionOffset(
      surface_origin_subpixel_offset + root_surface_origin_subpixel_offset);

  if (origin != host_window()->bounds().origin()) {
    AllocateLocalSurfaceId();
  }

  gfx::Rect surface_bounds(origin, host_window()->bounds().size());
  if (host_window()->bounds() == surface_bounds)
    return;
  // This may not be necessary
  set_bounds_is_dirty(true);
  host_window()->SetBounds(surface_bounds);
  UpdateHostWindowOpaqueRegion();
}

void ShellSurfaceBase::UpdateShadow() {
  if (!widget_ || !root_surface())
    return;

  aura::Window* window = widget_->GetNativeWindow();

  // Window shadows should be disabled if a window shape has been set.
  //
  // Or if `host_window()`'s layer is not commit_target_layer, `shadow_bounds_`
  // committed by the client should not go to current `widget_`'s shadow, but to
  // the old widget's shadow prior to layer clone. Don't show the new shadow for
  // now.
  // TODO(crbug.com/40285156): Find the old widget's shadow layer and update it,
  // and maybe show new widget's shadow by predicting its dimensions.
  int shadow_elevation = wm::kShadowElevationDefault;
  if (!shadow_bounds_ || shape_dp_.has_value() ||
      GetCommitTargetLayer() != host_window()->layer()) {
    shadow_elevation = wm::kShadowElevationNone;
  } else if (frame_type_ == SurfaceFrameType::SHADOW && is_popup_) {
    shadow_elevation = wm::kShadowElevationMenuOrTooltip;
  }
  wm::SetShadowElevation(window, shadow_elevation);

  // A window may not have a shadow object if the window was created in
  // maximized/fullscreen state.
  ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);
  if (shadow && shadow_elevation != wm::kShadowElevationNone) {
    gfx::Rect shadow_bounds = GetShadowBounds();
    gfx::Point origin = GetClientViewBounds().origin();

    if (!window->GetProperty(aura::client::kUseWindowBoundsForShadow)) {
      origin += GetSurfaceOrigin().OffsetFromOrigin();
      if (origin.x() != 0 || origin.y() != 0) {
        shadow_bounds.set_origin(origin);
        if (widget_) {
          gfx::Point widget_origin_in_root =
              widget_->GetNativeWindow()->bounds().origin();
          origin += ToFlooredVector2d(
              ScaleVector2d(gfx::Vector2d(widget_origin_in_root.x(),
                                          widget_origin_in_root.y()),
                            1.f / GetScale()));
          gfx::Rect bounds = geometry_;
          bounds.set_origin(origin);
          ash::Shell::Get()
              ->resize_shadow_controller()
              ->UpdateResizeShadowBoundsOfWindow(widget_->GetNativeWindow(),
                                                 bounds);
        }
      }
    }

    shadow->SetContentBounds(shadow_bounds);

    // Surfaces that can't be activated are usually menus and tooltips. Use a
    // small style shadow for them.
    if (!CanActivate())
      shadow->SetElevation(wm::kShadowElevationMenuOrTooltip);

    UpdateShadowRoundedCorners();
  }

  if (window->layer()->type() == ui::LAYER_NOT_DRAWN) {
    DCHECK(!window->GetProperty(chromeos::kWindowManagerManagesOpacityKey));

    // Snapped window should not be opaque because it can be drag-resized, in
    // which case the widget's window can be exposed while waiting for
    // configure_ack + commit.
    bool window_is_opaque = widget_->IsFullscreen() || widget_->IsMaximized();
    window->SetTransparent(!window_is_opaque);
    if (root_surface()->FillsBoundsOpaquely()) {
      // Manually control occlusion, but do not make the window
      // opaque as the host window may not be at the same size unless the
      // window state is either in fullscreen or maximized.
      window->SetOpaqueRegionsForOcclusion(
          {gfx::Rect(window->bounds().size())});
    } else {
      window->SetOpaqueRegionsForOcclusion({});
    }
  }
}

void ShellSurfaceBase::UpdateShadowRoundedCorners() {
  if (!widget_) {
    return;
  }

  shadow_corners_radii_dp_ = pending_shadow_corners_radii_dp_;

  aura::Window* window = widget_->GetNativeWindow();
  ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);

  if (!shadow) {
    return;
  }

  gfx::RoundedCornersF shadow_radii;

  const ash::WindowState* window_state = ash::WindowState::Get(window);
  if (window_state && window_state->IsPip()) {
    shadow_radii = gfx::RoundedCornersF(chromeos::kPipRoundedCornerRadius);
  } else if (chromeos::features::IsRoundedWindowsEnabled() &&
             (shadow_corners_radii_dp_.has_value() ||
              window_corners_radii_dp_.has_value())) {
    // For backward version compatibility, fallback to use the window radii if
    // the shadow radii is not specified.
    // TODO(crbug.com/40256581): Revisit once all the clients have migrated.
    shadow_radii = shadow_corners_radii_dp_.value_or(
        window_corners_radii_dp_.value_or(gfx::RoundedCornersF()));
  }

  // TODO(crbug.com/40256581): Support shadow with variable radius corners.
  shadow->SetRoundedCornerRadius(shadow_radii.upper_left());
}

void ShellSurfaceBase::UpdateFrameType() {
  // Nothing to do here for now as frame type is updated immediately in
  // OnSetFrame() by default.
}

void ShellSurfaceBase::UpdateWindowRoundedCorners() {
  // If non_client_view is not available, it means that widget_ is neither a
  // normal window or a bubble. Therefore it should not have any decorations
  // including a rounded window.
  if (!widget_ || !widget_->non_client_view()) {
    DCHECK(widget_ && !pending_window_corners_radii_dp_);
    // It is possible to get here before the widget has actually been created.
    // The state will be set once the widget gets created.
    return;
  }

  window_corners_radii_dp_ = pending_window_corners_radii_dp_;
  widget_->non_client_view()->frame_view()->UpdateWindowRoundedCorners();
}

gfx::Rect ShellSurfaceBase::GetVisibleBounds() const {
  // Use |geometry_| if set, otherwise use the visual bounds of the surface.
  if (geometry_.IsEmpty()) {
    gfx::Size size;
    if (root_surface()) {
      float int_part;
      DCHECK(std::modf(root_surface()->content_size().width(), &int_part) ==
                 0.0f &&
             std::modf(root_surface()->content_size().height(), &int_part) ==
                 0.0f);
      size = gfx::ToCeiledSize(root_surface()->content_size());
      if (client_submits_surfaces_in_pixel_coordinates()) {
        float dsf = host_window()->layer()->device_scale_factor();
        size = gfx::ScaleToRoundedSize(size, 1.0f / dsf);
      }
    }
    return gfx::Rect(size);
  }

  return geometry_;
}

gfx::Rect ShellSurfaceBase::GetClientViewBounds() const {
  return (widget_->non_client_view() && !frame_overlapped())
             ? widget_->non_client_view()
                   ->frame_view()
                   ->GetBoundsForClientView()
             // When frame is overlapped with client area, window bounds is the
             // same as client bounds.
             : gfx::Rect(widget_->GetWindowBoundsInScreen().size());
}

gfx::Rect ShellSurfaceBase::GetWidgetBoundsFromVisibleBounds() const {
  auto visible_bounds = GetVisibleBounds();
  return widget_->non_client_view()
             ? widget_->non_client_view()->GetWindowBoundsForClientBounds(
                   visible_bounds)
             : visible_bounds;
}

gfx::Rect ShellSurfaceBase::GetShadowBounds() const {
  return shadow_bounds_->IsEmpty()
             ? gfx::Rect(widget_->GetNativeWindow()->bounds().size())
             : gfx::ScaleToEnclosedRect(*shadow_bounds_, 1.f / GetScale());
}

void ShellSurfaceBase::InstallCustomWindowTargeter() {
  aura::Window* window = widget_->GetNativeWindow();
  window->SetEventTargeter(std::make_unique<CustomWindowTargeter>(this));
}

std::unique_ptr<views::NonClientFrameView>
ShellSurfaceBase::CreateNonClientFrameViewInternal(views::Widget* widget) {
  aura::Window* window = widget_->GetNativeWindow();
  // ShellSurfaces always use immersive mode.
  window->SetProperty(chromeos::kImmersiveIsActive, true);
  ash::WindowState* window_state = ash::WindowState::Get(window);
  if (!frame_enabled() && window_state && !window_state->HasDelegate()) {
    window_state->SetDelegate(std::make_unique<CustomWindowStateDelegate>());
  }
  auto frame_view =
      std::make_unique<CustomFrameView>(widget, this, frame_enabled());
  if (has_frame_colors_)
    frame_view->SetFrameColors(active_frame_color_, inactive_frame_color_);
  return frame_view;
}

bool ShellSurfaceBase::ShouldExitFullscreenFromRestoreOrMaximized() {
  if (widget_ && widget_->GetNativeWindow()) {
    return widget_->GetNativeWindow()->GetProperty(
        kRestoreOrMaximizeExitsFullscreen);
  }
  return false;
}

////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase, private:

float ShellSurfaceBase::GetScale() const {
  return 1.f;
}

void ShellSurfaceBase::StartCapture() {
  widget_->set_auto_release_capture(false);
  WMHelper::GetInstance()->GetCaptureClient()->AddObserver(this);
  // Just capture on the window.
  widget_->SetCapture(nullptr /* view */);
}

void ShellSurfaceBase::OnPostWidgetCommit() {
  // |shadow_bounds_changed_| represents whether |shadow_bounds_| has changed
  // since the last commit, but as UpdateShadow() can be called multiple times
  // in a single commit process, we need to ensure that it's not reset halfway
  // in the current commit by resetting it here.
  shadow_bounds_changed_ = false;

  UpdateTopInset();
}

void ShellSurfaceBase::ShowWidget(bool activate) {
  if (activate) {
    // Widget will minimize itself if the initial state is minimized.
    widget_->Show();
  } else {
    widget_->ShowInactive();
  }
}

void ShellSurfaceBase::SetContainerInternal(int container) {
  container_ = container;
  WidgetDelegate::SetCanMaximize(
      !parent_ && ash::desks_util::IsDeskContainerId(container_));
  WidgetDelegate::SetCanFullscreen(
      !parent_ && ash::desks_util::IsDeskContainerId(container_));
  if (widget_)
    widget_->OnSizeConstraintsChanged();
}

void ShellSurfaceBase::SetParentInternal(aura::Window* parent) {
  parent_ = parent;
  WidgetDelegate::SetCanMinimize(!parent_ && can_minimize_);
  UpdateResizability();
  if (widget_)
    widget_->OnSizeConstraintsChanged();
}

bool ShellSurfaceBase::CalculateCanResize() const {
  if (overlay_widget_ && overlay_can_resize_.has_value())
    return *overlay_can_resize_;
  return !movement_disabled_ && GetCanResizeFromSizeConstraints();
}

void ShellSurfaceBase::CommitWidget() {
  bool size_constraint_changed =
      requested_minimum_size_ != pending_minimum_size_ ||
      requested_maximum_size_ != pending_maximum_size_;
  set_bounds_is_dirty(
      bounds_is_dirty() || origin_ != pending_geometry_.origin() ||
      geometry_ != pending_geometry_ || display_id_ != pending_display_id_ ||
      size_constraint_changed);

  // Apply new window geometry.
  geometry_ = pending_geometry_;
  display_id_ = pending_display_id_;
  shape_dp_ = pending_shape_dp_;

  // Apply new minimum/maximum size.
  requested_minimum_size_ = pending_minimum_size_;
  requested_maximum_size_ = pending_maximum_size_;
  UpdateResizability();

  if (!widget_)
    return;

  if (!pending_aspect_ratio_.IsEmpty()) {
    widget_->SetAspectRatio(pending_aspect_ratio_);
  } else if (widget_->GetNativeWindow()) {
    // TODO(yoshiki): Move the logic to clear aspect ratio into view::Widget.
    widget_->GetNativeWindow()->ClearProperty(aura::client::kAspectRatio);
  }

  // The calling order matters. The frame type has to be updated before
  // calculating the bounds because the bounds computation depends on the frame
  // type (e.g. caption height).
  UpdateFrameType();
  UpdateWidgetBounds();
  UpdateSurfaceLayerSizeAndRootSurfaceOrigin();

  // System modal container is used by clients to implement overlay
  // windows using a single ShellSurface instance.  If hit-test
  // region is empty, then it is non interactive window and won't be
  // activated.
  if (container_ == ash::kShellWindowId_SystemModalContainer) {
    // Prevent window from being activated when hit test region is empty.
    bool activatable = activatable_ && HasHitTestRegion();
    if (activatable != CanActivate()) {
      SetCanActivate(activatable);
      // Activate or deactivate window if activation state changed.
      if (activatable) {
        // Automatically activate only if the window is modal.
        // Non modal window should be activated by a user action.
        // TODO(oshima): Non modal system window does not have an associated
        // task ID, and as a result, it cannot be activated from client side.
        // Fix this (b/65460424) and remove this if condition.
        if (system_modal_)
          wm::ActivateWindow(widget_->GetNativeWindow());
      } else if (widget_->IsActive()) {
        wm::DeactivateWindow(widget_->GetNativeWindow());
      }
    }
  }

  UpdateHostWindowOrigin();
  UpdateShape();

  gfx::Rect bounds = geometry_;
  if (!bounds.IsEmpty() && !widget_->GetNativeWindow()->GetProperty(
                               aura::client::kUseWindowBoundsForShadow)) {
    SetBoundsForShadows(std::make_optional(bounds));
  }

  // The calling order matters. Updated window radius is need to correctly
  // update the radius of the shadow.
  UpdateWindowRoundedCorners();
  UpdateShadow();

  // Don't show yet if the shell surface doesn't have content.
  bool should_show = !host_window()->bounds().IsEmpty();

  // Do not layout the window if the position should not be controlled by window
  // manager. (popup, emulating x11 override direct, or requested not to move)
  if (is_popup_ || movement_disabled_)
    needs_layout_on_show_ = false;

  // Do not center if the initial bounds is set.
  if (initial_bounds_)
    needs_layout_on_show_ = false;

  // Show widget if needed.
  if (pending_show_widget_ && should_show) {
    DCHECK(!widget_->IsClosed());
    DCHECK(!widget_->IsVisible());
    pending_show_widget_ = false;

    auto* window = widget_->GetNativeWindow();
    auto* window_state = ash::WindowState::Get(window);

    // TODO(oshima): This should be set to the
    // `views::Widget::InitParams.bounds`
    if (window_state && window_state->IsMaximizedOrFullscreenOrPinned() &&
        (!initial_bounds_ || initial_bounds_->IsEmpty())) {
      gfx::Size current_content_size = CalculatePreferredSize({});
      gfx::Rect restore_bounds = display::Screen::GetScreen()
                                     ->GetDisplayNearestWindow(window)
                                     .work_area();
      if (!current_content_size.IsEmpty())
        restore_bounds.ClampToCenteredSize(current_content_size);

      window_state->SetRestoreBoundsInScreen(restore_bounds);
    }

    // TODO(crbug.com/40212799): Hook this up with the WM's window positioning
    // logic.
    if (needs_layout_on_show_) {
      widget_->CenterWindow(GetWidgetBoundsFromVisibleBounds().size());
      needs_layout_on_show_ = false;
    }

    if (restore_window_id_.has_value()) {
      ash::LoginUnlockThroughputRecorder* throughput_recorder =
          ash::Shell::Get()->login_unlock_throughput_recorder();

      aura::Window* root_window = host_window()->GetRootWindow();
      if (root_window) {
        ui::Compositor* compositor = root_window->layer()->GetCompositor();
        throughput_recorder->OnBeforeRestoredWindowShown(
            restore_window_id_.value(), compositor);
      }
    }

    ShowWidget(initially_activated_);

    if (has_grab_)
      StartCapture();

    if (container_ == ash::kShellWindowId_SystemModalContainer)
      UpdateSystemModal();
  }

  if (size_constraint_changed)
    widget_->OnSizeConstraintsChanged();
}

bool ShellSurfaceBase::IsFrameDecorationSupported(SurfaceFrameType frame_type) {
  if (!is_popup_)
    return true;

  // Popup doesn't support frame types other than NONE/SHADOW.
  return frame_type == SurfaceFrameType::SHADOW ||
         frame_type == SurfaceFrameType::NONE;
}

void ShellSurfaceBase::SetOrientationLock(
    chromeos::OrientationType orientation_lock) {
  TRACE_EVENT1("exo", "ShellSurfaceBase::SetOrientationLock",
               "orientation_lock", static_cast<int>(orientation_lock));

  if (!widget_) {
    initial_orientation_lock_ = orientation_lock;
    return;
  }

  ash::Shell* shell = ash::Shell::Get();
  shell->screen_orientation_controller()->LockOrientationForWindow(
      widget_->GetNativeWindow(), orientation_lock);
}

void ShellSurfaceBase::SetZOrder(ui::ZOrderLevel z_order) {
  // If there is already a widget, we can immediately set its z order.
  if (widget_) {
    widget_->SetZOrderLevel(z_order);
    return;
  }

  // Otherwise, we want to save `z_order` for when `widget_` is initialized.
  initial_z_order_ = z_order;
}

void ShellSurfaceBase::SetShape(std::optional<cc::Region> shape) {
  if (!shape) {
    pending_shape_dp_.reset();
    return;
  }

  if (frame_enabled()) {
    LOG(ERROR) << "SetShape() is not supported for windows with frame enabled.";
    return;
  }

  // SetShape() may be called some time after a window has been created. In case
  // server_side_resize_ has been set we disable it here.
  server_side_resize_ = false;

  // Although window shape is only supported for frameless windows we must also
  // ensure window shadows are disabled as shadows can contribute to the widget
  // window's layer bounds.
  // TODO(crbug.com/40276217): This will not be necessary once the
  // implementation is updated to use the root surface's geometry.
  OnSetFrame(SurfaceFrameType::NONE);

  pending_shape_dp_ = std::move(shape);
}

// static
bool ShellSurfaceBase::IsPopupWithGrab(aura::Window* window) {
  ShellSurfaceBase* shell_surface = exo::GetShellSurfaceBaseForWindow(window);
  if (shell_surface && shell_surface->has_grab_) {
    DCHECK(shell_surface->is_popup_);
    return true;
  }

  return false;
}

}  // namespace exo