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 virtual 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 virtual 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 virtual ~Viewport() {}
99 virtual const char* GetClassName() const OVERRIDE
{
100 return "ScrollView::Viewport";
103 virtual void ScrollRectToVisible(const gfx::Rect
& rect
) OVERRIDE
{
104 if (!has_children() || !parent())
107 View
* contents
= child_at(0);
108 gfx::Rect
scroll_rect(rect
);
109 scroll_rect
.Offset(-contents
->x(), -contents
->y());
110 static_cast<ScrollView
*>(parent())->ScrollContentsRegionToBeVisible(
114 virtual void ChildPreferredSizeChanged(View
* child
) OVERRIDE
{
120 DISALLOW_COPY_AND_ASSIGN(Viewport
);
123 ScrollView::ScrollView()
125 contents_viewport_(new Viewport()),
127 header_viewport_(new Viewport()),
128 horiz_sb_(new NativeScrollBar(true)),
129 vert_sb_(new NativeScrollBar(false)),
130 corner_view_(new ScrollCornerView()),
133 hide_horizontal_scrollbar_(false) {
134 set_notify_enter_exit_on_child(true);
136 AddChildView(contents_viewport_
);
137 AddChildView(header_viewport_
);
139 // Don't add the scrollbars as children until we discover we need them
140 // (ShowOrHideScrollBar).
141 horiz_sb_
->SetVisible(false);
142 horiz_sb_
->set_controller(this);
143 vert_sb_
->SetVisible(false);
144 vert_sb_
->set_controller(this);
145 corner_view_
->SetVisible(false);
148 ScrollView::~ScrollView() {
149 // The scrollbars may not have been added, delete them to ensure they get
157 ScrollView
* ScrollView::CreateScrollViewWithBorder() {
158 return new ScrollViewWithBorder();
161 void ScrollView::SetContents(View
* a_view
) {
162 SetHeaderOrContents(contents_viewport_
, a_view
, &contents_
);
165 void ScrollView::SetHeader(View
* header
) {
166 SetHeaderOrContents(header_viewport_
, header
, &header_
);
169 gfx::Rect
ScrollView::GetVisibleRect() const {
172 return gfx::Rect(-contents_
->x(), -contents_
->y(),
173 contents_viewport_
->width(), contents_viewport_
->height());
176 void ScrollView::ClipHeightTo(int min_height
, int max_height
) {
177 min_height_
= min_height
;
178 max_height_
= max_height
;
181 int ScrollView::GetScrollBarWidth() const {
182 return vert_sb_
? vert_sb_
->GetLayoutSize() : 0;
185 int ScrollView::GetScrollBarHeight() const {
186 return horiz_sb_
? horiz_sb_
->GetLayoutSize() : 0;
189 void ScrollView::SetHorizontalScrollBar(ScrollBar
* horiz_sb
) {
191 horiz_sb
->SetVisible(horiz_sb_
->visible());
193 horiz_sb
->set_controller(this);
194 horiz_sb_
= horiz_sb
;
197 void ScrollView::SetVerticalScrollBar(ScrollBar
* vert_sb
) {
199 vert_sb
->SetVisible(vert_sb_
->visible());
201 vert_sb
->set_controller(this);
205 gfx::Size
ScrollView::GetPreferredSize() const {
207 return View::GetPreferredSize();
209 gfx::Size size
= contents()->GetPreferredSize();
210 size
.SetToMax(gfx::Size(size
.width(), min_height_
));
211 size
.SetToMin(gfx::Size(size
.width(), max_height_
));
212 gfx::Insets insets
= GetInsets();
213 size
.Enlarge(insets
.width(), insets
.height());
217 int ScrollView::GetHeightForWidth(int width
) const {
219 return View::GetHeightForWidth(width
);
221 gfx::Insets insets
= GetInsets();
222 width
= std::max(0, width
- insets
.width());
223 int height
= contents()->GetHeightForWidth(width
) + insets
.height();
224 return std::min(std::max(height
, min_height_
), max_height_
);
227 void ScrollView::Layout() {
229 int content_width
= width();
230 int content_height
= contents()->GetHeightForWidth(content_width
);
231 if (content_height
> height()) {
232 content_width
= std::max(content_width
- GetScrollBarWidth(), 0);
233 content_height
= contents()->GetHeightForWidth(content_width
);
235 if (contents()->bounds().size() != gfx::Size(content_width
, content_height
))
236 contents()->SetBounds(0, 0, content_width
, content_height
);
239 // Most views will want to auto-fit the available space. Most of them want to
240 // use all available width (without overflowing) and only overflow in
241 // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc.
242 // Other views want to fit in both ways. An example is PrintView. To make both
243 // happy, assume a vertical scrollbar but no horizontal scrollbar. To override
244 // this default behavior, the inner view has to calculate the available space,
245 // used ComputeScrollBarsVisibility() to use the same calculation that is done
246 // here and sets its bound to fit within.
247 gfx::Rect viewport_bounds
= GetContentsBounds();
248 const int contents_x
= viewport_bounds
.x();
249 const int contents_y
= viewport_bounds
.y();
250 if (viewport_bounds
.IsEmpty()) {
251 // There's nothing to layout.
255 const int header_height
=
256 std::min(viewport_bounds
.height(),
257 header_
? header_
->GetPreferredSize().height() : 0);
258 viewport_bounds
.set_height(
259 std::max(0, viewport_bounds
.height() - header_height
));
260 viewport_bounds
.set_y(viewport_bounds
.y() + header_height
);
261 // viewport_size is the total client space available.
262 gfx::Size viewport_size
= viewport_bounds
.size();
263 // Assumes a vertical scrollbar since most of the current views are designed
265 int horiz_sb_height
= GetScrollBarHeight();
266 int vert_sb_width
= GetScrollBarWidth();
267 viewport_bounds
.set_width(viewport_bounds
.width() - vert_sb_width
);
268 // Update the bounds right now so the inner views can fit in it.
269 contents_viewport_
->SetBoundsRect(viewport_bounds
);
271 // Give |contents_| a chance to update its bounds if it depends on the
276 bool should_layout_contents
= false;
277 bool horiz_sb_required
= false;
278 bool vert_sb_required
= false;
280 gfx::Size content_size
= contents_
->size();
281 ComputeScrollBarsVisibility(viewport_size
,
286 bool corner_view_required
= horiz_sb_required
&& vert_sb_required
;
288 SetControlVisibility(horiz_sb_
, horiz_sb_required
);
289 SetControlVisibility(vert_sb_
, vert_sb_required
);
290 SetControlVisibility(corner_view_
, corner_view_required
);
293 if (horiz_sb_required
) {
294 viewport_bounds
.set_height(
295 std::max(0, viewport_bounds
.height() - horiz_sb_height
));
296 should_layout_contents
= true;
299 if (!vert_sb_required
) {
300 viewport_bounds
.set_width(viewport_bounds
.width() + vert_sb_width
);
301 should_layout_contents
= true;
304 if (horiz_sb_required
) {
305 int height_offset
= horiz_sb_
->GetContentOverlapSize();
306 horiz_sb_
->SetBounds(0,
307 viewport_bounds
.bottom() - height_offset
,
308 viewport_bounds
.right(),
309 horiz_sb_height
+ height_offset
);
311 if (vert_sb_required
) {
312 int width_offset
= vert_sb_
->GetContentOverlapSize();
313 vert_sb_
->SetBounds(viewport_bounds
.right() - width_offset
,
315 vert_sb_width
+ width_offset
,
316 viewport_bounds
.bottom());
318 if (corner_view_required
) {
319 // Show the resize corner.
320 corner_view_
->SetBounds(viewport_bounds
.right(),
321 viewport_bounds
.bottom(),
326 // Update to the real client size with the visible scrollbars.
327 contents_viewport_
->SetBoundsRect(viewport_bounds
);
328 if (should_layout_contents
&& contents_
)
331 header_viewport_
->SetBounds(contents_x
, contents_y
,
332 viewport_bounds
.width(), header_height
);
336 CheckScrollBounds(header_viewport_
, header_
);
337 CheckScrollBounds(contents_viewport_
, contents_
);
339 UpdateScrollBarPositions();
342 bool ScrollView::OnKeyPressed(const ui::KeyEvent
& event
) {
343 bool processed
= false;
345 // Give vertical scrollbar priority
346 if (vert_sb_
->visible())
347 processed
= vert_sb_
->OnKeyPressed(event
);
349 if (!processed
&& horiz_sb_
->visible())
350 processed
= horiz_sb_
->OnKeyPressed(event
);
355 bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent
& e
) {
356 bool processed
= false;
357 // Give vertical scrollbar priority
358 if (vert_sb_
->visible())
359 processed
= vert_sb_
->OnMouseWheel(e
);
361 if (!processed
&& horiz_sb_
->visible())
362 processed
= horiz_sb_
->OnMouseWheel(e
);
367 void ScrollView::OnMouseEntered(const ui::MouseEvent
& event
) {
369 horiz_sb_
->OnMouseEnteredScrollView(event
);
371 vert_sb_
->OnMouseEnteredScrollView(event
);
374 void ScrollView::OnMouseExited(const ui::MouseEvent
& event
) {
376 horiz_sb_
->OnMouseExitedScrollView(event
);
378 vert_sb_
->OnMouseExitedScrollView(event
);
381 void ScrollView::OnGestureEvent(ui::GestureEvent
* event
) {
382 // If the event happened on one of the scrollbars, then those events are
383 // sent directly to the scrollbars. Otherwise, only scroll events are sent to
385 bool scroll_event
= event
->type() == ui::ET_GESTURE_SCROLL_UPDATE
||
386 event
->type() == ui::ET_GESTURE_SCROLL_BEGIN
||
387 event
->type() == ui::ET_GESTURE_SCROLL_END
||
388 event
->type() == ui::ET_SCROLL_FLING_START
;
390 if (vert_sb_
->visible()) {
391 if (vert_sb_
->bounds().Contains(event
->location()) || scroll_event
)
392 vert_sb_
->OnGestureEvent(event
);
394 if (!event
->handled() && horiz_sb_
->visible()) {
395 if (horiz_sb_
->bounds().Contains(event
->location()) || scroll_event
)
396 horiz_sb_
->OnGestureEvent(event
);
400 const char* ScrollView::GetClassName() const {
401 return kViewClassName
;
404 void ScrollView::ScrollToPosition(ScrollBar
* source
, int position
) {
408 if (source
== horiz_sb_
&& horiz_sb_
->visible()) {
409 position
= AdjustPosition(contents_
->x(), position
, contents_
->width(),
410 contents_viewport_
->width());
411 if (-contents_
->x() == position
)
413 contents_
->SetX(-position
);
415 header_
->SetX(-position
);
416 header_
->SchedulePaintInRect(header_
->GetVisibleBounds());
418 } else if (source
== vert_sb_
&& vert_sb_
->visible()) {
419 position
= AdjustPosition(contents_
->y(), position
, contents_
->height(),
420 contents_viewport_
->height());
421 if (-contents_
->y() == position
)
423 contents_
->SetY(-position
);
425 contents_
->SchedulePaintInRect(contents_
->GetVisibleBounds());
428 int ScrollView::GetScrollIncrement(ScrollBar
* source
, bool is_page
,
430 bool is_horizontal
= source
->IsHorizontal();
434 amount
= contents_
->GetPageScrollIncrement(
435 this, is_horizontal
, is_positive
);
437 amount
= contents_
->GetLineScrollIncrement(
438 this, is_horizontal
, is_positive
);
443 // No view, or the view didn't return a valid amount.
445 return is_horizontal
? contents_viewport_
->width() :
446 contents_viewport_
->height();
448 return is_horizontal
? contents_viewport_
->width() / 5 :
449 contents_viewport_
->height() / 5;
452 void ScrollView::SetHeaderOrContents(View
* parent
,
455 if (*member
== new_view
)
461 parent
->AddChildView(*member
);
465 void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect
& rect
) {
466 if (!contents_
|| (!horiz_sb_
->visible() && !vert_sb_
->visible()))
469 // Figure out the maximums for this scroll view.
470 const int contents_max_x
=
471 std::max(contents_viewport_
->width(), contents_
->width());
472 const int contents_max_y
=
473 std::max(contents_viewport_
->height(), contents_
->height());
475 // Make sure x and y are within the bounds of [0,contents_max_*].
476 int x
= std::max(0, std::min(contents_max_x
, rect
.x()));
477 int y
= std::max(0, std::min(contents_max_y
, rect
.y()));
479 // Figure out how far and down the rectangle will go taking width
480 // and height into account. This will be "clipped" by the viewport.
481 const int max_x
= std::min(contents_max_x
,
482 x
+ std::min(rect
.width(), contents_viewport_
->width()));
483 const int max_y
= std::min(contents_max_y
,
484 y
+ std::min(rect
.height(), contents_viewport_
->height()));
486 // See if the rect is already visible. Note the width is (max_x - x)
487 // and the height is (max_y - y) to take into account the clipping of
488 // either viewport or the content size.
489 const gfx::Rect vis_rect
= GetVisibleRect();
490 if (vis_rect
.Contains(gfx::Rect(x
, y
, max_x
- x
, max_y
- y
)))
493 // Shift contents_'s X and Y so that the region is visible. If we
494 // need to shift up or left from where we currently are then we need
495 // to get it so that the content appears in the upper/left
496 // corner. This is done by setting the offset to -X or -Y. For down
497 // or right shifts we need to make sure it appears in the
498 // lower/right corner. This is calculated by taking max_x or max_y
499 // and scaling it back by the size of the viewport.
501 (vis_rect
.x() > x
) ? x
: std::max(0, max_x
- contents_viewport_
->width());
503 (vis_rect
.y() > y
) ? y
: std::max(0, max_y
-
504 contents_viewport_
->height());
506 contents_
->SetX(-new_x
);
508 header_
->SetX(-new_x
);
509 contents_
->SetY(-new_y
);
510 UpdateScrollBarPositions();
513 void ScrollView::ComputeScrollBarsVisibility(const gfx::Size
& vp_size
,
514 const gfx::Size
& content_size
,
515 bool* horiz_is_shown
,
516 bool* vert_is_shown
) const {
517 // Try to fit both ways first, then try vertical bar only, then horizontal
518 // bar only, then defaults to both shown.
519 if (content_size
.width() <= vp_size
.width() &&
520 content_size
.height() <= vp_size
.height()) {
521 *horiz_is_shown
= false;
522 *vert_is_shown
= false;
523 } else if (content_size
.width() <= vp_size
.width() - GetScrollBarWidth()) {
524 *horiz_is_shown
= false;
525 *vert_is_shown
= true;
526 } else if (content_size
.height() <= vp_size
.height() - GetScrollBarHeight()) {
527 *horiz_is_shown
= true;
528 *vert_is_shown
= false;
530 *horiz_is_shown
= true;
531 *vert_is_shown
= true;
534 if (hide_horizontal_scrollbar_
)
535 *horiz_is_shown
= false;
538 // Make sure that a single scrollbar is created and visible as needed
539 void ScrollView::SetControlVisibility(View
* control
, bool should_show
) {
543 if (!control
->visible()) {
544 AddChildView(control
);
545 control
->SetVisible(true);
548 RemoveChildView(control
);
549 control
->SetVisible(false);
553 void ScrollView::UpdateScrollBarPositions() {
557 if (horiz_sb_
->visible()) {
558 int vw
= contents_viewport_
->width();
559 int cw
= contents_
->width();
560 int origin
= contents_
->x();
561 horiz_sb_
->Update(vw
, cw
, -origin
);
563 if (vert_sb_
->visible()) {
564 int vh
= contents_viewport_
->height();
565 int ch
= contents_
->height();
566 int origin
= contents_
->y();
567 vert_sb_
->Update(vh
, ch
, -origin
);
571 // VariableRowHeightScrollHelper ----------------------------------------------
573 VariableRowHeightScrollHelper::VariableRowHeightScrollHelper(
574 Controller
* controller
) : controller_(controller
) {
577 VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() {
580 int VariableRowHeightScrollHelper::GetPageScrollIncrement(
581 ScrollView
* scroll_view
, bool is_horizontal
, bool is_positive
) {
584 // y coordinate is most likely negative.
585 int y
= abs(scroll_view
->contents()->y());
586 int vis_height
= scroll_view
->contents()->parent()->height();
588 // Align the bottom most row to the top of the view.
589 int bottom
= std::min(scroll_view
->contents()->height() - 1,
591 RowInfo bottom_row_info
= GetRowInfo(bottom
);
592 // If 0, ScrollView will provide a default value.
593 return std::max(0, bottom_row_info
.origin
- y
);
595 // Align the row on the previous page to to the top of the view.
596 int last_page_y
= y
- vis_height
;
597 RowInfo last_page_info
= GetRowInfo(std::max(0, last_page_y
));
598 if (last_page_y
!= last_page_info
.origin
)
599 return std::max(0, y
- last_page_info
.origin
- last_page_info
.height
);
600 return std::max(0, y
- last_page_info
.origin
);
604 int VariableRowHeightScrollHelper::GetLineScrollIncrement(
605 ScrollView
* scroll_view
, bool is_horizontal
, bool is_positive
) {
608 // y coordinate is most likely negative.
609 int y
= abs(scroll_view
->contents()->y());
610 RowInfo row
= GetRowInfo(y
);
612 return row
.height
- (y
- row
.origin
);
613 } else if (y
== row
.origin
) {
614 row
= GetRowInfo(std::max(0, row
.origin
- 1));
615 return y
- row
.origin
;
617 return y
- row
.origin
;
621 VariableRowHeightScrollHelper::RowInfo
622 VariableRowHeightScrollHelper::GetRowInfo(int y
) {
623 return controller_
->GetRowInfo(y
);
626 // FixedRowHeightScrollHelper -----------------------------------------------
628 FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin
,
630 : VariableRowHeightScrollHelper(NULL
),
631 top_margin_(top_margin
),
632 row_height_(row_height
) {
633 DCHECK_GT(row_height
, 0);
636 VariableRowHeightScrollHelper::RowInfo
637 FixedRowHeightScrollHelper::GetRowInfo(int y
) {
639 return RowInfo(0, top_margin_
);
640 return RowInfo((y
- top_margin_
) / row_height_
* row_height_
+ top_margin_
,