#include "ash/display/cursor_window_controller.h"
#include <cmath>
#include <utility>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/constants/ash_constants.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/display_util.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/host/ash_window_tree_host.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "components/prefs/pref_service.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/aura/client/cursor_shape_client.h"
#include "ui/aura/test/aura_test_utils.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
#include "ui/base/resource/mock_resource_bundle_delegate.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/wm/core/cursor_manager.h"
namespace ash {
namespace {
float DistanceBetweenPoints(const gfx::Point& p1, const gfx::Point& p2) {
float x_diff = p1.x() - p2.x();
float y_diff = p1.y() - p2.y();
return std::sqrt(x_diff * x_diff + y_diff * y_diff);
}
float DistanceBetweenSizes(const gfx::Size& s1, const gfx::Size& s2) {
float width_diff = s1.width() - s2.width();
float height_diff = s1.height() - s2.height();
return std::sqrt(width_diff * width_diff + height_diff * height_diff);
}
}
using ::ui::mojom::CursorType;
class CursorWindowControllerTest : public AshTestBase {
public:
CursorWindowControllerTest() = default;
CursorWindowControllerTest(const CursorWindowControllerTest&) = delete;
CursorWindowControllerTest& operator=(const CursorWindowControllerTest&) =
delete;
~CursorWindowControllerTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
Shell::Get()->cursor_manager()->ShowCursor();
SetCursorCompositionEnabled(true);
}
CursorType GetCursorType() const {
return cursor_window_controller()->cursor_.type();
}
const gfx::Rect GetCursorBounds() const {
return cursor_window_controller()->GetCursorBoundsInScreenForTest();
}
const gfx::Point& GetCursorHotPoint() const {
return cursor_window_controller()->hot_point_;
}
const aura::Window* GetCursorHostWindow() const {
return cursor_window_controller()->GetCursorHostWindowForTest();
}
const gfx::ImageSkia& GetCursorImage() const {
return cursor_window_controller()->GetCursorImageForTest();
}
int64_t GetCursorDisplayId() const {
return cursor_window_controller()->display_.id();
}
void SetCursorCompositionEnabled(bool enabled) {
Shell::Get()->accessibility_controller()->high_contrast().SetEnabled(
enabled);
}
CursorWindowController* cursor_window_controller() const {
return Shell::Get()->window_tree_host_manager()->cursor_window_controller();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(CursorWindowControllerTest, MoveToDifferentDisplay) {
UpdateDisplay("300x200,300x200*2/r");
WindowTreeHostManager* window_tree_host_manager =
Shell::Get()->window_tree_host_manager();
int64_t primary_display_id = window_tree_host_manager->GetPrimaryDisplayId();
int64_t secondary_display_id =
display::test::DisplayManagerTestApi(display_manager())
.GetSecondaryDisplay()
.id();
aura::Window* primary_root =
window_tree_host_manager->GetRootWindowForDisplayId(primary_display_id);
ui::test::EventGenerator primary_generator(primary_root);
primary_generator.MoveMouseToInHost(20, 50);
EXPECT_TRUE(primary_root->Contains(GetCursorHostWindow()));
EXPECT_EQ(primary_display_id, GetCursorDisplayId());
EXPECT_EQ(CursorType::kNull, GetCursorType());
gfx::Point hot_point = GetCursorHotPoint();
EXPECT_EQ(gfx::Point(6, 4), hot_point);
gfx::Rect cursor_bounds = GetCursorBounds();
EXPECT_EQ(20, cursor_bounds.x() + hot_point.x());
EXPECT_EQ(50, cursor_bounds.y() + hot_point.y());
AshWindowTreeHost* secondary_ash_wth =
window_tree_host_manager->GetAshWindowTreeHostForDisplayId(
secondary_display_id);
auto* secondary_wth = secondary_ash_wth->AsWindowTreeHost();
auto* secondary_root = secondary_wth->window();
MoveCursorTo(secondary_ash_wth, gfx::Point(299, 50), true);
auto& pos = aura::test::QueryLatestMousePositionRequestInHost(secondary_wth);
ui::test::EventGenerator secondary_generator(secondary_root);
secondary_generator.MoveMouseToInHost(pos);
EXPECT_TRUE(secondary_root->Contains(GetCursorHostWindow()));
EXPECT_EQ(secondary_display_id, GetCursorDisplayId());
EXPECT_EQ(CursorType::kNull, GetCursorType());
hot_point = GetCursorHotPoint();
EXPECT_EQ(gfx::Point(6, 4), hot_point);
cursor_bounds = GetCursorBounds();
EXPECT_EQ(300, cursor_bounds.x() + hot_point.x());
EXPECT_EQ(50, cursor_bounds.y() + hot_point.y());
}
TEST_F(CursorWindowControllerTest, VisibilityTest) {
ASSERT_TRUE(GetCursorHostWindow());
EXPECT_TRUE(GetCursorHostWindow()->IsVisible());
aura::client::CursorClient* client = Shell::Get()->cursor_manager();
client->HideCursor();
ASSERT_TRUE(GetCursorHostWindow());
EXPECT_FALSE(GetCursorHostWindow()->IsVisible());
SetCursorCompositionEnabled(false);
ASSERT_FALSE(GetCursorHostWindow());
ASSERT_FALSE(client->IsCursorVisible());
SetCursorCompositionEnabled(true);
ASSERT_TRUE(GetCursorHostWindow());
EXPECT_FALSE(GetCursorHostWindow()->IsVisible());
SetCursorCompositionEnabled(false);
ASSERT_FALSE(GetCursorHostWindow());
ASSERT_FALSE(client->IsCursorVisible());
client->ShowCursor();
ASSERT_TRUE(client->IsCursorVisible());
SetCursorCompositionEnabled(true);
ASSERT_TRUE(GetCursorHostWindow());
EXPECT_TRUE(GetCursorHostWindow()->IsVisible());
}
TEST_F(CursorWindowControllerTest, DSF) {
const auto& cursor_shape_client = aura::client::GetCursorShapeClient();
auto cursor_test = [&](ui::Cursor cursor, float large_cursor_size_in_dip) {
const float dsf =
display::Screen::Get()->GetPrimaryDisplay().device_scale_factor();
SCOPED_TRACE(testing::Message()
<< cursor.type() << " at scale " << dsf << " and size "
<< large_cursor_size_in_dip);
cursor_window_controller()->SetCursor(cursor);
const std::optional<ui::CursorData> cursor_data =
cursor_shape_client.GetCursorData(cursor);
DCHECK(cursor_data);
const gfx::ImageSkiaRep& rep = GetCursorImage().GetRepresentation(dsf);
EXPECT_EQ(rep.scale(), dsf);
const gfx::Size original_cursor_size_in_dip =
gfx::ToFlooredSize(gfx::ConvertSizeToDips(
gfx::SkISizeToSize(cursor_data->bitmaps[0].dimensions()),
cursor_data->scale_factor));
const gfx::Size cursor_size_in_dip =
large_cursor_size_in_dip != 0
? gfx::Size(large_cursor_size_in_dip, large_cursor_size_in_dip)
: original_cursor_size_in_dip;
EXPECT_LE(DistanceBetweenSizes(GetCursorImage().size(), cursor_size_in_dip),
sqrt(2));
if (cursor.type() == CursorType::kCustom) {
const gfx::Point hotspot_in_dip =
gfx::ToFlooredPoint(gfx::ConvertPointToDips(
cursor_data->hotspot, cursor_data->scale_factor));
const float rescale = static_cast<float>(cursor_size_in_dip.height()) /
original_cursor_size_in_dip.height();
ASSERT_LE(DistanceBetweenPoints(
GetCursorHotPoint(),
gfx::ScaleToCeiledPoint(hotspot_in_dip, rescale)),
sqrt(2));
}
EXPECT_EQ(GetCursorBounds().size(), GetCursorImage().size());
};
auto* const cursor_manager = Shell::Get()->cursor_manager();
DCHECK(cursor_manager);
for (const float device_scale_factor : {1.0f, 1.5f, 2.0f, 2.5f}) {
for (const float zoom : {0.8f, 1.0f, 1.25f}) {
UpdateDisplay(
base::StringPrintf("1000x500*%f@%f", device_scale_factor, zoom));
const float dsf =
display::Screen::Get()->GetPrimaryDisplay().device_scale_factor();
for (const int large_cursor_size_in_dip : {0, 32, 64, 128}) {
cursor_manager->SetCursorSize(large_cursor_size_in_dip == 0
? ui::CursorSize::kNormal
: ui::CursorSize::kLarge);
Shell::Get()->SetLargeCursorSizeInDip(large_cursor_size_in_dip);
cursor_test(CursorType::kPointer, large_cursor_size_in_dip);
cursor_test(ui::Cursor::NewCustom(gfx::test::CreateBitmap(20),
gfx::Point(10, 10), dsf),
large_cursor_size_in_dip);
}
}
}
}
TEST_F(CursorWindowControllerTest, ShouldEnableCursorCompositing) {
PrefService* prefs =
Shell::Get()->session_controller()->GetActivePrefService();
display::Display display = display::Screen::Get()->GetPrimaryDisplay();
const float dsf = 2.0f;
display.set_device_scale_factor(dsf);
display.set_maximum_cursor_size(gfx::Size(128, 128));
const gfx::SizeF display_maximum_cursor_size_in_dip =
gfx::ConvertSizeToDips(display.maximum_cursor_size(), dsf);
SetCursorCompositionEnabled(false);
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
prefs->SetBoolean(prefs::kAccessibilityHighContrastEnabled, true);
EXPECT_TRUE(cursor_window_controller()->is_cursor_compositing_enabled());
prefs->SetBoolean(prefs::kAccessibilityHighContrastEnabled, false);
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
prefs->SetBoolean(prefs::kDockedMagnifierEnabled, true);
EXPECT_TRUE(cursor_window_controller()->is_cursor_compositing_enabled());
prefs->SetBoolean(prefs::kDockedMagnifierEnabled, false);
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
cursor_window_controller()->SetDisplay(display);
cursor_window_controller()->SetLargeCursorSizeInDip(
display_maximum_cursor_size_in_dip.height() + 1);
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, true);
EXPECT_TRUE(cursor_window_controller()->is_cursor_compositing_enabled());
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, false);
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
cursor_window_controller()->SetDisplay(display);
cursor_window_controller()->SetLargeCursorSizeInDip(
display_maximum_cursor_size_in_dip.height() - 1);
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, true);
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
}
TEST_F(CursorWindowControllerTest, LargeCursorColoringSpotCheck) {
PrefService* prefs =
Shell::Get()->session_controller()->GetActivePrefService();
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, true);
ASSERT_TRUE(cursor_window_controller()->is_cursor_compositing_enabled());
const struct {
SkColor cursor_color;
SkColor not_found;
SkColor found;
CursorType cursor_type;
} kColorTestCases[] = {
{SK_ColorRED, SK_ColorBLACK, SK_ColorWHITE, CursorType::kPointer},
{SK_ColorRED, SK_ColorBLACK, SK_ColorWHITE, CursorType::kCell},
{SK_ColorMAGENTA, SK_ColorWHITE, SK_ColorBLACK, CursorType::kHand},
{SK_ColorRED, SK_ColorGREEN, SK_ColorTRANSPARENT, CursorType::kPointer},
{SK_ColorBLUE, SK_ColorGREEN, SkColorSetRGB(181, 70, 72),
CursorType::kNoDrop},
{SK_ColorBLUE, SK_ColorRED, SkColorSetRGB(57, 149, 88),
CursorType::kCopy},
};
for (const auto& test : kColorTestCases) {
cursor_window_controller()->SetCursorColor(test.cursor_color);
cursor_window_controller()->SetCursor(test.cursor_type);
const SkBitmap* bitmap = GetCursorImage().bitmap();
bool has_color = false;
bool has_not_found_color = false;
bool has_found_color = false;
for (int x = 0; x < bitmap->width(); ++x) {
for (int y = 0; y < bitmap->height(); ++y) {
SkColor color = bitmap->getColor(x, y);
if (color == test.cursor_color)
has_color = true;
else if (color == test.not_found)
has_not_found_color = true;
else if (color == test.found)
has_found_color = true;
}
}
EXPECT_TRUE(has_color) << color_utils::SkColorToRgbaString(
test.cursor_color);
EXPECT_TRUE(has_found_color)
<< color_utils::SkColorToRgbaString(test.found);
EXPECT_FALSE(has_not_found_color)
<< color_utils::SkColorToRgbaString(test.not_found);
}
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, false);
SetCursorCompositionEnabled(false);
}
TEST_F(CursorWindowControllerTest, RefreshRateChangeUpdatesMaxUpdateRates) {
const int64_t display_id =
display::test::DisplayManagerTestApi(display_manager())
.SetFirstDisplayAsInternalDisplay();
display::Display display = display::Screen::Get()->GetPrimaryDisplay();
auto display_bounds = display.bounds();
EXPECT_NEAR(cursor_window_controller()->max_update_rate_ms(), 11.11, 0.01f);
float refresh_rate = 30.f;
display::ManagedDisplayInfo info =
display::CreateDisplayInfo(display_id, display_bounds);
info.set_refresh_rate(refresh_rate);
display::ManagedDisplayMode mode(display_bounds.size(), refresh_rate,
false,
true);
info.SetManagedDisplayModes({mode});
std::vector<display::ManagedDisplayInfo> display_info_list;
display_info_list.push_back(info);
display_manager()->OnNativeDisplaysChanged(display_info_list);
EXPECT_NEAR(cursor_window_controller()->max_update_rate_ms(), 22.22, 0.01f);
}
}