chromium/ash/wm/overview/overview_grid.cc

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

#include "ash/wm/overview/overview_grid.h"

#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/metrics/histogram_macros.h"
#include "ash/public/cpp/desk_template.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/saved_desk_delegate.h"
#include "ash/public/cpp/shelf_config.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/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/rounded_label_widget.h"
#include "ash/system/toast/toast_manager_impl.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "ash/wm/desks/default_desk_button.h"
#include "ash/wm/desks/desk_bar_view_base.h"
#include "ash/wm/desks/desk_icon_button.h"
#include "ash/wm/desks/desk_mini_view.h"
#include "ash/wm/desks/desk_mini_view_animations.h"
#include "ash/wm/desks/desk_name_view.h"
#include "ash/wm/desks/desks_constants.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/desks/overview_desk_bar_view.h"
#include "ash/wm/desks/templates/saved_desk_animations.h"
#include "ash/wm/desks/templates/saved_desk_grid_view.h"
#include "ash/wm/desks/templates/saved_desk_library_view.h"
#include "ash/wm/desks/templates/saved_desk_metrics_util.h"
#include "ash/wm/desks/templates/saved_desk_name_view.h"
#include "ash/wm/desks/templates/saved_desk_presenter.h"
#include "ash/wm/desks/templates/saved_desk_save_desk_button.h"
#include "ash/wm/desks/templates/saved_desk_save_desk_button_container.h"
#include "ash/wm/desks/templates/saved_desk_util.h"
#include "ash/wm/gestures/wm_gesture_handler.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/birch/birch_bar_controller.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_drop_target.h"
#include "ash/wm/overview/overview_grid_event_handler.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_item_base.h"
#include "ash/wm/overview/overview_item_view.h"
#include "ash/wm/overview/overview_metrics.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_types.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/overview/overview_window_drag_controller.h"
#include "ash/wm/overview/scoped_overview_animation_settings.h"
#include "ash/wm/overview/scoped_overview_hide_windows.h"
#include "ash/wm/overview/scoped_overview_wallpaper_clipper.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_overview_session.h"
#include "ash/wm/splitview/split_view_setup_view.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_restore/informed_restore_contents_view.h"
#include "ash/wm/window_restore/informed_restore_controller.h"
#include "ash/wm/window_state_delegate.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_constants.h"
#include "ash/wm/workspace/backdrop_controller.h"
#include "ash/wm/workspace/workspace_layout_manager.h"
#include "ash/wm/workspace_controller.h"
#include "base/containers/adapters.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "base/trace_event/trace_event.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/wm/window_util.h"
#include "components/app_restore/full_restore_utils.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/compositor_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/compositor/throughput_tracker.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/range/range_f.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/highlight_border.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {
namespace {

// Values for the no items indicator which appears when opening overview mode
// with no opened windows.
constexpr int kNoItemsIndicatorHeightDp = 32;
constexpr int kNoItemsIndicatorHorizontalPaddingDp = 16;
constexpr int kNoItemsIndicatorRoundingDp = 16;
constexpr int kNoItemsIndicatorVerticalPaddingDp = 8;

// Distance from the bottom of the save desk as template button to the top of
// the first overview item.
constexpr int kSaveDeskAsTemplateOverviewItemSpacingDp = 45;

// Distance from the bottom of the last overview item to the top of the split
// view setup view toast widget.
constexpr int kSplitViewSetupToastSpacingDp = 40;

// Distance between the bottom of the toast and the bottom of the work area
// which will be the top edge of the shelf if it is shown.
constexpr int kMinimumDistanceBetweenToastAndWorkAreaDp = 8;

// Windows are not allowed to get taller than this.
constexpr int kMaxHeight = 512;

// Margins reserved in the overview mode.
constexpr float kOverviewInsetRatio = 0.05f;

// Additional vertical inset reserved for windows in overview mode.
constexpr float kOverviewVerticalInset = 0.1f;

// Number of rows for windows in tablet overview mode.
constexpr int kScrollingLayoutRow = 2;

constexpr int kMinimumItemsForScrollingLayout = 6;

constexpr int kTabletModeOverviewItemTopPaddingDp = 16;

// The bottom padding applied to the bottom of the birch bar.
constexpr int kBirchBarBottomPadding = 16;

constexpr float kInformedRestoreDialogInitScale = 1.2f;

// Wait a while before unpausing the occlusion tracker after a scroll has
// completed as the user may start another scroll.
constexpr base::TimeDelta kOcclusionUnpauseDurationForScroll =
    base::Milliseconds(500);

constexpr base::TimeDelta kOcclusionUnpauseDurationForRotation =
    base::Milliseconds(300);

// Toast id for the toast that is displayed when a user tries to move a window
// that is visible on all desks to another desk.
constexpr char kMoveVisibleOnAllDesksWindowToastId[] =
    "ash.wm.overview.move_visible_on_all_desks_window_toast";

// Histogram names for overview enter/exit smoothness in clamshell,
// tablet mode and splitview.
constexpr char kOverviewEnterClamshellHistogram[] =
    "Ash.Overview.AnimationSmoothness.Enter.ClamshellMode";
constexpr char kOverviewEnterSingleClamshellHistogram[] =
    "Ash.Overview.AnimationSmoothness.Enter.SingleClamshellMode";
constexpr char kOverviewEnterTabletHistogram[] =
    "Ash.Overview.AnimationSmoothness.Enter.TabletMode";
constexpr char kOverviewEnterMinimizedTabletHistogram[] =
    "Ash.Overview.AnimationSmoothness.Enter.MinimizedTabletMode";
constexpr char kOverviewEnterSplitViewHistogram[] =
    "Ash.Overview.AnimationSmoothness.Enter.SplitView";

constexpr char kOverviewExitClamshellHistogram[] =
    "Ash.Overview.AnimationSmoothness.Exit.ClamshellMode";
constexpr char kOverviewExitSingleClamshellHistogram[] =
    "Ash.Overview.AnimationSmoothness.Exit.SingleClamshellMode";
constexpr char kOverviewExitTabletHistogram[] =
    "Ash.Overview.AnimationSmoothness.Exit.TabletMode";
constexpr char kOverviewExitMinimizedTabletHistogram[] =
    "Ash.Overview.AnimationSmoothness.Exit.MinimizedTabletMode";
constexpr char kOverviewExitSplitViewHistogram[] =
    "Ash.Overview.AnimationSmoothness.Exit.SplitView";

// The UMA histogram that records presentation time for grid scrolling in the
// new overview layout.
constexpr char kOverviewScrollHistogram[] =
    "Ash.Overview.Scroll.PresentationTime.TabletMode";
constexpr char kOverviewScrollMaxLatencyHistogram[] =
    "Ash.Overview.Scroll.PresentationTime.MaxLatency.TabletMode";

template <const char* clamshell_single_name,
          const char* clamshell_multi_name,
          const char* tablet_name,
          const char* splitview_name,
          const char* tablet_minimized_name>
class OverviewMetricsTracker : public OverviewGrid::MetricsTracker {
 public:
  OverviewMetricsTracker(ui::Compositor* compositor,
                         bool in_split_view,
                         bool single_animation_in_clamshell,
                         bool minimized_in_tablet)
      : tracker_(compositor->RequestNewThroughputTracker()) {
    tracker_.Start(metrics_util::ForSmoothnessV3(base::BindRepeating(
        &OverviewMetricsTracker::ReportOverviewSmoothness, in_split_view,
        single_animation_in_clamshell, minimized_in_tablet)));
  }
  OverviewMetricsTracker(const OverviewMetricsTracker&) = delete;
  OverviewMetricsTracker& operator=(const OverviewMetricsTracker&) = delete;
  ~OverviewMetricsTracker() override { tracker_.Stop(); }

  static void ReportOverviewSmoothness(bool in_split_view,
                                       bool single_animation_in_clamshell,
                                       bool minimized_in_tablet,
                                       int smoothness) {
    if (single_animation_in_clamshell)
      UMA_HISTOGRAM_PERCENTAGE_IN_CLAMSHELL(clamshell_single_name, smoothness);
    else
      UMA_HISTOGRAM_PERCENTAGE_IN_CLAMSHELL(clamshell_multi_name, smoothness);

    if (minimized_in_tablet) {
      UMA_HISTOGRAM_PERCENTAGE_IN_TABLET_NON_SPLITVIEW(
          in_split_view, tablet_minimized_name, smoothness);
    } else {
      UMA_HISTOGRAM_PERCENTAGE_IN_TABLET_NON_SPLITVIEW(in_split_view,
                                                       tablet_name, smoothness);
    }
    UMA_HISTOGRAM_PERCENTAGE_IN_SPLITVIEW(in_split_view, splitview_name,
                                          smoothness);
  }

