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

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

#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/views/app_list_keyboard_controller.h"
#include "ash/app_list/views/app_list_nudge_controller.h"
#include "ash/app_list/views/app_list_toast_container_view.h"
#include "ash/app_list/views/app_list_toast_view.h"
#include "ash/app_list/views/app_list_view_util.h"
#include "ash/app_list/views/continue_section_view.h"
#include "ash/app_list/views/recent_apps_view.h"
#include "ash/app_list/views/scrollable_apps_grid_view.h"
#include "ash/app_list/views/search_box_view.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/controls/rounded_scroll_bar.h"
#include "ash/controls/scroll_view_gradient_helper.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/typography.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.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_animation_duration_scale_mode.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"

using views::BoxLayout;

namespace ash {

namespace {

constexpr int kContinueColumnCount = 2;

// Insets for the vertical scroll bar. The bottom is pushed up slightly to keep
// the scroll bar from being clipped by the rounded corners.
constexpr auto kVerticalScrollInsets = gfx::Insets::TLBR(1, 0, 16, 1);

// The padding between different sections within the apps page. Also used for
// interior apps page container margin.
constexpr int kVerticalPaddingBetweenSections = 16;

// Label container padding in DIPs.
constexpr auto kContinueLabelContainerPadding = gfx::Insets::TLBR(0, 16, 0, 16);

// The horizontal interior margin for the apps page container - i.e. the margin
// between the apps page bounds and the page content.
constexpr int kHorizontalInteriorMargin = 16;

// The size of the scroll view gradient.
constexpr int kScrollViewGradientSize = 16;

// Insets for the continue section. These insets are required to make the
// suggestion icons visually align with the icons in the apps grid.
constexpr auto kContinueSectionInsets = gfx::Insets::VH(0, 4);

// Insets for the separator between the continue section and apps.
constexpr auto kSeparatorInsets = gfx::Insets::VH(0, 16);

// Delay for the show page transform and opacity animations.
constexpr base::TimeDelta kShowPageAnimationDelay = base::Milliseconds(50);

// The spec says "Down 40 -> 0, duration 250ms" with no delay, but the opacity
// animation has a 50ms delay that causes the first 50ms to be invisible. Just
// animate the 200ms visible part, which is 32 dips. This ensures the search
// page hide animation doesn't play at the same time as the apps page show
// animation.
constexpr int kShowPageAnimationVerticalOffset = 32;
constexpr base::TimeDelta kShowPageAnimationTransformDuration =
    base::Milliseconds(200);

// Duration of the show page opacity animation.
constexpr base::TimeDelta kShowPageAnimationOpacityDuration =
    base::Milliseconds(100);

// A view that runs a click callback when clicked or tapped.
class ClickableView : public views::View {
  METADATA_HEADER(ClickableView, views::View)

 public:
  explicit ClickableView(base::RepeatingClosure click_callback)
      : click_callback_(click_callback) {}
  ~ClickableView() override = default;

  // views::View:
  bool OnMousePressed(const ui::MouseEvent& event) override {
    views::View::OnMousePressed(event);
    // Return true so this object will receive a mouse released event.
    return true;
  }

  void OnMouseReleased(const ui::MouseEvent& event) override {
    views::View::OnMouseReleased(event);
    click_callback_.Run();
  }

  void OnGestureEvent(ui::GestureEvent* event) override {
    views::View::OnGestureEvent(event);
    if (event->type() == ui::EventType::kGestureTap) {
      event->SetHandled();
      click_callback_.Run();
    }
  }

