chromium/ash/wm/float/float_controller.cc

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

#include "ash/wm/float/float_controller.h"

#include <algorithm>
#include <cstddef>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/rotator/screen_rotation_animator.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/tablet_mode_tuck_education.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/scoped_window_tucker.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_default_layout_manager.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace/workspace_event_handler.h"
#include "ash/wm/workspace/workspace_layout_manager.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/wm/constants.h"
#include "chromeos/ui/wm/window_util.h"
#include "components/app_restore/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_observer.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/scoped_animation_disabler.h"

namespace ash {

namespace {

// The ideal dimensions of a clamshell floated window before factoring in its
// minimum size (if any) is the available work area multiplied by these ratios.
constexpr float kFloatedWindowClamshellWidthRatio = 1.f / 3.f;
constexpr float kFloatedWindowClamshellHeightRatio = 0.7f;

constexpr char kFloatWindowCountsPerSessionHistogramName[] =
    "Ash.Float.FloatWindowCountsPerSession";
constexpr char kFloatWindowDurationHistogramName[] =
    "Ash.Float.FloatWindowDuration";
constexpr char kFloatWindowMoveToAnotherDeskCountsHistogramName[] =
    "Ash.Float.FloatWindowMoveToAnotherDeskCounts";

// Disables the window's position auto management and returns its original
// value.
bool DisableAndGetOriginalPositionAutoManaged(aura::Window* window) {
  auto* window_state = WindowState::Get(window);
  const bool original_position_auto_managed =
      window_state->GetWindowPositionManaged();
  // Floated window position should not be auto-managed.
  if (original_position_auto_managed)
    window_state->SetWindowPositionManaged(false);
  return original_position_auto_managed;
}

// Updates `window`'s bounds while in tablet mode, using the given
// `animation_type`. Called after a drag is completed, switching between
// clamshell to tablet, and to tuck and untuck the window.
void UpdateWindowBoundsForTablet(
    aura::Window* window,
    WindowState::BoundsChangeAnimationType animation_type) {
  WindowState* window_state = WindowState::Get(window);
  DCHECK(window_state);
  // TODO(b/264962634): Remove this workaround.
  // Currently `TabletModeWindowState::UpdateWindowPosition` uses
  // `Window::SetBoundsDirect` which directly changes the bounds without waiting
  // for the ack from clients (e.g. ARC++). So we need to ensure to emit
  // `SetBoundsWMEvent` instead. Otherwise, the window bounds are updated only
  // in Chrome-side whereas ARC++ doesn’t know the changes. (See comments in
  // `TabletModeWindowState::UpdateWindowPosition`.)
  if (window_state->is_client_controlled()) {
    // If any animation is requested, it will directly animate the
    // client-controlled windows for a rich animation. The client bounds change
    // will follow.
    if (animation_type != WindowState::BoundsChangeAnimationType::kNone) {
      TabletModeWindowState::UpdateWindowPosition(window_state, animation_type);
    }
    const SetBoundsWMEvent event(
        TabletModeWindowState::GetBoundsInTabletMode(window_state),
        /*animate=*/animation_type !=
            WindowState::BoundsChangeAnimationType::kNone);
    window_state->OnWMEvent(&event);
    return;
  }
  TabletModeWindowState::UpdateWindowPosition(window_state, animation_type);
}

// Hides the given floated window.
void HideFloatedWindow(aura::Window* floated_window) {
  // Disable the window animation here, because during desk deactivation we
  // are taking a screenshot of the desk (used for desk switch animations.)
  // while the `Hide()` animation is still in progress, and this will
  // introduce a glitch.
  DCHECK(floated_window);
  wm::ScopedAnimationDisabler disabler(floated_window);
  floated_window->Hide();
}

// Shows the given floated window.
void ShowFloatedWindow(aura::Window* floated_window) {
  DCHECK(floated_window);
  if (floated_window->IsVisible()) {
    return;
  }

  wm::ScopedAnimationDisabler disabler(floated_window);
  floated_window->Show();
}

gfx::Rect GetFloatBounds(const gfx::Size& size,
                         const gfx::Rect& work_area_bounds,
                         chromeos::FloatStartLocation location) {
  const int padding_dp = chromeos::wm::kFloatedWindowPaddingDp;
  int origin_x;
  const int origin_y = work_area_bounds.bottom() - size.height() - padding_dp;
  switch (location) {
    case chromeos::FloatStartLocation::kBottomLeft: {
      origin_x = padding_dp;
      break;
    }
    case chromeos::FloatStartLocation::kBottomRight: {
      origin_x = work_area_bounds.right() - size.width() - padding_dp;
      break;
    }
  }
  return gfx::Rect(gfx::Point(origin_x, origin_y), size);
}

class FloatLayoutManager : public WmDefaultLayoutManager {
 public:
  FloatLayoutManager() = default;
  FloatLayoutManager(const FloatLayoutManager&) = delete;
  FloatLayoutManager& operator=(const FloatLayoutManager&) = delete;
  ~FloatLayoutManager() override = default;

  // WmDefaultLayoutManager:
  void OnWindowAddedToLayout(aura::Window* child) override {
    // We don't support multiple displays in tablet mode, so this function is
    // called when the window is moved into the float container from a desk
    // container. This happens during a state change, and we can let the
    // transition event handle setting the floated window bounds instead.
    if (Shell::Get()->IsInTabletMode()) {
      return;
    }

    WindowState* window_state = WindowState::Get(child);
    WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
    window_state->OnWMEvent(&event);
  }

  void OnWillRemoveWindowFromLayout(aura::Window* child) override {
    // Same as what we are doing inside
    // `WorkspaceLayoutManager::OnWillRemoveWindowFromLayout` for this. But we
    // need to do it separately here as `WorkspaceLayoutManager` is not tracking
    // the float container.
    WindowState::Get(child)->set_pre_added_to_workspace_window_bounds(
        child->bounds());
  }