 private:
  ui::ThroughputTracker tracker_;
};

using OverviewEnterMetricsTracker =
    OverviewMetricsTracker<kOverviewEnterSingleClamshellHistogram,
                           kOverviewEnterClamshellHistogram,
                           kOverviewEnterTabletHistogram,
                           kOverviewEnterSplitViewHistogram,
                           kOverviewEnterMinimizedTabletHistogram>;
using OverviewExitMetricsTracker =
    OverviewMetricsTracker<kOverviewExitSingleClamshellHistogram,
                           kOverviewExitClamshellHistogram,
                           kOverviewExitTabletHistogram,
                           kOverviewExitSplitViewHistogram,
                           kOverviewExitMinimizedTabletHistogram>;

class ShutdownAnimationMetricsTrackerObserver : public OverviewObserver,
                                                public ui::CompositorObserver {
 public:
  ShutdownAnimationMetricsTrackerObserver(ui::Compositor* compositor,
                                          bool in_split_view,
                                          bool single_animation,
                                          bool minimized_in_tablet)
      : compositor_(compositor),
        metrics_tracker_(compositor,
                         in_split_view,
                         single_animation,
                         minimized_in_tablet) {
    compositor->AddObserver(this);
    OverviewController::Get()->AddObserver(this);
  }
  ShutdownAnimationMetricsTrackerObserver(
      const ShutdownAnimationMetricsTrackerObserver&) = delete;
  ShutdownAnimationMetricsTrackerObserver& operator=(
      const ShutdownAnimationMetricsTrackerObserver&) = delete;
  ~ShutdownAnimationMetricsTrackerObserver() override {
    compositor_->RemoveObserver(this);
    if (OverviewController* overview_controller =
            Shell::Get()->overview_controller()) {
      overview_controller->RemoveObserver(this);
    }
  }

  // OverviewObserver:
  void OnOverviewModeEndingAnimationComplete(bool canceled) override {
    delete this;
  }

  void OnCompositingShuttingDown(ui::Compositor* compositor) override {
    DCHECK_EQ(compositor_, compositor);
    delete this;
  }

 private:
  raw_ptr<ui::Compositor> compositor_;
  OverviewExitMetricsTracker metrics_tracker_;
};

// Creates `save_desk_button_container_widget_`. It contains SaveDeskAsTemplate
// button and save for later button.
std::unique_ptr<views::Widget> CreateSaveDeskButtonContainerWidget(
    aura::Window* root_window) {
  views::Widget::InitParams params(
      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.name = "SaveDeskButtonContainerWidget";
  params.accept_events = true;
  // This widget is hidden during window dragging, but will become visible on
  // mouse/touch release. Place it in the active desk container so it remains
  // beneath the dragged window when it is animating back to the overview grid.
  params.parent = desks_util::GetActiveDeskContainerForRoot(root_window);
  params.init_properties_container.SetProperty(kHideInDeskMiniViewKey, true);
  // This should not show up in the MRU list. Otherwise, it will be treated as
  // unsupported crostini app.
  params.init_properties_container.SetProperty(kOverviewUiKey, true);

  auto widget = std::make_unique<views::Widget>();
  widget->set_focus_on_creation(false);
  widget->Init(std::move(params));
  // Turn off default widget animations.
  widget->SetVisibilityAnimationTransition(views::Widget::ANIMATE_NONE);

  aura::Window* window = widget->GetNativeWindow();
  window->parent()->StackChildAtBottom(window);
  return widget;
}

float GetWantedDropTargetOpacity(
    SplitViewDragIndicators::WindowDraggingState window_dragging_state) {
  switch (window_dragging_state) {
    case SplitViewDragIndicators::WindowDraggingState::kNoDrag:
    case SplitViewDragIndicators::WindowDraggingState::kOtherDisplay:
    case SplitViewDragIndicators::WindowDraggingState::kToSnapPrimary:
    case SplitViewDragIndicators::WindowDraggingState::kToSnapSecondary:
      return 0.f;
    case SplitViewDragIndicators::WindowDraggingState::kFromOverview:
    case SplitViewDragIndicators::WindowDraggingState::kFromTop:
    case SplitViewDragIndicators::WindowDraggingState::kFromShelf:
      return 1.f;
    case SplitViewDragIndicators::WindowDraggingState::kFromFloat:
      NOTREACHED();
  }
}

gfx::Insets GetGridInsetsImpl(const gfx::Rect& grid_bounds) {
  const int horizontal_inset =
      base::ClampFloor(kOverviewInsetRatio *
                       std::min(grid_bounds.width(), grid_bounds.height()));
  const int vertical_inset =
      horizontal_inset +
      kOverviewVerticalInset * (grid_bounds.height() - 2 * horizontal_inset);

  return gfx::Insets::VH(std::max(0, vertical_inset),
                         std::max(0, horizontal_inset));
}

bool ShouldExcludeItemFromGridLayout(
    OverviewItemBase* item,
    const base::flat_set<OverviewItemBase*>& ignored_items) {
  return item->animating_to_close() || ignored_items.contains(item);
}

bool IsUnsupportedWindow(aura::Window* window) {
  const bool has_restore_id =
      !wm::GetTransientParent(window) &&
      (OverviewController::Get()->disable_app_id_check_for_saved_desks() ||
       !saved_desk_util::GetAppId(window).empty());

  return !has_restore_id ||
         !Shell::Get()->saved_desk_delegate()->IsWindowSupportedForSavedDesk(
             window);
}

bool IsIncognitoWindow(aura::Window* window) {
  return !Shell::Get()->saved_desk_delegate()->IsWindowPersistable(window);
}

// Returns the window(s) associated with dragging which can be the window(s)
// represented by the `OverviewItemBase` or a preset `dragged_window`.
aura::Window::Windows GetWindowsAssociatedWithDragging(
    OverviewItemBase* grid_dragged_item,
    aura::Window* dragged_window) {
  return grid_dragged_item ? grid_dragged_item->GetWindows()
                           : aura::Window::Windows({dragged_window});
}

// Returns true if all the `windows` associated with the drag are not null and
// have parent.
bool AreDraggedWindowsValid(const aura::Window::Windows& windows) {
  for (const aura::Window* window : windows) {
    if (!window || !window->parent()) {
      return false;
    }
  }

  return true;
}

// Returns true if all the `windows` associated with the drag are maximized.
bool AreAllWindowsMaximized(const aura::Window::Windows& windows) {
  for (const aura::Window* window : windows) {
    if (!WindowState::Get(window)->IsMaximized()) {
      return false;
    }
  }

  return true;
}

// Returns the total size of the `windows` associated with the drag.
gfx::Size GetTotalDraggedWindowsSize(const aura::Window::Windows& windows) {
  gfx::Rect total_bounds;
  for (aura::Window* win : windows) {
    total_bounds.Union(win->bounds());
  }

  return total_bounds.size();
}

// Returns the total size of the given `windows` including the transient
// children. If the given `windows` belong to the same snap group, the total
// size needs to be enlarged to include the size of the divider.
gfx::SizeF GetTotalUnionSizeIncludingTransients(
    const aura::Window::Windows& windows) {
  gfx::RectF total_bounds;
  for (aura::Window* win : windows) {
    total_bounds.Union(GetUnionScreenBoundsForWindow(win));
  }

  gfx::SizeF total_size = total_bounds.size();
  // TODO(michelefan): Add extra width of the divider for the height of the
  // `total_size` in portrait mode.
  if (windows.size() == 2u) {
    total_size.Enlarge(kSplitviewDividerShortSideLength, 0);
  }

  return total_size;
}

// Returns the maximum of the `aura::client::kTopViewInset` among the `windows`.
int GetTopViewInset(const aura::Window::Windows& windows) {
  int inset = 0;
  for (aura::Window* win : windows) {
    const int win_inset = win->GetProperty(aura::client::kTopViewInset);
    inset = std::max(inset, win_inset);
  }

  return inset;
}

// Returns the bottom padding of birch bar according to the appearance of the
// home launcher.
int GetBirchBarBottomPadding(aura::Window* root_window) {
  // There is no padding when home launcher shows.
  return Shelf::ForWindow(root_window)
                     ->shelf_layout_manager()
                     ->hotseat_state() == HotseatState::kShownHomeLauncher
             ? 0
             : kBirchBarBottomPadding;
}

// Returns the corresponding `SplitViewOverviewSessionExitPoint` with the
// overview end action deduced from the given `overview_session`.
SplitViewOverviewSessionExitPoint GetSplitViewOverviewSessionExitPoint(
    OverviewSession* overview_session) {
  OverviewEndAction overview_end_action =
      overview_session->overview_end_action();
  if (overview_end_action == OverviewEndAction::kWindowActivating) {
    return SplitViewOverviewSessionExitPoint::kCompleteByActivating;
  } else if (overview_end_action == OverviewEndAction::kKeyEscapeOrBack ||
             overview_end_action ==
                 OverviewEndAction::kClickingOutsideWindowsInOverview) {
    return SplitViewOverviewSessionExitPoint::kSkip;
  } else if (overview_end_action == OverviewEndAction::kShuttingDown) {
    return SplitViewOverviewSessionExitPoint::kShutdown;
  }
  return SplitViewOverviewSessionExitPoint::kUnspecified;
}

// Returns false if any of the items in `grid` covers the entire workspace, true
// otherwise.
bool ShouldAnimateWallpaper(OverviewGrid* grid) {
  // Do not animate wallpaper if enter exit type is immediate.
  const OverviewEnterExitType enter_exit_type =
      grid->overview_session()->enter_exit_overview_type();
  if (enter_exit_type == OverviewEnterExitType::kImmediateEnter ||
      enter_exit_type == OverviewEnterExitType::kImmediateEnterWithoutFocus ||
      enter_exit_type == OverviewEnterExitType::kImmediateExit) {
    return false;
  }

  // If one of the windows covers the workspace, we can skip animating the
  // wallpaper.
  for (const auto& overview_item : grid->item_list()) {
    if (CanCoverAvailableWorkspace(overview_item->GetWindow())) {
      return false;
    }
  }

  return true;
}

// Returns true if the birch bar should be shown in current state.
bool ShouldShowBirchBar(aura::Window* root_window) {
  // The birch bar should not be shown in tablet mode, partial split view,
  // the forest feature is disabled, non-primary users, or the birch bars are
  // disabled by users. We don't need to worry about showing/hiding the bar
  // dynamically on primary/secondary user switch because we exit overview when
  // we switch users.
  return features::IsForestFeatureEnabled() &&
         Shell::Get()->session_controller()->IsUserPrimary() &&
         BirchBarController::Get()->GetShowBirchSuggestions() &&
         !SplitViewController::Get(root_window)->InSplitViewMode();
}

bool ShouldShowInformedRestoreDialog(aura::Window* root_window) {
  return root_window == Shell::GetPrimaryRootWindow() &&
         features::IsForestFeatureEnabled() &&
         !!Shell::Get()->informed_restore_controller()->contents_data();
}

enum class TooltipStatus {
  kOk = 0,
  kReachMax,
  kIncognitoWindow,
  kUnsupportedWindow,
  kIncognitoAndUnsupportedWindow,
  kNumberOfTooltipStatus,
};

constexpr std::array<int,
                     static_cast<int>(TooltipStatus::kNumberOfTooltipStatus)>
    kSaveAsTemplateButtonTooltipIDs = {
        IDS_ASH_DESKS_TEMPLATES_SAVE_DESK_AS_TEMPLATE_BUTTON,
        IDS_ASH_DESKS_TEMPLATES_MAX_TEMPLATES_TOOLTIP,
        IDS_ASH_DESKS_TEMPLATES_UNSUPPORTED_INCOGNITO_TOOLTIP,
        IDS_ASH_DESKS_TEMPLATES_UNSUPPORTED_LINUX_APPS_TOOLTIP,
        IDS_ASH_DESKS_TEMPLATES_UNSUPPORTED_LINUX_APPS_AND_INCOGNITO_TOOLTIP,
};

constexpr std::array<int,
                     static_cast<int>(TooltipStatus::kNumberOfTooltipStatus)>
    kSaveForLaterButtonTooltipIDs = {
        IDS_ASH_DESKS_TEMPLATES_SAVE_DESK_FOR_LATER_BUTTON,
        IDS_ASH_DESKS_TEMPLATES_MAX_SAVED_DESKS_TOOLTIP,
        IDS_ASH_DESKS_TEMPLATES_UNSUPPORTED_INCOGNITO_TOOLTIP,
        IDS_ASH_DESKS_TEMPLATES_UNSUPPORTED_LINUX_APPS_TOOLTIP,
        IDS_ASH_DESKS_TEMPLATES_UNSUPPORTED_LINUX_APPS_AND_INCOGNITO_TOOLTIP,
};

int GetTooltipID(DeskTemplateType type, TooltipStatus status) {
  switch (type) {
    case DeskTemplateType::kTemplate:
      return kSaveAsTemplateButtonTooltipIDs[static_cast<int>(status)];
    case DeskTemplateType::kSaveAndRecall:
      return kSaveForLaterButtonTooltipIDs[static_cast<int>(status)];
    case DeskTemplateType::kFloatingWorkspace:
    case DeskTemplateType::kUnknown:
      NOTREACHED();
  }
}

}  // namespace

OverviewGrid::OverviewGrid(
    aura::Window* root_window,
    const std::vector<raw_ptr<aura::Window, VectorExperimental>>& windows,
    OverviewSession* overview_session,
    base::WeakPtr<WindowOcclusionCalculator> window_occlusion_calculator)
    : root_window_(root_window),
      overview_session_(overview_session),
      split_view_drag_indicators_(
          ShouldAllowSplitView()
              ? std::make_unique<SplitViewDragIndicators>(root_window)
              : nullptr),
      bounds_(GetGridBoundsInScreen(root_window)),
      window_occlusion_calculator_(window_occlusion_calculator) {
  TRACE_EVENT0("ui", "OverviewGrid::OverviewGrid");

  for (aura::Window* window : windows) {
    if (window->GetRootWindow() != root_window)
      continue;

    // Stop ongoing animations before entering overview mode. Because we are
    // deferring SetTransform of the windows beneath the window covering the
    // available workspace, we need to set the correct transforms of these
    // windows before entering overview mode again in the
    // OnImplicitAnimationsCompleted() of the observer of the
    // available-workspace-covering window's animation.
    if (auto* animator = window->layer()->GetAnimator();
        animator && animator->is_animating()) {
      animator->StopAnimating();
    }

    std::unique_ptr<OverviewItemBase> overview_item_base =
        OverviewItemBase::Create(window, overview_session_, this);
    UpdateNumSavedDeskUnsupportedWindows(overview_item_base->GetWindows(),
                                         /*increment=*/true);
    item_list_.push_back(std::move(overview_item_base));
  }
}

OverviewGrid::~OverviewGrid() = default;

void OverviewGrid::Shutdown(OverviewEnterExitType exit_type) {
  TRACE_EVENT0("ui", "OverviewGrid::Shutdown");

  EndNudge();

  auto* root_controller = RootWindowController::ForWindow(root_window_);
  root_controller->EndSplitViewOverviewSession(
      GetSplitViewOverviewSessionExitPoint(overview_session_));
  SplitViewController::Get(root_window_)->RemoveObserver(this);
  if (auto* animator = root_controller->GetScreenRotationAnimator()) {
    animator->RemoveObserver(this);
  }

  Shell::Get()->wallpaper_controller()->RemoveObserver(this);
  grid_event_handler_.reset();

  if (IsShowingSavedDeskLibrary())
    HideSavedDeskLibrary(/*exit_overview=*/true);

  bool has_non_cover_animating = false;
  int animate_count = 0;

  for (const auto& item : item_list_) {
    if (item->should_animate_when_exiting() && !has_non_cover_animating) {
      has_non_cover_animating |= !CanCoverAvailableWorkspace(item->GetWindow());
      animate_count++;
    }
    item->Shutdown();
  }

  const bool in_split_view =
      SplitViewController::Get(root_window_)->InSplitViewMode();
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  const bool should_report_split_view_metrics =
      in_split_view ||
      (snap_group_controller &&
       snap_group_controller->GetTopmostVisibleSnapGroup(root_window_));
  // OverviewGrid in splitscreen does not include the window to be activated.
  if (!item_list_.empty() || should_report_split_view_metrics) {
    const bool minimized_in_tablet =
        overview_session_->enter_exit_overview_type() ==
        OverviewEnterExitType::kFadeOutExit;
    const bool single_animation_in_clamshell =
        (animate_count == 1 && !has_non_cover_animating) &&
        !display::Screen::GetScreen()->InTabletMode();
    // The following instance self-destructs when shutdown animation ends.
    new ShutdownAnimationMetricsTrackerObserver(
        root_window_->layer()->GetCompositor(),
        should_report_split_view_metrics, single_animation_in_clamshell,
        minimized_in_tablet);
  }

  drop_target_ = nullptr;
  item_list_.clear();

  overview_session_ = nullptr;

  if (no_windows_widget_) {
    if (exit_type == OverviewEnterExitType::kImmediateExit) {
      ImmediatelyCloseWidgetOnExit(std::move(no_windows_widget_));
    } else {
      // Fade out the no windows widget. This animation continues past the
      // lifetime of `this`.
      FadeOutWidgetFromOverview(std::move(no_windows_widget_),
                                OVERVIEW_ANIMATION_RESTORE_WINDOW);
    }
  }

  if (informed_restore_widget_) {
    if (exit_type == OverviewEnterExitType::kImmediateExit) {
      ImmediatelyCloseWidgetOnExit(std::move(informed_restore_widget_));
    } else {
      // This animation continues past the lifetime of `this`.
      FadeOutWidgetFromOverview(std::move(informed_restore_widget_),
                                OVERVIEW_ANIMATION_EXIT_OVERVIEW_MODE_FADE_OUT);
    }
  }

  // After this, the desk bar widget will not be owned by this overview grid
  // anymore.
  if (desks_widget_) {
    if (exit_type != OverviewEnterExitType::kImmediateExit) {
      PerformDeskBarSlideAnimation(std::move(desks_widget_),
                                   desks_bar_view_->IsZeroState());
    }
    desks_widget_.reset();
    desks_bar_view_ = nullptr;
  }

  if (birch_bar_widget_) {
    // Cache the widget since we may need to pass the ownership to animation
    // observer.
    auto birch_bar_widget = std::move(birch_bar_widget_);
    // Destroy the birch bar widget to clear the related pointers before
    // fade-out animation to avoid dangling ptrs.
    DestroyBirchBarWidget();
    if (exit_type != OverviewEnterExitType::kInformedRestore &&
        exit_type != OverviewEnterExitType::kImmediateExit) {
      FadeOutWidgetFromOverview(
          std::move(birch_bar_widget),
          OVERVIEW_ANIMATION_EXIT_OVERVIEW_MODE_BIRCH_BAR_FADE_OUT);
    }
  }
}

void OverviewGrid::PrepareForOverview() {
  const bool should_animate_wallpaper = ShouldAnimateWallpaper(this);
  if (!should_animate_wallpaper) {
    MaybeInitDesksWidget();
  }

  MaybeInitBirchBarWidget();

  OverviewEnterExitType enter_exit_type =
      overview_session_->enter_exit_overview_type();

  if (features::IsForestFeatureEnabled()) {
    auto animation_type =
        ScopedOverviewWallpaperClipper::AnimationType::kEnterOverview;
    if (!should_animate_wallpaper) {
      animation_type = ScopedOverviewWallpaperClipper::AnimationType::kNone;
    } else if (enter_exit_type == OverviewEnterExitType::kInformedRestore) {
      animation_type =
          ScopedOverviewWallpaperClipper::AnimationType::kEnterInformedRestore;
    }
    scoped_overview_wallpaper_clipper_ =
        std::make_unique<ScopedOverviewWallpaperClipper>(this, animation_type);
  }

  // TODO(b/326434696): Currently this will return false if there is no restore
  // data in the pine contents data. Show the zero-state dialog.
  if (ShouldShowInformedRestoreDialog(root_window_)) {
    informed_restore_widget_ =
        InformedRestoreContentsView::Create(GetGridEffectiveBounds());
    informed_restore_widget_->ShowInactive();

    // If the enter type is immediate, `ShowInactive()` is sufficient as
    // `informed_restore_widget_` has no default animation. Otherwise, set the
    // opacity to 0.f and perform a fade in animation.
    if (enter_exit_type != OverviewEnterExitType::kImmediateEnter) {
      auto* widget_layer = informed_restore_widget_->GetLayer();
      widget_layer->SetOpacity(0.f);
      widget_layer->SetTransform(gfx::TransformAboutPivot(
          gfx::RectF(widget_layer->size()).CenterPoint(),
          gfx::Transform::MakeScale(kInformedRestoreDialogInitScale)));
      FadeInAndTransformWidgetToOverview(
          informed_restore_widget_.get(), gfx::Transform(),
          OVERVIEW_ANIMATION_SHOW_INFORMED_RESTORE_DIALOG_ON_ENTER,
          /*observe=*/true);
    }
  }

  for (const auto& item : item_list_) {
    item->PrepareForOverview();
  }

  SplitViewController::Get(root_window_)->AddObserver(this);
  if (display::Screen::GetScreen()->InTabletMode()) {
    if (auto* animator = RootWindowController::ForWindow(root_window_)
                             ->GetScreenRotationAnimator()) {
      animator->AddObserver(this);
    }
  }

  grid_event_handler_ = std::make_unique<OverviewGridEventHandler>(this);
  Shell::Get()->wallpaper_controller()->AddObserver(this);
}

void OverviewGrid::PositionWindowsContinuously(float y_offset) {
  const float scroll_ratio = y_offset / WmGestureHandler::kVerticalThresholdDp;

  // Move the desks bar up/down.
  if (desks_bar_view_) {
    desks_bar_view_->layer()->SetTransform(gfx::Transform::MakeTranslation(
        0, desks_bar_view_->height() * (scroll_ratio - 1)));
  }

  // Compute and adjust the "No recent items" label.
  if (no_windows_widget_) {
    no_windows_widget_->GetLayer()->SetOpacity(
        std::clamp(0.01f, scroll_ratio, 1.f));
  }

  if (!cached_transforms_.empty()) {
    // TODO(http://b/297923747): Integrate continuous animation with snap
    // groups.
    for (const auto& [overview_item, transform] : cached_transforms_) {
      overview_item->OnOverviewItemContinuousScroll(transform, scroll_ratio);
    }
    return;
  }

  if (item_list_.empty()) {
    return;
  }

  // The first time we call this function we want to compute the target
  // transforms.
  const std::vector<gfx::RectF> target_rects =
      ShouldUseScrollingLayout(/*ignored_items_size=*/0u)
          ? GetWindowRectsForScrollingLayout({})
          : GetWindowRects({});
  CHECK_EQ(item_list_.size(), target_rects.size());

  for (size_t i = 0; i < item_list_.size(); ++i) {
    OverviewItemBase* overview_item = item_list_[i].get();
    cached_transforms_[overview_item] =
        overview_item->ComputeTargetTransform(target_rects[i]);

    if (WindowState::Get(overview_item->GetWindow())->IsMinimized()) {
      overview_item->SetBounds(target_rects[i], OVERVIEW_ANIMATION_NONE);
    } else {
      // Keep the overview header, backdrop, etc. and rounded corners and shadow
      // hidden during the trackpad swipe. They will be shown after the swipe is
      // completed.
      overview_item->item_widget()->GetLayer()->SetOpacity(0.f);
      overview_item->UpdateRoundedCornersAndShadow();
    }
  }

  // When starting a continuous scroll to EXIT overview mode, hide the save
  // desk button immediately.
  // When starting a continuous scroll to ENTER overview mode, the save desk
  // button will be shown once overview mode is fully entered.
  if (IsSaveDeskButtonContainerVisible()) {
    UpdateSaveDeskButtons();
  }
}

void OverviewGrid::PositionWindows(
    bool animate,
    const base::flat_set<OverviewItemBase*>& ignored_items,
    OverviewTransition transition) {
  if (!overview_session_ || suspend_reposition_) {
    return;
  }

  if (item_list_.empty()) {
    return;
  }

  DCHECK_NE(transition, OverviewTransition::kExit);

  std::vector<gfx::RectF> rects =
      ShouldUseScrollingLayout(ignored_items.size())
          ? GetWindowRectsForScrollingLayout(ignored_items)
          : GetWindowRects(ignored_items);

  if (transition == OverviewTransition::kEnter) {
    CalculateWindowListAnimationStates(/*selected_item=*/nullptr, transition,
                                       rects);
  }

  // Position the windows centering the left-aligned rows vertically. Do not
  // position items in |ignored_items|.
  OverviewAnimationType animation_type = OVERVIEW_ANIMATION_NONE;
  switch (transition) {
    case OverviewTransition::kEnter: {
      const bool entering_from_home =
          overview_session_->enter_exit_overview_type() ==
          OverviewEnterExitType::kFadeInEnter;
      animation_type = entering_from_home
                           ? OVERVIEW_ANIMATION_ENTER_FROM_HOME_LAUNCHER
                           : OVERVIEW_ANIMATION_LAYOUT_OVERVIEW_ITEMS_ON_ENTER;
      break;
    }
    case OverviewTransition::kInOverview:
      animation_type = OVERVIEW_ANIMATION_LAYOUT_OVERVIEW_ITEMS_IN_OVERVIEW;
      break;
    case OverviewTransition::kExit:
      NOTREACHED();
  }

  int animate_count = 0;
  bool has_non_cover_animating = false;
  std::vector<OverviewAnimationType> animation_types(rects.size());

  const bool can_do_spawn_animation =
      animate && transition == OverviewTransition::kInOverview;

  for (size_t i = 0; i < item_list_.size(); ++i) {
    OverviewItemBase* window_item = item_list_[i].get();
    if (ShouldExcludeItemFromGridLayout(window_item, ignored_items)) {
      rects[i].SetRect(0, 0, 0, 0);
      continue;
    }

    // Calculate if each window item needs animation.
    bool should_animate_item = animate;
    // If we're in entering overview process, not all window items in the grid
    // might need animation even if the grid needs animation.
    if (animate && transition == OverviewTransition::kEnter) {
      should_animate_item = window_item->should_animate_when_entering();
      if (should_animate_item && !has_non_cover_animating) {
        has_non_cover_animating |=
            !CanCoverAvailableWorkspace(window_item->GetWindow());
        ++animate_count;
      }
    }

    if (can_do_spawn_animation && window_item->should_use_spawn_animation())
      animation_type = OVERVIEW_ANIMATION_SPAWN_ITEM_IN_OVERVIEW;

    animation_types[i] =
        should_animate_item ? animation_type : OVERVIEW_ANIMATION_NONE;
  }

  if (animate && transition == OverviewTransition::kEnter &&
      !item_list_.empty()) {
    bool single_animation_in_clamshell =
        animate_count == 1 && !has_non_cover_animating &&
        !display::Screen::GetScreen()->InTabletMode();
    bool minimized_in_tablet = overview_session_->enter_exit_overview_type() ==
                               OverviewEnterExitType::kFadeInEnter;
    metrics_tracker_ = std::make_unique<OverviewEnterMetricsTracker>(
        item_list_[0]->GetWindow()->layer()->GetCompositor(),
        SplitViewController::Get(root_window_)->InSplitViewMode(),
        single_animation_in_clamshell, minimized_in_tablet);
  }

  // Apply the animation after creating metrics_tracker_ so that unit test
  // can correctly count the measure requests.
  for (size_t i = 0; i < item_list_.size(); ++i) {
    if (const gfx::RectF& rect = rects[i]; !rect.IsEmpty()) {
      item_list_[i].get()->SetBounds(rect, animation_types[i]);
    }
  }

  UpdateSaveDeskButtons();
  // Needed to include the toast when we init the grid.
  UpdateSplitViewSetupViewWidget();

  // This is a no-op if the feature ContinuousOverviewScrollAnimation is not
  // enabled. Once windows are placed at their final positions, clear transforms
  // so that they get re-calculated when a continuous downward scroll begins.
  cached_transforms_.clear();
}

OverviewItemBase* OverviewGrid::GetOverviewItemContaining(
    const aura::Window* window) const {
  for (const auto& window_item : item_list_) {
    if (window_item && window_item->Contains(window)) {
      return window_item.get();
    }
  }

  return nullptr;
}

void OverviewGrid::AddItem(
    aura::Window* window,
    bool reposition,
    bool animate,
    const base::flat_set<OverviewItemBase*>& ignored_items,
    size_t index,
    bool use_spawn_animation,
    bool restack) {
  CHECK(!GetOverviewItemContaining(window));
  CHECK_LE(index, item_list_.size());

  std::unique_ptr<OverviewItemBase> overview_item_base =
      OverviewItemBase::Create(window, overview_session_, this);
  UpdateNumSavedDeskUnsupportedWindows(overview_item_base->GetWindows(),
                                       /*increment=*/true);
  item_list_.insert(item_list_.begin() + index, std::move(overview_item_base));

  if (overview_session_)
    overview_session_->UpdateFrameThrottling();

  auto* item = item_list_[index].get();
  item->PrepareForOverview();

  // No animations if the saved desk grid is showing, even if `animate` is true.
  const bool should_animate = animate && !IsShowingSavedDeskLibrary();

  if (should_animate && use_spawn_animation && reposition) {
    item->SetShouldUseSpawnAnimation(true);
  } else {
    // The item is added after overview enter animation is complete, so
    // just call OnStartingAnimationComplete() only if we won't animate it with
    // with the spawn animation. Otherwise, OnStartingAnimationComplete() will
    // be called when the spawn-item-animation completes (See
    // OverviewItem::OnItemSpawnedAnimationCompleted()).
    item->OnStartingAnimationComplete();
  }

  if (restack) {
    if (reposition && should_animate)
      item->set_should_restack_on_animation_end(true);
    else
      item->Restack();
  }
  if (reposition)
    PositionWindows(should_animate, ignored_items);

  if (IsShowingSavedDeskLibrary()) {
    item->HideForSavedDeskLibrary(/*animate=*/false);
  }
}

void OverviewGrid::AppendItem(aura::Window* window,
                              bool reposition,
                              bool animate,
                              bool use_spawn_animation) {
  AddItem(window, reposition, animate, /*ignored_items=*/{}, item_list_.size(),
          use_spawn_animation, /*restack=*/false);
}

void OverviewGrid::AddItemInMruOrder(aura::Window* window,
                                     bool reposition,
                                     bool animate,
                                     bool restack,
                                     bool use_spawn_animation) {
  AddItem(window, reposition, animate, /*ignored_items=*/{},
          FindInsertionIndex(window), use_spawn_animation, restack);
}

void OverviewGrid::RemoveItem(OverviewItemBase* overview_item,
                              bool item_destroying,
                              bool reposition) {
  EndNudge();

  // Use reverse iterator to be efficient when removing all.
  auto iter = base::ranges::find(base::Reversed(item_list_), overview_item,
                                 &std::unique_ptr<OverviewItemBase>::get);
  CHECK(iter != item_list_.rend());

  UpdateNumSavedDeskUnsupportedWindows(overview_item->GetWindows(),
                                       /*increment=*/false);

  // Erase from the list first because deleting OverviewItem can lead to
  // iterating through the `item_list_`.
  std::unique_ptr<OverviewItemBase> tmp = std::move(*iter);
  item_list_.erase(std::next(iter).base());
  tmp.reset();

  if (overview_session_)
    overview_session_->UpdateFrameThrottling();

  if (!item_destroying || !overview_session_) {
    return;
  }

  if (empty()) {
    overview_session_->OnGridEmpty();
    return;
  }

  if (reposition) {
    // Update the grid bounds if needed and reposition the windows minus the
    // currently overview dragged window, if there is one. Note: this does not
    // update the grid bounds if the window being dragged from the top or shelf,
    // the former being handled in TabletModeWindowDragDelegate's destructor.
    base::flat_set<OverviewItemBase*> ignored_items;
    OverviewItemBase* dragged_item =
        overview_session_->GetCurrentDraggedOverviewItem();
    if (dragged_item)
      ignored_items.insert(dragged_item);
    const gfx::Rect grid_bounds = GetGridBoundsInScreen(
        root_window_,
        split_view_drag_indicators_
            ? std::make_optional(
                  split_view_drag_indicators_->current_window_dragging_state())
            : std::nullopt,
        /*account_for_hotseat=*/true);
    SetBoundsAndUpdatePositions(grid_bounds, ignored_items, /*animate=*/true);
  }
}

void OverviewGrid::RemoveAllItemsForSavedDeskLaunch() {
  {
    // Wait until the end to notify content changes for all desks.
    Desk::ScopedContentUpdateNotificationDisabler desks_scoped_notify_disabler(
        /*desks=*/DesksController::Get()->desks(),
        /*notify_when_destroyed=*/true);

    for (auto& item : item_list_) {
      item->RevertHideForSavedDeskLibrary(/*animate=*/false);
      item->RestoreWindow(/*reset_transform=*/true, /*animate=*/false);
    }
  }
  // Destroying OverviewItemBase can call back into `this` and try to use
  // `item_list_`; since the standard provides no guarantees about the
  // internal state of a vector being cleared, swap it with an empty vector on
  // the stack so that the destroyed items consistently see an empty vector.
  {
    decltype(item_list_) item_list;
    item_list_.swap(item_list);
  }
  num_incognito_windows_ = 0;
  num_unsupported_windows_ = 0;
  EnableSaveDeskButtonContainer();
}

void OverviewGrid::AddDropTargetForDraggingFromThisGrid(
    OverviewItemBase* dragged_item) {
  const size_t position = GetOverviewItemIndex(dragged_item) + 1u;
  AddDropTargetImpl(dragged_item, position, /*animate=*/false);
}

void OverviewGrid::AddDropTargetNotForDraggingFromThisGrid(
    aura::Window* dragged_window,
    bool animate) {
  const size_t position = FindInsertionIndex(dragged_window);
  AddDropTargetImpl(nullptr, position, /*animate=*/false);

  if (!animate) {
    return;
  }

  views::Widget* drop_target_widget = drop_target_->item_widget();
  drop_target_widget->SetOpacity(0.f);
  ScopedOverviewAnimationSettings settings(
      OVERVIEW_ANIMATION_DROP_TARGET_FADE,
      drop_target_widget->GetNativeWindow());
  drop_target_widget->SetOpacity(1.f);
}

void OverviewGrid::RemoveDropTarget() {
  CHECK(drop_target_);

  // Copy to a local first to avoid a dangling pointer.
  OverviewItemBase* drop_target_ptr = std::exchange(drop_target_, nullptr);

  size_t erased_count = std::erase_if(
      item_list_, base::MatchesUniquePtr<OverviewItemBase>(drop_target_ptr));
  CHECK_EQ(1u, erased_count);

  // Skip repositioning here. The caller is expected to call `PositionWindows()`
  // after more drag-ending cleanup in `OverviewWindowDragController`.
}

void OverviewGrid::SetBoundsAndUpdatePositions(
    const gfx::Rect& bounds_in_screen,
    const base::flat_set<OverviewItemBase*>& ignored_items,
    bool animate) {
  const bool bounds_updated = bounds_in_screen != bounds_;
  bounds_ = bounds_in_screen;
  MaybeUpdateDesksWidgetBounds();
  MaybeUpdateBirchBarWidgetBounds();

  if (scoped_overview_wallpaper_clipper_) {
    scoped_overview_wallpaper_clipper_->RefreshWallpaperClipBounds(
        ScopedOverviewWallpaperClipper::AnimationType::kNone,
        base::DoNothing());
  }

  PositionWindows(animate, ignored_items);

  if (bounds_updated && saved_desk_library_widget_)
    saved_desk_library_widget_->SetBounds(GetGridEffectiveBounds());
}

void OverviewGrid::RearrangeDuringDrag(
    OverviewItemBase* dragged_item,
    SplitViewDragIndicators::WindowDraggingState window_dragging_state) {
  // Update the drop target visibility according to `window_dragging_state`.
  if (drop_target_) {
    views::Widget* drop_target_widget = drop_target_->item_widget();
    ScopedOverviewAnimationSettings settings(
        OVERVIEW_ANIMATION_DROP_TARGET_FADE,
        drop_target_widget->GetNativeWindow());
    drop_target_widget->SetOpacity(
        GetWantedDropTargetOpacity(window_dragging_state));
  }

  // Update the grid's bounds.
  const gfx::Rect wanted_grid_bounds = GetGridBoundsInScreen(
      root_window_, std::make_optional(window_dragging_state),
      /*account_for_hotseat=*/true);
  if (bounds_ != wanted_grid_bounds) {
    base::flat_set<OverviewItemBase*> ignored_items;
    if (dragged_item)
      ignored_items.insert(dragged_item);
    SetBoundsAndUpdatePositions(wanted_grid_bounds, ignored_items,
                                /*animate=*/true);
  }
}

void OverviewGrid::SetSplitViewDragIndicatorsDraggedWindow(
    aura::Window* dragged_window) {
  if (split_view_drag_indicators_) {
    split_view_drag_indicators_->SetDraggedWindow(dragged_window);
  }
}

void OverviewGrid::SetSplitViewDragIndicatorsWindowDraggingState(
    SplitViewDragIndicators::WindowDraggingState window_dragging_state) {
  if (split_view_drag_indicators_) {
    split_view_drag_indicators_->SetWindowDraggingState(window_dragging_state);
  }
}

bool OverviewGrid::MaybeUpdateDesksWidgetBounds() {
  if (!desks_widget_)
    return false;

  const gfx::Rect desks_widget_bounds = GetDesksWidgetBounds();
  if (desks_widget_bounds != desks_widget_->GetWindowBoundsInScreen()) {
    // Note that the desks widget window is placed on the active desk container,
    // which has the kUsesScreenCoordinatesKey property set to true, and hence
    // we use the screen coordinates when positioning the desks widget.
    //
    // On certain display zooms, the requested |desks_widget_bounds| may differ
    // than the current screen bounds of the desks widget by 1dp, but internally
    // it will end up being the same and therefore a layout may not be
    // triggered. This can cause mini views not to show up at all. We must
    // guarantee that a layout will always occur by invalidating the layout.
    // See crbug.com/1056371 for more details.
    desks_bar_view_->InvalidateLayout();
    desks_widget_->SetBounds(desks_widget_bounds);

    if (scoped_overview_wallpaper_clipper_) {
      scoped_overview_wallpaper_clipper_->RefreshWallpaperClipBounds(
          ScopedOverviewWallpaperClipper::AnimationType::kNone,
          base::DoNothing());
    }

    return true;
  }
  return false;
}

bool OverviewGrid::MaybeUpdateBirchBarWidgetBounds() {
  if (!birch_bar_widget_) {
    return false;
  }

  const gfx::Rect birch_bar_widget_bounds = GetBirchBarWidgetBounds();
  if (birch_bar_widget_bounds != birch_bar_widget_->GetWindowBoundsInScreen()) {
    birch_bar_widget_->SetBounds(birch_bar_widget_bounds);
    return true;
  }
  return false;
}

void OverviewGrid::UpdateDropTargetBackgroundVisibility(
    OverviewItemBase* dragged_item,
    const gfx::PointF& location_in_screen) {
  CHECK(drop_target_);
  drop_target_->UpdateBackgroundVisibility(
      gfx::ToRoundedPoint(location_in_screen));
}

void OverviewGrid::OnOverviewItemDragStarted() {
  CommitNameChanges();
  for (auto& item : item_list_) {
    item->OnOverviewItemDragStarted();
  }
}

void OverviewGrid::OnOverviewItemDragEnded(bool snap) {
  for (auto& item : item_list_) {
    item->OnOverviewItemDragEnded(snap);
  }
}

void OverviewGrid::OnWindowDragStarted(aura::Window* dragged_window,
                                       bool animate) {
  dragged_window_ = dragged_window;
  AddDropTargetNotForDraggingFromThisGrid(dragged_window, animate);
  // Stack the |dragged_window| at top during drag.
  dragged_window->parent()->StackChildAtTop(dragged_window);
  // Called to set caption and title visibility during dragging.
  OnOverviewItemDragStarted();
}

void OverviewGrid::OnWindowDragContinued(
    aura::Window* dragged_window,
    const gfx::PointF& location_in_screen,
    SplitViewDragIndicators::WindowDraggingState window_dragging_state) {
  DCHECK_EQ(dragged_window_, dragged_window);
  DCHECK_EQ(dragged_window->GetRootWindow(), root_window_);

  RearrangeDuringDrag(nullptr, window_dragging_state);
  UpdateDropTargetBackgroundVisibility(nullptr, location_in_screen);
}

void OverviewGrid::OnWindowDragEnded(aura::Window* dragged_window,
                                     const gfx::PointF& location_in_screen,
                                     bool should_drop_window_into_overview,
                                     bool snap) {
  DCHECK_EQ(dragged_window_, dragged_window);
  DCHECK_EQ(dragged_window->GetRootWindow(), root_window_);
  CHECK(drop_target_);
  dragged_window_ = nullptr;

  // Add the dragged window into drop target in overview if
  // |should_drop_window_into_overview| is true.
  if (should_drop_window_into_overview) {
    AddDraggedWindowIntoOverviewOnDragEnd(dragged_window);
  }

  RemoveDropTarget();

  // Called to reset caption and title visibility after dragging.
  OnOverviewItemDragEnded(snap);

  // Update the grid bounds and reposition windows. Since the grid bounds might
  // be updated based on the preview area during drag, but the window finally
  // didn't be snapped to the preview area.
  RefreshGridBounds(/*animate=*/true);
}

void OverviewGrid::MergeWindowIntoOverviewForWebUITabStrip(
    aura::Window* dragged_window) {
  AddDraggedWindowIntoOverviewOnDragEnd(dragged_window);
  RefreshGridBounds(/*animate=*/true);
}

void OverviewGrid::SetVisibleDuringWindowDragging(bool visible, bool animate) {
  for (const auto& overview_item : item_list_) {
    overview_item->SetVisibleDuringItemDragging(visible, animate);
  }

  // Update |desks_widget_|.
  if (desks_widget_) {
    ui::Layer* layer = desks_widget_->GetNativeWindow()->layer();
    float new_opacity = visible ? 1.f : 0.f;
    if (layer->GetTargetOpacity() == new_opacity)
      return;

    if (animate) {
      ScopedOverviewAnimationSettings settings(
          OVERVIEW_ANIMATION_OPACITY_ON_WINDOW_DRAG,
          desks_widget_->GetNativeWindow());
      layer->SetOpacity(new_opacity);
    } else {
      layer->SetOpacity(new_opacity);
    }
  }
}

void OverviewGrid::OnDisplayMetricsChanged(uint32_t changed_metrics) {
  if (split_view_drag_indicators_)
    split_view_drag_indicators_->OnDisplayBoundsChanged();

  UpdateCannotSnapWarningVisibility(/*animate=*/true);

  // The `InformedRestoreContentsView` may need to be updated to match the
  // primary display orientation. If the pine widget exists, then this overview
  // grid is on the primary display, so we can tell the contents view to update
  // on rotation.
  if (informed_restore_widget_ &&
      (changed_metrics & display::DisplayObserver::DISPLAY_METRIC_ROTATION)) {
    views::AsViewClass<InformedRestoreContentsView>(
        informed_restore_widget_->GetContentsView())
        ->UpdateOrientation();
  }

  // In case of split view mode, the grid bounds and item positions will be
  // updated in |OnSplitViewDividerPositionChanged|.
  if (SplitViewController::Get(root_window_)->InSplitViewMode())
    return;
  RefreshGridBounds(/*animate=*/false);
}

void OverviewGrid::OnUserWorkAreaInsetsChanged(aura::Window* root_window) {
  DCHECK_EQ(root_window, root_window_);
  if (!desks_widget_ && !birch_bar_widget_) {
    return;
  }

  RefreshGridBounds(/*animate=*/false);
}

void OverviewGrid::OnStartingAnimationComplete(bool canceled) {
  metrics_tracker_.reset();
  if (canceled)
    return;

  if (ShouldInitDesksWidget()) {
    auto presentation_time_recorder = CreatePresentationTimeHistogramRecorder(
        root_window_->layer()->GetCompositor(),
        kOverviewDelayedDeskBarPresentationHistogram, "",
        kDeskBarEnterExitPresentationMaxLatency);
    presentation_time_recorder->RequestNext();
    MaybeInitDesksWidget();
  }

  MaybeInitBirchBarWidget();

  UpdateSaveDeskButtons();

  for (auto& item : item_list()) {
    item->OnStartingAnimationComplete();
  }
}

void OverviewGrid::CalculateWindowListAnimationStates(
    OverviewItemBase* selected_item,
    OverviewTransition transition,
    const std::vector<gfx::RectF>& target_bounds) {
  // Checks to enforce assumptions used in later codes.
  switch (transition) {
    case OverviewTransition::kEnter:
      CHECK_EQ(target_bounds.size(), item_list_.size());
      break;
    case OverviewTransition::kExit:
      CHECK(target_bounds.empty());
      break;
    case OverviewTransition::kInOverview:
      NOTREACHED();
  }

  // On top items are items that are higher up on the z-order, or in the always
  // on top or float containers.
  auto is_on_top_item = [](OverviewItemBase* item) -> bool {
    CHECK(item);

    if (item->GetWindow()->GetProperty(aura::client::kZOrderingKey) !=
        ui::ZOrderLevel::kNormal) {
      return true;
    }

    aura::Window* parent = item->GetWindow()->parent();
    aura::Window* root = parent->GetRootWindow();
    return parent == root->GetChildById(kShellWindowId_AlwaysOnTopContainer) ||
           parent == root->GetChildById(kShellWindowId_FloatContainer);
  };

  // Create a copy of `item_list_` which has the selected item and on top
  // windows in the front.
  std::vector<OverviewItemBase*> on_top_items;
  std::vector<OverviewItemBase*> regular_items;
  for (const std::unique_ptr<OverviewItemBase>& item : item_list_) {
    OverviewItemBase* item_ptr = item.get();
    CHECK(item_ptr);
    // Skip the selected item; it will be inserted into the front. Skip the drop
    // target, it is translucent and doesn't factor into any of these
    // calculations.
    if (item_ptr == selected_item || item_ptr == drop_target_) {
      continue;
    }

    if (is_on_top_item(item_ptr))
      on_top_items.push_back(item_ptr);
    else
      regular_items.push_back(item_ptr);
  }

  // Construct `items` so they are ordered like so.
  //   1) Selected window which is on top.
  //   2) On top windows.
  //   3) Selected window which is not on top.
  //   4) Regular window.
  // Windows in the same group maintain their ordering from `window_list`.
  std::vector<OverviewItemBase*> items;
  if (selected_item && is_on_top_item(selected_item))
    items.insert(items.begin(), selected_item);
  items.insert(items.end(), on_top_items.begin(), on_top_items.end());
  if (selected_item && !is_on_top_item(selected_item))
    items.insert(items.end(), selected_item);
  items.insert(items.end(), regular_items.begin(), regular_items.end());

  SkRegion occluded_region;
  auto* split_view_controller = SplitViewController::Get(root_window_);
  if (split_view_controller->InSplitViewMode()) {
    // Snapped windows and the split view divider are not included in
    // `target_bounds` or `item_list_`, but can occlude other windows, so add
    // them manually to `region` here.
    SkIRect snapped_window_bounds = gfx::RectToSkIRect(
        split_view_controller->GetDefaultSnappedWindow()->GetBoundsInScreen());
    occluded_region.op(snapped_window_bounds, SkRegion::kUnion_Op);

    if (auto* divider_widget =
            split_view_controller->split_view_divider()->divider_widget()) {
      aura::Window* divider_window = divider_widget->GetNativeWindow();
      SkIRect divider_bounds =
          gfx::RectToSkIRect(divider_window->GetBoundsInScreen());
      occluded_region.op(divider_bounds, SkRegion::kUnion_Op);
    }
  }

  // TODO(sammiequon): Investigate the bounds used here and if we need to
  // consider tucked windows.
  gfx::Rect grid_bounds = GetGridEffectiveBounds();
  for (size_t i = 0; i < items.size(); ++i) {
    const bool minimized =
        WindowState::Get(items[i]->GetWindow())->IsMinimized();
    bool src_occluded = minimized;
    bool dst_occluded = false;
    gfx::Rect src_bounds_temp =
        minimized ? gfx::Rect()
                  : items[i]->GetWindow()->GetBoundsInRootWindow();
    if (!src_bounds_temp.IsEmpty()) {
      if (transition == OverviewTransition::kEnter &&
          display::Screen::GetScreen()->InTabletMode()) {
        BackdropController* backdrop_controller =
            GetActiveWorkspaceController(root_window_)
                ->layout_manager()
                ->backdrop_controller();
        if (backdrop_controller->GetTopmostWindowWithBackdrop() ==
            items[i]->GetWindow()) {
          src_bounds_temp = screen_util::GetDisplayWorkAreaBoundsInParent(
              items[i]->GetWindow());
        }
      } else if (transition == OverviewTransition::kExit) {
        // On exiting overview, |GetBoundsInRootWindow()| will have the overview
        // translation applied to it, so use |bounds()| and
        // |ConvertRectToScreen()| to get the true target bounds.
        src_bounds_temp = items[i]->GetWindow()->bounds();
        wm::ConvertRectToScreen(items[i]->root_window(), &src_bounds_temp);
      }
      src_bounds_temp.Intersect(grid_bounds);
    }

    // The bounds of of the destination may be partially or fully offscreen.
    // Partially offscreen rects should be clipped so the onscreen portion is
    // treated normally. Fully offscreen rects (intersection with the screen
    // bounds is empty) should never be animated.
    gfx::Rect dst_bounds_temp = gfx::ToEnclosedRect(
        transition == OverviewTransition::kEnter ? target_bounds[i]
                                                 : items[i]->target_bounds());
    dst_bounds_temp.Intersect(grid_bounds);
    if (dst_bounds_temp.IsEmpty()) {
      items[i]->set_should_animate_when_entering(false);
      items[i]->set_should_animate_when_exiting(false);
      continue;
    }

    SkIRect src_bounds = gfx::RectToSkIRect(src_bounds_temp);
    SkIRect dst_bounds = gfx::RectToSkIRect(dst_bounds_temp);
    if (!occluded_region.isEmpty()) {
      src_occluded |=
          (!src_bounds.isEmpty() && occluded_region.contains(src_bounds));
      dst_occluded |= occluded_region.contains(dst_bounds);
    }

    // Add |src_bounds| to our region if it is not empty (minimized window).
    if (!src_bounds.isEmpty())
      occluded_region.op(src_bounds, SkRegion::kUnion_Op);

    const bool should_animate = !(src_occluded && dst_occluded);
    if (transition == OverviewTransition::kEnter)
      items[i]->set_should_animate_when_entering(should_animate);
    else if (transition == OverviewTransition::kExit)
      items[i]->set_should_animate_when_exiting(should_animate);
  }
}

void OverviewGrid::SetWindowListNotAnimatedWhenExiting() {
  should_animate_when_exiting_ = false;
  for (const auto& item : item_list_) {
    item->set_should_animate_when_exiting(false);
  }
}

void OverviewGrid::StartNudge(OverviewItemBase* item) {
  // When there is one window left, there is no need to nudge.
  if (item_list_.size() <= 1) {
    nudge_data_.clear();
    return;
  }

  // If any of the items are being animated to close, do not nudge any windows
  // otherwise we have to deal with potential items getting removed from
  // |item_list_| midway through a nudge.
  for (const auto& overview_item : item_list_) {
    if (overview_item->animating_to_close()) {
      nudge_data_.clear();
      return;
    }
  }

  DCHECK(item);

  // Get the bounds of the items currently, and the bounds if `item` were to be
  // removed.
  std::vector<gfx::RectF> src_rects;
  for (const auto& overview_item : item_list_) {
    src_rects.push_back(overview_item->target_bounds());
  }

  std::vector<gfx::RectF> dst_rects = GetWindowRects({item});

  const size_t index = GetOverviewItemIndex(item);

  // Returns a vector of integers indicating which row the item is in. |index|
  // is the index of the element which is going to be deleted and should not
  // factor into calculations. The call site should mark |index| as -1 if it
  // should not be used. The item at |index| is marked with a 0. The heights of
  // items are all set to the same value so a new row is determined if the y
  // value has changed from the previous item.
  auto get_rows = [](const std::vector<gfx::RectF>& bounds_list, size_t index) {
    std::vector<int> row_numbers;
    int current_row = 1;
    float last_y = 0;
    for (size_t i = 0; i < bounds_list.size(); ++i) {
      if (i == index) {
        row_numbers.push_back(0);
        continue;
      }

      // Update |current_row| if the y position has changed (heights are all
      // equal in overview, so a new y position indicates a new row).
      if (last_y != 0 && last_y != bounds_list[i].y())
        ++current_row;

      row_numbers.push_back(current_row);
      last_y = bounds_list[i].y();
    }

    return row_numbers;
  };

  std::vector<int> src_rows = get_rows(src_rects, -1);
  std::vector<int> dst_rows = get_rows(dst_rects, index);

  // Do nothing if the number of rows change.
  if (dst_rows.back() != 0 && src_rows.back() != dst_rows.back())
    return;
  size_t second_last_index = src_rows.size() - 2;
  if (dst_rows.back() == 0 &&
      src_rows[second_last_index] != dst_rows[second_last_index]) {
    return;
  }

  // Do nothing if the last item from the previous row will drop onto the
  // current row, this will cause the items in the current row to shift to the
  // right while the previous item stays in the previous row, which looks weird.
  if (src_rows[index] > 1) {
    // Find the last item from the previous row.
    size_t previous_row_last_index = index;
    while (src_rows[previous_row_last_index] == src_rows[index]) {
      --previous_row_last_index;
    }

    // Early return if the last item in the previous row changes rows.
    if (src_rows[previous_row_last_index] != dst_rows[previous_row_last_index])
      return;
  }

  // Helper to check whether the item at |item_index| will be nudged.
  auto should_nudge = [&src_rows, &dst_rows, &index](size_t item_index) {
    // Out of bounds.
    if (item_index >= src_rows.size())
      return false;

    // Nudging happens when the item stays on the same row and is also on the
    // same row as the item to be deleted was.
    if (dst_rows[item_index] == src_rows[index] &&
        dst_rows[item_index] == src_rows[item_index]) {
      return true;
    }

    return false;
  };

  // Starting from |index| go up and down while the nudge condition returns
  // true.
  std::vector<int> affected_indexes;
  size_t loop_index;

  if (index > 0) {
    loop_index = index - 1;
    while (should_nudge(loop_index)) {
      affected_indexes.push_back(loop_index);
      --loop_index;
    }
  }

  loop_index = index + 1;
  while (should_nudge(loop_index)) {
    affected_indexes.push_back(loop_index);
    ++loop_index;
  }

  // Populate |nudge_data_| with the indexes in |affected_indexes| and their
  // respective source and destination bounds.
  nudge_data_.resize(affected_indexes.size());
  for (size_t i = 0; i < affected_indexes.size(); ++i) {
    const size_t new_index = static_cast<size_t>(affected_indexes[i]);
    nudge_data_[i] = {.index = new_index,
                      .src = src_rects[new_index],
                      .dst = dst_rects[new_index]};
  }
}

void OverviewGrid::UpdateNudge(OverviewItemBase* item, double value) {
  for (const OverviewNudgeData& data : nudge_data_) {
    CHECK_LT(data.index, item_list_.size());

    OverviewItemBase* nudged_item = item_list_[data.index].get();
    double nudge_param = value * value / 30.0;
    nudge_param = std::clamp(nudge_param, 0.0, 1.0);
    gfx::RectF bounds =
        gfx::Tween::RectFValueBetween(nudge_param, data.src, data.dst);
    nudged_item->SetBounds(bounds, OVERVIEW_ANIMATION_NONE);
  }
}

void OverviewGrid::EndNudge() {
  nudge_data_.clear();
}

aura::Window* OverviewGrid::GetTargetWindowOnLocation(
    const gfx::PointF& location_in_screen,
    OverviewItemBase* ignored_item) {
  for (std::unique_ptr<OverviewItemBase>& item : item_list_) {
    if (item.get() == ignored_item)
      continue;
    if (item->target_bounds().Contains(location_in_screen))
      return item->GetWindow();
  }
  return nullptr;
}

bool OverviewGrid::IsDesksBarViewActive() const {
  DCHECK(desks_util::ShouldDesksBarBeCreated());

  // The desk bar view is not active if there is only a single desk when
  // overview is started. Or when the desks bar view has been created and in
  // zero state.
  return DesksController::Get()->desks().size() > 1 ||
         (desks_bar_view_ && !desks_bar_view_->IsZeroState());
}

gfx::Rect OverviewGrid::GetGridEffectiveBounds() const {
  gfx::Rect effective_bounds = bounds_;
  effective_bounds.Inset(GetGridHorizontalPaddings());
  effective_bounds.Inset(GetGridVerticalPaddings());

  return effective_bounds;
}

gfx::Insets OverviewGrid::GetGridHorizontalPaddings() const {
  if (!features::IsForestFeatureEnabled()) {
    return gfx::Insets();
  }

  // Use compact paddings for partial overview.
  if (SplitViewController::Get(root_window_)->InSplitViewMode()) {
    return gfx::Insets::VH(0, kCompactPaddingForEffectiveBounds);
  }

  // Use spacious padding for tablet mode.
  if (InTabletMode()) {
    return gfx::Insets::VH(0, kSpaciousPaddingForEffectiveBounds);
  }

  gfx::Insets horizontal_paddings;

  // Use compact padding for the side with shelf and spacious padding
  // otherwise.
  const auto* shelf = Shelf::ForWindow(root_window_);
  horizontal_paddings.set_left(shelf->SelectValueForShelfAlignment(
      /*bottom=*/kSpaciousPaddingForEffectiveBounds,
      /*left=*/kCompactPaddingForEffectiveBounds,
      /*right=*/kSpaciousPaddingForEffectiveBounds));
  horizontal_paddings.set_right(shelf->SelectValueForShelfAlignment(
      /*bottom=*/kSpaciousPaddingForEffectiveBounds,
      /*left=*/kSpaciousPaddingForEffectiveBounds,
      /*right=*/kCompactPaddingForEffectiveBounds));
  return horizontal_paddings;
}

gfx::Insets OverviewGrid::GetGridVerticalPaddings() const {
  const bool forest_enabled = features::IsForestFeatureEnabled();

  // Use compact paddings for partial overview.
  if (forest_enabled &&
      SplitViewController::Get(root_window_)->InSplitViewMode()) {
    return gfx::Insets::VH(kCompactPaddingForEffectiveBounds, 0);
  }

  gfx::Insets vertical_paddings;

  // Calculate the top padding according to the existence of desk bar.

  // There's an edge case where is in tablet mode, there're more than one desk,
  // after entering overview mode, deleting desks to just keep one, even though
  // there's only one desk now in tablet mode, the desks bar will stay. That's
  // why we need to check the existence of `desks_bar_view_` here.
  const bool has_desk_bar =
      desks_bar_view_ || desks_util::ShouldDesksBarBeCreated();

  const int no_desk_bar_padding =
      forest_enabled ? kSpaciousPaddingForEffectiveBounds : 0;
  vertical_paddings.set_top(has_desk_bar ? GetDesksBarHeight()
                                         : no_desk_bar_padding);

  if (!forest_enabled) {
    return vertical_paddings;
  }

  // Calculate the bottom padding according to the existence of birch bar,
  // shelf, and home launcher.
  if (birch_bar_view_) {
    // If birch bar exists, add compact padding with the maximum birch bar
    // height and birch bar bottom padding to the bottom.
    vertical_paddings.set_bottom(GetBirchBarBottomPadding(root_window_) +
                                 birch_bar_view_->GetMaximumHeight() +
                                 kCompactPaddingForEffectiveBounds);
    return vertical_paddings;
  }

  auto* shelf = Shelf::ForWindow(root_window_);

  if (InTabletMode()) {
    // Use compact padding for home launcher and spacious padding otherwise.
    vertical_paddings.set_bottom(
        shelf->shelf_layout_manager()->hotseat_state() ==
                HotseatState::kShownHomeLauncher
            ? kCompactPaddingForEffectiveBounds
            : kSpaciousPaddingForEffectiveBounds);
    return vertical_paddings;
  }

  // Otherwise, if there is a bottom shelf, use compact padding and spacious
  // padding otherwise.
  vertical_paddings.set_bottom(shelf->SelectValueForShelfAlignment(
      /*bottom=*/kCompactPaddingForEffectiveBounds,
      /*left=*/kSpaciousPaddingForEffectiveBounds,
      /*right=*/kSpaciousPaddingForEffectiveBounds));
  return vertical_paddings;
}

gfx::Insets OverviewGrid::GetGridInsets() const {
  return GetGridInsetsImpl(GetGridEffectiveBounds());
}

bool OverviewGrid::IntersectsWithDesksBar(const gfx::Point& screen_location,
                                          bool update_desks_bar_drag_details,
                                          bool for_drop) {
  DCHECK(desks_util::ShouldDesksBarBeCreated());

  const bool dragged_item_over_bar =
      desks_widget_->GetWindowBoundsInScreen().Contains(screen_location);
  if (update_desks_bar_drag_details) {
    desks_bar_view_->SetDragDetails(screen_location,
                                    !for_drop && dragged_item_over_bar);
  }
  return dragged_item_over_bar;
}

bool OverviewGrid::MaybeDropItemOnDeskMiniViewOrNewDeskButton(
    const gfx::Point& screen_location,
    OverviewItemBase* dragged_item) {
  CHECK(desks_util::ShouldDesksBarBeCreated());

  const bool has_windows_visible_on_all_desks =
      dragged_item->HasVisibleOnAllDesksWindow();

  // End the drag for the OverviewDeskBarView.
  if (!IntersectsWithDesksBar(screen_location,
                              /*update_desks_bar_drag_details=*/
                              !has_windows_visible_on_all_desks,
                              /*for_drop=*/true)) {
    return false;
  }

  if (has_windows_visible_on_all_desks) {
    // Show toast since items that are visible on all desks should not be able
    // to be unassigned during overview.
    // TODO(b/306034162): Consider updating the string for the toast with the
    // existence of group item.
    Shell::Get()->toast_manager()->Show(
        ToastData(kMoveVisibleOnAllDesksWindowToastId,
                  ToastCatalogName::kMoveVisibleOnAllDesksWindow,
                  l10n_util::GetStringUTF16(
                      IDS_ASH_OVERVIEW_VISIBLE_ON_ALL_DESKS_TOAST)));
    return false;
  }

  auto* desks_controller = DesksController::Get();

  for (DeskMiniView* mini_view : desks_bar_view_->mini_views()) {
    if (!mini_view->IsPointOnMiniView(screen_location))
      continue;

    Desk* const target_desk = mini_view->desk();
    if (target_desk == desks_controller->active_desk())
      return false;

    // Make sure that new desk button goes back to the expanded state after
    // the window is dropped on an existing desk.
    desks_bar_view_->UpdateDeskIconButtonState(
        desks_bar_view_->new_desk_button(),
        /*target_state=*/DeskIconButton::State::kExpanded);

    return desks_controller->MoveWindowFromActiveDeskTo(
        dragged_item->GetWindow(), target_desk, root_window_,
        DesksMoveWindowFromActiveDeskSource::kDragAndDrop);
  }

  if (!desks_controller->CanCreateDesks()) {
    return false;
  }

  if (!desks_bar_view_->new_desk_button()->IsPointOnButton(screen_location)) {
    return false;
  }

  desks_bar_view_->OnNewDeskButtonPressed(
      DesksCreationRemovalSource::kDragToNewDeskButton);

  auto* target_desk = desks_controller->desks().back().get();

  // When creating a new desk by by dragging and dropping a lacros browser
  // window to new desk button, set the desk's default profile based on the
  // profile lacros window is logged into.
  const auto windows = dragged_item->GetWindows();
  if (chromeos::features::IsDeskProfilesEnabled() && windows.size() == 1) {
    if (auto lacros_profile_id = windows[0]->GetProperty(kLacrosProfileId);
        lacros_profile_id != 0) {
      target_desk->SetLacrosProfileId(
          lacros_profile_id,
          DeskProfilesSelectProfileSource::kNewDeskButtonDrop);
    }
  }

  return desks_controller->MoveWindowFromActiveDeskTo(
      dragged_item->GetWindow(), target_desk, root_window_,
      DesksMoveWindowFromActiveDeskSource::kDragAndDrop);
}

void OverviewGrid::StartScroll() {
  scroll_pauser_ = OverviewController::Get()->PauseOcclusionTracker(
      kOcclusionUnpauseDurationForScroll);

  // Users are not allowed to scroll past the leftmost or rightmost bounds of
  // the items on screen in the grid. |scroll_offset_min_| is the amount needed
  // to fit the rightmost window into |total_bounds|. The max is zero which is
  // default because windows are aligned to the left from the beginning.
  gfx::Rect total_bounds = GetGridEffectiveBounds();
  total_bounds.Inset(GetGridInsetsImpl(total_bounds));

  float rightmost_window_right = 0;
  for (const auto& item : item_list_) {
    const gfx::RectF bounds = item->target_bounds();
    if (rightmost_window_right < bounds.right()) {
      rightmost_window_right = bounds.right();
    }

    item->set_scrolling_bounds(bounds);
  }

  // |rightmost_window_right| may have been modified by an earlier scroll.
  // |scroll_offset_| is added to adjust for that.
  rightmost_window_right -= scroll_offset_;
  scroll_offset_min_ = total_bounds.right() - rightmost_window_right;
  if (scroll_offset_min_ > 0.f)
    scroll_offset_min_ = 0.f;

  presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
      const_cast<ui::Compositor*>(root_window()->layer()->GetCompositor()),
      kOverviewScrollHistogram, kOverviewScrollMaxLatencyHistogram);
}

