// 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 "ash/system/progress_indicator/progress_indicator.h"
#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/system/progress_indicator/progress_icon_animation.h"
#include "ash/system/progress_indicator/progress_ring_animation.h"
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "chromeos/constants/chromeos_features.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPathMeasure.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/scoped_canvas.h"
namespace ash {
namespace {
// Appearance.
constexpr float kInnerIconSizeScaleFactor = 14.f / 28.f;
constexpr float kOuterRingOpacity = 0.6f;
constexpr float kInnerRingStrokeWidthScaleFactor = 1.5f / 28.f;
constexpr float kOuterRingStrokeWidth = 2.f;
constexpr float kOuterRingStrokeWidthScaleFactor = 4.f / 28.f;
// Helpers ---------------------------------------------------------------------
// Returns the segment of the specified `path` between `start` and `end`.
// NOTE: It is required that: `0.f` <= `start` <= `end` <= `1.f`.
SkPath CreatePathSegment(const SkPath& path, float start, float end) {
DCHECK_LE(0.f, start);
DCHECK_LE(start, end);
DCHECK_LE(end, 1.f);
SkPathMeasure measure(path, /*force_closed=*/false);
start *= measure.getLength();
end *= measure.getLength();
SkPath path_segment;
measure.getSegment(start, end, &path_segment, /*start_with_move_to=*/true);
return path_segment;
}
// Returns a rounded rect path from the specified `rect` and `corner_radius`.
// NOTE: Unlike a typical rounded rect which starts from the *top-left* corner
// and proceeds clockwise, the rounded rect returned by this method starts at
// the *top-center*. This is a subtle but important detail as calling
// `CreatePathSegment()` with a path created from this method will treat
// *top-center* as the start point, as is needed when painting progress.
SkPath CreateRoundedRectPath(const gfx::RectF& rect, float corner_radius) {
// Top center.
SkPoint top_center(SkPoint::Make(rect.width() / 2.f, 0.f));
// Top right.
SkPoint top_right(SkPoint::Make(rect.width(), 0.f));
SkPoint top_right_end(top_right);
top_right_end.offset(0.f, corner_radius);
// Bottom right.
SkPoint bottom_right(SkPoint::Make(rect.width(), rect.height()));
SkPoint bottom_right_end(bottom_right);
bottom_right_end.offset(-corner_radius, 0.f);
// Bottom left.
SkPoint bottom_left(SkPoint::Make(0.f, rect.height()));
SkPoint bottom_left_end(bottom_left);
bottom_left_end.offset(0.f, -corner_radius);
// Top left.
SkPoint top_left(SkPoint::Make(0.f, 0.f));
SkPoint top_left_end(top_left);
top_left_end.offset(corner_radius, 0.f);
// Build path in the order specified above.
return SkPath()
.moveTo(top_center)
.arcTo(top_right, top_right_end, corner_radius)
.arcTo(bottom_right, bottom_right_end, corner_radius)
.arcTo(bottom_left, bottom_left_end, corner_radius)
.arcTo(top_left, top_left_end, corner_radius)
.close()
.offset(rect.x(), rect.y());
}
// Returns the size for the inner icon given `layer` dimensions.
// NOTE: this method should only be called when v2 animations are enabled.
float GetInnerIconSize(const ui::Layer* layer) {
const gfx::Size& size = layer->size();
return kInnerIconSizeScaleFactor * std::min(size.width(), size.height());
}
// Returns the stroke width for the inner icon given `layer` dimensions.
// NOTE: this method should only be called when v2 animations are enabled.
float GetInnerRingStrokeWidth(const ui::Layer* layer) {
const gfx::Size& size = layer->size();
return kInnerRingStrokeWidthScaleFactor *
std::min(size.width(), size.height());
}
// TODO(b/324644877): We want the progress ring still keep the same opacity
// after the `Pulse` animation. Please also provide an option for our this
// expectation after removing `kForcedShow`.
// Returns the opacity for the outer ring given the current `progress`.
float GetOuterRingOpacity(const std::optional<float>& progress) {
return (progress == ProgressIndicator::kProgressComplete ||
progress == ProgressIndicator::kForcedShow)
? 1.f
: kOuterRingOpacity;
}
// Returns the stroke width for the outer ring given `layer` dimensions and
// the current `progress`.
float GetOuterRingStrokeWidth(const ui::Layer* layer,
const std::optional<float>& progress) {
if (progress != ProgressIndicator::kProgressComplete) {
const gfx::Size& size = layer->size();
return kOuterRingStrokeWidthScaleFactor *
std::min(size.width(), size.height());
}
return kOuterRingStrokeWidth;
}
// DefaultProgressIndicatorAnimationRegistry -----------------------------------
// A default implementation of `ProgressIndicatorAnimationRegistry` which is
// associated with a single `ProgressIndicator` and manage progress animations
// as needed.
class DefaultProgressIndicatorAnimationRegistry
: public ProgressIndicatorAnimationRegistry {
public:
DefaultProgressIndicatorAnimationRegistry() = default;
DefaultProgressIndicatorAnimationRegistry(
const DefaultProgressIndicatorAnimationRegistry&) = delete;
DefaultProgressIndicatorAnimationRegistry& operator=(
const DefaultProgressIndicatorAnimationRegistry&) = delete;
~DefaultProgressIndicatorAnimationRegistry() = default;
// Sets the `progress_indicator` for which this registry manages animations.
// NOTE: This method may be called only once.
void SetProgressIndicator(ProgressIndicator* progress_indicator) {
DCHECK(progress_indicator);
DCHECK(!progress_indicator_);
progress_indicator_ = progress_indicator;
progress_changed_subscription_ =
progress_indicator_->AddProgressChangedCallback(base::BindRepeating(
&DefaultProgressIndicatorAnimationRegistry::OnProgressChanged,
weak_ptr_factory_.GetWeakPtr()));
}
private:
// Invoked on changes to `progress_indicator_` progress.
void OnProgressChanged() {
const std::optional<float>& progress = progress_indicator_->progress();
if (!progress.has_value()) {
// Progress is indeterminate.
EnsureProgressIconAnimation();
EnsureProgressRingAnimationOfType(
ProgressRingAnimation::Type::kIndeterminate);
} else if (progress != ProgressIndicator::kProgressComplete) {
// Progress is determinate.
EnsureProgressIconAnimation();
EraseProgressRingAnimation();
} else if (previous_progress_ != ProgressIndicator::kProgressComplete) {
// Progress is complete.
EraseProgressIconAnimation();
EnsureProgressRingAnimationOfType(ProgressRingAnimation::Type::kPulse);
}
previous_progress_ = progress;
}
// Invoked on update of the specified `animation`.
void OnProgressRingAnimationUpdated(ProgressRingAnimation* animation) {
if (animation->IsAnimating())
return;
// On completion, `animation` can be removed from the registry. This cannot
// be done directly from `animation`'s subscription callback, so post a task
// to delete `animation` as soon as possible.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](const base::WeakPtr<DefaultProgressIndicatorAnimationRegistry>&
self,
MayBeDangling<ProgressRingAnimation> animation) {
if (!self) {
return;
}
auto key = self->progress_indicator_->animation_key();
if (self->GetProgressRingAnimationForKey(key) == animation) {
self->SetProgressRingAnimationForKey(key, nullptr);
}
},
weak_ptr_factory_.GetWeakPtr(), base::UnsafeDangling(animation)));
}
// Ensures that a progress icon animation exists and is started.
void EnsureProgressIconAnimation() {
auto key = progress_indicator_->animation_key();
if (!GetProgressIconAnimationForKey(key)) {
auto* icon_animation =
SetProgressIconAnimationForKey(key, ProgressIconAnimation::Create());
icon_animation->Start();
}
}
// Ensures that a progress ring animation of the specified `type` exists and
// is started.
void EnsureProgressRingAnimationOfType(ProgressRingAnimation::Type type) {
auto key = progress_indicator_->animation_key();
auto* ring_animation = GetProgressRingAnimationForKey(key);
if (ring_animation && ring_animation->type() == type)
return;
auto animation = ProgressRingAnimation::CreateOfType(type);
// NOTE: `animation` is owned by `this` so it is safe to use a raw pointer
// and subscription-less callback.
animation->AddUnsafeAnimationUpdatedCallback(
base::BindRepeating(&DefaultProgressIndicatorAnimationRegistry::
OnProgressRingAnimationUpdated,
base::Unretained(this), animation.get()));
SetProgressRingAnimationForKey(key, std::move(animation))->Start();
}
// Erases any existing progress icon animation.
void EraseProgressIconAnimation() {
SetProgressIconAnimationForKey(progress_indicator_->animation_key(),
nullptr);
}
// Erases any existing progress ring animation.
void EraseProgressRingAnimation() {
SetProgressRingAnimationForKey(progress_indicator_->animation_key(),
nullptr);
}
// The progress indicator for which to manage animations and a subscription
// to receive notification of progress change events.
raw_ptr<ProgressIndicator> progress_indicator_ = nullptr;
base::CallbackListSubscription progress_changed_subscription_;
// Instantiate `previous_progress_` to completion to avoid starting a pulse
// animation on first progress update.
std::optional<float> previous_progress_ =
ProgressIndicator::kProgressComplete;
base::WeakPtrFactory<DefaultProgressIndicatorAnimationRegistry>
weak_ptr_factory_{this};
};
// DefaultProgressIndicator ----------------------------------------------------
// A default implementation of `ProgressIndicator` which paints indication of
// progress returned by the specified `progress_callback_`. NOTE: This instance
// comes pre-wired with an animation `registry_` that will manage progress
// animations as needed.
class DefaultProgressIndicator : public ProgressIndicator {
public:
DefaultProgressIndicator(
std::unique_ptr<DefaultProgressIndicatorAnimationRegistry> registry,
base::RepeatingCallback<std::optional<float>()> progress_callback)
: ProgressIndicator(
registry.get(),
ProgressIndicatorAnimationRegistry::AsAnimationKey(this)),
registry_(std::move(registry)),
progress_callback_(std::move(progress_callback)) {
registry_->SetProgressIndicator(this);
}
DefaultProgressIndicator(const DefaultProgressIndicator&) = delete;
DefaultProgressIndicator& operator=(const DefaultProgressIndicator&) = delete;
~DefaultProgressIndicator() override = default;
private:
// ProgressIndicator:
std::optional<float> CalculateProgress() const override {
return progress_callback_.Run();
}
std::unique_ptr<DefaultProgressIndicatorAnimationRegistry> registry_;
base::RepeatingCallback<std::optional<float>()> progress_callback_;
};
} // namespace
// ProgressIndicator -----------------------------------------------------------
// static
constexpr char ProgressIndicator::kClassName[];
constexpr float ProgressIndicator::kProgressComplete;
ProgressIndicator::ProgressIndicator(
ProgressIndicatorAnimationRegistry* animation_registry,
ProgressIndicatorAnimationRegistry::AnimationKey animation_key)
: animation_registry_(animation_registry), animation_key_(animation_key) {
if (!animation_registry_)
return;
// Register to be notified of changes to the icon animation associated with
// this progress indicator's `animation_key_`. Note that it is safe to use a
// raw pointer here since `this` owns the subscription.
icon_animation_changed_subscription_ =
animation_registry_->AddProgressIconAnimationChangedCallbackForKey(
animation_key_,
base::BindRepeating(
&ProgressIndicator::OnProgressIconAnimationChanged,
base::Unretained(this)));
// If an `icon_animation` is already registered, perform additional
// initialization.
ProgressIconAnimation* icon_animation =
animation_registry_->GetProgressIconAnimationForKey(animation_key_);
if (icon_animation)
OnProgressIconAnimationChanged(icon_animation);
// Register to be notified of changes to the ring animation associated with
// this progress indicator's `animation_key_`. Note that it is safe to use a
// raw pointer here since `this` owns the subscription.
ring_animation_changed_subscription_ =
animation_registry_->AddProgressRingAnimationChangedCallbackForKey(
animation_key_,
base::BindRepeating(
&ProgressIndicator::OnProgressRingAnimationChanged,
base::Unretained(this)));
// If `ring_animation` is already registered, perform additional
// initialization.
ProgressRingAnimation* ring_animation =
animation_registry_->GetProgressRingAnimationForKey(animation_key_);
if (ring_animation)
OnProgressRingAnimationChanged(ring_animation);
}
ProgressIndicator::~ProgressIndicator() = default;
// static
std::unique_ptr<ProgressIndicator> ProgressIndicator::CreateDefaultInstance(
base::RepeatingCallback<std::optional<float>()> progress_callback) {
return std::make_unique<DefaultProgressIndicator>(
std::make_unique<DefaultProgressIndicatorAnimationRegistry>(),
std::move(progress_callback));
}
base::CallbackListSubscription ProgressIndicator::AddProgressChangedCallback(
base::RepeatingClosureList::CallbackType callback) {
return progress_changed_callback_list_.Add(std::move(callback));
}
ui::Layer* ProgressIndicator::CreateLayer(ColorResolver color_resolver) {
CHECK(!layer());
CHECK(color_resolver);
auto layer = std::make_unique<ui::Layer>(ui::LAYER_TEXTURED);
layer->set_delegate(this);
layer->SetFillsBoundsOpaquely(false);
layer->SetName(kClassName);
Reset(std::move(layer));
color_resolver_ = std::move(color_resolver);
return this->layer();
}
void ProgressIndicator::DestroyLayer() {
color_resolver_.Reset();
if (layer())
ReleaseLayer();
}
void ProgressIndicator::InvalidateLayer() {
if (layer())
layer()->SchedulePaint(gfx::Rect(layer()->size()));
}
void ProgressIndicator::SetColorId(const std::optional<ui::ColorId>& color_id) {
if (color_id_ == color_id) {
return;
}
color_id_ = color_id;
InvalidateLayer();
}
void ProgressIndicator::SetInnerIconVisible(bool visible) {
if (inner_icon_visible_ == visible)
return;
inner_icon_visible_ = visible;
// It's not necessary to invalidate the `layer()` if progress is complete
// since the inner icon is only painted while progress is incomplete.
if (progress_ != kProgressComplete)
InvalidateLayer();
}
void ProgressIndicator::SetInnerRingVisible(bool visible) {
if (inner_ring_visible_ == visible) {
return;
}
inner_ring_visible_ = visible;
// It's not necessary to invalidate the `layer()` if progress is complete
// since the inner ring is only painted while progress is incomplete.
if (progress_ != kProgressComplete) {
InvalidateLayer();
}
}
void ProgressIndicator::SetOuterRingTrackVisible(bool visible) {
if (outer_ring_track_visible_ == visible) {
return;
}
outer_ring_track_visible_ = visible;
// It's not necessary to invalidate the `layer()` if progress is complete
// since the progress ring track is only painted while progress is incomplete.
if (progress_ != kProgressComplete) {
InvalidateLayer();
}
}
void ProgressIndicator::SetOuterRingStrokeWidth(float width) {
if (outer_ring_stroke_width_ == width) {
return;
}
outer_ring_stroke_width_ = width;
// It's not necessary to invalidate the `layer()` if progress is complete
// since the outer ring is only painted while progress is incomplete.
if (progress_ != kProgressComplete) {
InvalidateLayer();
}
}
void ProgressIndicator::OnDeviceScaleFactorChanged(float old_scale,
float new_scale) {
InvalidateLayer();
}
void ProgressIndicator::OnPaintLayer(const ui::PaintContext& context) {
// Look up the associated `ring_animation` (if one exists).
ProgressRingAnimation* ring_animation =
animation_registry_
? animation_registry_->GetProgressRingAnimationForKey(animation_key_)
: nullptr;
// Unless `this` is animating, nothing will paint if `progress_` is complete.
if (progress_ == kProgressComplete && !ring_animation)
return;
float start, end, outer_ring_opacity;
if (ring_animation) {
start = ring_animation->start_position();
end = ring_animation->end_position();
outer_ring_opacity = ring_animation->outer_ring_opacity();
} else {
start = 0.f;
end = progress_.value();
outer_ring_opacity = 1.f;
}
DCHECK_GE(start, 0.f);
DCHECK_LE(start, 1.f);
DCHECK_GE(end, 0.f);
DCHECK_LE(end, 1.f);
DCHECK_GE(outer_ring_opacity, 0.f);
DCHECK_LE(outer_ring_opacity, 1.f);
ui::PaintRecorder recorder(context, layer()->size());
gfx::Canvas* canvas = recorder.canvas();
// The `canvas` should be flipped for RTL.
gfx::ScopedCanvas scoped_canvas(recorder.canvas());
scoped_canvas.FlipIfRTL(layer()->size().width());
// Look up the associated `icon_animation` (if one exists).
ProgressIconAnimation* icon_animation =
animation_registry_
? animation_registry_->GetProgressIconAnimationForKey(animation_key_)
: nullptr;
if (icon_animation) {
const float opacity = icon_animation->opacity();
DCHECK_GE(opacity, 0.f);
DCHECK_LE(opacity, 1.f);
canvas->SaveLayerAlpha(SK_AlphaOPAQUE * opacity);
}
float outer_ring_stroke_width = outer_ring_stroke_width_.value_or(
GetOuterRingStrokeWidth(layer(), progress_));
gfx::RectF bounds(gfx::SizeF(layer()->size()));
bounds.Inset(gfx::InsetsF(outer_ring_stroke_width / 2.f));
SkPath path(CreateRoundedRectPath(
bounds, /*radius=*/std::min(bounds.width(), bounds.height()) / 2.f));
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStrokeCap(cc::PaintFlags::Cap::kRound_Cap);
flags.setStrokeWidth(outer_ring_stroke_width);
flags.setStyle(cc::PaintFlags::Style::kStroke_Style);
const SkColor color =
color_resolver_.Run(color_id_.value_or(cros_tokens::kCrosSysPrimary));
flags.setColor(SkColorSetA(
color,
SK_AlphaOPAQUE * GetOuterRingOpacity(progress_) * outer_ring_opacity));
// Outer ring track.
if (outer_ring_track_visible_) {
canvas->DrawPath(path, flags);
}
// Outer ring.
if (start == end) {
// If `start` == `end`, prevent the canvas from drawing the caps.
} else if (start < end) {
// If `start` <= `end`, only a single path segment is necessary.
canvas->DrawPath(CreatePathSegment(path, start, end), flags);
} else {
// If `start` > `end`, join two path segments as a single path and use that
// to draw the progress ring. This works around limitations of
// `SkPathMeasure` which require that `start` be <= `end`.
SkPath joined_path(CreatePathSegment(path, start, 1.0f));
joined_path.addPath(CreatePathSegment(path, 0.f, end));
canvas->DrawPath(joined_path, flags);
}
// The inner ring and inner icon should be absent once progress completes.
// This would occur if the progress ring is animating post completion.
if (progress_ == kProgressComplete)
return;
float inner_ring_stroke_width = GetInnerRingStrokeWidth(layer());
if (icon_animation) {
inner_ring_stroke_width *=
icon_animation->inner_ring_stroke_width_scale_factor();
}
const bool inner_ring_visible =
inner_ring_visible_ &&
!cc::MathUtil::IsWithinEpsilon(inner_ring_stroke_width, 0.f);
// Inner ring.
if (inner_ring_visible) {
bounds.Inset(gfx::InsetsF(
(outer_ring_stroke_width + inner_ring_stroke_width) / 2.f));
path = CreateRoundedRectPath(
bounds, /*radius=*/std::min(bounds.width(), bounds.height()) / 2.f);
flags.setColor(color);
flags.setStrokeWidth(inner_ring_stroke_width);
canvas->DrawPath(path, flags);
}
// Inner icon.
if (inner_icon_visible_) {
float inner_icon_size = GetInnerIconSize(layer());
gfx::RectF inner_icon_bounds(gfx::SizeF(layer()->size()));
inner_icon_bounds.ClampToCenteredSize(
gfx::SizeF(inner_icon_size, inner_icon_size));
if (icon_animation) {
inner_icon_bounds.Offset(
/*horizontal=*/0.f,
/*vertical=*/icon_animation->inner_icon_translate_y_scale_factor() *
inner_icon_size);
}
gfx::Transform transform;
transform.Translate(inner_icon_bounds.x(), inner_icon_bounds.y());
canvas->Transform(transform);
gfx::PaintVectorIcon(canvas, kHoldingSpaceDownloadIcon, inner_icon_size,
color);
}
}
void ProgressIndicator::UpdateVisualState() {
const auto previous_progress = progress_;
// Cache `progress_`.
progress_ = CalculateProgress();
if (progress_.has_value()) {
DCHECK_GE(progress_.value(), 0.f);
DCHECK_LE(progress_.value(), 1.f);
}
// Notify `progress_` changes.
if (progress_ != previous_progress)
progress_changed_callback_list_.Notify();
}
void ProgressIndicator::OnProgressIconAnimationChanged(
ProgressIconAnimation* animation) {
// Trigger repaint of this progress indicator on `animation` updates. Note
// that it is safe to use a raw pointer here since `this` owns the
// subscription.
if (animation) {
icon_animation_updated_subscription_ =
animation->AddAnimationUpdatedCallback(base::BindRepeating(
&ProgressIndicator::InvalidateLayer, base::Unretained(this)));
}
InvalidateLayer();
}
void ProgressIndicator::OnProgressRingAnimationChanged(
ProgressRingAnimation* animation) {
// Trigger repaint of this progress indicator on `animation` updates. Note
// that it is safe to use a raw pointer here since `this` owns the
// subscription.
if (animation) {
ring_animation_updated_subscription_ =
animation->AddAnimationUpdatedCallback(base::BindRepeating(
&ProgressIndicator::InvalidateLayer, base::Unretained(this)));
}
InvalidateLayer();
}
} // namespace ash