  void SetChildBounds(aura::Window* child,
                      const gfx::Rect& requested_bounds) override {
    // This should result in sending a bounds change WMEvent to properly support
    // client-controlled windows (e.g. ARC++).
    WindowState* window_state = WindowState::Get(child);
    SetBoundsWMEvent event(requested_bounds);
    window_state->OnWMEvent(&event);
  }
};

class FloatScopedWindowTuckerDelegate : public ScopedWindowTucker::Delegate {
 public:
  FloatScopedWindowTuckerDelegate() = default;
  FloatScopedWindowTuckerDelegate(const FloatScopedWindowTuckerDelegate&) =
      delete;
  FloatScopedWindowTuckerDelegate& operator=(
      const FloatScopedWindowTuckerDelegate&) = delete;
  ~FloatScopedWindowTuckerDelegate() override = default;

  void PaintTuckHandle(gfx::Canvas* canvas, int width, bool left) override {
    // Flip the canvas horizontally for `left` tuck handle.
    if (left) {
      canvas->Translate(gfx::Vector2d(width, 0));
      canvas->Scale(-1, 1);
    }

    // We draw three icons on top of each other because we need separate
    // themeing on different parts which is not supported by `VectorIcon`.
    const bool dark_mode =
        DarkLightModeControllerImpl::Get()->IsDarkModeEnabled();

    // Paint the container bottom layer with default 80% opacity.
    SkColor color = dark_mode ? gfx::kGoogleGrey500 : gfx::kGoogleGrey600;
    const SkColor bottom_color =
        SkColorSetA(color, std::round(SkColorGetA(color) * 0.8f));

    const gfx::ImageSkia& tuck_container_bottom = gfx::CreateVectorIcon(
        kTuckHandleContainerBottomIcon, ScopedWindowTucker::kTuckHandleWidth,
        bottom_color);
    canvas->DrawImageInt(tuck_container_bottom, 0, 0);

    // Paint the container top layer. This is mostly transparent, with 12%
    // opacity.
    color = dark_mode ? gfx::kGoogleGrey200 : gfx::kGoogleGrey600;
    const SkColor top_color =
        SkColorSetA(color, std::round(SkColorGetA(color) * 0.12f));
    const gfx::ImageSkia& tuck_container_top =
        gfx::CreateVectorIcon(kTuckHandleContainerTopIcon,
                              ScopedWindowTucker::kTuckHandleWidth, top_color);
    canvas->DrawImageInt(tuck_container_top, 0, 0);

    const gfx::ImageSkia& tuck_icon = gfx::CreateVectorIcon(
        kTuckHandleChevronIcon, ScopedWindowTucker::kTuckHandleWidth,
        SK_ColorWHITE);
    canvas->DrawImageInt(tuck_icon, 0, 0);
  }

  int ParentContainerId() const override {
    return kShellWindowId_FloatContainer;
  }

  void UpdateWindowPosition(aura::Window* window, bool left) override {
    TabletModeWindowState::UpdateWindowPosition(
        WindowState::Get(window),
        WindowState::BoundsChangeAnimationType::kNone);
  }

  void UntuckWindow(aura::Window* window) override {
    Shell::Get()->float_controller()->MaybeUntuckFloatedWindowForTablet(window);
  }

  void OnAnimateTuckEnded(aura::Window* window) override {
    wm::ScopedAnimationDisabler disable(window);
    window->Hide();
  }

  gfx::Rect GetTuckHandleBounds(bool left,
                                const gfx::Rect& window_bounds) const override {
    const gfx::Point tuck_handle_origin =
        left ? window_bounds.right_center() -
                   gfx::Vector2d(0, ScopedWindowTucker::kTuckHandleHeight / 2)
             : window_bounds.left_center() -
                   gfx::Vector2d(ScopedWindowTucker::kTuckHandleWidth,
                                 ScopedWindowTucker::kTuckHandleHeight / 2);
    return gfx::Rect(tuck_handle_origin,
                     gfx::Size(ScopedWindowTucker::kTuckHandleWidth,
                               ScopedWindowTucker::kTuckHandleHeight));
  }
};

}  // namespace

// -----------------------------------------------------------------------------
// FloatedWindowInfo:

// Represents and stores information used for window's floated state.
class FloatController::FloatedWindowInfo : public aura::WindowObserver {
 public:
  FloatedWindowInfo(aura::Window* floated_window, const Desk* desk)
      : floated_window_(floated_window),
        was_position_auto_managed_(
            DisableAndGetOriginalPositionAutoManaged(floated_window)),
        desk_(desk) {
    DCHECK(floated_window_);
    floated_window_observation_.Observe(floated_window);

    if (desk->is_active())
      float_start_time_ = base::TimeTicks::Now();

    if (display::Screen::GetScreen()->InTabletMode() &&
        TabletModeTuckEducation::CanActivateTuckEducation() &&
        !Shell::Get()
             ->float_controller()
             ->disable_tuck_education_for_testing_) {
      tuck_education_ =
          std::make_unique<TabletModeTuckEducation>(floated_window);
    }
  }

  FloatedWindowInfo(const FloatedWindowInfo&) = delete;
  FloatedWindowInfo& operator=(const FloatedWindowInfo&) = delete;
  ~FloatedWindowInfo() override {
    // Reset the window position auto-managed status if it was auto managed.
    if (was_position_auto_managed_)
      WindowState::Get(floated_window_)->SetWindowPositionManaged(true);
    MaybeRecordFloatWindowDuration();
  }

  const Desk* desk() const { return desk_; }
  void set_desk(const Desk* desk) { desk_ = desk; }