 private:
  base::RepeatingClosure click_callback_;
};

BEGIN_METADATA(ClickableView)
END_METADATA

}  // namespace

AppListBubbleAppsPage::AppListBubbleAppsPage(
    AppListViewDelegate* view_delegate,
    ApplicationDragAndDropHost* drag_and_drop_host,
    AppListConfig* app_list_config,
    AppListA11yAnnouncer* a11y_announcer,
    AppListFolderController* folder_controller,
    SearchBoxView* search_box)
    : view_delegate_(view_delegate),
      search_box_(search_box),
      app_list_keyboard_controller_(
          std::make_unique<AppListKeyboardController>(this)),
      app_list_nudge_controller_(std::make_unique<AppListNudgeController>()) {
  DCHECK(view_delegate);
  DCHECK(drag_and_drop_host);
  DCHECK(a11y_announcer);
  DCHECK(folder_controller);

  AppListModelProvider::Get()->AddObserver(this);

  SetUseDefaultFillLayout(true);

  // The entire page scrolls.
  scroll_view_ = AddChildView(std::make_unique<views::ScrollView>(
      views::ScrollView::ScrollWithLayers::kEnabled));
  scroll_view_->ClipHeightTo(0, std::numeric_limits<int>::max());
  scroll_view_->SetDrawOverflowIndicator(false);
  // Don't paint a background. The bubble already has one.
  scroll_view_->SetBackgroundColor(std::nullopt);
  // Arrow keys are used to select app icons.
  scroll_view_->SetAllowKeyboardScrolling(false);

  // Scroll view will have a gradient mask layer, and is animated during
  // hide/show.
  scroll_view_->SetPaintToLayer();
  scroll_view_->layer()->SetFillsBoundsOpaquely(false);

  // Set up scroll bars.
  scroll_view_->SetHorizontalScrollBarMode(
      views::ScrollView::ScrollBarMode::kDisabled);
  auto vertical_scroll = std::make_unique<RoundedScrollBar>(
      views::ScrollBar::Orientation::kVertical);
  vertical_scroll->SetInsets(kVerticalScrollInsets);
  vertical_scroll->SetSnapBackOnDragOutside(false);
  scroll_bar_ = vertical_scroll.get();
  scroll_view_->SetVerticalScrollBar(std::move(vertical_scroll));

  auto scroll_contents = std::make_unique<views::View>();
  auto* layout = scroll_contents->SetLayoutManager(std::make_unique<BoxLayout>(
      BoxLayout::Orientation::kVertical,
      gfx::Insets::VH(kVerticalPaddingBetweenSections,
                      kHorizontalInteriorMargin),
      kVerticalPaddingBetweenSections));
  layout->set_cross_axis_alignment(BoxLayout::CrossAxisAlignment::kStretch);

  // The "Continue where you left off" label is in a container that is a child
  // of this view.
  InitContinueLabelContainer(scroll_contents.get());

  // Continue section row.
  continue_section_ = scroll_contents->AddChildView(
      std::make_unique<ContinueSectionView>(view_delegate, kContinueColumnCount,
                                            /*tablet_mode=*/false));
  continue_section_->SetBorder(
      views::CreateEmptyBorder(kContinueSectionInsets));
  continue_section_->SetNudgeController(app_list_nudge_controller_.get());
  // Decrease the between-sections spacing so the continue label is closer to
  // the continue tasks section.
  continue_section_->SetProperty(views::kMarginsKey,
                                 gfx::Insets::TLBR(-14, 0, 0, 0));

  // Observe changes in continue section visibility, to keep separator
  // visibility in sync.
  continue_section_->AddObserver(this);

  // Recent apps row.
  recent_apps_ = scroll_contents->AddChildView(std::make_unique<RecentAppsView>(
      app_list_keyboard_controller_.get(), view_delegate));
  recent_apps_->UpdateAppListConfig(app_list_config);
  // Observe changes in continue section visibility, to keep separator
  // visibility in sync.
  recent_apps_->AddObserver(this);

  // Horizontal separator.
  separator_ =
      scroll_contents->AddChildView(std::make_unique<views::Separator>());
  separator_->SetBorder(views::CreateEmptyBorder(kSeparatorInsets));
  separator_->SetColorId(cros_tokens::kCrosSysSeparator);

  // Add a empty container view. A toast view should be added to
  // `toast_container_` when the app list starts temporary sorting.
  toast_container_ =
      scroll_contents->AddChildView(std::make_unique<AppListToastContainerView>(
          app_list_nudge_controller_.get(), app_list_keyboard_controller_.get(),
          a11y_announcer, view_delegate,
          /*delegate=*/this,
          /*tablet_mode=*/false));

  // All apps section.
  scrollable_apps_grid_view_ =
      scroll_contents->AddChildView(std::make_unique<ScrollableAppsGridView>(
          a11y_announcer, view_delegate,
          /*folder_delegate=*/nullptr, scroll_view_, folder_controller,
          app_list_keyboard_controller_.get()));
  scrollable_apps_grid_view_->SetDragAndDropHostOfCurrentAppList(
      drag_and_drop_host);
  scrollable_apps_grid_view_->UpdateAppListConfig(app_list_config);
  scrollable_apps_grid_view_->SetMaxColumns(5);
  AppListModel* const model = AppListModelProvider::Get()->model();
  scrollable_apps_grid_view_->SetModel(model);
  scrollable_apps_grid_view_->SetItemList(model->top_level_item_list());
  scrollable_apps_grid_view_->ResetForShowApps();
  // Ensure the grid fills the remaining space in the bubble so that icons can
  // be dropped beneath the last row.
  layout->SetFlexForView(scrollable_apps_grid_view_, 1);

  scroll_view_->SetContents(std::move(scroll_contents));

  UpdateSuggestions();
  UpdateContinueSectionVisibility();

  on_contents_scrolled_subscription_ =
      scroll_view_->AddContentsScrolledCallback(base::BindRepeating(
          &AppListBubbleAppsPage::OnPageScrolled, base::Unretained(this)));
}

AppListBubbleAppsPage::~AppListBubbleAppsPage() {
  AppListModelProvider::Get()->RemoveObserver(this);
  continue_section_->RemoveObserver(this);
  recent_apps_->RemoveObserver(this);
}

void AppListBubbleAppsPage::UpdateSuggestions() {
  recent_apps_->SetModels(AppListModelProvider::Get()->search_model(),
                          AppListModelProvider::Get()->model());
  continue_section_->UpdateSuggestionTasks();
  UpdateSeparatorVisibility();
}

void AppListBubbleAppsPage::AnimateShowLauncher(bool is_side_shelf) {
  DCHECK(GetVisible());

  // Don't show the scroll bar due to thumb bounds changes. There's enough
  // visual movement going on during the animation.
  scroll_bar_->SetShowOnThumbBoundsChanged(false);

  // The animation relies on the correct positions of views, so force layout.
  if (needs_layout())
    DeprecatedLayoutImmediately();
  DCHECK(!needs_layout());

  // This part of the animation has a longer duration than the bubble part
  // handled in AppListBubbleView, so track overall smoothness here.
  ui::AnimationThroughputReporter reporter(
      scrollable_apps_grid_view_->layer()->GetAnimator(),
      metrics_util::ForSmoothnessV3(base::BindRepeating([](int value) {
        // This histogram name is used in Tast tests. Do not rename.
        base::UmaHistogramPercentage(
            "Apps.ClamshellLauncher.AnimationSmoothness.OpenAppsPage", value);
      })));

  // Side-shelf uses faster animations.
  const base::TimeDelta slide_duration =
      is_side_shelf ? base::Milliseconds(150) : base::Milliseconds(250);
  const gfx::Tween::Type tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN;

  // Animate the views. Each section is initially offset down, then slides up
  // into its final position. For side shelf, each section is initially offset
  // up, then it slides down. If a section isn't visible, skip it. The further
  // down the section, the greater its initial offset. This code uses multiple
  // animations because views::AnimationBuilder doesn't have a good way to
  // build a single animation with conditional parts. https://crbug.com/1266020
  const int section_offset = is_side_shelf ? -20 : 20;
  int vertical_offset = 0;
  const bool animate_continue_label_container =
      continue_label_container_ && continue_label_container_->GetVisible();
  if (animate_continue_label_container) {
    vertical_offset += section_offset;
    SlideViewIntoPosition(continue_label_container_, vertical_offset,
                          slide_duration, tween_type);
  }
  if (continue_section_->GetVisible() &&
      continue_section_->GetTasksSuggestionsCount() > 0) {
    // Only offset if this is the top section, otherwise animate next to the
    // continue label container above.
    if (!animate_continue_label_container)
      vertical_offset += section_offset;
    SlideViewIntoPosition(continue_section_, vertical_offset, slide_duration,
                          tween_type);
  }
  if (recent_apps_->GetVisible() && recent_apps_->GetItemViewCount() > 0) {
    vertical_offset += section_offset;
    SlideViewIntoPosition(recent_apps_, vertical_offset, slide_duration,
                          tween_type);
  }
  if (separator_->GetVisible()) {
    // The separator is not offset; it animates next to the view above it.
    SlideViewIntoPosition(separator_, vertical_offset, slide_duration,
                          tween_type);
  }
  if (toast_container_ && toast_container_->IsToastVisible()) {
    vertical_offset += section_offset;
    SlideViewIntoPosition(toast_container_, vertical_offset, slide_duration,
                          tween_type);
  }

  // The apps grid is always visible.
  vertical_offset += section_offset;
  // Use a special cleanup callback to show the gradient mask at the end of the
  // animation. No need to use SlideViewIntoPosition() because this view always
  // has a layer.

  // Set up fade in/fade out gradients at top/bottom of scroll view.
  gradient_helper_ = std::make_unique<ScrollViewGradientHelper>(
      scroll_view_, kScrollViewGradientSize);
  gradient_helper_->UpdateGradientMask();

  StartSlideInAnimation(
      scrollable_apps_grid_view_, vertical_offset, slide_duration, tween_type,
      base::BindRepeating(&AppListBubbleAppsPage::OnAppsGridViewAnimationEnded,
                          weak_factory_.GetWeakPtr()));
}

void AppListBubbleAppsPage::PrepareForHideLauncher() {
  // Remove the gradient mask from the scroll view to improve performance.
  gradient_helper_.reset();
  scrollable_apps_grid_view_->EndDrag(/*cancel=*/true);
}

void AppListBubbleAppsPage::AnimateShowPage() {
  // If skipping animations, just update visibility.
  if (ui::ScopedAnimationDurationScaleMode::is_zero()) {
    SetVisible(true);
    return;
  }

  // Ensure any in-progress animations have their cleanup callbacks called.
  // Note that this might call SetVisible(false) from the hide animation.
  AbortAllAnimations();

  // Ensure the view is visible.
  SetVisible(true);

  ui::Layer* scroll_view_layer = scroll_view_->layer();
  DCHECK(scroll_view_layer);
  DCHECK_EQ(scroll_view_layer->type(), ui::LAYER_TEXTURED);

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

  gfx::Transform translate_down;
  translate_down.Translate(0, kShowPageAnimationVerticalOffset);

  // Update view visibility when the animation is done. Needed to ensure
  // the view has the correct opacity and transform when the animation is
  // aborted.
  auto set_visible_true = base::BindRepeating(
      [](base::WeakPtr<AppListBubbleAppsPage> self) {
        if (!self)
          return;
        self->SetVisible(true);
        ui::Layer* layer = self->scroll_view()->layer();
        layer->SetOpacity(1.f);
        layer->SetTransform(gfx::Transform());
      },
      weak_factory_.GetWeakPtr());

  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .OnEnded(set_visible_true)
      .OnAborted(set_visible_true)
      .Once()
      .SetOpacity(scroll_view_layer, 0.f)
      .SetTransform(scroll_view_layer, translate_down)
      .At(kShowPageAnimationDelay)
      .SetDuration(kShowPageAnimationTransformDuration)
      .SetTransform(scroll_view_layer, gfx::Transform(),
                    gfx::Tween::LINEAR_OUT_SLOW_IN)
      .At(kShowPageAnimationDelay)
      .SetDuration(kShowPageAnimationOpacityDuration)
      .SetOpacity(scroll_view_layer, 1.f);
}

void AppListBubbleAppsPage::AnimateHidePage() {
  // If skipping animations, just update visibility.
  if (ui::ScopedAnimationDurationScaleMode::is_zero()) {
    SetVisible(false);
    return;
  }

  scrollable_apps_grid_view_->CancelDragWithNoDropAnimation();

  // Update view visibility when the animation is done.
  auto set_visible_false = base::BindRepeating(
      [](base::WeakPtr<AppListBubbleAppsPage> self) {
        if (!self)
          return;
        self->SetVisible(false);
        ui::Layer* layer = self->scroll_view()->layer();
        layer->SetOpacity(1.f);
        layer->SetTransform(gfx::Transform());
      },
      weak_factory_.GetWeakPtr());

  ui::Layer* scroll_view_layer = scroll_view_->layer();
  DCHECK(scroll_view_layer);
  DCHECK_EQ(scroll_view_layer->type(), ui::LAYER_TEXTURED);

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

  // The animation spec says 40 dips down over 250ms, but the opacity animation
  // renders the view invisible after 50ms, so animate the visible fraction.
  gfx::Transform translate_down;
  constexpr int kVerticalOffset = 40 * 50 / 250;
  translate_down.Translate(0, kVerticalOffset);

  // Opacity: 100% -> 0%, duration 50ms
  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .OnEnded(set_visible_false)
      .OnAborted(set_visible_false)
      .Once()
      .SetDuration(base::Milliseconds(50))
      .SetOpacity(scroll_view_layer, 0.f)
      .SetTransform(scroll_view_layer, translate_down);
}

void AppListBubbleAppsPage::ResetScrollPosition() {
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(), 0);
}

