chromium/ash/app_list/views/app_list_bubble_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/app_list/views/app_list_bubble_view.h"

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

#include "ash/accessibility/accessibility_controller.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/apps_collections_controller.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/views/app_list_a11y_announcer.h"
#include "ash/app_list/views/app_list_bubble_apps_collections_page.h"
#include "ash/app_list/views/app_list_bubble_apps_page.h"
#include "ash/app_list/views/app_list_bubble_search_page.h"
#include "ash/app_list/views/app_list_folder_view.h"
#include "ash/app_list/views/app_list_search_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/app_list/views/assistant/app_list_bubble_assistant_page.h"
#include "ash/app_list/views/folder_background_view.h"
#include "ash/app_list/views/scrollable_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/app_list/views/search_result_page_dialog_controller.h"
#include "ash/ash_element_identifiers.h"
#include "ash/bubble/bubble_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_config_provider.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/search_box/search_box_constants.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/icon_button.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "chromeos/constants/chromeos_features.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/ui_base_types.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_type.h"
#include "ui/events/event.h"
#include "ui/events/event_handler.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_shadow.h"

using views::BoxLayout;

namespace ash {
namespace {

// Folder view inset from the edge of the bubble.
constexpr int kFolderViewInset = 16;

AppListConfig* GetAppListConfig() {
  return AppListConfigProvider::Get().GetConfigForType(
      AppListConfigType::kDense, /*can_create=*/true);
}

// Returns true if ChromeVox (spoken feedback) is enabled.
bool IsSpokenFeedbackEnabled() {
  return Shell::Get()->accessibility_controller()->spoken_feedback().enabled();
}

// A simplified horizontal separator that uses a solid color layer for painting.
// This is more efficient than using a views::Separator, which would require
// SetPaintToLayer(ui::LAYER_TEXTURED).
class SeparatorWithLayer : public views::View {
  METADATA_HEADER(SeparatorWithLayer, views::View)

 public:
  SeparatorWithLayer() {
    SetPaintToLayer(ui::LAYER_SOLID_COLOR);
    // Color is set in OnThemeChanged().
    layer()->SetFillsBoundsOpaquely(false);
  }
  SeparatorWithLayer(const SeparatorWithLayer&) = delete;
  SeparatorWithLayer& operator=(const SeparatorWithLayer&) = delete;
  ~SeparatorWithLayer() override = default;

  // views::View:
  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override {
    // The parent's layout manager will stretch it horizontally.
    return gfx::Size(1, 1);
  }

  void OnThemeChanged() override {
    views::View::OnThemeChanged();
    layer()->SetColor(ColorProvider::Get()->GetContentLayerColor(
        ColorProvider::ContentLayerType::kSeparatorColor));
  }
};

BEGIN_METADATA(SeparatorWithLayer)
END_METADATA

// Returns the layer bounds to use for the start of the show animation and the
// end of the hide animation.
gfx::Rect GetShowHideAnimationBounds(bool is_side_shelf,
                                     gfx::Rect target_bounds) {
  // For either shelf: Height 75% → 100%
  const int delta_height = target_bounds.height() / 4;  // 25% of height
  const int initial_height = target_bounds.height() - delta_height;
  const int y_offset = 8;
  if (is_side_shelf) {
    // For side shelf: Y Position: Up 8px → End position, expanding down.
    return gfx::Rect(target_bounds.x(), target_bounds.y() - y_offset,
                     target_bounds.width(), initial_height);
  }
  // For bottom shelf: Y Position: Down 8px → End position, expanding up.
  return gfx::Rect(target_bounds.x(),
                   target_bounds.y() + delta_height + y_offset,
                   target_bounds.width(), initial_height);
}

const ui::DropTargetEvent GetTranslatedDropTargetEvent(
    const ui::DropTargetEvent event,
    views::View* src_view,
    views::View* dst_view) {
  gfx::Point event_location = event.location();
  views::View::ConvertPointToTarget(src_view, dst_view, &event_location);
  return ui::DropTargetEvent(event.data(), gfx::PointF(event_location),
                             event.root_location_f(),
                             event.source_operations());
}

}  // namespace

// Makes focus traversal skip the assistant button and the hide continue section
// button when pressing the down arrow key or the up arrow key. Normally views
// would move focus from the search box to the assistant button on arrow down.
// However, these buttons are visually to the right, so this feels weird.
// Likewise, on arrow up from continue tasks it feels better to put focus
// directly in the search box.
class ButtonFocusSkipper : public ui::EventHandler {
 public:
  ButtonFocusSkipper() { Shell::Get()->AddPreTargetHandler(this); }

