chromium/ash/shelf/scrollable_shelf_view.cc

// Copyright 2019 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/shelf/scrollable_shelf_view.h"

#include <algorithm>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/screen_util.h"
#include "ash/shelf/scrollable_shelf_constants.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_focus_cycler.h"
#include "ash/shelf/shelf_navigation_widget.h"
#include "ash/shelf/shelf_tooltip_manager.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/status_area_widget.h"
#include "ash/wm/desks/desk_button/desk_button_container.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/animation_throughput_reporter.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/focus/focus_search.h"
#include "ui/views/view_targeter_delegate.h"

namespace ash {

namespace {

// Returns the display id for the display that shows the shelf for |view|.
int64_t GetDisplayIdForView(const views::View* view) {
  aura::Window* window = view->GetWidget()->GetNativeWindow();
  return display::Screen::GetScreen()->GetDisplayNearestWindow(window).id();
}

void ReportSmoothness(bool tablet_mode, bool launcher_visible, int smoothness) {
  base::UmaHistogramPercentage(
      scrollable_shelf_constants::kAnimationSmoothnessHistogram, smoothness);
  if (tablet_mode) {
    if (launcher_visible) {
      base::UmaHistogramPercentage(
          scrollable_shelf_constants::
              kAnimationSmoothnessTabletLauncherVisibleHistogram,
          smoothness);
    } else {
      base::UmaHistogramPercentage(
          scrollable_shelf_constants::
              kAnimationSmoothnessTabletLauncherHiddenHistogram,
          smoothness);
    }
  } else {
    if (launcher_visible) {
      base::UmaHistogramPercentage(
          scrollable_shelf_constants::
              kAnimationSmoothnessClamshellLauncherVisibleHistogram,
          smoothness);
    } else {
      base::UmaHistogramPercentage(
          scrollable_shelf_constants::
              kAnimationSmoothnessClamshellLauncherHiddenHistogram,
          smoothness);
    }
  }
}

gfx::Insets GetMirroredInsets(const gfx::Insets& insets) {
  return gfx::Insets::TLBR(insets.top(), insets.right(), insets.bottom(),
                           insets.left());
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// ScrollableShelfArrowView

class ScrollableShelfView::ScrollableShelfArrowView
    : public ScrollArrowView,
      public views::ViewTargeterDelegate {
  METADATA_HEADER(ScrollableShelfArrowView, ScrollArrowView)

 public:
  explicit ScrollableShelfArrowView(ArrowType arrow_type,
                                    bool is_horizontal_alignment,
                                    ShelfView* shelf_view,
                                    ShelfButtonDelegate* shelf_button_delegate)
      : ScrollArrowView(arrow_type,
                        is_horizontal_alignment,
                        shelf_view->shelf(),
                        shelf_button_delegate),
        shelf_(shelf_view->shelf()) {
    views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::OFF);
    SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
    SetPaintToLayer();
    layer()->SetFillsBoundsOpaquely(false);
    set_context_menu_controller(shelf_view);

    // When the spoken feedback is enabled, scrollable shelf should ensure that
    // the hidden icon which receives the accessibility focus shows through
    // scroll animation. So the arrow button is not useful for the spoken
    // feedback users. The spoken feedback should ignore the arrow button.
    GetViewAccessibility().SetIsIgnored(/*value=*/true);
  }
  ~ScrollableShelfArrowView() override = default;

  // views::ViewTargeterDelegate:
  bool DoesIntersectRect(const views::View* target,
                         const gfx::Rect& rect) const override {
    DCHECK_EQ(target, this);
    const gfx::Rect bounds = gfx::Rect(size());

    // Calculates the tapping area. Note that tapping area is bigger than the
    // arrow button's bounds.
    gfx::Rect tap_rect(
        shelf_->PrimaryAxisValue(
            scrollable_shelf_constants::kArrowButtonTapAreaHorizontal,
            shelf_->hotseat_widget()->GetHotseatSize()),
        shelf_->PrimaryAxisValue(
            shelf_->hotseat_widget()->GetHotseatSize(),
            scrollable_shelf_constants::kArrowButtonTapAreaHorizontal));
    tap_rect -= gfx::Vector2d((tap_rect.width() - bounds.width()) / 2,
                              (tap_rect.height() - bounds.height()) / 2);
    DCHECK(tap_rect.Contains(bounds));

    return tap_rect.Intersects(rect);
  }

  // Make ScrollRectToVisible a no-op because ScrollableShelfArrowView is
  // always visible/invisible depending on the layout strategy at fixed
  // locations. So it does not need to be scrolled to show.
  // TODO (andrewxu): Moves all of functions related with scrolling into
  // ScrollableShelfContainerView. Then erase this empty function.
  void ScrollRectToVisible(const gfx::Rect& rect) override {}

 private:
  const raw_ptr<Shelf> shelf_;
};

BEGIN_METADATA(ScrollableShelfView, ScrollableShelfArrowView)
END_METADATA

////////////////////////////////////////////////////////////////////////////////
// ScopedActiveInkDropCountImpl

class ScrollableShelfView::ScopedActiveInkDropCountImpl
    : public ScrollableShelfView::ScopedActiveInkDropCount {
 public:
  explicit ScopedActiveInkDropCountImpl(
      base::RepeatingCallback<void(bool)> callback)
      : on_active_ink_drop_change_callback_(callback) {
    on_active_ink_drop_change_callback_.Run(/*increase=*/true);
  }

  ~ScopedActiveInkDropCountImpl() override {
    on_active_ink_drop_change_callback_.Run(/*increase=*/false);
  }

  ScopedActiveInkDropCountImpl(const ScopedActiveInkDropCountImpl& rhs) =
      delete;
  ScopedActiveInkDropCountImpl& operator=(
      const ScopedActiveInkDropCountImpl& rhs) = delete;

 private:
  base::RepeatingCallback<void(bool)> on_active_ink_drop_change_callback_;
};

////////////////////////////////////////////////////////////////////////////////
// ScrollableShelfContainerView

class ScrollableShelfContainerView : public ShelfContainerView,
                                     public views::ViewTargeterDelegate {
  METADATA_HEADER(ScrollableShelfContainerView, ShelfContainerView)

 public:
  explicit ScrollableShelfContainerView(
      ScrollableShelfView* scrollable_shelf_view)
      : ShelfContainerView(scrollable_shelf_view->shelf_view()),
        scrollable_shelf_view_(scrollable_shelf_view) {
    SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
  }

  ScrollableShelfContainerView(const ScrollableShelfContainerView&) = delete;
  ScrollableShelfContainerView& operator=(const ScrollableShelfContainerView&) =
      delete;

  ~ScrollableShelfContainerView() override = default;

  // ShelfContainerView:
  void TranslateShelfView(const gfx::Vector2dF& offset) override;

 private:
  // views::View:
  void Layout(PassKey) override;

  // views::ViewTargeterDelegate:
  bool DoesIntersectRect(const views::View* target,
                         const gfx::Rect& rect) const override;

  raw_ptr<ScrollableShelfView> scrollable_shelf_view_ = nullptr;
};

void ScrollableShelfContainerView::TranslateShelfView(
    const gfx::Vector2dF& offset) {
  ShelfContainerView::TranslateShelfView(
      scrollable_shelf_view_->ShouldAdaptToRTL() ? -offset : offset);
}

void ScrollableShelfContainerView::Layout(PassKey) {
  // Should not use ShelfView::GetPreferredSize in replace of
  // CalculateIdealSize. Because ShelfView::CalculatePreferredSize relies on the
  // bounds of app icon. Meanwhile, the icon's bounds may be updated by
  // animation.
  const gfx::Rect ideal_bounds = gfx::Rect(CalculatePreferredSize({}));

  const gfx::Rect local_bounds = GetLocalBounds();
  gfx::Rect shelf_view_bounds =
      local_bounds.Contains(ideal_bounds) ? local_bounds : ideal_bounds;

  if (shelf_view_->shelf()->IsHorizontalAlignment())
    shelf_view_bounds.set_x(ShelfConfig::Get()->GetAppIconEndPadding());
  else
    shelf_view_bounds.set_y(ShelfConfig::Get()->GetAppIconEndPadding());

  shelf_view_->SetBoundsRect(shelf_view_bounds);
  shelf_view_->shelf()
      ->shelf_layout_manager()
      ->HandleScrollableShelfContainerBoundsChange();
}

bool ScrollableShelfContainerView::DoesIntersectRect(
    const views::View* target,
    const gfx::Rect& rect) const {
  // This view's layer is clipped. So the view should only handle the events
  // within the area after cilp.
  gfx::RectF bounds(scrollable_shelf_view_->visible_space());
  views::View::ConvertRectToTarget(scrollable_shelf_view_, this, &bounds);
  return ToEnclosedRect(bounds).Contains(rect);
}

BEGIN_METADATA(ScrollableShelfContainerView)
END_METADATA

////////////////////////////////////////////////////////////////////////////////
// ScrollableShelfFocusSearch

class ScrollableShelfFocusSearch : public views::FocusSearch {
 public:
  explicit ScrollableShelfFocusSearch(
      ScrollableShelfView* scrollable_shelf_view)
      : FocusSearch(/*root=*/nullptr,
                    /*cycle=*/true,
                    /*accessibility_mode=*/true),
        scrollable_shelf_view_(scrollable_shelf_view) {}

  ScrollableShelfFocusSearch(const ScrollableShelfFocusSearch&) = delete;
  ScrollableShelfFocusSearch& operator=(const ScrollableShelfFocusSearch&) =
      delete;

  ~ScrollableShelfFocusSearch() override = default;

  // views::FocusSearch
  views::View* FindNextFocusableView(
      views::View* starting_view,
      FocusSearch::SearchDirection search_direction,
      FocusSearch::TraversalDirection traversal_direction,
      FocusSearch::StartingViewPolicy check_starting_view,
      FocusSearch::AnchoredDialogPolicy can_go_into_anchored_dialog,
      views::FocusTraversable** focus_traversable,
      views::View** focus_traversable_view) override {
    std::vector<views::View*> focusable_views;
    ShelfView* shelf_view = scrollable_shelf_view_->shelf_view();

    for (int i : shelf_view->visible_views_indices())
      focusable_views.push_back(shelf_view->view_model()->view_at(i));

    int start_index = 0;
    for (size_t i = 0; i < focusable_views.size(); ++i) {
      if (focusable_views[i] == starting_view) {
        start_index = i;
        break;
      }
    }

    int new_index =
        start_index +
        (search_direction == FocusSearch::SearchDirection::kBackwards ? -1 : 1);

    // Scrolls to the new page if the focused shelf item is not tappable
    // on the current page.
    if (new_index < 0) {
      new_index = focusable_views.size() - 1;
    } else if (static_cast<size_t>(new_index) >= focusable_views.size()) {
      new_index = 0;
    } else if (static_cast<size_t>(new_index) <
               scrollable_shelf_view_->first_tappable_app_index()) {
      scrollable_shelf_view_->ScrollToNewPage(/*forward=*/false);
    } else if (static_cast<size_t>(new_index) >
               scrollable_shelf_view_->last_tappable_app_index()) {
      scrollable_shelf_view_->ScrollToNewPage(/*forward=*/true);
    }

    return focusable_views[new_index];
  }

