1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/views/controls/scroll_view.h"
6 
7 #include <algorithm>
8 
9 #include "base/bind.h"
10 #include "base/feature_list.h"
11 #include "base/logging.h"
12 #include "base/macros.h"
13 #include "base/numerics/ranges.h"
14 #include "build/build_config.h"
15 #include "ui/base/ui_base_features.h"
16 #include "ui/compositor/overscroll/scroll_input_handler.h"
17 #include "ui/events/event.h"
18 #include "ui/gfx/canvas.h"
19 #include "ui/native_theme/native_theme.h"
20 #include "ui/views/background.h"
21 #include "ui/views/border.h"
22 #include "ui/views/controls/focus_ring.h"
23 #include "ui/views/style/platform_style.h"
24 #include "ui/views/widget/widget.h"
25 
26 namespace views {
27 
28 namespace {
29 
30 class ScrollCornerView : public View {
31  public:
32   ScrollCornerView() = default;
33 
OnPaint(gfx::Canvas * canvas)34   void OnPaint(gfx::Canvas* canvas) override {
35     ui::NativeTheme::ExtraParams ignored;
36     GetNativeTheme()->Paint(
37         canvas->sk_canvas(), ui::NativeTheme::kScrollbarCorner,
38         ui::NativeTheme::kNormal, GetLocalBounds(), ignored);
39   }
40 
41  private:
42   DISALLOW_COPY_AND_ASSIGN(ScrollCornerView);
43 };
44 
45 // Returns true if any descendants of |view| have a layer (not including
46 // |view|).
DoesDescendantHaveLayer(View * view)47 bool DoesDescendantHaveLayer(View* view) {
48   return std::any_of(view->children().cbegin(), view->children().cend(),
49                      [](View* child) {
50                        return child->layer() || DoesDescendantHaveLayer(child);
51                      });
52 }
53 
54 // Returns the position for the view so that it isn't scrolled off the visible
55 // region.
CheckScrollBounds(int viewport_size,int content_size,int current_pos)56 int CheckScrollBounds(int viewport_size, int content_size, int current_pos) {
57   return base::ClampToRange(current_pos, 0,
58                             std::max(content_size - viewport_size, 0));
59 }
60 
61 // Make sure the content is not scrolled out of bounds
ConstrainScrollToBounds(View * viewport,View * view,bool scroll_with_layers_enabled)62 void ConstrainScrollToBounds(View* viewport,
63                              View* view,
64                              bool scroll_with_layers_enabled) {
65   if (!view)
66     return;
67 
68   // Note that even when ScrollView::ScrollsWithLayers() is true, the header row
69   // scrolls by repainting.
70   const bool scrolls_with_layers =
71       scroll_with_layers_enabled && viewport->layer() != nullptr;
72   if (scrolls_with_layers) {
73     DCHECK(view->layer());
74     DCHECK_EQ(0, view->x());
75     DCHECK_EQ(0, view->y());
76   }
77   gfx::ScrollOffset offset = scrolls_with_layers
78                                  ? view->layer()->CurrentScrollOffset()
79                                  : gfx::ScrollOffset(-view->x(), -view->y());
80 
81   int x = CheckScrollBounds(viewport->width(), view->width(), offset.x());
82   int y = CheckScrollBounds(viewport->height(), view->height(), offset.y());
83 
84   if (scrolls_with_layers) {
85     view->layer()->SetScrollOffset(gfx::ScrollOffset(x, y));
86   } else {
87     // This is no op if bounds are the same
88     view->SetBounds(-x, -y, view->width(), view->height());
89   }
90 }
91 
92 // Used by ScrollToPosition() to make sure the new position fits within the
93 // allowed scroll range.
AdjustPosition(int current_position,int new_position,int content_size,int viewport_size)94 int AdjustPosition(int current_position,
95                    int new_position,
96                    int content_size,
97                    int viewport_size) {
98   if (-current_position == new_position)
99     return new_position;
100   if (new_position < 0)
101     return 0;
102   const int max_position = std::max(0, content_size - viewport_size);
103   return (new_position > max_position) ? max_position : new_position;
104 }
105 
106 }  // namespace
107 
108 // Viewport contains the contents View of the ScrollView.
109 class ScrollView::Viewport : public View {
110  public:
Viewport(ScrollView * scroll_view)111   explicit Viewport(ScrollView* scroll_view) : scroll_view_(scroll_view) {}
112   ~Viewport() override = default;
113 
ScrollRectToVisible(const gfx::Rect & rect)114   void ScrollRectToVisible(const gfx::Rect& rect) override {
115     if (children().empty() || !parent())
116       return;
117 
118     View* contents = children().front();
119     gfx::Rect scroll_rect(rect);
120 
121     if (scroll_view_->ScrollsWithLayers()) {
122       // With layer scrolling, there's no need to "undo" the offset done in the
123       // child's View::ScrollRectToVisible() before it calls this.
124       DCHECK_EQ(0, contents->x());
125       DCHECK_EQ(0, contents->y());
126     } else {
127       scroll_rect.Offset(-contents->x(), -contents->y());
128     }
129 
130     scroll_view_->ScrollContentsRegionToBeVisible(scroll_rect);
131   }
132 
133   // TODO(https://crbug.com/947053): this override should not be necessary, but
134   // there are some assumptions that this calls Layout().
ChildPreferredSizeChanged(View * child)135   void ChildPreferredSizeChanged(View* child) override {
136     if (parent())
137       parent()->Layout();
138   }
139 
ViewHierarchyChanged(const ViewHierarchyChangedDetails & details)140   void ViewHierarchyChanged(
141       const ViewHierarchyChangedDetails& details) override {
142     if (details.is_add && IsContentsViewport() && Contains(details.parent))
143       scroll_view_->UpdateViewportLayerForClipping();
144   }
145 
OnChildLayerChanged(View * child)146   void OnChildLayerChanged(View* child) override {
147     if (IsContentsViewport())
148       scroll_view_->UpdateViewportLayerForClipping();
149   }
150 
151  private:
IsContentsViewport() const152   bool IsContentsViewport() const {
153     return parent() && scroll_view_->contents_viewport_ == this;
154   }
155 
156   ScrollView* scroll_view_;
157 
158   DISALLOW_COPY_AND_ASSIGN(Viewport);
159 };
160 
ScrollView()161 ScrollView::ScrollView()
162     : horiz_sb_(PlatformStyle::CreateScrollBar(true)),
163       vert_sb_(PlatformStyle::CreateScrollBar(false)),
164       corner_view_(std::make_unique<ScrollCornerView>()),
165       scroll_with_layers_enabled_(base::FeatureList::IsEnabled(
166           ::features::kUiCompositorScrollWithLayers)) {
167   set_notify_enter_exit_on_child(true);
168 
169   // Since |contents_viewport_| is accessed during the AddChildView call, make
170   // sure the field is initialized.
171   auto contents_viewport = std::make_unique<Viewport>(this);
172   contents_viewport_ = contents_viewport.get();
173   AddChildView(std::move(contents_viewport));
174   header_viewport_ = AddChildView(std::make_unique<Viewport>(this));
175 
176   // Don't add the scrollbars as children until we discover we need them
177   // (ShowOrHideScrollBar).
178   horiz_sb_->SetVisible(false);
179   horiz_sb_->set_controller(this);
180   vert_sb_->SetVisible(false);
181   vert_sb_->set_controller(this);
182   corner_view_->SetVisible(false);
183 
184   // Just make sure the more_content indicators aren't visible for now. They'll
185   // be added as child controls and appropriately made visible depending on
186   // |show_edges_with_hidden_content_|.
187   more_content_left_->SetVisible(false);
188   more_content_top_->SetVisible(false);
189   more_content_right_->SetVisible(false);
190   more_content_bottom_->SetVisible(false);
191 
192   if (scroll_with_layers_enabled_)
193     EnableViewportLayer();
194 
195   // If we're scrolling with layers, paint the overflow indicators to the layer.
196   if (ScrollsWithLayers()) {
197     more_content_left_->SetPaintToLayer();
198     more_content_top_->SetPaintToLayer();
199     more_content_right_->SetPaintToLayer();
200     more_content_bottom_->SetPaintToLayer();
201   }
202   UpdateBackground();
203 
204   focus_ring_ = FocusRing::Install(this);
205   focus_ring_->SetHasFocusPredicate([](View* view) -> bool {
206     auto* v = static_cast<ScrollView*>(view);
207     return v->draw_focus_indicator_;
208   });
209 }
210 
211 ScrollView::~ScrollView() = default;
212 
213 // static
CreateScrollViewWithBorder()214 std::unique_ptr<ScrollView> ScrollView::CreateScrollViewWithBorder() {
215   auto scroll_view = std::make_unique<ScrollView>();
216   scroll_view->AddBorder();
217   return scroll_view;
218 }
219 
220 // static
GetScrollViewForContents(View * contents)221 ScrollView* ScrollView::GetScrollViewForContents(View* contents) {
222   View* grandparent =
223       contents->parent() ? contents->parent()->parent() : nullptr;
224   if (!grandparent || grandparent->GetClassName() != ScrollView::kViewClassName)
225     return nullptr;
226 
227   auto* scroll_view = static_cast<ScrollView*>(grandparent);
228   DCHECK_EQ(contents, scroll_view->contents());
229   return scroll_view;
230 }
231 
SetContentsImpl(std::unique_ptr<View> a_view)232 void ScrollView::SetContentsImpl(std::unique_ptr<View> a_view) {
233   // Protect against clients passing a contents view that has its own Layer.
234   DCHECK(!a_view->layer());
235   if (ScrollsWithLayers()) {
236     bool fills_opaquely = true;
237     if (!a_view->background()) {
238       // Contents views may not be aware they need to fill their entire bounds -
239       // play it safe here to avoid graphical glitches
240       // (https://crbug.com/826472). If there's no solid background, mark the
241       // view as not filling its bounds opaquely.
242       if (GetBackgroundColor()) {
243         a_view->SetBackground(
244             CreateSolidBackground(GetBackgroundColor().value()));
245       } else {
246         fills_opaquely = false;
247       }
248     }
249     a_view->SetPaintToLayer();
250     a_view->layer()->SetDidScrollCallback(base::BindRepeating(
251         &ScrollView::OnLayerScrolled, base::Unretained(this)));
252     a_view->layer()->SetScrollable(contents_viewport_->bounds().size());
253     a_view->layer()->SetFillsBoundsOpaquely(fills_opaquely);
254   }
255   SetHeaderOrContents(contents_viewport_, std::move(a_view), &contents_);
256 }
257 
SetContents(std::nullptr_t)258 void ScrollView::SetContents(std::nullptr_t) {
259   SetContentsImpl(nullptr);
260 }
261 
SetHeaderImpl(std::unique_ptr<View> a_header)262 void ScrollView::SetHeaderImpl(std::unique_ptr<View> a_header) {
263   SetHeaderOrContents(header_viewport_, std::move(a_header), &header_);
264 }
265 
SetHeader(std::nullptr_t)266 void ScrollView::SetHeader(std::nullptr_t) {
267   SetHeaderImpl(nullptr);
268 }
269 
SetBackgroundColor(const base::Optional<SkColor> & color)270 void ScrollView::SetBackgroundColor(const base::Optional<SkColor>& color) {
271   if (background_color_ == color && !background_color_id_)
272     return;
273   background_color_ = color;
274   background_color_id_ = base::nullopt;
275   UpdateBackground();
276   OnPropertyChanged(&background_color_, kPropertyEffectsPaint);
277 }
278 
SetBackgroundThemeColorId(const base::Optional<ui::NativeTheme::ColorId> & color_id)279 void ScrollView::SetBackgroundThemeColorId(
280     const base::Optional<ui::NativeTheme::ColorId>& color_id) {
281   if (background_color_id_ == color_id && !background_color_)
282     return;
283   background_color_id_ = color_id;
284   background_color_ = base::nullopt;
285   UpdateBackground();
286   OnPropertyChanged(&background_color_id_, kPropertyEffectsPaint);
287 }
288 
GetVisibleRect() const289 gfx::Rect ScrollView::GetVisibleRect() const {
290   if (!contents_)
291     return gfx::Rect();
292   gfx::ScrollOffset offset = CurrentOffset();
293   return gfx::Rect(offset.x(), offset.y(), contents_viewport_->width(),
294                    contents_viewport_->height());
295 }
296 
SetHideHorizontalScrollBar(bool visible)297 void ScrollView::SetHideHorizontalScrollBar(bool visible) {
298   if (hide_horizontal_scrollbar_ == visible)
299     return;
300   hide_horizontal_scrollbar_ = visible;
301   OnPropertyChanged(&hide_horizontal_scrollbar_, kPropertyEffectsPaint);
302 }
303 
SetDrawOverflowIndicator(bool draw_overflow_indicator)304 void ScrollView::SetDrawOverflowIndicator(bool draw_overflow_indicator) {
305   if (draw_overflow_indicator_ == draw_overflow_indicator)
306     return;
307   draw_overflow_indicator_ = draw_overflow_indicator;
308   OnPropertyChanged(&draw_overflow_indicator_, kPropertyEffectsPaint);
309 }
310 
ClipHeightTo(int min_height,int max_height)311 void ScrollView::ClipHeightTo(int min_height, int max_height) {
312   min_height_ = min_height;
313   max_height_ = max_height;
314 }
315 
GetScrollBarLayoutWidth() const316 int ScrollView::GetScrollBarLayoutWidth() const {
317   return vert_sb_ && !vert_sb_->OverlapsContent() ? vert_sb_->GetThickness()
318                                                   : 0;
319 }
320 
GetScrollBarLayoutHeight() const321 int ScrollView::GetScrollBarLayoutHeight() const {
322   return horiz_sb_ && !horiz_sb_->OverlapsContent() ? horiz_sb_->GetThickness()
323                                                     : 0;
324 }
325 
SetHorizontalScrollBar(std::unique_ptr<ScrollBar> horiz_sb)326 ScrollBar* ScrollView::SetHorizontalScrollBar(
327     std::unique_ptr<ScrollBar> horiz_sb) {
328   DCHECK(horiz_sb);
329   horiz_sb->SetVisible(horiz_sb_->GetVisible());
330   horiz_sb->set_controller(this);
331   horiz_sb_ = std::move(horiz_sb);
332   return horiz_sb_.get();
333 }
334 
SetVerticalScrollBar(std::unique_ptr<ScrollBar> vert_sb)335 ScrollBar* ScrollView::SetVerticalScrollBar(
336     std::unique_ptr<ScrollBar> vert_sb) {
337   DCHECK(vert_sb);
338   vert_sb->SetVisible(vert_sb_->GetVisible());
339   vert_sb->set_controller(this);
340   vert_sb_ = std::move(vert_sb);
341   return vert_sb_.get();
342 }
343 
SetHasFocusIndicator(bool has_focus_indicator)344 void ScrollView::SetHasFocusIndicator(bool has_focus_indicator) {
345   if (has_focus_indicator == draw_focus_indicator_)
346     return;
347   draw_focus_indicator_ = has_focus_indicator;
348 
349   focus_ring_->SchedulePaint();
350   SchedulePaint();
351   OnPropertyChanged(&draw_focus_indicator_, kPropertyEffectsPaint);
352 }
353 
CalculatePreferredSize() const354 gfx::Size ScrollView::CalculatePreferredSize() const {
355   if (!is_bounded())
356     return View::CalculatePreferredSize();
357 
358   gfx::Size size = contents_->GetPreferredSize();
359   size.SetToMax(gfx::Size(size.width(), min_height_));
360   size.SetToMin(gfx::Size(size.width(), max_height_));
361   gfx::Insets insets = GetInsets();
362   size.Enlarge(insets.width(), insets.height());
363   return size;
364 }
365 
GetHeightForWidth(int width) const366 int ScrollView::GetHeightForWidth(int width) const {
367   if (!is_bounded())
368     return View::GetHeightForWidth(width);
369 
370   gfx::Insets insets = GetInsets();
371   width = std::max(0, width - insets.width());
372   int height = contents_->GetHeightForWidth(width) + insets.height();
373   return base::ClampToRange(height, min_height_, max_height_);
374 }
375 
Layout()376 void ScrollView::Layout() {
377   // When horizontal scrollbar is disabled, it should not matter
378   // if its OverlapsContent matches vertical bar's.
379   if (!hide_horizontal_scrollbar_) {
380 #if defined(OS_MACOSX)
381     // On Mac, scrollbars may update their style one at a time, so they may
382     // temporarily be of different types. Refuse to lay out at this point.
383     if (horiz_sb_->OverlapsContent() != vert_sb_->OverlapsContent())
384       return;
385 #endif
386     DCHECK_EQ(horiz_sb_->OverlapsContent(), vert_sb_->OverlapsContent());
387   }
388 
389   if (focus_ring_)
390     focus_ring_->Layout();
391 
392   gfx::Rect available_rect = GetContentsBounds();
393   if (is_bounded()) {
394     int content_width = available_rect.width();
395     int content_height = contents_->GetHeightForWidth(content_width);
396     if (content_height > height()) {
397       content_width = std::max(content_width - GetScrollBarLayoutWidth(), 0);
398       content_height = contents_->GetHeightForWidth(content_width);
399     }
400     contents_->SetSize(gfx::Size(content_width, content_height));
401   }
402 
403   // Place an overflow indicator on each of the four edges of the content
404   // bounds.
405   PositionOverflowIndicators();
406 
407   // Most views will want to auto-fit the available space. Most of them want to
408   // use all available width (without overflowing) and only overflow in
409   // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc.
410   // Other views want to fit in both ways. An example is PrintView. To make both
411   // happy, assume a vertical scrollbar but no horizontal scrollbar. To override
412   // this default behavior, the inner view has to calculate the available space,
413   // used ComputeScrollBarsVisibility() to use the same calculation that is done
414   // here and sets its bound to fit within.
415   gfx::Rect viewport_bounds = available_rect;
416   const int contents_x = viewport_bounds.x();
417   const int contents_y = viewport_bounds.y();
418   if (viewport_bounds.IsEmpty()) {
419     // There's nothing to layout.
420     return;
421   }
422 
423   const int header_height =
424       std::min(viewport_bounds.height(),
425                header_ ? header_->GetPreferredSize().height() : 0);
426   viewport_bounds.set_height(
427       std::max(0, viewport_bounds.height() - header_height));
428   viewport_bounds.set_y(viewport_bounds.y() + header_height);
429   // viewport_size is the total client space available.
430   gfx::Size viewport_size = viewport_bounds.size();
431 
432   // Assume both a vertical and horizontal scrollbar exist before calling
433   // contents_->Layout(). This is because some contents_ will set their own size
434   // to the contents_viewport_'s bounds. Failing to pre-allocate space for
435   // the scrollbars will [non-intuitively] cause scrollbars to appear in
436   // ComputeScrollBarsVisibility. This solution is also not perfect - if
437   // scrollbars turn out *not* to be necessary, the contents will have slightly
438   // less horizontal/vertical space than it otherwise would have had access to.
439   // Unfortunately, there's no way to determine this without introducing a
440   // circular dependency.
441   const int horiz_sb_layout_height = GetScrollBarLayoutHeight();
442   const int vert_sb_layout_width = GetScrollBarLayoutWidth();
443   viewport_bounds.set_width(viewport_bounds.width() - vert_sb_layout_width);
444   viewport_bounds.set_height(viewport_bounds.height() - horiz_sb_layout_height);
445 
446   // Update the bounds right now so the inner views can fit in it.
447   contents_viewport_->SetBoundsRect(viewport_bounds);
448 
449   // Give |contents_| a chance to update its bounds if it depends on the
450   // viewport.
451   if (contents_)
452     contents_->Layout();
453 
454   bool should_layout_contents = false;
455   bool horiz_sb_required = false;
456   bool vert_sb_required = false;
457   if (contents_) {
458     gfx::Size content_size = contents_->size();
459     ComputeScrollBarsVisibility(viewport_size, content_size, &horiz_sb_required,
460                                 &vert_sb_required);
461   }
462   // Overlay scrollbars don't need a corner view.
463   bool corner_view_required =
464       horiz_sb_required && vert_sb_required && !vert_sb_->OverlapsContent();
465   // Take action.
466   SetControlVisibility(horiz_sb_.get(), horiz_sb_required);
467   SetControlVisibility(vert_sb_.get(), vert_sb_required);
468   SetControlVisibility(corner_view_.get(), corner_view_required);
469 
470   // Default.
471   if (!horiz_sb_required) {
472     viewport_bounds.set_height(viewport_bounds.height() +
473                                horiz_sb_layout_height);
474     should_layout_contents = true;
475   }
476   // Default.
477   if (!vert_sb_required) {
478     viewport_bounds.set_width(viewport_bounds.width() + vert_sb_layout_width);
479     should_layout_contents = true;
480   }
481 
482   if (horiz_sb_required) {
483     gfx::Rect horiz_sb_bounds(contents_x, viewport_bounds.bottom(),
484                               viewport_bounds.right() - contents_x,
485                               horiz_sb_layout_height);
486     if (horiz_sb_->OverlapsContent()) {
487       horiz_sb_bounds.Inset(
488           gfx::Insets(-horiz_sb_->GetThickness(), 0, 0,
489                       vert_sb_required ? vert_sb_->GetThickness() : 0));
490     }
491 
492     horiz_sb_->SetBoundsRect(horiz_sb_bounds);
493   }
494   if (vert_sb_required) {
495     gfx::Rect vert_sb_bounds(viewport_bounds.right(), contents_y,
496                              vert_sb_layout_width,
497                              viewport_bounds.bottom() - contents_y);
498     if (vert_sb_->OverlapsContent()) {
499       // In the overlay scrollbar case, the scrollbar only covers the viewport
500       // (and not the header).
501       vert_sb_bounds.Inset(
502           gfx::Insets(header_height, -vert_sb_->GetThickness(),
503                       horiz_sb_required ? horiz_sb_->GetThickness() : 0, 0));
504     }
505 
506     vert_sb_->SetBoundsRect(vert_sb_bounds);
507   }
508   if (corner_view_required) {
509     // Show the resize corner.
510     corner_view_->SetBounds(vert_sb_->bounds().x(), horiz_sb_->bounds().y(),
511                             vert_sb_layout_width, horiz_sb_layout_height);
512   }
513 
514   // Update to the real client size with the visible scrollbars.
515   contents_viewport_->SetBoundsRect(viewport_bounds);
516   if (should_layout_contents && contents_)
517     contents_->Layout();
518 
519   // Even when |contents_| needs to scroll, it can still be narrower or wider
520   // the viewport. So ensure the scrolling layer can fill the viewport, so that
521   // events will correctly hit it, and overscroll looks correct.
522   if (contents_ && ScrollsWithLayers()) {
523     gfx::Size container_size = contents_ ? contents_->size() : gfx::Size();
524     container_size.SetToMax(viewport_bounds.size());
525     contents_->SetBoundsRect(gfx::Rect(container_size));
526     contents_->layer()->SetScrollable(viewport_bounds.size());
527 
528     // Flip the viewport with layer transforms under RTL. Note the net effect is
529     // to flip twice, so the text is not mirrored. This is necessary because
530     // compositor scrolling is not RTL-aware. So although a toolkit-views layout
531     // will flip, increasing a horizontal gfx::ScrollOffset will move content to
532     // the left, regardless of RTL. A gfx::ScrollOffset must be positive, so to
533     // move (unscrolled) content to the right, we need to flip the viewport
534     // layer. That would flip all the content as well, so flip (and translate)
535     // the content layer. Compensating in this way allows the scrolling/offset
536     // logic to remain the same when scrolling via layers or bounds offsets.
537     if (base::i18n::IsRTL()) {
538       gfx::Transform flip;
539       flip.Translate(viewport_bounds.width(), 0);
540       flip.Scale(-1, 1);
541       contents_viewport_->layer()->SetTransform(flip);
542 
543       // Add `contents_->width() - viewport_width` to the translation step. This
544       // is to prevent the top-left of the (flipped) contents aligning to the
545       // top-left of the viewport. Instead, the top-right should align in RTL.
546       gfx::Transform shift;
547       shift.Translate(2 * contents_->width() - viewport_bounds.width(), 0);
548       shift.Scale(-1, 1);
549       contents_->layer()->SetTransform(shift);
550     }
551   }
552 
553   header_viewport_->SetBounds(contents_x, contents_y, viewport_bounds.width(),
554                               header_height);
555   if (header_)
556     header_->Layout();
557 
558   ConstrainScrollToBounds(header_viewport_, header_,
559                           scroll_with_layers_enabled_);
560   ConstrainScrollToBounds(contents_viewport_, contents_,
561                           scroll_with_layers_enabled_);
562   SchedulePaint();
563   UpdateScrollBarPositions();
564   if (contents_)
565     UpdateOverflowIndicatorVisibility(CurrentOffset());
566 }
567 
OnKeyPressed(const ui::KeyEvent & event)568 bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) {
569   bool processed = false;
570 
571   // Give vertical scrollbar priority
572   if (vert_sb_->GetVisible())
573     processed = vert_sb_->OnKeyPressed(event);
574 
575   if (!processed && horiz_sb_->GetVisible())
576     processed = horiz_sb_->OnKeyPressed(event);
577 
578   return processed;
579 }
580 
OnMouseWheel(const ui::MouseWheelEvent & e)581 bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) {
582   bool processed = false;
583 
584   // TODO(https://crbug.com/615948): Use composited scrolling.
585   if (vert_sb_->GetVisible())
586     processed = vert_sb_->OnMouseWheel(e);
587 
588   if (horiz_sb_->GetVisible())
589     processed = horiz_sb_->OnMouseWheel(e) || processed;
590 
591   return processed;
592 }
593 
OnScrollEvent(ui::ScrollEvent * event)594 void ScrollView::OnScrollEvent(ui::ScrollEvent* event) {
595   if (!contents_)
596     return;
597 
598   ui::ScrollInputHandler* compositor_scroller =
599       GetWidget()->GetCompositor()->scroll_input_handler();
600   if (compositor_scroller) {
601     DCHECK(scroll_with_layers_enabled_);
602     if (compositor_scroller->OnScrollEvent(*event, contents_->layer())) {
603       event->SetHandled();
604       event->StopPropagation();
605     }
606   }
607 
608   // A direction might not be known when the event stream starts, notify both
609   // scrollbars that they may be about scroll, or that they may need to cancel
610   // UI feedback once the scrolling direction is known.
611   if (horiz_sb_)
612     horiz_sb_->ObserveScrollEvent(*event);
613   if (vert_sb_)
614     vert_sb_->ObserveScrollEvent(*event);
615 }
616 
OnGestureEvent(ui::GestureEvent * event)617 void ScrollView::OnGestureEvent(ui::GestureEvent* event) {
618   // If the event happened on one of the scrollbars, then those events are
619   // sent directly to the scrollbars. Otherwise, only scroll events are sent to
620   // the scrollbars.
621   bool scroll_event = event->type() == ui::ET_GESTURE_SCROLL_UPDATE ||
622                       event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
623                       event->type() == ui::ET_GESTURE_SCROLL_END ||
624                       event->type() == ui::ET_SCROLL_FLING_START;
625 
626   // TODO(https://crbug.com/615948): Use composited scrolling.
627   if (vert_sb_->GetVisible()) {
628     if (vert_sb_->bounds().Contains(event->location()) || scroll_event)
629       vert_sb_->OnGestureEvent(event);
630   }
631   if (!event->handled() && horiz_sb_->GetVisible()) {
632     if (horiz_sb_->bounds().Contains(event->location()) || scroll_event)
633       horiz_sb_->OnGestureEvent(event);
634   }
635 }
636 
OnThemeChanged()637 void ScrollView::OnThemeChanged() {
638   View::OnThemeChanged();
639   UpdateBorder();
640   if (background_color_id_)
641     UpdateBackground();
642 }
643 
ScrollToPosition(ScrollBar * source,int position)644 void ScrollView::ScrollToPosition(ScrollBar* source, int position) {
645   if (!contents_)
646     return;
647 
648   gfx::ScrollOffset offset = CurrentOffset();
649   if (source == horiz_sb_.get() && horiz_sb_->GetVisible()) {
650     position = AdjustPosition(offset.x(), position, contents_->width(),
651                               contents_viewport_->width());
652     if (offset.x() == position)
653       return;
654     offset.set_x(position);
655   } else if (source == vert_sb_.get() && vert_sb_->GetVisible()) {
656     position = AdjustPosition(offset.y(), position, contents_->height(),
657                               contents_viewport_->height());
658     if (offset.y() == position)
659       return;
660     offset.set_y(position);
661   }
662   ScrollToOffset(offset);
663 
664   if (!ScrollsWithLayers())
665     contents_->SchedulePaintInRect(contents_->GetVisibleBounds());
666 }
667 
GetScrollIncrement(ScrollBar * source,bool is_page,bool is_positive)668 int ScrollView::GetScrollIncrement(ScrollBar* source,
669                                    bool is_page,
670                                    bool is_positive) {
671   bool is_horizontal = source->IsHorizontal();
672   if (is_page) {
673     return is_horizontal ? contents_viewport_->width()
674                          : contents_viewport_->height();
675   }
676   return is_horizontal ? contents_viewport_->width() / 5
677                        : contents_viewport_->height() / 5;
678 }
679 
DoesViewportOrScrollViewHaveLayer() const680 bool ScrollView::DoesViewportOrScrollViewHaveLayer() const {
681   return layer() || contents_viewport_->layer();
682 }
683 
UpdateViewportLayerForClipping()684 void ScrollView::UpdateViewportLayerForClipping() {
685   if (scroll_with_layers_enabled_)
686     return;
687 
688   const bool has_layer = DoesViewportOrScrollViewHaveLayer();
689   const bool needs_layer = DoesDescendantHaveLayer(contents_viewport_);
690   if (has_layer == needs_layer)
691     return;
692   if (needs_layer)
693     EnableViewportLayer();
694   else
695     contents_viewport_->DestroyLayer();
696 }
697 
SetHeaderOrContents(View * parent,std::unique_ptr<View> new_view,View ** member)698 void ScrollView::SetHeaderOrContents(View* parent,
699                                      std::unique_ptr<View> new_view,
700                                      View** member) {
701   delete *member;
702   if (new_view.get())
703     *member = parent->AddChildView(std::move(new_view));
704   else
705     *member = nullptr;
706   // TODO(https://crbug.com/947053): this should call InvalidateLayout(), but
707   // there are some assumptions that it call Layout(). These assumptions should
708   // be updated.
709   Layout();
710 }
711 
ScrollContentsRegionToBeVisible(const gfx::Rect & rect)712 void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) {
713   if (!contents_ || (!horiz_sb_->GetVisible() && !vert_sb_->GetVisible()))
714     return;
715 
716   // Figure out the maximums for this scroll view.
717   const int contents_max_x =
718       std::max(contents_viewport_->width(), contents_->width());
719   const int contents_max_y =
720       std::max(contents_viewport_->height(), contents_->height());
721 
722   int x = base::ClampToRange(rect.x(), 0, contents_max_x);
723   int y = base::ClampToRange(rect.y(), 0, contents_max_y);
724 
725   // Figure out how far and down the rectangle will go taking width
726   // and height into account.  This will be "clipped" by the viewport.
727   const int max_x = std::min(
728       contents_max_x, x + std::min(rect.width(), contents_viewport_->width()));
729   const int max_y =
730       std::min(contents_max_y,
731                y + std::min(rect.height(), contents_viewport_->height()));
732 
733   // See if the rect is already visible. Note the width is (max_x - x)
734   // and the height is (max_y - y) to take into account the clipping of
735   // either viewport or the content size.
736   const gfx::Rect vis_rect = GetVisibleRect();
737   if (vis_rect.Contains(gfx::Rect(x, y, max_x - x, max_y - y)))
738     return;
739 
740   // Shift contents_'s X and Y so that the region is visible. If we
741   // need to shift up or left from where we currently are then we need
742   // to get it so that the content appears in the upper/left
743   // corner. This is done by setting the offset to -X or -Y.  For down
744   // or right shifts we need to make sure it appears in the
745   // lower/right corner. This is calculated by taking max_x or max_y
746   // and scaling it back by the size of the viewport.
747   const int new_x =
748       (vis_rect.x() > x) ? x : std::max(0, max_x - contents_viewport_->width());
749   const int new_y = (vis_rect.y() > y)
750                         ? y
751                         : std::max(0, max_y - contents_viewport_->height());
752 
753   ScrollToOffset(gfx::ScrollOffset(new_x, new_y));
754 }
755 
ComputeScrollBarsVisibility(const gfx::Size & vp_size,const gfx::Size & content_size,bool * horiz_is_shown,bool * vert_is_shown) const756 void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size,
757                                              const gfx::Size& content_size,
758                                              bool* horiz_is_shown,
759                                              bool* vert_is_shown) const {
760   if (hide_horizontal_scrollbar_) {
761     *horiz_is_shown = false;
762     *vert_is_shown = content_size.height() > vp_size.height();
763     return;
764   }
765 
766   // Try to fit both ways first, then try vertical bar only, then horizontal
767   // bar only, then defaults to both shown.
768   if (content_size.width() <= vp_size.width() &&
769       content_size.height() <= vp_size.height()) {
770     *horiz_is_shown = false;
771     *vert_is_shown = false;
772   } else if (content_size.width() <=
773              vp_size.width() - GetScrollBarLayoutWidth()) {
774     *horiz_is_shown = false;
775     *vert_is_shown = true;
776   } else if (content_size.height() <=
777              vp_size.height() - GetScrollBarLayoutHeight()) {
778     *horiz_is_shown = true;
779     *vert_is_shown = false;
780   } else {
781     *horiz_is_shown = true;
782     *vert_is_shown = true;
783   }
784 }
785 
786 // Make sure that a single scrollbar is created and visible as needed
SetControlVisibility(View * control,bool should_show)787 void ScrollView::SetControlVisibility(View* control, bool should_show) {
788   if (!control)
789     return;
790   if (should_show) {
791     if (!control->GetVisible()) {
792       AddChildView(control);
793       control->SetVisible(true);
794     }
795   } else {
796     RemoveChildView(control);
797     control->SetVisible(false);
798   }
799 }
800 
UpdateScrollBarPositions()801 void ScrollView::UpdateScrollBarPositions() {
802   if (!contents_)
803     return;
804 
805   const gfx::ScrollOffset offset = CurrentOffset();
806   if (horiz_sb_->GetVisible()) {
807     int vw = contents_viewport_->width();
808     int cw = contents_->width();
809     horiz_sb_->Update(vw, cw, offset.x());
810   }
811   if (vert_sb_->GetVisible()) {
812     int vh = contents_viewport_->height();
813     int ch = contents_->height();
814     vert_sb_->Update(vh, ch, offset.y());
815   }
816 }
817 
CurrentOffset() const818 gfx::ScrollOffset ScrollView::CurrentOffset() const {
819   return ScrollsWithLayers()
820              ? contents_->layer()->CurrentScrollOffset()
821              : gfx::ScrollOffset(-contents_->x(), -contents_->y());
822 }
823 
ScrollToOffset(const gfx::ScrollOffset & offset)824 void ScrollView::ScrollToOffset(const gfx::ScrollOffset& offset) {
825   if (ScrollsWithLayers()) {
826     contents_->layer()->SetScrollOffset(offset);
827 
828     // TODO(tapted): Remove this call to OnLayerScrolled(). It's unnecessary,
829     // but will only be invoked (asynchronously) when a Compositor is present
830     // and commits a frame, which isn't true in some tests.
831     // See http://crbug.com/637521.
832     OnLayerScrolled(offset, contents_->layer()->element_id());
833   } else {
834     contents_->SetPosition(gfx::Point(-offset.x(), -offset.y()));
835     ScrollHeader();
836   }
837   UpdateOverflowIndicatorVisibility(offset);
838   UpdateScrollBarPositions();
839 }
840 
ScrollsWithLayers() const841 bool ScrollView::ScrollsWithLayers() const {
842   if (!scroll_with_layers_enabled_)
843     return false;
844   // Just check for the presence of a layer since it's cheaper than querying the
845   // Feature flag each time.
846   return contents_viewport_->layer() != nullptr;
847 }
848 
EnableViewportLayer()849 void ScrollView::EnableViewportLayer() {
850   if (DoesViewportOrScrollViewHaveLayer())
851     return;
852 
853   contents_viewport_->SetPaintToLayer();
854   contents_viewport_->layer()->SetMasksToBounds(true);
855   more_content_left_->SetPaintToLayer();
856   more_content_top_->SetPaintToLayer();
857   more_content_right_->SetPaintToLayer();
858   more_content_bottom_->SetPaintToLayer();
859   UpdateBackground();
860 }
861 
OnLayerScrolled(const gfx::ScrollOffset &,const cc::ElementId &)862 void ScrollView::OnLayerScrolled(const gfx::ScrollOffset&,
863                                  const cc::ElementId&) {
864   UpdateScrollBarPositions();
865   ScrollHeader();
866 }
867 
ScrollHeader()868 void ScrollView::ScrollHeader() {
869   if (!header_)
870     return;
871 
872   int x_offset = CurrentOffset().x();
873   if (header_->x() != -x_offset) {
874     header_->SetX(-x_offset);
875     header_->SchedulePaintInRect(header_->GetVisibleBounds());
876   }
877 }
878 
AddBorder()879 void ScrollView::AddBorder() {
880   draw_border_ = true;
881   UpdateBorder();
882 }
883 
UpdateBorder()884 void ScrollView::UpdateBorder() {
885   if (!draw_border_ || !GetWidget())
886     return;
887 
888   SetBorder(CreateSolidBorder(
889       1, GetNativeTheme()->GetSystemColor(
890              draw_focus_indicator_
891                  ? ui::NativeTheme::kColorId_FocusedBorderColor
892                  : ui::NativeTheme::kColorId_UnfocusedBorderColor)));
893 }
894 
UpdateBackground()895 void ScrollView::UpdateBackground() {
896   const base::Optional<SkColor> background_color = GetBackgroundColor();
897 
898   auto create_background = [background_color]() {
899     return background_color ? CreateSolidBackground(background_color.value())
900                             : nullptr;
901   };
902 
903   SetBackground(create_background());
904   // In addition to setting the background of |this|, set the background on
905   // the viewport as well. This way if the viewport has a layer
906   // SetFillsBoundsOpaquely() is honored.
907   contents_viewport_->SetBackground(create_background());
908   if (contents_ && ScrollsWithLayers())
909     contents_->SetBackground(create_background());
910   if (contents_viewport_->layer()) {
911     contents_viewport_->layer()->SetFillsBoundsOpaquely(!!background_color);
912   }
913   SchedulePaint();
914 }
915 
GetBackgroundColor() const916 base::Optional<SkColor> ScrollView::GetBackgroundColor() const {
917   return background_color_id_
918              ? GetNativeTheme()->GetSystemColor(background_color_id_.value())
919              : background_color_;
920 }
921 
GetBackgroundThemeColorId() const922 base::Optional<ui::NativeTheme::ColorId> ScrollView::GetBackgroundThemeColorId()
923     const {
924   return background_color_id_;
925 }
926 
PositionOverflowIndicators()927 void ScrollView::PositionOverflowIndicators() {
928   const gfx::Rect bounds = GetContentsBounds();
929   const int x = bounds.x();
930   const int y = bounds.y();
931   const int w = bounds.width();
932   const int h = bounds.height();
933   const int t = Separator::kThickness;
934   more_content_left_->SetBounds(x, y, t, h);
935   more_content_top_->SetBounds(x, y, w, t);
936   more_content_right_->SetBounds(bounds.right() - t, y, t, h);
937   more_content_bottom_->SetBounds(x, bounds.bottom() - t, w, t);
938 }
939 
UpdateOverflowIndicatorVisibility(const gfx::ScrollOffset & offset)940 void ScrollView::UpdateOverflowIndicatorVisibility(
941     const gfx::ScrollOffset& offset) {
942   SetControlVisibility(more_content_top_.get(),
943                        !draw_border_ && !header_ && vert_sb_->GetVisible() &&
944                            offset.y() > vert_sb_->GetMinPosition() &&
945                            draw_overflow_indicator_);
946   SetControlVisibility(
947       more_content_bottom_.get(),
948       !draw_border_ && vert_sb_->GetVisible() && !horiz_sb_->GetVisible() &&
949           offset.y() < vert_sb_->GetMaxPosition() && draw_overflow_indicator_);
950   SetControlVisibility(more_content_left_.get(),
951                        !draw_border_ && horiz_sb_->GetVisible() &&
952                            offset.x() > horiz_sb_->GetMinPosition() &&
953                            draw_overflow_indicator_);
954   SetControlVisibility(
955       more_content_right_.get(),
956       !draw_border_ && horiz_sb_->GetVisible() && !vert_sb_->GetVisible() &&
957           offset.x() < horiz_sb_->GetMaxPosition() && draw_overflow_indicator_);
958 }
959 
960 BEGIN_METADATA(ScrollView)
METADATA_PARENT_CLASS(View)961 METADATA_PARENT_CLASS(View)
962 ADD_READONLY_PROPERTY_METADATA(ScrollView, int, MinHeight)
963 ADD_READONLY_PROPERTY_METADATA(ScrollView, int, MaxHeight)
964 ADD_PROPERTY_METADATA(ScrollView, base::Optional<SkColor>, BackgroundColor)
965 ADD_PROPERTY_METADATA(ScrollView,
966                       base::Optional<ui::NativeTheme::ColorId>,
967                       BackgroundThemeColorId)
968 ADD_PROPERTY_METADATA(ScrollView, bool, DrawOverflowIndicator)
969 ADD_PROPERTY_METADATA(ScrollView, bool, HasFocusIndicator)
970 ADD_PROPERTY_METADATA(ScrollView, bool, HideHorizontalScrollBar)
971 END_METADATA()
972 
973 // VariableRowHeightScrollHelper ----------------------------------------------
974 
975 VariableRowHeightScrollHelper::VariableRowHeightScrollHelper(
976     Controller* controller)
977     : controller_(controller) {}
978 
979 VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() = default;
980 
GetPageScrollIncrement(ScrollView * scroll_view,bool is_horizontal,bool is_positive)981 int VariableRowHeightScrollHelper::GetPageScrollIncrement(
982     ScrollView* scroll_view,
983     bool is_horizontal,
984     bool is_positive) {
985   if (is_horizontal)
986     return 0;
987   // y coordinate is most likely negative.
988   int y = abs(scroll_view->contents()->y());
989   int vis_height = scroll_view->contents()->parent()->height();
990   if (is_positive) {
991     // Align the bottom most row to the top of the view.
992     int bottom =
993         std::min(scroll_view->contents()->height() - 1, y + vis_height);
994     RowInfo bottom_row_info = GetRowInfo(bottom);
995     // If 0, ScrollView will provide a default value.
996     return std::max(0, bottom_row_info.origin - y);
997   } else {
998     // Align the row on the previous page to to the top of the view.
999     int last_page_y = y - vis_height;
1000     RowInfo last_page_info = GetRowInfo(std::max(0, last_page_y));
1001     if (last_page_y != last_page_info.origin)
1002       return std::max(0, y - last_page_info.origin - last_page_info.height);
1003     return std::max(0, y - last_page_info.origin);
1004   }
1005 }
1006 
GetLineScrollIncrement(ScrollView * scroll_view,bool is_horizontal,bool is_positive)1007 int VariableRowHeightScrollHelper::GetLineScrollIncrement(
1008     ScrollView* scroll_view,
1009     bool is_horizontal,
1010     bool is_positive) {
1011   if (is_horizontal)
1012     return 0;
1013   // y coordinate is most likely negative.
1014   int y = abs(scroll_view->contents()->y());
1015   RowInfo row = GetRowInfo(y);
1016   if (is_positive) {
1017     return row.height - (y - row.origin);
1018   } else if (y == row.origin) {
1019     row = GetRowInfo(std::max(0, row.origin - 1));
1020     return y - row.origin;
1021   } else {
1022     return y - row.origin;
1023   }
1024 }
1025 
1026 VariableRowHeightScrollHelper::RowInfo
GetRowInfo(int y)1027 VariableRowHeightScrollHelper::GetRowInfo(int y) {
1028   return controller_->GetRowInfo(y);
1029 }
1030 
1031 // FixedRowHeightScrollHelper -----------------------------------------------
1032 
FixedRowHeightScrollHelper(int top_margin,int row_height)1033 FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin,
1034                                                        int row_height)
1035     : VariableRowHeightScrollHelper(nullptr),
1036       top_margin_(top_margin),
1037       row_height_(row_height) {
1038   DCHECK_GT(row_height, 0);
1039 }
1040 
GetRowInfo(int y)1041 VariableRowHeightScrollHelper::RowInfo FixedRowHeightScrollHelper::GetRowInfo(
1042     int y) {
1043   if (y < top_margin_)
1044     return RowInfo(0, top_margin_);
1045   return RowInfo((y - top_margin_) / row_height_ * row_height_ + top_margin_,
1046                  row_height_);
1047 }
1048 
1049 }  // namespace views
1050