chromium/ash/app_list/views/contents_view.cc

// Copyright 2012 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/contents_view.h"

#include <algorithm>
#include <vector>

#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/views/app_list_folder_view.h"
#include "ash/app_list/views/app_list_main_view.h"
#include "ash/app_list/views/app_list_view.h"
#include "ash/app_list/views/apps_container_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/app_list/views/assistant/assistant_page_view.h"
#include "ash/app_list/views/paged_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/app_list/views/search_result_page_view.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_enums.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {
// The preferred search box height.
constexpr int kSearchBoxHeight = 48;

// The top search box margin (measured from the app list view top bound) when
// app list view is in peeking state on non-apps page.
constexpr int kDefaultSearchBoxTopMarginInPeekingState = 24;

// The top search box margin (measured from the app list view top bound) when
// app list view is in peeking state on the apps page.
constexpr int kSearchBoxTopMarginInPeekingAppsPage = 84;
constexpr int kSearchBarMinWidth = 440;

// Duration for page transition.
constexpr base::TimeDelta kPageTransitionDuration = base::Milliseconds(250);

// Duration for overscroll page transition.
constexpr base::TimeDelta kOverscrollPageTransitionDuration =
    base::Milliseconds(50);

}  // namespace

ContentsView::ContentsView(AppListView* app_list_view)
    : app_list_view_(app_list_view) {
  pagination_model_.SetTransitionDurations(kPageTransitionDuration,
                                           kOverscrollPageTransitionDuration);
  pagination_model_.AddObserver(this);
}

ContentsView::~ContentsView() {
  pagination_model_.RemoveObserver(this);
}

// static
int ContentsView::GetPeekingSearchBoxTopMarginOnPage(AppListState page) {
  return page == AppListState::kStateApps
             ? kSearchBoxTopMarginInPeekingAppsPage
             : kDefaultSearchBoxTopMarginInPeekingState;
}

void ContentsView::Init() {
  AppListViewDelegate* view_delegate = GetAppListMainView()->view_delegate();
  apps_container_view_ = AddLauncherPage(
      std::make_unique<AppsContainerView>(this), AppListState::kStateApps);

  // Search results UI.
  auto search_result_page_view = std::make_unique<SearchResultPageView>();
  search_result_page_view->InitializeContainers(view_delegate,
                                                GetSearchBoxView());

  search_result_page_view_ = AddLauncherPage(std::move(search_result_page_view),
                                             AppListState::kStateSearchResults);

  auto assistant_page_view = std::make_unique<AssistantPageView>(
      view_delegate->GetAssistantViewDelegate());
  assistant_page_view->SetVisible(false);
  assistant_page_view_ = AddLauncherPage(std::move(assistant_page_view),
                                         AppListState::kStateEmbeddedAssistant);

  int initial_page_index = GetPageIndexForState(AppListState::kStateApps);
  DCHECK_GE(initial_page_index, 0);

  page_before_search_ = initial_page_index;
  // Must only call SetTotalPages once all the launcher pages have been added
  // (as it will trigger a SelectedPageChanged call).
  pagination_model_.SetTotalPages(app_list_pages_.size());

  // Page 0 is selected by SetTotalPages and needs to be 'hidden' when selecting
  // the initial page.
  app_list_pages_[GetActivePageIndex()]->OnWillBeHidden();

  pagination_model_.SelectPage(initial_page_index, false);

  ActivePageChanged();

  // Hide the search results initially.
  ShowSearchResults(false);
}

void ContentsView::ResetForShow() {
  apps_container_view_->ResetForShowApps();
  // SearchBoxView::ResetForShow() before SetActiveState(). It clears the search
  // query internally, which can show the search results page through
  // QueryChanged(). Since it wants to reset to kStateApps, first reset the
  // search box and then set its active state to kStateApps.
  GetSearchBoxView()->ResetForShow();
  // Make sure the default visibilities of the pages. This should be done before
  // SetActiveState() since it checks the visibility of the pages.
  apps_container_view_->SetVisible(true);
  search_result_page_view_->SetVisible(false);
  if (assistant_page_view_)
    assistant_page_view_->SetVisible(false);
  SetActiveState(AppListState::kStateApps, /*animate=*/false);
}

void ContentsView::CancelDrag() {
  if (apps_container_view_->apps_grid_view()->has_dragged_item())
    apps_container_view_->apps_grid_view()->EndDrag(true);
  if (apps_container_view_->app_list_folder_view()
          ->items_grid_view()
          ->has_dragged_item()) {
    apps_container_view_->app_list_folder_view()->items_grid_view()->EndDrag(
        true);
  }
}

