chromium/ash/wm/window_cycle/window_cycle_view.cc

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

#include "ash/wm/window_cycle/window_cycle_view.h"

#include <algorithm>
#include <optional>
#include <vector>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/system_shadow.h"
#include "ash/style/tab_slider.h"
#include "ash/style/tab_slider_button.h"
#include "ash/utility/occlusion_tracker_pauser.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/window_cycle/window_cycle_controller.h"
#include "ash/wm/window_cycle/window_cycle_item_view.h"
#include "ash/wm/window_cycle/window_cycle_list.h"
#include "ash/wm/window_mini_view.h"
#include "ash/wm/wm_constants.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/animation_throughput_reporter.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/font.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"

namespace ash {

namespace {

// Shield rounded corner radius.
constexpr int kBackgroundCornerRadius = 16;

// Shield horizontal inset.
constexpr int kBackgroundHorizontalInsetDp = 40;

// Vertical padding between the alt-tab bandshield and the window previews.
constexpr int kInsideBorderVerticalPaddingDp = 60;

// Padding between the alt-tab bandshield and the tab slider container.
constexpr int kMirrorContainerVerticalPaddingDp = 24;

// Padding between the window previews within the alt-tab bandshield.
constexpr int kBetweenChildPaddingDp = 12;

// Padding between the tab slider button and the tab slider container.
constexpr int kTabSliderContainerVerticalPaddingDp = 32;

// The font size of "No recent items" string when there's no window in the
// window cycle list.
constexpr int kNoRecentItemsLabelFontSizeDp = 14;

// The UMA histogram that logs smoothness of the fade-in animation.
constexpr char kShowAnimationSmoothness[] =
    "Ash.WindowCycleView.AnimationSmoothness.Show";

// The UMA histogram that logs smoothness of the window container animation.
constexpr char kContainerAnimationSmoothness[] =
    "Ash.WindowCycleView.AnimationSmoothness.Container";

// Duration of the window cycle UI fade in animation.
constexpr base::TimeDelta kFadeInDuration = base::Milliseconds(100);

// Duration of the window cycle elements slide animation.
constexpr base::TimeDelta kContainerSlideDuration = base::Milliseconds(120);

// Duration of the window cycle scale animation when a user toggles alt-tab
// modes.
constexpr base::TimeDelta kToggleModeScaleDuration = base::Milliseconds(150);

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

// Builds the item view for window cycling for the given `window` with the
// correct parent. If the given `window` is a free-form window, the direct
// parent will be `mirror_container`. For `window` that belongs to a snap group,
// however, a `GroupContainerCycleView` will be added. If `same_app_only` is
// true, `GroupContainerCycleView` will only be created if both the windows in
// snap group belongs to the same app.
WindowMiniViewBase* BuildAndConfigureCycleView(
    aura::Window* window,
    views::View* mirror_container,
    std::vector<raw_ptr<WindowMiniViewBase, VectorExperimental>>& cycle_views,
    const std::vector<raw_ptr<aura::Window, VectorExperimental>>& windows,
    const bool same_app_only) {
  if (auto* snap_group_controller = SnapGroupController::Get()) {
    if (auto* snap_group =
            snap_group_controller->GetSnapGroupForGivenWindow(window)) {
      if (!same_app_only ||
          (same_app_only && base::Contains(windows, snap_group->window1()) &&
           base::Contains(windows, snap_group->window2()))) {
        // Create `GroupContainerCycleView` if `window` is physically left / top
        // snapped, which adds two child views subsequently. Skip adding
        // `GroupContainerCycleView` if `window` is secondary snapped since the
        // corresponding container view has been built.
        return window == snap_group->GetPhysicallyLeftOrTopWindow()
                   ? mirror_container->AddChildView(
                         std::make_unique<GroupContainerCycleView>(snap_group))
                   : nullptr;
      }
    }
  }

  // `mirror_container_` owns `view`. The `preview_view_` in `view` will use
  // trilinear filtering in InitLayerOwner().
  return mirror_container->AddChildView(
      std::make_unique<WindowCycleItemView>(window));
}

}  // namespace

WindowCycleView::WindowCycleView(aura::Window* root_window,
                                 const WindowList& windows,
                                 const bool same_app_only)
    : root_window_(root_window), same_app_only_(same_app_only) {
  const bool is_interactive_alt_tab_mode_allowed =
      Shell::Get()->window_cycle_controller()->IsInteractiveAltTabModeAllowed();

  DCHECK(!windows.empty() || is_interactive_alt_tab_mode_allowed);
  // Start the occlusion tracker pauser. It's used to increase smoothness for
  // the fade in but we also create windows here which may occlude other
  // windows.
  Shell::Get()->occlusion_tracker_pauser()->PauseUntilAnimationsEnd(
      kOcclusionTrackerPauseTimeout);

  // The layer for `this` is responsible for showing background blur and fade
  // and clip animations.
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetName("WindowCycleView");
  layer()->SetMasksToBounds(true);
  if (features::IsBackgroundBlurEnabled()) {
    layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
    layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
  }

  SetBackground(views::CreateThemedRoundedRectBackground(
      cros_tokens::kCrosSysScrim2, kBackgroundCornerRadius));
  SetBorder(std::make_unique<views::HighlightBorder>(
      kBackgroundCornerRadius,
      views::HighlightBorder::Type::kHighlightBorderOnShadow));

  // `mirror_container_` may be larger than `this`. In this case, it will be
  // shifted along the x-axis when the user tabs through. It is a container
  // for the previews and has no rendered content.
  mirror_container_ = AddChildView(
      views::Builder<views::BoxLayoutView>()
          .SetPaintToLayer(ui::LAYER_NOT_DRAWN)
          .SetOrientation(views::BoxLayout::Orientation::kHorizontal)
          .SetInsideBorderInsets(gfx::Insets::TLBR(
              is_interactive_alt_tab_mode_allowed
                  ? kMirrorContainerVerticalPaddingDp
                  : kInsideBorderVerticalPaddingDp,
              WindowCycleView::kInsideBorderHorizontalPaddingDp,
              kInsideBorderVerticalPaddingDp,
              WindowCycleView::kInsideBorderHorizontalPaddingDp))
          .SetBetweenChildSpacing(kBetweenChildPaddingDp)
          .SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStart)
          .Build());
  mirror_container_->AddObserver(this);
  mirror_container_->layer()->SetName("WindowCycleView/MirrorContainer");

