910e62b5创建于 1月15日历史提交
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/scroll_view.h"

#include <algorithm>
#include <utility>

#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_variant.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/overscroll/scroll_input_handler.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/metadata/type_conversion.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace views {

namespace {

// Returns the combined scroll amount given separate x and y offsets. This is
// used in the "treat all scroll events as horizontal" case when there is both
// an x and y offset and we do not want them to add in unintuitive ways.
//
// The current approach is to return whichever offset has the larger absolute
// value, which should at least handle the case in which the gesture is mostly
// vertical or horizontal. It does mean that for a gesture at 135° or 315° from
// the x axis there is a breakpoint where scroll direction reverses, but we do
// not typically expect users to try to scroll a horizontal-scroll-only view at
// this exact angle.
template <class T>
T CombineScrollOffsets(T x, T y) {
  return std::abs(x) >= std::abs(y) ? x : y;
}

class ScrollCornerView : public View {
  METADATA_HEADER(ScrollCornerView, View)

 public:
  ScrollCornerView() = default;
  ScrollCornerView(const ScrollCornerView&) = delete;
  ScrollCornerView& operator=(const ScrollCornerView&) = delete;

  void OnPaint(gfx::Canvas* canvas) override {
#if BUILDFLAG(IS_APPLE)
    ui::NativeTheme::ExtraParams params(
        std::in_place_type<ui::NativeTheme::ScrollbarExtraParams>);
#else
    ui::NativeTheme::ExtraParams params(
        std::in_place_type<ui::NativeTheme::ScrollbarTrackExtraParams>);
#endif
    GetNativeTheme()->Paint(canvas->sk_canvas(), GetColorProvider(),
                            ui::NativeTheme::kScrollbarCorner,
                            ui::NativeTheme::kNormal, GetLocalBounds(), params);
  }
};

BEGIN_METADATA(ScrollCornerView)
END_METADATA

// Returns true if any descendants of |view| have a layer (not including
// |view|).
bool DoesDescendantHaveLayer(View* view) {
  return std::ranges::any_of(view->children(), [](View* child) {
    return child->layer() || DoesDescendantHaveLayer(child);
  });
}

// Returns the position for the view so that it isn't scrolled off the visible
// region.
int CheckScrollBounds(int viewport_size, int content_size, int current_pos) {
  return std::clamp(current_pos, 0, std::max(content_size - viewport_size, 0));
}

// Make sure the content is not scrolled out of bounds
void ConstrainScrollToBounds(View* viewport,
                             View* view,
                             bool scroll_with_layers_enabled) {
  if (!view) {
    return;
  }

  // Note that even when ScrollView::ScrollsWithLayers() is true, the header row
  // scrolls by repainting.
  const bool scrolls_with_layers =
      scroll_with_layers_enabled && viewport->layer() != nullptr;
  if (scrolls_with_layers) {
    DCHECK(view->layer());
    DCHECK_EQ(0, view->x());
    DCHECK_EQ(0, view->y());
  }
  gfx::PointF offset = scrolls_with_layers
                           ? view->layer()->CurrentScrollOffset()
                           : gfx::PointF(-view->x(), -view->y());

  int x = CheckScrollBounds(viewport->width(), view->width(), offset.x());
  int y = CheckScrollBounds(viewport->height(), view->height(), offset.y());

  if (scrolls_with_layers) {
    view->layer()->SetScrollOffset(gfx::PointF(x, y));
  } else {
    // This is no op if bounds are the same
    view->SetBounds(-x, -y, view->width(), view->height());
  }
}

// Used by ScrollToPosition() to make sure the new position fits within the
// allowed scroll range.
int AdjustPosition(int current_position,
                   int new_position,
                   int content_size,
                   int viewport_size) {
  if (-current_position == new_position) {
    return new_position;
  }
  if (new_position < 0) {
    return 0;
  }
  const int max_position = std::max(0, content_size - viewport_size);
  return (new_position > max_position) ? max_position : new_position;
}

}  // namespace

// Viewport contains the contents View of the ScrollView.
class ScrollView::Viewport : public View {
  METADATA_HEADER(Viewport, View)

 public:
  explicit Viewport(ScrollView* scroll_view) : scroll_view_(scroll_view) {}
  Viewport(const Viewport&) = delete;
  Viewport& operator=(const Viewport&) = delete;
  ~Viewport() override = default;

  void ScrollRectToVisible(const gfx::Rect& rect) override {
    if (children().empty() || !parent()) {
      return;
    }

    // If scrolling is disabled, it may have been handled by a parent View class
    // so fall back to it.
    if (!scroll_view_->IsHorizontalScrollEnabled() &&
        !scroll_view_->IsVerticalScrollEnabled()) {
      View::ScrollRectToVisible(rect);
      return;
    }

    View* contents = children().front();
    gfx::Rect scroll_rect(rect);

    if (scroll_view_->ScrollsWithLayers()) {
      // With layer scrolling, there's no need to "undo" the offset done in the
      // child's View::ScrollRectToVisible() before it calls this.
      DCHECK_EQ(0, contents->x());
      DCHECK_EQ(0, contents->y());
    } else {
      scroll_rect.Offset(-contents->x(), -contents->y());
    }

    scroll_view_->ScrollContentsRegionToBeVisible(scroll_rect);
  }

  void ViewHierarchyChanged(
      const ViewHierarchyChangedDetails& details) override {
    if (details.is_add && GetIsContentsViewport() && Contains(details.parent)) {
      scroll_view_->UpdateViewportLayerForClipping();
      UpdateContentsViewportLayer();
    }
  }

  void OnChildLayerChanged(View* child) override {
    // If scroll_with_layers is enabled, explicitly disallowing to change the
    // layer on contents after the contents of ScrollView are set.
    DCHECK(!scroll_view_->scroll_with_layers_enabled_ ||
           child != scroll_view_->contents_)
        << "Layer of contents cannot be changed manually after the contents "
           "are set when scroll_with_layers is enabled.";

    if (GetIsContentsViewport()) {
      scroll_view_->UpdateViewportLayerForClipping();
      UpdateContentsViewportLayer();
    }
  }

  void InitializeContentsViewportLayer() {
    const ui::LayerType layer_type = CalculateLayerTypeForContentsViewport();
    SetContentsViewportLayer(layer_type);
  }

 private:
  void UpdateContentsViewportLayer() {
    if (!layer()) {
      return;
    }

    const ui::LayerType new_layer_type =
        CalculateLayerTypeForContentsViewport();

    bool layer_needs_update = layer()->type() != new_layer_type;
    if (layer_needs_update) {
      SetContentsViewportLayer(new_layer_type);
    }
  }