  bool is_tucked_for_tablet() const { return is_tucked_for_tablet_; }

  MagnetismCorner magnetism_corner() const { return magnetism_corner_; }
  void set_magnetism_corner(MagnetismCorner magnetism_corner) {
    magnetism_corner_ = magnetism_corner;
  }

  void MaybeRecordFloatWindowDuration() {
    if (!float_start_time_.is_null()) {
      base::UmaHistogramCustomCounts(
          kFloatWindowDurationHistogramName,
          (base::TimeTicks::Now() - float_start_time_).InMinutes(), 1,
          base::Days(7).InMinutes(), 50);
      float_start_time_ = base::TimeTicks();
    }
  }

  void MaybeTuckWindow(bool left) {
    // The order here matters: `is_tucked_for_tablet_` must be set to true
    // while in the constructor and also before `AnimateUntuck()` gets the
    // tucked window bounds.
    is_tucked_for_tablet_ = true;
    scoped_window_tucker_ = std::make_unique<ScopedWindowTucker>(
        std::make_unique<FloatScopedWindowTuckerDelegate>(), floated_window_,
        left);
    scoped_window_tucker_->AnimateTuck();

    // Education doesn't need to happen after the user has successfully tucked
    // once.
    TabletModeTuckEducation::OnWindowTucked();
  }

  void OnUntuckAnimationEnded() {
    scoped_window_tucker_.reset();

    // No-op for non-client-controlled windows. For the client-controlled
    // windows, this ensures the bounds is sync between Chrome and the client.
    // We don't send the offscreen bounds to the client when tucked, so we need
    // to send the proper floated bounds when untucked.
    UpdateWindowBoundsForTablet(floated_window_,
                                WindowState::BoundsChangeAnimationType::kNone);
  }

  void MaybeUntuckWindow(bool animate) {
    // The order here matters: `is_tucked_for_tablet_` must be set to false
    // before `TabletModeWindowState::UpdateWindowPosition()` or
    // `AnimateUntuck()` gets the untucked window bounds.
    is_tucked_for_tablet_ = false;

    if (!animate) {
      scoped_window_tucker_.reset();
      UpdateWindowBoundsForTablet(
          floated_window_, WindowState::BoundsChangeAnimationType::kNone);
      return;
    }

    if (scoped_window_tucker_) {
      scoped_window_tucker_->AnimateUntuck(
          base::BindOnce(&FloatedWindowInfo::OnUntuckAnimationEnded,
                         weak_ptr_factory_.GetWeakPtr()));
    }
  }

  views::Widget* GetTuckHandleWidget() {
    DCHECK(scoped_window_tucker_);
    return scoped_window_tucker_->tuck_handle_widget();
  }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    DCHECK_EQ(floated_window_, window);
    DCHECK(
        floated_window_observation_.IsObservingSource(floated_window_.get()));
    // Note that `this` is deleted below in `OnFloatedWindowDestroying()` and
    // should not be accessed after this.
    Shell::Get()->float_controller()->OnFloatedWindowDestroying(window);
  }

  void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
    if (window != floated_window_)
      return;

    // When a floated window switches desks, it is hidden or shown. We track the
    // amount of time a floated window is visible on the active desk to avoid
    // recording the cases if a floated window is floated indefinitely on an
    // inactive desk. Check if the desk is active as well, as some UI such as
    // the saved desks library view may temporarily hide the floated window on
    // the active desk.
    if (visible && desk_->is_active()) {
      if (float_start_time_.is_null())
        float_start_time_ = base::TimeTicks::Now();
      return;
    }

    if (!visible && !desk_->is_active())
      MaybeRecordFloatWindowDuration();
  }

  void OnWindowPropertyChanged(aura::Window* window,
                               const void* key,
                               intptr_t old) override {
    CHECK_EQ(floated_window_, window);

    if (key == aura::client::kWindowWorkspaceKey &&
        desks_util::IsZOrderTracked(window)) {
      auto* desks_controller = Shell::Get()->desks_controller();
      if (desks_util::IsWindowVisibleOnAllWorkspaces(window)) {
        desks_controller->AddVisibleOnAllDesksWindow(window);
      } else {
        desks_controller->MaybeRemoveVisibleOnAllDesksWindow(window);
      }

      return;
    }

    // Always on top window cannot be floated, so if a floated window becomes
    // always on top, exit float state.
    if (key == aura::client::kZOrderingKey) {
      if (window->GetProperty(aura::client::kZOrderingKey) !=
          ui::ZOrderLevel::kNormal) {
        // Destroys `this`.
        Shell::Get()->float_controller()->ResetFloatedWindow(floated_window_);
      }
      return;
    }

    if (key != aura::client::kResizeBehaviorKey) {
      return;
    }

    // If `window` is in transitional snapped state, `window` is going to be
    // snapped very soon so we don't need to apply the float bounds policies.
    // Otherwise, the bounds change request may be queued and applied after
    // `window` is snapped.
    if (SplitViewController::Get(window)->IsWindowInTransitionalState(window)) {
      return;
    }

    // The minimum size could change and as a result, the floated window might
    // not be floatable anymore. In this case, unfloat it.
    if (!chromeos::wm::CanFloatWindow(floated_window_)) {
      Shell::Get()->float_controller()->ResetFloatedWindow(floated_window_);
      return;
    }

    if (Shell::Get()->IsInTabletMode()) {
      UpdateWindowBoundsForTablet(
          floated_window_, WindowState::BoundsChangeAnimationType::kNone);
    }
  }

 private:
  // The `floated_window` this object is hosting information for.
  raw_ptr<aura::Window> floated_window_;

  // When a window is floated, the window position should not be auto-managed.
  // Use this value to reset the auto-managed state when unfloating a window.
  const bool was_position_auto_managed_;

  // Scoped object that handles the special tucked window state, which is not
  // a normal window state. Null when `floated_window_` is currently not tucked.
  std::unique_ptr<ScopedWindowTucker> scoped_window_tucker_;

  // An object responsible for managing the tuck education nudge and animations.
  std::unique_ptr<TabletModeTuckEducation> tuck_education_;

  // Used to get the tucked window bounds (as opposed to normal floated). False
  // during `scoped_window_tucker_` construction.
  bool is_tucked_for_tablet_ = false;

  // The desk where floated window belongs to.
  // When a window is getting floated, it moves from desk container to float
  // container, this Desk pointer is used to determine floating window's desk
  // ownership, since floated window should only be shown on the desk it belongs
  // to.
  raw_ptr<const Desk, DanglingUntriaged> desk_;

  // The start time when the floated window is on the active desk. Used for
  // logging the amount of time a window is floated. Logged when the desk
  // changes to inactive (when combining desks we can change desks, but remain
  // on the active desk), or when the window is unfloated.
  base::TimeTicks float_start_time_;

  // The corner the `floated_window_` should be magnetized to.
  // By default it magnetizes to the bottom right when first floated.
  MagnetismCorner magnetism_corner_ = MagnetismCorner::kBottomRight;

  base::ScopedObservation<aura::Window, aura::WindowObserver>
      floated_window_observation_{this};

  base::WeakPtrFactory<FloatedWindowInfo> weak_ptr_factory_{this};
};