void ContentsView::SetDragAndDropHostOfCurrentAppList(
    ApplicationDragAndDropHost* drag_and_drop_host) {
  apps_container_view_->SetDragAndDropHostOfCurrentAppList(drag_and_drop_host);
}

void ContentsView::OnAppListViewTargetStateChanged(
    AppListViewState target_state) {
  if (target_state == AppListViewState::kClosed) {
    CancelDrag();
    return;
  }
}

void ContentsView::SetActiveState(AppListState state) {
  SetActiveState(state, true /*animate*/);
}

void ContentsView::SetActiveState(AppListState state, bool animate) {
  if (IsStateActive(state))
    return;

  // The primary way to set the state to search or Assistant results should be
  // via |ShowSearchResults| or |ShowEmbeddedAssistantUI|.
  DCHECK(state != AppListState::kStateSearchResults &&
         state != AppListState::kStateEmbeddedAssistant);

  const int page_index = GetPageIndexForState(state);
  page_before_search_ = page_index;
  SetActiveStateInternal(page_index, animate);
}

int ContentsView::GetActivePageIndex() const {
  // The active page is changed at the beginning of an animation, not the end.
  return pagination_model_.SelectedTargetPage();
}

AppListState ContentsView::GetActiveState() const {
  return GetStateForPageIndex(GetActivePageIndex());
}

bool ContentsView::IsStateActive(AppListState state) const {
  int active_page_index = GetActivePageIndex();
  return active_page_index >= 0 &&
         GetPageIndexForState(state) == active_page_index;
}

int ContentsView::GetPageIndexForState(AppListState state) const {
  // Find the index of the view corresponding to the given state.
  std::map<AppListState, int>::const_iterator it = state_to_view_.find(state);
  if (it == state_to_view_.end())
    return -1;

  return it->second;
}

AppListState ContentsView::GetStateForPageIndex(int index) const {
  std::map<int, AppListState>::const_iterator it = view_to_state_.find(index);
  if (it == view_to_state_.end())
    return AppListState::kInvalidState;

  return it->second;
}

int ContentsView::NumLauncherPages() const {
  return pagination_model_.total_pages();
}

gfx::Size ContentsView::AdjustSearchBoxSizeToFitMargins(
    const gfx::Size& preferred_size) const {
  const int padded_width =
      GetContentsBounds().width() - 2 * AppsContainerView::kHorizontalMargin;
  return gfx::Size(
      std::clamp(padded_width, kSearchBarMinWidth, preferred_size.width()),
      preferred_size.height());
}

void ContentsView::SetActiveStateInternal(int page_index, bool animate) {
  if (!GetPageView(page_index)->GetVisible())
    return;

  app_list_pages_[GetActivePageIndex()]->OnWillBeHidden();

  // Start animating to the new page. Disable animation for tests.
  bool should_animate = animate && !set_active_state_without_animation_ &&
                        !ui::ScopedAnimationDurationScaleMode::is_zero();

  // There's a chance of selecting page during the transition animation. To
  // reschedule the new animation from the beginning, |pagination_model_| needs
  // to finish the ongoing animation here.
  if (should_animate && pagination_model_.has_transition() &&
      pagination_model_.transition().target_page != page_index) {
    pagination_model_.FinishAnimation();
    // If the pending animation was animating from the current target page, the
    // target page might have got hidden as the animation was finished. Make
    // sure the page is reshown in that case.
    GetPageView(page_index)->SetVisible(true);
  }
  pagination_model_.SelectPage(page_index, should_animate);
  ActivePageChanged();

  if (!should_animate)
    DeprecatedLayoutImmediately();
}

void ContentsView::ActivePageChanged() {
  AppListState state = AppListState::kInvalidState;

  std::map<int, AppListState>::const_iterator it =
      view_to_state_.find(GetActivePageIndex());
  if (it != view_to_state_.end())
    state = it->second;

  app_list_pages_[GetActivePageIndex()]->OnWillBeShown();

  GetAppListMainView()->view_delegate()->OnAppListPageChanged(state);
  UpdateSearchBoxVisibility(state);
  app_list_view_->UpdateWindowTitle();
}