 private:
  raw_ptr<ScrollableShelfView> scrollable_shelf_view_ = nullptr;
};

////////////////////////////////////////////////////////////////////////////////
// ScrollableShelfView

ScrollableShelfView::ScrollableShelfView(ShelfModel* model, Shelf* shelf)
    : shelf_view_(new ShelfView(model,
                                shelf,
                                /*drag_and_drop_host=*/this,
                                /*shelf_button_delegate=*/this)),
      page_flip_time_threshold_(
          scrollable_shelf_constants::kShelfPageFlipDelay) {
  Shell::Get()->AddShellObserver(this);
  ShelfConfig::Get()->AddObserver(this);
  set_allow_deactivate_on_esc(true);
}

ScrollableShelfView::~ScrollableShelfView() {
  ShelfConfig::Get()->RemoveObserver(this);
  Shell::Get()->RemoveShellObserver(this);
  GetShelf()->tooltip()->set_shelf_tooltip_delegate(nullptr);
}

void ScrollableShelfView::Init() {
  // Although there is no animation for ScrollableShelfView, a layer is still
  // needed. Otherwise, the child view without its own layer will be painted on
  // RootView which is beneath |translucent_background_| in ShelfWidget.
  // As a result, the child view will not show.
  SetPaintToLayer(ui::LAYER_NOT_DRAWN);
  layer()->SetFillsBoundsOpaquely(false);

  // Initialize the shelf container view.
  // Note that |shelf_container_view_| should be under the arrow buttons. It
  // ensures that the arrow button receives the tapping events which happen
  // within the overlapping zone between the arrow button's tapping area and
  // the bounds of |shelf_container_view_|.
  shelf_container_view_ =
      AddChildView(std::make_unique<ScrollableShelfContainerView>(this));
  shelf_container_view_->Initialize();

  // Initialize the left arrow button.
  left_arrow_ = AddChildView(std::make_unique<ScrollableShelfArrowView>(
      ScrollArrowView::kLeft, GetShelf()->IsHorizontalAlignment(), shelf_view_,
      this));

  // Initialize the right arrow button.
  right_arrow_ = AddChildView(std::make_unique<ScrollableShelfArrowView>(
      ScrollArrowView::kRight, GetShelf()->IsHorizontalAlignment(), shelf_view_,
      this));

  focus_search_ = std::make_unique<ScrollableShelfFocusSearch>(this);

  GetShelf()->tooltip()->set_shelf_tooltip_delegate(this);

  set_context_menu_controller(this);

  // Initializes |shelf_view_| after scrollable shelf view's children are
  // initialized.
  shelf_view_->Init(focus_search_.get());
}

void ScrollableShelfView::OnFocusRingActivationChanged(bool activated) {
  if (activated) {
    focus_ring_activated_ = true;
    SetPaneFocusAndFocusDefault();
    force_show_hotseat_resetter_ =
        GetShelf()->shelf_widget()->ForceShowHotseatInTabletMode();
  } else {
    // Shows the gradient shader when the focus ring is disabled.
    focus_ring_activated_ = false;
    if (force_show_hotseat_resetter_)
      force_show_hotseat_resetter_.RunAndReset();
  }

  MaybeUpdateGradientZone();
}

void ScrollableShelfView::ScrollToNewPage(bool forward) {
  const float offset = CalculatePageScrollingOffset(forward, layout_strategy_);
  if (GetShelf()->IsHorizontalAlignment())
    ScrollByXOffset(offset, /*animating=*/true);
  else
    ScrollByYOffset(offset, /*animating=*/true);
}

views::FocusSearch* ScrollableShelfView::GetFocusSearch() {
  return focus_search_.get();
}

views::FocusTraversable* ScrollableShelfView::GetFocusTraversableParent() {
  return parent()->GetFocusTraversable();
}

views::View* ScrollableShelfView::GetFocusTraversableParentView() {
  return this;
}

views::View* ScrollableShelfView::GetDefaultFocusableChild() {
  // Adapts |scroll_offset_| to show the view properly right after the focus
  // ring is enabled.

  if (default_last_focusable_child_) {
    ScrollToMainOffset(CalculateScrollUpperBound(GetSpaceForIcons()),
                       /*animating=*/true);
    return FindLastFocusableChild();
  }

  ScrollToMainOffset(/*target_offset=*/0.f, /*animating=*/true);
  return FindFirstFocusableChild();
}

gfx::Rect ScrollableShelfView::GetHotseatBackgroundBounds() const {
  return available_space_;
}

bool ScrollableShelfView::ShouldAdaptToRTL() const {
  return base::i18n::IsRTL() && GetShelf()->IsHorizontalAlignment();
}

bool ScrollableShelfView::NeedUpdateToTargetBounds() const {
  return GetAvailableLocalBounds(/*use_target_bounds=*/true) !=
         GetAvailableLocalBounds(/*use_target_bounds=*/false);
}

gfx::Rect ScrollableShelfView::GetTargetScreenBoundsOfItemIcon(
    const ShelfID& id) const {
  const int item_index_in_model = shelf_view_->model()->ItemIndexByID(id);

  // Return a dummy value if the item specified by `id` does not exist in the
  // shelf model.
  // TODO(crbug.com/40057927): it is a quick fixing. We should
  // investigate the root cause.
  if (item_index_in_model < 0)
    return gfx::Rect();

  // Calculates the available space for child views based on the target bounds.
  // To ease coding, we use the variables before mirroring in computation.
  const gfx::Insets target_edge_padding_RTL_mirrored =
      CalculateMirroredEdgePadding(/*use_target_bounds=*/true);
  const gfx::Insets target_edge_padding_before_RTL_mirror =
      ShouldAdaptToRTL() ? GetMirroredInsets(target_edge_padding_RTL_mirrored)
                         : target_edge_padding_RTL_mirrored;
  gfx::Rect target_space_before_RTL_mirror =
      GetAvailableLocalBounds(/*use_target_bounds=*/true);
  target_space_before_RTL_mirror.Inset(target_edge_padding_before_RTL_mirror);

  const gfx::Insets current_edge_padding_RTL_mirrored = edge_padding_insets_;
  const gfx::Insets current_edge_padding_before_RTL_mirror =
      ShouldAdaptToRTL() ? GetMirroredInsets(current_edge_padding_RTL_mirrored)
                         : current_edge_padding_RTL_mirrored;
  gfx::Rect icon_bounds =
      shelf_view_->view_model()->ideal_bounds(item_index_in_model);
  icon_bounds.Offset(target_edge_padding_before_RTL_mirror.left() -
                         current_edge_padding_before_RTL_mirror.left(),
                     0);

  // Transforms |icon_bounds| from shelf view's coordinates to scrollable shelf
  // view's coordinates manually.
  const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment();
  const int shelf_view_offset = ShelfConfig::Get()->GetAppIconEndPadding();
  const int shelf_view_container_offset =
      is_horizontal_alignment ? shelf_container_view_->bounds().x()
                              : shelf_container_view_->bounds().y();
  const int target_scroll_offset = CalculateScrollOffsetForTargetAvailableSpace(
      target_space_before_RTL_mirror);
  const int delta =
      -target_scroll_offset + shelf_view_container_offset + shelf_view_offset;
  const gfx::Vector2d bounds_offset = is_horizontal_alignment
                                          ? gfx::Vector2d(delta, 0)
                                          : gfx::Vector2d(0, delta);
  icon_bounds.Offset(bounds_offset);

  // If the icon is invisible under the target view bounds, replaces the actual
  // icon's bounds with the rectangle centering on the edge of |target_space|.
  const gfx::Point icon_bounds_center = icon_bounds.CenterPoint();
  if (icon_bounds_center.x() > target_space_before_RTL_mirror.right()) {
    icon_bounds.Offset(
        target_space_before_RTL_mirror.right_center().OffsetFromOrigin() -
        icon_bounds_center.OffsetFromOrigin());
  } else if (icon_bounds_center.x() < target_space_before_RTL_mirror.x()) {
    icon_bounds.Offset(
        target_space_before_RTL_mirror.left_center().OffsetFromOrigin() -
        icon_bounds_center.OffsetFromOrigin());
  }

  // Hotseat's target bounds may differ from the actual bounds. So it has to
  // transform the bounds manually from view's local coordinates to screen.
  // Notes that the target bounds stored in shelf layout manager are adapted to
  // RTL already while |icon_bounds| are not adjusted to RTL yet.
  gfx::Rect hotseat_bounds_in_screen =
      GetShelf()->hotseat_widget()->GetTargetBounds();
  if (ShouldAdaptToRTL()) {
    // One simple way for transformation under RTL is: (1) Transforms hotseat
    // target bounds from RTL to LTR. (2) Calculates the icon's bounds in screen
    // under LTR. (3) Transforms the icon's bounds to RTL.
    gfx::Rect display_bounds =
        display::Screen::GetScreen()
            ->GetDisplayNearestWindow(GetWidget()->GetNativeView())
            .bounds();
    hotseat_bounds_in_screen.set_x(display_bounds.right() -
                                   hotseat_bounds_in_screen.right());
    icon_bounds.Offset(hotseat_bounds_in_screen.OffsetFromOrigin());
    icon_bounds.set_x(display_bounds.right() - icon_bounds.right());
  } else {
    icon_bounds.Offset(hotseat_bounds_in_screen.OffsetFromOrigin());
  }

  return icon_bounds;
}

bool ScrollableShelfView::RequiresScrollingForItemSize(
    const gfx::Size& target_size,
    int button_size) const {
  const gfx::Size icons_preferred_size =
      shelf_container_view_->CalculateIdealSize(button_size);
  return !CanFitAllAppsWithoutScrolling(target_size, icons_preferred_size);
}

void ScrollableShelfView::SetEdgePaddingInsets(
    const gfx::Insets& padding_insets) {
  edge_padding_insets_ = padding_insets;
  shelf_view_->LayoutIfAppIconsOffsetUpdates();
}

gfx::Insets ScrollableShelfView::CalculateMirroredEdgePadding(
    bool use_target_bounds) const {
  // Tries display centering strategy.
  const gfx::Insets display_centering_edge_padding =
      CalculateMirroredPaddingForDisplayCentering(use_target_bounds);
  if (!display_centering_edge_padding.IsEmpty()) {
    // Returns early if the value is legal.
    return display_centering_edge_padding;
  }

  const int icons_size =
      shelf_view_->GetSizeOfAppButtons(shelf_view_->number_of_visible_apps(),
                                       shelf_view_->GetButtonSize()) +
      2 * ShelfConfig::Get()->GetAppIconEndPadding();

  const gfx::Rect available_local_bounds =
      GetAvailableLocalBounds(use_target_bounds);
  const int available_size_for_app_icons = GetShelf()->PrimaryAxisValue(
      available_local_bounds.width(), available_local_bounds.height());

  int gap = CanFitAllAppsWithoutScrolling(available_local_bounds.size(),
                                          CalculatePreferredSize({}))
                ? available_size_for_app_icons - icons_size
                : 0;  // overflow

  // Calculates the paddings before/after the visible area of scrollable shelf.
  // |after_padding| being zero ensures that the available space after the
  // visible area is filled first.
  const int before_padding = gap;
  const int after_padding = 0;

  gfx::Insets padding_insets;
  if (GetShelf()->IsHorizontalAlignment()) {
    padding_insets = gfx::Insets::TLBR(0, before_padding, 0, after_padding);
    if (ShouldAdaptToRTL())
      padding_insets = GetMirroredInsets(padding_insets);
  } else {
    padding_insets = gfx::Insets::TLBR(before_padding, 0, after_padding, 0);
  }

  return padding_insets;
}

bool ScrollableShelfView::CalculateShelfOverflowForAvailableLength(
    int available_length) const {
  return available_length < CalculateShelfIconsPreferredLength();
}

views::View* ScrollableShelfView::GetShelfContainerViewForTest() {
  return shelf_container_view_;
}

bool ScrollableShelfView::ShouldAdjustForTest() const {
  return CalculateAdjustmentOffset(CalculateMainAxisScrollDistance(),
                                   layout_strategy_, GetSpaceForIcons());
}

void ScrollableShelfView::SetTestObserver(TestObserver* test_observer) {
  DCHECK(!(test_observer && test_observer_));

  test_observer_ = test_observer;
}

bool ScrollableShelfView::IsAnyCornerButtonInkDropActivatedForTest() const {
  return activated_corner_buttons_ > 0;
}

float ScrollableShelfView::GetScrollUpperBoundForTest() const {
  return CalculateScrollUpperBound(GetSpaceForIcons());
}

bool ScrollableShelfView::IsPageFlipTimerBusyForTest() const {
  return page_flip_timer_.IsRunning();
}

int ScrollableShelfView::GetSumOfButtonSizeAndSpacing() const {
  return shelf_view_->GetButtonSize() + ShelfConfig::Get()->button_spacing();
}

int ScrollableShelfView::GetGestureDragThreshold() const {
  return shelf_view_->GetButtonSize() / 2;
}

float ScrollableShelfView::CalculateScrollUpperBound(
    int available_space_for_icons) const {
  if (layout_strategy_ == kNotShowArrowButtons)
    return 0.f;

  return std::max(
      0, CalculateShelfIconsPreferredLength() - available_space_for_icons);
}

float ScrollableShelfView::CalculateClampedScrollOffset(
    float scroll,
    int available_space_for_icons) const {
  const float scroll_upper_bound =
      CalculateScrollUpperBound(available_space_for_icons);
  scroll = std::clamp(scroll, 0.0f, scroll_upper_bound);
  return scroll;
}

void ScrollableShelfView::StartShelfScrollAnimation(float scroll_distance) {
  const gfx::Vector2dF scroll_offset_before_update = scroll_offset_;
  UpdateScrollOffset(scroll_distance);

  if (scroll_offset_before_update == scroll_offset_)
    return;

  StopObservingImplicitAnimations();

  during_scroll_animation_ = true;
  MaybeUpdateGradientZone();

  // In tablet mode, if the target layout only has one arrow button, enable the
  // rounded corners of the shelf container layer in order to cut off the icons
  // outside of the hotseat background.
  const bool one_arrow_in_target_state =
      (layout_strategy_ == LayoutStrategy::kShowLeftArrowButton ||
       layout_strategy_ == LayoutStrategy::kShowRightArrowButton);
  if (one_arrow_in_target_state)
    EnableShelfRoundedCorners(/*enable=*/true);

  ui::ScopedLayerAnimationSettings animation_settings(
      shelf_view_->layer()->GetAnimator());
  animation_settings.SetTweenType(gfx::Tween::EASE_OUT);
  animation_settings.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
  animation_settings.AddObserver(this);

  ui::AnimationThroughputReporter reporter(
      animation_settings.GetAnimator(),
      metrics_util::ForSmoothnessV3(
          base::BindRepeating(&ReportSmoothness, Shell::Get()->IsInTabletMode(),
                              Shell::Get()->app_list_controller()->IsVisible(
                                  GetDisplayIdForView(this)))));

  shelf_container_view_->TranslateShelfView(scroll_offset_);
}

ScrollableShelfView::LayoutStrategy
ScrollableShelfView::CalculateLayoutStrategy(float scroll_distance_on_main_axis,
                                             int available_length) const {
  if (available_length >= CalculateShelfIconsPreferredLength()) {
    return kNotShowArrowButtons;
  }

  if (scroll_distance_on_main_axis == 0.f) {
    // No invisible shelf buttons at the left side. So hide the left button.
    return kShowRightArrowButton;
  }

  if (scroll_distance_on_main_axis ==
      CalculateScrollUpperBound(available_length)) {
    // If there is no invisible shelf button at the right side, hide the right
    // button.
    return kShowLeftArrowButton;
  }

  // There are invisible shelf buttons at both sides. So show two buttons.
  return kShowButtons;
}

Shelf* ScrollableShelfView::GetShelf() {
  return const_cast<Shelf*>(
      const_cast<const ScrollableShelfView*>(this)->GetShelf());
}

const Shelf* ScrollableShelfView::GetShelf() const {
  return shelf_view_->shelf();
}

gfx::Size ScrollableShelfView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return shelf_container_view_->GetPreferredSize(available_size);
}

void ScrollableShelfView::Layout(PassKey) {
  gfx::Rect shelf_container_bounds = gfx::Rect(size());

  // Transpose and layout as if it is horizontal.
  const bool is_horizontal = GetShelf()->IsHorizontalAlignment();
  if (!is_horizontal)
    shelf_container_bounds.Transpose();

  gfx::Size arrow_button_size(scrollable_shelf_constants::kArrowButtonSize,
                              shelf_container_bounds.height());
  gfx::Size arrow_button_group_size(
      scrollable_shelf_constants::kArrowButtonGroupWidth,
      shelf_container_bounds.height());

  // The bounds of |left_arrow_| and |right_arrow_| are in the
  // ScrollableShelfView's local coordinates.
  gfx::Rect left_arrow_bounds;
  gfx::Rect right_arrow_bounds;

  int before_padding;
  if (ShouldAdaptToRTL()) {
    before_padding = edge_padding_insets_.right();
  } else {
    before_padding = is_horizontal ? edge_padding_insets_.left()
                                   : edge_padding_insets_.top();
  }

  int after_padding;
  if (ShouldAdaptToRTL()) {
    after_padding = edge_padding_insets_.left();
  } else {
    after_padding = is_horizontal ? edge_padding_insets_.right()
                                  : edge_padding_insets_.bottom();
  }

  // Calculates the bounds of the left arrow button. If the left arrow button
  // should not show, |left_arrow_bounds| should be empty.
  if (layout_strategy_ == kShowLeftArrowButton ||
      layout_strategy_ == kShowButtons) {
    gfx::Point left_arrow_start_point(shelf_container_bounds.x(), 0);
    left_arrow_bounds =
        gfx::Rect(left_arrow_start_point, arrow_button_group_size);
    left_arrow_bounds.Offset(before_padding, 0);
    left_arrow_bounds.Inset(gfx::Insets::TLBR(
        0, scrollable_shelf_constants::kArrowButtonEndPadding, 0,
        scrollable_shelf_constants::kDistanceToArrowButton));
    left_arrow_bounds.ClampToCenteredSize(arrow_button_size);
  }

  if (layout_strategy_ == kShowRightArrowButton ||
      layout_strategy_ == kShowButtons) {
    gfx::Point right_arrow_start_point(
        shelf_container_bounds.right() - after_padding -
            scrollable_shelf_constants::kArrowButtonGroupWidth,
        0);
    right_arrow_bounds =
        gfx::Rect(right_arrow_start_point, arrow_button_group_size);
    right_arrow_bounds.Inset(gfx::Insets::TLBR(
        0, scrollable_shelf_constants::kDistanceToArrowButton, 0,
        scrollable_shelf_constants::kArrowButtonEndPadding));
    right_arrow_bounds.ClampToCenteredSize(arrow_button_size);
  }

  // Adjust the bounds when not showing in the horizontal
  // alignment.tShelf()->IsHorizontalAlignment()) {
  if (!is_horizontal) {
    left_arrow_bounds.Transpose();
    right_arrow_bounds.Transpose();
    shelf_container_bounds.Transpose();
  }

  // Layout |left_arrow_| if it should show.
  left_arrow_->SetVisible(!left_arrow_bounds.IsEmpty());
  left_arrow_->SetBoundsRect(left_arrow_bounds);

  // Layout |right_arrow_| if it should show.
  right_arrow_->SetVisible(!right_arrow_bounds.IsEmpty());
  right_arrow_->SetBoundsRect(right_arrow_bounds);

  MaybeUpdateGradientZone();

  // Layout |shelf_container_view_|.
  shelf_container_view_->SetBoundsRect(shelf_container_bounds);

  EnableLayerClipOnShelfContainerView(ShouldEnableLayerClip());
}

void ScrollableShelfView::ChildPreferredSizeChanged(views::View* child) {
  // Add/remove a shelf icon may change the layout strategy.
  UpdateAvailableSpaceAndScroll();
  shelf_container_view_->TranslateShelfView(scroll_offset_);
  DeprecatedLayoutImmediately();
}

void ScrollableShelfView::OnScrollEvent(ui::ScrollEvent* event) {
  if (event->finger_count() != 2)
    return;
  if (ShouldDelegateScrollToShelf(*event)) {
    GetShelf()->ProcessScrollEvent(event);
    event->StopPropagation();
  }
}

void ScrollableShelfView::OnMouseEvent(ui::MouseEvent* event) {
  if (event->IsMouseWheelEvent()) {
    HandleMouseWheelEvent(event->AsMouseWheelEvent());
    return;
  }

  // The mouse event's location may be outside of ShelfView but within the
  // bounds of the ScrollableShelfView. Meanwhile, ScrollableShelfView should
  // handle the mouse event consistently with ShelfView. To achieve this,
  // we simply redirect |event| to ShelfView.
  gfx::Point location_in_shelf_view = event->location();
  View::ConvertPointToTarget(this, shelf_view_, &location_in_shelf_view);
  event->set_location(location_in_shelf_view);
  shelf_view_->OnMouseEvent(event);
}

void ScrollableShelfView::OnGestureEvent(ui::GestureEvent* event) {
  if (ShouldHandleGestures(*event) && ProcessGestureEvent(*event)) {
    // |event| is consumed by ScrollableShelfView.
    event->SetHandled();
  } else if (shelf_view_->HandleGestureEvent(event)) {
    // |event| is consumed by ShelfView.
    event->StopPropagation();
  } else if (event->type() == ui::EventType::kGestureScrollBegin) {
    // |event| is consumed by neither ScrollableShelfView nor ShelfView. So the
    // gesture end event will not be propagated to this view. Then we need to
    // reset the class members related with scroll status explicitly.
    ResetScrollStatus();
  }
}

void ScrollableShelfView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  GetViewAccessibility().SetNextFocus(GetShelf()->GetStatusAreaWidget());
  GetViewAccessibility().SetPreviousFocus(
      GetShelf()->shelf_widget()->navigation_widget());
}