bool OverviewGrid::UpdateScrollOffset(float delta) {
  float new_scroll_offset = scroll_offset_;
  new_scroll_offset += delta;
  new_scroll_offset = std::clamp(new_scroll_offset, scroll_offset_min_, 0.f);

  // For flings, we want to return false if we hit one of the edges, which is
  // when |new_scroll_offset| is exactly 0.f or |scroll_offset_min_|.
  const bool in_range =
      new_scroll_offset < 0.f && new_scroll_offset > scroll_offset_min_;
  if (new_scroll_offset == scroll_offset_)
    return in_range;

  // Update the bounds of the items which are currently visible on screen.
  for (const auto& item : item_list_) {
    std::optional<gfx::RectF> scrolling_bounds_optional =
        item->scrolling_bounds();
    // Scrolling bounds may not be set if the item was added after scrolling
    // started (i.e. another desk was combined into the active desk).
    if (!scrolling_bounds_optional) {
      continue;
    }
    const gfx::RectF previous_bounds = scrolling_bounds_optional.value();
    gfx::RectF new_bounds = previous_bounds;
    new_bounds.Offset(new_scroll_offset - scroll_offset_, 0.f);
    item->set_scrolling_bounds(new_bounds);
    if (gfx::RectF(GetGridEffectiveBounds()).Intersects(new_bounds) ||
        gfx::RectF(GetGridEffectiveBounds()).Intersects(previous_bounds)) {
      item->SetBounds(new_bounds, OVERVIEW_ANIMATION_NONE);
    }
  }

  scroll_offset_ = new_scroll_offset;

  DCHECK(presentation_time_recorder_);
  presentation_time_recorder_->RequestNext();
  return in_range;
}