  ~ButtonFocusSkipper() override { Shell::Get()->RemovePreTargetHandler(this); }

  void AddButton(views::View* button) {
    DCHECK(button);
    buttons_.push_back(button);
  }

  // ui::EventHandler:
  void OnEvent(ui::Event* event) override {
    // Don't adjust focus behavior if the user already focused the button.
    for (views::View* button : buttons_) {
      if (button->HasFocus()) {
        return;
      }
    }

    bool skip_focus = false;
    // This class overrides OnEvent() to examine all events so that focus
    // behavior is restored by mouse events, gesture events, etc.
    if (event->type() == ui::EventType::kKeyPressed) {
      ui::KeyboardCode key = event->AsKeyEvent()->key_code();
      if (key == ui::VKEY_UP || key == ui::VKEY_DOWN) {
        skip_focus = true;
      }
    }
    for (views::View* button : buttons_) {
      button->SetFocusBehavior(skip_focus ? views::View::FocusBehavior::NEVER
                                          : views::View::FocusBehavior::ALWAYS);
    }
  }

 private:
  std::vector<raw_ptr<views::View, VectorExperimental>> buttons_;
};

AppListBubbleView::AppListBubbleView(
    AppListViewDelegate* view_delegate,
    ApplicationDragAndDropHost* drag_and_drop_host)
    : view_delegate_(view_delegate) {
  DCHECK(view_delegate);
  DCHECK(drag_and_drop_host);
  SetProperty(views::kElementIdentifierKey, kAppListBubbleViewElementId);

  const float corner_radius = GetBubbleCornerRadius();
  // Set up rounded corners and background blur, similar to TrayBubbleView.
  // Layer background is set in OnThemeChanged().
  SetPaintToLayer();
  layer()->SetRoundedCornerRadius(gfx::RoundedCornersF{corner_radius});
  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetIsFastRoundedCorner(true);
  layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
  layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);

  ui::ColorId background_color_id = cros_tokens::kCrosSysSystemBaseElevated;
  SetBackground(views::CreateThemedRoundedRectBackground(background_color_id,
                                                         corner_radius));

  SetBorder(std::make_unique<views::HighlightBorder>(
      corner_radius, views::HighlightBorder::Type::kHighlightBorderOnShadow,
      /*insets_type=*/views::HighlightBorder::InsetsType::kHalfInsets));

  SetLayoutManager(std::make_unique<views::FillLayout>());
  a11y_announcer_ = std::make_unique<AppListA11yAnnouncer>(
      AddChildView(std::make_unique<views::View>()));
  InitContentsView(drag_and_drop_host);

  // Add assistant page as a top-level child so it will fill the bubble and
  // suggestion chips will appear at the bottom of the bubble view.
  assistant_page_ = AddChildView(std::make_unique<AppListBubbleAssistantPage>(
      view_delegate_->GetAssistantViewDelegate()));
  assistant_page_->SetVisible(false);

  InitFolderView(drag_and_drop_host);
  // Folder view is laid out manually based on its contents.
  folder_view_->SetProperty(views::kViewIgnoredByLayoutKey, true);

  AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
  AddAccelerator(ui::Accelerator(ui::VKEY_BROWSER_BACK, ui::EF_NONE));
}

AppListBubbleView::~AppListBubbleView() {
  // `a11y_announcer_` depends on a child view, so shut it down before view
  // hierarchy is destroyed.
  a11y_announcer_->Shutdown();

  // AppListFolderView may reference/observe an item on the root apps grid view
  // (associated with the folder), so destroy it before the root apps grid view.
  delete folder_view_;
  folder_view_ = nullptr;

  // Reset the dialog_controller for the AppsCollections page to prevent it from
  // dangling at destruction after the views are removed.
  apps_collections_page_->SetDialogController(nullptr);
}

void AppListBubbleView::UpdateSuggestions() {
  apps_page_->UpdateSuggestions();
}