void ScrollableShelfView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  const gfx::Insets old_edge_padding_insets = edge_padding_insets_;
  const gfx::Vector2dF old_scroll_offset = scroll_offset_;

  // The changed view bounds may lead to update on the available space.
  UpdateAvailableSpaceAndScroll();

  // Relayout shelf items if the preferred padding changed.
  if (old_edge_padding_insets != edge_padding_insets_)
    shelf_view_->OnBoundsChanged(shelf_view_->GetBoundsInScreen());

  // Avoids calling AdjustOffset() when the scrollable shelf view is
  // under scroll along the main axis. Otherwise, animation will conflict with
  // scroll gesture. Meanwhile, translates the shelf view
  // if AdjustOffset() returns false since when AdjustOffset() returns true,
  // shelf view is scrolled by animation.
  const bool should_translate_shelf_view =
      scroll_status_ == kAlongMainAxisScroll || !AdjustOffset();

  if (should_translate_shelf_view && old_scroll_offset != scroll_offset_)
    shelf_container_view_->TranslateShelfView(scroll_offset_);
}

void ScrollableShelfView::ViewHierarchyChanged(
    const views::ViewHierarchyChangedDetails& details) {
  if (details.parent != shelf_view_.get()) {
    return;
  }

  shelf_view_->UpdateShelfItemViewsVisibility();

  // When app scaling state needs update, hotseat bounds should change. Then
  // it is not meaningful to do further work in the current view bounds. So
  // returns early.
  if (GetShelf()->hotseat_widget()->UpdateTargetHotseatDensityIfNeeded())
    return;

  const gfx::Vector2dF old_scroll_offset = scroll_offset_;

  // Adding/removing an icon may change the padding then affect the available
  // space.
  UpdateAvailableSpaceAndScroll();

  if (old_scroll_offset != scroll_offset_)
    shelf_container_view_->TranslateShelfView(scroll_offset_);
}