// -----------------------------------------------------------------------------
// FloatController:

FloatController::FloatController() {
  shell_observation_.Observe(Shell::Get());
  for (aura::Window* root : Shell::GetAllRootWindows())
    OnRootWindowAdded(root);
}

FloatController::~FloatController() {
  // Record how many windows are floated per session.
  base::UmaHistogramCounts100(kFloatWindowCountsPerSessionHistogramName,
                              floated_window_counter_);
  // Record how many windows are moved to another desk per session.
  base::UmaHistogramCounts100(kFloatWindowMoveToAnotherDeskCountsHistogramName,
                              floated_window_move_to_another_desk_counter_);
}

// static
gfx::Rect FloatController::GetFloatWindowClamshellBounds(
    aura::Window* window,
    chromeos::FloatStartLocation location) {
  DCHECK(chromeos::wm::CanFloatWindow(window));

  // In the case of window restore, as we re-float previously floated window, we
  // will use `window->bounds()`to restore floated window's previous
  // location.
  if (window->GetProperty(app_restore::kLaunchedFromAppRestoreKey)) {
    return window->bounds();
  }

  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(window);

  const int padding_dp = chromeos::wm::kFloatedWindowPaddingDp;

  if ((window->GetProperty(aura::client::kResizeBehaviorKey) &
       aura::client::kResizeBehaviorCanResize) == 0) {
    // Unresizable windows must not be resized for any reason.
    return GetFloatBounds(window->bounds().size(), work_area, location);
  }

  // Default float size is 1/3 width and 70% height of `work_area`.
  // Float bounds also should not be smaller than min bounds, use min
  // width/height if it exceeds the limit.
  const gfx::Size minimum_size = window->delegate()->GetMinimumSize();
  gfx::Rect preferred_bounds =
      gfx::Rect(std::max(static_cast<int>(work_area.width() *
                                          kFloatedWindowClamshellWidthRatio),
                         minimum_size.width()),
                std::max(static_cast<int>(work_area.height() *
                                          kFloatedWindowClamshellHeightRatio),
                         minimum_size.height()));

  // If user has already adjusted the window to be a size smaller than the
  // calculated preferred size, use user size instead.
  if (window->bounds().height() <= preferred_bounds.height() &&
      window->bounds().width() <= preferred_bounds.width()) {
    preferred_bounds = window->bounds();
  }

  const int preferred_width =
      std::min(preferred_bounds.width(), work_area.width() - 2 * padding_dp);
  const int preferred_height =
      std::min(preferred_bounds.height(), work_area.height() - 2 * padding_dp);
  return GetFloatBounds(gfx::Size(preferred_width, preferred_height), work_area,
                        location);
}

// static
gfx::Rect FloatController::GetFloatWindowTabletBounds(aura::Window* window) {
  const gfx::Size preferred_size =
      chromeos::wm::GetFloatedWindowTabletSize(window);

  const int width = preferred_size.width();
  const int height = preferred_size.height();

  // Get `floated_window_info` from the float controller. For non
  // client-controlled apps, it is expected we call this function on already
  // floated windows. For client controlled windows, we need to send the floated
  // bounds before the client applies the float state, which results in using
  // `GetFloatWindowTabletBounds` before `FloatImpl` is called.
  auto* floated_window_info =
      Shell::Get()->float_controller()->MaybeGetFloatedWindowInfo(window);
#if DCHECK_IS_ON()
  if (!WindowState::Get(window)->is_client_controlled()) {
    DCHECK(floated_window_info);
  }
#endif

  const gfx::Rect work_area =
      screen_util::GetDisplayWorkAreaBoundsInParent(window);

  // Update the origin of the floated window based on whichever corner it is
  // magnetized to.
  gfx::Point origin;

  const MagnetismCorner magnetism_corner =
      floated_window_info ? floated_window_info->magnetism_corner()
                          : MagnetismCorner::kBottomRight;
  const int padding_dp = chromeos::wm::kFloatedWindowPaddingDp;
  switch (magnetism_corner) {
    case MagnetismCorner::kTopLeft:
      origin =
          gfx::Point(work_area.x() + padding_dp, work_area.y() + padding_dp);
      break;
    case MagnetismCorner::kTopRight:
      origin = gfx::Point(work_area.right() - width - padding_dp,
                          work_area.y() + padding_dp);
      break;
    case MagnetismCorner::kBottomLeft:
      origin = gfx::Point(work_area.x() + padding_dp,
                          work_area.bottom() - height - padding_dp);
      break;
    case MagnetismCorner::kBottomRight:
      origin = gfx::Point(work_area.right() - width - padding_dp,
                          work_area.bottom() - height - padding_dp);
      break;
  }

  // If the window is tucked, shift it so the window is offscreen.
  if (floated_window_info && floated_window_info->is_tucked_for_tablet()) {
    int x_offset;
    switch (magnetism_corner) {
      case MagnetismCorner::kTopLeft:
      case MagnetismCorner::kBottomLeft:
        x_offset = -width - padding_dp;
        break;
      case MagnetismCorner::kTopRight:
      case MagnetismCorner::kBottomRight:
        x_offset = width + padding_dp;
        break;
    }
    origin.Offset(x_offset, 0);
  }

  return gfx::Rect(origin, gfx::Size(width, height));
}