  // Calculates the layer type to use for |contents_viewport_|.
  ui::LayerType CalculateLayerTypeForContentsViewport() const {
    // Since contents_viewport_ is transparent, layer of contents_viewport_
    // can be NOT_DRAWN if contents_ have a TEXTURED layer.

    // When scroll_with_layers is enabled, we can always determine the layer
    // type of contents_viewport based on the type of layer that will be enabled
    // on contents.
    if (scroll_view_->scroll_with_layers_enabled_) {
      return scroll_view_->layer_type_ == ui::LAYER_TEXTURED
                 ? ui::LAYER_NOT_DRAWN
                 : ui::LAYER_TEXTURED;
    }

    // Getting contents of viewport through view hierarchy tree rather than
    // scroll_view->contents_, as this method can be called after the view
    // hierarchy is changed but before contents_ variable is updated. Hence
    // scroll_view->contents_ will have stale value in such situation.
    const View* contents =
        !this->children().empty() ? this->children()[0] : nullptr;

    auto has_textured_layer{[](const View* contents) {
      return contents->layer() &&
             contents->layer()->type() == ui::LAYER_TEXTURED;
    }};

    if (!contents || has_textured_layer(contents)) {
      return ui::LAYER_NOT_DRAWN;
    } else {
      return ui::LAYER_TEXTURED;
    }
  }

  // Initializes or updates the layer of |contents_viewport|.
  void SetContentsViewportLayer(ui::LayerType layer_type) {
    // Only LAYER_NOT_DRAWN and LAYER_TEXTURED are allowed since
    // contents_viewport is a container view.
    DCHECK(layer_type == ui::LAYER_TEXTURED ||
           layer_type == ui::LAYER_NOT_DRAWN);

    SetPaintToLayer(layer_type);
  }

  bool GetIsContentsViewport() const {
    return parent() && scroll_view_->contents_viewport_ == this;
  }

  raw_ptr<ScrollView> scroll_view_;
};

BEGIN_METADATA(ScrollView, Viewport)
ADD_READONLY_PROPERTY_METADATA(bool, IsContentsViewport)
END_METADATA

ScrollView::ScrollView()
    : ScrollView(base::FeatureList::IsEnabled(
                     ::features::kUiCompositorScrollWithLayers)
                     ? ScrollWithLayers::kEnabled
                     : ScrollWithLayers::kDisabled) {}

ScrollView::ScrollView(ScrollWithLayers scroll_with_layers)
    : horiz_sb_(AddChildView(
          PlatformStyle::CreateScrollBar(ScrollBar::Orientation::kHorizontal))),
      vert_sb_(AddChildView(
          PlatformStyle::CreateScrollBar(ScrollBar::Orientation::kVertical))),
      corner_view_(std::make_unique<ScrollCornerView>()),
      scroll_with_layers_enabled_(scroll_with_layers ==
                                  ScrollWithLayers::kEnabled) {
  SetNotifyEnterExitOnChild(true);

  // Since |contents_viewport_| is accessed during the AddChildView call, make
  // sure the field is initialized.
  auto contents_viewport = std::make_unique<Viewport>(this);
  contents_viewport_ = contents_viewport.get();
  // Add content view port as the first child, so that the scollbars can
  // overlay it.
  AddChildViewAt(std::move(contents_viewport), 0);
  header_viewport_ = AddChildView(std::make_unique<Viewport>(this));

  horiz_sb_->SetVisible(false);
  horiz_sb_->set_controller(this);
  vert_sb_->SetVisible(false);
  vert_sb_->set_controller(this);
  corner_view_->SetVisible(false);

  // "Ignored" removes the scrollbar from the accessibility tree.
  // "IsLeaf" removes their children (e.g. the buttons and thumb).
  horiz_sb_->GetViewAccessibility().SetIsIgnored(true);
  horiz_sb_->GetViewAccessibility().SetIsLeaf(true);
  vert_sb_->GetViewAccessibility().SetIsIgnored(true);
  vert_sb_->GetViewAccessibility().SetIsLeaf(true);

  GetViewAccessibility().SetIsScrollable(true);
  GetViewAccessibility().SetScrollXMin(horiz_sb_->GetMinPosition());
  GetViewAccessibility().SetScrollXMax(horiz_sb_->GetMaxPosition());
  GetViewAccessibility().SetScrollYMin(vert_sb_->GetMinPosition());
  GetViewAccessibility().SetScrollYMax(vert_sb_->GetMaxPosition());
  GetViewAccessibility().SetRole(ax::mojom::Role::kScrollView);

  // Just make sure the more_content indicators aren't visible for now. They'll
  // be added as child controls and appropriately made visible depending on
  // |show_edges_with_hidden_content_|.
  more_content_left_->SetVisible(false);
  more_content_top_->SetVisible(false);
  more_content_right_->SetVisible(false);
  more_content_bottom_->SetVisible(false);

  if (scroll_with_layers_enabled_) {
    EnableViewportLayer();
  }

  // If we're scrolling with layers, paint the overflow indicators to the layer.
  if (ScrollsWithLayers()) {
    more_content_left_->SetPaintToLayer();
    more_content_top_->SetPaintToLayer();
    more_content_right_->SetPaintToLayer();
    more_content_bottom_->SetPaintToLayer();
  }

  FocusRing::Install(this);
  views::FocusRing::Get(this)->SetHasFocusPredicate(
      base::BindRepeating([](const View* view) {
        const auto* v = views::AsViewClass<ScrollView>(view);
        CHECK(v);
        return v->draw_focus_indicator_;
      }));
}

ScrollView::~ScrollView() = default;

// static
std::unique_ptr<ScrollView> ScrollView::CreateScrollViewWithBorder() {
  auto scroll_view = std::make_unique<ScrollView>();
  scroll_view->AddBorder();
  return scroll_view;
}

// static
ScrollView* ScrollView::GetScrollViewForContents(View* contents) {
  View* grandparent =
      contents->parent() ? contents->parent()->parent() : nullptr;
  if (!grandparent || !IsViewClass<ScrollView>(grandparent)) {
    return nullptr;
  }

  auto* scroll_view = static_cast<ScrollView*>(grandparent);
  DCHECK_EQ(contents, scroll_view->contents());
  return scroll_view;
}

void ScrollView::SetContentsImpl(std::unique_ptr<View> a_view) {
  // Protect against clients passing a contents view that has its own Layer.
  DCHECK(!a_view || !a_view->layer());

  if (a_view && ScrollsWithLayers()) {
    a_view->SetPaintToLayer(layer_type_);
    a_view->layer()->SetDidScrollCallback(base::BindRepeating(
        &ScrollView::OnLayerScrolled, base::Unretained(this)));
    a_view->layer()->SetScrollable(contents_viewport_->bounds().size());
  }
  contents_ = ReplaceChildView(
      contents_viewport_, contents_.ExtractAsDangling(), std::move(a_view));
  UpdateBackground();
}

void ScrollView::SetContents(std::nullptr_t) {
  SetContentsImpl(nullptr);
}