void ScrollableShelfView::ScrollRectToVisible(const gfx::Rect& rect) {
  // Transform |rect| to local view coordinates taking |scroll_offset_| into
  // consideration.
  const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment();
  gfx::Rect rect_after_adjustment = rect;
  if (is_horizontal_alignment)
    rect_after_adjustment.Offset(-scroll_offset_.x(), 0);
  else
    rect_after_adjustment.Offset(0, -scroll_offset_.y());

  // Notes that |rect| is not mirrored under RTL while |visible_space_| has been
  // mirrored. It is easier for coding if we mirror |visible_space_| back and
  // then do the calculation.
  const gfx::Rect visible_space_without_RTL = GetMirroredRect(visible_space_);

  // |rect_after_adjustment| is already shown completely. So scroll is not
  // needed.
  if (visible_space_without_RTL.Contains(rect_after_adjustment)) {
    AdjustOffset();
    return;
  }

  const float original_offset = CalculateMainAxisScrollDistance();

  // |forward| indicates the scroll direction.
  const bool forward =
      is_horizontal_alignment
          ? rect_after_adjustment.right() > visible_space_without_RTL.right()
          : rect_after_adjustment.bottom() > visible_space_without_RTL.bottom();

  // Scrolling |shelf_view_| has the following side-effects:
  // (1) May change the layout strategy.
  // (2) May change the visible space.
  // (3) Must change the scrolling offset.
  // (4) Must change |rect_after_adjustment|'s coordinates after adjusting the
  // scroll.
  LayoutStrategy layout_strategy_after_scroll = layout_strategy_;
  float main_axis_offset_after_scroll = original_offset;
  gfx::Rect visible_space_after_scroll = visible_space_without_RTL;
  gfx::Rect rect_after_scroll = rect_after_adjustment;

  // In each iteration, it scrolls |shelf_view_| to the neighboring page.
  // Terminating the loop iteration if:
  // (1) Find the suitable page which shows |rect| completely.
  // (2) Cannot scroll |shelf_view_| anymore (it may happen with ChromeVox
  // enabled).
  while (!visible_space_after_scroll.Contains(rect_after_scroll)) {
    int page_scroll_distance =
        CalculatePageScrollingOffset(forward, layout_strategy_after_scroll);

    // Breaking the while loop if it cannot scroll anymore.
    if (!page_scroll_distance)
      break;

    main_axis_offset_after_scroll = CalculateTargetOffsetAfterScroll(
        main_axis_offset_after_scroll, page_scroll_distance);
    layout_strategy_after_scroll = CalculateLayoutStrategy(
        main_axis_offset_after_scroll, GetSpaceForIcons());
    visible_space_after_scroll =
        GetMirroredRect(CalculateVisibleSpace(layout_strategy_after_scroll));
    rect_after_scroll = rect_after_adjustment;
    const int offset_delta = main_axis_offset_after_scroll - original_offset;
    if (is_horizontal_alignment)
      rect_after_scroll.Offset(-offset_delta, 0);
    else
      rect_after_scroll.Offset(0, -offset_delta);
  }

  if (!visible_space_after_scroll.Contains(rect_after_scroll))
    return;

  ScrollToMainOffset(main_axis_offset_after_scroll, /*animating=*/true);
}

std::unique_ptr<ui::Layer> ScrollableShelfView::RecreateLayer() {
  layer()->SetGradientMask(gfx::LinearGradient::GetEmpty());
  return views::View::RecreateLayer();
}

void ScrollableShelfView::OnShelfButtonAboutToRequestFocusFromTabTraversal(
    ShelfButton* button,
    bool reverse) {
  if ((button == left_arrow_) || (button == right_arrow_))
    return;

  shelf_view_->OnShelfButtonAboutToRequestFocusFromTabTraversal(button,
                                                                reverse);
  ShelfWidget* shelf_widget = GetShelf()->shelf_widget();
  // In tablet mode, when the hotseat is not extended but one of the buttons
  // gets focused, it should update the visibility of the hotseat.
  if (Shell::Get()->IsInTabletMode() &&
      !shelf_widget->hotseat_widget()->IsExtended()) {
    shelf_widget->shelf_layout_manager()->UpdateVisibilityState(
        /*force_layout=*/false);
  }
}

void ScrollableShelfView::ButtonPressed(views::Button* sender,
                                        const ui::Event& event,
                                        views::InkDrop* ink_drop) {
  if ((sender == left_arrow_) || (sender == right_arrow_)) {
    ScrollToNewPage(sender == right_arrow_);
    return;
  }

  shelf_view_->ButtonPressed(sender, event, ink_drop);
}

void ScrollableShelfView::HandleAccessibleActionScrollToMakeVisible(
    ShelfButton* button) {
  // Scrollable shelf can only be hidden in tablet mode.
  GetShelf()->hotseat_widget()->set_manually_extended(true);
  GetShelf()->shelf_widget()->shelf_layout_manager()->UpdateVisibilityState(
      /*force_layout=*/false);
}

void ScrollableShelfView::OnButtonWillBeRemoved() {
  const int view_size_before_removal = shelf_view_->view_model()->view_size();
  DCHECK_GT(view_size_before_removal, 0);

  // Ensure `last_tappable_app_index_` to be valid after removal. Normally
  // `last_tappable_app_index_` updates when the shelf button is removed. But
  // button removal could be performed at the end of the button fade out
  // animation, which means that incorrect `last_tappable_app_index_` could be
  // accessed during the animation. To handle this issue, update
  // `last_tappable_app_index_` before removal finishes.
  // The code block also covers the edge case that the only shelf item is going
  // to be removed, i.e. `view_size_before_removal_` is one. In this case,
  // both `first_tappable_app_index_` and `last_tappable_app_index_` are reset
  // to invalid values (see https://crbug.com/1300561).
  if (view_size_before_removal < 2) {
    last_tappable_app_index_ = std::nullopt;
  } else {
    last_tappable_app_index_ = std::min(
        last_tappable_app_index_,
        std::make_optional(static_cast<size_t>(view_size_before_removal - 2)));
  }
  first_tappable_app_index_ =
      std::min(first_tappable_app_index_, last_tappable_app_index_);
}

void ScrollableShelfView::OnAppButtonActivated(const ShelfButton* button) {
  ScrollRectToVisible(button->bounds());
}

std::unique_ptr<ScrollableShelfView::ScopedActiveInkDropCount>
ScrollableShelfView::CreateScopedActiveInkDropCount(const ShelfButton* sender) {
  if (!ShouldCountActivatedInkDrop(sender))
    return nullptr;

  return std::make_unique<ScopedActiveInkDropCountImpl>(
      base::BindRepeating(&ScrollableShelfView::OnActiveInkDropChange,
                          weak_ptr_factory_.GetWeakPtr()));
}

void ScrollableShelfView::ShowContextMenuForViewImpl(
    views::View* source,
    const gfx::Point& point,
    ui::MenuSourceType source_type) {
  // |point| is in screen coordinates. So it does not need to transform.
  shelf_view_->ShowContextMenuForViewImpl(shelf_view_, point, source_type);
}

void ScrollableShelfView::OnShelfAlignmentChanged(
    aura::Window* root_window,
    ShelfAlignment old_alignment) {
  const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment();
  left_arrow_->set_is_horizontal_alignment(is_horizontal_alignment);
  right_arrow_->set_is_horizontal_alignment(is_horizontal_alignment);
  scroll_offset_ = gfx::Vector2dF();
  ScrollToMainOffset(CalculateMainAxisScrollDistance(), /*animating=*/false);
  DeprecatedLayoutImmediately();
}

void ScrollableShelfView::OnShelfConfigUpdated() {
  UpdateAvailableSpaceAndScroll();
  shelf_view_->OnShelfConfigUpdated();
}

bool ScrollableShelfView::ShouldShowTooltipForView(
    const views::View* view) const {
  if (!view || !view->parent())
    return false;

  if (view == left_arrow_ || view == right_arrow_)
    return true;

  // TODO(b/288898225): Move shelf tooltip manager delegate implementation
  // outside of `ScrollableShelfView` now that it deals with views outside the
  // `ScrollableShelfView`.
  if (DeskButtonWidget* desk_button_widget = GetShelf()->desk_button_widget()) {
    DeskButtonContainer* desk_button_container =
        desk_button_widget->GetDeskButtonContainer();
    if (view->parent() == desk_button_container && view->GetEnabled() &&
        !desk_button_container->GetTitleForView(view).empty()) {
      return true;
    }
  }

  if (view->parent() != shelf_view_)
    return false;

  // The shelf item corresponding to |view| may have been removed from the
  // model.
  if (!shelf_view_->ShouldShowTooltipForChildView(view))
    return false;

  const gfx::Rect screen_bounds = view->GetBoundsInScreen();
  gfx::Rect visible_bounds_in_screen = visible_space_;
  views::View::ConvertRectToScreen(this, &visible_bounds_in_screen);

  return visible_bounds_in_screen.Contains(screen_bounds);
}

bool ScrollableShelfView::ShouldHideTooltip(const gfx::Point& cursor_location,
                                            views::View* delegate_view) const {
  if (DeskButtonWidget* desk_button_widget = GetShelf()->desk_button_widget()) {
    DeskButtonContainer* desk_button_container =
        desk_button_widget->GetDeskButtonContainer();
    if (delegate_view == desk_button_container) {
      return !desk_button_container->GetLocalBounds().Contains(cursor_location);
    }
  }

  if ((ShouldShowLeftArrow() &&
       left_arrow_->GetMirroredBounds().Contains(cursor_location)) ||
      (ShouldShowRightArrow() &&
       right_arrow_->GetMirroredBounds().Contains(cursor_location))) {
    return false;
  }

  // Should hide the tooltip if |cursor_location| is not in |visible_space_|.
  if (!visible_space_.Contains(cursor_location))
    return true;

  gfx::Point location_in_shelf_view = cursor_location;
  views::View::ConvertPointToTarget(this, shelf_view_, &location_in_shelf_view);
  return shelf_view_->ShouldHideTooltip(location_in_shelf_view, delegate_view);
}

const std::vector<aura::Window*> ScrollableShelfView::GetOpenWindowsForView(
    views::View* view) {
  if (!view || view->parent() != shelf_view_)
    return std::vector<aura::Window*>();

  return shelf_view_->GetOpenWindowsForView(view);
}

std::u16string ScrollableShelfView::GetTitleForView(
    const views::View* view) const {
  if (!view || !view->parent())
    return std::u16string();

  if (view->parent() == shelf_view_)
    return shelf_view_->GetTitleForView(view);

  if (DeskButtonWidget* desk_button_widget = GetShelf()->desk_button_widget()) {
    DeskButtonContainer* desk_button_container =
        desk_button_widget->GetDeskButtonContainer();
    if (view->parent() == desk_button_container) {
      return desk_button_container->GetTitleForView(view);
    }
  }

  if (view == left_arrow_)
    return l10n_util::GetStringUTF16(IDS_SHELF_PREVIOUS);

  if (view == right_arrow_)
    return l10n_util::GetStringUTF16(IDS_SHELF_NEXT);

  return std::u16string();
}