void AppListBubbleAppsPage::AbortAllAnimations() {
  auto abort_animations = [](views::View* view) {
    if (view->layer())
      view->layer()->GetAnimator()->AbortAllAnimations();
  };
  abort_animations(scroll_view_);
  abort_animations(continue_section_);
  abort_animations(recent_apps_);
  abort_animations(separator_);
  if (toast_container_)
    abort_animations(toast_container_);
  abort_animations(scrollable_apps_grid_view_);
}

void AppListBubbleAppsPage::DisableFocusForShowingActiveFolder(bool disabled) {
  toggle_continue_section_button_->SetEnabled(!disabled);
  // Prevent container items from being accessed by ChromeVox.
  SetViewIgnoredForAccessibility(continue_label_container_, disabled);

  continue_section_->DisableFocusForShowingActiveFolder(disabled);
  recent_apps_->DisableFocusForShowingActiveFolder(disabled);
  if (toast_container_)
    toast_container_->DisableFocusForShowingActiveFolder(disabled);
  scrollable_apps_grid_view_->DisableFocusForShowingActiveFolder(disabled);
}

void AppListBubbleAppsPage::UpdateForNewSortingOrder(
    const std::optional<AppListSortOrder>& new_order,
    bool animate,
    base::OnceClosure update_position_closure,
    base::OnceClosure animation_done_closure) {
  DCHECK_EQ(animate, !update_position_closure.is_null());
  DCHECK(!animation_done_closure || animate);

  // A11y announcements must happen before animations, otherwise the undo
  // guidance is spoken first because focus moves immediately to the undo button
  // on the toast. Note that when `new_order` is null, `animate` was set to true
  // only if the sort was reverted.
  if (new_order) {
    if (*new_order != AppListSortOrder::kAlphabeticalEphemeralAppFirst)
      toast_container_->AnnounceSortOrder(*new_order);
  } else if (animate) {
    toast_container_->AnnounceUndoSort();
  }

  if (!animate) {
    // Reordering is not required so update the undo toast and return early.
    app_list_nudge_controller_->OnTemporarySortOrderChanged(new_order);
    toast_container_->OnTemporarySortOrderChanged(new_order);
    HandleFocusAfterSort();
    return;
  }

  // Abort the old reorder animation if any before closure update to avoid data
  // races on closures.
  scrollable_apps_grid_view_->MaybeAbortWholeGridAnimation();
  DCHECK(!update_position_closure_);
  update_position_closure_ = std::move(update_position_closure);
  DCHECK(!reorder_animation_done_closure_);
  reorder_animation_done_closure_ = std::move(animation_done_closure);

  views::AnimationBuilder animation_builder =
      scrollable_apps_grid_view_->FadeOutVisibleItemsForReorder(
          base::BindRepeating(
              &AppListBubbleAppsPage::OnAppsGridViewFadeOutAnimationEnded,
              weak_factory_.GetWeakPtr(), new_order));

  // Configure the toast fade out animation if the toast is going to be hidden.
  const bool current_toast_visible = toast_container_->IsToastVisible();
  const bool target_toast_visible =
      toast_container_->GetVisibilityForSortOrder(new_order);
  if (current_toast_visible && !target_toast_visible) {
    // Because `toast_container_` does not have a layer before the fade in
    // animation, create one.
    DCHECK(!toast_container_->layer());
    toast_container_->SetPaintToLayer();
    toast_container_->layer()->SetFillsBoundsOpaquely(false);

    animation_builder.GetCurrentSequence().SetOpacity(toast_container_->layer(),
                                                      0.f);
  }
}