  if (is_interactive_alt_tab_mode_allowed) {
    tab_slider_ = AddChildView(std::make_unique<TabSlider>(/*max_tab_num=*/2));
    all_desks_tab_slider_button_ =
        tab_slider_->AddButton(std::make_unique<LabelSliderButton>(
            base::BindRepeating(
                &WindowCycleController::OnModeChanged,
                base::Unretained(Shell::Get()->window_cycle_controller()),
                /*per_desk=*/false,
                WindowCycleController::ModeSwitchSource::kClick),
            l10n_util::GetStringUTF16(IDS_ASH_ALT_TAB_ALL_DESKS_MODE)));
    current_desk_tab_slider_button_ =
        tab_slider_->AddButton(std::make_unique<LabelSliderButton>(
            base::BindRepeating(
                &WindowCycleController::OnModeChanged,
                base::Unretained(Shell::Get()->window_cycle_controller()),
                /*per_desk=*/true,
                WindowCycleController::ModeSwitchSource::kClick),
            l10n_util::GetStringUTF16(IDS_ASH_ALT_TAB_CURRENT_DESK_MODE)));

    auto* tab_slider_selector_view = tab_slider_->GetSelectorView();
    // Configure the focus ring for the tab slider selector view.
    views::FocusRing::Install(tab_slider_selector_view);
    auto* focus_ring = views::FocusRing::Get(tab_slider_selector_view);
    focus_ring->SetOutsetFocusRingDisabled(true);
    focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
    const float halo_inset = focus_ring->GetHaloThickness() / 2.f + 2;
    focus_ring->SetHaloInset(-halo_inset);
    // Set a pill shaped (fully rounded rect) highlight path to focus ring.
    focus_ring->SetPathGenerator(
        std::make_unique<views::PillHighlightPathGenerator>());
    focus_ring->SetHasFocusPredicate(base::BindRepeating(
        [](const WindowCycleView* cycle_view, const views::View* view) {
          return cycle_view->IsTabSliderFocused();
        },
        base::Unretained(this)));

    const bool per_desk =
        Shell::Get()->window_cycle_controller()->IsAltTabPerActiveDesk();
    current_desk_tab_slider_button_->SetSelected(per_desk);
    all_desks_tab_slider_button_->SetSelected(!per_desk);

    no_recent_items_label_ = AddChildView(std::make_unique<views::Label>(
        l10n_util::GetStringUTF16(IDS_ASH_OVERVIEW_NO_RECENT_ITEMS)));
    no_recent_items_label_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
    no_recent_items_label_->SetVerticalAlignment(gfx::ALIGN_MIDDLE);

    no_recent_items_label_->SetEnabledColorId(kColorAshIconColorSecondary);
    no_recent_items_label_->SetFontList(
        no_recent_items_label_->font_list()
            .DeriveWithSizeDelta(
                kNoRecentItemsLabelFontSizeDp -
                no_recent_items_label_->font_list().GetFontSize())
            .DeriveWithWeight(gfx::Font::Weight::NORMAL));
    no_recent_items_label_->SetVisible(windows.empty());
    no_recent_items_label_->SetPreferredSize(gfx::Size(
        tab_slider_->GetPreferredSize().width() +
            2 * WindowCycleView::kInsideBorderHorizontalPaddingDp,
        WindowCycleItemView::kFixedPreviewHeightDp +
            kWindowMiniViewHeaderHeight + kMirrorContainerVerticalPaddingDp +
            kInsideBorderVerticalPaddingDp + 8));
  }

