chromium/ash/shelf/drag_window_from_shelf_controller.cc

// Copyright 2019 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/shelf/drag_window_from_shelf_controller.h"

#include <algorithm>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/window_backdrop.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/window_scale_animation.h"
#include "ash/shell.h"
#include "ash/wallpaper/views/wallpaper_view.h"
#include "ash/wallpaper/views/wallpaper_widget_controller.h"
#include "ash/wallpaper/wallpaper_constants.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_drag_indicators.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_transient_descendant_iterator.h"
#include "ash/wm/window_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/scoped_animation_disabler.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// The minimum window scale factor when dragging a window from shelf.
constexpr float kMinimumWindowScaleDuringDragging = 0.3f;

// The ratio in display height at which point the dragged window shrinks to its
// minimum scale kMinimumWindowScaleDuringDragging.
constexpr float kMinYDisplayHeightRatio = 0.125f;

// Amount of time to wait to show overview after the user slows down or stops
// window dragging.
constexpr base::TimeDelta kShowOverviewTimeWhenDragSuspend =
    base::Milliseconds(40);

// The scroll update threshold to restart the show overview timer.
constexpr float kScrollUpdateOverviewThreshold = 2.f;

// Once we have dragged the window more than the display height divided by this
// ratio, the other window copy will be fully faded out.
constexpr float kOtherWindowFullFadeHeightRatio = 8.f;

// The other window will be scaled down to this maximum during dragging.
constexpr float kOtherWindowMaxScale = 0.9f;

// Presentation time histogram names.
constexpr char kDragWindowFromShelfHistogram[] =
    "Ash.DragWindowFromShelf.PresentationTime";
constexpr char kDragWindowFromShelfMaxLatencyHistogram[] =
    "Ash.DragWindowFromShelf.PresentationTime.MaxLatency";

// Self deleting class that takes ownership of the other window copy and
// animates it on drag finished. Deletes itself when the animation is done. The
// other window refers to the secondary window that moves when dragging from
// the shelf.
class OtherWindowCopyAnimation {
 public:
  // Takes ownership of the layer tree `other_winodw_copy`. Use a fade in and
  // scale up animation if `show` is true. Use a fade out animation if `show` is
  // false.
  OtherWindowCopyAnimation(
      std::unique_ptr<ui::LayerTreeOwner> other_window_copy,
      bool show)
      : other_window_copy_(std::move(other_window_copy)) {
    ui::Layer* layer = other_window_copy_->root();

    views::AnimationBuilder builder;
    builder
        .OnEnded(base::BindOnce(&OtherWindowCopyAnimation::OnAnimationEnded,
                                base::Unretained(this)))
        .OnAborted(base::BindOnce(&OtherWindowCopyAnimation::OnAnimationEnded,
                                  base::Unretained(this)))
        .SetPreemptionStrategy(
            ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
        .Once()
        .SetDuration(base::Milliseconds(350))
        .SetOpacity(layer, show ? 1.f : 0.f, gfx::Tween::LINEAR);
    if (show) {
      builder.Once()
          .SetDuration(base::Milliseconds(350))
          .SetTransform(layer, gfx::Transform(), gfx::Tween::LINEAR);
    }
  }
  OtherWindowCopyAnimation(const OtherWindowCopyAnimation&) = delete;
  OtherWindowCopyAnimation& operator=(const OtherWindowCopyAnimation&) = delete;
  ~OtherWindowCopyAnimation() = default;

  void OnAnimationEnded() { delete this; }

 private:
  std::unique_ptr<ui::LayerTreeOwner> other_window_copy_;
};

}  // namespace

