| // Copyright 2021 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 "ash/controls/rounded_scroll_bar.h" |
| |
| #include <limits> |
| |
| #include "ash/public/cpp/style/color_provider.h" |
| #include "base/bind.h" |
| #include "base/numerics/ranges.h" |
| #include "base/time/time.h" |
| #include "cc/paint/paint_flags.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" |
| |
| namespace ash { |
| namespace { |
| |
| // Thickness of scroll bar thumb. |
| constexpr int kScrollThumbThicknessDp = 8; |
| // Radius of the scroll bar thumb. |
| constexpr int kScrollThumbRadiusDp = 4; |
| // How long for the scrollbar to hide after no scroll events have been received? |
| constexpr base::TimeDelta kScrollThumbHideTimeout = base::Milliseconds(500); |
| // How long for the scrollbar to fade away? |
| constexpr base::TimeDelta kScrollThumbFadeDuration = base::Milliseconds(240); |
| // Opacity values from go/semantic-color-system for "Scrollbar". |
| constexpr float kDefaultOpacity = 0.38f; |
| constexpr float kActiveOpacity = 1.0f; |
| |
| } // namespace |
| |
| // A scroll bar "thumb" that paints itself with rounded ends. |
| class RoundedScrollBar::Thumb : public views::BaseScrollBarThumb { |
| public: |
| explicit Thumb(RoundedScrollBar* scroll_bar) |
| : BaseScrollBarThumb(scroll_bar), scroll_bar_(scroll_bar) {} |
| Thumb(const Thumb&) = delete; |
| Thumb& operator=(const Thumb&) = delete; |
| ~Thumb() override = default; |
| |
| bool ShouldPaintAsActive() const { |
| // The thumb is active during hover and also when the user is dragging the |
| // thumb with the mouse. In the latter case, the mouse might be outside the |
| // scroll bar, due to mouse capture. |
| return IsMouseHovered() || GetState() == views::Button::STATE_PRESSED; |
| } |
| |
| // views::BaseScrollBarThumb: |
| gfx::Size CalculatePreferredSize() const override { |
| return gfx::Size(kScrollThumbThicknessDp, kScrollThumbThicknessDp); |
| } |
| |
| void OnPaint(gfx::Canvas* canvas) override { |
| cc::PaintFlags fill_flags; |
| fill_flags.setStyle(cc::PaintFlags::kFill_Style); |
| fill_flags.setAntiAlias(true); |
| // May be null in tests. |
| if (auto* color_provider = ColorProvider::Get(); color_provider) { |
| fill_flags.setColor(color_provider->GetContentLayerColor( |
| ColorProvider::ContentLayerType::kScrollBarColor)); |
| } |
| canvas->DrawRoundRect(GetLocalBounds(), kScrollThumbRadiusDp, fill_flags); |
| } |
| |
| void OnBoundsChanged(const gfx::Rect& previous_bounds) override { |
| scroll_bar_->OnThumbBoundsChanged(); |
| } |
| |
| void OnStateChanged() override { scroll_bar_->OnThumbStateChanged(); } |
| |
| private: |
| RoundedScrollBar* const scroll_bar_; |
| }; |
| |
| RoundedScrollBar::RoundedScrollBar(bool horizontal) |
| : ScrollBar(horizontal), |
| thumb_(new Thumb(this)), // Owned by views hierarchy. |
| hide_scrollbar_timer_( |
| FROM_HERE, |
| kScrollThumbHideTimeout, |
| base::BindRepeating(&RoundedScrollBar::HideScrollBar, |
| base::Unretained(this))) { |
| // Moving the mouse directly into the thumb will also notify this view. |
| SetNotifyEnterExitOnChild(true); |
| |
| SetThumb(thumb_); |
| thumb_->SetPaintToLayer(); |
| thumb_->layer()->SetFillsBoundsOpaquely(false); |
| // The thumb is hidden by default. |
| thumb_->layer()->SetOpacity(0.f); |
| } |
| |
| RoundedScrollBar::~RoundedScrollBar() = default; |
| |
| void RoundedScrollBar::SetInsets(const gfx::Insets& insets) { |
| insets_ = insets; |
| } |
| |
| void RoundedScrollBar::SetSnapBackOnDragOutside(bool snap) { |
| thumb_->SetSnapBackOnDragOutside(snap); |
| } |
| |
| void RoundedScrollBar::SetShowOnThumbBoundsChanged(bool show) { |
| show_on_thumb_bounds_changed_ = show; |
| } |
| |
| gfx::Rect RoundedScrollBar::GetTrackBounds() const { |
| gfx::Rect bounds = GetLocalBounds(); |
| bounds.Inset(insets_); |
| return bounds; |
| } |
| |
| bool RoundedScrollBar::OverlapsContent() const { |
| return true; |
| } |
| |
| int RoundedScrollBar::GetThickness() const { |
| // Extend the thickness by the insets on the sides of the bar. |
| const int sides = IsHorizontal() ? insets_.top() + insets_.bottom() |
| : insets_.left() + insets_.right(); |
| return kScrollThumbThicknessDp + sides; |
| } |
| |
| void RoundedScrollBar::OnMouseEntered(const ui::MouseEvent& event) { |
| ShowScrollbar(); |
| } |
| |
| void RoundedScrollBar::OnMouseExited(const ui::MouseEvent& event) { |
| if (!hide_scrollbar_timer_.IsRunning()) |
| hide_scrollbar_timer_.Reset(); |
| } |
| |
| void RoundedScrollBar::ScrollToPosition(int position) { |
| ShowScrollbar(); |
| views::ScrollBar::ScrollToPosition(position); |
| } |
| |
| void RoundedScrollBar::ObserveScrollEvent(const ui::ScrollEvent& event) { |
| // Scroll fling events are generated by moving a single finger over the |
| // trackpad; do not show the scrollbar for these events. |
| if (event.type() == ui::ET_SCROLL_FLING_CANCEL) |
| return; |
| ShowScrollbar(); |
| } |
| |
| views::BaseScrollBarThumb* RoundedScrollBar::GetThumbForTest() const { |
| return thumb_; |
| } |
| |
| void RoundedScrollBar::ShowScrollbar() { |
| if (!IsMouseHovered()) |
| hide_scrollbar_timer_.Reset(); |
| |
| const float target_opacity = |
| thumb_->ShouldPaintAsActive() ? kActiveOpacity : kDefaultOpacity; |
| if (base::IsApproximatelyEqual(thumb_->layer()->GetTargetOpacity(), |
| target_opacity, |
| std::numeric_limits<float>::epsilon())) { |
| return; |
| } |
| ui::ScopedLayerAnimationSettings animation(thumb_->layer()->GetAnimator()); |
| animation.SetTransitionDuration(kScrollThumbFadeDuration); |
| thumb_->layer()->SetOpacity(target_opacity); |
| } |
| |
| void RoundedScrollBar::HideScrollBar() { |
| // Never hide the scrollbar if the mouse is over it. The auto-hide timer |
| // will be reset when the mouse leaves the scrollable area. |
| if (IsMouseHovered()) |
| return; |
| |
| hide_scrollbar_timer_.Stop(); |
| ui::ScopedLayerAnimationSettings animation(thumb_->layer()->GetAnimator()); |
| animation.SetTransitionDuration(kScrollThumbFadeDuration); |
| thumb_->layer()->SetOpacity(0.f); |
| } |
| |
| void RoundedScrollBar::OnThumbStateChanged() { |
| // If the mouse is still in the scroll bar, the thumb hover state may have |
| // changed, so recompute opacity. |
| if (IsMouseHovered()) |
| ShowScrollbar(); |
| } |
| |
| void RoundedScrollBar::OnThumbBoundsChanged() { |
| // Optionally show the scroll bar on thumb bounds changes (e.g. keyboard |
| // driven scroll position changes). |
| if (show_on_thumb_bounds_changed_) |
| ShowScrollbar(); |
| } |
| |
| BEGIN_METADATA(RoundedScrollBar, ScrollBar) |
| END_METADATA |
| |
| } // namespace ash |