void OverviewGrid::EndScroll() {
  scroll_pauser_.reset();
  for (const auto& item : item_list_) {
    item->set_scrolling_bounds(std::nullopt);
  }
  presentation_time_recorder_.reset();

  if (!overview_session_->is_shutting_down())
    PositionWindows(/*animate=*/false);
}

int OverviewGrid::CalculateWidthAndMaybeSetUnclippedBounds(
    OverviewItemBase* item,
    int height) {
  gfx::SizeF target_size = item->GetWindowsUnionScreenBounds().size();
  float scale = item->GetItemScale(height);
  OverviewItemFillMode item_fill_mode = item->GetOverviewItemFillMode();

  // The drop target, unlike the other windows has its bounds set directly, so
  // `GetWindowsUnionScreenBounds()` won't return the value we want. Instead,
  // get the scale from the window(s) it was meant to be a placeholder for.
  if (drop_target_ == item) {
    auto* window_drag_controller = overview_session_->window_drag_controller();
    OverviewItemBase* grid_dragged_item =
        window_drag_controller ? window_drag_controller->item() : nullptr;
    aura::Window::Windows dragged_windows =
        GetWindowsAssociatedWithDragging(grid_dragged_item, dragged_window_);
    if (AreDraggedWindowsValid(dragged_windows)) {
      const gfx::Size work_area_size =
          screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
              root_window_)
              .size();
      if (AreAllWindowsMaximized(dragged_windows)) {
        // When dragging a maximized window across displays, when dragging over
        // this grid, the drop target size should reflect the maximized window
        // size on this grid's display (i.e. this display's work area size)
        // which can be different than the source display's work area size.
        item_fill_mode = GetOverviewItemFillMode(work_area_size);
        target_size = gfx::SizeF(work_area_size);
      } else {
        // If the drag started from a different root window, `dragged_windows`
        // may not fit into the work area of `root_window_`. Then if the
        // `dragged_windows` is dropped into this grid, `dragged_windows` will
        // shrink to fit into this work area. The drop target shall reflect
        // that.
        gfx::Size dragged_item_size =
            GetTotalDraggedWindowsSize(dragged_windows);
        dragged_item_size.SetToMin(work_area_size);
        item_fill_mode = GetOverviewItemFillMode(dragged_item_size);
        target_size = GetTotalUnionSizeIncludingTransients(dragged_windows);
        target_size.SetToMin(gfx::SizeF(work_area_size));
      }

      scale = ScopedOverviewTransformWindow::GetItemScale(
          target_size.height(), height, GetTopViewInset(dragged_windows),
          kWindowMiniViewHeaderHeight);
    }
  }

  int width = std::max(1, base::ClampFloor(target_size.width() * scale));
  switch (item_fill_mode) {
    case OverviewItemFillMode::kLetterBoxed:
      width = kExtremeWindowRatioThreshold * height;
      break;
    case OverviewItemFillMode::kPillarBoxed:
      width = height / kExtremeWindowRatioThreshold;
      break;
    default:
      break;
  }

  if (drop_target_ == item) {
    return width;
  }

  // Get the bounds of the item if there is a snapped window or a window
  // about to be snapped. If the height is less than that of the header, there
  // is nothing from the original window to be shown and nothing to be clipped.
  // Floated windows doesn't need this special handling (see b/323136574).
  auto* window = item->GetWindow();
  const bool is_floated = WindowState::Get(window)->IsFloated();
  std::optional<gfx::RectF> split_view_bounds =
      GetSplitviewBoundsMaintainingAspectRatio();
  if (is_floated || !split_view_bounds ||
      split_view_bounds->height() < kWindowMiniViewHeaderHeight) {
    item->set_unclipped_size(std::nullopt);
    return width;
  }

  // Perform horizontal clipping if the window's aspect ratio is wider than the
  // split view bounds aspect ratio, and vertical clipping otherwise.
  const float aspect_ratio =
      target_size.width() /
      (target_size.height() - window->GetProperty(aura::client::kTopViewInset));
  const float target_aspect_ratio =
      static_cast<float>(split_view_bounds->width()) /
      split_view_bounds->height();
  const bool clip_horizontally = aspect_ratio > target_aspect_ratio;
  const int window_height = height - kWindowMiniViewHeaderHeight;
  gfx::Size unclipped_size;
  if (clip_horizontally) {
    unclipped_size.set_width(width);
    unclipped_size.set_height(height);
    // For horizontal clipping, shrink |width| so that the aspect ratio matches
    // that of |split_view_bounds|.
    width = std::max(1, base::ClampFloor(target_aspect_ratio * window_height));
  } else {
    // For vertical clipping, we want |height| to stay the same, so calculate
    // what the unclipped height would be based on |split_view_bounds|.

    // Find the width so that it matches height and matches the aspect ratio of
    // |split_view_bounds|.
    // TODO(sammiequon): Check to see if we can unify this with the `width`
    // calculation in the above branch where we do the clamp and the max.
    width = target_aspect_ratio * window_height;
    // The unclipped height is the height which matches |width| but keeps the
    // aspect ratio of |target_bounds|. Clipping takes the overview header into
    // account, so add that back in.
    const int unclipped_height =
        width * target_size.height() / target_size.width();
    unclipped_size.set_width(width);
    unclipped_size.set_height(unclipped_height + kWindowMiniViewHeaderHeight);
  }

  DCHECK(!unclipped_size.IsEmpty());
  item->set_unclipped_size(std::make_optional(unclipped_size));
  return width;
}