// Hide all visible windows expect the dragged windows or the window showing in
// splitview during dragging.
class DragWindowFromShelfController::WindowsHider
    : public aura::WindowObserver {
 public:
  WindowsHider(aura::Window* dragged_window, aura::Window* other_window)
      : dragged_window_(dragged_window) {
    std::vector<raw_ptr<aura::Window, VectorExperimental>> windows =
        Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
    for (aura::Window* window : windows) {
      if (window == dragged_window_ || window == other_window) {
        continue;
      }
      if (wm::HasTransientAncestor(window, dragged_window_))
        continue;
      if (!window->IsVisible())
        continue;
      if (SplitViewController::Get(window)->IsWindowInSplitView(window))
        continue;
      auto* overview_controller = Shell::Get()->overview_controller();
      if (overview_controller->InOverviewSession() &&
          overview_controller->overview_session()->IsWindowInOverview(window)) {
        continue;
      }

      hidden_windows_.push_back(window);
      window->AddObserver(this);
      window->SetProperty(kHideDuringWindowDragging, true);
    }
    window_util::MinimizeAndHideWithoutAnimation(hidden_windows_);
  }

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

  ~WindowsHider() override {
    for (aura::Window* window : hidden_windows_) {
      window->RemoveObserver(this);
      window->ClearProperty(kHideDuringWindowDragging);
    }
    hidden_windows_.clear();
  }

  void RestoreWindowsVisibility() {
    for (aura::Window* window : hidden_windows_) {
      window->RemoveObserver(this);
      wm::ScopedAnimationDisabler disabler(window);
      window->Show();
      window->ClearProperty(kHideDuringWindowDragging);
    }
    hidden_windows_.clear();
  }

  // Even though we explicitly minimize the windows, some (i.e. ARC apps)
  // minimize asynchronously so they may not be truly minimized after |this| is
  // constructed.
  bool WindowsMinimized() {
    return base::ranges::all_of(hidden_windows_, [](const aura::Window* w) {
      return WindowState::Get(w)->IsMinimized();
    });
  }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    window->RemoveObserver(this);
    hidden_windows_.erase(base::ranges::find(hidden_windows_, window));
  }

 private:
  raw_ptr<aura::Window, DanglingUntriaged> dragged_window_;
  std::vector<raw_ptr<aura::Window, VectorExperimental>> hidden_windows_;
};

// static
float DragWindowFromShelfController::GetReturnToMaximizedThreshold() {
  return Shell::GetPrimaryRootWindowController()
      ->shelf()
      ->hotseat_widget()
      ->GetHotseatFullDragAmount();
}

DragWindowFromShelfController::DragWindowFromShelfController(
    aura::Window* window,
    const gfx::PointF& location_in_screen)
    : window_(window) {
  window_->AddObserver(this);

  // Find the other window that is visible while `window_` is being dragged.
  // There will only be another window if there is a float window (splitview has
  // two visible windows but is handled separately).
  if (auto* floated_window = window_util::GetFloatedWindowForActiveDesk()) {
    // If the floated window is the dragged window, then the other window is
    // the top most non floated window, if it exists. Otherwise the floated
    // window is the active window.
    if (floated_window == window_) {
      aura::Window* candidate_other_window =
          window_util::GetTopNonFloatedWindow();
      if (candidate_other_window &&
          !WindowState::Get(candidate_other_window)->IsMinimized()) {
        other_window_ = candidate_other_window;
      }
    } else {
      other_window_ = floated_window;
    }

    // Create a copy of the other window. This will be stacked on top and
    // faded out as we drag. The original window will be placed immediately
    // into overview mode on a successful drag, or return to its original
    // position on a canceled drag.
    if (other_window_) {
      other_window_->AddObserver(this);
      other_window_copy_ = wm::RecreateLayers(other_window_);
      other_window_copy_->root()->SetVisible(true);
      other_window_copy_->root()->SetOpacity(1.f);

      // If `other_window_` is the floated window, we need to move the copy to
      // the active desk container. The float container will be moved under the
      // desk containers (see `ScopedFloatContainerStacker `), so that the
      // overview item does not appear above the dragged window during the drag.
      if (other_window_ == floated_window) {
        ui::Layer* new_parent = desks_util::GetActiveDeskContainerForRoot(
                                    Shell::GetPrimaryRootWindow())
                                    ->layer();
        new_parent->Add(other_window_copy_->root());
      } else {
        other_window_->layer()->parent()->StackAbove(other_window_copy_->root(),
                                                     other_window_->layer());
      }
    }
  }

  OnDragStarted(location_in_screen);

  presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
      window_->GetHost()->compositor(), kDragWindowFromShelfHistogram,
      kDragWindowFromShelfMaxLatencyHistogram);
}

