chromium/ash/app_list/views/app_drag_icon_proxy_unittest.cc

// 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/app_list/views/app_drag_icon_proxy.h"

#include <memory>

#include "ash/public/cpp/image_util.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/test/bind.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/views/widget/widget.h"

namespace ash {

class AppDragIconProxyTest : public AshTestBase {
 public:
  AppDragIconProxyTest() = default;
  AppDragIconProxyTest(const AppDragIconProxyTest&) = delete;
  AppDragIconProxyTest& operator=(const AppDragIconProxyTest&) = delete;
  ~AppDragIconProxyTest() override = default;

  gfx::Rect GetTargetAppDragIconImageBounds(AppDragIconProxy* drag_icon_proxy) {
    gfx::Rect layer_target_position =
        drag_icon_proxy->GetImageLayerForTesting()
            ->GetTargetTransform()
            .MapRect(
                drag_icon_proxy->GetImageLayerForTesting()->GetTargetBounds());
    gfx::Rect widget_target_bounds =
        drag_icon_proxy->GetWidgetForTesting()->GetLayer()->GetTargetBounds();
    layer_target_position.Offset(widget_target_bounds.OffsetFromOrigin());
    return layer_target_position;
  }
};

TEST_F(AppDragIconProxyTest, UpdatingLocationRespectsIconOffset) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);

  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(150, 200));
  EXPECT_EQ(gfx::Rect(gfx::Point(115, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(150, 250));
  EXPECT_EQ(gfx::Rect(gfx::Point(115, 205), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(120, 270));
  EXPECT_EQ(gfx::Rect(gfx::Point(85, 225), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());
}

TEST_F(AppDragIconProxyTest, SecondaryDisplay) {
  UpdateDisplay("1000x800,1000x800");
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetRootWindowForDisplayId(GetSecondaryDisplay().id()),
      ash::image_util::CreateEmptyImage(gfx::Size(50, 50)), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(1100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);

  EXPECT_EQ(gfx::Rect(gfx::Point(1065, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(1150, 200));
  EXPECT_EQ(gfx::Rect(gfx::Point(1115, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(1150, 250));
  EXPECT_EQ(gfx::Rect(gfx::Point(1115, 205), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(1120, 270));
  EXPECT_EQ(gfx::Rect(gfx::Point(1085, 225), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());
}

TEST_F(AppDragIconProxyTest, ScaledBounds) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(200, 400),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/2.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);

  EXPECT_EQ(gfx::Rect(gfx::Point(140, 330), gfx::Size(100, 100)),
            drag_icon_proxy->GetBoundsInScreen());

  drag_icon_proxy->UpdatePosition(gfx::Point(150, 300));
  EXPECT_EQ(gfx::Rect(gfx::Point(90, 230), gfx::Size(100, 100)),
            drag_icon_proxy->GetBoundsInScreen());
}

TEST_F(AppDragIconProxyTest, BlurSetsRoundedCorners) {
  const gfx::Size image_size = gfx::Size(50, 50);
  // Create a folder icon proxy because only folder icons have background blur.
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/true, /*shadow_size=*/image_size);

  // The background should be circular.
  EXPECT_EQ(gfx::RoundedCornersF(25.0f).ToString(),
            drag_icon_proxy->GetBlurredLayerForTesting()
                ->rounded_corner_radii()
                .ToString());
  EXPECT_EQ(30.0f,
            drag_icon_proxy->GetBlurredLayerForTesting()->background_blur());

  // Test that background corner radii are scaled with the image.
  drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/2.0f,
      /*is_folder_icon=*/true, /*shadow_size=*/image_size);

  // The background should be circular.
  EXPECT_EQ(gfx::RoundedCornersF(50.0f).ToString(),
            drag_icon_proxy->GetBlurredLayerForTesting()
                ->rounded_corner_radii()
                .ToString());
  EXPECT_EQ(30.0f,
            drag_icon_proxy->GetBlurredLayerForTesting()->background_blur());
}

TEST_F(AppDragIconProxyTest, AnimateBoundsForClosure) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);
  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  base::RunLoop run_loop;
  gfx::Rect target_bounds = gfx::Rect(gfx::Point(50, 50), gfx::Size(10, 10));
  drag_icon_proxy->AnimateToBoundsAndCloseWidget(target_bounds,
                                                 run_loop.QuitClosure());

  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());
  EXPECT_EQ(target_bounds,
            GetTargetAppDragIconImageBounds(drag_icon_proxy.get()));

  // Verify the animation callback gets called.
  run_loop.Run();

  // The widget should be closed at this point - verify public methods do no
  // crash if used.
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->UpdatePosition(gfx::Point(20, 20));
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->SetOpacity(1.0f);
}

TEST_F(AppDragIconProxyTest, CloseAnimationCallbackCalledWithZeroAnimation) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(gfx::Size(50, 50)), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);
  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
      ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);

  bool animation_callback_called = false;
  gfx::Rect target_bounds = gfx::Rect(gfx::Point(50, 50), gfx::Size(10, 10));
  drag_icon_proxy->AnimateToBoundsAndCloseWidget(
      target_bounds, base::BindLambdaForTesting([&animation_callback_called]() {
        animation_callback_called = true;
      }));
  EXPECT_TRUE(animation_callback_called);
  // The widget should be closed at this point - verify public methods do no
  // crash if used.
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->UpdatePosition(gfx::Point(20, 20));
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->SetOpacity(1.0f);
}