void ScrollView::SetContentsLayerType(ui::LayerType layer_type) {
  // This function should only be called when scroll with layers is enabled and
  // before `contents_` is set.
  DCHECK(ScrollsWithLayers());
  DCHECK(!contents_);

  // Currently only allow LAYER_TEXTURED and LAYER_NOT_DRAWN. If other types of
  // layer are needed, consult with the owner.
  DCHECK(layer_type == ui::LAYER_TEXTURED || layer_type == ui::LAYER_NOT_DRAWN);

  if (layer_type_ == layer_type) {
    return;
  }

  layer_type_ = layer_type;
}

void ScrollView::SetHeaderImpl(std::unique_ptr<View> a_header) {
  header_ = ReplaceChildView(header_viewport_, header_.ExtractAsDangling(),
                             std::move(a_header));
}

void ScrollView::SetHeader(std::nullptr_t) {
  SetHeaderImpl(nullptr);
}

void ScrollView::SetPreferredViewportMargins(const gfx::Insets& margins) {
  preferred_viewport_margins_ = margins;
}

void ScrollView::SetViewportRoundedCornerRadius(
    const gfx::RoundedCornersF& radii) {
  DCHECK(contents_viewport_->layer())
      << "Please ensure you have enabled ScrollWithLayers.";

  contents_viewport_->layer()->SetRoundedCornerRadius(radii);
}

void ScrollView::SetBackgroundColor(
    const std::optional<ui::ColorVariant>& color) {
  if (background_color_ == color) {
    return;
  }
  background_color_ = color;
  UpdateBackground();
  OnPropertyChanged(&background_color_, PropertyEffects::kPaint);
}

gfx::Rect ScrollView::GetVisibleRect() const {
  if (!contents_) {
    return gfx::Rect();
  }
  gfx::PointF offset = CurrentOffset();
  return gfx::Rect(offset.x(), offset.y(), contents_viewport_->width(),
                   contents_viewport_->height());
}

void ScrollView::SetHorizontalScrollBarMode(
    ScrollBarMode horizontal_scroll_bar_mode) {
  if (horizontal_scroll_bar_mode_ == horizontal_scroll_bar_mode) {
    return;
  }
  horizontal_scroll_bar_mode_ = horizontal_scroll_bar_mode;
  OnPropertyChanged(&horizontal_scroll_bar_mode_, PropertyEffects::kPaint);

  // "Ignored" removes the scrollbar from the accessibility tree.
  // "IsLeaf" removes their children (e.g. the buttons and thumb).
  bool is_disabled = horizontal_scroll_bar_mode == ScrollBarMode::kDisabled;
  horiz_sb_->GetViewAccessibility().SetIsIgnored(is_disabled);
  horiz_sb_->GetViewAccessibility().SetIsLeaf(is_disabled);
}

void ScrollView::SetVerticalScrollBarMode(
    ScrollBarMode vertical_scroll_bar_mode) {
  if (vertical_scroll_bar_mode_ == vertical_scroll_bar_mode) {
    return;
  }

  // Enabling vertical scrolling is incompatible with all scrolling being
  // interpreted as horizontal.
  DCHECK(!treat_all_scroll_events_as_horizontal_ ||
         vertical_scroll_bar_mode == ScrollBarMode::kDisabled);

  vertical_scroll_bar_mode_ = vertical_scroll_bar_mode;
  OnPropertyChanged(&vertical_scroll_bar_mode_, PropertyEffects::kPaint);

  // "Ignored" removes the scrollbar from the accessibility tree.
  // "IsLeaf" removes their children (e.g. the buttons and thumb).
  bool is_disabled = vertical_scroll_bar_mode == ScrollBarMode::kDisabled;
  vert_sb_->GetViewAccessibility().SetIsIgnored(is_disabled);
  vert_sb_->GetViewAccessibility().SetIsLeaf(is_disabled);
}

void ScrollView::SetTreatAllScrollEventsAsHorizontal(
    bool treat_all_scroll_events_as_horizontal) {
  if (treat_all_scroll_events_as_horizontal_ ==
      treat_all_scroll_events_as_horizontal) {
    return;
  }
  treat_all_scroll_events_as_horizontal_ =
      treat_all_scroll_events_as_horizontal;
  OnPropertyChanged(&treat_all_scroll_events_as_horizontal_,
                    PropertyEffects::kNone);

  // Since this effectively disables vertical scrolling, don't show a
  // vertical scrollbar.
  SetVerticalScrollBarMode(ScrollBarMode::kDisabled);
}

void ScrollView::SetAllowKeyboardScrolling(bool allow_keyboard_scrolling) {
  if (allow_keyboard_scrolling_ == allow_keyboard_scrolling) {
    return;
  }
  allow_keyboard_scrolling_ = allow_keyboard_scrolling;
  OnPropertyChanged(&allow_keyboard_scrolling_, PropertyEffects::kNone);
}

void ScrollView::SetDrawOverflowIndicator(bool draw_overflow_indicator) {
  if (draw_overflow_indicator_ == draw_overflow_indicator) {
    return;
  }
  draw_overflow_indicator_ = draw_overflow_indicator;
  OnPropertyChanged(&draw_overflow_indicator_, PropertyEffects::kPaint);
}

View* ScrollView::SetCustomOverflowIndicator(OverflowIndicatorAlignment side,
                                             std::unique_ptr<View> indicator,
                                             int thickness,
                                             bool fills_opaquely) {
  if (thickness < 0) {
    thickness = 0;
  }

  if (ScrollsWithLayers()) {
    indicator->SetPaintToLayer();
    indicator->layer()->SetFillsBoundsOpaquely(fills_opaquely);
  }

  View* indicator_ptr = indicator.get();
  switch (side) {
    case OverflowIndicatorAlignment::kLeft:
      more_content_left_ = std::move(indicator);
      more_content_left_thickness_ = thickness;
      break;
    case OverflowIndicatorAlignment::kTop:
      more_content_top_ = std::move(indicator);
      more_content_top_thickness_ = thickness;
      break;
    case OverflowIndicatorAlignment::kRight:
      more_content_right_ = std::move(indicator);
      more_content_right_thickness_ = thickness;
      break;
    case OverflowIndicatorAlignment::kBottom:
      more_content_bottom_ = std::move(indicator);
      more_content_bottom_thickness_ = thickness;
      break;
    default:
      NOTREACHED();
  }

  UpdateOverflowIndicatorVisibility(CurrentOffset());
  PositionOverflowIndicators();

  return indicator_ptr;
}

void ScrollView::ClipHeightTo(int min_height, int max_height) {
  if (min_height != min_height_ || max_height != max_height_) {
    PreferredSizeChanged();
  }

  min_height_ = min_height;
  max_height_ = max_height;
}

int ScrollView::GetScrollBarLayoutWidth() const {
  return vert_sb_->OverlapsContent() ? 0 : vert_sb_->GetThickness();
}

int ScrollView::GetScrollBarLayoutHeight() const {
  return horiz_sb_->OverlapsContent() ? 0 : horiz_sb_->GetThickness();
}