DragWindowFromShelfController::~DragWindowFromShelfController() {
  CancelDrag();
  if (window_)
    window_->RemoveObserver(this);
  ResetOtherWindow(/*show=*/std::nullopt);
}

void DragWindowFromShelfController::Drag(const gfx::PointF& location_in_screen,
                                         float scroll_x,
                                         float scroll_y) {
  // |window_| might have been destroyed during dragging.
  if (!window_)
    return;

  if (!drag_started_)
    return;

  presentation_time_recorder_->RequestNext();
  UpdateDraggedWindow(location_in_screen);

  DCHECK(windows_hider_);
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (std::abs(scroll_y) <= kOpenOverviewThreshold &&
      windows_hider_->WindowsMinimized()) {
    // Open overview if the window has been dragged far enough and the scroll
    // delta has decreased to `kOpenOverviewThreshold`. Wait until all windows
    // are minimized or they will not show up in overview.
    if (!overview_controller->InOverviewSession() &&
        overview_controller->StartOverview(
            OverviewStartAction::kDragWindowFromShelf,
            OverviewEnterExitType::kImmediateEnter)) {
      OnWindowDragStartedInOverview();
    }
  }

  // If overview is active, update its splitview indicator during dragging if
  // splitview is allowed in current configuration.
  if (overview_controller->InOverviewSession()) {
    const SnapPosition snap_position = GetSnapPosition(location_in_screen);
    const SplitViewDragIndicators::WindowDraggingState window_dragging_state =
        SplitViewDragIndicators::ComputeWindowDraggingState(
            /*is_dragging=*/true,
            SplitViewDragIndicators::WindowDraggingState::kFromShelf,
            snap_position);
    OverviewSession* overview_session = overview_controller->overview_session();
    overview_session->UpdateSplitViewDragIndicatorsWindowDraggingStates(
        Shell::GetPrimaryRootWindow(), window_dragging_state);
    overview_session->OnWindowDragContinued(window_, location_in_screen,
                                            window_dragging_state);

    if (snap_position != SnapPosition::kNone) {
      // If the dragged window is in snap preview area, make sure overview is
      // visible.
      ShowOverviewDuringOrAfterDrag();
    } else if (std::abs(scroll_x) > kShowOverviewThreshold ||
               std::abs(scroll_y) > kShowOverviewThreshold) {
      // If the dragging velocity is large enough, hide overview windows.
      show_overview_timer_.Stop();
      HideOverviewDuringDrag();
    } else if (!show_overview_timer_.IsRunning() ||
               std::abs(scroll_x) > kScrollUpdateOverviewThreshold ||
               std::abs(scroll_y) > kScrollUpdateOverviewThreshold) {
      // Otherwise start the |show_overview_timer_| to show and update
      // overview when the dragging slows down or stops. Note if the window is
      // still being dragged with scroll rate more than
      // kScrollUpdateOverviewThreshold, we restart the show overview timer.
      show_overview_timer_.Start(
          FROM_HERE, kShowOverviewTimeWhenDragSuspend, this,
          &DragWindowFromShelfController::ShowOverviewDuringOrAfterDrag);
    }
  }

  previous_location_in_screen_ = location_in_screen;
}

