blob: 8c3fb43b11664b8a935b5b59c752cacb46ffccb9 [file] [log] [blame]
// 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_, kPropertyEffectsPaint);
}
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_, kPropertyEffectsPaint);
// "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_, kPropertyEffectsPaint);
// "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_,
kPropertyEffectsNone);
// 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_, kPropertyEffectsNone);
}
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_, kPropertyEffectsPaint);
}
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_, kPropertyEffectsPaint);
}
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"})