bool AppListBubbleAppsPage::MaybeScrollToShowToast() {
  gfx::Point toast_origin;
  views::View::ConvertPointToTarget(toast_container_, scroll_view_->contents(),
                                    &toast_origin);
  const gfx::Rect toast_bounds_in_scroll_view =
      gfx::Rect(toast_origin, toast_container_->size());

  // Do not scroll if the toast is already fully shown.
  if (scroll_view_->GetVisibleRect().Contains(toast_bounds_in_scroll_view))
    return false;

  const int scroll_offset = separator_->GetVisible() ? separator_->y() : 0;
  scroll_view_->ScrollToPosition(scroll_view_->vertical_scroll_bar(),
                                 scroll_offset);

  return true;
}

void AppListBubbleAppsPage::Layout(PassKey) {
  LayoutSuperclass<views::View>(this);
  if (gradient_helper_)
    gradient_helper_->UpdateGradientMask();
}

void AppListBubbleAppsPage::VisibilityChanged(views::View* starting_from,
                                              bool is_visible) {
  // Cancel any in progress drag without running drop animation if the bubble
  // apps page is hiding.
  if (!is_visible) {
    scrollable_apps_grid_view_->CancelDragWithNoDropAnimation();
  }

  // Updates the visibility state in toast container.
  AppListToastContainerView::VisibilityState state =
      is_visible ? AppListToastContainerView::VisibilityState::kShown
                 : AppListToastContainerView::VisibilityState::kHidden;
  toast_container_->UpdateVisibilityState(state);

  // Check if the reorder nudge view needs update if the bubble apps page is
  // showing.
  if (is_visible) {
    toast_container_->MaybeUpdateReorderNudgeView();
  }
}