std::optional<ShelfWindowDragResult> DragWindowFromShelfController::EndDrag(
    const gfx::PointF& location_in_screen,
    std::optional<float> velocity_y) {
  if (!drag_started_)
    return std::nullopt;

  UpdateDraggedWindow(location_in_screen);

  drag_started_ = false;
  previous_location_in_screen_ = location_in_screen;
  presentation_time_recorder_.reset();
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  const bool in_overview = overview_controller->InOverviewSession();
  const bool in_splitview = split_view_controller->InSplitViewMode();
  const bool drop_window_in_overview =
      ShouldDropWindowInOverview(location_in_screen, velocity_y);
  end_snap_position_ = GetSnapPositionOnDragEnd(location_in_screen, velocity_y);

  window_drag_result_ = std::nullopt;
  if (ShouldGoToHomeScreen(location_in_screen, velocity_y)) {
    DCHECK(!in_splitview);
    if (in_overview) {
      overview_controller->EndOverview(OverviewEndAction::kDragWindowFromShelf,
                                       OverviewEnterExitType::kFadeOutExit);
    }
    window_drag_result_ = ShelfWindowDragResult::kGoToHomeScreen;
  } else if (ShouldRestoreToOriginalBounds(location_in_screen, velocity_y)) {
    window_drag_result_ = ShelfWindowDragResult::kRestoreToOriginalBounds;
  } else if (!in_overview) {
    // if overview is not active during the entire drag process, scale down the
    // dragged window to go to home screen.
    window_drag_result_ = ShelfWindowDragResult::kGoToHomeScreen;
  } else {
    if (drop_window_in_overview) {
      window_drag_result_ = ShelfWindowDragResult::kGoToOverviewMode;
    } else if (end_snap_position_ != SnapPosition::kNone) {
      window_drag_result_ = ShelfWindowDragResult::kGoToSplitviewMode;
    }
    // For window that may drop in overview or snap in split screen, restore its
    // original backdrop mode.
    WindowBackdrop::Get(window_)->RestoreBackdrop();
  }
  WindowState::Get(window_)->DeleteDragDetails();

  if (window_drag_result_.has_value()) {
    UMA_HISTOGRAM_ENUMERATION(kHandleDragWindowFromShelfHistogramName,
                              *window_drag_result_);
  }
  return window_drag_result_;
}

void DragWindowFromShelfController::CancelDrag() {
  if (!drag_started_)
    return;

  UMA_HISTOGRAM_ENUMERATION(kHandleDragWindowFromShelfHistogramName,
                            ShelfWindowDragResult::kDragCanceled);

  drag_started_ = false;
  presentation_time_recorder_.reset();
  // Reset the window's transform to identity transform.
  window_->SetTransform(gfx::Transform());
  WindowBackdrop::Get(window_)->RestoreBackdrop();

  // End overview if it was opened during dragging.
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (overview_controller->InOverviewSession()) {
    overview_controller->EndOverview(OverviewEndAction::kDragWindowFromShelf,
                                     OverviewEnterExitType::kImmediateExit);
  }
  ReshowHiddenWindowsOnDragEnd();

  window_drag_result_ = ShelfWindowDragResult::kDragCanceled;
  // When the drag is cancelled, the window should restore to its original snap
  // position.
  OnDragEnded(previous_location_in_screen_,
              /*should_drop_window_in_overview=*/false,
              /*snap_position=*/initial_snap_position_);
  WindowState::Get(window_)->DeleteDragDetails();
}

bool DragWindowFromShelfController::IsDraggedWindowAnimating() const {
  return window_ && window_->layer()->GetAnimator()->is_animating();
}

void DragWindowFromShelfController::FinalizeDraggedWindow() {
  if (!window_drag_result_.has_value()) {
    started_in_overview_ = false;
    return;
  }

  DCHECK(!drag_started_);
  DCHECK(window_);

  OnDragEnded(previous_location_in_screen_,
              *window_drag_result_ == ShelfWindowDragResult::kGoToOverviewMode,
              end_snap_position_);
}

void DragWindowFromShelfController::OnWindowDestroying(aura::Window* window) {
  if (window == other_window_) {
    ResetOtherWindow(/*show=*/std::nullopt);
    return;
  }

  DCHECK_EQ(window_, window);

  CancelDrag();
  window_->RemoveObserver(this);
  window_ = nullptr;
}

void DragWindowFromShelfController::OnDragStarted(
    const gfx::PointF& location_in_screen) {
  drag_started_ = true;
  started_in_overview_ =
      Shell::Get()->overview_controller()->InOverviewSession();
  initial_location_in_screen_ = location_in_screen;
  previous_location_in_screen_ = location_in_screen;
  WindowState::Get(window_)->CreateDragDetails(
      initial_location_in_screen_, HTCLIENT, ::wm::WINDOW_MOVE_SOURCE_TOUCH);

  // Disable the backdrop on the dragged window during dragging.
  WindowBackdrop::Get(window_)->DisableBackdrop();

  // Hide all visible windows behind the dragged window during dragging.
  windows_hider_ = std::make_unique<WindowsHider>(window_, other_window_);

  // Hide the home launcher until it's eligible to show it.
  Shell::Get()->app_list_controller()->OnWindowDragStarted();

  // If the dragged window is one of the snapped window in splitview, it needs
  // to be detached from splitview before start dragging.
  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  // Preserve initial snap position
  if (split_view_controller->IsWindowInSplitView(window_)) {
    initial_snap_position_ =
        split_view_controller->GetPositionOfSnappedWindow(window_);
  }
  split_view_controller->OnWindowDragStarted(window_);
  // Note SplitViewController::OnWindowDragStarted() may open overview.
  if (Shell::Get()->overview_controller()->InOverviewSession())
    OnWindowDragStartedInOverview();
}