views::View* ScrollableShelfView::GetViewForEvent(const ui::Event& event) {
  if (event.target() == GetWidget()->GetNativeWindow())
    return this;

  if (DeskButtonWidget* desk_button_widget = GetShelf()->desk_button_widget()) {
    if (event.target() == desk_button_widget->GetNativeWindow()) {
      return desk_button_widget->GetDeskButtonContainer();
    }
  }

  return nullptr;
}

void ScrollableShelfView::ScheduleScrollForItemDragIfNeeded(
    const gfx::Rect& item_bounds_in_screen) {
  gfx::Rect visible_space_in_screen = visible_space_;
  views::View::ConvertRectToScreen(this, &visible_space_in_screen);

  drag_item_bounds_in_screen_.emplace(item_bounds_in_screen);
  if (AreBoundsWithinVisibleSpace(*drag_item_bounds_in_screen_)) {
    page_flip_timer_.AbandonAndStop();
    return;
  }

  if (!page_flip_timer_.IsRunning()) {
    page_flip_timer_.Start(FROM_HERE, page_flip_time_threshold_, this,
                           &ScrollableShelfView::OnPageFlipTimer);
  }
}
void ScrollableShelfView::CancelScrollForItemDrag() {
  drag_item_bounds_in_screen_.reset();
  if (page_flip_timer_.IsRunning())
    page_flip_timer_.AbandonAndStop();
}

void ScrollableShelfView::OnImplicitAnimationsCompleted() {
  during_scroll_animation_ = false;
  DeprecatedLayoutImmediately();

  EnableShelfRoundedCorners(/*enable=*/false);

  if (scroll_status_ != kAlongMainAxisScroll)
    UpdateTappableIconIndices();

  // Notifies ChromeVox of the changed location at the end of animation.
  shelf_view_->NotifyAccessibilityEvent(ax::mojom::Event::kLocationChanged,
                                        /*send_native_event=*/true);

  if (!drag_item_bounds_in_screen_ ||
      AreBoundsWithinVisibleSpace(*drag_item_bounds_in_screen_)) {
    return;
  }

  // Keep scrolling if the dragged shelf item is outside of the visible space.
  page_flip_timer_.Start(FROM_HERE, page_flip_time_threshold_, this,
                         &ScrollableShelfView::OnPageFlipTimer);
}

bool ScrollableShelfView::ShouldShowLeftArrow() const {
  return (layout_strategy_ == kShowLeftArrowButton) ||
         (layout_strategy_ == kShowButtons);
}

bool ScrollableShelfView::ShouldShowRightArrow() const {
  return (layout_strategy_ == kShowRightArrowButton) ||
         (layout_strategy_ == kShowButtons);
}

gfx::Rect ScrollableShelfView::GetAvailableLocalBounds(
    bool use_target_bounds) const {
  return use_target_bounds
             ? gfx::Rect(GetShelf()->hotseat_widget()->GetTargetBounds().size())
             : GetLocalBounds();
}

gfx::Insets ScrollableShelfView::CalculateMirroredPaddingForDisplayCentering(
    bool use_target_bounds) const {
  const int icons_size =
      shelf_view_->GetSizeOfAppButtons(shelf_view_->number_of_visible_apps(),
                                       shelf_view_->GetButtonSize()) +
      2 * ShelfConfig::Get()->GetAppIconEndPadding();
  const gfx::Rect display_bounds =
      screen_util::GetDisplayBoundsWithShelf(GetWidget()->GetNativeWindow());
  const int display_size_primary = GetShelf()->PrimaryAxisValue(
      display_bounds.width(), display_bounds.height());
  const int gap = (display_size_primary - icons_size) / 2;

  // Calculates paddings in view coordinates.
  const gfx::Rect screen_bounds =
      use_target_bounds ? GetShelf()->hotseat_widget()->GetTargetBounds()
                        : GetBoundsInScreen();
  int before_padding =
      gap - GetShelf()->PrimaryAxisValue(
                ShouldAdaptToRTL()
                    ? display_bounds.right() - screen_bounds.right()
                    : screen_bounds.x() - display_bounds.x(),
                screen_bounds.y() - display_bounds.y());
  int after_padding =
      gap - GetShelf()->PrimaryAxisValue(
                ShouldAdaptToRTL()
                    ? screen_bounds.x() - display_bounds.x()
                    : display_bounds.right() - screen_bounds.right(),
                display_bounds.bottom() - screen_bounds.bottom());

  // Checks whether there is enough space to ensure |base_padding_|. Returns
  // empty insets if not.
  if (before_padding < 0 || after_padding < 0)
    return gfx::Insets();

  gfx::Insets padding_insets;
  if (GetShelf()->IsHorizontalAlignment()) {
    padding_insets = gfx::Insets::TLBR(0, before_padding, 0, after_padding);
    if (ShouldAdaptToRTL())
      padding_insets = GetMirroredInsets(padding_insets);
  } else {
    padding_insets = gfx::Insets::TLBR(before_padding, 0, after_padding, 0);
  }

  return padding_insets;
}

bool ScrollableShelfView::ShouldHandleGestures(const ui::GestureEvent& event) {
  // ScrollableShelfView only handles the gesture scrolling along the main axis.
  // For other gesture events, including the scrolling across the main axis,
  // they are handled by ShelfView.

  if (scroll_status_ == kNotInScroll && !event.IsScrollGestureEvent())
    return false;

  if (event.type() == ui::EventType::kGestureScrollBegin) {
    CHECK_EQ(scroll_status_, kNotInScroll);

    float main_offset = event.details().scroll_x_hint();
    float cross_offset = event.details().scroll_y_hint();
    if (!GetShelf()->IsHorizontalAlignment())
      std::swap(main_offset, cross_offset);

    if (std::abs(main_offset) < std::abs(cross_offset)) {
      scroll_status_ = kAcrossMainAxisScroll;
    } else if (layout_strategy_ != kNotShowArrowButtons) {
      // Note that if the scrollable shelf is not in overflow mode, scroll along
      // the main axis should not make any UI differences. Do not handle scroll
      // in this scenario.
      scroll_status_ = kAlongMainAxisScroll;
    }
  }

  bool should_handle_gestures = scroll_status_ == kAlongMainAxisScroll;

  if (scroll_status_ == kAlongMainAxisScroll &&
      event.type() == ui::EventType::kGestureScrollBegin) {
    scroll_offset_before_main_axis_scrolling_ = scroll_offset_;
    layout_strategy_before_main_axis_scrolling_ = layout_strategy_;

    // The change in |scroll_status_| may lead to update on the gradient zone.
    MaybeUpdateGradientZone();
  }

  if (event.type() == ui::EventType::kGestureEnd) {
    ResetScrollStatus();
  }

  return should_handle_gestures;
}

void ScrollableShelfView::ResetScrollStatus() {
  scroll_status_ = kNotInScroll;
  scroll_offset_before_main_axis_scrolling_ = gfx::Vector2dF();
  layout_strategy_before_main_axis_scrolling_ = kNotShowArrowButtons;

  // The change in |scroll_status_| may lead to update on the gradient zone.
  MaybeUpdateGradientZone();
}

bool ScrollableShelfView::ProcessGestureEvent(const ui::GestureEvent& event) {
  if (layout_strategy_ == kNotShowArrowButtons)
    return true;

  // Handle scroll-related events, but don't do anything special for begin and
  // end.
  if (event.type() == ui::EventType::kGestureScrollBegin) {
    DCHECK(!presentation_time_recorder_);
    if (Shell::Get()->IsInTabletMode()) {
      if (Shell::Get()->app_list_controller()->IsVisible(
              GetDisplayIdForView(this))) {
        presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
            GetWidget()->GetCompositor(),
            scrollable_shelf_constants::
                kScrollDraggingTabletLauncherVisibleHistogram,
            scrollable_shelf_constants::
                kScrollDraggingTabletLauncherVisibleMaxLatencyHistogram);
      } else {
        presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
            GetWidget()->GetCompositor(),
            scrollable_shelf_constants::
                kScrollDraggingTabletLauncherHiddenHistogram,
            scrollable_shelf_constants::
                kScrollDraggingTabletLauncherHiddenMaxLatencyHistogram);
      }
    } else {
      if (Shell::Get()->app_list_controller()->IsVisible(
              GetDisplayIdForView(this))) {
        presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
            GetWidget()->GetCompositor(),
            scrollable_shelf_constants::
                kScrollDraggingClamshellLauncherVisibleHistogram,
            scrollable_shelf_constants::
                kScrollDraggingClamshellLauncherVisibleMaxLatencyHistogram);
      } else {
        presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
            GetWidget()->GetCompositor(),
            scrollable_shelf_constants::
                kScrollDraggingClamshellLauncherHiddenHistogram,
            scrollable_shelf_constants::
                kScrollDraggingClamshellLauncherHiddenMaxLatencyHistogram);
      }
    }
    return true;
  }

  if (event.type() == ui::EventType::kGestureEnd) {
    // Do not reset |presentation_time_recorder_| in
    // ui::EventType::kGestureScrollEnd event because it may not exist due to
    // gesture fling.
    presentation_time_recorder_.reset();

    // The type of scrolling offset is float to ensure that ScrollableShelfView
    // is responsive to slow gesture scrolling. However, after offset
    // adjustment, the scrolling offset should be floored.
    scroll_offset_ = gfx::ToFlooredVector2d(scroll_offset_);

    // If the scroll animation is created, tappable icon indices are updated
    // at the end of animation.
    if (!AdjustOffset() && !during_scroll_animation_)
      UpdateTappableIconIndices();
    return true;
  }

  if (event.type() == ui::EventType::kScrollFlingStart) {
    const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment();
    if (!ShouldHandleScroll(gfx::Vector2dF(event.details().velocity_x(),
                                           event.details().velocity_y()),
                            /*is_gesture_fling=*/true)) {
      return false;
    }

    int scroll_velocity = is_horizontal_alignment
                              ? event.details().velocity_x()
                              : event.details().velocity_y();
    if (ShouldAdaptToRTL())
      scroll_velocity = -scroll_velocity;
    float page_scrolling_offset = CalculatePageScrollingOffset(
        scroll_velocity < 0, layout_strategy_before_main_axis_scrolling_);

    // Only starts animation when scroll distance is greater than zero.
    if (std::fabs(page_scrolling_offset) > 0.f) {
      ScrollToMainOffset((is_horizontal_alignment
                              ? scroll_offset_before_main_axis_scrolling_.x()
                              : scroll_offset_before_main_axis_scrolling_.y()) +
                             page_scrolling_offset,
                         /*animating=*/true);
    }

    return true;
  }

  if (event.type() != ui::EventType::kGestureScrollUpdate) {
    return false;
  }

  float scroll_delta = 0.f;
  const bool is_horizontal = GetShelf()->IsHorizontalAlignment();
  if (is_horizontal) {
    scroll_delta = -event.details().scroll_x();
    scroll_delta = ShouldAdaptToRTL() ? -scroll_delta : scroll_delta;
  } else {
    scroll_delta = -event.details().scroll_y();
  }

  // Return early if scrollable shelf cannot be scrolled anymore because it has
  // reached to the end.
  const float current_scroll_offset = CalculateMainAxisScrollDistance();
  if ((current_scroll_offset == 0.f && scroll_delta <= 0.f) ||
      (current_scroll_offset == CalculateScrollUpperBound(GetSpaceForIcons()) &&
       scroll_delta >= 0.f)) {
    return true;
  }

  DCHECK(presentation_time_recorder_);
  presentation_time_recorder_->RequestNext();

  if (is_horizontal)
    ScrollByXOffset(scroll_delta, /*animate=*/false);
  else
    ScrollByYOffset(scroll_delta, /*animate=*/false);

  return true;
}