  for (aura::Window* window : windows) {
    if (auto* view = BuildAndConfigureCycleView(
            window, mirror_container_, cycle_views_, windows, same_app_only)) {
      cycle_views_.push_back(view);
      no_previews_list_.push_back(view);
    }
  }

  // The insets in the `WindowCycleItemView` are coming from its border, which
  // paints the focus ring around the view when it is focused. Exclude the
  // insets such that the spacing between the contents of the views rather
  // than the views themselves is `kBetweenChildPaddingDp`.
  const gfx::Insets cycle_item_insets =
      cycle_views_.empty() ? gfx::Insets() : cycle_views_.front()->GetInsets();
  mirror_container_->SetBetweenChildSpacing(kBetweenChildPaddingDp -
                                            cycle_item_insets.width());

  shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView(
      this, SystemShadow::Type::kElevation4);
  shadow_->SetRoundedCornerRadius(kBackgroundCornerRadius);
}

WindowCycleView::~WindowCycleView() = default;

void WindowCycleView::ScaleCycleView(const gfx::Rect& screen_bounds) {
  auto* layer_animator = layer()->GetAnimator();
  if (layer_animator->is_animating()) {
    // There is an existing scaling animation occurring. To accurately get the
    // new bounds for the next layout, we must abort the ongoing animation so
    // `this` will set the previous bounds of the widget and clear the clip
    // rect.
    layer_animator->AbortAllAnimations();
  }

  // `screen_bounds` is in screen coords so store it in local coordinates in
  // `new_bounds`.
  gfx::Rect old_bounds = GetLocalBounds();
  gfx::Rect new_bounds = gfx::Rect(screen_bounds.size());

  if (old_bounds == new_bounds)
    return;

  if (new_bounds.width() >= old_bounds.width()) {
    // In this case, the cycle view is growing. To achieve the scaling
    // animation we set the widget bounds immediately and scale the clipping
    // rect of `this`'s layer from where the `old_bounds` would be in the
    // new local coordinates.
    GetWidget()->SetBounds(screen_bounds);
    old_bounds +=
        gfx::Vector2d((new_bounds.width() - old_bounds.width()) / 2, 0);
  } else {
    // In this case, the cycle view is shrinking. To achieve the scaling
    // animation, we first scale the clipping rect and defer updating the
    // widget's bounds to when the animation is complete. If we instantly
    // laid out, then it wouldn't appear as though the background is
    // shrinking.
    new_bounds +=
        gfx::Vector2d((old_bounds.width() - new_bounds.width()) / 2, 0);
    defer_widget_bounds_update_ = true;
  }

  // Hide the shadow while animating because the clip rect animation clips away
  // visible portions of `this` while the shadow remains the size of `this`.
  shadow_->GetLayer()->SetVisible(false);

  layer()->SetClipRect(old_bounds);
  ui::ScopedLayerAnimationSettings settings(layer_animator);
  settings.SetTransitionDuration(kToggleModeScaleDuration);
  settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN_2);
  settings.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
  settings.AddObserver(this);
  layer()->SetClipRect(new_bounds);
}