void DragWindowFromShelfController::OnDragEnded(
    const gfx::PointF& location_in_screen,
    bool should_drop_window_in_overview,
    SnapPosition snap_position) {
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (overview_controller->InOverviewSession()) {
    // Make sure overview is visible after drag ends.
    ShowOverviewDuringOrAfterDrag();

    OverviewSession* overview_session = overview_controller->overview_session();
    overview_session->ResetSplitViewDragIndicatorsWindowDraggingStates();

    // No need to reposition overview windows if we are not dropping the dragged
    // window into overview. Overview will either be exited or unchanged, and
    // the extra movement from existing window will just add unnecessary
    // movement which will also slow down our dragged window animation.
    if (!should_drop_window_in_overview)
      overview_session->SuspendReposition();
    overview_session->OnWindowDragEnded(
        window_, location_in_screen, should_drop_window_in_overview,
        /*snap=*/snap_position != SnapPosition::kNone);
    overview_session->ResumeReposition();
  }

  SplitViewController* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());
  if (split_view_controller->InSplitViewMode() ||
      snap_position != SnapPosition::kNone) {
    split_view_controller->OnWindowDragEnded(
        window_, snap_position, gfx::ToRoundedPoint(location_in_screen),
        WindowSnapActionSource::kDragUpFromShelfToSnap);
  }

  // Scale-in-to-show home screen if home screen should be shown after drag
  // ends.
  Shell::Get()->app_list_controller()->OnWindowDragEnded(/*animate=*/true);

  DCHECK(window_drag_result_.has_value());
  switch (*window_drag_result_) {
    case ShelfWindowDragResult::kGoToHomeScreen:
      ScaleDownWindowAfterDrag();
      windows_hider_.reset();
      break;
    case ShelfWindowDragResult::kRestoreToOriginalBounds:
      ScaleUpToRestoreWindowAfterDrag();
      // Do not reset |windows_hider_| here because
      // |ScaleUpToRestoreWindowAfterDrag()| ends up using |windows_hider_| in
      // an async manner.
      break;
    case ShelfWindowDragResult::kGoToOverviewMode:
    case ShelfWindowDragResult::kGoToSplitviewMode:
    case ShelfWindowDragResult::kDragCanceled:
      windows_hider_.reset();
      break;
  }

  // If it exists, `other_window_copy_` will restore to its initial bounds and
  // opacity if the drag result is to restore windows to their original bounds.
  // Otherwise we fade out the copy before destroying it.
  ResetOtherWindow(/*show=*/*window_drag_result_ ==
                   ShelfWindowDragResult::kRestoreToOriginalBounds);

  window_drag_result_.reset();
  started_in_overview_ = false;
}