void AppListBubbleView::SetDragAndDropHostOfCurrentAppList(
    ApplicationDragAndDropHost* drag_and_drop_host) {
  apps_page_->scrollable_apps_grid_view()->SetDragAndDropHostOfCurrentAppList(
      drag_and_drop_host);
  folder_view_->items_grid_view()->SetDragAndDropHostOfCurrentAppList(
      drag_and_drop_host);
}

void AppListBubbleView::InitContentsView(
    ApplicationDragAndDropHost* drag_and_drop_host) {
  auto* contents = AddChildView(std::make_unique<views::View>());

  auto* layout = contents->SetLayoutManager(
      std::make_unique<BoxLayout>(BoxLayout::Orientation::kVertical));
  layout->set_cross_axis_alignment(BoxLayout::CrossAxisAlignment::kStretch);

  search_box_view_ = contents->AddChildView(std::make_unique<SearchBoxView>(
      /*delegate=*/this, view_delegate_, /*is_app_list_bubble=*/true));
  search_box_view_->InitializeForBubbleLauncher();

  // Skip the assistant button on arrow up/down in app list.
  button_focus_skipper_ = std::make_unique<ButtonFocusSkipper>();
  if (features::IsSunfishFeatureEnabled()) {
    button_focus_skipper_->AddButton(search_box_view_->sunfish_button());
  }
  button_focus_skipper_->AddButton(search_box_view_->assistant_button());

  // The main view has a solid color layer, so the separator needs its own
  // layer to visibly paint.
  separator_ = contents->AddChildView(std::make_unique<SeparatorWithLayer>());

  auto* pages_container =
      contents->AddChildView(std::make_unique<views::View>());

  // Apps page and search page must both fill the page for page transition
  // animations to look right.
  pages_container->SetUseDefaultFillLayout(true);

  // The apps page must fill the bubble so that the apps grid view can flex to
  // include empty space below the visible icons. The search page doesn't care,
  // so flex the entire container.
  layout->SetFlexForView(pages_container, 1);

  search_page_dialog_controller_ =
      std::make_unique<SearchResultPageDialogController>(search_box_view_);

  // NOTE: Passing drag and drop host from a specific shelf instance assumes
  // that the `apps_page_` will not get reused for showing the app list in
  // another root window.
  apps_page_ =
      pages_container->AddChildView(std::make_unique<AppListBubbleAppsPage>(
          view_delegate_, drag_and_drop_host, GetAppListConfig(),
          a11y_announcer_.get(), /*folder_controller=*/this,
          /*search_box=*/search_box_view_));

  apps_collections_page_ = pages_container->AddChildView(
      std::make_unique<AppListBubbleAppsCollectionsPage>(
          view_delegate_, GetAppListConfig(), a11y_announcer_.get(),
          search_page_dialog_controller_.get(),
          base::BindOnce(&AppListBubbleView::ShowPage,
                         weak_factory_.GetWeakPtr(),
                         AppListBubblePage::kApps)));

  // Skip the "hide continue section" button on arrow up/down in app list.
  button_focus_skipper_->AddButton(
      apps_page_->toggle_continue_section_button());

  search_page_ =
      pages_container->AddChildView(std::make_unique<AppListBubbleSearchPage>(
          view_delegate_, search_page_dialog_controller_.get(),
          search_box_view_));
  search_page_->SetVisible(false);
}

bool AppListBubbleView::GetDropFormats(
    int* formats,
    std::set<ui::ClipboardFormatType>* format_types) {
  return apps_page_->scrollable_apps_grid_view()->GetDropFormats(formats,
                                                                 format_types);
}

bool AppListBubbleView::CanDrop(const OSExchangeData& data) {
  if (!apps_page_->GetVisible()) {
    return false;
  }

  return apps_page_->scrollable_apps_grid_view()->WillAcceptDropEvent(data);
}

void AppListBubbleView::OnDragExited() {
  apps_page_->scrollable_apps_grid_view()->OnDragExited();
}

void AppListBubbleView::OnDragEntered(const ui::DropTargetEvent& event) {
  AppsGridView* const scrollable_apps_grid =
      apps_page_->scrollable_apps_grid_view();

  scrollable_apps_grid->OnDragEntered(
      GetTranslatedDropTargetEvent(event, this, scrollable_apps_grid));
}

