// Copyright 2019 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/shelf/window_scale_animation.h"
#include <optional>
#include <vector>
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/window_backdrop.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/screen_util.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/wm/core/scoped_animation_disabler.h"
namespace ash {
namespace {
// The time to do window transform to scale up to its original position or
// scale down to homescreen animation.
constexpr base::TimeDelta kWindowScaleUpOrDownTime = base::Milliseconds(350);
// The delay to do window opacity fade out when scaling down the dragged window.
constexpr base::TimeDelta kWindowFadeOutDelay = base::Milliseconds(100);
// The window scale down factor if we head to home screen after drag ends.
constexpr float kWindowScaleDownFactor = 0.001f;
// The fast animation time to do window transform for transient child window.
// This will only be used in tests.
constexpr base::TimeDelta kFastAnimationTime = base::Milliseconds(100);
// This will only be updated to true in tests via
// |EnableScopedFastAnimationForTransientChildForTest()|.
bool g_should_use_fast_animation_for_transient_child = false;
// Returns the transform that should be applied to |window| if we should head to
// shelf after dragging.
gfx::Transform GetWindowTransformToShelf(aura::Window* window) {
// The origin of bounds returned by GetBoundsInScreen() is transformed using
// the window's transform. The transform that should be applied to the
// window is calculated relative to the window bounds with no transforms
// applied, and thus need the un-transformed window origin.
const gfx::RectF window_bounds(window->GetBoundsInScreen());
gfx::PointF origin = window_bounds.origin();
gfx::PointF origin_without_transform =
window->transform().InverseMapPoint(origin).value_or(origin);
gfx::Transform transform;
Shelf* shelf = Shelf::ForWindow(window);
const gfx::Rect shelf_item_bounds =
shelf->GetScreenBoundsOfItemIconForWindow(window);
if (!shelf_item_bounds.IsEmpty()) {
const gfx::RectF shelf_item_bounds_f(shelf_item_bounds);
const gfx::PointF shelf_item_center = shelf_item_bounds_f.CenterPoint();
transform.Translate(shelf_item_center.x() - origin_without_transform.x(),
shelf_item_center.y() - origin_without_transform.y());
transform.Scale(shelf_item_bounds_f.width() / window_bounds.width(),
shelf_item_bounds_f.height() / window_bounds.height());
} else {
const gfx::PointF shelf_center_point =
gfx::RectF(shelf->GetIdealBounds()).CenterPoint();
transform.Translate(shelf_center_point.x() - origin_without_transform.x(),
shelf_center_point.y() - origin_without_transform.y());
transform.Scale(kWindowScaleDownFactor, kWindowScaleDownFactor);
}
return transform;
}
base::TimeDelta GetWindowAnimationTime(aura::Window* window) {
if (g_should_use_fast_animation_for_transient_child &&
window != wm::GetTransientRoot(window)) {
return kFastAnimationTime;
}
return kWindowScaleUpOrDownTime;
}
} // namespace
// -----------------------------------------------------------------------------
// WindowScaleAnimation::AnimationObserver:
// It's owned by |WindowScaleAnimation| and will be destroyed when its
// |window_| animation is completed or its |window_| is being destroyed.
class WindowScaleAnimation::AnimationObserver
: public ui::ImplicitAnimationObserver,
public aura::WindowObserver {
public:
AnimationObserver(aura::Window* window,
WindowScaleAnimation* window_scale_animation)
: window_(window), window_scale_animation_(window_scale_animation) {
window_observation_.Observe(window_.get());
}
AnimationObserver(const AnimationObserver&) = delete;
AnimationObserver& operator=(const AnimationObserver&) = delete;
~AnimationObserver() override {
// Explicitly stopping observing will prevent
// `OnImplicitAnimationsCompleted()` from being called.
StopObservingImplicitAnimations();
}
aura::Window* window() { return window_; }
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
window_scale_animation_->DestroyWindowAnimationObserver(this);
// |this| is destroyed after the above line.
}
// aura::WindowObserver:
void OnWindowDestroying(aura::Window* window) override {
window_scale_animation_->DestroyWindowAnimationObserver(this);
// |this| is destroyed after the above line.
}
private:
// Pointers to the window and the parent scale animation. Guaranteed to
// outlive `this`.
const raw_ptr<aura::Window> window_;
const raw_ptr<WindowScaleAnimation> window_scale_animation_;
base::ScopedObservation<aura::Window, aura::WindowObserver>
window_observation_{this};
};
WindowScaleAnimation::WindowScaleAnimation(WindowScaleType scale_type,
base::OnceClosure opt_callback)
: opt_callback_(std::move(opt_callback)), scale_type_(scale_type) {}
WindowScaleAnimation::~WindowScaleAnimation() {
if (!opt_callback_.is_null())
std::move(opt_callback_).Run();
}
void WindowScaleAnimation::Start(aura::Window* window) {
// In the destructor of `ScopedLayerAnimationSettings`, it will activate all
// of its observers. What we want is to activate the observer for each
// transient child window after the for loop is done, otherwise `this` can be
// early released via `WindowScaleAnimation::DestroyWindowAnimationObserver`.
// Hence creating this vector outside of the for loop.
std::vector<std::unique_ptr<ui::ScopedLayerAnimationSettings>> all_settings;
for (auto* transient_window : GetTransientTreeIterator(window)) {
window_animation_observers_.push_back(
std::make_unique<AnimationObserver>(transient_window, this));
WindowBackdrop::Get(transient_window)->DisableBackdrop();
all_settings.push_back(std::make_unique<ui::ScopedLayerAnimationSettings>(
transient_window->layer()->GetAnimator()));
auto* settings = all_settings.back().get();
settings->SetTransitionDuration(GetWindowAnimationTime(transient_window));
settings->SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
settings->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
settings->AddObserver(window_animation_observers_.back().get());
if (scale_type_ == WindowScaleType::kScaleDownToShelf) {
transient_window->layer()->GetAnimator()->SchedulePauseForProperties(
kWindowFadeOutDelay, ui::LayerAnimationElement::OPACITY);
transient_window->layer()->SetTransform(
GetWindowTransformToShelf(transient_window));
transient_window->layer()->SetOpacity(0.f);
} else {
transient_window->layer()->SetTransform(gfx::Transform());
}
}
}
// static
base::AutoReset<bool>
WindowScaleAnimation::EnableScopedFastAnimationForTransientChildForTest() {
return base::AutoReset<bool>(&g_should_use_fast_animation_for_transient_child,
true);
}
void WindowScaleAnimation::DestroyWindowAnimationObserver(
WindowScaleAnimation::AnimationObserver* animation_observer) {
// `animation_observer` will get deleted on the next line.
auto* window = animation_observer->window();
std::erase_if(window_animation_observers_,
base::MatchesUniquePtr(animation_observer));
if (window_animation_observers_.empty()) {
// Do the scale transform for the entire transient tree.
OnScaleWindowsOnAnimationsCompleted(window);
// self-destructed when all windows' transform animation is done.
delete this;
}
}
void WindowScaleAnimation::OnScaleWindowsOnAnimationsCompleted(
aura::Window* window) {
// Scale-down or scale-up window(s) with the windows' descending order
// in the transient tree. We need to use this fixed order to ensure the
// transient child window will be visible after returning back from home
// screen to the window. If the transient child window is minimized before its
// parent window, its visibility is not controlled by its parent anymore.
// Check |TransientWindowManager::UpdateTransientChildVisibility()| for more
// details.
const bool is_scaling_down =
scale_type_ == WindowScaleAnimation::WindowScaleType::kScaleDownToShelf;
for (auto* transient_window : GetTransientTreeIterator(window)) {
if (transient_window->is_destroying())
continue;
if (is_scaling_down) {
// Minimize the dragged window after transform animation is completed.
window_util::MinimizeAndHideWithoutAnimation({transient_window});
// Reset its transform to identity transform and its original backdrop
// mode.
transient_window->layer()->SetTransform(gfx::Transform());
transient_window->layer()->SetOpacity(1.f);
}
WindowBackdrop::Get(transient_window)->RestoreBackdrop();
}
}
} // namespace ash