void FloatController::ToggleFloat(aura::Window* window) {
  if (WindowState::Get(window)->IsFloated()) {
    UnsetFloat(window);
  } else {
    SetFloat(window, chromeos::FloatStartLocation::kBottomRight);
  }
}

void FloatController::MaybeUntuckFloatedWindowForTablet(
    aura::Window* floated_window) {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
  DCHECK(floated_window_info);
  floated_window_info->MaybeUntuckWindow(/*animate=*/true);
}

bool FloatController::IsFloatedWindowTuckedForTablet(
    const aura::Window* floated_window) const {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
  // This can be called during state transition, where window is getting into
  // float state but float info is not created, return false as default tuck
  // status is false.
  if (!floated_window_info) {
    return false;
  }
  return floated_window_info->is_tucked_for_tablet();
}

bool FloatController::IsFloatedWindowAlignedWithShelf(
    aura::Window* floated_window) const {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
  DCHECK(floated_window_info);
  if (floated_window_info->is_tucked_for_tablet()) {
    return false;
  }

  MagnetismCorner magnetism_corner = floated_window_info->magnetism_corner();
  return magnetism_corner == MagnetismCorner::kBottomLeft ||
         magnetism_corner == MagnetismCorner::kBottomRight;
}

views::Widget* FloatController::GetTuckHandleWidget(
    const aura::Window* floated_window) const {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
  DCHECK(floated_window_info);
  return floated_window_info->GetTuckHandleWidget();
}

void FloatController::OnDragCompletedForTablet(aura::Window* floated_window) {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
  DCHECK(floated_window_info);
  floated_window_info->set_magnetism_corner(
      GetMagnetismCornerForBounds(floated_window->GetBoundsInScreen()));
  UpdateWindowBoundsForTablet(floated_window,
                              WindowState::BoundsChangeAnimationType::kAnimate);
}

void FloatController::OnFlingOrSwipeForTablet(aura::Window* floated_window,
                                              float velocity_x,
                                              float velocity_y) {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(floated_window);
  DCHECK(floated_window_info);
  // Move the window in the direction of the vertical velocity.
  MagnetismCorner magnetism_corner = floated_window_info->magnetism_corner();
  bool start_left = magnetism_corner == MagnetismCorner::kTopLeft ||
                    magnetism_corner == MagnetismCorner::kBottomLeft;
  if (velocity_y < 0.f) {
    floated_window_info->set_magnetism_corner(
        start_left ? MagnetismCorner::kTopLeft : MagnetismCorner::kTopRight);
  } else if (velocity_y > 0.f) {
    floated_window_info->set_magnetism_corner(
        start_left ? MagnetismCorner::kBottomLeft
                   : MagnetismCorner::kBottomRight);
  }

  // Move the window in the direction of the horizontal velocity. Note that the
  // updated `magnetism_corner()` must be used to get the direction of both
  // velocities.
  magnetism_corner = floated_window_info->magnetism_corner();
  bool start_top = magnetism_corner == MagnetismCorner::kTopLeft ||
                   magnetism_corner == MagnetismCorner::kTopRight;
  if (velocity_x < 0.f) {
    floated_window_info->set_magnetism_corner(
        start_top ? MagnetismCorner::kTopLeft : MagnetismCorner::kBottomLeft);
  } else if (velocity_x > 0.f) {
    floated_window_info->set_magnetism_corner(
        start_top ? MagnetismCorner::kTopRight : MagnetismCorner::kBottomRight);
  }

  // If the horizontal velocity was in the direction of `start` tuck the
  // window, otherwise magnetize it.
  if ((start_left && velocity_x < 0.f) || (!start_left && velocity_x > 0.f)) {
    floated_window_info->MaybeTuckWindow(start_left);
    return;
  }
  UpdateWindowBoundsForTablet(floated_window,
                              WindowState::BoundsChangeAnimationType::kAnimate);
}

const Desk* FloatController::FindDeskOfFloatedWindow(
    const aura::Window* window) const {
  if (auto* info = MaybeGetFloatedWindowInfo(window))
    return info->desk();
  return nullptr;
}

aura::Window* FloatController::FindFloatedWindowOfDesk(const Desk* desk) const {
  DCHECK(desk);
  for (const auto& [window, info] : floated_window_info_map_) {
    if (info->desk() == desk)
      return window;
  }
  return nullptr;
}

