// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/native_theme/scrollbar_animator_mac.h"
#include <algorithm>
#include "base/task/single_thread_task_runner.h"
namespace ui {
///////////////////////////////////////////////////////////////////////////////
// ScrollbarAnimationTimerMac
ScrollbarAnimationTimerMac::ScrollbarAnimationTimerMac(
base::RepeatingCallback<void(double)> callback,
double duration,
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: start_time_(0.0), duration_(duration), callback_(std::move(callback)) {
timing_function_ = gfx::CubicBezierTimingFunction::CreatePreset(
gfx::CubicBezierTimingFunction::EaseType::EASE_IN_OUT);
}
ScrollbarAnimationTimerMac::~ScrollbarAnimationTimerMac() {}
void ScrollbarAnimationTimerMac::Start() {
start_time_ = base::Time::Now().InSecondsFSinceUnixEpoch();
// Set the framerate of the animation. NSAnimation uses a default
// framerate of 60 Hz, so use that here.
timer_.Start(FROM_HERE, base::Seconds(1.0 / 60.0), this,
&ScrollbarAnimationTimerMac::TimerFired);
}
void ScrollbarAnimationTimerMac::Stop() {
timer_.Stop();
}
void ScrollbarAnimationTimerMac::SetDuration(double duration) {
duration_ = duration;
}
void ScrollbarAnimationTimerMac::TimerFired() {
double current_time = base::Time::Now().InSecondsFSinceUnixEpoch();
double delta = current_time - start_time_;
if (delta >= duration_)
timer_.Stop();
double fraction = delta / duration_;
fraction = std::clamp(fraction, 0.0, 1.0);
double progress = timing_function_->GetValue(fraction);
// Note that `this` may be destroyed from within `callback_`, so it is not
// safe to call any other code after it.
callback_.Run(progress);
}
///////////////////////////////////////////////////////////////////////////////
// OverlayScrollbarAnimatorMac
OverlayScrollbarAnimatorMac::OverlayScrollbarAnimatorMac(
Client* client,
int thumb_width_expanded,
int thumb_width_unexpanded,
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: client_(client),
thumb_width_expanded_(thumb_width_expanded),
thumb_width_unexpanded_(thumb_width_unexpanded),
thumb_width_(thumb_width_unexpanded),
task_runner_(task_runner),
weak_factory_(this) {}
OverlayScrollbarAnimatorMac::~OverlayScrollbarAnimatorMac() = default;
void OverlayScrollbarAnimatorMac::MouseDidEnter() {
// If the scrollbar is completely hidden, ignore this. We will initialize
// the `mouse_in_track_` state if there's a scroll.
if (thumb_alpha_ == 0.f)
return;
if (mouse_in_track_)
return;
mouse_in_track_ = true;
// Cancel any in-progress fade-out, and ensure that the fade-out timer be
// disabled.
if (fade_out_animation_)
FadeOutAnimationCancel();
FadeOutTimerUpdate();
// Start the fade-in animation (unless it is in progress or has already
// completed).
if (!fade_in_track_animation_ && track_alpha_ != 1.f)
FadeInTrackAnimationStart();
// Start the expand-thumb animation (unless it is in progress or has already
// completed).
if (!expand_thumb_animation_ && thumb_width_ != thumb_width_expanded_)
ExpandThumbAnimationStart();
}
void OverlayScrollbarAnimatorMac::MouseDidExit() {
// Ensure that the fade-out timer be re-enabled.
mouse_in_track_ = false;
FadeOutTimerUpdate();
}
void OverlayScrollbarAnimatorMac::DidScroll() {
// If we were fading out, then immediately return to being fully opaque for
// both the thumb and track.
if (fade_out_animation_) {
FadeOutAnimationCancel();
FadeOutTimerUpdate();
return;
}
// If the scrollbar was already fully visible, then just update the fade-out
// timer.
if (thumb_alpha_ == 1.f) {
FadeOutTimerUpdate();
return;
}
// This is an initial scroll causing the thumb to appear.
DCHECK_EQ(thumb_width_, thumb_width_unexpanded_);
DCHECK_EQ(thumb_alpha_, 0.f);
DCHECK(!fade_in_track_animation_);
thumb_width_ = thumb_width_unexpanded_;
thumb_alpha_ = 1;
client_->SetThumbNeedsDisplay();
client_->SetHidden(false);
// If the initial scroll is done inside the scrollbar's area, then also
// cause the track to appear and the thumb to enlarge.
if (client_->IsMouseInScrollbarFrameRect()) {
mouse_in_track_ = true;
thumb_width_ = thumb_width_expanded_;
track_alpha_ = 1;
client_->SetTrackNeedsDisplay();
}
// Update the fade-out timer (now that we know whether or not the mouse
// is in the track).
FadeOutTimerUpdate();
}
void OverlayScrollbarAnimatorMac::ExpandThumbAnimationStart() {
DCHECK(!expand_thumb_animation_);
DCHECK_NE(thumb_width_, thumb_width_expanded_);
expand_thumb_animation_ = std::make_unique<ScrollbarAnimationTimerMac>(
base::BindRepeating(
&OverlayScrollbarAnimatorMac::ExpandThumbAnimationTicked,
weak_factory_.GetWeakPtr()),
kAnimationDurationSeconds, task_runner_);
expand_thumb_animation_->Start();
}
void OverlayScrollbarAnimatorMac::ExpandThumbAnimationTicked(double progress) {
thumb_width_ = (1 - progress) * thumb_width_unexpanded_ +
progress * thumb_width_expanded_;
client_->SetThumbNeedsDisplay();
if (progress == 1)
expand_thumb_animation_.reset();
}
void OverlayScrollbarAnimatorMac::FadeInTrackAnimationStart() {
DCHECK(!fade_in_track_animation_);
DCHECK(!fade_out_animation_);
fade_in_track_animation_ = std::make_unique<ScrollbarAnimationTimerMac>(
base::BindRepeating(
&OverlayScrollbarAnimatorMac::FadeInTrackAnimationTicked,
weak_factory_.GetWeakPtr()),
kAnimationDurationSeconds, task_runner_);
fade_in_track_animation_->Start();
}
void OverlayScrollbarAnimatorMac::FadeInTrackAnimationTicked(double progress) {
// Fade-in and fade-out are mutually exclusive.
DCHECK(!fade_out_animation_);
track_alpha_ = progress;
client_->SetTrackNeedsDisplay();
if (progress == 1) {
fade_in_track_animation_.reset();
}
}
void OverlayScrollbarAnimatorMac::FadeOutTimerUpdate() {
if (mouse_in_track_) {
start_scrollbar_fade_out_timer_.reset();
return;
}
if (!start_scrollbar_fade_out_timer_) {
start_scrollbar_fade_out_timer_ =
std::make_unique<base::RetainingOneShotTimer>(
FROM_HERE, kFadeOutDelay,
base::BindRepeating(
&OverlayScrollbarAnimatorMac::FadeOutAnimationStart,
weak_factory_.GetWeakPtr()));
start_scrollbar_fade_out_timer_->SetTaskRunner(task_runner_);
}
start_scrollbar_fade_out_timer_->Reset();
}
void OverlayScrollbarAnimatorMac::FadeOutAnimationStart() {
start_scrollbar_fade_out_timer_.reset();
fade_in_track_animation_.reset();
fade_out_animation_.reset();
fade_out_animation_ = std::make_unique<ScrollbarAnimationTimerMac>(
base::BindRepeating(&OverlayScrollbarAnimatorMac::FadeOutAnimationTicked,
weak_factory_.GetWeakPtr()),
kAnimationDurationSeconds, task_runner_);
fade_out_animation_->Start();
}
void OverlayScrollbarAnimatorMac::FadeOutAnimationTicked(double progress) {
DCHECK(!fade_in_track_animation_);
// Fade out the thumb.
thumb_alpha_ = 1 - progress;
client_->SetThumbNeedsDisplay();
// If the track is not already invisible, fade it out.
if (track_alpha_ != 0) {
track_alpha_ = 1 - progress;
client_->SetTrackNeedsDisplay();
}
// Once completely faded out, reset all state.
if (progress == 1) {
expand_thumb_animation_.reset();
fade_out_animation_.reset();
thumb_width_ = thumb_width_unexpanded_;
DCHECK_EQ(thumb_alpha_, 0.f);
DCHECK_EQ(track_alpha_, 0.f);
// Mark that the scrollbars were hidden.
client_->SetHidden(true);
}
}
void OverlayScrollbarAnimatorMac::FadeOutAnimationCancel() {
DCHECK(fade_out_animation_);
fade_out_animation_.reset();
thumb_alpha_ = 1;
client_->SetThumbNeedsDisplay();
if (track_alpha_ > 0) {
track_alpha_ = 1;
client_->SetTrackNeedsDisplay();
}
}
const float OverlayScrollbarAnimatorMac::kAnimationDurationSeconds = 0.25f;
const base::TimeDelta OverlayScrollbarAnimatorMac::kFadeOutDelay =
base::Milliseconds(500);
} // namespace ui