ScrollBar* ScrollView::SetHorizontalScrollBar(
    std::unique_ptr<ScrollBar> horiz_sb) {
  horiz_sb->SetVisible(horiz_sb_->GetVisible());
  horiz_sb->set_controller(this);
  RemoveChildViewT(horiz_sb_.ExtractAsDangling());
  horiz_sb_ = AddChildView(std::move(horiz_sb));
  GetViewAccessibility().SetScrollXMin(horiz_sb_->GetMinPosition());
  GetViewAccessibility().SetScrollXMax(horiz_sb_->GetMaxPosition());
  return horiz_sb_;
}

ScrollBar* ScrollView::SetVerticalScrollBar(
    std::unique_ptr<ScrollBar> vert_sb) {
  DCHECK(vert_sb);
  vert_sb->SetVisible(vert_sb_->GetVisible());
  vert_sb->set_controller(this);
  RemoveChildViewT(vert_sb_.ExtractAsDangling());
  vert_sb_ = AddChildView(std::move(vert_sb));
  GetViewAccessibility().SetScrollYMin(vert_sb_->GetMinPosition());
  GetViewAccessibility().SetScrollYMax(vert_sb_->GetMaxPosition());
  return vert_sb_;
}

void ScrollView::SetHasFocusIndicator(bool has_focus_indicator) {
  if (has_focus_indicator == draw_focus_indicator_) {
    return;
  }
  draw_focus_indicator_ = has_focus_indicator;

  views::FocusRing::Get(this)->SchedulePaint();
  SchedulePaint();
  OnPropertyChanged(&draw_focus_indicator_, PropertyEffects::kPaint);
}

base::CallbackListSubscription ScrollView::AddContentsScrolledCallback(
    ScrollViewCallback callback) {
  return on_contents_scrolled_.Add(std::move(callback));
}

base::CallbackListSubscription ScrollView::AddContentsScrollEndedCallback(
    ScrollViewCallback callback) {
  return on_contents_scroll_ended_.Add(std::move(callback));
}

gfx::Size ScrollView::CalculatePreferredSize(
    const SizeBounds& available_size) const {
  gfx::Insets insets = GetInsets();
  gfx::Size size =
      contents_ ? contents_->GetPreferredSize(available_size.Inset(insets))
                : gfx::Size();
  size.Enlarge(insets.width(), insets.height());

  if (is_bounded()) {
    size.SetToMax(gfx::Size(size.width(), min_height_));
    size.SetToMin(gfx::Size(size.width(), max_height_));
  }
  return size;
}