int AppListBubbleView::OnDragUpdated(const ui::DropTargetEvent& event) {
  AppsGridView* const scrollable_apps_grid =
      apps_page_->scrollable_apps_grid_view();

  return scrollable_apps_grid->OnDragUpdated(
      GetTranslatedDropTargetEvent(event, this, scrollable_apps_grid));
}

views::View::DropCallback AppListBubbleView::GetDropCallback(
    const ui::DropTargetEvent& event) {
  AppsGridView* const scrollable_apps_grid =
      apps_page_->scrollable_apps_grid_view();

  return scrollable_apps_grid->GetDropCallback(
      GetTranslatedDropTargetEvent(event, this, scrollable_apps_grid));
}

void AppListBubbleView::InitFolderView(
    ApplicationDragAndDropHost* drag_and_drop_host) {
  auto folder_view = std::make_unique<AppListFolderView>(
      this, apps_page_->scrollable_apps_grid_view(), a11y_announcer_.get(),
      view_delegate_, /*tablet_mode=*/false);
  folder_view->items_grid_view()->SetDragAndDropHostOfCurrentAppList(
      drag_and_drop_host);
  folder_view->UpdateAppListConfig(GetAppListConfig());
  folder_background_view_ =
      AddChildView(std::make_unique<FolderBackgroundView>(folder_view.get()));
  folder_background_view_->SetVisible(false);

  folder_view_ = AddChildView(std::move(folder_view));
  // Folder view will be set visible by its show animation.
  folder_view_->SetVisible(false);
}

void AppListBubbleView::StartShowAnimation(bool is_side_shelf) {
  // For performance, don't animate the shadow.
  view_shadow_.reset();

  // Ensure layout is up-to-date before animating views.
  if (needs_layout()) {
    DeprecatedLayoutImmediately();
  }
  DCHECK(!needs_layout());

  ui::AnimationThroughputReporter reporter(
      layer()->GetAnimator(),
      metrics_util::ForSmoothnessV3(base::BindRepeating([](int value) {
        base::UmaHistogramPercentage(
            "Apps.ClamshellLauncher.AnimationSmoothness.Open", value);
      })));

  // Animation specification for bottom shelf:
  //
  // Y Position: Down 8px → End position (visually moves up)
  // Duration: 150ms
  // Ease: (0.00, 0.00, 0.20, 1.00)
  //
  // Height: 75% → 100%
  // Duration: 150ms
  // Ease: (0.00, 0.00, 0.20, 1.00)
  //
  // Opacity: 0% → 100%
  // Duration: 150ms
  // Ease: Linear
  //
  // Side shelf uses shorter duration (100ms) and visually moves down.

  // Start by making the layer shorter, pushed down, and transparent.
  const gfx::Rect target_bounds = layer()->GetTargetBounds();
  layer()->SetBounds(GetShowHideAnimationBounds(is_side_shelf, target_bounds));
  layer()->SetOpacity(0.f);

  const base::TimeDelta duration =
      is_side_shelf ? base::Milliseconds(100) : base::Milliseconds(150);

  // Animate the layer to fully opaque at its target bounds.
  views::AnimationBuilder()
      .OnEnded(base::BindOnce(&AppListBubbleView::OnShowAnimationEnded,
                              weak_factory_.GetWeakPtr(), target_bounds))
      .OnAborted(base::BindOnce(&AppListBubbleView::OnShowAnimationEnded,
                                weak_factory_.GetWeakPtr(), target_bounds))
      .Once()
      .SetDuration(duration)
      .SetBounds(layer(), target_bounds, gfx::Tween::LINEAR_OUT_SLOW_IN)
      .SetOpacity(layer(), 1.f, gfx::Tween::LINEAR);

  // AppListBubbleAppsPage handles moving the individual views. It also handles
  // smoothness reporting, because the view movement animation has a longer
  // duration.
  if (current_page_ == AppListBubblePage::kApps)
    apps_page_->AnimateShowLauncher(is_side_shelf);

  // Note: The assistant page handles its own show animation internally.
}