// Tests the timing of animation callback if app drag icon is deleted while
// another layer animation is in progress.
TEST_F(AppDragIconProxyTest,
       CloseAnimationInterruptsAnotherLayerTransformAnimation) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);
  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  // Animate the drag image layer transform, so
  // `AnimateToBoundsAndCloseWidget()` interrupts an animation.
  {
    ui::ScopedLayerAnimationSettings animation_settings(
        drag_icon_proxy->GetImageLayerForTesting()->GetAnimator());
    animation_settings.SetTransitionDuration(base::Milliseconds(300));
    gfx::Transform transform;
    transform.Translate(100, 100);
    drag_icon_proxy->GetImageLayerForTesting()->SetTransform(transform);
  }

  base::RunLoop run_loop;
  bool animation_callback_called = false;
  gfx::Rect target_bounds = gfx::Rect(gfx::Point(50, 50), gfx::Size(10, 10));
  drag_icon_proxy->AnimateToBoundsAndCloseWidget(
      target_bounds,
      base::BindLambdaForTesting([&animation_callback_called, &run_loop]() {
        animation_callback_called = true;
        run_loop.Quit();
      }));

  // The animation callback should not yet been called, as the close animation
  // has not finished.
  ASSERT_FALSE(animation_callback_called);

  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());
  EXPECT_EQ(target_bounds,
            GetTargetAppDragIconImageBounds(drag_icon_proxy.get()));

  // Wait for the layer animation to complete.
  run_loop.Run();

  // The widget should be closed at this point - verify public methods do no
  // crash if used.
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->UpdatePosition(gfx::Point(20, 20));
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->SetOpacity(1.0f);
}

// Verifies that "close" animation completion callback gets called if the proxy
// is reset during the animation.
TEST_F(AppDragIconProxyTest, ProxyResetDuringCloseAnimation) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);
  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  bool animation_callback_called = false;
  gfx::Rect target_bounds = gfx::Rect(gfx::Point(50, 50), gfx::Size(10, 10));
  drag_icon_proxy->AnimateToBoundsAndCloseWidget(
      target_bounds, base::BindLambdaForTesting([&animation_callback_called]() {
        animation_callback_called = true;
      }));
  EXPECT_FALSE(animation_callback_called);

  drag_icon_proxy.reset();
  EXPECT_TRUE(animation_callback_called);
}

// Tests that calls to `UpdatePosition()` are ignored if
// `AnimateToBoundsAndCloseWidget()` has been called.
TEST_F(AppDragIconProxyTest, UpdatePositionDuringCloseIsNoOp) {
  const gfx::Size image_size = gfx::Size(50, 50);
  auto drag_icon_proxy = std::make_unique<AppDragIconProxy>(
      Shell::GetPrimaryRootWindow(),
      ash::image_util::CreateEmptyImage(image_size), gfx::ImageSkia(),
      /*pointer_location_in_screen=*/gfx::Point(100, 200),
      /*pointer_offset_from_center=*/gfx::Vector2d(10, 20),
      /*scale_factor=*/1.0f,
      /*is_folder_icon=*/false, /*shadow_size=*/image_size);
  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());

  ui::ScopedAnimationDurationScaleMode non_zero_duration_mode(
      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  base::RunLoop run_loop;
  gfx::Rect target_bounds = gfx::Rect(gfx::Point(50, 50), gfx::Size(10, 10));
  drag_icon_proxy->AnimateToBoundsAndCloseWidget(target_bounds,
                                                 run_loop.QuitClosure());

  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());
  EXPECT_EQ(target_bounds,
            GetTargetAppDragIconImageBounds(drag_icon_proxy.get()));

  // Updating location should not work at this point.
  drag_icon_proxy->UpdatePosition(gfx::Point(200, 300));
  EXPECT_EQ(gfx::Rect(gfx::Point(65, 155), gfx::Size(50, 50)),
            drag_icon_proxy->GetBoundsInScreen());
  EXPECT_EQ(target_bounds,
            GetTargetAppDragIconImageBounds(drag_icon_proxy.get()));

  // Verify the animation callback gets called.
  run_loop.Run();

  // The widget should be closed at this point - verify public methods do no
  // crash if used.
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->UpdatePosition(gfx::Point(20, 20));
  EXPECT_EQ(gfx::Rect(), drag_icon_proxy->GetBoundsInScreen());
  drag_icon_proxy->SetOpacity(1.0f);
}

}  // namespace ash