void ContentsView::ShowSearchResults(bool show) {
  int search_page = GetPageIndexForState(AppListState::kStateSearchResults);
  DCHECK_GE(search_page, 0);

  // SetVisible() only when showing search results, the search results page will
  // be hidden at the end of its own bounds animation.
  if (show) {
    search_result_page_view()->SetVisible(true);

    // Always to hide `assistant_page_view_` in case it is visible.
    assistant_page_view_->SetVisible(false);

    // `page_before_search_` could be invisible when showing
    // `assistant_page_view_`.
    GetPageView(page_before_search_)->SetVisible(true);
  }

  SetActiveStateInternal(show ? search_page : page_before_search_,
                         true /*animate*/);
  if (show)
    search_result_page_view()->UpdateResultContainersVisibility();
}

bool ContentsView::IsShowingSearchResults() const {
  return IsStateActive(AppListState::kStateSearchResults);
}

void ContentsView::ShowEmbeddedAssistantUI(bool show) {
  const int assistant_page =
      GetPageIndexForState(AppListState::kStateEmbeddedAssistant);
  DCHECK_GE(assistant_page, 0);

  const int current_page = pagination_model_.SelectedTargetPage();
  // When closing the Assistant UI we return to the last page before the
  // search box.
  const int next_page = show ? assistant_page : page_before_search_;

  // Show or hide results.
  if (current_page != next_page) {
    GetPageView(current_page)->SetVisible(false);
    GetPageView(next_page)->SetVisible(true);
  }

  SetActiveStateInternal(next_page, true /*animate*/);
  // Sometimes the page stays in |assistant_page|, but the preferred bounds
  // might change meanwhile.
  if (show && current_page == assistant_page) {
    GetPageView(assistant_page)
        ->UpdatePageBoundsForState(
            AppListState::kStateEmbeddedAssistant, GetContentsBounds(),
            GetSearchBoxBounds(AppListState::kStateEmbeddedAssistant));
  }
  // If |next_page| is kStateApps, we need to set app_list_view to
  // kPeeking and layout the suggestion chips.
  if (next_page == GetPageIndexForState(AppListState::kStateApps)) {
    GetSearchBoxView()->ClearSearch();
    GetSearchBoxView()->SetSearchBoxActive(false, ui::EventType::kUnknown);
    apps_container_view_->DeprecatedLayoutImmediately();
  }
}

bool ContentsView::IsShowingEmbeddedAssistantUI() const {
  return IsStateActive(AppListState::kStateEmbeddedAssistant);
}

void ContentsView::InitializeSearchBoxAnimation(AppListState current_state,
                                                AppListState target_state) {
  SearchBoxView* search_box = GetSearchBoxView();
  if (!search_box->GetWidget())
    return;

  search_box->UpdateLayout(target_state,
                           GetSearchBoxSize(target_state).height());

  gfx::Rect target_bounds = GetSearchBoxBounds(target_state);
  target_bounds = search_box->GetViewBoundsForSearchBoxContentsBounds(
      ConvertRectToWidgetWithoutTransform(target_bounds));

  // The search box animation is conducted as transform animation. Initially
  // search box changes its bounds to the target bounds but sets the transform
  // to be original bounds. Note that this transform shouldn't be animated
  // through ui::LayerAnimator since intermediate transformed bounds might not
  // match with other animation and that could look janky.
  search_box->SetBoundsRect(target_bounds);

  UpdateSearchBoxAnimation(0.0f, current_state, target_state);
}

void ContentsView::UpdateSearchBoxAnimation(double progress,
                                            AppListState current_state,
                                            AppListState target_state) {
  SearchBoxView* search_box = GetSearchBoxView();
  if (!search_box->GetWidget())
    return;

  gfx::Rect previous_bounds = GetSearchBoxBounds(current_state);
  previous_bounds = search_box->GetViewBoundsForSearchBoxContentsBounds(
      ConvertRectToWidgetWithoutTransform(previous_bounds));
  gfx::Rect target_bounds = GetSearchBoxBounds(target_state);
  target_bounds = search_box->GetViewBoundsForSearchBoxContentsBounds(
      ConvertRectToWidgetWithoutTransform(target_bounds));

  gfx::Rect current_bounds =
      gfx::Tween::RectValueBetween(progress, previous_bounds, target_bounds);
  gfx::Transform transform;

  if (current_bounds != target_bounds) {
    transform.Translate(current_bounds.origin() - target_bounds.origin());
    transform.Scale(
        static_cast<float>(current_bounds.width()) / target_bounds.width(),
        static_cast<float>(current_bounds.height()) / target_bounds.height());
  }
  search_box->layer()->SetTransform(transform);

  // Update search box view layer.
  const float current_radius =
      search_box->GetSearchBoxBorderCornerRadiusForState(current_state);
  const float target_radius =
      search_box->GetSearchBoxBorderCornerRadiusForState(target_state);
  search_box->layer()->SetClipRect(search_box->GetContentsBounds());
  search_box->layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(
      gfx::Tween::FloatValueBetween(progress, current_radius, target_radius)));
}