void AppListBubbleAppsPage::OnBoundsChanged(const gfx::Rect& old_bounds) {
  // Toast container, and continue section may contain toasts with multiline
  // labels, whose preferred height will depend on the apps page bounds (in
  // particular, the amount of horizontal space available to lay out labels).
  // Propagate the amount of available width for toasts before layout starts, so
  // the toast views can correctly calculate their preferred size during the
  // ensuing layout pass (otherwise, the preferred toast size may change as
  // result of the layout).
  toast_container_->ConfigureLayoutForAvailableWidth(
      bounds().width() - 2 * kHorizontalInteriorMargin);
  continue_section_->ConfigureLayoutForAvailableWidth(
      bounds().width() - 2 * kHorizontalInteriorMargin -
      kContinueSectionInsets.width());
}

void AppListBubbleAppsPage::OnActiveAppListModelsChanged(
    AppListModel* model,
    SearchModel* search_model) {
  scrollable_apps_grid_view_->SetModel(model);
  scrollable_apps_grid_view_->SetItemList(model->top_level_item_list());

  recent_apps_->SetModels(search_model, model);
}

void AppListBubbleAppsPage::OnViewVisibilityChanged(
    views::View* observed_view,
    views::View* starting_view) {
  if (starting_view == continue_section_ || starting_view == recent_apps_)
    UpdateSeparatorVisibility();
}

void AppListBubbleAppsPage::OnNudgeRemoved() {
  const gfx::Rect current_grid_bounds = scrollable_apps_grid_view_->bounds();

  if (needs_layout())
    DeprecatedLayoutImmediately();

  const gfx::Rect target_grid_bounds = scrollable_apps_grid_view_->bounds();
  const int offset = current_grid_bounds.y() - target_grid_bounds.y();

  // With the nudge gone, animate the apps grid up to its new target location.
  StartSlideInAnimation(scrollable_apps_grid_view_, offset,
                        base::Milliseconds(300),
                        gfx::Tween::ACCEL_40_DECEL_100_3, base::DoNothing());
}