bool OverviewGrid::IsDeskNameBeingModified() const {
  return desks_bar_view_ && desks_bar_view_->IsDeskNameBeingModified();
}

void OverviewGrid::CommitNameChanges() {
  // The desks bar widget may not be ready, since it is created asynchronously
  // later when the entering overview animations finish.
  if (desks_widget_)
    DeskNameView::CommitChanges(desks_widget_.get());

  // The saved desk grid may not be shown.
  if (saved_desk_library_widget_)
    SavedDeskNameView::CommitChanges(saved_desk_library_widget_.get());
}

void OverviewGrid::ShowSavedDeskLibrary() {
  if (!saved_desk_library_widget_) {
    saved_desk_library_widget_ =
        SavedDeskLibraryView::CreateSavedDeskLibraryWidget(root_window_);

    // Compute bounds for the library using the expanded height of the desk
    // bar. `GetGridEffectiveBounds` will not be the correct bounds for the
    // library if we are currently in the zero state mode.
    gfx::Rect library_bounds = bounds_;
    library_bounds.Inset(gfx::Insets::TLBR(
        OverviewDeskBarView::GetPreferredBarHeight(
            root_window_, OverviewDeskBarView::Type::kOverview,
            OverviewDeskBarView::State::kExpanded),
        0, 0, 0));

    saved_desk_library_widget_->SetBounds(library_bounds);
  }

  {
    // Wait until the end to notify content changes for all desks.
    Desk::ScopedContentUpdateNotificationDisabler desks_scoped_notify_disabler(
        /*desks=*/DesksController::Get()->desks(),
        /*notify_when_destroyed=*/true);

    for (auto& overview_item : item_list_) {
      overview_item->HideForSavedDeskLibrary(/*animate=*/true);
    }
  }

  // There may be an existing animation in progress triggered by
  // `HideSavedDeskLibrary()` below, which animates a widget to 0.f before
  // calling `OnSavedDeskGridFadedOut()` to hide the widget on animation end.
  // Stop animating so that the callbacks associated get fired, otherwise we may
  // end up trying to show a widget that's already shown. `StopAnimating()` is a
  // no-op if there is no animation in progress.
  saved_desk_library_widget_->GetLayer()->GetAnimator()->StopAnimating();
  saved_desk_library_widget_->Show();

  // Fade in the widget from its current opacity.
  PerformFadeInLayer(saved_desk_library_widget_->GetLayer(), /*animate=*/true);

  UpdateSaveDeskButtons();

  // If the overview desk bar is not created at this point, create it. This is
  // only possible for clicking library button on the desk button desk bar,
  // since for the overview desk bar, there has to be a bar before we can click
  // on the library button.
  if (!desks_widget_) {
    MaybeInitDesksWidget();
  }

  // When desks bar is at zero state, the library button's state update will be
  // handled by `UpdateNewMiniViews` when expanding the desks bar.
  if (desks_bar_view_->IsZeroState()) {
    desks_bar_view_->UpdateNewMiniViews(/*initializing_bar_view=*/false,
                                        /*expanding_bar_view=*/true);
  } else {
    desks_bar_view_->UpdateDeskIconButtonState(
        desks_bar_view_->library_button(),
        /*target_state=*/DeskIconButton::State::kActive);
  }

  desks_bar_view_->UpdateButtonsForSavedDeskGrid();

  if (informed_restore_widget_) {
    informed_restore_widget_->GetNativeWindow()->SetEventTargetingPolicy(
        aura::EventTargetingPolicy::kNone);
    PerformFadeOutLayer(informed_restore_widget_->GetLayer(), /*animate=*/true,
                        base::DoNothing());
  }
}

void OverviewGrid::HideSavedDeskLibrary(bool exit_overview) {
  if (!saved_desk_library_widget_)
    return;

  auto* grid_layer = saved_desk_library_widget_->GetLayer();
  const bool already_hiding_grid = grid_layer->GetAnimator()->is_animating() &&
                                   grid_layer->GetTargetOpacity() == 0.f;
  if (already_hiding_grid)
    return;

  // Wait until the end to notify content changes for all desks.
  Desk::ScopedContentUpdateNotificationDisabler desks_scoped_notify_disabler(
      /*desks=*/DesksController::Get()->desks(),
      /*notify_when_destroyed=*/true);

  if (exit_overview && overview_session_->enter_exit_overview_type() ==
                           OverviewEnterExitType::kImmediateExit) {
    // Since we're immediately exiting, we don't need to animate anything.
    // Reshow the overview items and let the `saved_desk_library_widget_`
    // handle its own destruction.
    for (auto& overview_mode_item : item_list_) {
      overview_mode_item->RevertHideForSavedDeskLibrary(/*animate=*/false);
    }
    return;
  }

  if (exit_overview) {
    // Un-hide the overview mode items.
    for (auto& overview_mode_item : item_list_) {
      overview_mode_item->RevertHideForSavedDeskLibrary(/*animate=*/true);
    }

    // Disable the `saved_desk_library_widget_`'s event targeting so it can't
    // get any events during the animation.
    saved_desk_library_widget_->GetNativeWindow()->SetEventTargetingPolicy(
        aura::EventTargetingPolicy::kNone);

    FadeOutWidgetFromOverview(
        std::move(saved_desk_library_widget_),
        OVERVIEW_ANIMATION_EXIT_OVERVIEW_MODE_SAVED_DESK_GRID_FADE_OUT);
    return;
  }

  // Fade out the `saved_desk_library_widget_` and then when its animation is
  // done fade in the supporting widgets and revert the overview item hides.
  PerformFadeOutLayer(saved_desk_library_widget_->GetLayer(),
                      /*animate=*/true,
                      base::BindOnce(&OverviewGrid::OnSavedDeskGridFadedOut,
                                     weak_ptr_factory_.GetWeakPtr()));
  // The saved desk library is hidden because of a new desk is created for
  // saved desk. We have animation of adding a new desk for the library
  // button, thus to avoid the animation glitches, directly update the state
  // for the library button instead of applying the scale animation to it.
  desks_bar_view_->library_button()->UpdateState(
      DeskIconButton::State::kExpanded);
}

bool OverviewGrid::IsShowingSavedDeskLibrary() const {
  return saved_desk_library_widget_ &&
         saved_desk_library_widget_->IsVisible() &&
         saved_desk_library_widget_->GetLayer()->GetTargetOpacity() == 1.0f;
}