void ScrollView::Layout(PassKey) {
  // When either scrollbar is disabled, it should not matter
  // if its OverlapsContent matches other bar's.
  if (horizontal_scroll_bar_mode_ == ScrollBarMode::kEnabled &&
      vertical_scroll_bar_mode_ == ScrollBarMode::kEnabled) {
#if BUILDFLAG(IS_MAC)
    // On Mac, scrollbars may update their style one at a time, so they may
    // temporarily be of different types. Refuse to lay out at this point.
    if (horiz_sb_->OverlapsContent() != vert_sb_->OverlapsContent()) {
      return;
    }
#endif
    DCHECK_EQ(horiz_sb_->OverlapsContent(), vert_sb_->OverlapsContent());
  }

  if (views::FocusRing::Get(this)) {
    views::FocusRing::Get(this)->DeprecatedLayoutImmediately();
  }

  gfx::Rect available_rect = GetContentsBounds();
  views::SizeBounds available_size(available_rect.size());
  if (is_bounded() && contents_) {
    int content_width = available_rect.width();
    int content_height = contents_->GetHeightForWidth(content_width);
    if (content_height > available_rect.height()) {
      content_width = std::max(content_width - GetScrollBarLayoutWidth(), 0);
      content_height = contents_->GetHeightForWidth(content_width);
    }
    contents_->SetSize(gfx::Size(content_width, content_height));
  }

  // Place an overflow indicator on each of the four edges of the content
  // bounds.
  PositionOverflowIndicators();

  // Most views will want to auto-fit the available space. Most of them want to
  // use all available width (without overflowing) and only overflow in
  // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc.
  // Other views want to fit in both ways. An example is PrintView. To make both
  // happy, assume a vertical scrollbar but no horizontal scrollbar. To override
  // this default behavior, the inner view has to calculate the available space,
  // used ComputeScrollBarsVisibility() to use the same calculation that is done
  // here and sets its bound to fit within.
  gfx::Rect viewport_bounds = available_rect;
  const int contents_x = viewport_bounds.x();
  const int contents_y = viewport_bounds.y();
  if (viewport_bounds.IsEmpty()) {
    // There's nothing to layout.
    return;
  }

  const int header_height =
      std::min(viewport_bounds.height(),
               header_ ? header_->GetPreferredSize({}).height() : 0);
  viewport_bounds.set_height(
      std::max(0, viewport_bounds.height() - header_height));
  viewport_bounds.set_y(viewport_bounds.y() + header_height);
  // viewport_size is the total client space available.
  gfx::Size viewport_size = viewport_bounds.size();

  // Assume both a vertical and horizontal scrollbar exist before calling
  // contents_->DeprecatedLayoutImmediately(). This is because some contents_
  // will set their own size to the contents_viewport_'s bounds. Failing to
  // pre-allocate space for the scrollbars will [non-intuitively] cause
  // scrollbars to appear in ComputeScrollBarsVisibility. This solution is also
  // not perfect - if scrollbars turn out *not* to be necessary, the contents
  // will have slightly less horizontal/vertical space than it otherwise would
  // have had access to. Unfortunately, there's no way to determine this without
  // introducing a circular dependency.
  const int horiz_sb_layout_height = GetScrollBarLayoutHeight();
  const int vert_sb_layout_width = GetScrollBarLayoutWidth();
  viewport_bounds.set_width(viewport_bounds.width() - vert_sb_layout_width);
  viewport_bounds.set_height(viewport_bounds.height() - horiz_sb_layout_height);

  // Update the bounds right now so the inner views can fit in it.
  contents_viewport_->SetBoundsRect(viewport_bounds);

  // Give |contents_| a chance to update its bounds if it depends on the
  // viewport.
  if (contents_) {
    contents_->DeprecatedLayoutImmediately();
  }

  bool should_layout_contents = false;
  bool horiz_sb_required = false;
  bool vert_sb_required = false;
  if (contents_) {
    gfx::Size content_size = contents_->size();
    if (use_contents_preferred_size_ &&
        !contents_->GetPreferredSize(available_size).IsEmpty()) {
      content_size = contents_->GetPreferredSize(available_size);
    }
    ComputeScrollBarsVisibility(viewport_size, content_size, &horiz_sb_required,
                                &vert_sb_required);
  }
  // Overlay scrollbars don't need a corner view.
  bool corner_view_required =
      horiz_sb_required && vert_sb_required && !vert_sb_->OverlapsContent();
  // Take action.
  horiz_sb_->SetVisible(horiz_sb_required);
  vert_sb_->SetVisible(vert_sb_required);
  SetControlVisibility(corner_view_.get(), corner_view_required);

  // Default.
  if (!horiz_sb_required) {
    viewport_bounds.set_height(viewport_bounds.height() +
                               horiz_sb_layout_height);
    should_layout_contents = true;
  }
  // Default.
  if (!vert_sb_required) {
    viewport_bounds.set_width(viewport_bounds.width() + vert_sb_layout_width);
    should_layout_contents = true;
  }

  if (horiz_sb_required) {
    gfx::Rect horiz_sb_bounds(contents_x, viewport_bounds.bottom(),
                              viewport_bounds.right() - contents_x,
                              horiz_sb_layout_height);
    if (horiz_sb_->OverlapsContent()) {
      horiz_sb_bounds.Inset(
          gfx::Insets::TLBR(-horiz_sb_->GetThickness(), 0, 0,
                            vert_sb_required ? vert_sb_->GetThickness() : 0));
    }

    horiz_sb_->SetBoundsRect(horiz_sb_bounds);
  }
  if (vert_sb_required) {
    gfx::Rect vert_sb_bounds(viewport_bounds.right(), contents_y,
                             vert_sb_layout_width,
                             viewport_bounds.bottom() - contents_y);
    if (vert_sb_->OverlapsContent()) {
      // In the overlay scrollbar case, the scrollbar only covers the viewport
      // (and not the header).
      vert_sb_bounds.Inset(gfx::Insets::TLBR(
          header_height, -vert_sb_->GetThickness(),
          horiz_sb_required ? horiz_sb_->GetThickness() : 0, 0));
    }

    vert_sb_->SetBoundsRect(vert_sb_bounds);
  }
  if (corner_view_required) {
    // Show the resize corner.
    corner_view_->SetBounds(vert_sb_->bounds().x(), horiz_sb_->bounds().y(),
                            vert_sb_layout_width, horiz_sb_layout_height);
  }

  // Update to the real client size with the visible scrollbars.
  contents_viewport_->SetBoundsRect(viewport_bounds);
  if (should_layout_contents && contents_) {
    contents_->DeprecatedLayoutImmediately();
  }

  // Even when |contents_| needs to scroll, it can still be narrower or wider
  // the viewport. So ensure the scrolling layer can fill the viewport, so that
  // events will correctly hit it, and overscroll looks correct.
  if (contents_ && ScrollsWithLayers()) {
    gfx::Size container_size = contents_ ? contents_->size() : gfx::Size();
    if (contents_ && use_contents_preferred_size_ &&
        !contents_->GetPreferredSize(available_size).IsEmpty()) {
      container_size = contents_->GetPreferredSize(available_size);
    }
    container_size.SetToMax(viewport_bounds.size());
    contents_->SetBoundsRect(gfx::Rect(container_size));
    contents_->layer()->SetScrollable(viewport_bounds.size());

    // Flip the viewport with layer transforms under RTL. Note the net effect is
    // to flip twice, so the text is not mirrored. This is necessary because
    // compositor scrolling is not RTL-aware. So although a toolkit-views layout
    // will flip, increasing a horizontal scroll offset will move content to
    // the left, regardless of RTL. A scroll offset must be positive, so to
    // move (unscrolled) content to the right, we need to flip the viewport
    // layer. That would flip all the content as well, so flip (and translate)
    // the content layer. Compensating in this way allows the scrolling/offset
    // logic to remain the same when scrolling via layers or bounds offsets.
    if (base::i18n::IsRTL()) {
      gfx::Transform flip;
      flip.Translate(viewport_bounds.width(), 0);
      flip.Scale(-1, 1);
      contents_viewport_->layer()->SetTransform(flip);

      // Add `contents_->width() - viewport_width` to the translation step. This
      // is to prevent the top-left of the (flipped) contents aligning to the
      // top-left of the viewport. Instead, the top-right should align in RTL.
      gfx::Transform shift;
      shift.Translate(2 * contents_->width() - viewport_bounds.width(), 0);
      shift.Scale(-1, 1);
      contents_->layer()->SetTransform(shift);
    }
  }

  header_viewport_->SetBounds(contents_x, contents_y, viewport_bounds.width(),
                              header_height);
  if (header_) {
    header_->DeprecatedLayoutImmediately();
  }

  ConstrainScrollToBounds(header_viewport_, header_,
                          scroll_with_layers_enabled_);
  ConstrainScrollToBounds(contents_viewport_, contents_,
                          scroll_with_layers_enabled_);
  SchedulePaint();
  UpdateScrollBarPositions();
  if (contents_) {
    UpdateOverflowIndicatorVisibility(CurrentOffset());
  }

  // If registered, run the post-layout callback. This is used to move the
  // scroll view contents to the appropriate position that's different from the
  // position assigned above.
  if (post_layout_callback_) {
    const bool layout_needed = needs_layout();
    post_layout_callback_.Run(this);
    CHECK_EQ(layout_needed, needs_layout());
  }
}

bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) {
  bool processed = false;

  if (!allow_keyboard_scrolling_) {
    return false;
  }

  // Give vertical scrollbar priority
  if (IsVerticalScrollEnabled()) {
    processed = vert_sb_->OnKeyPressed(event);
  }

  if (!processed && IsHorizontalScrollEnabled()) {
    processed = horiz_sb_->OnKeyPressed(event);
  }

  return processed;
}

bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) {
  bool processed = false;

  const ui::MouseWheelEvent to_propagate =
      treat_all_scroll_events_as_horizontal_
          ? ui::MouseWheelEvent(
                e, CombineScrollOffsets(e.x_offset(), e.y_offset()), 0)
          : e;

  // TODO(crbug.com/40471184): Use composited scrolling.
  if (IsVerticalScrollEnabled()) {
    processed = vert_sb_->OnMouseWheel(to_propagate);
  }

  if (IsHorizontalScrollEnabled()) {
    // When there is no vertical scrollbar, allow vertical scroll events to be
    // interpreted as horizontal scroll events.
    processed |= horiz_sb_->OnMouseWheel(to_propagate);
  }

  return processed;
}

void ScrollView::OnScrollEvent(ui::ScrollEvent* event) {
  if (!contents_) {
    return;
  }

  // Possibly force the scroll event to horizontal based on the configuration
  // option.
  ui::ScrollEvent e =
      treat_all_scroll_events_as_horizontal_
          ? ui::ScrollEvent(
                event->type(), event->location_f(), event->root_location_f(),
                event->time_stamp(), event->flags(),
                CombineScrollOffsets(event->x_offset(), event->y_offset()),
                0.0f,
                CombineScrollOffsets(event->y_offset_ordinal(),
                                     event->x_offset_ordinal()),
                0.0f, event->finger_count(), event->momentum_phase(),
                event->scroll_event_phase())
          : *event;

  ui::ScrollInputHandler* compositor_scroller =
      GetWidget()->GetCompositor()->scroll_input_handler();
  if (compositor_scroller) {
    DCHECK(scroll_with_layers_enabled_);
    if (compositor_scroller->OnScrollEvent(e, contents_->layer())) {
      e.SetHandled();
      e.StopPropagation();
    }
  }

  // A direction might not be known when the event stream starts, notify both
  // scrollbars that they may be about scroll, or that they may need to cancel
  // UI feedback once the scrolling direction is known.
  horiz_sb_->ObserveScrollEvent(e);
  vert_sb_->ObserveScrollEvent(e);

  // Need to copy state back to original event.
  if (e.handled()) {
    event->SetHandled();
  }
  if (e.stopped_propagation()) {
    event->StopPropagation();
  }
}