ContinueSectionView* AppListBubbleAppsPage::GetContinueSectionView() {
  return continue_section_;
}

RecentAppsView* AppListBubbleAppsPage::GetRecentAppsView() {
  return recent_apps_;
}

AppListToastContainerView* AppListBubbleAppsPage::GetToastContainerView() {
  return toast_container_;
}

AppsGridView* AppListBubbleAppsPage::GetAppsGridView() {
  return scrollable_apps_grid_view_;
}

ui::Layer* AppListBubbleAppsPage::GetPageAnimationLayerForTest() {
  // Animating the `scroll_view_`'s content layer can have its transform
  // animations interrupted when the content layer's transforms get set due to
  // rtl specific transforms in ScrollView code. Use the `scroll_view_` layer
  // for animations to avoid this.
  return scroll_view_->layer();
}

////////////////////////////////////////////////////////////////////////////////
// private:

void AppListBubbleAppsPage::InitContinueLabelContainer(
    views::View* scroll_contents) {
  // The entire container view is clickable/tappable. The view is not focusable,
  // but the toggle button is focusable, and that satisfies the user's need for
  // an element with keyboard or accessibility focus.
  continue_label_container_ =
      scroll_contents->AddChildView(std::make_unique<ClickableView>(
          base::BindRepeating(&AppListBubbleAppsPage::OnToggleContinueSection,
                              base::Unretained(this))));
  continue_label_container_->SetBorder(
      views::CreateEmptyBorder(kContinueLabelContainerPadding));

  auto* layout = continue_label_container_->SetLayoutManager(
      std::make_unique<BoxLayout>(BoxLayout::Orientation::kHorizontal));
  layout->set_cross_axis_alignment(BoxLayout::CrossAxisAlignment::kCenter);

  continue_label_ =
      continue_label_container_->AddChildView(std::make_unique<views::Label>(
          l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_CONTINUE_SECTION_LABEL)));
  bubble_utils::ApplyStyle(continue_label_, TypographyToken::kCrosAnnotation1,
                           cros_tokens::kCrosSysOnSurfaceVariant);
  continue_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);

  // Button should be right aligned, so flex label to fill empty space.
  layout->SetFlexForView(continue_label_, 1);

  // The toggle button is clickable/tappable in addition to the container view.
  // This ensures ink drop ripple effects play when the button is clicked.
  toggle_continue_section_button_ =
      continue_label_container_->AddChildView(std::make_unique<IconButton>(
          base::BindRepeating(&AppListBubbleAppsPage::OnToggleContinueSection,
                              base::Unretained(this)),
          IconButton::Type::kSmallFloating, &kChevronUpIcon,
          /*is_togglable=*/false,
          /*has_border=*/false));
  // See ButtonFocusSkipper in app_list_bubble_view.cc for focus handling.
}

void AppListBubbleAppsPage::UpdateContinueSectionVisibility() {
  // The continue section view and recent apps view manage their own visibility
  // internally.
  continue_section_->UpdateElementsVisibility();
  recent_apps_->UpdateVisibility();
  UpdateContinueLabelContainer();
  UpdateSeparatorVisibility();
}

void AppListBubbleAppsPage::OnPageScrolled() {
  // Do not log anything if the contents are not scrollable.
  if (scroll_view_->GetVisibleRect().height() >=
      scroll_view_->contents()->height()) {
    return;
  }

  if (scroll_view_->GetVisibleRect().bottom() ==
      scroll_view_->contents()->bounds().bottom()) {
    RecordLauncherWorkflowMetrics(
        AppListUserAction::kNavigatedToBottomOfAppList,
        /*is_tablet_mode = */ false, std::nullopt);
  }
}

void AppListBubbleAppsPage::UpdateContinueLabelContainer() {
  if (!continue_label_container_)
    return;

  // If there are no suggested tasks and no recent apps, it doesn't make sense
  // to show the continue label. This can happen for brand-new users.
  continue_label_container_->SetVisible(
      continue_section_->GetTasksSuggestionsCount() > 0 ||
      recent_apps_->GetItemViewCount() > 0);

  // Update the toggle continue section button tooltip and image.
  bool is_hidden = view_delegate_->ShouldHideContinueSection();
  toggle_continue_section_button_->SetTooltipText(l10n_util::GetStringUTF16(
      is_hidden ? IDS_ASH_LAUNCHER_SHOW_CONTINUE_SECTION_TOOLTIP
                : IDS_ASH_LAUNCHER_HIDE_CONTINUE_SECTION_TOOLTIP));
  toggle_continue_section_button_->SetVectorIcon(is_hidden ? kChevronDownIcon
                                                           : kChevronUpIcon);
}