void DragWindowFromShelfController::UpdateDraggedWindow(
    const gfx::PointF& location_in_screen) {
  gfx::Rect bounds = window_->bounds();
  wm::ConvertRectToScreen(window_->parent(), &bounds);

  // Calculate the window's transform based on the location.
  // For scale, at |initial_location_in_screen_| or bounds.bottom(), the scale
  // is 1.0, and at the |min_y| position of its bounds, it reaches to its
  // minimum scale |kMinimumWindowScaleDuringDragging|. Calculate the desired
  // scale based on the current y position.
  const gfx::Rect display_bounds =
      display::Screen::GetScreen()
          ->GetDisplayNearestPoint(gfx::ToRoundedPoint(location_in_screen))
          .bounds();
  const float min_y = display_bounds.y() +
                      display_bounds.height() * kMinYDisplayHeightRatio +
                      kMinimumWindowScaleDuringDragging * bounds.height();
  float y_full =
      std::min(initial_location_in_screen_.y(), (float)bounds.bottom()) - min_y;
  float y_diff = location_in_screen.y() - min_y;
  float scale = (1.0f - kMinimumWindowScaleDuringDragging) * y_diff / y_full +
                kMinimumWindowScaleDuringDragging;
  scale = std::clamp(scale, /*min=*/kMinimumWindowScaleDuringDragging,
                     /*max=*/1.f);

  // Calculate the desired translation so that the dragged window stays under
  // the finger during the dragging.
  // Since vertical drag doesn't start until after passing the top of the shelf,
  // the y calculations should be relative to the window bounds instead of
  // |initial_location_in_screen| (which is on the shelf)
  gfx::Transform transform;
  transform.Translate(
      (location_in_screen.x() - bounds.x()) -
          (initial_location_in_screen_.x() - bounds.x()) * scale,
      (location_in_screen.y() - bounds.y()) - bounds.height() * scale);
  transform.Scale(scale, scale);

  // The dragged window cannot exceed the top or bottom of the display. So
  // calculate the expected transformed bounds and then adjust the transform if
  // needed.
  const gfx::Transform new_tranform =
      TransformAboutPivot(gfx::PointF(window_->bounds().origin()), transform);
  gfx::RectF transformed_bounds =
      new_tranform.MapRect(gfx::RectF(window_->bounds()));
  wm::TranslateRectToScreen(window_->parent(), &transformed_bounds);
  if (transformed_bounds.y() < display_bounds.y()) {
    transform.Translate(0,
                        (display_bounds.y() - transformed_bounds.y()) / scale);
  } else if (transformed_bounds.bottom() > bounds.bottom()) {
    DCHECK_EQ(1.f, scale);
    transform.Translate(
        0, (bounds.bottom() - transformed_bounds.bottom()) / scale);
  }

  window_util::SetTransform(window_, transform);

  if (other_window_copy_) {
    // When we have dragged 1/8th of the display height, the copy should be
    // fully faded out and shrunk.
    float copy_scale =
        (bounds.bottom() - location_in_screen.y()) /
        (display_bounds.height() / kOtherWindowFullFadeHeightRatio);
    copy_scale = 1.f - std::clamp(copy_scale, 0.f, 1.f);

    other_window_copy_->root()->SetOpacity(copy_scale);

    CHECK(other_window_);
    if (!WindowState::Get(other_window_)->IsFloated()) {
      const float copy_transform_scale =
          std::clamp(copy_scale, kOtherWindowMaxScale, 1.f);
      const gfx::Transform copy_transform = gfx::GetScaleTransform(
          other_window_copy_->root()->bounds().CenterPoint(),
          copy_transform_scale);
      other_window_copy_->root()->SetTransform(copy_transform);
    }
  }
}

SnapPosition DragWindowFromShelfController::GetSnapPosition(
    const gfx::PointF& location_in_screen) const {
  // if |location_in_screen| is close to the bottom of the screen and is
  // inside of GetReturnToMaximizedThreshold() threshold, we should not try to
  // snap the window.
  if (ShouldRestoreToOriginalBounds(location_in_screen, std::nullopt)) {
    return SnapPosition::kNone;
  }

  aura::Window* root_window = Shell::GetPrimaryRootWindow();
  SnapPosition snap_position = ::ash::GetSnapPosition(
      root_window, window_, gfx::ToRoundedPoint(location_in_screen),
      gfx::ToRoundedPoint(initial_location_in_screen_),
      /*snap_distance_from_edge=*/kDistanceFromEdge,
      /*minimum_drag_distance=*/kMinDragDistance,
      /*horizontal_edge_inset=*/kScreenEdgeInsetForSnap,
      /*vertical_edge_inset=*/kScreenEdgeInsetForSnap);

  // For portrait mode, since the drag starts from the bottom of the screen,
  // we should only allow the window to snap to the top of the screen.
  const bool is_landscape = IsCurrentScreenOrientationLandscape();
  const bool is_primary = IsCurrentScreenOrientationPrimary();
  if (!is_landscape &&
      ((is_primary && snap_position == SnapPosition::kSecondary) ||
       (!is_primary && snap_position == SnapPosition::kPrimary))) {
    snap_position = SnapPosition::kNone;
  }

  return snap_position;
}