void ScrollView::OnGestureEvent(ui::GestureEvent* event) {
  // If the event happened on one of the scrollbars, then those events are
  // sent directly to the scrollbars. Otherwise, only scroll events are sent to
  // the scrollbars.
  bool scroll_event = event->type() == ui::EventType::kGestureScrollUpdate ||
                      event->type() == ui::EventType::kGestureScrollBegin ||
                      event->type() == ui::EventType::kGestureScrollEnd ||
                      event->type() == ui::EventType::kScrollFlingStart;

  // Note: we will not invert gesture events because it will be confusing to
  // have a vertical finger gesture on a touchscreen cause the scroll pane to
  // scroll horizontally.

  // TODO(crbug.com/40471184): Use composited scrolling.
  if (IsVerticalScrollEnabled() &&
      (scroll_event || (vert_sb_->GetVisible() &&
                        vert_sb_->bounds().Contains(event->location())))) {
    vert_sb_->OnGestureEvent(event);
  }
  if (!event->handled() && IsHorizontalScrollEnabled() &&
      (scroll_event || (horiz_sb_->GetVisible() &&
                        horiz_sb_->bounds().Contains(event->location())))) {
    horiz_sb_->OnGestureEvent(event);
  }
}

void ScrollView::OnThemeChanged() {
  View::OnThemeChanged();
  UpdateBorder();
  UpdateBackground();
}

bool ScrollView::HandleAccessibleAction(const ui::AXActionData& action_data) {
  if (!contents_) {
    return View::HandleAccessibleAction(action_data);
  }

  switch (action_data.action) {
    case ax::mojom::Action::kScrollLeft:
      return horiz_sb_->ScrollByAmount(ScrollBar::ScrollAmount::kPrevPage);
    case ax::mojom::Action::kScrollRight:
      return horiz_sb_->ScrollByAmount(ScrollBar::ScrollAmount::kNextPage);
    case ax::mojom::Action::kScrollUp:
      return vert_sb_->ScrollByAmount(ScrollBar::ScrollAmount::kPrevPage);
    case ax::mojom::Action::kScrollDown:
      return vert_sb_->ScrollByAmount(ScrollBar::ScrollAmount::kNextPage);
    case ax::mojom::Action::kSetScrollOffset:
      ScrollToOffset(gfx::PointF(action_data.target_point));
      return true;
    default:
      return View::HandleAccessibleAction(action_data);
  }
}

void ScrollView::ScrollToPosition(ScrollBar* source, int position) {
  if (!contents_) {
    return;
  }

  gfx::PointF offset = CurrentOffset();
  if (source == horiz_sb_ && IsHorizontalScrollEnabled()) {
    position = AdjustPosition(offset.x(), position, contents_->width(),
                              contents_viewport_->width());
    if (offset.x() == position) {
      return;
    }
    offset.set_x(position);
  } else if (source == vert_sb_ && IsVerticalScrollEnabled()) {
    position = AdjustPosition(offset.y(), position, contents_->height(),
                              contents_viewport_->height());
    if (offset.y() == position) {
      return;
    }
    offset.set_y(position);
  }
  ScrollToOffset(offset);

  if (!ScrollsWithLayers()) {
    contents_->SchedulePaintInRect(contents_->GetVisibleBounds());
  }
}

int ScrollView::GetScrollIncrement(ScrollBar* source,
                                   bool is_page,
                                   bool is_positive) {
  bool is_horizontal =
      source->GetOrientation() == ScrollBar::Orientation::kHorizontal;
  if (is_page) {
    return is_horizontal ? contents_viewport_->width()
                         : contents_viewport_->height();
  }
  return is_horizontal ? contents_viewport_->width() / 5
                       : contents_viewport_->height() / 5;
}

void ScrollView::OnScrollEnded() {
  on_contents_scroll_ended_.Notify();
}

bool ScrollView::DoesViewportOrScrollViewHaveLayer() const {
  return layer() || contents_viewport_->layer();
}

void ScrollView::UpdateViewportLayerForClipping() {
  if (scroll_with_layers_enabled_) {
    return;
  }

  const bool has_layer = DoesViewportOrScrollViewHaveLayer();
  const bool needs_layer = DoesDescendantHaveLayer(contents_viewport_);
  if (has_layer == needs_layer) {
    return;
  }
  if (needs_layer) {
    EnableViewportLayer();
  } else {
    contents_viewport_->DestroyLayer();
  }
}

View* ScrollView::ReplaceChildView(View* parent,
                                   raw_ptr<View>::DanglingType old_view,
                                   std::unique_ptr<View> new_view) {
  if (old_view) {
    parent->RemoveChildViewT(old_view);
  }
  View* result = nullptr;
  if (new_view.get()) {
    result = parent->AddChildViewAt(std::move(new_view), 0);
  }
  InvalidateLayout();
  return result;
}

void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) {
  if (!contents_) {
    return;
  }

  gfx::Rect contents_region = rect;
  contents_region.Inset(-preferred_viewport_margins_);

  // Figure out the maximums for this scroll view.
  const int contents_max_x =
      std::max(contents_viewport_->width(), contents_->width());
  const int contents_max_y =
      std::max(contents_viewport_->height(), contents_->height());

  int x = std::clamp(contents_region.x(), 0, contents_max_x);
  int y = std::clamp(contents_region.y(), 0, contents_max_y);

  // Figure out how far and down the rectangle will go taking width
  // and height into account.  This will be "clipped" by the viewport.
  const int max_x = std::min(
      contents_max_x,
      x + std::min(contents_region.width(), contents_viewport_->width()));
  const int max_y = std::min(
      contents_max_y,
      y + std::min(contents_region.height(), contents_viewport_->height()));

  // See if the rect is already visible. Note the width is (max_x - x)
  // and the height is (max_y - y) to take into account the clipping of
  // either viewport or the content size.
  const gfx::Rect vis_rect = GetVisibleRect();
  if (vis_rect.Contains(gfx::Rect(x, y, max_x - x, max_y - y))) {
    return;
  }

  // Shift contents_'s X and Y so that the region is visible. If we
  // need to shift up or left from where we currently are then we need
  // to get it so that the content appears in the upper/left
  // corner. This is done by setting the offset to -X or -Y.  For down
  // or right shifts we need to make sure it appears in the
  // lower/right corner. This is calculated by taking max_x or max_y
  // and scaling it back by the size of the viewport.
  const int new_x =
      (vis_rect.x() > x) ? x : std::max(0, max_x - contents_viewport_->width());
  const int new_y = (vis_rect.y() > y)
                        ? y
                        : std::max(0, max_y - contents_viewport_->height());

  ScrollToOffset(gfx::PointF(new_x, new_y));
}

