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.
5 #include "ui/views/controls/scroll_view.h"
7 #include "base/logging.h"
8 #include "ui/events/event.h"
9 #include "ui/gfx/canvas.h"
10 #include "ui/native_theme/native_theme.h"
11 #include "ui/views/border.h"
12 #include "ui/views/controls/scrollbar/native_scroll_bar.h"
13 #include "ui/views/widget/root_view.h"
17 const char ScrollView::kViewClassName
[] = "ScrollView";
21 // Subclass of ScrollView that resets the border when the theme changes.
22 class ScrollViewWithBorder
: public views::ScrollView
{
24 ScrollViewWithBorder() {}
27 void OnNativeThemeChanged(const ui::NativeTheme
* theme
) override
{
28 SetBorder(Border::CreateSolidBorder(
30 theme
->GetSystemColor(ui::NativeTheme::kColorId_UnfocusedBorderColor
)));
34 DISALLOW_COPY_AND_ASSIGN(ScrollViewWithBorder
);
37 class ScrollCornerView
: public views::View
{
41 void OnPaint(gfx::Canvas
* canvas
) override
{
42 ui::NativeTheme::ExtraParams ignored
;
43 GetNativeTheme()->Paint(canvas
->sk_canvas(),
44 ui::NativeTheme::kScrollbarCorner
,
45 ui::NativeTheme::kNormal
,
51 DISALLOW_COPY_AND_ASSIGN(ScrollCornerView
);
54 // Returns the position for the view so that it isn't scrolled off the visible
56 int CheckScrollBounds(int viewport_size
, int content_size
, int current_pos
) {
57 int max
= std::max(content_size
- viewport_size
, 0);
60 if (current_pos
> max
)
65 // Make sure the content is not scrolled out of bounds
66 void CheckScrollBounds(View
* viewport
, View
* view
) {
70 int x
= CheckScrollBounds(viewport
->width(), view
->width(), -view
->x());
71 int y
= CheckScrollBounds(viewport
->height(), view
->height(), -view
->y());
73 // This is no op if bounds are the same
74 view
->SetBounds(-x
, -y
, view
->width(), view
->height());
77 // Used by ScrollToPosition() to make sure the new position fits within the
78 // allowed scroll range.
79 int AdjustPosition(int current_position
,
83 if (-current_position
== new_position
)
87 const int max_position
= std::max(0, content_size
- viewport_size
);
88 return (new_position
> max_position
) ? max_position
: new_position
;
93 // Viewport contains the contents View of the ScrollView.
94 class ScrollView::Viewport
: public View
{
97 ~Viewport() override
{}
99 const char* GetClassName() const override
{ return "ScrollView::Viewport"; }
101 void ScrollRectToVisible(const gfx::Rect
& rect
) override
{
102 if (!has_children() || !parent())
105 View
* contents
= child_at(0);
106 gfx::Rect
scroll_rect(rect
);
107 scroll_rect
.Offset(-contents
->x(), -contents
->y());
108 static_cast<ScrollView
*>(parent())->ScrollContentsRegionToBeVisible(
112 void ChildPreferredSizeChanged(View
* child
) override
{
118 DISALLOW_COPY_AND_ASSIGN(Viewport
);
121 ScrollView::ScrollView()
123 contents_viewport_(new Viewport()),
125 header_viewport_(new Viewport()),
126 horiz_sb_(new NativeScrollBar(true)),
127 vert_sb_(new NativeScrollBar(false)),
128 corner_view_(new ScrollCornerView()),
131 hide_horizontal_scrollbar_(false) {
132 set_notify_enter_exit_on_child(true);
134 AddChildView(contents_viewport_
);
135 AddChildView(header_viewport_
);
137 // Don't add the scrollbars as children until we discover we need them
138 // (ShowOrHideScrollBar).
139 horiz_sb_
->SetVisible(false);
140 horiz_sb_
->set_controller(this);
141 vert_sb_
->SetVisible(false);
142 vert_sb_
->set_controller(this);
143 corner_view_
->SetVisible(false);
146 ScrollView::~ScrollView() {
147 // The scrollbars may not have been added, delete them to ensure they get
155 ScrollView
* ScrollView::CreateScrollViewWithBorder() {
156 return new ScrollViewWithBorder();
159 void ScrollView::SetContents(View
* a_view
) {
160 SetHeaderOrContents(contents_viewport_
, a_view
, &contents_
);
163 void ScrollView::SetHeader(View
* header
) {
164 SetHeaderOrContents(header_viewport_
, header
, &header_
);
167 gfx::Rect
ScrollView::GetVisibleRect() const {
170 return gfx::Rect(-contents_
->x(), -contents_
->y(),
171 contents_viewport_
->width(), contents_viewport_
->height());
174 void ScrollView::ClipHeightTo(int min_height
, int max_height
) {
175 min_height_
= min_height
;
176 max_height_
= max_height
;
179 int ScrollView::GetScrollBarWidth() const {
180 return vert_sb_
? vert_sb_
->GetLayoutSize() : 0;
183 int ScrollView::GetScrollBarHeight() const {
184 return horiz_sb_
? horiz_sb_
->GetLayoutSize() : 0;
187 void ScrollView::SetHorizontalScrollBar(ScrollBar
* horiz_sb
) {
189 horiz_sb
->SetVisible(horiz_sb_
->visible());
191 horiz_sb
->set_controller(this);
192 horiz_sb_
= horiz_sb
;
195 void ScrollView::SetVerticalScrollBar(ScrollBar
* vert_sb
) {
197 vert_sb
->SetVisible(vert_sb_
->visible());
199 vert_sb
->set_controller(this);
203 gfx::Size
ScrollView::GetPreferredSize() const {
205 return View::GetPreferredSize();
207 gfx::Size size
= contents()->GetPreferredSize();
208 size
.SetToMax(gfx::Size(size
.width(), min_height_
));
209 size
.SetToMin(gfx::Size(size
.width(), max_height_
));
210 gfx::Insets insets
= GetInsets();
211 size
.Enlarge(insets
.width(), insets
.height());
215 int ScrollView::GetHeightForWidth(int width
) const {
217 return View::GetHeightForWidth(width
);
219 gfx::Insets insets
= GetInsets();
220 width
= std::max(0, width
- insets
.width());
221 int height
= contents()->GetHeightForWidth(width
) + insets
.height();
222 return std::min(std::max(height
, min_height_
), max_height_
);
225 void ScrollView::Layout() {
227 int content_width
= width();
228 int content_height
= contents()->GetHeightForWidth(content_width
);
229 if (content_height
> height()) {
230 content_width
= std::max(content_width
- GetScrollBarWidth(), 0);
231 content_height
= contents()->GetHeightForWidth(content_width
);
233 if (contents()->bounds().size() != gfx::Size(content_width
, content_height
))
234 contents()->SetBounds(0, 0, content_width
, content_height
);
237 // Most views will want to auto-fit the available space. Most of them want to
238 // use all available width (without overflowing) and only overflow in
239 // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc.
240 // Other views want to fit in both ways. An example is PrintView. To make both
241 // happy, assume a vertical scrollbar but no horizontal scrollbar. To override
242 // this default behavior, the inner view has to calculate the available space,
243 // used ComputeScrollBarsVisibility() to use the same calculation that is done
244 // here and sets its bound to fit within.
245 gfx::Rect viewport_bounds
= GetContentsBounds();
246 const int contents_x
= viewport_bounds
.x();
247 const int contents_y
= viewport_bounds
.y();
248 if (viewport_bounds
.IsEmpty()) {
249 // There's nothing to layout.
253 const int header_height
=
254 std::min(viewport_bounds
.height(),
255 header_
? header_
->GetPreferredSize().height() : 0);
256 viewport_bounds
.set_height(
257 std::max(0, viewport_bounds
.height() - header_height
));
258 viewport_bounds
.set_y(viewport_bounds
.y() + header_height
);
259 // viewport_size is the total client space available.
260 gfx::Size viewport_size
= viewport_bounds
.size();
261 // Assumes a vertical scrollbar since most of the current views are designed
263 int horiz_sb_height
= GetScrollBarHeight();
264 int vert_sb_width
= GetScrollBarWidth();
265 viewport_bounds
.set_width(viewport_bounds
.width() - vert_sb_width
);
266 // Update the bounds right now so the inner views can fit in it.
267 contents_viewport_
->SetBoundsRect(viewport_bounds
);
269 // Give |contents_| a chance to update its bounds if it depends on the
274 bool should_layout_contents
= false;
275 bool horiz_sb_required
= false;
276 bool vert_sb_required
= false;
278 gfx::Size content_size
= contents_
->size();
279 ComputeScrollBarsVisibility(viewport_size
,
284 bool corner_view_required
= horiz_sb_required
&& vert_sb_required
;
286 SetControlVisibility(horiz_sb_
, horiz_sb_required
);
287 SetControlVisibility(vert_sb_
, vert_sb_required
);
288 SetControlVisibility(corner_view_
, corner_view_required
);
291 if (horiz_sb_required
) {
292 viewport_bounds
.set_height(
293 std::max(0, viewport_bounds
.height() - horiz_sb_height
));
294 should_layout_contents
= true;
297 if (!vert_sb_required
) {
298 viewport_bounds
.set_width(viewport_bounds
.width() + vert_sb_width
);
299 should_layout_contents
= true;
302 if (horiz_sb_required
) {
303 int height_offset
= horiz_sb_
->GetContentOverlapSize();
304 horiz_sb_
->SetBounds(0,
305 viewport_bounds
.bottom() - height_offset
,
306 viewport_bounds
.right(),
307 horiz_sb_height
+ height_offset
);
309 if (vert_sb_required
) {
310 int width_offset
= vert_sb_
->GetContentOverlapSize();
311 vert_sb_
->SetBounds(viewport_bounds
.right() - width_offset
,
313 vert_sb_width
+ width_offset
,
314 viewport_bounds
.bottom());
316 if (corner_view_required
) {
317 // Show the resize corner.
318 corner_view_
->SetBounds(viewport_bounds
.right(),
319 viewport_bounds
.bottom(),
324 // Update to the real client size with the visible scrollbars.
325 contents_viewport_
->SetBoundsRect(viewport_bounds
);
326 if (should_layout_contents
&& contents_
)
329 header_viewport_
->SetBounds(contents_x
, contents_y
,
330 viewport_bounds
.width(), header_height
);
334 CheckScrollBounds(header_viewport_
, header_
);
335 CheckScrollBounds(contents_viewport_
, contents_
);
337 UpdateScrollBarPositions();
340 bool ScrollView::OnKeyPressed(const ui::KeyEvent
& event
) {
341 bool processed
= false;
343 // Give vertical scrollbar priority
344 if (vert_sb_
->visible())
345 processed
= vert_sb_
->OnKeyPressed(event
);
347 if (!processed
&& horiz_sb_
->visible())
348 processed
= horiz_sb_
->OnKeyPressed(event
);
353 bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent
& e
) {
354 bool processed
= false;
356 if (vert_sb_
->visible())
357 processed
= vert_sb_
->OnMouseWheel(e
);
359 if (horiz_sb_
->visible())
360 processed
= horiz_sb_
->OnMouseWheel(e
) || processed
;
365 void ScrollView::OnMouseEntered(const ui::MouseEvent
& event
) {
367 horiz_sb_
->OnMouseEnteredScrollView(event
);
369 vert_sb_
->OnMouseEnteredScrollView(event
);
372 void ScrollView::OnMouseExited(const ui::MouseEvent
& event
) {
374 horiz_sb_
->OnMouseExitedScrollView(event
);
376 vert_sb_
->OnMouseExitedScrollView(event
);
379 void ScrollView::OnGestureEvent(ui::GestureEvent
* event
) {
380 // If the event happened on one of the scrollbars, then those events are
381 // sent directly to the scrollbars. Otherwise, only scroll events are sent to
383 bool scroll_event
= event
->type() == ui::ET_GESTURE_SCROLL_UPDATE
||
384 event
->type() == ui::ET_GESTURE_SCROLL_BEGIN
||
385 event
->type() == ui::ET_GESTURE_SCROLL_END
||
386 event
->type() == ui::ET_SCROLL_FLING_START
;
388 if (vert_sb_
->visible()) {
389 if (vert_sb_
->bounds().Contains(event
->location()) || scroll_event
)
390 vert_sb_
->OnGestureEvent(event
);
392 if (!event
->handled() && horiz_sb_
->visible()) {
393 if (horiz_sb_
->bounds().Contains(event
->location()) || scroll_event
)
394 horiz_sb_
->OnGestureEvent(event
);
398 const char* ScrollView::GetClassName() const {
399 return kViewClassName
;
402 void ScrollView::ScrollToPosition(ScrollBar
* source
, int position
) {
406 if (source
== horiz_sb_
&& horiz_sb_
->visible()) {
407 position
= AdjustPosition(contents_
->x(), position
, contents_
->width(),
408 contents_viewport_
->width());
409 if (-contents_
->x() == position
)
411 contents_
->SetX(-position
);
413 header_
->SetX(-position
);
414 header_
->SchedulePaintInRect(header_
->GetVisibleBounds());
416 } else if (source
== vert_sb_
&& vert_sb_
->visible()) {
417 position
= AdjustPosition(contents_
->y(), position
, contents_
->height(),
418 contents_viewport_
->height());
419 if (-contents_
->y() == position
)
421 contents_
->SetY(-position
);
423 contents_
->SchedulePaintInRect(contents_
->GetVisibleBounds());
426 int ScrollView::GetScrollIncrement(ScrollBar
* source
, bool is_page
,
428 bool is_horizontal
= source
->IsHorizontal();
432 amount
= contents_
->GetPageScrollIncrement(
433 this, is_horizontal
, is_positive
);
435 amount
= contents_
->GetLineScrollIncrement(
436 this, is_horizontal
, is_positive
);
441 // No view, or the view didn't return a valid amount.
443 return is_horizontal
? contents_viewport_
->width() :
444 contents_viewport_
->height();
446 return is_horizontal
? contents_viewport_
->width() / 5 :
447 contents_viewport_
->height() / 5;
450 void ScrollView::SetHeaderOrContents(View
* parent
,
453 if (*member
== new_view
)
459 parent
->AddChildView(*member
);
463 void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect
& rect
) {
464 if (!contents_
|| (!horiz_sb_
->visible() && !vert_sb_
->visible()))
467 // Figure out the maximums for this scroll view.
468 const int contents_max_x
=
469 std::max(contents_viewport_
->width(), contents_
->width());
470 const int contents_max_y
=
471 std::max(contents_viewport_
->height(), contents_
->height());
473 // Make sure x and y are within the bounds of [0,contents_max_*].
474 int x
= std::max(0, std::min(contents_max_x
, rect
.x()));
475 int y
= std::max(0, std::min(contents_max_y
, rect
.y()));
477 // Figure out how far and down the rectangle will go taking width
478 // and height into account. This will be "clipped" by the viewport.
479 const int max_x
= std::min(contents_max_x
,
480 x
+ std::min(rect
.width(), contents_viewport_
->width()));
481 const int max_y
= std::min(contents_max_y
,
482 y
+ std::min(rect
.height(), contents_viewport_
->height()));
484 // See if the rect is already visible. Note the width is (max_x - x)
485 // and the height is (max_y - y) to take into account the clipping of
486 // either viewport or the content size.
487 const gfx::Rect vis_rect
= GetVisibleRect();
488 if (vis_rect
.Contains(gfx::Rect(x
, y
, max_x
- x
, max_y
- y
)))
491 // Shift contents_'s X and Y so that the region is visible. If we
492 // need to shift up or left from where we currently are then we need
493 // to get it so that the content appears in the upper/left
494 // corner. This is done by setting the offset to -X or -Y. For down
495 // or right shifts we need to make sure it appears in the
496 // lower/right corner. This is calculated by taking max_x or max_y
497 // and scaling it back by the size of the viewport.
499 (vis_rect
.x() > x
) ? x
: std::max(0, max_x
- contents_viewport_
->width());
501 (vis_rect
.y() > y
) ? y
: std::max(0, max_y
-
502 contents_viewport_
->height());
504 contents_
->SetX(-new_x
);
506 header_
->SetX(-new_x
);
507 contents_
->SetY(-new_y
);
508 UpdateScrollBarPositions();
511 void ScrollView::ComputeScrollBarsVisibility(const gfx::Size
& vp_size
,
512 const gfx::Size
& content_size
,
513 bool* horiz_is_shown
,
514 bool* vert_is_shown
) const {
515 // Try to fit both ways first, then try vertical bar only, then horizontal
516 // bar only, then defaults to both shown.
517 if (content_size
.width() <= vp_size
.width() &&
518 content_size
.height() <= vp_size
.height()) {
519 *horiz_is_shown
= false;
520 *vert_is_shown
= false;
521 } else if (content_size
.width() <= vp_size
.width() - GetScrollBarWidth()) {
522 *horiz_is_shown
= false;
523 *vert_is_shown
= true;
524 } else if (content_size
.height() <= vp_size
.height() - GetScrollBarHeight()) {
525 *horiz_is_shown
= true;
526 *vert_is_shown
= false;
528 *horiz_is_shown
= true;
529 *vert_is_shown
= true;
532 if (hide_horizontal_scrollbar_
)
533 *horiz_is_shown
= false;
536 // Make sure that a single scrollbar is created and visible as needed
537 void ScrollView::SetControlVisibility(View
* control
, bool should_show
) {
541 if (!control
->visible()) {
542 AddChildView(control
);
543 control
->SetVisible(true);
546 RemoveChildView(control
);
547 control
->SetVisible(false);
551 void ScrollView::UpdateScrollBarPositions() {
555 if (horiz_sb_
->visible()) {
556 int vw
= contents_viewport_
->width();
557 int cw
= contents_
->width();
558 int origin
= contents_
->x();
559 horiz_sb_
->Update(vw
, cw
, -origin
);
561 if (vert_sb_
->visible()) {
562 int vh
= contents_viewport_
->height();
563 int ch
= contents_
->height();
564 int origin
= contents_
->y();
565 vert_sb_
->Update(vh
, ch
, -origin
);
569 // VariableRowHeightScrollHelper ----------------------------------------------
571 VariableRowHeightScrollHelper::VariableRowHeightScrollHelper(
572 Controller
* controller
) : controller_(controller
) {
575 VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() {
578 int VariableRowHeightScrollHelper::GetPageScrollIncrement(
579 ScrollView
* scroll_view
, bool is_horizontal
, bool is_positive
) {
582 // y coordinate is most likely negative.
583 int y
= abs(scroll_view
->contents()->y());
584 int vis_height
= scroll_view
->contents()->parent()->height();
586 // Align the bottom most row to the top of the view.
587 int bottom
= std::min(scroll_view
->contents()->height() - 1,
589 RowInfo bottom_row_info
= GetRowInfo(bottom
);
590 // If 0, ScrollView will provide a default value.
591 return std::max(0, bottom_row_info
.origin
- y
);
593 // Align the row on the previous page to to the top of the view.
594 int last_page_y
= y
- vis_height
;
595 RowInfo last_page_info
= GetRowInfo(std::max(0, last_page_y
));
596 if (last_page_y
!= last_page_info
.origin
)
597 return std::max(0, y
- last_page_info
.origin
- last_page_info
.height
);
598 return std::max(0, y
- last_page_info
.origin
);
602 int VariableRowHeightScrollHelper::GetLineScrollIncrement(
603 ScrollView
* scroll_view
, bool is_horizontal
, bool is_positive
) {
606 // y coordinate is most likely negative.
607 int y
= abs(scroll_view
->contents()->y());
608 RowInfo row
= GetRowInfo(y
);
610 return row
.height
- (y
- row
.origin
);
611 } else if (y
== row
.origin
) {
612 row
= GetRowInfo(std::max(0, row
.origin
- 1));
613 return y
- row
.origin
;
615 return y
- row
.origin
;
619 VariableRowHeightScrollHelper::RowInfo
620 VariableRowHeightScrollHelper::GetRowInfo(int y
) {
621 return controller_
->GetRowInfo(y
);
624 // FixedRowHeightScrollHelper -----------------------------------------------
626 FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin
,
628 : VariableRowHeightScrollHelper(NULL
),
629 top_margin_(top_margin
),
630 row_height_(row_height
) {
631 DCHECK_GT(row_height
, 0);
634 VariableRowHeightScrollHelper::RowInfo
635 FixedRowHeightScrollHelper::GetRowInfo(int y
) {
637 return RowInfo(0, top_margin_
);
638 return RowInfo((y
- top_margin_
) / row_height_
* row_height_
+ top_margin_
,