void AppListBubbleView::StartHideAnimation(
    bool is_side_shelf,
    base::OnceClosure on_animation_ended) {
  is_hiding_ = true;
  on_hide_animation_ended_ = std::move(on_animation_ended);

  // For performance, don't animate the shadow.
  view_shadow_.reset();

  // Ensure any in-progress animations have their cleanup callbacks called.
  AbortAllAnimations();

  if (current_page_ == AppListBubblePage::kApps)
    apps_page_->PrepareForHideLauncher();

  const gfx::Rect target_bounds = layer()->GetTargetBounds();

  if (view_delegate_->ShouldDismissImmediately()) {
    // Don't animate, just clean up.
    OnHideAnimationEnded(target_bounds);
    return;
  }

  ui::AnimationThroughputReporter reporter(
      layer()->GetAnimator(),
      metrics_util::ForSmoothnessV3(base::BindRepeating([](int value) {
        base::UmaHistogramPercentage(
            "Apps.ClamshellLauncher.AnimationSmoothness.Close", value);
      })));

  // Animation spec:
  //
  // Y Position: Current → Down 8px, or for side shelf: Current → Up 8px
  // Duration: 100ms
  // Ease: (0.4, 0, 1, 1).
  //
  // Height: 100% → 75%
  // Duration: 100ms
  // Ease: (0.4, 0, 1, 1)
  //
  // Opacity: 100% → 0%
  // Duration: 100ms
  // Ease: Linear
  const gfx::Rect final_bounds =
      GetShowHideAnimationBounds(is_side_shelf, target_bounds);

  views::AnimationBuilder()
      .OnEnded(base::BindOnce(&AppListBubbleView::OnHideAnimationEnded,
                              weak_factory_.GetWeakPtr(), target_bounds))
      .OnAborted(base::BindOnce(&AppListBubbleView::OnHideAnimationEnded,
                                weak_factory_.GetWeakPtr(), target_bounds))
      .Once()
      .SetDuration(base::Milliseconds(100))
      .SetBounds(layer(), final_bounds, gfx::Tween::FAST_OUT_LINEAR_IN)
      .SetOpacity(layer(), 0.f, gfx::Tween::LINEAR);
}

void AppListBubbleView::AbortAllAnimations() {
  apps_page_->AbortAllAnimations();
  search_page_->AbortAllAnimations();
  apps_collections_page_->AbortAllAnimations();
  layer()->GetAnimator()->AbortAllAnimations();
}

bool AppListBubbleView::Back() {
  if (showing_folder_) {
    folder_view_->CloseFolderPage();
    return true;
  }
  if (search_box_view_->HasSearch()) {
    // When showing the `AppListBubblePage::kAssistant`, it will not change the
    // search box text. Therefore, if the `AppListBubblePage::kAssistant` is
    // from search result, the search query is not empty, Back() here will clear
    // the search, QueryChanged() will set the page to
    // `AppListBubblePage::kApps`. If the `AppListBubblePage::kAssistant` is
    // from other `AssistantVisibilityEntryPoint`, the search box is empty,
    // Back() will return false and then the AppList will be closed.
    if (IsShowingEmbeddedAssistantUI()) {
      view_delegate_->EndAssistant(
          assistant::AssistantExitPoint::kBackInLauncher);
    }
    search_box_view_->ClearSearch();
    return true;
  }

  return false;
}