void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size,
                                             const gfx::Size& content_size,
                                             bool* horiz_is_shown,
                                             bool* vert_is_shown) const {
  const bool horizontal_enabled =
      horizontal_scroll_bar_mode_ == ScrollBarMode::kEnabled;
  const bool vertical_enabled =
      vertical_scroll_bar_mode_ == ScrollBarMode::kEnabled;
  if (!horizontal_enabled) {
    *horiz_is_shown = false;
    *vert_is_shown =
        vertical_enabled && content_size.height() > vp_size.height();
    return;
  }
  if (!vertical_enabled) {
    *vert_is_shown = false;
    *horiz_is_shown = content_size.width() > vp_size.width();
    return;
  }

  // Try to fit both ways first, then try vertical bar only, then horizontal
  // bar only, then defaults to both shown.
  if (content_size.width() <= vp_size.width() &&
      content_size.height() <= vp_size.height()) {
    *horiz_is_shown = false;
    *vert_is_shown = false;
  } else if (content_size.width() <=
             vp_size.width() - GetScrollBarLayoutWidth()) {
    *horiz_is_shown = false;
    *vert_is_shown = true;
  } else if (content_size.height() <=
             vp_size.height() - GetScrollBarLayoutHeight()) {
    *horiz_is_shown = true;
    *vert_is_shown = false;
  } else {
    *horiz_is_shown = true;
    *vert_is_shown = true;
  }
}

// Make sure that a single scrollbar is created and visible as needed
void ScrollView::SetControlVisibility(View* control, bool should_show) {
  if (!control) {
    return;
  }
  if (should_show) {
    if (!control->GetVisible()) {
      AddChildViewRaw(control);
      control->SetVisible(true);
    }
  } else {
    RemoveChildView(control);
    control->SetVisible(false);
  }
}

void ScrollView::UpdateScrollBarPositions() {
  if (!contents_) {
    return;
  }

  const gfx::PointF offset = CurrentOffset();
  if (IsHorizontalScrollEnabled()) {
    int vw = contents_viewport_->width();
    int cw = contents_->width();
    horiz_sb_->Update(vw, cw, offset.x());
  }
  if (IsVerticalScrollEnabled()) {
    int vh = contents_viewport_->height();
    int ch = contents_->height();
    vert_sb_->Update(vh, ch, offset.y());
  }
  GetViewAccessibility().SetScrollXMin(horiz_sb_->GetMinPosition());
  GetViewAccessibility().SetScrollXMax(horiz_sb_->GetMaxPosition());
  GetViewAccessibility().SetScrollYMin(vert_sb_->GetMinPosition());
  GetViewAccessibility().SetScrollYMax(vert_sb_->GetMaxPosition());
}

void ScrollView::ScrollByOffset(const gfx::PointF& offset) {
  if (!contents_) {
    return;
  }

  gfx::PointF current_offset = CurrentOffset();
  ScrollToOffset(gfx::PointF(current_offset.x() + offset.x(),
                             current_offset.y() + offset.y()));
}

void ScrollView::ScrollToOffset(const gfx::PointF& offset) {
  if (ScrollsWithLayers()) {
    contents_->layer()->SetScrollOffset(offset);
  } else {
    contents_->SetPosition(gfx::Point(-offset.x(), -offset.y()));
  }
  GetViewAccessibility().SetScrollX(offset.x());
  GetViewAccessibility().SetScrollY(offset.y());
  OnScrolled(offset);
}

gfx::PointF ScrollView::CurrentOffset() const {
  return ScrollsWithLayers() ? contents_->layer()->CurrentScrollOffset()
                             : gfx::PointF(-contents_->x(), -contents_->y());
}

bool ScrollView::ScrollsWithLayers() const {
  if (!scroll_with_layers_enabled_) {
    return false;
  }
  // Just check for the presence of a layer since it's cheaper than querying the
  // Feature flag each time.
  return contents_viewport_->layer() != nullptr;
}

bool ScrollView::IsHorizontalScrollEnabled() const {
  return horizontal_scroll_bar_mode_ == ScrollBarMode::kHiddenButEnabled ||
         (horizontal_scroll_bar_mode_ == ScrollBarMode::kEnabled &&
          horiz_sb_->GetVisible());
}

bool ScrollView::IsVerticalScrollEnabled() const {
  return vertical_scroll_bar_mode_ == ScrollBarMode::kHiddenButEnabled ||
         (vertical_scroll_bar_mode_ == ScrollBarMode::kEnabled &&
          vert_sb_->GetVisible());
}

void ScrollView::EnableViewportLayer() {
  if (DoesViewportOrScrollViewHaveLayer()) {
    return;
  }
  contents_viewport_->InitializeContentsViewportLayer();
  contents_viewport_->layer()->SetMasksToBounds(true);
  more_content_left_->SetPaintToLayer();
  more_content_top_->SetPaintToLayer();
  more_content_right_->SetPaintToLayer();
  more_content_bottom_->SetPaintToLayer();
  UpdateBackground();
}

void ScrollView::OnLayerScrolled(const gfx::PointF& current_offset,
                                 const cc::ElementId&) {
  OnScrolled(current_offset);
}

void ScrollView::OnScrolled(const gfx::PointF& offset) {
  UpdateOverflowIndicatorVisibility(offset);
  UpdateScrollBarPositions();
  ScrollHeader();

  on_contents_scrolled_.Notify();

  NotifyAccessibilityEventDeprecated(ax::mojom::Event::kScrollPositionChanged,
                                     /*send_native_event=*/true);
}

void ScrollView::ScrollHeader() {
  if (!header_) {
    return;
  }

  int x_offset = CurrentOffset().x();
  if (header_->x() != -x_offset) {
    header_->SetX(-x_offset);
    header_->SchedulePaintInRect(header_->GetVisibleBounds());
  }
}

void ScrollView::AddBorder() {
  draw_border_ = true;
  UpdateBorder();
}

void ScrollView::UpdateBorder() {
  if (!draw_border_ || !GetWidget()) {
    return;
  }

  SetBorder(CreateSolidBorder(1, draw_focus_indicator_
                                     ? ui::kColorFocusableBorderFocused
                                     : ui::kColorFocusableBorderUnfocused));
}