void FloatController::OnMovingAllWindowsOutToDesk(Desk* original_desk,
                                                  Desk* target_desk) {
  auto* original_desk_floated_window = FindFloatedWindowOfDesk(original_desk);
  if (!original_desk_floated_window)
    return;
  // Records floated window being moved to another desk.
  ++floated_window_move_to_another_desk_counter_;
  auto* target_desk_floated_window = FindFloatedWindowOfDesk(target_desk);

  // Float window might have been hidden on purpose and won't show
  // automatically.
  ShowFloatedWindow(original_desk_floated_window);
  // During desk removal/combine, if `target_desk` has a floated window, we
  // will unfloat the floated window in `original_desk` and re-parent it back
  // to its desk container.
  if (target_desk_floated_window) {
    // Unfloat the floated window at `original_desk` desk.
    ResetFloatedWindow(original_desk_floated_window);
  } else {
    floated_window_info_map_[original_desk_floated_window]->set_desk(
        target_desk);
    // Note that other windows that belong to the "same container"
    //  are being re-sorted at the end of
    // `Desk::MoveWindowsToDesk`. This ensures windows associated with removed
    // desk appear as least recent in MRU order, since they get appended at
    // the end of overview. we are calling it here so the floated window
    // that's being moved to the target desk is also being sorted for the same
    // reason.
    Shell::Get()->mru_window_tracker()->OnWindowMovedOutFromRemovingDesk(
        original_desk_floated_window);
  }
}

void FloatController::OnMovingFloatedWindowToDesk(aura::Window* floated_window,
                                                  Desk* active_desk,
                                                  Desk* target_desk,
                                                  aura::Window* target_root) {
  auto* target_desk_floated_window = FindFloatedWindowOfDesk(target_desk);
  aura::Window* root = floated_window->GetRootWindow();
  if (target_desk_floated_window) {
    // Unfloat the floated window at `target_desk`.
    ResetFloatedWindow(target_desk_floated_window);
  }
  auto* float_info = MaybeGetFloatedWindowInfo(floated_window);
  DCHECK(float_info);
  DCHECK_EQ(float_info->desk(), active_desk);
  float_info->set_desk(target_desk);
  // Records floated window being moved to another desk.
  ++floated_window_move_to_another_desk_counter_;
  if (root != target_root) {
    // If `floated_window_` is dragged to a desk on a different display, we
    // also need to move it to the target display.
    window_util::MoveWindowToDisplay(floated_window,
                                     display::Screen::GetScreen()
                                         ->GetDisplayNearestWindow(target_root)
                                         .id());
  }

  if (!desks_util::IsWindowVisibleOnAllWorkspaces(floated_window)) {
    // Update `floated_window` visibility based on target desk's activation
    // status.
    if (target_desk->is_active()) {
      ShowFloatedWindow(floated_window);
    } else {
      HideFloatedWindow(floated_window);
    }
  }

  active_desk->NotifyContentChanged();
  target_desk->NotifyContentChanged();
}

void FloatController::ClearWorkspaceEventHandler(aura::Window* root) {
  workspace_event_handlers_.erase(root);
}

void FloatController::OnDeskActivationChanged(const Desk* activated,
                                              const Desk* deactivated) {
  // Since floated windows are not children of desk containers, switching desks
  // (which changes the visibility of desks' containers) won't automatically
  // update the floated windows' visibility. Therefore, here we hide the floated
  // window belonging to the deactivated desk, and show the one belonging to the
  // activated desk.
  auto deactivated_desk_floated_window_info_iter = base::ranges::find_if(
      floated_window_info_map_, [deactivated](const auto& floated_window_info) {
        return floated_window_info.second->desk() == deactivated;
      });
  if (deactivated_desk_floated_window_info_iter !=
      floated_window_info_map_.end()) {
    // If we are currently not in tablet mode, no need to untuck, which would
    // update the window bounds.
    if (Shell::Get()->IsInTabletMode()) {
      deactivated_desk_floated_window_info_iter->second->MaybeUntuckWindow(
          /*animate=*/false);
    }
    HideFloatedWindow(deactivated_desk_floated_window_info_iter->first);
  }

  if (auto* activated_desk_floated_window =
          FindFloatedWindowOfDesk(activated)) {
    ShowFloatedWindow(activated_desk_floated_window);
    // Activate the floated window if it is the top window. This is normally
    // done in `Desk::Activate`, but floated windows are technically not owned
    // by the desk, and the window is still hidden at that point so it isn't in
    // the MRU list.
    if (auto* top_window = window_util::GetTopWindow();
        top_window == activated_desk_floated_window) {
      wm::ActivateWindow(top_window);
    }
  }
}

void FloatController::OnDisplayMetricsChanged(const display::Display& display,
                                              uint32_t metrics) {
  // The work area can change while entering or exiting tablet mode. The float
  // window changes related with those changes are handled in
  // `OnTabletModeStarting`, `OnTabletModeEnding` or attaching/detaching window
  // states.
  display::TabletState tablet_state =
      display::Screen::GetScreen()->GetTabletState();
  if (tablet_state == display::TabletState::kEnteringTabletMode ||
      tablet_state == display::TabletState::kExitingTabletMode) {
    return;
  }

  const uint32_t filter = DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_WORK_AREA;
  if ((filter & metrics) == 0) {
    return;
  }

  DCHECK(!floated_window_info_map_.empty());
  std::vector<aura::Window*> windows_need_reset;
  for (auto& [window, info] : floated_window_info_map_) {
    if (!chromeos::wm::CanFloatWindow(window)) {
      windows_need_reset.push_back(window);
    } else {
      // Let the state object handle the display change. This is normally
      // handled by the `WorkspaceLayoutManager`, but the float container does
      // not have one attached.
      if (metrics & DISPLAY_METRIC_BOUNDS ||
          metrics & DISPLAY_METRIC_WORK_AREA) {
        const DisplayMetricsChangedWMEvent wm_event(metrics);
        WindowState::Get(window)->OnWMEvent(&wm_event);
      }
    }
  }
  for (auto* window : windows_need_reset)
    ResetFloatedWindow(window);

  // Do not observe the animator in `OnRootWindowAdded` because there is an
  // unittest that overwrites the animator for the root window just before
  // running the animation.
  if (DISPLAY_METRIC_ROTATION & metrics) {
    if (auto* root_controller =
            Shell::GetRootWindowControllerWithDisplayId(display.id())) {
      if (auto* animator = root_controller->GetScreenRotationAnimator();
          animator &&
          !screen_rotation_observations_.IsObservingSource(animator)) {
        screen_rotation_observations_.AddObservation(animator);
      }
    }
  }
}

