diff --git a/ash/BUILD.gn b/ash/BUILD.gn index 5378386aae2fc..6ab88e8854fb0 100644 --- a/ash/BUILD.gn +++ b/ash/BUILD.gn @@ -2174,6 +2174,7 @@ test("ash_unittests") { "wm/default_window_resizer_unittest.cc", "wm/desks/autotest_desks_api_unittests.cc", "wm/desks/desks_unittests.cc", + "wm/desks/root_window_desk_switch_animator_unittest.cc", "wm/drag_window_resizer_unittest.cc", "wm/fullscreen_window_finder_unittest.cc", "wm/gestures/back_gesture/back_gesture_affordance_unittest.cc", @@ -2543,6 +2544,8 @@ static_library("test_support") { "wm/cursor_manager_test_api.h", "wm/desks/desks_test_util.cc", "wm/desks/desks_test_util.h", + "wm/desks/root_window_desk_switch_animator_test_api.cc", + "wm/desks/root_window_desk_switch_animator_test_api.h", "wm/gestures/back_gesture/test_back_gesture_contextual_nudge_delegate.cc", "wm/gestures/back_gesture/test_back_gesture_contextual_nudge_delegate.h", "wm/lock_state_controller_test_api.cc", diff --git a/ash/wm/desks/root_window_desk_switch_animator.cc b/ash/wm/desks/root_window_desk_switch_animator.cc index 679a44db70382..47a80c046b06e 100644 --- a/ash/wm/desks/root_window_desk_switch_animator.cc +++ b/ash/wm/desks/root_window_desk_switch_animator.cc @@ -29,9 +29,6 @@ namespace ash { namespace { -// The space between the starting and ending desks screenshots in dips. -constexpr int kDesksSpacing = 50; - // The maximum number of times to retry taking a screenshot for either the // starting or the ending desks. After this maximum number is reached, we ignore // a failed screenshot request and proceed with next phases. @@ -50,12 +47,6 @@ constexpr base::TimeDelta kAnimationDuration = // desk change. constexpr int kTouchpadSwipeLengthForDeskChange = 100; -// The animation layer has extra padding at its two edges. The width in dips is -// a ratio of the root window width. This padding is to notify users there are -// no more desks on that side by showing a black region as we swipe -// continuously. -constexpr float kEdgePaddingRatio = 0.15f; - // The amount, by which the detached old layers of the removed desk's windows, // is translated vertically during the for-remove desk switch animation. constexpr int kRemovedDeskWindowYTranslation = 20; diff --git a/ash/wm/desks/root_window_desk_switch_animator.h b/ash/wm/desks/root_window_desk_switch_animator.h index b398b7d20122b..ab42a90483dec 100644 --- a/ash/wm/desks/root_window_desk_switch_animator.h +++ b/ash/wm/desks/root_window_desk_switch_animator.h @@ -7,6 +7,7 @@ #include <memory> +#include "ash/ash_export.h" #include "base/memory/weak_ptr.h" #include "ui/compositor/layer_animation_observer.h" @@ -157,7 +158,8 @@ namespace ash { // slightly more to accommodate the continuous desk animations feature. The base // algorithm is still valid, but once the features are near completion these // need to be updated. -class RootWindowDeskSwitchAnimator : public ui::ImplicitAnimationObserver { +class ASH_EXPORT RootWindowDeskSwitchAnimator + : public ui::ImplicitAnimationObserver { public: class Delegate { public: @@ -179,6 +181,15 @@ class RootWindowDeskSwitchAnimator : public ui::ImplicitAnimationObserver { virtual ~Delegate() = default; }; + // The space between the starting and ending desks screenshots in dips. + static constexpr int kDesksSpacing = 50; + + // The animation layer has extra padding at its two edges. The width in dips + // is a ratio of the root window width. This padding is to notify users there + // are no more desks on that side by showing a black region as we swipe + // continuously. + static constexpr float kEdgePaddingRatio = 0.15f; + RootWindowDeskSwitchAnimator(aura::Window* root, int starting_desk_index, int ending_desk_index, @@ -235,6 +246,8 @@ class RootWindowDeskSwitchAnimator : public ui::ImplicitAnimationObserver { void OnImplicitAnimationsCompleted() override; private: + friend class RootWindowDeskSwitchAnimatorTestApi; + // Completes the first phase of the animation using the given |layer| as the // screenshot layer of the starting desk. This layer will be parented to the // animation layer, which will be setup with its initial transform according diff --git a/ash/wm/desks/root_window_desk_switch_animator_test_api.cc b/ash/wm/desks/root_window_desk_switch_animator_test_api.cc new file mode 100644 index 0000000000000..f93cb6559d02f --- /dev/null +++ b/ash/wm/desks/root_window_desk_switch_animator_test_api.cc @@ -0,0 +1,43 @@ +// Copyright 2020 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/wm/desks/root_window_desk_switch_animator_test_api.h" + +#include "ash/wm/desks/root_window_desk_switch_animator.h" +#include "ui/compositor/layer.h" +#include "ui/compositor/layer_tree_owner.h" + +namespace ash { + +RootWindowDeskSwitchAnimatorTestApi::RootWindowDeskSwitchAnimatorTestApi( + RootWindowDeskSwitchAnimator* animator) + : animator_(animator) { + DCHECK(animator_); +} + +RootWindowDeskSwitchAnimatorTestApi::~RootWindowDeskSwitchAnimatorTestApi() = + default; + +ui::Layer* RootWindowDeskSwitchAnimatorTestApi::GetAnimationLayer() { + return animator_->animation_layer_owner_->root(); +} + +ui::Layer* +RootWindowDeskSwitchAnimatorTestApi::GetScreenshotLayerOfDeskWithIndex( + int desk_index) { + auto screenshot_layers = animator_->screenshot_layers_; + + DCHECK_GE(desk_index, 0); + DCHECK_LT(desk_index, int{screenshot_layers.size()}); + + ui::Layer* layer = screenshot_layers[desk_index]; + DCHECK(layer); + return layer; +} + +int RootWindowDeskSwitchAnimatorTestApi::GetEndingDeskIndex() const { + return animator_->ending_desk_index_; +} + +} // namespace ash diff --git a/ash/wm/desks/root_window_desk_switch_animator_test_api.h b/ash/wm/desks/root_window_desk_switch_animator_test_api.h new file mode 100644 index 0000000000000..ad1cf763d1c52 --- /dev/null +++ b/ash/wm/desks/root_window_desk_switch_animator_test_api.h @@ -0,0 +1,40 @@ +// Copyright 2020 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. + +#ifndef ASH_WM_DESKS_ROOT_WINDOW_DESK_SWITCH_ANIMATOR_TEST_API_H_ +#define ASH_WM_DESKS_ROOT_WINDOW_DESK_SWITCH_ANIMATOR_TEST_API_H_ + +namespace ui { +class Layer; +} + +namespace ash { + +class RootWindowDeskSwitchAnimator; + +// Use the api in this class to test the internals of +// RootWindowDeskSwitchAnimator. +class RootWindowDeskSwitchAnimatorTestApi { + public: + explicit RootWindowDeskSwitchAnimatorTestApi( + RootWindowDeskSwitchAnimator* animator); + RootWindowDeskSwitchAnimatorTestApi( + const RootWindowDeskSwitchAnimatorTestApi&) = delete; + RootWindowDeskSwitchAnimatorTestApi& operator=( + const RootWindowDeskSwitchAnimatorTestApi&) = delete; + ~RootWindowDeskSwitchAnimatorTestApi(); + + // Getters for the layers associated with the animation. + ui::Layer* GetAnimationLayer(); + ui::Layer* GetScreenshotLayerOfDeskWithIndex(int desk_index); + + int GetEndingDeskIndex() const; + + private: + RootWindowDeskSwitchAnimator* const animator_; +}; + +} // namespace ash + +#endif // ASH_WM_DESKS_ROOT_WINDOW_DESK_SWITCH_ANIMATOR_TEST_API_H_ diff --git a/ash/wm/desks/root_window_desk_switch_animator_unittest.cc b/ash/wm/desks/root_window_desk_switch_animator_unittest.cc new file mode 100644 index 0000000000000..f4c91105fbd9a --- /dev/null +++ b/ash/wm/desks/root_window_desk_switch_animator_unittest.cc @@ -0,0 +1,344 @@ +// Copyright 2020 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/wm/desks/root_window_desk_switch_animator.h" + +#include <memory> + +#include "ash/public/cpp/ash_features.h" +#include "ash/shell.h" +#include "ash/test/ash_test_base.h" +#include "ash/wm/desks/root_window_desk_switch_animator_test_api.h" +#include "base/callback_forward.h" +#include "base/test/scoped_feature_list.h" +#include "ui/compositor/layer.h" +#include "ui/compositor/scoped_animation_duration_scale_mode.h" + +namespace ash { + +namespace { + +// Computes the animation layer expected size. Each screenshot is the size of +// the primary root window, and there is spacing between each screenshot, which +// are children of the animation layer. The animation layer has padding on each +// end. +gfx::Size ComputeAnimationLayerExpectedSize(int expected_screenshots) { + const gfx::Size root_window_size = + Shell::GetPrimaryRootWindow()->bounds().size(); + const int edge_padding = + std::round(RootWindowDeskSwitchAnimator::kEdgePaddingRatio * + root_window_size.width()); + gfx::Size expected_size = root_window_size; + expected_size.set_width(root_window_size.width() * expected_screenshots + + RootWindowDeskSwitchAnimator::kDesksSpacing * + (expected_screenshots - 1) + + 2 * edge_padding); + return expected_size; +} + +// Computes where |child_layer| is shown in root window coordinates. This is +// done by applying its parent's transform to it. |child_layer| itself is not +// expected to have a transform and its grandparent is the root window. If +// |use_target_transform| is false apply the parent's current transform, +// otherwise apply the parent's target transform (the expected transform at the +// end of an ongoing animation). +gfx::Rect GetVisibleBounds(ui::Layer* child_layer, + ui::Layer* animating_layer, + bool use_target_transform = false) { + DCHECK_EQ(animating_layer, child_layer->parent()); + DCHECK(child_layer->transform().IsIdentity()); + DCHECK_EQ(Shell::GetPrimaryRootWindow()->layer(), animating_layer->parent()); + + const gfx::Transform animating_layer_transform = + use_target_transform ? animating_layer->GetTargetTransform() + : animating_layer->transform(); + DCHECK(animating_layer_transform.IsIdentityOr2DTranslation()); + gfx::RectF bounds(child_layer->bounds()); + animating_layer_transform.TransformRect(&bounds); + return gfx::ToRoundedRect(bounds); +} + +gfx::Rect GetTargetVisibleBounds(ui::Layer* child_layer, + ui::Layer* animating_layer) { + return GetVisibleBounds(child_layer, animating_layer, + /*use_target_transform=*/true); +} + +} // namespace + +class RootWindowDeskSwitchAnimatorTest + : public AshTestBase, + public RootWindowDeskSwitchAnimator::Delegate { + public: + RootWindowDeskSwitchAnimatorTest() = default; + RootWindowDeskSwitchAnimatorTest(const RootWindowDeskSwitchAnimatorTest&) = + delete; + RootWindowDeskSwitchAnimatorTest& operator=( + const RootWindowDeskSwitchAnimatorTest&) = delete; + ~RootWindowDeskSwitchAnimatorTest() override = default; + + RootWindowDeskSwitchAnimatorTestApi* test_api() { return test_api_.get(); } + RootWindowDeskSwitchAnimator* animator() { return animator_.get(); } + + int starting_desk_screenshot_taken_count() const { + return starting_desk_screenshot_taken_count_; + } + int ending_desk_screenshot_taken_count() const { + return ending_desk_screenshot_taken_count_; + } + + // Creates an animator from the given indices on the primary root window. + // Creates a test api for the animator as well. + void InitAnimator(int starting_desk_index, int ending_desk_index) { + animator_ = std::make_unique<RootWindowDeskSwitchAnimator>( + Shell::GetPrimaryRootWindow(), starting_desk_index, ending_desk_index, + this, /*for_remove=*/false); + test_api_ = + std::make_unique<RootWindowDeskSwitchAnimatorTestApi>(animator_.get()); + } + + // Wrappers for Take{Starting|Ending}DeskScreenshot that wait for the async + // operation to finish. + void TakeStartingDeskScreenshotAndWait() { + base::RunLoop run_loop; + run_loop_quit_closure_ = run_loop.QuitClosure(); + animator_->TakeStartingDeskScreenshot(); + run_loop.Run(); + } + + void TakeEndingDeskScreenshotAndWait() { + base::RunLoop run_loop; + run_loop_quit_closure_ = run_loop.QuitClosure(); + animator_->TakeEndingDeskScreenshot(); + run_loop.Run(); + } + + // AshTestBase: + void SetUp() override { + scoped_feature_list_.InitAndEnableFeature( + features::kEnhancedDeskAnimations); + AshTestBase::SetUp(); + } + + // RootWindowDeskSwitchAnimator::Delegate: + void OnStartingDeskScreenshotTaken(int ending_desk_index) override { + ++starting_desk_screenshot_taken_count_; + + DCHECK(!run_loop_quit_closure_.is_null()); + std::move(run_loop_quit_closure_).Run(); + } + + void OnEndingDeskScreenshotTaken() override { + ++ending_desk_screenshot_taken_count_; + + DCHECK(!run_loop_quit_closure_.is_null()); + std::move(run_loop_quit_closure_).Run(); + } + + void OnDeskSwitchAnimationFinished() override {} + + private: + base::test::ScopedFeatureList scoped_feature_list_; + + // The RootWindowDeskSwitchAnimator we are testing. + std::unique_ptr<RootWindowDeskSwitchAnimator> animator_; + + // The support test api associated with |animator_|. + std::unique_ptr<RootWindowDeskSwitchAnimatorTestApi> test_api_; + + // Run loop quit closure for waiting for both starting and ending screenshots. + base::OnceClosure run_loop_quit_closure_; + + int starting_desk_screenshot_taken_count_ = 0; + int ending_desk_screenshot_taken_count_ = 0; +}; + +// Tests a simple animation from one desk to another. +TEST_F(RootWindowDeskSwitchAnimatorTest, SimpleAnimation) { + InitAnimator(1, 2); + TakeStartingDeskScreenshotAndWait(); + TakeEndingDeskScreenshotAndWait(); + + // Tests that a simple animation has 2 screenshots, one for each desk. + EXPECT_EQ(1, starting_desk_screenshot_taken_count()); + EXPECT_EQ(1, ending_desk_screenshot_taken_count()); + + // Tests that the animation layer is the expected size. + auto* animation_layer = test_api()->GetAnimationLayer(); + EXPECT_EQ(2u, animation_layer->children().size()); + EXPECT_EQ(ComputeAnimationLayerExpectedSize(2), + animation_layer->bounds().size()); + + // Tests that the screenshot associated with desk index 1 is the one that is + // shown at the beginning of the animation. + EXPECT_EQ(Shell::GetPrimaryRootWindow()->bounds(), + GetVisibleBounds(test_api()->GetScreenshotLayerOfDeskWithIndex(1), + animation_layer)); + + // Tests that the screenshot associated with desk index 2 is the one that is + // shown at the end of the animation. + animator()->StartAnimation(); + EXPECT_EQ(Shell::GetPrimaryRootWindow()->bounds(), + GetVisibleBounds(test_api()->GetScreenshotLayerOfDeskWithIndex(2), + animation_layer)); + EXPECT_EQ(2, test_api()->GetEndingDeskIndex()); +} + +// Tests a chained animation where the replaced animation already has a +// screenshot layer stored. +TEST_F(RootWindowDeskSwitchAnimatorTest, ChainedAnimationNoNewScreenshot) { + InitAnimator(1, 2); + TakeStartingDeskScreenshotAndWait(); + TakeEndingDeskScreenshotAndWait(); + + // Replacing needs to be done while a current animation is underway, otherwise + // it will have no effect. + ui::ScopedAnimationDurationScaleMode non_zero( + ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); + + animator()->StartAnimation(); + // Replacing with an animation going back to desk index 1. No new screenshot + // is needed. + bool needs_screenshot = animator()->ReplaceAnimation(1); + EXPECT_FALSE(needs_screenshot); + + // Tests that no new screenshot was taken as it already existed. + auto* animation_layer = test_api()->GetAnimationLayer(); + EXPECT_EQ(2u, animation_layer->children().size()); + EXPECT_EQ(1, starting_desk_screenshot_taken_count()); + EXPECT_EQ(1, ending_desk_screenshot_taken_count()); + + // Tests that the screenshot associated with desk index 1 is the one that is + // shown at the end of the animation. + animator()->StartAnimation(); + EXPECT_EQ( + Shell::GetPrimaryRootWindow()->bounds(), + GetTargetVisibleBounds(test_api()->GetScreenshotLayerOfDeskWithIndex(1), + animation_layer)); +} + +// Tests a chained animation where we are adding an animation to the right of +// the current animating desks, causing the animation layer to shift left. +TEST_F(RootWindowDeskSwitchAnimatorTest, ChainedAnimationMovingLeft) { + InitAnimator(1, 2); + TakeStartingDeskScreenshotAndWait(); + TakeEndingDeskScreenshotAndWait(); + + // Replacing needs to be done while a current animation is underway, otherwise + // it will have no effect. + ui::ScopedAnimationDurationScaleMode non_zero( + ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); + + // Tests that the animation layer originally has 2 children. + auto* animation_layer = test_api()->GetAnimationLayer(); + EXPECT_EQ(2u, animation_layer->children().size()); + + animator()->StartAnimation(); + + // Replace the current animation to one that goes to desk index 3. + EXPECT_EQ(2, test_api()->GetEndingDeskIndex()); + bool needs_screenshot = animator()->ReplaceAnimation(3); + ASSERT_TRUE(needs_screenshot); + EXPECT_EQ(3, test_api()->GetEndingDeskIndex()); + + // Take a screenshot at the new ending desk. Test that the animation layer now + // has 3 children. + TakeEndingDeskScreenshotAndWait(); + EXPECT_EQ(1, starting_desk_screenshot_taken_count()); + EXPECT_EQ(2, ending_desk_screenshot_taken_count()); + EXPECT_EQ(3u, animation_layer->children().size()); + EXPECT_EQ(ComputeAnimationLayerExpectedSize(3), + animation_layer->bounds().size()); + + animator()->StartAnimation(); + // Tests that the screenshot associated with desk index 3 is the one that is + // shown at the end of the animation. + EXPECT_EQ( + Shell::GetPrimaryRootWindow()->bounds(), + GetTargetVisibleBounds(test_api()->GetScreenshotLayerOfDeskWithIndex(3), + animation_layer)); +} + +// Tests a chained animation where we are adding an animation to the left of +// the current animating desks, causing the animation layer to shift right. +TEST_F(RootWindowDeskSwitchAnimatorTest, ChainedAnimationMovingRight) { + InitAnimator(3, 2); + TakeStartingDeskScreenshotAndWait(); + TakeEndingDeskScreenshotAndWait(); + + // Replacing needs to be done while a current animation is underway, otherwise + // it will have no effect. + ui::ScopedAnimationDurationScaleMode non_zero( + ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); + + animator()->StartAnimation(); + + // Replace the current animation to one that goes to desk index 1. + EXPECT_EQ(2, test_api()->GetEndingDeskIndex()); + bool needs_screenshot = animator()->ReplaceAnimation(1); + ASSERT_TRUE(needs_screenshot); + EXPECT_EQ(1, test_api()->GetEndingDeskIndex()); + + // Take a screenshot at the new ending desk. Test that the animation layer now + // has 3 children. + TakeEndingDeskScreenshotAndWait(); + EXPECT_EQ(1, starting_desk_screenshot_taken_count()); + EXPECT_EQ(2, ending_desk_screenshot_taken_count()); + auto* animation_layer = test_api()->GetAnimationLayer(); + EXPECT_EQ(3u, animation_layer->children().size()); + EXPECT_EQ(ComputeAnimationLayerExpectedSize(3), + animation_layer->bounds().size()); + + animator()->StartAnimation(); + // Tests that the screenshot associated with desk index 1 is the one that is + // shown at the end of the animation. + EXPECT_EQ( + Shell::GetPrimaryRootWindow()->bounds(), + GetTargetVisibleBounds(test_api()->GetScreenshotLayerOfDeskWithIndex(1), + animation_layer)); +} + +// Tests a complex animation which multiple animations are started and replaced. +TEST_F(RootWindowDeskSwitchAnimatorTest, MultipleReplacements) { + InitAnimator(1, 2); + TakeStartingDeskScreenshotAndWait(); + TakeEndingDeskScreenshotAndWait(); + + // Replacing needs to be done while a current animation is underway, otherwise + // it will have no effect. + ui::ScopedAnimationDurationScaleMode non_zero( + ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); + + animator()->StartAnimation(); + + // Replace all the indices in the list one at a time. + auto animation_indices = {1, 0, 1, 2, 1, 2, 3, 2, 1}; + ui::Layer* animation_layer = test_api()->GetAnimationLayer(); + for (int index : animation_indices) { + if (animator()->ReplaceAnimation(index)) + TakeEndingDeskScreenshotAndWait(); + EXPECT_EQ(index, test_api()->GetEndingDeskIndex()); + + // Start the replacement animation. The new animation should have a target + // transform such that the desk at |index| is visible on animation end. + animator()->StartAnimation(); + EXPECT_EQ(Shell::GetPrimaryRootWindow()->bounds(), + GetTargetVisibleBounds( + test_api()->GetScreenshotLayerOfDeskWithIndex(index), + animation_layer)); + } + + // Only 4 screenshots are taken as they are reused. + EXPECT_EQ(1, starting_desk_screenshot_taken_count()); + EXPECT_EQ(3, ending_desk_screenshot_taken_count()); + + // Tests that the screenshot associated with desk index 1 is the one that is + // shown at the end of the animation. + EXPECT_EQ( + Shell::GetPrimaryRootWindow()->bounds(), + GetTargetVisibleBounds(test_api()->GetScreenshotLayerOfDeskWithIndex(1), + animation_layer)); +} + +} // namespace ash