bool DragWindowFromShelfController::ShouldRestoreToOriginalBounds(
    const gfx::PointF& location_in_screen,
    std::optional<float> velocity_y) const {
  const gfx::Rect display_bounds =
      display::Screen::GetScreen()
          ->GetDisplayNearestPoint(gfx::ToRoundedPoint(location_in_screen))
          .bounds();
  gfx::RectF transformed_window_bounds =
      window_util::GetTransformedBounds(window_, /*top_inset=*/0);

  // If overview is invisible when the drag ends with downward velocity, we
  // should restore to original bounds.
  if (Shell::Get()->overview_controller()->InOverviewSession() &&
      !show_overview_windows_ && velocity_y.has_value() &&
      velocity_y.value() > 0) {
    return true;
  }

  // Otherwise restore the bounds if the downward vertical velocity exceeds the
  // threshold, or if the bottom of the dragged window is within the
  // GetReturnToMaximizedThreshold() threshold.
  return (velocity_y.has_value() &&
          velocity_y.value() >= kVelocityToRestoreBoundsThreshold) ||
         transformed_window_bounds.bottom() >
             display_bounds.bottom() - GetReturnToMaximizedThreshold();
}

bool DragWindowFromShelfController::ShouldGoToHomeScreen(
    const gfx::PointF& location_in_screen,
    std::optional<float> velocity_y) const {
  // If the drag ends below the shelf, do not go to home screen (theoretically
  // it may happen in kExtended hotseat case when drag can start and end below
  // the shelf).
  if (location_in_screen.y() >=
      Shelf::ForWindow(window_)->GetIdealBoundsForWorkAreaCalculation().y()) {
    return false;
  }

  // Do not go home if we're in split screen.
  if (SplitViewController::Get(Shell::GetPrimaryRootWindow())
          ->InSplitViewMode()) {
    return false;
  }

  // If overview is invisible when the drag ends with upward velocity or no
  // velocity, we should go to home screen.
  if (Shell::Get()->overview_controller()->InOverviewSession() &&
      !show_overview_windows_ &&
      (!velocity_y.has_value() || velocity_y.value() <= 0)) {
    return true;
  }

  // Otherwise go home if the upward vertical velocity exceeds the threshold.
  return velocity_y.has_value() &&
         velocity_y.value() <= -kVelocityToHomeScreenThreshold;
}

SnapPosition DragWindowFromShelfController::GetSnapPositionOnDragEnd(
    const gfx::PointF& location_in_screen,
    std::optional<float> velocity_y) const {
  if (!Shell::Get()->overview_controller()->InOverviewSession() ||
      ShouldGoToHomeScreen(location_in_screen, velocity_y)) {
    return SnapPosition::kNone;
  }

  // When dragging ends but restore to original bounds, we should restore
  // window's initial snap position
  if (ShouldRestoreToOriginalBounds(location_in_screen, velocity_y))
    return initial_snap_position_;

  return GetSnapPosition(location_in_screen);
}

bool DragWindowFromShelfController::ShouldDropWindowInOverview(
    const gfx::PointF& location_in_screen,
    std::optional<float> velocity_y) const {
  if (!Shell::Get()->overview_controller()->InOverviewSession())
    return false;

  if (ShouldGoToHomeScreen(location_in_screen, velocity_y))
    return false;

  const bool in_splitview =
      SplitViewController::Get(Shell::GetPrimaryRootWindow())
          ->InSplitViewMode();
  if (!in_splitview &&
      ShouldRestoreToOriginalBounds(location_in_screen, velocity_y)) {
    return false;
  }

  if (in_splitview) {
    if (velocity_y.has_value() &&
        velocity_y.value() <= -kVelocityToOverviewThreshold) {
      return true;
    }
    if (ShouldRestoreToOriginalBounds(location_in_screen, velocity_y))
      return false;
  }

  return GetSnapPositionOnDragEnd(location_in_screen, velocity_y) ==
         SnapPosition::kNone;
}

void DragWindowFromShelfController::ReshowHiddenWindowsOnDragEnd() {
  windows_hider_->RestoreWindowsVisibility();
}