gfx::Rect WindowCycleView::GetTargetBounds() const {
  // The widget is sized clamped to the screen bounds. Its child, the mirror
  // container which is parent to all the previews may be larger than the
  // widget as some previews will be offscreen. When `cycle_view_` does layout
  // the mirror container will be slid back and forth depending on the target
  // window.
  gfx::Rect widget_rect = root_window_->GetBoundsInScreen();
  widget_rect.ClampToCenteredSize(GetPreferredSize());
  return widget_rect;
}

void WindowCycleView::UpdateWindows(const WindowList& windows) {
  const bool no_windows = windows.empty();
  const bool is_interactive_alt_tab_mode_allowed =
      Shell::Get()->window_cycle_controller()->IsInteractiveAltTabModeAllowed();

  if (is_interactive_alt_tab_mode_allowed) {
    DCHECK(no_recent_items_label_);
    no_recent_items_label_->SetVisible(no_windows);
  }

  if (no_windows)
    return;

  for (aura::Window* window : windows) {
    if (auto* view = BuildAndConfigureCycleView(
            window, mirror_container_, cycle_views_, windows, same_app_only_)) {
      cycle_views_.push_back(view);
      no_previews_list_.push_back(view);
    }
  }

  // If there was an ongoing drag session, it's now been completed so reset
  // `horizontal_distance_dragged_`.
  horizontal_distance_dragged_ = 0.f;

  gfx::Rect widget_rect = GetTargetBounds();
  if (is_interactive_alt_tab_mode_allowed)
    ScaleCycleView(widget_rect);
  else
    GetWidget()->SetBounds(widget_rect);

  SetTargetWindow(windows[0]);
  ScrollToWindow(windows[0]);
}

void WindowCycleView::FadeInLayer() {
  DCHECK(GetWidget());

  layer()->SetOpacity(0.f);
  ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
  settings.SetTransitionDuration(kFadeInDuration);
  settings.AddObserver(this);
  settings.CacheRenderSurface();
  ui::AnimationThroughputReporter reporter(
      settings.GetAnimator(),
      metrics_util::ForSmoothnessV3(base::BindRepeating([](int smoothness) {
        UMA_HISTOGRAM_PERCENTAGE(kShowAnimationSmoothness, smoothness);
      })));

  layer()->SetOpacity(1.f);
}

void WindowCycleView::ScrollToWindow(aura::Window* target) {
  current_window_ = target;

  // If there was an ongoing drag session, it's now been completed so reset
  // |`horizontal_distance_dragged_`.
  horizontal_distance_dragged_ = 0.f;

  if (GetWidget())
    DeprecatedLayoutImmediately();
}

void WindowCycleView::SetTargetWindow(aura::Window* new_target) {
  // Hide the focus border of the previous target window and show the focus
  // border of the new one.
  if (target_window_) {
    if (auto* view = GetCycleViewForWindow(target_window_)) {
      view->ClearFocusSelection();
    }
  }

  target_window_ = new_target;
  if (auto* view = GetCycleViewForWindow(target_window_)) {
    view->SetSelectedWindowForFocus(target_window_);
  }

  // Focus the target window if the user is not currently switching the mode
  // while ChromeVox is on.
  // During the mode switch, we prevent ChromeVox auto-announce the window
  // title from the focus and send our custom string to announce both window
  // title and the selected mode together
  // (see `WindowCycleController::OnModeChanged`).
  auto* a11y_controller = Shell::Get()->accessibility_controller();
  auto* window_cycle_controller = Shell::Get()->window_cycle_controller();
  const bool chromevox_enabled = a11y_controller->spoken_feedback().enabled();
  const bool is_switching_mode = window_cycle_controller->IsSwitchingMode();
  if (!target_window_ || (chromevox_enabled && is_switching_mode)) {
    return;
  }

  auto* cycle_view = GetCycleViewForWindow(target_window_);
  CHECK(cycle_view);
  if (GetWidget()) {
    cycle_view->RequestFocus();
  } else {
    SetInitiallyFocusedView(cycle_view);
    // When alt-tab mode selection is available, announce via ChromeVox the
    // current mode and the directional cue for mode switching.
    if (window_cycle_controller->IsInteractiveAltTabModeAllowed()) {
      a11y_controller->TriggerAccessibilityAlertWithMessage(
          l10n_util::GetStringUTF8(
              window_cycle_controller->IsAltTabPerActiveDesk()
                  ? IDS_ASH_ALT_TAB_FOCUS_CURRENT_DESK_MODE
                  : IDS_ASH_ALT_TAB_FOCUS_ALL_DESKS_MODE));
    }
  }
}