void FloatController::OnDisplayTabletStateChanged(display::TabletState state) {
  switch (state) {
    case display::TabletState::kInClamshellMode:
    case display::TabletState::kEnteringTabletMode:
      break;
    case display::TabletState::kInTabletMode:
      OnTabletModeStarted();
      break;
    case display::TabletState::kExitingTabletMode:
      OnTabletModeEnding();
      break;
  }
}

void FloatController::OnRootWindowAdded(aura::Window* root_window) {
  workspace_event_handlers_[root_window] =
      std::make_unique<WorkspaceEventHandler>(
          root_window->GetChildById(kShellWindowId_FloatContainer));
  root_window->GetChildById(kShellWindowId_FloatContainer)
      ->SetLayoutManager(std::make_unique<FloatLayoutManager>());
}

void FloatController::OnRootWindowWillShutdown(aura::Window* root_window) {
  if (auto* const animator = RootWindowController::ForWindow(root_window)
                                 ->GetScreenRotationAnimator();
      animator && screen_rotation_observations_.IsObservingSource(animator)) {
    screen_rotation_observations_.RemoveObservation(animator);
  }
}

void FloatController::OnScreenCopiedBeforeRotation() {}

void FloatController::OnScreenRotationAnimationFinished(
    ScreenRotationAnimator* animator,
    bool canceled) {
  // Re-send the correct floated bounds here. ARC sometimes overwrites the
  // floated bounds against the new bounds during the rotation animation.
  // TODO(b/278519956): Remove this workaround once ARC/Exo handle rotation
  // bounds better.
  for (auto& [window, info] : floated_window_info_map_) {
    if (WindowState::Get(window)->is_client_controlled()) {
      const gfx::Rect bounds =
          display::Screen::GetScreen()->InTabletMode()
              ? GetFloatWindowTabletBounds(window)
              : GetFloatWindowClamshellBounds(
                    window, chromeos::FloatStartLocation::kBottomRight);
      const SetBoundsWMEvent event(bounds);
      WindowState::Get(window)->OnWMEvent(&event);

      // When a window is tucked, ash has full control over the bounds.
      if (IsFloatedWindowTuckedForTablet(window)) {
        TabletModeWindowState::UpdateWindowPosition(
            WindowState::Get(window),
            WindowState::BoundsChangeAnimationType::kNone);
      }
    }
  }
}

void FloatController::OnPinnedStateChanged(aura::Window* pinned_window) {
  if (aura::Window* floated_window =
          window_util::GetFloatedWindowForActiveDesk()) {
    // Note that the `pinned_window` will still not be null when unpinning.
    // Check the screen pinning controller for the to be pinned window.
    if (aura::Window* to_be_pinned_window =
            Shell::Get()->screen_pinning_controller()->pinned_window()) {
      if (to_be_pinned_window != floated_window) {
        HideFloatedWindow(floated_window);
      }
    } else {
      ShowFloatedWindow(floated_window);
    }
  }
}

void FloatController::SetFloat(
    aura::Window* window,
    chromeos::FloatStartLocation float_start_location) {
  auto* window_state = WindowState::Get(window);
  if (!window_state->IsFloated()) {
    const WindowFloatWMEvent float_event(float_start_location);
    window_state->OnWMEvent(&float_event);
  }
}

void FloatController::UnsetFloat(aura::Window* window) {
  auto* window_state = WindowState::Get(window);
  if (window_state->IsFloated()) {
    const WMEvent restore_event(WM_EVENT_RESTORE);
    window_state->OnWMEvent(&restore_event);
  }
}

// static
FloatController::MagnetismCorner FloatController::GetMagnetismCornerForBounds(
    const gfx::Rect& bounds_in_screen) {
  // Check which corner to magnetize to based on which quadrant of the display
  // the centerpoint of the window was on touch released. Note that the
  // centerpoint may be offscreen.
  const gfx::Point display_bounds_center =
      display::Screen::GetScreen()
          ->GetDisplayMatching(bounds_in_screen)
          .bounds()
          .CenterPoint();
  const gfx::Point center_point = bounds_in_screen.CenterPoint();
  const bool is_left_half = center_point.x() < display_bounds_center.x();
  if (center_point.y() < display_bounds_center.y()) {
    // Top half.
    return is_left_half ? FloatController::MagnetismCorner::kTopLeft
                        : FloatController::MagnetismCorner::kTopRight;
  }
  // Bottom half.
  return is_left_half ? FloatController::MagnetismCorner::kBottomLeft
                      : FloatController::MagnetismCorner::kBottomRight;
}

void FloatController::FloatForTablet(aura::Window* window,
                                     chromeos::WindowStateType old_state_type) {
  CHECK(Shell::Get()->IsInTabletMode());

  FloatImpl(window);

  // Update the magnetism if we are coming from a state that can restore to
  // float state, or from snap state. The bounds will be updated later based on
  // the magnetism and account for work area.
  std::optional<MagnetismCorner> magnetism_corner;
  if (chromeos::IsMinimizedWindowStateType(old_state_type)) {
    magnetism_corner = GetMagnetismCornerForBounds(window->GetBoundsInScreen());
  } else if (chromeos::IsSnappedWindowStateType(old_state_type)) {
    // Update magnetism so that the float window is roughly in the same
    // location as it was when it was snapped.
    const bool left_or_top =
        old_state_type == chromeos::WindowStateType::kPrimarySnapped;
    const bool landscape = IsCurrentScreenOrientationLandscape();
    if (!left_or_top) {
      // Bottom or right snapped.
      magnetism_corner = MagnetismCorner::kBottomRight;
    } else if (landscape) {
      // Left snapped.
      magnetism_corner = MagnetismCorner::kBottomLeft;
    } else {
      CHECK(left_or_top && !landscape);
      // Top snapped.
      magnetism_corner = MagnetismCorner::kTopRight;
    }
  }

  if (!magnetism_corner) {
    return;
  }

  auto* floated_window_info = MaybeGetFloatedWindowInfo(window);
  CHECK(floated_window_info);
  floated_window_info->set_magnetism_corner(*magnetism_corner);
}