void ScrollView::UpdateBackground() {
  if (!GetWidget()) {
    return;
  }

  const std::optional<ui::ColorVariant> background_color = GetBackgroundColor();

  auto create_background = [background_color]() {
    return background_color ? CreateSolidBackground(background_color.value())
                            : nullptr;
  };

  SetBackground(create_background());
  // In addition to setting the background of |this|, set the background on
  // the viewport as well. This way if the viewport has a layer
  // SetFillsBoundsOpaquely() is honored.
  contents_viewport_->SetBackground(create_background());
  if (contents_ && ScrollsWithLayers()) {
    contents_->SetBackground(create_background());
    // Contents views may not be aware they need to fill their entire bounds -
    // play it safe here to avoid graphical glitches (https://crbug.com/826472).
    // If there's no solid background, mark the contents view as not filling its
    // bounds opaquely.
    contents_->layer()->SetFillsBoundsOpaquely(!!background_color);
  }
  if (contents_viewport_->layer()) {
    contents_viewport_->layer()->SetFillsBoundsOpaquely(!!background_color);
  }
}

std::optional<ui::ColorVariant> ScrollView::GetBackgroundColor() const {
  return background_color_;
}

void ScrollView::PositionOverflowIndicators() {
  // TODO(crbug.com/40742414): Use a layout manager to position these.
  const gfx::Rect contents_bounds = GetContentsBounds();
  const int x = contents_bounds.x();
  const int y = contents_bounds.y();
  const int w = contents_bounds.width();
  const int h = contents_bounds.height();

  more_content_left_->SetBoundsRect(
      gfx::Rect(x, y, more_content_left_thickness_, h));
  more_content_top_->SetBoundsRect(
      gfx::Rect(x, y, w, more_content_top_thickness_));
  more_content_right_->SetBoundsRect(
      gfx::Rect(contents_bounds.right() - more_content_right_thickness_, y,
                more_content_right_thickness_, h));
  more_content_bottom_->SetBoundsRect(
      gfx::Rect(x, contents_bounds.bottom() - more_content_bottom_thickness_, w,
                more_content_bottom_thickness_));
}

void ScrollView::UpdateOverflowIndicatorVisibility(const gfx::PointF& offset) {
  SetControlVisibility(more_content_top_.get(),
                       !draw_border_ && !header_ && IsVerticalScrollEnabled() &&
                           offset.y() > vert_sb_->GetMinPosition() &&
                           draw_overflow_indicator_);
  SetControlVisibility(
      more_content_bottom_.get(),
      !draw_border_ && IsVerticalScrollEnabled() && !horiz_sb_->GetVisible() &&
          offset.y() < vert_sb_->GetMaxPosition() && draw_overflow_indicator_);

  SetControlVisibility(more_content_left_.get(),
                       !draw_border_ && IsHorizontalScrollEnabled() &&
                           offset.x() > horiz_sb_->GetMinPosition() &&
                           draw_overflow_indicator_);
  SetControlVisibility(
      more_content_right_.get(),
      !draw_border_ && IsHorizontalScrollEnabled() && !vert_sb_->GetVisible() &&
          offset.x() < horiz_sb_->GetMaxPosition() && draw_overflow_indicator_);
}

void ScrollView::RegisterPostLayoutCallback(
    base::RepeatingCallback<void(ScrollView*)> post_layout_callback) {
  post_layout_callback_ = post_layout_callback;
}

View* ScrollView::GetContentsViewportForTest() const {
  return contents_viewport_;
}

BEGIN_METADATA(ScrollView)
ADD_READONLY_PROPERTY_METADATA(int, MinHeight)
ADD_READONLY_PROPERTY_METADATA(int, MaxHeight)
ADD_PROPERTY_METADATA(bool, AllowKeyboardScrolling)
ADD_PROPERTY_METADATA(std::optional<ui::ColorVariant>, BackgroundColor)
ADD_PROPERTY_METADATA(bool, DrawOverflowIndicator)
ADD_PROPERTY_METADATA(bool, HasFocusIndicator)
ADD_PROPERTY_METADATA(ScrollView::ScrollBarMode, HorizontalScrollBarMode)
ADD_PROPERTY_METADATA(ScrollView::ScrollBarMode, VerticalScrollBarMode)
ADD_PROPERTY_METADATA(bool, TreatAllScrollEventsAsHorizontal)
END_METADATA

// VariableRowHeightScrollHelper ----------------------------------------------

VariableRowHeightScrollHelper::VariableRowHeightScrollHelper(
    Controller* controller)
    : controller_(controller) {}

VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() = default;

int VariableRowHeightScrollHelper::GetPageScrollIncrement(
    ScrollView* scroll_view,
    bool is_horizontal,
    bool is_positive) {
  if (is_horizontal) {
    return 0;
  }
  // y coordinate is most likely negative.
  int y = abs(scroll_view->contents()->y());
  int vis_height = scroll_view->contents()->parent()->height();
  if (is_positive) {
    // Align the bottom most row to the top of the view.
    int bottom =
        std::min(scroll_view->contents()->height() - 1, y + vis_height);
    RowInfo bottom_row_info = GetRowInfo(bottom);
    // If 0, ScrollView will provide a default value.
    return std::max(0, bottom_row_info.origin - y);
  } else {
    // Align the row on the previous page to to the top of the view.
    int last_page_y = y - vis_height;
    RowInfo last_page_info = GetRowInfo(std::max(0, last_page_y));
    if (last_page_y != last_page_info.origin) {
      return std::max(0, y - last_page_info.origin - last_page_info.height);
    }
    return std::max(0, y - last_page_info.origin);
  }
}

int VariableRowHeightScrollHelper::GetLineScrollIncrement(
    ScrollView* scroll_view,
    bool is_horizontal,
    bool is_positive) {
  if (is_horizontal) {
    return 0;
  }
  // y coordinate is most likely negative.
  int y = abs(scroll_view->contents()->y());
  RowInfo row = GetRowInfo(y);
  if (is_positive) {
    return row.height - (y - row.origin);
  } else if (y == row.origin) {
    row = GetRowInfo(std::max(0, row.origin - 1));
    return y - row.origin;
  } else {
    return y - row.origin;
  }
}

VariableRowHeightScrollHelper::RowInfo
VariableRowHeightScrollHelper::GetRowInfo(int y) {
  return controller_->GetRowInfo(y);
}

// FixedRowHeightScrollHelper -----------------------------------------------

FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin,
                                                       int row_height)
    : VariableRowHeightScrollHelper(nullptr),
      top_margin_(top_margin),
      row_height_(row_height) {
  DCHECK_GT(row_height, 0);
}

VariableRowHeightScrollHelper::RowInfo FixedRowHeightScrollHelper::GetRowInfo(
    int y) {
  if (y < top_margin_) {
    return RowInfo(0, top_margin_);
  }
  return RowInfo((y - top_margin_) / row_height_ * row_height_ + top_margin_,
                 row_height_);
}

}  // namespace views

DEFINE_ENUM_CONVERTERS(
    views::ScrollView::ScrollBarMode,
    {views::ScrollView::ScrollBarMode::kDisabled, u"kDisabled"},
    {views::ScrollView::ScrollBarMode::kHiddenButEnabled, u"kHiddenButEnabled"},
    {views::ScrollView::ScrollBarMode::kEnabled, u"kEnabled"})