void WindowCycleView::HandleWindowDestruction(aura::Window* destroying_window,
                                              aura::Window* new_target) {
  WindowMiniViewBase* preview = GetCycleViewForWindow(destroying_window);
  CHECK(preview);
  views::View* parent = preview->parent();
  CHECK_EQ(mirror_container_, parent);

  if (preview->TryRemovingChildItem(destroying_window) == 0) {
    // With no remaining child mini views contained in `preview`, we need to
    // remove `preview` and clean up the `preview` in `cycle_views_` and
    // `no_previews_list_`.
    std::erase(cycle_views_, preview);
    std::erase(no_previews_list_, preview);
    parent->RemoveChildViewT(preview);
  }
  // With one of its children now gone, we must re-layout `mirror_container_`.
  // This must happen before `ScrollToWindow()` to make sure our own `Layout()`
  // works correctly when it's calculating highlight bounds.
  parent->DeprecatedLayoutImmediately();
  SetTargetWindow(new_target);
  ScrollToWindow(new_target);
}

void WindowCycleView::DestroyContents() {
  is_destroying_ = true;
  cycle_views_.clear();
  no_previews_list_.clear();
  target_window_ = nullptr;
  current_window_ = nullptr;
  mirror_container_ = nullptr;
  no_recent_items_label_ = nullptr;
  tab_slider_ = nullptr;
  all_desks_tab_slider_button_ = nullptr;
  current_desk_tab_slider_button_ = nullptr;
  defer_widget_bounds_update_ = false;
  RemoveAllChildViews();
  OnFlingEnd();
}

void WindowCycleView::Drag(float delta_x) {
  horizontal_distance_dragged_ += delta_x;
  DeprecatedLayoutImmediately();
}

void WindowCycleView::StartFling(float velocity_x) {
  fling_handler_ = std::make_unique<WmFlingHandler>(
      gfx::Vector2dF(velocity_x, 0),
      GetWidget()->GetNativeWindow()->GetRootWindow(),
      base::BindRepeating(&WindowCycleView::OnFlingStep,
                          base::Unretained(this)),
      base::BindRepeating(&WindowCycleView::OnFlingEnd,
                          base::Unretained(this)));
}

bool WindowCycleView::OnFlingStep(float offset) {
  DCHECK(fling_handler_);
  horizontal_distance_dragged_ += offset;
  DeprecatedLayoutImmediately();
  return true;
}

void WindowCycleView::OnFlingEnd() {
  fling_handler_.reset();
}

void WindowCycleView::SetFocusTabSlider(bool focus) {
  DCHECK(tab_slider_);
  if (focus == is_tab_slider_focused_) {
    return;
  }

  is_tab_slider_focused_ = focus;
  views::FocusRing::Get(tab_slider_->GetSelectorView())->SchedulePaint();
}

bool WindowCycleView::IsTabSliderFocused() const {
  DCHECK(tab_slider_);
  return is_tab_slider_focused_;
}

aura::Window* WindowCycleView::GetWindowAtPoint(
    const gfx::Point& screen_point) {
  for (const ash::WindowMiniViewBase* view : cycle_views_) {
    if (auto* window = view->GetWindowAtPoint(screen_point)) {
      return window;
    }
  }
  return nullptr;
}

void WindowCycleView::OnModePrefsChanged() {
  const bool per_desk =
      Shell::Get()->window_cycle_controller()->IsAltTabPerActiveDesk();

  current_desk_tab_slider_button_->SetSelected(per_desk);
  all_desks_tab_slider_button_->SetSelected(!per_desk);
}

bool WindowCycleView::IsEventInTabSliderContainer(
    const gfx::Point& screen_point) const {
  return tab_slider_ && tab_slider_->GetBoundsInScreen().Contains(screen_point);
}