void AppListBubbleView::ShowPage(AppListBubblePage page) {
  DVLOG(1) << __PRETTY_FUNCTION__ << " page " << page;
  if (page == current_page_) {
    return;
  }

  const AppListBubblePage previous_page = current_page_;
  current_page_ = page;

  // The assistant has its own text input field.
  search_box_view_->SetVisible(page != AppListBubblePage::kAssistant);
  separator_->SetVisible(page != AppListBubblePage::kAssistant);

  const bool supports_anchored_dialogs =
      page == AppListBubblePage::kApps ||
      page == AppListBubblePage::kAppsCollections ||
      page == AppListBubblePage::kSearch;

  search_page_dialog_controller_->Reset(/*enabled=*/supports_anchored_dialogs);
  assistant_page_->SetVisible(page == AppListBubblePage::kAssistant);
  switch (current_page_) {
    case AppListBubblePage::kNone:
      NOTREACHED();
    case AppListBubblePage::kApps:
      apps_page_->ResetScrollPosition();
      if (previous_page == AppListBubblePage::kSearch) {
        // Trigger hiding first so animations don't overlap.
        search_page_->AnimateHidePage();
        apps_page_->AnimateShowPage();
      } else if (previous_page == AppListBubblePage::kAppsCollections) {
        apps_collections_page_->AnimateHidePage();
        apps_page_->AnimateShowPage();
      } else {
        apps_page_->SetVisible(true);
        apps_collections_page_->SetVisible(false);
        search_page_->SetVisible(false);
      }
      a11y_announcer_->AnnounceAppListShown();
      MaybeFocusAndActivateSearchBox();
      break;
    case AppListBubblePage::kAppsCollections:
      if (previous_page == AppListBubblePage::kApps) {
        apps_page_->AnimateHidePage();
        apps_collections_page_->AnimateShowPage();
      } else if (previous_page == AppListBubblePage::kSearch) {
        // Trigger hiding first so animations don't overlap.
        search_page_->AnimateHidePage();
        apps_collections_page_->AnimateShowPage();
      } else {
        search_page_->SetVisible(false);
        apps_page_->SetVisible(false);
        apps_collections_page_->SetVisible(true);
      }
      MaybeFocusAndActivateSearchBox();
      break;
    case AppListBubblePage::kSearch:
      if (previous_page == AppListBubblePage::kApps) {
        apps_page_->AnimateHidePage();
        search_page_->AnimateShowPage();
      } else if (previous_page == AppListBubblePage::kAppsCollections) {
        apps_collections_page_->AnimateHidePage();
        search_page_->AnimateShowPage();
      } else {
        apps_page_->SetVisible(false);
        apps_collections_page_->SetVisible(false);
        search_page_->SetVisible(true);
      }
      MaybeFocusAndActivateSearchBox();
      break;
    case AppListBubblePage::kAssistant:
      if (showing_folder_)
        HideFolderView(/*animate=*/false, /*hide_for_reparent=*/false);
      if (previous_page == AppListBubblePage::kApps)
        apps_page_->AnimateHidePage();
      else
        apps_page_->SetVisible(false);
      search_page_->SetVisible(false);
      apps_collections_page_->SetVisible(false);
      // Explicitly set search box inactive so the next attempt to activate it
      // will succeed.
      search_box_view_->SetSearchBoxActive(
          false,
          /*event_type=*/ui::EventType::kUnknown);
      assistant_page_->RequestFocus();
      break;
  }
}

bool AppListBubbleView::IsShowingEmbeddedAssistantUI() const {
  return current_page_ == AppListBubblePage::kAssistant;
}

void AppListBubbleView::ShowEmbeddedAssistantUI() {
  DVLOG(1) << __PRETTY_FUNCTION__;
  if (IsShowingEmbeddedAssistantUI()) {
    return;
  }
  ShowPage(AppListBubblePage::kAssistant);
}

int AppListBubbleView::GetHeightToFitAllApps() const {
  return apps_page_->scroll_view()->contents()->bounds().height() +
         search_box_view_->GetPreferredSize().height();
}

void AppListBubbleView::UpdateContinueSectionVisibility() {
  apps_page_->UpdateContinueSectionVisibility();
}

void AppListBubbleView::UpdateForNewSortingOrder(
    const std::optional<AppListSortOrder>& new_order,
    bool animate,
    base::OnceClosure update_position_closure) {
  // If app list sort order change is animated, hide any open folders as part of
  // animation. If the update is not animated, e.g. when committing sort order,
  // keep the folder open to prevent folder closure when apps within the folder
  // are reordered, or whe the folder gets renamed.
  if (animate && showing_folder_)
    HideFolderView(/*animate=*/false, /*hide_for_reparent=*/false);

  base::OnceClosure done_closure;
  if (animate) {
    // The search box to ignore a11y events during the reorder animation
    // so that the announcement of app list reorder is made before that of
    // focus change.
    SetViewIgnoredForAccessibility(search_box_view_, true);

    // Focus on the search box before starting the reorder animation to prevent
    // focus moving through app list items as they're being hidden for order
    // update animation.
    search_box_view_->search_box()->RequestFocus();

    done_closure =
        base::BindOnce(&AppListBubbleView::OnAppListReorderAnimationDone,
                       weak_factory_.GetWeakPtr());
  }

  apps_page_->UpdateForNewSortingOrder(new_order, animate,
                                       std::move(update_position_closure),
                                       std::move(done_closure));
}

bool AppListBubbleView::AcceleratorPressed(const ui::Accelerator& accelerator) {
  switch (accelerator.key_code()) {
    case ui::VKEY_ESCAPE:
    case ui::VKEY_BROWSER_BACK:
      // If the ContentsView does not handle the back action, then this is the
      // top level, so we close the app list.
      if (!Back()) {
        view_delegate_->DismissAppList();
      }
      break;
    default:
      NOTREACHED();
  }

  // Don't let the accelerator propagate any further.
  return true;
}