void FloatController::FloatImpl(aura::Window* window) {
  if (floated_window_info_map_.contains(window))
    return;

  auto* desk_controller = DesksController::Get();
  // Get the desk where the window belongs to before moving it to float
  // container.
  const Desk* desk = desks_util::GetDeskForContext(window);
  if (!desk) {
    return;
  }

  // If the window we want to float is already visible on all desks, then we
  // need to unfloat any other currently floated windows that exist on each
  // desk, as there should only be one floated window per desk.
  const bool reset_all_desks =
      desks_util::IsWindowVisibleOnAllWorkspaces(window);
  std::vector<aura::Window*> windows_to_reset;

  for (const auto& [floated_window, info] : floated_window_info_map_) {
    // Regardless if `window` is visible on all desks or not, if a floated
    // window already exists at the current desk, then we also want to unfloat
    // it.
    if (reset_all_desks || info->desk() == desk) {
      windows_to_reset.push_back(floated_window);
    }
  }

  // Since a floated window is always on top, we don't want to track its
  // z-ordering.
  if (reset_all_desks) {
    desk_controller->UntrackWindowFromAllDesks(window);
  }

  // Add floated window to `floated_window_info_map_`.
  // Note: this has to be called before `ResetFloatedWindow`. Because in the
  // call sequence of `ResetFloatedWindow` we will access
  // `floated_window_info_map_`, and hit a corner case where window
  // `IsFloated()` returns true, but `FindDeskOfFloatedWindow` returns nullptr.
  floated_window_info_map_.emplace(
      window, std::make_unique<FloatedWindowInfo>(window, desk));
  for (auto* reset_window : windows_to_reset) {
    ResetFloatedWindow(reset_window);
  }

  aura::Window* floated_container =
      window->GetRootWindow()->GetChildById(kShellWindowId_FloatContainer);
  DCHECK_NE(window->parent(), floated_container);
  floated_container->AddChild(window);

  if (!desk->is_active())
    HideFloatedWindow(window);

  // Update floated window counts.
  // Note that if the same window gets floated 2 times in the same session, it's
  // counted as 2 floated windows.
  ++floated_window_counter_;

  if (!desks_controller_observation_.IsObserving())
    desks_controller_observation_.Observe(desk_controller);
  if (!display_observer_)
    display_observer_.emplace(this);
}

void FloatController::UnfloatImpl(aura::Window* window) {
  auto* floated_window_info = MaybeGetFloatedWindowInfo(window);
  if (!floated_window_info)
    return;

  // Floated window have been hidden on purpose on the inactive desk.
  ShowFloatedWindow(window);
  // Re-parent window to the "parent" desk's desk container.
  floated_window_info->desk()
      ->GetDeskContainerForRoot(window->GetRootWindow())
      ->AddChild(window);
  floated_window_info_map_.erase(window);
  if (floated_window_info_map_.empty()) {
    desks_controller_observation_.Reset();
    display_observer_.reset();
  }

  // A floated window does not have per-desk z-order, so we need to start
  // tracking the window again after it is unfloated.
  if (desks_util::IsWindowVisibleOnAllWorkspaces(window)) {
    DesksController::Get()->TrackWindowOnAllDesks(window);
  }
}

void FloatController::ResetFloatedWindow(aura::Window* floated_window) {
  DCHECK(floated_window);
  DCHECK(WindowState::Get(floated_window)->IsFloated());
  UnsetFloat(floated_window);
}

FloatController::FloatedWindowInfo* FloatController::MaybeGetFloatedWindowInfo(
    const aura::Window* window) const {
  const auto iter = floated_window_info_map_.find(window);
  if (iter == floated_window_info_map_.end())
    return nullptr;
  return iter->second.get();
}

void FloatController::OnFloatedWindowDestroying(aura::Window* floated_window) {
  DesksController::Get()->MaybeRemoveVisibleOnAllDesksWindow(floated_window);

  floated_window_info_map_.erase(floated_window);
  if (floated_window_info_map_.empty()) {
    desks_controller_observation_.Reset();
    display_observer_.reset();
  }
}

void FloatController::OnTabletModeStarted() {
  DCHECK(!floated_window_info_map_.empty());
  // If a window can still remain floated, update its bounds, otherwise unfloat
  // it. Note that the bounds update has to happen after tablet mode has started
  // as opposed to while it is still starting, since some windows change their
  // minimum size, which tablet float bounds depend on.
  for (auto& [window, info] : floated_window_info_map_) {
    if (chromeos::wm::CanFloatWindow(window)) {
      info->set_magnetism_corner(
          GetMagnetismCornerForBounds(window->GetBoundsInScreen()));
      UpdateWindowBoundsForTablet(
          window, WindowState::BoundsChangeAnimationType::kCrossFade);
    } else {
      ResetFloatedWindow(window);
    }
  }
}

void FloatController::OnTabletModeEnding() {
  for (auto& [window, info] : floated_window_info_map_) {
    info->MaybeUntuckWindow(/*animate=*/false);
  }
}

}  // namespace ash