void ContentsView::UpdateSearchBoxVisibility(AppListState current_state) {
  // Hide search box widget in order to click on the embedded Assistant UI.
  const bool show_search_box =
      current_state != AppListState::kStateEmbeddedAssistant;
  GetSearchBoxView()->SetVisible(show_search_box);
}

AppListPage* ContentsView::GetPageView(int index) const {
  DCHECK_GT(static_cast<int>(app_list_pages_.size()), index);
  return app_list_pages_[index];
}

SearchBoxView* ContentsView::GetSearchBoxView() const {
  return GetAppListMainView()->search_box_view();
}

AppListMainView* ContentsView::GetAppListMainView() const {
  return app_list_view_->app_list_main_view();
}

void ContentsView::AddLauncherPageInternal(std::unique_ptr<AppListPage> view,
                                           AppListState state) {
  view->set_contents_view(this);
  app_list_pages_.push_back(AddChildView(std::move(view)));
  int page_index = app_list_pages_.size() - 1;
  bool success =
      state_to_view_.insert(std::make_pair(state, page_index)).second;
  success = success &&
            view_to_state_.insert(std::make_pair(page_index, state)).second;

  // There shouldn't be duplicates in either map.
  DCHECK(success);
}

gfx::Rect ContentsView::GetSearchBoxBounds(AppListState state) const {
  const gfx::Size size = GetSearchBoxSize(state);
  const int top =
      apps_container_view_
          ->CalculateMarginsForAvailableBounds(
              GetContentsBounds(), GetSearchBoxSize(AppListState::kStateApps))
          .top();
  return gfx::Rect(gfx::Point((width() - size.width()) / 2, top), size);
}

gfx::Size ContentsView::GetSearchBoxSize(AppListState state) const {
  AppListPage* page = GetPageView(GetPageIndexForState(state));
  gfx::Size size_preferred_by_page = page->GetPreferredSearchBoxSize();
  if (!size_preferred_by_page.IsEmpty())
    return AdjustSearchBoxSizeToFitMargins(size_preferred_by_page);

  gfx::Size preferred_size = GetSearchBoxView()->GetPreferredSize();

  preferred_size.set_height(kSearchBoxHeight);

  return AdjustSearchBoxSizeToFitMargins(preferred_size);
}

bool ContentsView::Back() {
  // If the virtual keyboard is visible, dismiss the keyboard and return early
  auto* const keyboard_controller = keyboard::KeyboardUIController::Get();
  if (keyboard_controller->IsKeyboardVisible()) {
    keyboard_controller->HideKeyboardByUser();
    return true;
  }

  AppListState state = view_to_state_[GetActivePageIndex()];
  switch (state) {
    case AppListState::kStateApps: {
      PaginationModel* pagination_model =
          apps_container_view_->apps_grid_view()->pagination_model();
      if (apps_container_view_->IsInFolderView()) {
        apps_container_view_->app_list_folder_view()->CloseFolderPage();
      } else if (pagination_model->total_pages() > 0 &&
                 pagination_model->selected_page() > 0) {
        bool animate = !ui::ScopedAnimationDurationScaleMode::is_zero();
        pagination_model->SelectPage(0, animate);
      } else {
        return false;
      }
      break;
    }
    case AppListState::kStateSearchResults:
      GetSearchBoxView()->ClearSearchAndDeactivateSearchBox();
      ShowSearchResults(false);
      break;
    case AppListState::kStateEmbeddedAssistant:
      GetAppListMainView()->view_delegate()->EndAssistant(
          assistant::AssistantExitPoint::kBackInLauncher);
      ShowEmbeddedAssistantUI(false);
      break;
    case AppListState::kStateStart_DEPRECATED:
    case AppListState::kInvalidState:
      NOTREACHED();
  }
  return true;
}

void ContentsView::Layout(PassKey) {
  const gfx::Rect rect = GetContentsBounds();
  if (rect.IsEmpty())
    return;

  if (pagination_model_.has_transition())
    return;

  UpdateYPositionAndOpacity();

  const AppListState current_state =
      GetStateForPageIndex(pagination_model_.selected_page());
  SearchBoxView* const search_box = GetSearchBoxView();
  const int search_box_height = GetSearchBoxSize(current_state).height();
  search_box->UpdateLayout(current_state, search_box_height);
  search_box->UpdateBackground(current_state);

  // Reset the transform which can be set through animation
  search_box->layer()->SetTransform(gfx::Transform());
}