void AppListBubbleAppsPage::UpdateSeparatorVisibility() {
  // The separator only hides if the user has the continue section shown but
  // there are no suggested tasks and no apps. This is rare.
  separator_->SetVisible(view_delegate_->ShouldHideContinueSection() ||
                         recent_apps_->GetVisible() ||
                         continue_section_->GetVisible());
}

void AppListBubbleAppsPage::DestroyLayerForView(views::View* view) {
  // This function is not static so it can be bound with a weak pointer.
  view->DestroyLayer();
}

void AppListBubbleAppsPage::OnAppsGridViewAnimationEnded() {
  // Show the scroll bar for keyboard-driven scroll position changes.
  scroll_bar_->SetShowOnThumbBoundsChanged(true);
}

void AppListBubbleAppsPage::HandleFocusAfterSort() {
  // As the sort update on AppListBubbleAppsPage can be called in both clamshell
  // mode and tablet mode, return early if it's currently in tablet mode because
  // the AppListBubbleAppsPage isn't visible.
  if (view_delegate_->IsInTabletMode())
    return;

  // Focusing toast button may show the tooltip anchored on the button - make
  // sure the toast button bounds are correctly set before tooltip is shown.
  if (GetWidget()) {
    GetWidget()->LayoutRootViewIfNecessary();
  }

  // If the sort is done and the toast is visible and not fading out, request
  // the focus on the undo button on the toast. Otherwise request the focus on
  // the search box.
  if (toast_container_->IsToastVisible()) {
    toast_container_->toast_view()->toast_button()->RequestFocus();
  } else {
    search_box_->search_box()->RequestFocus();
  }
}

void AppListBubbleAppsPage::OnAppsGridViewFadeOutAnimationEnded(
    const std::optional<AppListSortOrder>& new_order,
    bool aborted) {
  // Update item positions after the fade out animation but before the fade in
  // animation. NOTE: `update_position_closure_` can be empty in some edge
  // cases. For example, the app list is set with a new order denoted by Order
  // A. Then before the fade out animation is completed, the app list order is
  // reset with the old value. In this case, `update_position_closure_` for
  // Order A is never called. As a result, the closure for resetting the order
  // is empty.
  // Also update item positions only when the fade out animation ends normally.
  // Because a fade out animation is aborted when:
  // (1) Another reorder animation starts, or
  // (2) The apps grid's view model updates due to the reasons such as app
  // installation or model reset.
  // It is meaningless to update item positions in either case.
  if (update_position_closure_ && !aborted)
    std::move(update_position_closure_).Run();

  // Record the undo toast's visibility before update.
  const bool old_toast_visible = toast_container_->IsToastVisible();

  toast_container_->OnTemporarySortOrderChanged(new_order);
  HandleFocusAfterSort();
  const bool target_toast_visible = toast_container_->IsToastVisible();

  // If there is a layer created for fading out `toast_container_`, destroy
  // the layer when the fade out animation ends. NOTE: when the reorder toast
  // is faded out, it should not be faded in along with the apps grid fade in
  // animation. Therefore destroy the layer when the fade out animation ends.
  if (toast_container_->layer()) {
    DCHECK(!target_toast_visible);
    toast_container_->DestroyLayer();
  }

  // Skip the fade in animation if the fade out animation is aborted.
  if (aborted) {
    OnReorderAnimationEnded();
    return;
  }

  const bool toast_visibility_change =
      (old_toast_visible != target_toast_visible);

  // When the undo toast's visibility changes, the apps grid's bounds should
  // change. Meanwhile, the fade in animation relies on the apps grid's bounds
  // to calculate visible items. Therefore trigger layout before starting the
  // fade in animation.
  if (toast_visibility_change)
    DeprecatedLayoutImmediately();

  // Ensure to scroll before triggering apps grid fade in animation so that
  // the bubble apps page's layout is ready.
  const bool scroll_performed = MaybeScrollToShowToast();

  views::AnimationBuilder animation_builder =
      scrollable_apps_grid_view_->FadeInVisibleItemsForReorder(
          base::BindRepeating(
              &AppListBubbleAppsPage::OnAppsGridViewFadeInAnimationEnded,
              weak_factory_.GetWeakPtr()));

  // Fade in the undo toast when:
  // (1) The toast's visibility becomes true from false, or
  // (2) The apps page is scrolled to show the toast.
  const bool should_fade_in_toast =
      target_toast_visible && (scroll_performed || toast_visibility_change);

  if (!should_fade_in_toast)
    return;

  // Because `toast_container_` does not have a layer before the fade in
  // animation, create one.
  DCHECK(!toast_container_->layer());
  toast_container_->SetPaintToLayer();
  toast_container_->layer()->SetFillsBoundsOpaquely(false);

  // Hide the undo toast instantly before starting the toast fade in animation.
  toast_container_->layer()->SetOpacity(0.f);

  animation_builder.GetCurrentSequence().SetOpacity(
      toast_container_->layer(), 1.f, gfx::Tween::ACCEL_5_70_DECEL_90);
}