void AppListBubbleView::Layout(PassKey) {
  LayoutSuperclass<views::View>(this);

  // The folder view has custom layout code that centers the folder over the
  // associated root apps grid folder item.
  // Folder bounds depend on the associated item view location in the apps
  // grid, so the folder needs to be laid out after the root apps grid.
  if (showing_folder_) {
    gfx::Rect folder_bounding_box = GetLocalBounds();
    folder_bounding_box.Inset(kFolderViewInset);
    folder_view_->SetBoundingBox(folder_bounding_box);
    folder_view_->UpdatePreferredBounds();
    // NOTE: Folder view bounds are also modified during reparent drag when the
    // view is "visible" but hidden offscreen. See app_list_folder_view.cc.
    folder_view_->SetBoundsRect(folder_view_->preferred_bounds());
    // The folder view updates the shadow bounds on its own when animating, so
    // only update the shadow bounds here when not animating.
    if (!folder_view_->IsAnimationRunning()) {
      folder_view_->UpdateShadowBounds();
    }
  }
}

void AppListBubbleView::QueryChanged(const std::u16string& trimmed_query,
                                     bool initiated_by_user) {
  if (current_page_ != AppListBubblePage::kNone) {
    search_page_->search_view()->UpdateForNewSearch(!trimmed_query.empty());
    if (!trimmed_query.empty()) {
      ShowPage(AppListBubblePage::kSearch);
    } else if (AppsCollectionsController::Get()->ShouldShowAppsCollection()) {
      ShowPage(AppListBubblePage::kAppsCollections);
    } else {
      ShowPage(AppListBubblePage::kApps);
    }
  }
  SchedulePaint();
}

void AppListBubbleView::AssistantButtonPressed() {
  // Showing the assistant via the delegate triggers the assistant's visibility
  // change notification and ensures its initial visual state is correct.
  view_delegate_->StartAssistant(
      assistant::AssistantEntryPoint::kLauncherSearchBoxIcon);
}

void AppListBubbleView::CloseButtonPressed() {
  // Activate and focus the search box.
  search_box_view_->SetSearchBoxActive(true,
                                       /*event_type=*/ui::EventType::kUnknown);
  search_box_view_->ClearSearch();
}

void AppListBubbleView::OnSearchBoxKeyEvent(ui::KeyEvent* event) {
  // Nothing to do. Search box starts focused, and FocusManager handles arrow
  // key traversal from there. ButtonFocusSkipper above handles skipping the
  // assistant and hide continue section buttons on arrow up and arrow down.
}

bool AppListBubbleView::CanSelectSearchResults() {
  return current_page_ == AppListBubblePage::kSearch &&
         search_page_->search_view()->CanSelectSearchResults();
}

void AppListBubbleView::ShowFolderForItemView(AppListItemView* folder_item_view,
                                              bool focus_name_input,
                                              base::OnceClosure hide_callback) {
  DVLOG(1) << __FUNCTION__;
  if (folder_view_->IsAnimationRunning()) {
    return;
  }

  // TODO(jamescook): Record metric for folder open. Either use the existing
  // Apps.AppListFolderOpened or introduce a new metric.

  DCHECK(folder_item_view->is_folder());
  folder_view_->ConfigureForFolderItemView(folder_item_view,
                                           std::move(hide_callback));
  showing_folder_ = true;
  DeprecatedLayoutImmediately();
  folder_background_view_->SetVisible(true);
  folder_view_->ScheduleShowHideAnimation(/*show=*/true,
                                          /*hide_for_reparent=*/false);
  if (focus_name_input) {
    folder_view_->FocusNameInput();
  } else if (apps_page_->scrollable_apps_grid_view()->has_selected_view() ||
             IsSpokenFeedbackEnabled()) {
    // If the user is keyboard navigating, or using ChromeVox (spoken feedback),
    // move focus into the folder.
    folder_view_->FocusFirstItem(/*silently=*/false);
  } else {
    // Release focus so that disabling the views below does not shift focus
    // into the folder grid.
    GetFocusManager()->ClearFocus();
  }
  // Disable items behind the folder so they will not be reached in focus
  // traversal.
  DisableFocusForShowingActiveFolder(true);
}