void ScrollableShelfView::HandleMouseWheelEvent(ui::MouseWheelEvent* event) {
  // Note that the scrolling from touchpad is propagated as mouse wheel event.
  // Let the shelf handle mouse wheel events over the empty area of the shelf
  // view, as these events would be ignored by the scrollable shelf view.
  gfx::Point location_in_shelf_view = event->location();
  View::ConvertPointToTarget(this, shelf_view_, &location_in_shelf_view);
  if (!shelf_view_->LocationInsideVisibleShelfItemBounds(
          location_in_shelf_view)) {
    GetShelf()->ProcessMouseWheelEvent(event);
    return;
  }

  if (!ShouldHandleScroll(gfx::Vector2dF(event->x_offset(), event->y_offset()),
                          /*is_gesture_fling=*/false)) {
    return;
  }

  event->SetHandled();

  // Scrolling the mouse wheel may create multiple mouse wheel events at the
  // same time. If the scrollable shelf view is during scrolling animation at
  // this moment, do not handle the mouse wheel event.
  if (shelf_view_->layer()->GetAnimator()->is_animating())
    return;

  if (GetShelf()->IsHorizontalAlignment()) {
    const float x_offset = event->x_offset();
    const float y_offset = event->y_offset();
    // If the shelf is bottom aligned, we can scroll over the shelf contents if
    // the scroll is horizontal or vertical (in the case of a mousewheel
    // scroll). We take the biggest offset difference of the vertical and
    // horizontal components to determine the offset to scroll over the
    // contents.
    float max_absolute_offset =
        abs(x_offset) > abs(y_offset) ? x_offset : y_offset;
    ScrollByXOffset(
        CalculatePageScrollingOffset(max_absolute_offset < 0, layout_strategy_),
        /*animating=*/true);
  } else {
    ScrollByYOffset(
        CalculatePageScrollingOffset(event->y_offset() < 0, layout_strategy_),
        /*animating=*/true);
  }
}

void ScrollableShelfView::ScrollByXOffset(float x_offset, bool animating) {
  ScrollToMainOffset(scroll_offset_.x() + x_offset, animating);
}

void ScrollableShelfView::ScrollByYOffset(float y_offset, bool animating) {
  ScrollToMainOffset(scroll_offset_.y() + y_offset, animating);
}

void ScrollableShelfView::ScrollToMainOffset(float target_offset,
                                             bool animating) {
  if (animating) {
    StartShelfScrollAnimation(target_offset);
  } else {
    UpdateScrollOffset(target_offset);
    shelf_container_view_->TranslateShelfView(scroll_offset_);
  }
}

float ScrollableShelfView::CalculatePageScrollingOffset(
    bool forward,
    LayoutStrategy layout_strategy) const {
  // Returns zero if inputs are invalid.
  const bool invalid = (layout_strategy == kNotShowArrowButtons) ||
                       (layout_strategy == kShowLeftArrowButton && forward) ||
                       (layout_strategy == kShowRightArrowButton && !forward);
  if (invalid)
    return 0;

  float offset = CalculatePageScrollingOffsetInAbs(layout_strategy);

  if (!forward)
    offset = -offset;

  return offset;
}

float ScrollableShelfView::CalculatePageScrollingOffsetInAbs(
    LayoutStrategy layout_strategy) const {
  // Implement the arrow button handler in the same way with the gesture
  // scrolling. The key is to calculate the suitable scroll distance.

  float offset = 0.f;

  // The available space for icons excluding the area taken by arrow button(s).
  int space_excluding_arrow;

  const int space_needed_for_button = GetSumOfButtonSizeAndSpacing();

  if (layout_strategy == kShowRightArrowButton) {
    space_excluding_arrow =
        GetSpaceForIcons() - scrollable_shelf_constants::kArrowButtonGroupWidth;

    // After scrolling, the left arrow button will show. Adapts the offset
    // to the extra arrow button.
    const int offset_for_extra_arrow =
        scrollable_shelf_constants::kArrowButtonGroupWidth -
        ShelfConfig::Get()->GetAppIconEndPadding();

    const int mod = space_excluding_arrow % space_needed_for_button;
    offset = space_excluding_arrow - mod - offset_for_extra_arrow;
  } else if (layout_strategy == kShowButtons ||
             layout_strategy == kShowLeftArrowButton) {
    space_excluding_arrow =
        GetSpaceForIcons() -
        2 * scrollable_shelf_constants::kArrowButtonGroupWidth;
    const int mod = space_excluding_arrow % space_needed_for_button;
    offset = space_excluding_arrow - mod;

    // Layout of kShowLeftArrowButton can be regarded as the layout of
    // kShowButtons with extra offset.
    if (layout_strategy == kShowLeftArrowButton) {
      const int extra_offset =
          -ShelfConfig::Get()->button_spacing() -
          (GetSpaceForIcons() -
           scrollable_shelf_constants::kArrowButtonGroupWidth) %
              space_needed_for_button +
          ShelfConfig::Get()->GetAppIconEndPadding();
      offset += extra_offset;
    }
  }

  // Ensure the return value to be non-negative. Note that if the screen is too
  // small (usually on the Linux emulator), `offset` may be negative.
  return std::fmax(offset, 0.f);
}

float ScrollableShelfView::CalculateTargetOffsetAfterScroll(
    float start_offset,
    float scroll_distance) const {
  float target_offset = start_offset;

  target_offset += scroll_distance;
  target_offset =
      CalculateClampedScrollOffset(target_offset, GetSpaceForIcons());
  LayoutStrategy layout_strategy_after_scroll =
      CalculateLayoutStrategy(target_offset, GetSpaceForIcons());
  target_offset = CalculateScrollDistanceAfterAdjustment(
      target_offset, layout_strategy_after_scroll);

  return target_offset;
}

void ScrollableShelfView::CalculateHorizontalGradient(
    gfx::LinearGradient* gradient_mask) {
  auto get_clamped = [](int position, int total) -> float {
    return std::clamp(static_cast<float>(position) / total, 0.f, 1.f);
  };

  // Do not add gradient if visible width is too narrow.
  if (visible_space_.right() <
      visible_space_.x() +
          2 * scrollable_shelf_constants::kGradientZoneLength) {
    return;
  }

  float gradient_start, gradient_end;

  const bool rtl = ShouldAdaptToRTL();

  // Horizontal linear gradient, from left to right
  gradient_mask->set_angle(0);

  // If true, create a gradient area that fades in the shelf app buttons at
  // the beginning
  const bool show_fade_in =
      rtl ? should_show_end_gradient_zone_ : should_show_start_gradient_zone_;
  if (show_fade_in) {
    gradient_start = get_clamped((visible_space_.x() - 1), width());
    gradient_end = get_clamped(
        (visible_space_.x() + scrollable_shelf_constants::kGradientZoneLength),
        width());

    // When the scroll arrow button shows, `gradient_start` is greater than 0.
    // Ensure that the area in the range [0, gradient_start) has an opaque
    // opacity so that the scroll arrow button is visible.
    if (gradient_start > 0) {
      gradient_mask->AddStep(0, /*alpha=*/255);
      gradient_mask->AddStep(get_clamped((visible_space_.x() - 2), width()),
                             255);
    }
    gradient_mask->AddStep(gradient_start, 0);
    gradient_mask->AddStep(gradient_end, 255);
  }

  // If true, create a gradient area that fades out the shelf app buttons at
  // the end
  bool show_fade_out =
      rtl ? should_show_start_gradient_zone_ : should_show_end_gradient_zone_;
  if (show_fade_out) {
    gradient_start =
        get_clamped((visible_space_.right() -
                     scrollable_shelf_constants::kGradientZoneLength),
                    width());
    gradient_end = get_clamped((visible_space_.right() + 1), width());
    gradient_mask->AddStep(gradient_start, /*alpha=*/255);
    gradient_mask->AddStep(gradient_end, 0);

    // When the scroll arrow button shows, `gradient_end` is less than 1.
    // Ensure that the area in the range (gradient_end, 1] has an opaque
    // opacity so that the scroll arrow button is visible.
    if (gradient_end < 1) {
      gradient_mask->AddStep(get_clamped((visible_space_.right() + 2), width()),
                             255);
      gradient_mask->AddStep(1, 255);
    }
  }
}

void ScrollableShelfView::CalculateVerticalGradient(
    gfx::LinearGradient* gradient_mask) {
  auto get_clamped = [](int position, int total) -> float {
    return std::clamp(static_cast<float>(position) / total, 0.f, 1.f);
  };

  // Do not add gradient if visible height is too small.
  if (visible_space_.bottom() <
      visible_space_.y() +
          2 * scrollable_shelf_constants::kGradientZoneLength) {
    return;
  }

  float gradient_start, gradient_end;

  DCHECK(!ShouldAdaptToRTL());

  // Vertical gradient from top to bottom.
  gradient_mask->set_angle(-90);

  if (should_show_start_gradient_zone_) {
    gradient_start = get_clamped((visible_space_.y() - 1), height());
    gradient_end = get_clamped(
        (visible_space_.y() + scrollable_shelf_constants::kGradientZoneLength),
        height());

    // When the scroll arrow button shows, `gradient_start` is greater than 0.
    // Ensure that the area in the range [0, gradient_start) has an opaque
    // opacity so that the scroll arrow button is visible.
    if (gradient_start > 0) {
      gradient_mask->AddStep(0, /*alpha=*/255);
      gradient_mask->AddStep(get_clamped((visible_space_.y() - 2), height()),
                             255);
    }
    gradient_mask->AddStep(gradient_start, 0);
    gradient_mask->AddStep(gradient_end, 255);
  }

  if (should_show_end_gradient_zone_) {
    gradient_start =
        get_clamped((visible_space_.bottom() -
                     scrollable_shelf_constants::kGradientZoneLength),
                    height());
    gradient_end = get_clamped((visible_space_.bottom() + 1), height());
    gradient_mask->AddStep(gradient_start,
                           /*alpha=*/255);
    gradient_mask->AddStep(gradient_end, 0);

    // When the scroll arrow button shows, `gradient_end` is less than 1.
    // Ensure that the area in the range (gradient_end, 1] has an opaque
    // opacity so that the scroll arrow button is visible.
    if (gradient_end < 1) {
      gradient_mask->AddStep(
          get_clamped((visible_space_.bottom() + 2), height()), 255);
      gradient_mask->AddStep(1, 255);
    }
  }
}

void ScrollableShelfView::UpdateGradientMask() {
  // There is no visible shelf app buttons so return early
  if (bounds().IsEmpty() || visible_space_.IsEmpty())
    return;

  gfx::LinearGradient gradient_mask;

  if (GetShelf()->IsHorizontalAlignment()) {
    CalculateHorizontalGradient(&gradient_mask);
  } else {
    CalculateVerticalGradient(&gradient_mask);
  }

  // Return if the gradients do not change.
  if (gradient_mask == layer()->gradient_mask())
    return;

  layer()->SetGradientMask(gradient_mask);
}