int WindowCycleView::CalculateMaxWidth() const {
  return root_window_->GetBoundsInScreen().size().width() -
         2 * kBackgroundHorizontalInsetDp;
}

gfx::Size WindowCycleView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  gfx::Size size = GetContentContainerBounds().size();
  // `mirror_container_` can have window list that overflow out of the
  // screen, but the window cycle view with a bandshield, cropping the
  // overflow window list, should remain within the specified horizontal
  // insets of the screen width.
  const int max_width = CalculateMaxWidth();
  size.set_width(std::min(size.width(), max_width));
  if (Shell::Get()
          ->window_cycle_controller()
          ->IsInteractiveAltTabModeAllowed()) {
    CHECK(tab_slider_);
    // `mirror_container_` can have window list with width smaller the tab
    // slider's width. The padding should be 64px from the tab slider.
    const int min_width = tab_slider_->GetPreferredSize().width() +
                          2 * WindowCycleView::kInsideBorderHorizontalPaddingDp;
    size.set_width(std::max(size.width(), min_width));
    size.Enlarge(0, tab_slider_->GetPreferredSize().height() +
                        kTabSliderContainerVerticalPaddingDp);
  }
  return size;
}

void WindowCycleView::Layout(PassKey) {
  if (is_destroying_)
    return;

  const bool is_interactive_alt_tab_mode_allowed =
      Shell::Get()->window_cycle_controller()->IsInteractiveAltTabModeAllowed();
  if (bounds().IsEmpty() || (!is_interactive_alt_tab_mode_allowed &&
                             (!target_window_ || !current_window_))) {
    return;
  }

  const bool first_layout = mirror_container_->bounds().IsEmpty();
  // If `mirror_container_` has not yet been laid out, we must lay it and
  // its descendants out so that the calculations based on `target_view`
  // work properly.
  if (first_layout) {
    mirror_container_->SizeToPreferredSize();
    layer()->SetRoundedCornerRadius(
        gfx::RoundedCornersF{kBackgroundCornerRadius});
  }

  gfx::RectF target_bounds;
  if (current_window_ || !is_interactive_alt_tab_mode_allowed) {
    views::View* target_view = GetCycleViewForWindow(current_window_);
    target_bounds = gfx::RectF(target_view->GetLocalBounds());
    views::View::ConvertRectToTarget(target_view, mirror_container_,
                                     &target_bounds);
  } else {
    CHECK(no_recent_items_label_);
    target_bounds = gfx::RectF(no_recent_items_label_->bounds());
  }

  // Content container represents the mirror container with >=1 windows or
  // no-recent-items label when there is no window to be shown.
  gfx::Rect content_container_bounds = GetContentContainerBounds();

  // Case one: the container is narrower than the screen. Center the
  // container.
  int x_offset = (width() - content_container_bounds.width()) / 2;
  if (x_offset < 0) {
    // Case two: the container is wider than the screen. Center the target
    // view by moving the list just enough to ensure the target view is in
    // the center. Additionally, offset by however much the user has dragged.
    x_offset = width() / 2 - mirror_container_->GetMirroredXInView(
                                 target_bounds.CenterPoint().x());

    // However, the container must span the screen, i.e. the maximum x is 0
    // and the minimum for its right boundary is the width of the screen.
    int minimum_x = width() - content_container_bounds.width();
    x_offset = std::clamp(x_offset, minimum_x, 0);

    // If the user has dragged, offset the container based on how much they
    // have dragged. Cap `horizontal_distance_dragged_` based on the available
    // distance from the container to the left and right boundaries.
    float clamped_horizontal_distance_dragged = std::clamp(
        horizontal_distance_dragged_, static_cast<float>(minimum_x - x_offset),
        static_cast<float>(-x_offset));
    if (horizontal_distance_dragged_ != clamped_horizontal_distance_dragged)
      OnFlingEnd();

    horizontal_distance_dragged_ = clamped_horizontal_distance_dragged;
    x_offset += horizontal_distance_dragged_;
  }
  content_container_bounds.set_x(x_offset);

  // Layout a tab slider if there is more than one desk.
  if (is_interactive_alt_tab_mode_allowed) {
    CHECK(tab_slider_);
    CHECK(no_recent_items_label_);
    // Layout the tab slider.
    const gfx::Size tab_slider_size = tab_slider_->GetPreferredSize();
    const gfx::Rect tab_slider_mirror_container_bounds(
        (width() - tab_slider_size.width()) / 2,
        kTabSliderContainerVerticalPaddingDp, tab_slider_size.width(),
        tab_slider_size.height());
    tab_slider_->SetBoundsRect(tab_slider_mirror_container_bounds);

    // Move window cycle container down.
    content_container_bounds.set_y(tab_slider_->y() + tab_slider_->height());

    // Unlike the bounds of scrollable mirror container, the bounds of label
    // should not overflow out of the screen.
    const gfx::Rect no_recent_item_bounds_(
        std::max(0, content_container_bounds.x()), content_container_bounds.y(),
        std::min(width(), content_container_bounds.width()),
        content_container_bounds.height());
    no_recent_items_label_->SetBoundsRect(no_recent_item_bounds_);
  }

  // Enable animations only after the first layout pass. If `this` is animating
  // or `defer_widget_bounds_update_`, don't animate as well since the cycle
  // view is already being animated or just finished animating for mode switch.
  std::unique_ptr<ui::ScopedLayerAnimationSettings> settings;
  std::optional<ui::AnimationThroughputReporter> reporter;
  if (!first_layout && !this->layer()->GetAnimator()->is_animating() &&
      !defer_widget_bounds_update_ &&
      mirror_container_->bounds() != content_container_bounds) {
    settings = std::make_unique<ui::ScopedLayerAnimationSettings>(
        mirror_container_->layer()->GetAnimator());
    settings->SetTransitionDuration(kContainerSlideDuration);
    reporter.emplace(
        settings->GetAnimator(),
        metrics_util::ForSmoothnessV3(base::BindRepeating([](int smoothness) {
          // Reports animation metrics when the mirror container, which holds
          // all the preview views slides along the x-axis. This can happen
          // while tabbing through windows, if the window cycle ui spans the
          // length of the display.
          UMA_HISTOGRAM_PERCENTAGE(kContainerAnimationSmoothness, smoothness);
        })));
  }
  mirror_container_->SetBoundsRect(content_container_bounds);
}