void DragWindowFromShelfController::ShowOverviewDuringOrAfterDrag() {
  show_overview_timer_.Stop();
  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (!overview_controller->InOverviewSession())
    return;

  show_overview_windows_ = true;
  overview_controller->overview_session()->SetVisibleDuringWindowDragging(
      /*visible=*/true, /*animate=*/true);
  if (on_overview_shown_callback_for_testing_)
    std::move(on_overview_shown_callback_for_testing_).Run();
}

void DragWindowFromShelfController::HideOverviewDuringDrag() {
  show_overview_windows_ = false;

  OverviewController* overview_controller = Shell::Get()->overview_controller();
  if (!overview_controller->InOverviewSession())
    return;
  overview_controller->overview_session()->SetVisibleDuringWindowDragging(
      /*visible=*/false,
      /*animate=*/false);
}

void DragWindowFromShelfController::ScaleDownWindowAfterDrag() {
  // Notify home screen controller that the home screen is about to be shown, so
  // home screen and shelf start updating their state as the window is
  // minimizing.
  Shell::Get()->app_list_controller()->OnHomeLauncherPositionChanged(
      /*percent_shown=*/100,
      display::Screen::GetScreen()->GetPrimaryDisplay().id());

  (new WindowScaleAnimation(
       WindowScaleAnimation::WindowScaleType::kScaleDownToShelf,
       base::BindOnce(
           &DragWindowFromShelfController::OnWindowScaledDownAfterDrag,
           weak_ptr_factory_.GetWeakPtr())))
      ->Start(window_);
}

void DragWindowFromShelfController::OnWindowScaledDownAfterDrag() {
  AppListControllerImpl* app_list_controller =
      Shell::Get()->app_list_controller();
  if (!app_list_controller)
    return;

  app_list_controller->OnHomeLauncherAnimationComplete(
      /*shown=*/true, display::Screen::GetScreen()->GetPrimaryDisplay().id());
}

void DragWindowFromShelfController::ScaleUpToRestoreWindowAfterDrag() {
  (new WindowScaleAnimation(
       WindowScaleAnimation::WindowScaleType::kScaleUpToRestore,
       base::BindOnce(
           &DragWindowFromShelfController::OnWindowRestoredToOriginalBounds,
           weak_ptr_factory_.GetWeakPtr(),
           /*should_end_overview=*/!started_in_overview_)))
      ->Start(window_);
}

void DragWindowFromShelfController::OnWindowRestoredToOriginalBounds(
    bool end_overview) {
  base::AutoReset<bool> auto_reset(&during_window_restoration_, true);
  if (end_overview) {
    Shell::Get()->overview_controller()->EndOverview(
        OverviewEndAction::kDragWindowFromShelf,
        OverviewEnterExitType::kImmediateExit);
  }
  ReshowHiddenWindowsOnDragEnd();
}

void DragWindowFromShelfController::OnWindowDragStartedInOverview() {
  OverviewSession* overview_session =
      Shell::Get()->overview_controller()->overview_session();
  DCHECK(overview_session);
  overview_session->OnWindowDragStarted(window_, /*animate=*/false);
  if (ShouldAllowSplitView())
    overview_session->SetSplitViewDragIndicatorsDraggedWindow(window_);
  // Hide overview windows first and fade in the windows after delaying
  // kShowOverviewTimeWhenDragSuspend.
  HideOverviewDuringDrag();
}

void DragWindowFromShelfController::ResetOtherWindow(std::optional<bool> show) {
  if (other_window_) {
    other_window_->RemoveObserver(this);
    other_window_ = nullptr;

    if (show.has_value()) {
      DCHECK(other_window_copy_);

      // We can skip the animation if the copy is already dragged to its final
      // opacity and transform (if showing). This can happen since the copy is
      // fully transparent after dragging more than 1/8th of the display height.
      ui::Layer* layer = other_window_copy_->root();
      if (show.value()) {
        if (layer->GetTargetOpacity() != 1.f &&
            layer->GetTargetTransform() != gfx::Transform()) {
          new OtherWindowCopyAnimation(std::move(other_window_copy_),
                                       /*show=*/true);
        }
      } else {
        if (layer->GetTargetOpacity() != 0.f) {
          new OtherWindowCopyAnimation(std::move(other_window_copy_),
                                       /*show=*/false);
        }
      }
    }
  }
  other_window_copy_.reset();
}

}  // namespace ash