void ScrollableShelfView::UpdateGradientZoneState() {
  // The gradient zone is not painted when the focus ring shows in order to
  // display the focus ring correctly.
  if (focus_ring_activated_) {
    should_show_start_gradient_zone_ = false;
    should_show_end_gradient_zone_ = false;
    return;
  }

  if (during_scroll_animation_) {
    should_show_start_gradient_zone_ = true;
    should_show_end_gradient_zone_ = true;
    return;
  }

  should_show_start_gradient_zone_ = layout_strategy_ == kShowLeftArrowButton ||
                                     (layout_strategy_ == kShowButtons &&
                                      scroll_status_ == kAlongMainAxisScroll);
  should_show_end_gradient_zone_ = ShouldShowRightArrow();
}

void ScrollableShelfView::MaybeUpdateGradientZone() {
  if (!ShouldApplyMaskLayerGradientZone())
    return;

  // Fade zones should be updated if:
  // (1) Fade zone's visibility changes.
  // (2) Fade zone should show and the arrow button's location changes.
  UpdateGradientZoneState();
  UpdateGradientMask();
}

bool ScrollableShelfView::ShouldApplyMaskLayerGradientZone() const {
  return layout_strategy_ != LayoutStrategy::kNotShowArrowButtons;
}

float ScrollableShelfView::GetActualScrollOffset(
    float main_axis_scroll_distance,
    LayoutStrategy layout_strategy) const {
  return (layout_strategy == kShowButtons ||
          layout_strategy == kShowLeftArrowButton)
             ? (main_axis_scroll_distance +
                scrollable_shelf_constants::kArrowButtonGroupWidth -
                ShelfConfig::Get()->GetAppIconEndPadding())
             : main_axis_scroll_distance;
}

void ScrollableShelfView::UpdateTappableIconIndices() {
  // Scrollable shelf should be not under the scroll along the main axis, which
  // means that the decimal part of the main scroll offset should be zero.
  DCHECK(scroll_status_ != kAlongMainAxisScroll);

  // The value returned by CalculateMainAxisScrollDistance() can be casted into
  // an integer without losing precision since the decimal part is zero.
  const auto tappable_indices = CalculateTappableIconIndices(
      layout_strategy_, CalculateMainAxisScrollDistance());
  first_tappable_app_index_ = tappable_indices.first;
  last_tappable_app_index_ = tappable_indices.second;
}

std::pair<std::optional<size_t>, std::optional<size_t>>
ScrollableShelfView::CalculateTappableIconIndices(
    ScrollableShelfView::LayoutStrategy layout_strategy,
    int scroll_distance_on_main_axis) const {
  const auto& visible_views_indices = shelf_view_->visible_views_indices();

  if (visible_views_indices.empty() || visible_space_.IsEmpty())
    return {std::nullopt, std::nullopt};

  if (layout_strategy == ScrollableShelfView::kNotShowArrowButtons) {
    return {visible_views_indices.front(), visible_views_indices.back()};
  }

  const int visible_size = GetShelf()->IsHorizontalAlignment()
                               ? visible_space_.width()
                               : visible_space_.height();

  const int space_needed_for_button = GetSumOfButtonSizeAndSpacing();

  // Note that some apps may have their |ShelfAppButton| views hidden, when they
  // are on an inactive desk. Therefore, the indices of tappable apps may not be
  // contiguous, so we need to map from a visible view index back to an app
  // index. The below are indices into the |visible_views_indices| vector.
  size_t first_visible_view_index;
  size_t last_visible_view_index;
  if (layout_strategy == kShowRightArrowButton ||
      layout_strategy == kShowButtons) {
    first_visible_view_index =
        scroll_distance_on_main_axis / space_needed_for_button +
        (layout_strategy == kShowButtons ? 1 : 0);
    last_visible_view_index =
        first_visible_view_index + visible_size / space_needed_for_button;

    const int end_of_last_visible_view =
        last_visible_view_index * space_needed_for_button +
        shelf_view_->GetButtonSize() - scroll_distance_on_main_axis;

    // It is very rare but |visible_size| may be smaller than
    // |space_needed_for_button| as reported in https://crbug.com/1094363.
    if (end_of_last_visible_view > visible_size &&
        last_visible_view_index > first_visible_view_index) {
      last_visible_view_index--;
    }
  } else {
    DCHECK_EQ(layout_strategy, kShowLeftArrowButton);
    last_visible_view_index = visible_views_indices.size() - 1;

    // In fuzz tests, `visible_size` may be smaller than
    // `space_needed_for_button` although it never happens on real devices.
    first_visible_view_index =
        visible_size >= space_needed_for_button
            ? last_visible_view_index - visible_size / space_needed_for_button +
                  1
            : last_visible_view_index;
  }

  DCHECK_LT(first_visible_view_index, visible_views_indices.size());
  DCHECK_LT(last_visible_view_index, visible_views_indices.size());

  // Ensure that each visible view index is within the bounds of
  // `visible_views_indices`.
  // TODO(b/268401797): Rewrite CalculateTappableIconIndices() as a more
  // thorough fix for out of bound indices.
  first_visible_view_index =
      std::clamp(first_visible_view_index, static_cast<size_t>(0),
                 visible_views_indices.size() - 1);
  last_visible_view_index =
      std::clamp(last_visible_view_index, first_visible_view_index,
                 visible_views_indices.size() - 1);

  return {visible_views_indices[first_visible_view_index],
          visible_views_indices[last_visible_view_index]};
}

views::View* ScrollableShelfView::FindFirstFocusableChild() {
  return shelf_view_->FindFirstFocusableChild();
}

views::View* ScrollableShelfView::FindLastFocusableChild() {
  return shelf_view_->FindLastFocusableChild();
}

int ScrollableShelfView::GetSpaceForIcons() const {
  return GetShelf()->IsHorizontalAlignment() ? available_space_.width()
                                             : available_space_.height();
}

bool ScrollableShelfView::CanFitAllAppsWithoutScrolling(
    const gfx::Size& available_size,
    const gfx::Size& icons_preferred_size) const {
  const int available_length =
      (GetShelf()->IsHorizontalAlignment() ? available_size.width()
                                           : available_size.height());

  int preferred_length = GetShelf()->IsHorizontalAlignment()
                             ? icons_preferred_size.width()
                             : icons_preferred_size.height();
  preferred_length += 2 * ShelfConfig::Get()->GetAppIconEndPadding();

  return available_length >= preferred_length;
}

bool ScrollableShelfView::ShouldHandleScroll(const gfx::Vector2dF& offset,
                                             bool is_gesture_scrolling) const {
  // When the shelf is aligned at the bottom, a horizontal mousewheel scroll may
  // also be handled by the ScrollableShelf if the offset along the main axis is
  // 0. This case is mainly triggered by an event generated in the MouseWheel,
  // but not in the touchpad, as touchpads events are caught on ScrollEvent.
  // If there is an x component to the scroll, consider this instead of the y
  // axis because the horizontal scroll could move the scrollable shelf.
  const float main_axis_offset =
      GetShelf()->IsHorizontalAlignment() && offset.x() != 0 ? offset.x()
                                                             : offset.y();

  const int threshold =
      is_gesture_scrolling
          ? scrollable_shelf_constants::kGestureFlingVelocityThreshold
          : scrollable_shelf_constants::kScrollOffsetThreshold;
  return abs(main_axis_offset) > threshold;
}

bool ScrollableShelfView::AdjustOffset() {
  const float offset = CalculateAdjustmentOffset(
      CalculateMainAxisScrollDistance(), layout_strategy_, GetSpaceForIcons());

  // Returns early when it does not need to adjust the shelf view's location.
  if (!offset)
    return false;

  if (GetShelf()->IsHorizontalAlignment())
    ScrollByXOffset(offset, /*animate=*/true);
  else
    ScrollByYOffset(offset, /*animate=*/true);

  return true;
}

float ScrollableShelfView::CalculateAdjustmentOffset(
    int main_axis_scroll_distance,
    LayoutStrategy layout_strategy,
    int available_space_for_icons) const {
  // Scrollable shelf should be not under the scroll along the main axis, which
  // means that the decimal part of the main scroll offset should be zero.
  DCHECK(scroll_status_ != kAlongMainAxisScroll);

  // Returns early when it does not need to adjust the shelf view's location.
  if (layout_strategy == kNotShowArrowButtons ||
      main_axis_scroll_distance >=
          CalculateScrollUpperBound(available_space_for_icons)) {
    return 0;
  }

  // Because the decimal part of the scroll offset is zero, it is meaningful
  // to use modulo operation here.
  const int remainder = static_cast<int>(GetActualScrollOffset(
                            main_axis_scroll_distance, layout_strategy)) %
                        GetSumOfButtonSizeAndSpacing();
  int offset = remainder > GetGestureDragThreshold()
                   ? GetSumOfButtonSizeAndSpacing() - remainder
                   : -remainder;

  return offset;
}

int ScrollableShelfView::CalculateScrollDistanceAfterAdjustment(
    int main_axis_scroll_distance,
    LayoutStrategy layout_strategy) const {
  return main_axis_scroll_distance +
         CalculateAdjustmentOffset(main_axis_scroll_distance, layout_strategy,
                                   GetSpaceForIcons());
}

void ScrollableShelfView::UpdateAvailableSpace() {
  if (!is_padding_configured_externally_) {
    edge_padding_insets_ =
        CalculateMirroredEdgePadding(/*use_target_bounds=*/false);
  }

  available_space_ = GetLocalBounds();
  available_space_.Inset(edge_padding_insets_);

  // The hotseat uses |available_space_| to determine where to show its
  // background, so notify it when it is recalculated.
  if (HotseatWidget::ShouldShowHotseatBackground())
    GetShelf()->hotseat_widget()->UpdateTranslucentBackground();
}

gfx::Rect ScrollableShelfView::CalculateVisibleSpace(
    LayoutStrategy layout_strategy) const {
  const bool in_tablet_mode = Shell::Get()->IsInTabletMode();
  if (layout_strategy == kNotShowArrowButtons && !in_tablet_mode)
    return GetAvailableLocalBounds(/*use_target_bounds=*/false);

  const bool should_show_left_arrow =
      (layout_strategy == kShowLeftArrowButton) ||
      (layout_strategy == kShowButtons);
  const bool should_show_right_arrow =
      (layout_strategy == kShowRightArrowButton) ||
      (layout_strategy == kShowButtons);

  const int before_padding =
      (should_show_left_arrow
           ? scrollable_shelf_constants::kArrowButtonGroupWidth
           : 0);
  const int after_padding =
      (should_show_right_arrow
           ? scrollable_shelf_constants::kArrowButtonGroupWidth
           : 0);

  gfx::Insets visible_space_insets;
  if (ShouldAdaptToRTL()) {
    visible_space_insets =
        gfx::Insets::TLBR(0, after_padding, 0, before_padding);
  } else {
    visible_space_insets =
        GetShelf()->IsHorizontalAlignment()
            ? gfx::Insets::TLBR(0, before_padding, 0, after_padding)
            : gfx::Insets::TLBR(before_padding, 0, after_padding, 0);
  }
  visible_space_insets -= CalculateRipplePaddingInsets();

  gfx::Rect visible_space = available_space_;
  visible_space.Inset(visible_space_insets);

  return visible_space;
}