void AppListBubbleAppsPage::OnAppsGridViewFadeInAnimationEnded(bool aborted) {
  // Destroy the layer created for the layer animation.
  toast_container_->DestroyLayer();

  OnReorderAnimationEnded();
}

void AppListBubbleAppsPage::OnReorderAnimationEnded() {
  update_position_closure_.Reset();

  if (reorder_animation_done_closure_)
    std::move(reorder_animation_done_closure_).Run();
}

void AppListBubbleAppsPage::SlideViewIntoPosition(views::View* view,
                                                  int vertical_offset,
                                                  base::TimeDelta duration,
                                                  gfx::Tween::Type tween_type) {
  // Animation spec:
  //
  // Y Position: Down (offset) → End position
  // Ease: (0.00, 0.00, 0.20, 1.00)

  const bool create_layer = PrepareForLayerAnimation(view);

  // If we created a layer for the view, undo that when the animation ends.
  // The underlying views don't expose weak pointers directly, so use a weak
  // pointer to this view, which owns its children.
  auto cleanup = create_layer ? base::BindRepeating(
                                    &AppListBubbleAppsPage::DestroyLayerForView,
                                    weak_factory_.GetWeakPtr(), view)
                              : base::DoNothing();
  StartSlideInAnimation(view, vertical_offset, duration, tween_type, cleanup);
}

void AppListBubbleAppsPage::FadeInContinueSectionView(views::View* view) {
  const bool create_layer = PrepareForLayerAnimation(view);

  // If we created a layer for the view, undo that when the animation ends.
  // The underlying views don't expose weak pointers directly, so use a weak
  // pointer to this view, which owns its children.
  auto cleanup = create_layer ? base::BindRepeating(
                                    &AppListBubbleAppsPage::DestroyLayerForView,
                                    weak_factory_.GetWeakPtr(), view)
                              : base::DoNothing();

  view->layer()->SetOpacity(0.0f);

  // The animation has a delay to give the separator and apps grid time to
  // partially slide out of the way.
  views::AnimationBuilder()
      .SetPreemptionStrategy(ui::LayerAnimator::PreemptionStrategy::
                                 IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .OnEnded(cleanup)
      .OnAborted(cleanup)
      .Once()
      .At(base::Milliseconds(100))
      .SetOpacity(view->layer(), 1.0f)
      .SetDuration(base::Milliseconds(200));
}

void AppListBubbleAppsPage::OnToggleContinueSection() {
  const int separator_initial_y = separator_->y();

  // Toggle the section visibility.
  bool should_hide = !view_delegate_->ShouldHideContinueSection();
  view_delegate_->SetHideContinueSection(should_hide);
  // AppListControllerImpl will trigger UpdateContinueSectionVisibility().

  // Layout will change the position of the separator and apps grid based on the
  // visibility of the continue section view and recent apps.
  if (needs_layout())
    DeprecatedLayoutImmediately();

  // The vertical offset for slide animations is the difference in separator
  // position from before layout versus its position now.
  const int vertical_offset = separator_initial_y - separator_->y();
  const base::TimeDelta duration = base::Milliseconds(300);
  const gfx::Tween::Type tween_type = gfx::Tween::ACCEL_LIN_DECEL_100_3;

  if (should_hide) {
    // Don't try to fade out the continue section and recent apps because the
    // view is already invisible. UX is OK with these sections not animating.

    // The separator and apps grid slide up.
    DCHECK_GE(vertical_offset, 0);
    SlideViewIntoPosition(separator_, vertical_offset, duration, tween_type);
    SlideViewIntoPosition(scrollable_apps_grid_view_, vertical_offset, duration,
                          tween_type);
  } else {
    // The continue section and recent apps fade in.
    FadeInContinueSectionView(continue_section_);
    FadeInContinueSectionView(recent_apps_);

    // The separator and apps grid slide down.
    DCHECK_LE(vertical_offset, 0);
    SlideViewIntoPosition(separator_, vertical_offset, duration, tween_type);
    SlideViewIntoPosition(scrollable_apps_grid_view_, vertical_offset, duration,
                          tween_type);
  }
}

void AppListBubbleAppsPage::RecordAboveTheFoldMetrics() {
  std::vector<std::string> apps_above_the_fold = {};
  std::vector<std::string> apps_below_the_fold = {};
  for (size_t i = 0; i < scrollable_apps_grid_view_->view_model()->view_size();
       ++i) {
    AppListItemView* child_view =
        scrollable_apps_grid_view_->view_model()->view_at(i);
    if (scrollable_apps_grid_view_->IsAboveTheFold(child_view)) {
      apps_above_the_fold.push_back(child_view->item()->id());
    } else {
      apps_below_the_fold.push_back(child_view->item()->id());
    }
  }
  view_delegate_->RecordAppsDefaultVisibility(
      std::move(apps_above_the_fold), std::move(apps_below_the_fold),
      /*is_apps_collections_page=*/false);
}

BEGIN_METADATA(AppListBubbleAppsPage)
END_METADATA

}  // namespace ash