// Copyright (c) 2012 The Chromium Authors. All rights reserved. // 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 #include "base/bind.h" #include "base/check_op.h" #include "base/cxx17_backports.h" #include "base/feature_list.h" #include "base/i18n/rtl.h" #include "base/macros.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/compositor/layer.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/background.h" #include "ui/views/border.h" #include "ui/views/controls/focus_ring.h" #include "ui/views/style/platform_style.h" #include "ui/views/view.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 T CombineScrollOffsets(T x, T y) { return std::abs(x) >= std::abs(y) ? x : y; } class ScrollCornerView : public View { public: METADATA_HEADER(ScrollCornerView); ScrollCornerView() = default; ScrollCornerView(const ScrollCornerView&) = delete; ScrollCornerView& operator=(const ScrollCornerView&) = delete; void OnPaint(gfx::Canvas* canvas) override { ui::NativeTheme::ExtraParams ignored; GetNativeTheme()->Paint( canvas->sk_canvas(), ui::NativeTheme::kScrollbarCorner, ui::NativeTheme::kNormal, GetLocalBounds(), ignored); } }; BEGIN_METADATA(ScrollCornerView, View) END_METADATA // Returns true if any descendants of |view| have a layer (not including // |view|). bool DoesDescendantHaveLayer(View* view) { return std::any_of(view->children().cbegin(), view->children().cend(), [](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 base::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::ScrollOffset offset = scrolls_with_layers ? view->layer()->CurrentScrollOffset() : gfx::ScrollOffset(-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::ScrollOffset(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 { public: METADATA_HEADER(Viewport); 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; 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(); } void OnChildLayerChanged(View* child) override { if (GetIsContentsViewport()) scroll_view_->UpdateViewportLayerForClipping(); } private: bool GetIsContentsViewport() const { return parent() && scroll_view_->contents_viewport_ == this; } ScrollView* scroll_view_; }; BEGIN_METADATA(ScrollView, Viewport, View) 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(true))), vert_sb_(AddChildView(PlatformStyle::CreateScrollBar(false))), corner_view_(std::make_unique()), 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(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(this)); horiz_sb_->SetVisible(false); horiz_sb_->set_controller(this); vert_sb_->SetVisible(false); vert_sb_->set_controller(this); corner_view_->SetVisible(false); // 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([](View* view) -> bool { auto* v = static_cast(view); return v->draw_focus_indicator_; }); } ScrollView::~ScrollView() = default; // static std::unique_ptr ScrollView::CreateScrollViewWithBorder() { auto scroll_view = std::make_unique(); scroll_view->AddBorder(); return scroll_view; } // static ScrollView* ScrollView::GetScrollViewForContents(View* contents) { View* grandparent = contents->parent() ? contents->parent()->parent() : nullptr; if (!grandparent || grandparent->GetClassName() != ScrollView::kViewClassName) return nullptr; auto* scroll_view = static_cast(grandparent); DCHECK_EQ(contents, scroll_view->contents()); return scroll_view; } void ScrollView::SetContentsImpl(std::unique_ptr 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(); a_view->layer()->SetDidScrollCallback(base::BindRepeating( &ScrollView::OnLayerScrolled, base::Unretained(this))); a_view->layer()->SetScrollable(contents_viewport_->bounds().size()); } SetHeaderOrContents(contents_viewport_, std::move(a_view), &contents_); UpdateBackground(); } void ScrollView::SetContents(std::nullptr_t) { SetContentsImpl(nullptr); } void ScrollView::SetHeaderImpl(std::unique_ptr a_header) { SetHeaderOrContents(header_viewport_, std::move(a_header), &header_); } void ScrollView::SetHeader(std::nullptr_t) { SetHeaderImpl(nullptr); } void ScrollView::SetBackgroundColor(const absl::optional& color) { if (background_color_ == color && !background_color_id_) return; background_color_ = color; background_color_id_ = absl::nullopt; UpdateBackground(); OnPropertyChanged(&background_color_, kPropertyEffectsPaint); } void ScrollView::SetBackgroundThemeColorId( const absl::optional& color_id) { if (background_color_id_ == color_id && !background_color_) return; background_color_id_ = color_id; background_color_ = absl::nullopt; UpdateBackground(); OnPropertyChanged(&background_color_id_, kPropertyEffectsPaint); } gfx::Rect ScrollView::GetVisibleRect() const { if (!contents_) return gfx::Rect(); gfx::ScrollOffset 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); } 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); } 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::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 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(); break; } 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 horiz_sb) { horiz_sb->SetVisible(horiz_sb_->GetVisible()); horiz_sb->set_controller(this); RemoveChildViewT(horiz_sb_); horiz_sb_ = AddChildView(std::move(horiz_sb)); return horiz_sb_; } ScrollBar* ScrollView::SetVerticalScrollBar( std::unique_ptr vert_sb) { DCHECK(vert_sb); vert_sb->SetVisible(vert_sb_->GetVisible()); vert_sb->set_controller(this); RemoveChildViewT(vert_sb_); vert_sb_ = AddChildView(std::move(vert_sb)); 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); } void ScrollView::AddScrollViewObserver(Observer* observer) { observers_.AddObserver(observer); } void ScrollView::RemoveScrollViewObserver(Observer* observer) { observers_.RemoveObserver(observer); } gfx::Size ScrollView::CalculatePreferredSize() const { gfx::Size size = contents_ ? contents_->GetPreferredSize() : gfx::Size(); if (is_bounded()) { size.SetToMax(gfx::Size(size.width(), min_height_)); size.SetToMin(gfx::Size(size.width(), max_height_)); } gfx::Insets insets = GetInsets(); size.Enlarge(insets.width(), insets.height()); return size; } int ScrollView::GetHeightForWidth(int width) const { if (!is_bounded()) return View::GetHeightForWidth(width); gfx::Insets insets = GetInsets(); width = std::max(0, width - insets.width()); int height = contents_ ? contents_->GetHeightForWidth(width) + insets.height() : insets.height(); return base::clamp(height, min_height_, max_height_); } void ScrollView::Layout() { // 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 defined(OS_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)->Layout(); gfx::Rect available_rect = GetContentsBounds(); if (is_bounded()) { 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_->Layout(). 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_->Layout(); bool should_layout_contents = false; bool horiz_sb_required = false; bool vert_sb_required = false; if (contents_) { gfx::Size content_size = contents_->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(-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(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_->Layout(); // 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(); 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 gfx::ScrollOffset will move content to // the left, regardless of RTL. A gfx::ScrollOffset 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_->Layout(); ConstrainScrollToBounds(header_viewport_, header_, scroll_with_layers_enabled_); ConstrainScrollToBounds(contents_viewport_, contents_, scroll_with_layers_enabled_); SchedulePaint(); UpdateScrollBarPositions(); if (contents_) UpdateOverflowIndicatorVisibility(CurrentOffset()); } bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) { bool processed = 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(https://crbug.com/615948): 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::ET_GESTURE_SCROLL_UPDATE || event->type() == ui::ET_GESTURE_SCROLL_BEGIN || event->type() == ui::ET_GESTURE_SCROLL_END || event->type() == ui::ET_SCROLL_FLING_START; // 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(https://crbug.com/615948): 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(); } void ScrollView::GetAccessibleNodeData(ui::AXNodeData* node_data) { View::GetAccessibleNodeData(node_data); if (!contents_) return; node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollX, CurrentOffset().x()); node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMin, horiz_sb_->GetMinPosition()); node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollXMax, horiz_sb_->GetMaxPosition()); node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollY, CurrentOffset().y()); node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMin, vert_sb_->GetMinPosition()); node_data->AddIntAttribute(ax::mojom::IntAttribute::kScrollYMax, vert_sb_->GetMaxPosition()); node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable, true); } 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::ScrollOffset(action_data.target_point.x(), action_data.target_point.y())); return true; default: return View::HandleAccessibleAction(action_data); } } void ScrollView::ScrollToPosition(ScrollBar* source, int position) { if (!contents_) return; gfx::ScrollOffset 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->IsHorizontal(); 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() { for (auto& observer : observers_) observer.OnContentsScrollEnded(); } 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(); } void ScrollView::SetHeaderOrContents(View* parent, std::unique_ptr new_view, View** member) { if (*member) parent->RemoveChildViewT(*member); if (new_view.get()) *member = parent->AddChildViewAt(std::move(new_view), 0); else *member = nullptr; InvalidateLayout(); } void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) { if (!contents_ || (!IsHorizontalScrollEnabled() && !IsVerticalScrollEnabled())) { return; } // 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 = base::clamp(rect.x(), 0, contents_max_x); int y = base::clamp(rect.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(rect.width(), contents_viewport_->width())); const int max_y = std::min(contents_max_y, y + std::min(rect.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::ScrollOffset(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()) { AddChildView(control); control->SetVisible(true); } } else { RemoveChildView(control); control->SetVisible(false); } } void ScrollView::UpdateScrollBarPositions() { if (!contents_) return; const gfx::ScrollOffset 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()); } } gfx::ScrollOffset ScrollView::CurrentOffset() const { return ScrollsWithLayers() ? contents_->layer()->CurrentScrollOffset() : gfx::ScrollOffset(-contents_->x(), -contents_->y()); } void ScrollView::ScrollToOffset(const gfx::ScrollOffset& offset) { if (ScrollsWithLayers()) { contents_->layer()->SetScrollOffset(offset); } else { contents_->SetPosition(gfx::Point(-offset.x(), -offset.y())); } OnScrolled(offset); } 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_->SetPaintToLayer(); 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::ScrollOffset& current_offset, const cc::ElementId&) { OnScrolled(current_offset); } void ScrollView::OnScrolled(const gfx::ScrollOffset& offset) { UpdateOverflowIndicatorVisibility(offset); UpdateScrollBarPositions(); ScrollHeader(); for (auto& observer : observers_) observer.OnContentsScrolled(); NotifyAccessibilityEvent(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, GetNativeTheme()->GetSystemColor( draw_focus_indicator_ ? ui::NativeTheme::kColorId_FocusedBorderColor : ui::NativeTheme::kColorId_UnfocusedBorderColor))); } void ScrollView::UpdateBackground() { if (!GetWidget()) return; const absl::optional 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); } } absl::optional ScrollView::GetBackgroundColor() const { return background_color_id_ ? GetNativeTheme()->GetSystemColor(background_color_id_.value()) : background_color_; } absl::optional ScrollView::GetBackgroundThemeColorId() const { return background_color_id_; } void ScrollView::PositionOverflowIndicators() { // TODO(https://crbug.com/1166949): 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::ScrollOffset& 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_); } BEGIN_METADATA(ScrollView, View) ADD_READONLY_PROPERTY_METADATA(int, MinHeight) ADD_READONLY_PROPERTY_METADATA(int, MaxHeight) ADD_PROPERTY_METADATA(absl::optional, BackgroundColor) ADD_PROPERTY_METADATA(absl::optional, BackgroundThemeColorId) 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