gfx::Insets ScrollableShelfView::CalculateRipplePaddingInsets() const {
  // Indicates whether it is in tablet mode with hotseat enabled.
  const bool in_tablet_mode = display::Screen::GetScreen()->InTabletMode();

  const int ripple_padding =
      ShelfConfig::Get()->scrollable_shelf_ripple_padding();
  const int before_padding =
      (in_tablet_mode && !ShouldShowLeftArrow()) ? 0 : ripple_padding;
  const int after_padding =
      (in_tablet_mode && !ShouldShowRightArrow()) ? 0 : ripple_padding;

  if (ShouldAdaptToRTL())
    return gfx::Insets::TLBR(0, after_padding, 0, before_padding);

  return GetShelf()->IsHorizontalAlignment()
             ? gfx::Insets::TLBR(0, before_padding, 0, after_padding)
             : gfx::Insets::TLBR(before_padding, 0, after_padding, 0);
}

gfx::RoundedCornersF
ScrollableShelfView::CalculateShelfContainerRoundedCorners() const {
  if (!display::Screen::GetScreen()->InTabletMode()) {
    return gfx::RoundedCornersF();
  }

  const bool is_horizontal_alignment = GetShelf()->IsHorizontalAlignment();
  const float radius = (is_horizontal_alignment ? height() : width()) / 2.f;

  int upper_left = ShouldShowLeftArrow() ? 0 : radius;

  int upper_right;
  if (is_horizontal_alignment)
    upper_right = ShouldShowRightArrow() ? 0 : radius;
  else
    upper_right = ShouldShowLeftArrow() ? 0 : radius;

  int lower_right = ShouldShowRightArrow() ? 0 : radius;

  int lower_left;
  if (is_horizontal_alignment)
    lower_left = ShouldShowLeftArrow() ? 0 : radius;
  else
    lower_left = ShouldShowRightArrow() ? 0 : radius;

  if (ShouldAdaptToRTL()) {
    std::swap(upper_left, upper_right);
    std::swap(lower_left, lower_right);
  }

  return gfx::RoundedCornersF(upper_left, upper_right, lower_right, lower_left);
}

void ScrollableShelfView::OnPageFlipTimer() {
  gfx::Rect visible_space_in_screen = visible_space_;
  views::View::ConvertRectToScreen(this, &visible_space_in_screen);

  // Calculates the page scrolling direction based on the drag item bounds and
  // the bounds of the visible space.
  bool should_scroll_to_next;
  if (ShouldAdaptToRTL()) {
    should_scroll_to_next =
        drag_item_bounds_in_screen_->x() < visible_space_in_screen.x();
  } else {
    should_scroll_to_next = GetShelf()->IsHorizontalAlignment()
                                ? drag_item_bounds_in_screen_->right() >
                                      visible_space_in_screen.right()
                                : drag_item_bounds_in_screen_->bottom() >
                                      visible_space_in_screen.bottom();
  }

  ScrollToNewPage(/*forward=*/should_scroll_to_next);

  if (test_observer_)
    test_observer_->OnPageFlipTimerFired();
}

bool ScrollableShelfView::AreBoundsWithinVisibleSpace(
    const gfx::Rect& bounds_in_screen) const {
  if (bounds_in_screen.IsEmpty())
    return false;

  gfx::Rect visible_space_in_screen = visible_space_;
  views::View::ConvertRectToScreen(this, &visible_space_in_screen);

  if (GetShelf()->IsHorizontalAlignment()) {
    return bounds_in_screen.x() >= visible_space_in_screen.x() &&
           bounds_in_screen.right() <= visible_space_in_screen.right();
  }

  return bounds_in_screen.y() >= visible_space_in_screen.y() &&
         bounds_in_screen.bottom() <= visible_space_in_screen.bottom();
}

bool ScrollableShelfView::ShouldDelegateScrollToShelf(
    const ui::ScrollEvent& event) const {
  // When the shelf is not aligned in the bottom, the events should be
  // propagated and handled as MouseWheel events.

  if (event.type() != ui::EventType::kScroll) {
    return false;
  }

  const float main_offset =
      GetShelf()->IsHorizontalAlignment() ? event.x_offset() : event.y_offset();
  const float cross_offset =
      GetShelf()->IsHorizontalAlignment() ? event.y_offset() : event.x_offset();

  // We only delegate to the shelf scroll events across the main axis,
  // otherwise, let them propagate and be handled as MouseWheel Events.
  return std::abs(main_offset) < std::abs(cross_offset);
}

float ScrollableShelfView::CalculateMainAxisScrollDistance() const {
  return GetShelf()->IsHorizontalAlignment() ? scroll_offset_.x()
                                             : scroll_offset_.y();
}

void ScrollableShelfView::UpdateScrollOffset(float target_offset) {
  target_offset =
      CalculateClampedScrollOffset(target_offset, GetSpaceForIcons());

  if (GetShelf()->IsHorizontalAlignment())
    scroll_offset_.set_x(target_offset);
  else
    scroll_offset_.set_y(target_offset);

  // Calculating the layout strategy relies on |scroll_offset_|.
  LayoutStrategy new_strategy = CalculateLayoutStrategy(
      CalculateMainAxisScrollDistance(), GetSpaceForIcons());

  const bool strategy_needs_update = (layout_strategy_ != new_strategy);
  if (strategy_needs_update) {
    layout_strategy_ = new_strategy;
    const bool has_gradient_zone = layer()->HasGradientMask();
    const bool should_have_gradient_zone = ShouldApplyMaskLayerGradientZone();
    if (has_gradient_zone && !should_have_gradient_zone) {
      layer()->SetGradientMask(gfx::LinearGradient::GetEmpty());
    }
    InvalidateLayout();
  }

  visible_space_ = CalculateVisibleSpace(layout_strategy_);

  if (scroll_status_ != kAlongMainAxisScroll)
    UpdateTappableIconIndices();
}

void ScrollableShelfView::UpdateAvailableSpaceAndScroll() {
  UpdateAvailableSpace();
  UpdateScrollOffset(CalculateMainAxisScrollDistance());
}

int ScrollableShelfView::CalculateScrollOffsetForTargetAvailableSpace(
    const gfx::Rect& target_space) const {
  // Ensures that the scroll offset is legal under the updated available space.
  const int available_space_for_icons =
      GetShelf()->PrimaryAxisValue(target_space.width(), target_space.height());
  int target_scroll_offset = CalculateClampedScrollOffset(
      CalculateMainAxisScrollDistance(), available_space_for_icons);

  // Calculates the layout strategy based on the new scroll offset.
  LayoutStrategy new_strategy =
      CalculateLayoutStrategy(target_scroll_offset, available_space_for_icons);

  // Adjusts the scroll offset with the new strategy.
  target_scroll_offset += CalculateAdjustmentOffset(
      target_scroll_offset, new_strategy, available_space_for_icons);

  return target_scroll_offset;
}

bool ScrollableShelfView::ShouldCountActivatedInkDrop(
    const views::View* sender) const {
  bool should_count = false;

  // When scrolling shelf by gestures, the shelf icon's ink drop ripple may be
  // activated accidentally. So ignore the ink drop activity during animation.
  if (during_scroll_animation_)
    return should_count;

  if (!first_tappable_app_index_.has_value() ||
      !last_tappable_app_index_.has_value()) {
    // Verify that `first_tappable_app_index_` and `last_tappable_app_index_`
    // are both illegal. In that case, return early.
    DCHECK(first_tappable_app_index_ == last_tappable_app_index_);
    return false;
  }

  // The ink drop needs to be clipped only if |sender| is the app at one of the
  // corners of the shelf. This happens if it is either the first or the last
  // tappable app and no arrow is showing on its side.
  if (shelf_view_->view_model()->view_at(first_tappable_app_index_.value()) ==
      sender) {
    should_count = !(layout_strategy_ == kShowButtons ||
                     layout_strategy_ == kShowLeftArrowButton);
  } else if (shelf_view_->view_model()->view_at(
                 last_tappable_app_index_.value()) == sender) {
    should_count = !(layout_strategy_ == kShowButtons ||
                     layout_strategy_ == kShowRightArrowButton);
  }

  return should_count;
}

void ScrollableShelfView::EnableShelfRoundedCorners(bool enable) {
  // Only enable shelf rounded corners in tablet mode. Note that we allow
  // disabling rounded corners in clamshell. Because when switching to clamshell
  // from tablet, this method may be called after tablet mode ends.
  if (enable && !display::Screen::GetScreen()->InTabletMode()) {
    return;
  }

  ui::Layer* layer = shelf_container_view_->layer();

  const bool has_rounded_corners = !(layer->rounded_corner_radii().IsEmpty());
  if (enable == has_rounded_corners)
    return;

  // In non-overflow mode, only apply layer clip on |shelf_container_view_|
  // when the ripple ring of the first/last shelf icon shows.
  // Note that |layout_strategy_| may update while EnableShelfRoundedCorners()
  // is not called. For example, the first icon's context menu shows, then the
  // system notification makes shelf view enter overflow mode. So
  // |layer_clip_in_non_overflow_| updates regardless of |layout_strategy_|.
  layer_clip_in_non_overflow_ = enable;

  if (layout_strategy_ == kNotShowArrowButtons)
    EnableLayerClipOnShelfContainerView(layer_clip_in_non_overflow_);

  layer->SetRoundedCornerRadius(enable ? CalculateShelfContainerRoundedCorners()
                                       : gfx::RoundedCornersF());

  if (!layer->is_fast_rounded_corner())
    layer->SetIsFastRoundedCorner(/*enable=*/true);
}

void ScrollableShelfView::OnActiveInkDropChange(bool increase) {
  if (increase)
    ++activated_corner_buttons_;
  else
    --activated_corner_buttons_;

  // When long pressing icons, sometimes there are more ripple animations
  // pending over others buttons. Only activate rounded corners when at least
  // one button needs them.
  // NOTE: `last_tappable_app_index_` is used to compute whether a button is
  // at the corner or not. Meanwhile, `last_tappable_app_index_` could update
  // before the button fade out animation ends. As a result, in edge cases
  // `activated_corner_buttons_` could be greater than 2.
  CHECK_GE(activated_corner_buttons_, 0);
  EnableShelfRoundedCorners(activated_corner_buttons_ > 0);
}

bool ScrollableShelfView::ShouldEnableLayerClip() const {
  // Always use layer clip in overflow mode.
  if (layout_strategy_ != LayoutStrategy::kNotShowArrowButtons)
    return true;

  // In clamshell, only use layer clip in overflow mode.
  if (!display::Screen::GetScreen()->InTabletMode()) {
    return false;
  }

  // In tablet mode, whether using layer clip in non-overflow mode depends on
  // |layer_clip_in_non_overflow_|.
  return layer_clip_in_non_overflow_;
}

void ScrollableShelfView::EnableLayerClipOnShelfContainerView(bool enable) {
  if (!enable) {
    shelf_container_view_->layer()->SetClipRect(gfx::Rect());
    return;
  }

  // |visible_space_| is in local coordinates. It should be transformed into
  // |shelf_container_view_|'s coordinates for layer clip.
  gfx::RectF visible_space_in_shelf_container_coordinates(visible_space_);
  views::View::ConvertRectToTarget(
      this, shelf_container_view_,
      &visible_space_in_shelf_container_coordinates);
  shelf_container_view_->layer()->SetClipRect(
      gfx::ToEnclosedRect(visible_space_in_shelf_container_coordinates));
}

int ScrollableShelfView::CalculateShelfIconsPreferredLength() const {
  const gfx::Size shelf_preferred_size(
      shelf_container_view_->GetPreferredSize());
  const int preferred_length =
      (GetShelf()->IsHorizontalAlignment() ? shelf_preferred_size.width()
                                           : shelf_preferred_size.height());
  return preferred_length + 2 * ShelfConfig::Get()->GetAppIconEndPadding();
}

BEGIN_METADATA(ScrollableShelfView)
END_METADATA

}  // namespace ash