void WindowCycleView::OnImplicitAnimationsCompleted() {
  layer()->SetClipRect(gfx::Rect());
  if (defer_widget_bounds_update_) {
    // This triggers layout, so reset `defer_widget_bounds_update_` after
    // calling `SetBounds()` to prevent the mirror container from animating.
    GetWidget()->SetBounds(GetTargetBounds());
    defer_widget_bounds_update_ = false;
  }

  shadow_->GetLayer()->SetVisible(true);
}

gfx::Rect WindowCycleView::GetContentContainerBounds() const {
  const bool empty_mirror_container = mirror_container_->children().empty();
  if (empty_mirror_container && no_recent_items_label_)
    return gfx::Rect(no_recent_items_label_->GetPreferredSize(
        views::SizeBounds(no_recent_items_label_->width(), {})));
  return gfx::Rect(mirror_container_->GetPreferredSize());
}

WindowMiniViewBase* WindowCycleView::GetCycleViewForWindow(
    aura::Window* window) const {
  for (ash::WindowMiniViewBase* view : cycle_views_) {
    if (view->Contains(window)) {
      return view;
    }
  }
  return nullptr;
}

void WindowCycleView::OnViewBoundsChanged(views::View* observed_view) {
  CHECK_EQ(mirror_container_.get(), observed_view);
  // If an element in `no_previews_list_` is onscreen (its bounds in `this`
  // coordinates intersects `this`), create the rest of its elements and
  // remove it from the set.
  const gfx::RectF local_bounds(GetLocalBounds());
  for (auto it = no_previews_list_.begin(); it != no_previews_list_.end();) {
    WindowMiniViewBase* view = *it;
    gfx::RectF bounds(view->GetLocalBounds());
    views::View::ConvertRectToTarget(view, this, &bounds);
    if (bounds.Intersects(local_bounds)) {
      view->SetShowPreview(true);
      view->RefreshItemVisuals();
      it = no_previews_list_.erase(it);
    } else {
      ++it;
    }
  }
}

BEGIN_METADATA(WindowCycleView)
END_METADATA

}  // namespace ash