bool OverviewGrid::IsSavedDeskNameBeingModified() const {
  if (const SavedDeskLibraryView* library_view = GetSavedDeskLibraryView()) {
    for (SavedDeskGridView* grid_view : library_view->grid_views()) {
      if (grid_view->IsSavedDeskNameBeingModified()) {
        return true;
      }
    }
  }
  return false;
}

void OverviewGrid::UpdateNoWindowsWidget(bool no_items,
                                         bool animate,
                                         bool is_continuous_enter) {
  // `no_windows_widget_` will show in normal full overview, when there are no
  // items and the saved desk library is not showing.
  if (!no_items || IsShowingSavedDeskLibrary() ||
      ShouldShowInformedRestoreDialog(root_window_)) {
    no_windows_widget_.reset();
    return;
  }

  if (!no_windows_widget_) {
    // Create and fade in the widget.
    RoundedLabelWidget::InitParams params;
    params.name = "OverviewNoWindowsLabel";
    params.horizontal_padding = kNoItemsIndicatorHorizontalPaddingDp;
    params.vertical_padding = kNoItemsIndicatorVerticalPaddingDp;
    params.rounding_dp = kNoItemsIndicatorRoundingDp;
    params.preferred_height = kNoItemsIndicatorHeightDp;
    params.message = IDS_ASH_OVERVIEW_NO_RECENT_ITEMS;

    params.parent =
        root_window_->GetChildById(desks_util::GetActiveDeskContainerId());
    params.disable_default_visibility_animation = !animate;

    no_windows_widget_ = std::make_unique<RoundedLabelWidget>();
    no_windows_widget_->Init(std::move(params));

    aura::Window* widget_window = no_windows_widget_->GetNativeWindow();
    widget_window->parent()->StackChildAtBottom(widget_window);

    ScopedOverviewAnimationSettings settings(
        animate && !is_continuous_enter ? OVERVIEW_ANIMATION_NO_RECENTS_FADE
                                        : OVERVIEW_ANIMATION_NONE,
        widget_window);
    // Start the opacity at zero for continuous enter. Its opacity will change
    // with the trackpad events as they come.
    no_windows_widget_->SetOpacity(is_continuous_enter ? 0.f : 1.f);
    // When initializing the widget, no need to re-layout and animate again in
    // `RoundedLabelWidget::SetBoundsCenteredIn()`.
    animate = false;
  }

  const gfx::Rect grid_bounds(GetGridEffectiveBounds());
  no_windows_widget_->SetBoundsCenteredIn(grid_bounds, animate);
}

void OverviewGrid::RefreshGridBounds(bool animate) {
  SetBoundsAndUpdatePositions(GetGridBoundsInScreen(root_window_),
                              /*ignored_items=*/{}, animate);

  if (informed_restore_widget_) {
    auto* contents_view = views::AsViewClass<InformedRestoreContentsView>(
        informed_restore_widget_->GetContentsView());
    CHECK(contents_view);
    contents_view->UpdatePrimaryContainerPreferredWidth(
        root_window_, /*is_landscape=*/std::nullopt);

    gfx::Rect pine_bounds = GetGridEffectiveBounds();
    pine_bounds.ClampToCenteredSize(contents_view->GetPreferredSize());
    informed_restore_widget_->SetBounds(pine_bounds);
  }

  if (scoped_overview_wallpaper_clipper_) {
    scoped_overview_wallpaper_clipper_->RefreshWallpaperClipBounds(
        ScopedOverviewWallpaperClipper::AnimationType::kNone,
        base::DoNothing());
  }
}

void OverviewGrid::UpdateSaveDeskButtons() {
  // TODO(crbug.com/40207000): The button should be updated whenever the
  // overview grid changes, i.e. switches between active desks and/or the
  // saved desk grid. This will be needed when we make it so that switching
  // desks keeps us in overview mode.
  if (!saved_desk_util::ShouldShowSavedDesksOptions()) {
    return;
  }

  // If there is only one item and it is animating to close, hide the widget as
  // the closing window cannot be saved as part of a template.
  // TODO(http://b/327639285): Also hide save desk context menu items if item is
  // animating to close.
  const bool no_items =
      item_list_.empty() ||
      (item_list_.size() == 1u && item_list_.front()->animating_to_close());

  // Do not create or show the save desk buttons if there are no
  // windows in this grid, during a window drag or in tablet mode, the saved
  // desk grid is visible, if the desks bar hasn't been created yet, or if the
  // feature ContinuousOverviewScrollAnimation is enabled and a continuous
  // scroll is in progress.
  const bool target_visible =
      !no_items && !overview_session_->GetCurrentDraggedOverviewItem() &&
      !display::Screen::GetScreen()->InTabletMode() &&
      !IsShowingSavedDeskLibrary() && desks_widget_ &&
      (!features::IsContinuousOverviewScrollAnimationEnabled() ||
       !OverviewController::Get()->is_continuous_scroll_in_progress());

  const bool visibility_changed =
      target_visible != IsSaveDeskButtonContainerVisible();

  // If the saved desk options (either the buttons or the menu options) are
  // viable to be shown, then we want to record a histogram for holdback
  // purposes.
  if (target_visible && visibility_changed) {
    if (features::IsSavedDeskUiRevampEnabled()) {
      base::UmaHistogramBoolean(kShowSavedDeskButtonsRevampEnabledHistogramName,
                                true);
    } else {
      base::UmaHistogramBoolean(
          kShowSavedDeskButtonsRevampDisabledHistogramName, true);
    }
  }

  // If the UI revamp is enabled, we return as the buttons will not be shown.
  if (features::IsSavedDeskUiRevampEnabled()) {
    return;
  }

  // Adds or removes the widget from the accessibility focus order when exiting
  // the scope. Skip the update if the widget's visibility hasn't changed.
  absl::Cleanup update_accessibility_focus = [this, visibility_changed] {
    if (visibility_changed) {
      overview_session_->UpdateAccessibilityFocus();
    }
  };

  if (!target_visible) {
    if (visibility_changed && save_desk_button_container_widget_) {
      PerformFadeOutLayer(
          save_desk_button_container_widget_->GetLayer(),
          /*animate=*/true,
          base::BindOnce(&OverviewGrid::OnSaveDeskButtonContainerFadedOut,
                         weak_ptr_factory_.GetWeakPtr()));
    }
    return;
  }

  // Create `save_desk_button_container_widget_`.
  if (!save_desk_button_container_widget_) {
    save_desk_button_container_widget_ =
        CreateSaveDeskButtonContainerWidget(root_window_);
    save_desk_button_container_widget_->SetContentsView(
        std::make_unique<SavedDeskSaveDeskButtonContainer>(
            base::BindRepeating(
                &OverviewGrid::OnSaveDeskAsTemplateButtonPressed,
                weak_ptr_factory_.GetWeakPtr()),
            base::BindRepeating(&OverviewGrid::OnSaveDeskForLaterButtonPressed,
                                weak_ptr_factory_.GetWeakPtr())));
  }

  // If a desk animation is in progress, we don't want to animate
  // `save_desk_button_container_widget_`.
  const bool in_desk_animation = DesksController::Get()->animation();

  // There may be an existing animation in progress triggered by
  // `PerformFadeOutLayer()` above, which animates a widget to 0.f before
  // calling `OnSaveDeskButtonContainerFadedOut()` to hide the widget on
  // animation end. Stop animating so that the callbacks associated get fired,
  // otherwise we may end up trying to show a widget that's already shown.
  // `StopAnimating()` is a no-op if there is no animation in progress.
  if (visibility_changed) {
    save_desk_button_container_widget_->GetLayer()
        ->GetAnimator()
        ->StopAnimating();
    save_desk_button_container_widget_->ShowInactive();
    PerformFadeInLayer(save_desk_button_container_widget_->GetLayer(),
                       /*animate=*/!in_desk_animation);
  }

  // Enable/disable button and update tooltip.
  auto* container = views::AsViewClass<SavedDeskSaveDeskButtonContainer>(
      save_desk_button_container_widget_->GetContentsView());
  CHECK(container);

  SaveDeskOptionStatus template_status =
      GetEnableStateAndTooltipIDForTemplateType(DeskTemplateType::kTemplate);
  SaveDeskOptionStatus save_later_status =
      GetEnableStateAndTooltipIDForTemplateType(
          DeskTemplateType::kSaveAndRecall);

  container->UpdateButtonEnableStateAndTooltip(DeskTemplateType::kTemplate,
                                               template_status);
  container->UpdateButtonEnableStateAndTooltip(DeskTemplateType::kSaveAndRecall,
                                               save_later_status);

  // Set the widget position above the overview item window and default width
  // and height.
  gfx::RectF first_overview_item_bounds;
  if (item_list_.front()->animating_to_close()) {
    CHECK_GT(item_list_.size(), 1u);
    first_overview_item_bounds = item_list_[1]->target_bounds();
  } else {
    first_overview_item_bounds = item_list_.front()->target_bounds();
  }

  // Animate the widget so it moves with the items. The widget's size isn't
  // changing, so its ok to use a bounds animation as opposed to a transform
  // animation. If the visibility has changed, skip the bounds animation and use
  // the fade animation from above. Align the widget so it is visually aligned
  // with the first overview item.
  ScopedOverviewAnimationSettings settings(
      visibility_changed || in_desk_animation
          ? OVERVIEW_ANIMATION_NONE
          : OVERVIEW_ANIMATION_LAYOUT_OVERVIEW_ITEMS_IN_OVERVIEW,
      save_desk_button_container_widget_->GetNativeWindow());
  gfx::Point available_origin =
      gfx::ToRoundedPoint(first_overview_item_bounds.origin()) +
      gfx::Vector2d(0, -kSaveDeskAsTemplateOverviewItemSpacingDp);
  save_desk_button_container_widget_->SetBounds(gfx::Rect(
      available_origin, save_desk_button_container_widget_->GetContentsView()
                            ->GetPreferredSize()));
}

void OverviewGrid::EnableSaveDeskButtonContainer() {
  if (!save_desk_button_container_widget_ ||
      overview_session_->is_shutting_down()) {
    return;
  }
  save_desk_button_container_widget_->GetContentsView()->SetEnabled(true);
}

bool OverviewGrid::IsSaveDeskButtonContainerVisible() const {
  // The widget may be visible but in the process of fading away. We treat that
  // as not visible.
  return save_desk_button_container_widget_ &&
         save_desk_button_container_widget_->IsVisible() &&
         save_desk_button_container_widget_->GetLayer()->GetTargetOpacity() ==
             1.f;
}

bool OverviewGrid::IsSaveDeskAsTemplateButtonVisible() const {
  if (!IsSaveDeskButtonContainerVisible())
    return false;
  const auto* container = GetSaveDeskButtonContainer();
  return container && container->save_desk_as_template_button() &&
         container->save_desk_as_template_button()->GetVisible();
}

bool OverviewGrid::IsSaveDeskForLaterButtonVisible() const {
  if (!IsSaveDeskButtonContainerVisible())
    return false;
  const auto* container = GetSaveDeskButtonContainer();
  return container && container->save_desk_for_later_button() &&
         container->save_desk_for_later_button()->GetVisible();
}

void OverviewGrid::OnTabletModeChanged() {
  // We may not show virtual desk bar in clamshell mode such as in split view
  // setup session, and the desk bar will be created in tablet mode either. In
  // this case, we may need to init the virtual desk bar.
  MaybeInitDesksWidget();

  MaybeInitBirchBarWidget();
}

size_t OverviewGrid::GetNumWindows() const {
  size_t size = 0u;
  for (const std::unique_ptr<OverviewItemBase>& item : item_list_) {
    size += item->GetWindows().size();
  }
  return size;
}

SavedDeskSaveDeskButton* OverviewGrid::GetSaveDeskAsTemplateButton() {
  auto* container = GetSaveDeskButtonContainer();
  return container ? container->save_desk_as_template_button() : nullptr;
}

SavedDeskSaveDeskButton* OverviewGrid::GetSaveDeskForLaterButton() {
  auto* container = GetSaveDeskButtonContainer();
  return container ? container->save_desk_for_later_button() : nullptr;
}

SavedDeskSaveDeskButtonContainer* OverviewGrid::GetSaveDeskButtonContainer() {
  return save_desk_button_container_widget_
             ? views::AsViewClass<SavedDeskSaveDeskButtonContainer>(
                   save_desk_button_container_widget_->GetContentsView())
             : nullptr;
}

const SavedDeskSaveDeskButtonContainer*
OverviewGrid::GetSaveDeskButtonContainer() const {
  return save_desk_button_container_widget_
             ? views::AsViewClass<SavedDeskSaveDeskButtonContainer>(
                   save_desk_button_container_widget_->GetContentsView())
             : nullptr;
}

const SplitViewSetupView* OverviewGrid::GetSplitViewSetupView() const {
  return split_view_setup_widget_
             ? views::AsViewClass<SplitViewSetupView>(
                   split_view_setup_widget_->GetContentsView())
             : nullptr;
}

gfx::Rect OverviewGrid::GetWallpaperClipBounds() const {
  // The bottom of the clipping bounds should be above the birch bar.
  gfx::Rect clipping_bounds = GetGridEffectiveBounds();

  // If we are dragging while in portrait mode, the desk bar will shift down to
  // accommodate the snap region. The clip region should be updated so that the
  // desks bar is not covering the wallpaper. We update here instead of updating
  // `bounds_` so we do not relayout the grid.
  if (split_view_drag_indicators_ &&
      split_view_drag_indicators_->current_window_dragging_state() ==
          SplitViewDragIndicators::WindowDraggingState::kFromOverview &&
      !chromeos::wm::IsLandscapeOrientationForWindow(root_window_)) {
    clipping_bounds.SetVerticalBounds(
        clipping_bounds.y() +
            split_view_drag_indicators_->GetLeftHighlightViewBounds().height(),
        clipping_bounds.bottom());
  }

  if (!birch_bar_widget_) {
    return clipping_bounds;
  }

  const gfx::Rect birch_bar_bounds =
      birch_bar_widget_->GetWindowBoundsInScreen();

  // If there are chips in the bar, the bottom of clipping area should be above
  // the top of birch bar. Otherwise, removing the birch bar height and top
  // padding from the effect bounds to get clipping area.
  const int clipping_bottom =
      birch_bar_view_->GetChipsNum()
          ? birch_bar_bounds.y() - kCompactPaddingForEffectiveBounds
          : birch_bar_bounds.bottom();
  clipping_bounds.SetVerticalBounds(clipping_bounds.y(), clipping_bottom);
  return clipping_bounds;
}

void OverviewGrid::MaybeInitBirchBarWidget(bool by_user) {
  if (!ShouldShowBirchBar(root_window_) || birch_bar_widget_) {
    return;
  }

  birch_bar_widget_ = BirchBarView::CreateBirchBarWidget(root_window_);
  birch_bar_view_ =
      views::AsViewClass<BirchBarView>(birch_bar_widget_->GetContentsView());
  birch_bar_view_->SetRelayoutCallback(base::BindRepeating(
      &OverviewGrid::OnBirchBarLayoutChanged, weak_ptr_factory_.GetWeakPtr()));

  // Initialize the birch bar view with birch bar controller.
  auto* birch_bar_controller = BirchBarController::Get();
  CHECK(birch_bar_controller);

  // Show loading state if the data is loading.
  auto loading_state = BirchBarView::State::kLoading;
  if (by_user) {
    loading_state = BirchBarView::State::kLoadingByUser;
  } else if (overview_session_->enter_exit_overview_type() ==
             OverviewEnterExitType::kInformedRestore) {
    loading_state = BirchBarView::State::kLoadingForInformedRestore;
  }

  // Note that we should set loading state before registering the bar to
  // controller, since if there are cached items in controller, the bar would be
  // set up without knowing the current loading state.
  birch_bar_view_->SetState(loading_state);

  birch_bar_controller->RegisterBar(birch_bar_view_);

  // Stack birch bar at bottom to guarantee the dragged window is above it.
  auto* window = birch_bar_widget_->GetNativeWindow();
  window->parent()->StackChildAtBottom(window);

  // Initialize the birch bar bounds to get correct paddings for grid.
  MaybeUpdateBirchBarWidgetBounds();
}

void OverviewGrid::ShutdownBirchBarWidgetByUser() {
  if (birch_bar_widget_) {
    // Prevent the birch bar from receiving events while shutting down.
    PrepareWidgetForShutdownAnimation(birch_bar_widget_.get());
    birch_bar_view_->SetState(BirchBarView::State::kShuttingDown);
  }
}

void OverviewGrid::DestroyBirchBarWidget(bool by_user) {
  // The birch bar controller may be destroyed when shutting down Overview.
  if (auto* birch_bar_controller = BirchBarController::Get()) {
    birch_bar_controller->OnBarDestroying(birch_bar_view_);
  }
  birch_bar_view_ = nullptr;
  birch_bar_widget_.reset();

  if (by_user) {
    RefreshGridBounds(/*animate=*/true);
  }
}