void AppListBubbleView::ShowApps(AppListItemView* folder_item_view,
                                 bool select_folder) {
  DVLOG(1) << __FUNCTION__;
  if (folder_view_->IsAnimationRunning()) {
    return;
  }

  HideFolderView(/*animate=*/folder_item_view, /*hide_for_reparent=*/false);

  if (folder_item_view && select_folder)
    folder_item_view->RequestFocus();
  else
    search_box_view_->search_box()->RequestFocus();
}

void AppListBubbleView::ReparentFolderItemTransit(
    AppListFolderItem* folder_item) {
  DVLOG(1) << __FUNCTION__;
  if (folder_view_->IsAnimationRunning()) {
    return;
  }

  HideFolderView(/*animate=*/true, /*hide_for_reparent=*/true);
}

void AppListBubbleView::ReparentDragEnded() {
  DVLOG(1) << __FUNCTION__;
  // Nothing to do.
}

void AppListBubbleView::InitializeUIForBubbleView() {
  assistant_page_->InitializeUIForBubbleView();
}

void AppListBubbleView::DisableFocusForShowingActiveFolder(bool disabled) {
  search_box_view_->SetEnabled(!disabled);
  SetViewIgnoredForAccessibility(search_box_view_, disabled);

  apps_page_->DisableFocusForShowingActiveFolder(disabled);
}

void AppListBubbleView::OnShowAnimationEnded(const gfx::Rect& layer_bounds) {
  // Restore the layer bounds. If the animation completed normally, this isn't
  // visible because the bounds won't change. If the animation was aborted, this
  // is needed to reset state before starting the hide animation.
  layer()->SetBounds(layer_bounds);

  if (current_page_ == AppListBubblePage::kAppsCollections) {
    apps_collections_page_->RecordAboveTheFoldMetrics();
  } else if (current_page_ == AppListBubblePage::kApps) {
    apps_page_->RecordAboveTheFoldMetrics();
  }
}

void AppListBubbleView::OnHideAnimationEnded(const gfx::Rect& layer_bounds) {
  // Restore the layer bounds. This isn't visible because opacity is 0.
  layer()->SetBounds(layer_bounds);

  // NOTE: This may cause a query update and switch to the apps page if the
  // if the search box was not empty.
  search_box_view_->ClearSearch();

  // Hide any open folder.
  HideFolderView(/*animate=*/false, /*hide_for_reparent=*/false);

  // Reset pages to default visibility.
  current_page_ = AppListBubblePage::kNone;
  apps_page_->SetVisible(true);
  search_page_->SetVisible(false);
  assistant_page_->SetVisible(false);

  is_hiding_ = false;
  if (on_hide_animation_ended_) {
    std::move(on_hide_animation_ended_).Run();
  }
}

void AppListBubbleView::HideFolderView(bool animate, bool hide_for_reparent) {
  showing_folder_ = false;
  DeprecatedLayoutImmediately();
  folder_background_view_->SetVisible(false);
  if (!hide_for_reparent) {
    apps_page_->scrollable_apps_grid_view()->ResetForShowApps();
    folder_view_->ResetItemsGridForClose();
  }
  if (animate) {
    folder_view_->ScheduleShowHideAnimation(/*show=*/false, hide_for_reparent);
  } else {
    folder_view_->HideViewImmediately();
  }
  DisableFocusForShowingActiveFolder(false);
}

void AppListBubbleView::OnAppListReorderAnimationDone() {
  // Re-enable the search box to handle a11y events.
  SetViewIgnoredForAccessibility(search_box_view_, false);
}

void AppListBubbleView::MaybeFocusAndActivateSearchBox() {
  // Don't focus the search box while the view is hiding. The app list may be
  // dismissed when focus moves to another view (e.g. the message center).
  // Attempting to focus the search box could make that other view close.
  // https://crbug.com/1313140
  if (is_hiding_) {
    return;
  }

  search_box_view_->SetSearchBoxActive(true,
                                       /*event_type=*/ui::EventType::kUnknown);
  // Explicitly request focus in case the search box was active before.
  search_box_view_->search_box()->RequestFocus();
}

BEGIN_METADATA(AppListBubbleView)
END_METADATA

}  // namespace ash