blob: 0659d09ce283edca77459bf6ecbb6cf7195a0a2b [file] [log] [blame]
// Copyright 2016 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.
#import "ui/views/controls/scrollbar/cocoa_scroll_bar.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/stl_util.h"
#include "cc/paint/paint_shader.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
namespace views {
namespace {
// The length of the fade animation.
const int kFadeDurationMs = 240;
// The length of the expand animation.
const int kExpandDurationMs = 240;
// How long we should wait before hiding the scrollbar.
const int kScrollbarHideTimeoutMs = 500;
// The thickness of the normal and expanded scrollbars.
const int kScrollbarThickness = 12;
const int kExpandedScrollbarThickness = 16;
// The width of the scroller track border.
const int kScrollerTrackBorderWidth = 1;
// The amount the thumb is inset from the ends and the inside edge of track
// border.
const int kScrollbarThumbInset = 2;
// Scrollbar thumb colors.
const SkColor kScrollerDefaultThumbColor = SkColorSetARGB(0x38, 0, 0, 0);
const SkColor kScrollerHoverThumbColor = SkColorSetARGB(0x80, 0, 0, 0);
// Opacity of the overlay scrollbar.
const float kOverlayOpacity = 0.8f;
// Scroller track colors.
const SkColor kScrollerTrackGradientColors[] = {
SkColorSetRGB(0xEF, 0xEF, 0xEF), SkColorSetRGB(0xF9, 0xF9, 0xF9),
SkColorSetRGB(0xFD, 0xFD, 0xFD), SkColorSetRGB(0xF6, 0xF6, 0xF6)};
const SkColor kScrollerTrackInnerBorderColor = SkColorSetRGB(0xE4, 0xE4, 0xE4);
const SkColor kScrollerTrackOuterBorderColor = SkColorSetRGB(0xEF, 0xEF, 0xEF);
} // namespace
//////////////////////////////////////////////////////////////////
// CocoaScrollBarThumb
class CocoaScrollBarThumb : public BaseScrollBarThumb {
public:
explicit CocoaScrollBarThumb(CocoaScrollBar* scroll_bar);
~CocoaScrollBarThumb() override;
// Returns true if the thumb is in hovered state.
bool IsStateHovered() const;
// Returns true if the thumb is in pressed state.
bool IsStatePressed() const;
void UpdateIsMouseOverTrack(bool mouse_over_track);
protected:
// View:
gfx::Size CalculatePreferredSize() const override;
void OnPaint(gfx::Canvas* canvas) override;
bool OnMousePressed(const ui::MouseEvent& event) override;
void OnMouseReleased(const ui::MouseEvent& event) override;
void OnMouseEntered(const ui::MouseEvent& event) override;
void OnMouseExited(const ui::MouseEvent& event) override;
private:
// The CocoaScrollBar that owns us.
CocoaScrollBar* cocoa_scroll_bar_; // weak.
DISALLOW_COPY_AND_ASSIGN(CocoaScrollBarThumb);
};
CocoaScrollBarThumb::CocoaScrollBarThumb(CocoaScrollBar* scroll_bar)
: BaseScrollBarThumb(scroll_bar), cocoa_scroll_bar_(scroll_bar) {
DCHECK(scroll_bar);
// This is necessary, otherwise the thumb will be rendered below the views if
// those views paint to their own layers.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
}
CocoaScrollBarThumb::~CocoaScrollBarThumb() {}
bool CocoaScrollBarThumb::IsStateHovered() const {
return GetState() == Button::STATE_HOVERED;
}
bool CocoaScrollBarThumb::IsStatePressed() const {
return GetState() == Button::STATE_PRESSED;
}
void CocoaScrollBarThumb::UpdateIsMouseOverTrack(bool mouse_over_track) {
// The state should not change if the thumb is pressed. The thumb will be
// set back to its hover or normal state when the mouse is released.
if (IsStatePressed())
return;
SetState(mouse_over_track ? Button::STATE_HOVERED : Button::STATE_NORMAL);
}
gfx::Size CocoaScrollBarThumb::CalculatePreferredSize() const {
int thickness = cocoa_scroll_bar_->ScrollbarThickness();
return gfx::Size(thickness, thickness);
}
void CocoaScrollBarThumb::OnPaint(gfx::Canvas* canvas) {
SkColor thumb_color = kScrollerDefaultThumbColor;
if (cocoa_scroll_bar_->GetScrollerStyle() == NSScrollerStyleOverlay ||
IsStateHovered() || IsStatePressed()) {
thumb_color = kScrollerHoverThumbColor;
}
gfx::Rect bounds(GetLocalBounds());
bounds.Inset(kScrollbarThumbInset, kScrollbarThumbInset);
if (IsHorizontal())
bounds.Inset(0, kScrollerTrackBorderWidth, 0, 0);
else if (base::i18n::IsRTL())
bounds.Inset(0, 0, kScrollerTrackBorderWidth, 0);
else
bounds.Inset(kScrollerTrackBorderWidth, 0, 0, 0);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(thumb_color);
const SkScalar radius = std::min(bounds.width(), bounds.height());
canvas->DrawRoundRect(bounds, radius, flags);
}
bool CocoaScrollBarThumb::OnMousePressed(const ui::MouseEvent& event) {
// Ignore the mouse press if the scrollbar is hidden.
if (cocoa_scroll_bar_->IsScrollbarFullyHidden())
return false;
return BaseScrollBarThumb::OnMousePressed(event);
}
void CocoaScrollBarThumb::OnMouseReleased(const ui::MouseEvent& event) {
BaseScrollBarThumb::OnMouseReleased(event);
scroll_bar()->OnMouseReleased(event);
}
void CocoaScrollBarThumb::OnMouseEntered(const ui::MouseEvent& event) {
BaseScrollBarThumb::OnMouseEntered(event);
scroll_bar()->OnMouseEntered(event);
}
void CocoaScrollBarThumb::OnMouseExited(const ui::MouseEvent& event) {
// The thumb should remain pressed when dragged, even if the mouse leaves
// the scrollview. The thumb will be set back to its hover or normal state
// when the mouse is released.
if (GetState() != Button::STATE_PRESSED)
SetState(Button::STATE_NORMAL);
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar class
CocoaScrollBar::CocoaScrollBar(bool horizontal)
: BaseScrollBar(horizontal),
hide_scrollbar_timer_(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kScrollbarHideTimeoutMs),
base::Bind(&CocoaScrollBar::HideScrollbar, base::Unretained(this))),
thickness_animation_(this),
last_contents_scroll_offset_(0),
is_expanded_(false),
did_start_dragging_(false) {
SetThumb(new CocoaScrollBarThumb(this));
bridge_.reset([[ViewsScrollbarBridge alloc] initWithDelegate:this]);
scroller_style_ = [ViewsScrollbarBridge getPreferredScrollerStyle];
thickness_animation_.SetSlideDuration(kExpandDurationMs);
SetPaintToLayer();
has_scrolltrack_ = scroller_style_ == NSScrollerStyleLegacy;
layer()->SetOpacity(scroller_style_ == NSScrollerStyleOverlay ? 0.0f : 1.0f);
}
CocoaScrollBar::~CocoaScrollBar() {
[bridge_ clearDelegate];
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar, BaseScrollBar:
gfx::Rect CocoaScrollBar::GetTrackBounds() const {
return GetLocalBounds();
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar, ScrollBar:
int CocoaScrollBar::GetThickness() const {
return ScrollbarThickness();
}
bool CocoaScrollBar::OverlapsContent() const {
return scroller_style_ == NSScrollerStyleOverlay;
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar::View:
void CocoaScrollBar::Layout() {
// Set the thickness of the thumb according to the track bounds.
// The length of the thumb is set by BaseScrollBar::Update().
gfx::Rect thumb_bounds(GetThumb()->bounds());
gfx::Rect track_bounds(GetTrackBounds());
if (IsHorizontal()) {
GetThumb()->SetBounds(thumb_bounds.x(),
track_bounds.y(),
thumb_bounds.width(),
track_bounds.height());
} else {
GetThumb()->SetBounds(track_bounds.x(),
thumb_bounds.y(),
track_bounds.width(),
thumb_bounds.height());
}
}
gfx::Size CocoaScrollBar::CalculatePreferredSize() const {
return gfx::Size();
}
void CocoaScrollBar::OnPaint(gfx::Canvas* canvas) {
if (!has_scrolltrack_)
return;
// Paint the scrollbar track background.
gfx::Rect track_rect = GetLocalBounds();
SkPoint gradient_bounds[2];
if (IsHorizontal()) {
gradient_bounds[0].set(track_rect.x(), track_rect.y());
gradient_bounds[1].set(track_rect.x(), track_rect.bottom());
} else {
gradient_bounds[0].set(track_rect.x(), track_rect.y());
gradient_bounds[1].set(track_rect.right(), track_rect.y());
}
cc::PaintFlags gradient;
gradient.setShader(cc::PaintShader::MakeLinearGradient(
gradient_bounds, kScrollerTrackGradientColors, nullptr,
base::size(kScrollerTrackGradientColors), SkShader::kClamp_TileMode));
canvas->DrawRect(track_rect, gradient);
// Draw the inner border: top if horizontal, left if vertical.
cc::PaintFlags flags;
flags.setColor(kScrollerTrackInnerBorderColor);
gfx::Rect inner_border(track_rect);
if (IsHorizontal())
inner_border.set_height(kScrollerTrackBorderWidth);
else
inner_border.set_width(kScrollerTrackBorderWidth);
canvas->DrawRect(inner_border, flags);
// Draw the outer border: bottom if horizontal, right if veritcal.
flags.setColor(kScrollerTrackOuterBorderColor);
gfx::Rect outer_border(inner_border);
if (IsHorizontal())
outer_border.set_y(track_rect.bottom());
else
outer_border.set_x(track_rect.right());
canvas->DrawRect(outer_border, flags);
}
bool CocoaScrollBar::OnMousePressed(const ui::MouseEvent& event) {
// Ignore the mouse press if the scrollbar is hidden.
if (IsScrollbarFullyHidden())
return false;
return BaseScrollBar::OnMousePressed(event);
}
void CocoaScrollBar::OnMouseReleased(const ui::MouseEvent& event) {
ResetOverlayScrollbar();
BaseScrollBar::OnMouseReleased(event);
}
void CocoaScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
GetCocoaScrollBarThumb()->UpdateIsMouseOverTrack(true);
if (scroller_style_ == NSScrollerStyleLegacy)
return;
// If the scrollbar thumb did not completely fade away, then reshow it when
// the mouse enters the scrollbar thumb.
if (!IsScrollbarFullyHidden())
ShowScrollbar();
// Expand the scrollbar. If the scrollbar is hidden, don't animate it.
if (!is_expanded_) {
SetScrolltrackVisible(true);
is_expanded_ = true;
if (IsScrollbarFullyHidden()) {
thickness_animation_.Reset(1.0);
UpdateScrollbarThickness();
} else {
thickness_animation_.Show();
}
}
hide_scrollbar_timer_.Reset();
}
void CocoaScrollBar::OnMouseExited(const ui::MouseEvent& event) {
GetCocoaScrollBarThumb()->UpdateIsMouseOverTrack(false);
ResetOverlayScrollbar();
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar::ScrollBar:
void CocoaScrollBar::Update(int viewport_size,
int content_size,
int contents_scroll_offset) {
// TODO(tapted): Pass in overscroll amounts from the Layer and "Squish" the
// scroller thumb accordingly.
BaseScrollBar::Update(viewport_size, content_size, contents_scroll_offset);
// Only reveal the scroller when |contents_scroll_offset| changes. Note this
// is different to GetPosition() which can change due to layout. A layout
// change can also change the offset; show the scroller in these cases. This
// is consistent with WebContents (Cocoa will also show a scroller with any
// mouse-initiated layout, but not programmatic size changes).
if (contents_scroll_offset == last_contents_scroll_offset_)
return;
last_contents_scroll_offset_ = contents_scroll_offset;
if (GetCocoaScrollBarThumb()->IsStatePressed())
did_start_dragging_ = true;
if (scroller_style_ == NSScrollerStyleOverlay) {
ShowScrollbar();
hide_scrollbar_timer_.Reset();
}
}
void CocoaScrollBar::ObserveScrollEvent(const ui::ScrollEvent& event) {
// Do nothing if the delayed hide timer is running. This means there has been
// some recent scrolling in this direction already.
if (scroller_style_ != NSScrollerStyleOverlay ||
hide_scrollbar_timer_.IsRunning()) {
return;
}
// Otherwise, when starting the event stream, show an overlay scrollbar to
// indicate possible scroll directions, but do not start the hide timer.
if (event.momentum_phase() == ui::EventMomentumPhase::MAY_BEGIN) {
// Show only if the direction isn't yet known.
if (event.x_offset() == 0 && event.y_offset() == 0)
ShowScrollbar();
return;
}
// If the direction matches, do nothing. This is needed in addition to the
// hide timer check because Update() is called asynchronously, after event
// processing. So when |event| is the first event in a particular direction
// the hide timer will not have started.
if ((IsHorizontal() ? event.x_offset() : event.y_offset()) != 0)
return;
// Otherwise, scrolling has started, but not in this scroller direction. If
// already faded out, don't start another fade animation since that would
// immediately finish the first fade animation.
if (layer()->GetTargetOpacity() != 0) {
// If canceling rather than picking a direction, fade out after a delay.
if (event.momentum_phase() == ui::EventMomentumPhase::END)
hide_scrollbar_timer_.Reset();
else
HideScrollbar(); // Fade out immediately.
}
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar::ViewsScrollbarBridge:
void CocoaScrollBar::OnScrollerStyleChanged() {
NSScrollerStyle scroller_style =
[ViewsScrollbarBridge getPreferredScrollerStyle];
if (scroller_style_ == scroller_style)
return;
// Cancel all of the animations.
thickness_animation_.Reset();
layer()->GetAnimator()->AbortAllAnimations();
scroller_style_ = scroller_style;
// Ensure that the ScrollView updates the scrollbar's layout.
if (parent())
parent()->Layout();
if (scroller_style_ == NSScrollerStyleOverlay) {
// Hide the scrollbar, but don't fade out.
layer()->SetOpacity(0.0f);
ResetOverlayScrollbar();
GetThumb()->SchedulePaint();
} else {
is_expanded_ = false;
SetScrolltrackVisible(true);
ShowScrollbar();
}
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar::ImplicitAnimationObserver:
void CocoaScrollBar::OnImplicitAnimationsCompleted() {
DCHECK_EQ(scroller_style_, NSScrollerStyleOverlay);
ResetOverlayScrollbar();
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar::AnimationDelegate:
void CocoaScrollBar::AnimationProgressed(const gfx::Animation* animation) {
DCHECK(is_expanded_);
UpdateScrollbarThickness();
}
void CocoaScrollBar::AnimationEnded(const gfx::Animation* animation) {
// Remove the scrolltrack and set |is_expanded| to false at the end of
// the shrink animation.
if (!thickness_animation_.IsShowing()) {
is_expanded_ = false;
SetScrolltrackVisible(false);
}
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar, public:
int CocoaScrollBar::ScrollbarThickness() const {
if (scroller_style_ == NSScrollerStyleLegacy)
return kScrollbarThickness;
return thickness_animation_.CurrentValueBetween(kScrollbarThickness,
kExpandedScrollbarThickness);
}
bool CocoaScrollBar::IsScrollbarFullyHidden() const {
return layer()->opacity() == 0.0f;
}
//////////////////////////////////////////////////////////////////
// CocoaScrollBar, private:
void CocoaScrollBar::HideScrollbar() {
DCHECK_EQ(scroller_style_, NSScrollerStyleOverlay);
// Don't disappear if the scrollbar is hovered, or pressed but not dragged.
// This behavior matches the Cocoa scrollbars, but differs from the Blink
// scrollbars which would just disappear.
CocoaScrollBarThumb* thumb = GetCocoaScrollBarThumb();
if (IsMouseHovered() || thumb->IsStateHovered() ||
(thumb->IsStatePressed() && !did_start_dragging_)) {
hide_scrollbar_timer_.Reset();
return;
}
did_start_dragging_ = false;
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
animation.SetTransitionDuration(
base::TimeDelta::FromMilliseconds(kFadeDurationMs));
animation.AddObserver(this);
layer()->SetOpacity(0.0f);
}
void CocoaScrollBar::ShowScrollbar() {
// If the scrollbar is still expanded but has not completely faded away,
// then shrink it back to its original state.
if (is_expanded_ && !IsHoverOrPressedState() &&
layer()->GetAnimator()->IsAnimatingProperty(
ui::LayerAnimationElement::OPACITY)) {
DCHECK_EQ(scroller_style_, NSScrollerStyleOverlay);
thickness_animation_.Hide();
}
// Updates the scrolltrack and repaint it, if necessary.
double opacity =
scroller_style_ == NSScrollerStyleOverlay ? kOverlayOpacity : 1.0f;
layer()->SetOpacity(opacity);
hide_scrollbar_timer_.Stop();
}
bool CocoaScrollBar::IsHoverOrPressedState() const {
CocoaScrollBarThumb* thumb = GetCocoaScrollBarThumb();
return thumb->IsStateHovered() ||
thumb->IsStatePressed() ||
IsMouseHovered();
}
void CocoaScrollBar::UpdateScrollbarThickness() {
int thickness = ScrollbarThickness();
if (IsHorizontal())
SetBounds(x(), bounds().bottom() - thickness, width(), thickness);
else
SetBounds(bounds().right() - thickness, y(), thickness, height());
}
void CocoaScrollBar::ResetOverlayScrollbar() {
if (!IsHoverOrPressedState() && IsScrollbarFullyHidden() &&
!thickness_animation_.IsClosing()) {
if (is_expanded_) {
is_expanded_ = false;
thickness_animation_.Reset();
UpdateScrollbarThickness();
}
SetScrolltrackVisible(false);
}
}
void CocoaScrollBar::SetScrolltrackVisible(bool visible) {
has_scrolltrack_ = visible;
SchedulePaint();
}
CocoaScrollBarThumb* CocoaScrollBar::GetCocoaScrollBarThumb() const {
return static_cast<CocoaScrollBarThumb*>(GetThumb());
}
// static
base::RetainingOneShotTimer* BaseScrollBar::GetHideTimerForTest(
BaseScrollBar* scroll_bar) {
return &static_cast<CocoaScrollBar*>(scroll_bar)->hide_scrollbar_timer_;
}
} // namespace views