void OverviewGrid::OnSplitViewStateChanged(
    SplitViewController::State previous_state,
    SplitViewController::State state) {
  // Do nothing if overview is being shutdown.
  OverviewController* overview_controller = OverviewController::Get();
  if (!overview_controller->InOverviewSession()) {
    return;
  }

  SplitViewController* split_view_controller =
      SplitViewController::Get(root_window_);
  const auto end_reason = split_view_controller->end_reason();
  const bool unsnappable_window_activated =
      state == SplitViewController::State::kNoSnap &&
      end_reason == SplitViewController::EndReason::kUnsnappableWindowActivated;

  // If two windows were snapped to both sides of the screen or an unsnappable
  // window was just activated, or we're in single split mode in clamshell mode
  // and there is no window in overview, end overview mode and bail out.
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  const bool both_snapped_windows =
      state == SplitViewController::State::kBothSnapped ||
      (snap_group_controller &&
       snap_group_controller->GetTopmostVisibleSnapGroup(root_window_));
  if (both_snapped_windows || unsnappable_window_activated ||
      (split_view_controller->InClamshellSplitViewMode() &&
       overview_session_->IsEmpty())) {
    overview_session_->RestoreWindowActivation(false);
    overview_controller->EndOverview(both_snapped_windows
                                         ? OverviewEndAction::kWindowActivating
                                         : OverviewEndAction::kSplitView);
    return;
  }

  // Update visibility on split state change: hide `birch_bar_widget_`,
  // `desks_widget_`, `saved_desk_library_widget_`, and
  // `save_desk_button_container_widget_` in partial overview; show in full
  // overview.
  if (state == SplitViewController::State::kNoSnap) {
    MaybeInitBirchBarWidget();
    RefreshDesksWidgets(/*visible=*/true);
  } else {
    DestroyBirchBarWidget();
    RefreshDesksWidgets(/*visible=*/false);
    UpdateSplitViewSetupViewWidget();
  }

  // Update the cannot snap warnings and adjust the grid bounds.
  UpdateCannotSnapWarningVisibility(/*animate=*/true);
  RefreshGridBounds(/*animate=*/false);

  // If split view mode was ended, then activate the overview focus window, to
  // match the behavior of entering overview mode in the beginning.
  if (state == SplitViewController::State::kNoSnap)
    wm::ActivateWindow(overview_session_->GetOverviewFocusWindow());
}

void OverviewGrid::OnSplitViewDividerPositionChanged() {
  if (overview_session_->is_shutting_down() ||
      window_util::IsInFasterSplitScreenSetupSession(root_window_)) {
    // `SplitViewOverviewSession` will manually update the bounds so we don't
    // need to update here in split view setup session.
    return;
  }

  SetBoundsAndUpdatePositions(
      GetGridBoundsInScreen(root_window_,
                            /*window_dragging_state=*/std::nullopt,
                            /*account_for_hotseat=*/true),
      /*ignored_items=*/{}, /*animate=*/false);
}

void OverviewGrid::OnScreenCopiedBeforeRotation() {
  rotation_pauser_ = OverviewController::Get()->PauseOcclusionTracker(
      kOcclusionUnpauseDurationForRotation);

  for (auto& item : item_list()) {
    item->UpdateRoundedCornersAndShadow();
    item->StopWidgetAnimation();
  }
}

void OverviewGrid::OnScreenRotationAnimationFinished(
    ScreenRotationAnimator* animator,
    bool canceled) {
  OverviewController* overview_controller = OverviewController::Get();
  overview_controller->DelayedUpdateRoundedCornersAndShadow();
  rotation_pauser_.reset();
}

void OverviewGrid::OnWallpaperChanging() {
  grid_event_handler_.reset();
}

void OverviewGrid::OnWallpaperChanged() {
  grid_event_handler_ = std::make_unique<OverviewGridEventHandler>(this);
}

void OverviewGrid::OnOverviewItemWindowDestroying(OverviewItem* overview_item,
                                                  bool reposition) {
  // `this` will be the delegate to handle the window destroying if the
  // underlying window represented by the corresponding overview item is a
  // single window.
  // Remove the item from `overview_session_` which will remove it from the
  // grid. If `overview_session_` is not available then remove it from the grid
  // directly.
  // TODO(b/299391958): Investigate why `overview_session_` might be unavailable
  // while grid is still alive.
  if (overview_session_) {
    overview_session_->RemoveItem(overview_item, /*item_destroying=*/true,
                                  reposition);
  } else {
    RemoveItem(overview_item, /*item_destroying=*/true, reposition);
  }
}

SavedDeskLibraryView* OverviewGrid::GetSavedDeskLibraryView() {
  return saved_desk_library_widget_
             ? views::AsViewClass<SavedDeskLibraryView>(
                   saved_desk_library_widget_->GetContentsView())
             : nullptr;
}

const SavedDeskLibraryView* OverviewGrid::GetSavedDeskLibraryView() const {
  return saved_desk_library_widget_
             ? views::AsViewClass<SavedDeskLibraryView>(
                   saved_desk_library_widget_->GetContentsView())
             : nullptr;
}

SaveDeskOptionStatus OverviewGrid::GetEnableStateAndTooltipIDForTemplateType(
    DeskTemplateType type) const {
  // The state and tooltips are only valid for the "Save desk as template" and
  // "Save desk for later" buttons/menu items.
  CHECK(type == DeskTemplateType::kTemplate ||
        type == DeskTemplateType::kSaveAndRecall);

  const SavedDeskPresenter* saved_desk_presenter =
      overview_session_->saved_desk_presenter();
  int current_entry_count = saved_desk_presenter->GetEntryCount(type);
  int max_entry_count = saved_desk_presenter->GetMaxEntryCount(type);

  // Disable if we already have the max supported saved desks.
  if (current_entry_count >= max_entry_count) {
    return SaveDeskOptionStatus{
        .enabled = false,
        .tooltip_id = GetTooltipID(type, TooltipStatus::kReachMax)};
  }

  // Iterate through all the windows in the grid to determine the number of
  // unsupported and/or incognito windows.
  aura::Window::Windows windows;
  for (const auto& item : item_list_) {
    auto item_windows = item.get()->GetWindows();
    for (aura::Window* window : item_windows) {
      windows.push_back(window);
    }
  }

  // A snapped window is not part of the grid but needs to be considered.
  if (auto* snapped_window =
          SplitViewController::Get(root_window_)->GetDefaultSnappedWindow()) {
    windows.push_back(snapped_window);
  }

  int incognito_window_count = 0;
  int unsupported_window_count = 0;
  for (aura::Window* window : windows) {
    if (IsUnsupportedWindow(window)) {
      ++unsupported_window_count;
    } else if (IsIncognitoWindow(window)) {
      ++incognito_window_count;
    }
  }

  // Enable if there are any supported window.
  if (incognito_window_count + unsupported_window_count !=
      static_cast<int>(windows.size())) {
    return {.enabled = true,
            .tooltip_id = GetTooltipID(type, TooltipStatus::kOk)};
  }

  // Disable if there are incognito windows and unsupported Linux Apps but no
  // supported windows.
  if (incognito_window_count && unsupported_window_count) {
    return {.enabled = false,
            .tooltip_id = GetTooltipID(
                type, TooltipStatus::kIncognitoAndUnsupportedWindow)};
  }

  // Disable if there are incognito windows but no supported windows.
  if (incognito_window_count) {
    return {.enabled = false,
            .tooltip_id = GetTooltipID(type, TooltipStatus::kIncognitoWindow)};
  }

  // Disable if there are unsupported Linux Apps but no supported windows.
  DCHECK(unsupported_window_count);
  return {.enabled = false,
          .tooltip_id = GetTooltipID(type, TooltipStatus::kUnsupportedWindow)};
}

void OverviewGrid::MaybeInitDesksWidget() {
  TRACE_EVENT0("ui", "OverviewGrid::MaybeInitDesksWidget");
  if (!ShouldInitDesksWidget()) {
    return;
  }

  base::ScopedUmaHistogramTimer latency_recorder(
      "Ash.Overview.DeskBarInitLatency");
  desks_widget_ = DeskBarViewBase::CreateDeskWidget(
      root_window_, GetDesksWidgetBounds(), DeskBarViewBase::Type::kOverview);

  // The following order of function calls is significant: SetContentsView()
  // must be called before OverviewDeskBarView:: Init(). This is needed because
  // the desks mini views need to access the widget to get the root window in
  // order to know how to layout themselves.
  desks_bar_view_ =
      desks_widget_->SetContentsView(std::make_unique<OverviewDeskBarView>(
          weak_ptr_factory_.GetWeakPtr(), window_occlusion_calculator_));
  desks_bar_view_->Init();

  // If the feature ContinuousOverviewScrollAnimation is enabled and a
  // continuous scroll is now starting, move the desk bar up so we can slowly
  // place it downward in relation to the scroll offset.
  if (overview_session_->enter_exit_overview_type() ==
      OverviewEnterExitType::kContinuousAnimationEnterOnScrollUpdate) {
    gfx::Transform transform;
    transform.Translate(0, -desks_bar_view_->GetBoundsInScreen().height());
    auto* layer = desks_bar_view_->layer();
    layer->SetTransform(transform);
  }

  desks_widget_->Show();

  // Stack desks bar at bottom to guarantee the dragged window is above it.
  auto* window = desks_widget_->GetNativeWindow();
  window->parent()->StackChildAtBottom(window);
}

std::vector<gfx::RectF> OverviewGrid::GetWindowRects(
    const base::flat_set<OverviewItemBase*>& ignored_items) {
  gfx::Rect total_bounds = GetGridEffectiveBounds();

  // Windows occupy vertically centered area with additional vertical insets.
  total_bounds.Inset(GetGridInsetsImpl(total_bounds));
  std::vector<gfx::RectF> rects;

  // Keep track of the lowest coordinate.
  int max_bottom = total_bounds.y();

  // Right bound of the narrowest row.
  int min_right = total_bounds.right();
  // Right bound of the widest row.
  int max_right = total_bounds.x();

  // Keep track of the difference between the narrowest and the widest row.
  // Initially this is set to the worst it can ever be assuming the windows fit.
  int width_diff = total_bounds.width();

  // Initially allow the windows to occupy all available width. Shrink this
  // available space horizontally to find the breakdown into rows that achieves
  // the minimal |width_diff|.
  int right_bound = total_bounds.right();

  // Determine the optimal height bisecting between |low_height| and
  // |high_height|. Once this optimal height is known, |height_fixed| is set to
  // true and the rows are balanced by repeatedly squeezing the widest row to
  // cause windows to overflow to the subsequent rows.
  int low_height = kVerticalSpaceBetweenItemsDp;
  int high_height = std::max(low_height, total_bounds.height() + 1);
  int height = 0.5 * (low_height + high_height);
  bool height_fixed = false;

  // Repeatedly try to fit the windows |rects| within |right_bound|.
  // If a maximum |height| is found such that all window |rects| fit, this
  // fitting continues while shrinking the |right_bound| in order to balance the
  // rows. If the windows fit the |right_bound| would have been decremented at
  // least once so it needs to be incremented once before getting out of this
  // loop and one additional pass made to actually fit the |rects|.
  // If the |rects| cannot fit (e.g. there are too many windows) the bisection
  // will still finish and we might increment the |right_bound| once pixel extra
  // which is acceptable since there is an unused margin on the right.
  bool make_last_adjustment = false;
  while (true) {
    gfx::Rect overview_mode_bounds(total_bounds);
    overview_mode_bounds.set_width(right_bound - total_bounds.x());
    bool windows_fit = FitWindowRectsInBounds(
        overview_mode_bounds, std::min(kMaxHeight, height), ignored_items,
        &rects, &max_bottom, &min_right, &max_right);

    if (height_fixed) {
      if (!windows_fit) {
        // Revert the previous change to |right_bound| and do one last pass.
        right_bound++;
        make_last_adjustment = true;
        break;
      }
      // Break if all the windows are zero-width at the current scale.
      if (max_right <= total_bounds.x())
        break;
    } else {
      // Find the optimal row height bisecting between |low_height| and
      // |high_height|.
      if (windows_fit)
        low_height = height;
      else
        high_height = height;
      height = 0.5 * (low_height + high_height);
      // When height can no longer be improved, start balancing the rows.
      if (height == low_height)
        height_fixed = true;
    }

    if (windows_fit && height_fixed) {
      if (max_right - min_right <= width_diff) {
        // Row alignment is getting better. Try to shrink the |right_bound| in
        // order to squeeze the widest row.
        right_bound = max_right - 1;
        width_diff = max_right - min_right;
      } else {
        // Row alignment is getting worse.
        // Revert the previous change to |right_bound| and do one last pass.
        right_bound++;
        make_last_adjustment = true;
        break;
      }
    }
  }
  // Once the windows in |item_list_| no longer fit, the change to
  // |right_bound| was reverted. Perform one last pass to position the |rects|.
  if (make_last_adjustment) {
    gfx::Rect overview_mode_bounds(total_bounds);
    overview_mode_bounds.set_width(right_bound - total_bounds.x());
    FitWindowRectsInBounds(overview_mode_bounds, std::min(kMaxHeight, height),
                           ignored_items, &rects, &max_bottom, &min_right,
                           &max_right);
  }

  MaybeCenterOverviewItems(ignored_items, rects);

  gfx::Vector2dF offset(0, (total_bounds.bottom() - max_bottom) / 2.f);
  for (auto& rect : rects)
    rect += offset;

  return rects;
}

std::vector<gfx::RectF> OverviewGrid::GetWindowRectsForScrollingLayout(
    const base::flat_set<OverviewItemBase*>& ignored_items) {
  gfx::Rect total_bounds = GetGridEffectiveBounds();
  // Windows occupy vertically centered area with additional vertical insets.
  total_bounds.Inset(GetGridInsetsImpl(total_bounds));
  total_bounds.Inset(
      gfx::Insets::TLBR(kTabletModeOverviewItemTopPaddingDp, 0, 0, 0));

  // `scroll_offset_min_` may be changed on positioning (either by closing
  // windows or display changes). Recalculate it and clamp `scroll_offset_`, so
  // that the items are always aligned left or right.
  float rightmost_window_right = 0;
  for (const auto& item : item_list_) {
    if (ShouldExcludeItemFromGridLayout(item.get(), ignored_items))
      continue;
    rightmost_window_right =
        std::max(rightmost_window_right, item->target_bounds().right());
  }

  // `rightmost_window_right` may have been modified by an earlier scroll.
  // `scroll_offset_` is added to adjust for that. If `rightmost_window_right`
  // is less than `total_bounds.right()`, the grid cannot be scrolled. Set
  // `scroll_offset_min_` to 0 so that `std::clamp()` is happy.
  rightmost_window_right -= scroll_offset_;
  scroll_offset_min_ = total_bounds.right() - rightmost_window_right;
  if (scroll_offset_min_ > 0.f)
    scroll_offset_min_ = 0.f;

  scroll_offset_ = std::clamp(scroll_offset_, scroll_offset_min_, 0.f);

  // Map which contains up to |kScrollingLayoutRow| entries with information on
  // the last items right bound per row. Used so we can place the next item
  // directly next to the last item. The key is the y-value of the row, and the
  // value is the rightmost x-value.
  base::flat_map<float, float> right_edge_map;

  // Since the number of rows is limited, windows are laid out column-wise so
  // that the most recently used windows are displayed first. When the dragged
  // item becomes an |ignored_item|, move the other windows accordingly.
  // |window_position| matches the positions of the windows' indexes from
  // |item_list_|. However, if a window turns out to be an ignored item,
  // |window_position| remains where the item was as to then reposition the
  // other window's bounds in place of that item.
  const int height = (total_bounds.height() - ((kScrollingLayoutRow - 1) *
                                               kVerticalSpaceBetweenItemsDp)) /
                     kScrollingLayoutRow;
  int window_position = 0;
  std::vector<gfx::RectF> rects;
  for (const auto& window : item_list_) {
    OverviewItemBase* item = window.get();
    if (ShouldExcludeItemFromGridLayout(item, ignored_items)) {
      rects.emplace_back();
      continue;
    }

    // Calculate the width and y position of the item.
    const int width = CalculateWidthAndMaybeSetUnclippedBounds(item, height);
    const int y = (height + kVerticalSpaceBetweenItemsDp) *
                      (window_position % kScrollingLayoutRow) +
                  total_bounds.y();

    // Use the right bounds of the item next to in the row as the x position, if
    // that item exists.
    const int x = right_edge_map.contains(y)
                      ? right_edge_map[y]
                      : total_bounds.x() + scroll_offset_;
    right_edge_map[y] = x + width + kHorizontalSpaceBetweenItemsDp;
    DCHECK_LE(static_cast<int>(right_edge_map.size()), kScrollingLayoutRow);

    const gfx::RectF bounds(x, y, width, height);
    rects.push_back(bounds);
    ++window_position;
  }

  return rects;
}

bool OverviewGrid::FitWindowRectsInBounds(
    const gfx::Rect& bounds,
    int height,
    const base::flat_set<OverviewItemBase*>& ignored_items,
    std::vector<gfx::RectF>* out_rects,
    int* out_max_bottom,
    int* out_min_right,
    int* out_max_right) {
  const size_t item_count = item_list_.size();
  out_rects->resize(item_count);

  // Start in the top-left corner of |bounds|.
  int left = bounds.x();
  int top = bounds.y();

  // Keep track of the lowest coordinate.
  *out_max_bottom = bounds.y();

  // Right bound of the narrowest row.
  *out_min_right = bounds.right();
  // Right bound of the widest row.
  *out_max_right = bounds.x();

  // All elements are of same height and only the height is necessary to
  // determine each item's scale.
  for (size_t i = 0u; i < item_count; ++i) {
    const auto& item = item_list_[i];
    if (ShouldExcludeItemFromGridLayout(item.get(), ignored_items)) {
      continue;
    }

    int width = CalculateWidthAndMaybeSetUnclippedBounds(item.get(), height);

    if ((left + width + kHorizontalSpaceBetweenItemsDp) > bounds.right()) {
      // Move to the next row if possible.
      if (*out_min_right > left)
        *out_min_right = left;
      if (*out_max_right < left)
        *out_max_right = left;
      top += (height + kVerticalSpaceBetweenItemsDp);

      // Check if the new row reaches the bottom or if the first item in the new
      // row does not fit within the available width.
      if ((top + height + kVerticalSpaceBetweenItemsDp) > bounds.bottom() ||
          bounds.x() + width + kHorizontalSpaceBetweenItemsDp >
              bounds.right()) {
        return false;
      }
      left = bounds.x();
    }

    // Position the current rect.
    (*out_rects)[i] = gfx::RectF(left, top, width, height);

    // Increment horizontal position using sanitized positive `width`.
    left += (width + kHorizontalSpaceBetweenItemsDp);

    *out_max_bottom = top + height;
  }

  // Update the narrowest and widest row width for the last row.
  if (*out_min_right > left)
    *out_min_right = left;
  if (*out_max_right < left)
    *out_max_right = left;

  return true;
}