void ContentsView::TotalPagesChanged(int previous_page_count,
                                     int new_page_count) {}

void ContentsView::SelectedPageChanged(int old_selected, int new_selected) {
  if (old_selected >= 0)
    app_list_pages_[old_selected]->OnHidden();

  if (new_selected >= 0)
    app_list_pages_[new_selected]->OnShown();
}

void ContentsView::TransitionStarted() {
  const int current_page = pagination_model_.selected_page();
  const int target_page = pagination_model_.transition().target_page;

  const AppListState current_state = GetStateForPageIndex(current_page);
  const AppListState target_state = GetStateForPageIndex(target_page);
  for (AppListPage* page : app_list_pages_)
    page->OnAnimationStarted(current_state, target_state);

  InitializeSearchBoxAnimation(current_state, target_state);
}

void ContentsView::TransitionChanged() {
  const int current_page = pagination_model_.selected_page();
  const int target_page = pagination_model_.transition().target_page;

  const AppListState current_state = GetStateForPageIndex(current_page);
  const AppListState target_state = GetStateForPageIndex(target_page);
  const double progress = pagination_model_.transition().progress;
  for (AppListPage* page : app_list_pages_) {
    if (!page->GetVisible() ||
        !ShouldLayoutPage(page, current_state, target_state)) {
      continue;
    }
    page->OnAnimationUpdated(progress, current_state, target_state);
  }

  // Update search box's transform gradually. See the comment in
  // InitiateSearchBoxAnimation for why it's not animated through
  // ui::LayerAnimator.
  UpdateSearchBoxAnimation(progress, current_state, target_state);
}

void ContentsView::UpdateYPositionAndOpacity() {
  const int current_page = pagination_model_.has_transition()
                               ? pagination_model_.transition().target_page
                               : pagination_model_.selected_page();
  const AppListState current_state = GetStateForPageIndex(current_page);

  // The search box bounds are determined by the apps container internal
  // margins, which depend on the apps container view size and app list config.
  // Make sure the apps container bounds are set before calculating search box
  // bounds, so `apps_container_view_` has up to date AppListConfig when
  // AppsContainerView::CalculateMarginsForAvailableBounds() gets called when
  // calculating search box y position.
  apps_container_view_->SetBoundsRect(GetContentsBounds());

  SearchBoxView* search_box = GetSearchBoxView();
  const gfx::Rect search_box_bounds = GetSearchBoxBounds(current_state);
  const gfx::Rect search_rect =
      search_box->GetViewBoundsForSearchBoxContentsBounds(
          ConvertRectToWidgetWithoutTransform(search_box_bounds));
  search_box->SetBoundsRect(search_rect);

  for (AppListPage* page : app_list_pages_) {
    page->UpdatePageBoundsForState(current_state, GetContentsBounds(),
                                   search_box_bounds);
    page->UpdatePageOpacityForState(current_state, 1.0f);
  }
}

std::unique_ptr<ui::ScopedLayerAnimationSettings>
ContentsView::CreateTransitionAnimationSettings(ui::Layer* layer) const {
  DCHECK(pagination_model_.has_transition());
  auto settings =
      std::make_unique<ui::ScopedLayerAnimationSettings>(layer->GetAnimator());
  settings->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  settings->SetTransitionDuration(
      pagination_model_.GetTransitionAnimationSlideDuration());
  return settings;
}

bool ContentsView::ShouldLayoutPage(AppListPage* page,
                                    AppListState current_state,
                                    AppListState target_state) const {
  if (page == apps_container_view_ || page == search_result_page_view_) {
    return ((current_state == AppListState::kStateSearchResults &&
             target_state == AppListState::kStateApps) ||
            (current_state == AppListState::kStateApps &&
             target_state == AppListState::kStateSearchResults));
  }

  if (page == assistant_page_view_) {
    return current_state == AppListState::kStateEmbeddedAssistant ||
           target_state == AppListState::kStateEmbeddedAssistant;
  }

  return false;
}

gfx::Rect ContentsView::ConvertRectToWidgetWithoutTransform(
    const gfx::Rect& rect) {
  gfx::Rect widget_rect = rect;
  for (const views::View* v = this; v; v = v->parent()) {
    widget_rect.Offset(v->GetMirroredPosition().OffsetFromOrigin());
  }
  return widget_rect;
}

BEGIN_METADATA(ContentsView)
END_METADATA

}  // namespace ash