// 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 "base/feature_list.h" #include "base/logging.h" #include "base/macros.h" #include "ui/base/material_design/material_design_controller.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/controls/separator.h" #include "ui/views/style/platform_style.h" #include "ui/views/widget/widget.h" namespace views { const char ScrollView::kViewClassName[] = "ScrollView"; namespace { const base::Feature kToolkitViewsScrollWithLayers { "ToolkitViewsScrollWithLayers", #if defined(OS_MACOSX) base::FEATURE_ENABLED_BY_DEFAULT #else base::FEATURE_DISABLED_BY_DEFAULT #endif }; class ScrollCornerView : public View { public: ScrollCornerView() {} void OnPaint(gfx::Canvas* canvas) override { ui::NativeTheme::ExtraParams ignored; GetNativeTheme()->Paint(canvas->sk_canvas(), ui::NativeTheme::kScrollbarCorner, ui::NativeTheme::kNormal, GetLocalBounds(), ignored); } private: DISALLOW_COPY_AND_ASSIGN(ScrollCornerView); }; // Returns true if any descendants of |view| have a layer (not including // |view|). bool DoesDescendantHaveLayer(View* view) { for (int i = 0; i < view->child_count(); ++i) { View* child = view->child_at(i); if (child->layer() || DoesDescendantHaveLayer(child)) return true; } return false; } // 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) { int max = std::max(content_size - viewport_size, 0); if (current_pos < 0) return 0; if (current_pos > max) return max; return current_pos; } // 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: explicit Viewport(ScrollView* scroll_view) : scroll_view_(scroll_view) {} ~Viewport() override {} const char* GetClassName() const override { return "ScrollView::Viewport"; } void ScrollRectToVisible(const gfx::Rect& rect) override { if (!has_children() || !parent()) return; View* contents = child_at(0); 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 ChildPreferredSizeChanged(View* child) override { if (parent()) parent()->Layout(); } void ViewHierarchyChanged( const ViewHierarchyChangedDetails& details) override { if (details.is_add && IsContentsViewport() && Contains(details.parent)) scroll_view_->UpdateViewportLayerForClipping(); } void OnChildLayerChanged(View* child) override { if (IsContentsViewport()) scroll_view_->UpdateViewportLayerForClipping(); } private: bool IsContentsViewport() const { return parent() && scroll_view_->contents_viewport_ == this; } ScrollView* scroll_view_; DISALLOW_COPY_AND_ASSIGN(Viewport); }; ScrollView::ScrollView() : contents_(nullptr), contents_viewport_(new Viewport(this)), header_(nullptr), header_viewport_(new Viewport(this)), horiz_sb_(PlatformStyle::CreateScrollBar(true).release()), vert_sb_(PlatformStyle::CreateScrollBar(false).release()), corner_view_(new ScrollCornerView()), more_content_left_(std::make_unique()), more_content_top_(std::make_unique()), more_content_right_(std::make_unique()), more_content_bottom_(std::make_unique()), min_height_(-1), max_height_(-1), hide_horizontal_scrollbar_(false), scroll_with_layers_enabled_( base::FeatureList::IsEnabled(kToolkitViewsScrollWithLayers)) { set_notify_enter_exit_on_child(true); AddChildView(contents_viewport_); AddChildView(header_viewport_); // Don't add the scrollbars as children until we discover we need them // (ShowOrHideScrollBar). 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(); } UpdateBackground(); } ScrollView::~ScrollView() { // The scrollbars may not have been added, delete them to ensure they get // deleted. delete horiz_sb_; delete vert_sb_; delete corner_view_; } // static ScrollView* ScrollView::CreateScrollViewWithBorder() { auto* scroll_view = new ScrollView(); 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::SetContents(View* a_view) { // Protect against clients passing a contents view that has its own Layer. DCHECK(!a_view->layer()); if (ScrollsWithLayers()) { bool fills_opaquely = true; if (!a_view->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 // view as not filling its bounds opaquely. if (GetBackgroundColor() != SK_ColorTRANSPARENT) a_view->SetBackground(CreateSolidBackground(GetBackgroundColor())); else fills_opaquely = false; } a_view->SetPaintToLayer(); a_view->layer()->SetDidScrollCallback( base::Bind(&ScrollView::OnLayerScrolled, base::Unretained(this))); a_view->layer()->SetScrollable(contents_viewport_->bounds().size()); a_view->layer()->SetFillsBoundsOpaquely(fills_opaquely); } SetHeaderOrContents(contents_viewport_, a_view, &contents_); } void ScrollView::SetHeader(View* header) { SetHeaderOrContents(header_viewport_, header, &header_); } void ScrollView::SetBackgroundColor(SkColor color) { background_color_data_.color = color; use_color_id_ = false; UpdateBackground(); } void ScrollView::SetBackgroundThemeColorId(ui::NativeTheme::ColorId color_id) { background_color_data_.color_id = color_id; use_color_id_ = true; UpdateBackground(); } 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::ClipHeightTo(int min_height, int max_height) { min_height_ = min_height; max_height_ = max_height; } int ScrollView::GetScrollBarLayoutWidth() const { return vert_sb_ && !vert_sb_->OverlapsContent() ? vert_sb_->GetThickness() : 0; } int ScrollView::GetScrollBarLayoutHeight() const { return horiz_sb_ && !horiz_sb_->OverlapsContent() ? horiz_sb_->GetThickness() : 0; } void ScrollView::SetHorizontalScrollBar(ScrollBar* horiz_sb) { DCHECK(horiz_sb); horiz_sb->SetVisible(horiz_sb_->visible()); delete horiz_sb_; horiz_sb->set_controller(this); horiz_sb_ = horiz_sb; } void ScrollView::SetVerticalScrollBar(ScrollBar* vert_sb) { DCHECK(vert_sb); vert_sb->SetVisible(vert_sb_->visible()); delete vert_sb_; vert_sb->set_controller(this); vert_sb_ = vert_sb; } void ScrollView::SetHasFocusIndicator(bool has_focus_indicator) { if (has_focus_indicator == draw_focus_indicator_) return; draw_focus_indicator_ = has_focus_indicator; if (ui::MaterialDesignController::IsSecondaryUiMaterial()) { DCHECK_EQ(draw_focus_indicator_, !focus_ring_); if (has_focus_indicator) { focus_ring_ = FocusRing::Install(this); } else { FocusRing::Uninstall(this); focus_ring_ = nullptr; } } else { UpdateBorder(); } SchedulePaint(); } gfx::Size ScrollView::CalculatePreferredSize() const { if (!is_bounded()) return View::CalculatePreferredSize(); gfx::Size size = contents_->GetPreferredSize(); 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_->GetHeightForWidth(width) + insets.height(); return std::min(std::max(height, min_height_), max_height_); } void ScrollView::Layout() { #if defined(OS_MACOSX) // 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 (focus_ring_) focus_ring_->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 > 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. SetControlVisibility(horiz_sb_, horiz_sb_required); SetControlVisibility(vert_sb_, vert_sb_required); SetControlVisibility(corner_view_, 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()); } 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 (vert_sb_->visible()) processed = vert_sb_->OnKeyPressed(event); if (!processed && horiz_sb_->visible()) processed = horiz_sb_->OnKeyPressed(event); return processed; } bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) { bool processed = false; if (vert_sb_->visible()) processed = vert_sb_->OnMouseWheel(e); if (horiz_sb_->visible()) processed = horiz_sb_->OnMouseWheel(e) || processed; return processed; } void ScrollView::OnScrollEvent(ui::ScrollEvent* event) { #if defined(OS_MACOSX) if (!contents_) return; // TODO(tapted): Send |event| to a cc::InputHandler. For now, there's nothing // to do because Widget::OnScrollEvent() will automatically process an // unhandled ScrollEvent as a MouseWheelEvent. // 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. if (horiz_sb_) horiz_sb_->ObserveScrollEvent(*event); if (vert_sb_) vert_sb_->ObserveScrollEvent(*event); #endif } 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; if (vert_sb_->visible()) { if (vert_sb_->bounds().Contains(event->location()) || scroll_event) vert_sb_->OnGestureEvent(event); } if (!event->handled() && horiz_sb_->visible()) { if (horiz_sb_->bounds().Contains(event->location()) || scroll_event) horiz_sb_->OnGestureEvent(event); } } const char* ScrollView::GetClassName() const { return kViewClassName; } void ScrollView::OnNativeThemeChanged(const ui::NativeTheme* theme) { UpdateBorder(); if (use_color_id_) UpdateBackground(); } void ScrollView::ScrollToPosition(ScrollBar* source, int position) { if (!contents_) return; gfx::ScrollOffset offset = CurrentOffset(); if (source == horiz_sb_ && horiz_sb_->visible()) { position = AdjustPosition(offset.x(), position, contents_->width(), contents_viewport_->width()); if (offset.x() == position) return; offset.set_x(position); } else if (source == vert_sb_ && vert_sb_->visible()) { 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(); int amount = 0; if (contents_) { if (is_page) { amount = contents_->GetPageScrollIncrement( this, is_horizontal, is_positive); } else { amount = contents_->GetLineScrollIncrement( this, is_horizontal, is_positive); } if (amount > 0) return amount; } // No view, or the view didn't return a valid amount. if (is_page) { return is_horizontal ? contents_viewport_->width() : contents_viewport_->height(); } return is_horizontal ? contents_viewport_->width() / 5 : contents_viewport_->height() / 5; } 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, View* new_view, View** member) { if (*member == new_view) return; delete *member; *member = new_view; if (*member) parent->AddChildView(*member); Layout(); } void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) { if (!contents_ || (!horiz_sb_->visible() && !vert_sb_->visible())) 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()); // Make sure x and y are within the bounds of [0,contents_max_*]. int x = std::max(0, std::min(contents_max_x, rect.x())); int y = std::max(0, std::min(contents_max_y, rect.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)); UpdateScrollBarPositions(); } void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size, const gfx::Size& content_size, bool* horiz_is_shown, bool* vert_is_shown) const { if (hide_horizontal_scrollbar_) { *horiz_is_shown = false; *vert_is_shown = content_size.height() > vp_size.height(); 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->visible()) { AddChildView(control); control->SetVisible(true); } } else { RemoveChildView(control); control->SetVisible(false); } } void ScrollView::UpdateScrollBarPositions() { if (!contents_) return; const gfx::ScrollOffset offset = CurrentOffset(); if (horiz_sb_->visible()) { int vw = contents_viewport_->width(); int cw = contents_->width(); horiz_sb_->Update(vw, cw, offset.x()); } if (vert_sb_->visible()) { 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); // TODO(tapted): Remove this call to OnLayerScrolled(). It's unnecessary, // but will only be invoked (asynchronously) when a Compositor is present // and commits a frame, which isn't true in some tests. // See http://crbug.com/637521. OnLayerScrolled(offset, contents_->layer()->element_id()); } else { contents_->SetPosition(gfx::Point(-offset.x(), -offset.y())); ScrollHeader(); } UpdateOverflowIndicatorVisibility(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; } 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&, const cc::ElementId&) { UpdateScrollBarPositions(); ScrollHeader(); } 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() { const SkColor background_color = GetBackgroundColor(); SetBackground(CreateSolidBackground(background_color)); // 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(CreateSolidBackground(background_color)); if (contents_ && ScrollsWithLayers()) contents_->SetBackground(CreateSolidBackground(background_color)); if (contents_viewport_->layer()) { contents_viewport_->layer()->SetFillsBoundsOpaquely(background_color != SK_ColorTRANSPARENT); } SchedulePaint(); } SkColor ScrollView::GetBackgroundColor() const { return use_color_id_ ? GetNativeTheme()->GetSystemColor(background_color_data_.color_id) : background_color_data_.color; } void ScrollView::PositionOverflowIndicators() { const gfx::Rect bounds = GetContentsBounds(); const int x = bounds.x(); const int y = bounds.y(); const int w = bounds.width(); const int h = bounds.height(); const int t = Separator::kThickness; more_content_left_->SetBounds(x, y, t, h); more_content_top_->SetBounds(x, y, w, t); more_content_right_->SetBounds(bounds.right() - t, y, t, h); more_content_bottom_->SetBounds(x, bounds.bottom() - t, w, t); } void ScrollView::UpdateOverflowIndicatorVisibility( const gfx::ScrollOffset& offset) { SetControlVisibility(more_content_top_.get(), !draw_border_ && !header_ && vert_sb_->visible() && offset.y() > vert_sb_->GetMinPosition() && draw_overflow_indicator_); SetControlVisibility( more_content_bottom_.get(), !draw_border_ && vert_sb_->visible() && !horiz_sb_->visible() && offset.y() < vert_sb_->GetMaxPosition() && draw_overflow_indicator_); SetControlVisibility(more_content_left_.get(), !draw_border_ && horiz_sb_->visible() && offset.x() > horiz_sb_->GetMinPosition() && draw_overflow_indicator_); SetControlVisibility( more_content_right_.get(), !draw_border_ && horiz_sb_->visible() && !vert_sb_->visible() && offset.x() < horiz_sb_->GetMaxPosition() && draw_overflow_indicator_); } // VariableRowHeightScrollHelper ---------------------------------------------- VariableRowHeightScrollHelper::VariableRowHeightScrollHelper( Controller* controller) : controller_(controller) { } VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() { } 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(NULL), 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