void OverviewGrid::MaybeCenterOverviewItems(
    const base::flat_set<OverviewItemBase*>& ignored_items,
    std::vector<gfx::RectF>& out_window_rects) {
  if (!features::IsForestFeatureEnabled()) {
    return;
  }

  if (out_window_rects.empty()) {
    return;
  }

  gfx::RangeF current_row_union_range(out_window_rects[0].x(),
                                      out_window_rects[0].right());
  int current_row_y = out_window_rects[0].y();
  int current_row_first_item_index = 0;

  // Batch process to center overview items within the same row.
  auto batch_center_overview_items = [&](size_t end_index) {
    // Calculate the shift amount `current_diff` required to center the overview
    // items.
    const float range_center =
        (current_row_union_range.start() + current_row_union_range.end()) / 2.f;
    const int center_position = GetGridEffectiveBounds().CenterPoint().x();
    float current_diff = std::round(std::abs(center_position - range_center));
    for (size_t j = current_row_first_item_index; j < end_index; j++) {
      out_window_rects[j].Offset(current_diff, 0);
    }
  };

  for (size_t i = 0; i < out_window_rects.size(); i++) {
    if (ShouldExcludeItemFromGridLayout(item_list_[i].get(), ignored_items)) {
      continue;
    }

    gfx::RectF& rect = out_window_rects[i];
    if (rect.y() != current_row_y) {
      // As a new row begins processing, batch-shift the previous row's rects
      // and reset its parameters.
      batch_center_overview_items(i);
      current_row_union_range.set_start(rect.x());
      current_row_y = rect.y();
      current_row_first_item_index = i;
    }

    // Extend the range by adding the `rect`'s width and extra in-between items
    // spacing.
    current_row_union_range.set_end(rect.right());
  }

  // Post-processing rects in the last row.
  batch_center_overview_items(out_window_rects.size());
}

size_t OverviewGrid::GetOverviewItemIndex(OverviewItemBase* item) const {
  auto iter = base::ranges::find(item_list_, item,
                                 &std::unique_ptr<OverviewItemBase>::get);
  CHECK(iter != item_list_.end());
  return iter - item_list_.begin();
}

size_t OverviewGrid::FindInsertionIndex(const aura::Window* window) const {
  const auto mru_windows =
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);

  // As we iterate over the whole MRU window list, the windows in this grid
  // will be encountered in the same order, but possibly with other windows in
  // between. Ignore those other windows, and only increment `grid_item_index`
  // when we reach the next window in this grid.
  size_t grid_item_index = 0, mru_window_index = 0;
  while (grid_item_index < item_list_.size() &&
         mru_window_index < mru_windows.size()) {
    OverviewItemBase* grid_item = item_list_[grid_item_index].get();
    aura::Window* mru_window = mru_windows[mru_window_index];
    if (grid_item == drop_target_ || mru_window == window) {
      return grid_item_index;
    }

    if (grid_item->Contains(mru_window)) {
      grid_item_index++;
    }

    mru_window_index++;
  }

  // If there is no drop target window and `window` is not in the MRU window
  // list, insert at the end.
  return item_list_.size();
}

void OverviewGrid::AddDraggedWindowIntoOverviewOnDragEnd(
    aura::Window* dragged_window) {
  DCHECK(overview_session_);
  if (overview_session_->IsWindowInOverview(dragged_window))
    return;

  overview_session_->AddItemInMruOrder(dragged_window, /*reposition=*/false,
                                       /*animate=*/false, /*restack=*/true,
                                       /*use_spawn_animation=*/false);
}

gfx::Rect OverviewGrid::GetDesksWidgetBounds() const {
  gfx::Rect desks_widget_screen_bounds = bounds_;
  desks_widget_screen_bounds.set_height(GetDesksBarHeight());

  // Shift the widget down to make room for the splitview indicator guidance
  // when it's shown at the top of the screen and no other windows are snapped.
  if (split_view_drag_indicators_ &&
      split_view_drag_indicators_->current_window_dragging_state() ==
          SplitViewDragIndicators::WindowDraggingState::kFromOverview &&
      !IsLayoutHorizontal(root_window_) &&
      !SplitViewController::Get(root_window_)->InSplitViewMode()) {
    desks_widget_screen_bounds.Offset(
        0, split_view_drag_indicators_->GetLeftHighlightViewBounds().height() +
               2 * kHighlightScreenEdgePaddingDp);
  }

  return screen_util::SnapBoundsToDisplayEdge(desks_widget_screen_bounds,
                                              root_window_);
}

gfx::Rect OverviewGrid::GetBirchBarWidgetBounds() const {
  CHECK(birch_bar_view_);

  // Calculate the available space for birch bar.
  const gfx::Insets paddings = GetGridHorizontalPaddings();
  gfx::Rect available_space = bounds_;
  available_space.Inset(paddings);

  // Update the available space of the birch bar and get the preferred size.
  birch_bar_view_->UpdateAvailableSpace(available_space.width());
  const gfx::Size birch_bar_widget_size = birch_bar_view_->GetPreferredSize();

  const int birch_bar_bottom_padding = GetBirchBarBottomPadding(root_window_);

  // Centeralize the bich bar at the bottom.
  const int top_inset = available_space.height() -
                        birch_bar_widget_size.height() -
                        birch_bar_bottom_padding;
  const int horizontal_inset =
      (available_space.width() - birch_bar_widget_size.width()) / 2;

  available_space.Inset(gfx::Insets::TLBR(
      top_inset, horizontal_inset, birch_bar_bottom_padding, horizontal_inset));
  return available_space;
}

void OverviewGrid::UpdateCannotSnapWarningVisibility(bool animate) {
  for (auto& overview_mode_item : item_list_) {
    overview_mode_item->UpdateCannotSnapWarningVisibility(animate);
  }
}

void OverviewGrid::OnSaveDeskAsTemplateButtonPressed() {
  auto* container = save_desk_button_container_widget_->GetContentsView();
  // Disable the save desk button container after the first click to prevent
  // unwanted clicks/user interaction.
  if (!container->GetEnabled())
    return;
  container->SetEnabled(false);

  overview_session_->saved_desk_presenter()->MaybeSaveActiveDeskAsSavedDesk(
      DeskTemplateType::kTemplate, root_window());
}

void OverviewGrid::OnSaveDeskForLaterButtonPressed() {
  auto* container = save_desk_button_container_widget_->GetContentsView();
  // Disable the save desk button container after the first click to prevent
  // unwanted clicks/user interaction.
  if (!container->GetEnabled())
    return;
  container->SetEnabled(false);

  overview_session_->saved_desk_presenter()->MaybeSaveActiveDeskAsSavedDesk(
      DeskTemplateType::kSaveAndRecall, root_window());
}

void OverviewGrid::OnSavedDeskGridFadedOut() {
  for (auto& overview_mode_item : item_list_) {
    overview_mode_item->RevertHideForSavedDeskLibrary(/*animate=*/true);
  }

  saved_desk_library_widget_->Hide();

  desks_bar_view_->UpdateButtonsForSavedDeskGrid();
  UpdateSaveDeskButtons();
  UpdateNoWindowsWidget(/*no_items=*/empty(), /*animate=*/true,
                        /*is_continuous_enter=*/false);
  if (informed_restore_widget_) {
    informed_restore_widget_->GetNativeWindow()->SetEventTargetingPolicy(
        aura::EventTargetingPolicy::kTargetAndDescendants);
    PerformFadeInLayer(informed_restore_widget_->GetLayer(), /*animate=*/true);
  }
}

void OverviewGrid::OnSaveDeskButtonContainerFadedOut() {
  save_desk_button_container_widget_->Hide();
}

void OverviewGrid::OnBirchBarLayoutChanged(
    BirchBarView::RelayoutReason reason) {
  if (reason == BirchBarView::RelayoutReason::kAvailableSpaceChanged) {
    return;
  }

  if (!MaybeUpdateBirchBarWidgetBounds()) {
    return;
  }

  // Animate wallpaper clipping.
  if (scoped_overview_wallpaper_clipper_) {
    // Perform wallpaper clipping animations according to relayout reason.
    using AnimationType = ScopedOverviewWallpaperClipper::AnimationType;
    using RelayoutReason = BirchBarView::RelayoutReason;

    auto animation_type = AnimationType::kNone;
    base::OnceClosure animation_callback;
    switch (reason) {
      case RelayoutReason::kSetup:
        animation_type = AnimationType::kShowBirchBarInOverview;
        break;
      case RelayoutReason::kSetupByUser:
        animation_type = AnimationType::kShowBirchBarByUser;
        break;
      case RelayoutReason::kClearOnDisabled:
        animation_type = AnimationType::kHideBirchBarByUser;
        animation_callback =
            base::BindOnce(&OverviewGrid::DestroyBirchBarWidget,
                           weak_ptr_factory_.GetWeakPtr(), /*by_user=*/true);
        break;
      case RelayoutReason::kAddRemoveChip:
        // If the last chip was removed, perform hiding bar animation.
        if (!birch_bar_view_->GetChipsNum()) {
          animation_type = AnimationType::kHideBirchBarByUser;
        }
        break;
      case RelayoutReason::kAvailableSpaceChanged:
        break;
    }
    scoped_overview_wallpaper_clipper_->RefreshWallpaperClipBounds(
        animation_type, std::move(animation_callback));

    // If the relayout is due to showing birch bar by user, we need to refresh
    // the grids with wallpaper clipping animation. This must be called after
    // refreshing wallpaper clipping bounds, because refreshing grid bounds may
    // also update wallpaper clipping bounds without animation.
    if (reason == RelayoutReason::kSetupByUser) {
      RefreshGridBounds(/*animate=*/true);
    }
  }

  // A relayout means the bar's accessibility may have changed.
  overview_session_->UpdateAccessibilityFocus();
}

void OverviewGrid::RefreshDesksWidgets(bool visible) {
  if (!visible) {
    views::Widget::Widgets desks_widgets = {
        desks_widget_.get(), saved_desk_library_widget_.get(),
        save_desk_button_container_widget_.get()};
    aura::Window::Windows hide_windows;
    base::ranges::for_each(
        desks_widgets, [&hide_windows](views::Widget* widget) {
          if (widget) {
            hide_windows.emplace_back(widget->GetNativeWindow());
          }
        });

    hide_windows_in_partial_overview_ =
        std::make_unique<ScopedOverviewHideWindows>(
            /*windows=*/hide_windows,
            /*forced_hidden=*/true);
  } else {
    hide_windows_in_partial_overview_.reset();
    MaybeUpdateDesksWidgetBounds();
  }
}

void OverviewGrid::UpdateNumSavedDeskUnsupportedWindows(
    const std::vector<raw_ptr<aura::Window, VectorExperimental>>& windows,
    bool increment) {
  if (!saved_desk_util::ShouldShowSavedDesksOptions()) {
    return;
  }

  int addend = increment ? 1 : -1;

  // Track the number of unsupported and incognito windows. The saved desk
  // buttons are disabled if there are no supported or non-incognito windows.
  for (aura::Window* window : windows) {
    if (IsUnsupportedWindow(window)) {
      num_unsupported_windows_ += addend;
    } else if (IsIncognitoWindow(window)) {
      num_incognito_windows_ += addend;
    }

    // TODO(b/319904368): Clean this up after we figure out which app changes
    // its supported/incognito type and a proper fix is made.
    if (num_unsupported_windows_ < 0) {
      num_unsupported_windows_ = 0;
      SCOPED_CRASH_KEY_NUMBER(
          "OG_UNSDUW", "unsupported_app_type",
          static_cast<int>(window->GetProperty(chromeos::kAppTypeKey)));
      SCOPED_CRASH_KEY_STRING32("OG_UNSDUW", "unsupported_app_id",
                                ::full_restore::GetAppId(window));
      base::debug::DumpWithoutCrashing();
    } else if (num_incognito_windows_ < 0) {
      num_incognito_windows_ = 0;
      SCOPED_CRASH_KEY_NUMBER(
          "OG_UNSDUW", "incognito_app_type",
          static_cast<int>(window->GetProperty(chromeos::kAppTypeKey)));
      SCOPED_CRASH_KEY_STRING32("OG_UNSDUW", "incognito_app_id",
                                ::full_restore::GetAppId(window));
      base::debug::DumpWithoutCrashing();
    }
  }
}

int OverviewGrid::GetDesksBarHeight() const {
  DeskBarViewBase::State state = desks_bar_view_
                                     ? desks_bar_view_->state()
                                     : DeskBarViewBase::GetPreferredState(
                                           DeskBarViewBase::Type::kOverview);
  return DeskBarViewBase::GetPreferredBarHeight(
      root_window_, DeskBarViewBase::Type::kOverview, state);
}

bool OverviewGrid::ShouldUseScrollingLayout(size_t ignored_items_size) const {
  if (Shell::Get()->IsInTabletMode()) {
    return item_list_.size() - ignored_items_size >=
           kMinimumItemsForScrollingLayout;
  }

  return false;
}

void OverviewGrid::AddDropTargetImpl(OverviewItemBase* dragged_item,
                                     size_t position,
                                     bool animate) {
  CHECK(!drop_target_);

  auto drop_target = std::make_unique<OverviewDropTarget>(this);
  drop_target_ = drop_target.get();
  item_list_.insert(item_list_.begin() + position, std::move(drop_target));

  base::flat_set<OverviewItemBase*> ignored_items;
  if (dragged_item) {
    ignored_items.insert(dragged_item);
  }
  PositionWindows(animate, ignored_items);
  UpdateNoWindowsWidget(empty(), animate, /*is_continuous_enter=*/false);
}

void OverviewGrid::OnSkipButtonPressed() {
  // Destroys `this`.
  // TODO(sophiewen): Consider adding another exit point metric.
  OverviewController::Get()->EndOverview(OverviewEndAction::kKeyEscapeOrBack);
}

void OverviewGrid::OnSettingsButtonPressed() {
  // Opens the OS Settings page, which causes a window activation change and
  // `EndOverview()` and destroys `this`.
  Shell::Get()->shell_delegate()->OpenMultitaskingSettings();
}

void OverviewGrid::UpdateSplitViewSetupViewWidget() {
  if (!SplitViewController::Get(root_window_)->InClamshellSplitViewMode()) {
    // If we aren't in split view, don't show the widget.
    split_view_setup_widget_.reset();
    return;
  }

  if (!split_view_setup_widget_) {
    views::Widget::InitParams params(
        views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
        views::Widget::InitParams::TYPE_POPUP);
    params.activatable = views::Widget::InitParams::Activatable::kYes;
    params.parent = desks_util::GetActiveDeskContainerForRoot(root_window_);
    params.name = "SplitViewSetupViewWidget";
    params.init_properties_container.SetProperty(kHideInDeskMiniViewKey, true);
    params.init_properties_container.SetProperty(kOverviewUiKey, true);
    split_view_setup_widget_ =
        std::make_unique<views::Widget>(std::move(params));
    split_view_setup_widget_->GetLayer()->SetFillsBoundsOpaquely(false);
    split_view_setup_widget_->SetContentsView(
        std::make_unique<SplitViewSetupView>(
            base::BindRepeating(&OverviewGrid::OnSkipButtonPressed,
                                weak_ptr_factory_.GetWeakPtr()),
            base::BindRepeating(&OverviewGrid::OnSettingsButtonPressed,
                                weak_ptr_factory_.GetWeakPtr())));
    split_view_setup_widget_->ShowInactive();
  }

  const gfx::Rect grid_bounds = GetGridEffectiveBounds();
  gfx::Rect centered_bounds(grid_bounds);
  const gfx::Size preferred_size =
      split_view_setup_widget_->GetContentsView()->GetPreferredSize();
  centered_bounds.ClampToCenteredSize(preferred_size);

  // If there are no windows, set it in the center of the grid.
  if (item_list_.empty()) {
    split_view_setup_widget_->SetBounds(centered_bounds);
    return;
  }

  // Position the widget under the bottom of the last overview item, but
  // centered horizontally.
  const int last_overview_item_bottom =
      item_list_.back()->target_bounds().bottom();

  // We need to maintain a minimum distance between the bottom of the toast and
  // the bottom of the grid bounds so that it won't be hidden by other UI
  // elements such as shelf. Under extreme condition, which should rarely
  // happen, if the bottom are of the partial overview grids is too small to
  // accommodate for both `kMinimumDistanceBetweenToastAndWorkAreaDp` and
  // `kSplitViewSetupToastSpacingDp`. We will prioritize the minimum
  // distance, under which condition the toast and settings button may appear
  // above the overview items.
  const int toast_y = std::min(
      last_overview_item_bottom + kSplitViewSetupToastSpacingDp,
      grid_bounds.bottom() - kMinimumDistanceBetweenToastAndWorkAreaDp -
          preferred_size.height());

  centered_bounds.set_y(toast_y);
  split_view_setup_widget_->SetBounds(centered_bounds);

  overview_session_->UpdateAccessibilityFocus();
}

bool OverviewGrid::ShouldInitDesksWidget() const {
  return desks_util::ShouldDesksBarBeCreated() && !desks_widget_;
}

}  // namespace ash