
Bug: b/313978693 AX-Relnotes: N/A Test: Existing Change-Id: Ie7b74edcf712fcfb5ea7159a66645230a34efc4e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5535230 Reviewed-by: Nikki Moteva <moteva@google.com> Reviewed-by: Kyungjun Lee <kyungjunlee@google.com> Commit-Queue: Katie Dektar <katie@chromium.org> Reviewed-by: Ahmed Fakhry <afakhry@chromium.org> Reviewed-by: Xiaohui Chen <xiaohuic@chromium.org> Cr-Commit-Position: refs/heads/main@{#1305259}
464 lines
18 KiB
C++
464 lines
18 KiB
C++
// Copyright 2015 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/display/cursor_window_controller.h"
|
|
#include "base/memory/raw_ptr.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/session/session_controller_impl.h"
|
|
#include "ash/shell.h"
|
|
#include "ash/test/ash_test_base.h"
|
|
#include "base/command_line.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/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);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
using ::ui::mojom::CursorType;
|
|
|
|
class CursorWindowControllerTest : public AshTestBase {
|
|
public:
|
|
CursorWindowControllerTest() = default;
|
|
|
|
CursorWindowControllerTest(const CursorWindowControllerTest&) = delete;
|
|
CursorWindowControllerTest& operator=(const CursorWindowControllerTest&) =
|
|
delete;
|
|
|
|
~CursorWindowControllerTest() override = default;
|
|
|
|
// AshTestBase:
|
|
void SetUp() override {
|
|
AshTestBase::SetUp();
|
|
|
|
// Shell hides the cursor by default; show it for these tests.
|
|
Shell::Get()->cursor_manager()->ShowCursor();
|
|
|
|
SetCursorCompositionEnabled(true);
|
|
}
|
|
|
|
CursorType GetCursorType() const {
|
|
return cursor_window_controller()->cursor_.type();
|
|
}
|
|
|
|
const gfx::Point& GetCursorHotPoint() const {
|
|
return cursor_window_controller()->hot_point_;
|
|
}
|
|
|
|
aura::Window* GetCursorWindow() const {
|
|
return cursor_window_controller()->cursor_window_.get();
|
|
}
|
|
|
|
const gfx::ImageSkia& GetCursorImage() const {
|
|
return cursor_window_controller()->GetCursorImageForTest();
|
|
}
|
|
|
|
int64_t GetCursorDisplayId() const {
|
|
return cursor_window_controller()->display_.id();
|
|
}
|
|
|
|
void SetCursorCompositionEnabled(bool enabled) {
|
|
// Cursor compositing will be enabled when high contrast mode is turned on.
|
|
// Cursor compositing will be disabled when high contrast mode is the only
|
|
// feature using it and is turned off.
|
|
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 that the composited cursor moves to another display when the real cursor
|
|
// moves to another display.
|
|
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);
|
|
aura::Window* secondary_root =
|
|
window_tree_host_manager->GetRootWindowForDisplayId(secondary_display_id);
|
|
|
|
ui::test::EventGenerator primary_generator(primary_root);
|
|
primary_generator.MoveMouseToInHost(20, 50);
|
|
|
|
EXPECT_TRUE(primary_root->Contains(GetCursorWindow()));
|
|
EXPECT_EQ(primary_display_id, GetCursorDisplayId());
|
|
EXPECT_EQ(CursorType::kNull, GetCursorType());
|
|
gfx::Point hot_point = GetCursorHotPoint();
|
|
EXPECT_EQ(gfx::Point(4, 4), hot_point);
|
|
gfx::Rect cursor_bounds = GetCursorWindow()->GetBoundsInScreen();
|
|
EXPECT_EQ(20, cursor_bounds.x() + hot_point.x());
|
|
EXPECT_EQ(50, cursor_bounds.y() + hot_point.y());
|
|
|
|
// The cursor can only be moved between displays via
|
|
// WindowTreeHost::MoveCursorTo(). EventGenerator uses a hack to move the
|
|
// cursor between displays.
|
|
// Screen location: 220, 50
|
|
// Root location: 20, 50
|
|
secondary_root->MoveCursorTo(gfx::Point(20, 50));
|
|
|
|
// Chrome relies on WindowTreeHost::MoveCursorTo() dispatching a mouse move
|
|
// asynchronously. This is implemented in a platform specific way. Generate a
|
|
// fake mouse move instead of waiting.
|
|
gfx::Point new_cursor_position_in_host(20, 50);
|
|
secondary_root->GetHost()->ConvertDIPToPixels(&new_cursor_position_in_host);
|
|
ui::test::EventGenerator secondary_generator(secondary_root);
|
|
secondary_generator.MoveMouseToInHost(new_cursor_position_in_host);
|
|
|
|
EXPECT_TRUE(secondary_root->Contains(GetCursorWindow()));
|
|
EXPECT_EQ(secondary_display_id, GetCursorDisplayId());
|
|
EXPECT_EQ(CursorType::kNull, GetCursorType());
|
|
hot_point = GetCursorHotPoint();
|
|
EXPECT_EQ(gfx::Point(3, 3), hot_point);
|
|
cursor_bounds = GetCursorWindow()->GetBoundsInScreen();
|
|
EXPECT_EQ(320, cursor_bounds.x() + hot_point.x());
|
|
EXPECT_EQ(50, cursor_bounds.y() + hot_point.y());
|
|
}
|
|
|
|
// Make sure that composition cursor inherits the visibility state.
|
|
TEST_F(CursorWindowControllerTest, VisibilityTest) {
|
|
ASSERT_TRUE(GetCursorWindow());
|
|
EXPECT_TRUE(GetCursorWindow()->IsVisible());
|
|
aura::client::CursorClient* client = Shell::Get()->cursor_manager();
|
|
client->HideCursor();
|
|
ASSERT_TRUE(GetCursorWindow());
|
|
EXPECT_FALSE(GetCursorWindow()->IsVisible());
|
|
|
|
// Normal cursor should be in the correct state.
|
|
SetCursorCompositionEnabled(false);
|
|
ASSERT_FALSE(GetCursorWindow());
|
|
ASSERT_FALSE(client->IsCursorVisible());
|
|
|
|
// Cursor was hidden.
|
|
SetCursorCompositionEnabled(true);
|
|
ASSERT_TRUE(GetCursorWindow());
|
|
EXPECT_FALSE(GetCursorWindow()->IsVisible());
|
|
|
|
// Goback to normal cursor and show the cursor.
|
|
SetCursorCompositionEnabled(false);
|
|
ASSERT_FALSE(GetCursorWindow());
|
|
ASSERT_FALSE(client->IsCursorVisible());
|
|
client->ShowCursor();
|
|
ASSERT_TRUE(client->IsCursorVisible());
|
|
|
|
// Cursor was shown.
|
|
SetCursorCompositionEnabled(true);
|
|
ASSERT_TRUE(GetCursorWindow());
|
|
EXPECT_TRUE(GetCursorWindow()->IsVisible());
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Emulates the behavior of BitmapImageSource used in ResourceBundle.
|
|
class TestCursorImageSource : public gfx::ImageSkiaSource {
|
|
public:
|
|
TestCursorImageSource() = default;
|
|
TestCursorImageSource(const TestCursorImageSource&) = delete;
|
|
TestCursorImageSource operator=(const TestCursorImageSource&) = delete;
|
|
~TestCursorImageSource() override = default;
|
|
|
|
// gfx::ImageSkiaSource:
|
|
gfx::ImageSkiaRep GetImageForScale(float scale) override {
|
|
float resource_scale = ui::GetSupportedResourceScaleFactor(scale);
|
|
if (resource_scale == 1.f) {
|
|
return rep_1x_;
|
|
} else if (resource_scale == 2.f) {
|
|
return rep_2x_;
|
|
}
|
|
NOTREACHED_IN_MIGRATION();
|
|
return rep_1x_;
|
|
}
|
|
|
|
private:
|
|
gfx::ImageSkiaRep rep_1x_ =
|
|
gfx::ImageSkiaRep(gfx::test::CreateBitmap(/*size=*/25, SK_ColorBLACK),
|
|
1.f);
|
|
gfx::ImageSkiaRep rep_2x_ =
|
|
gfx::ImageSkiaRep(gfx::test::CreateBitmap(/*size=*/50, SK_ColorWHITE),
|
|
2.f);
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// Make sure that composition cursor uses correct assets with various scales.
|
|
TEST_F(CursorWindowControllerTest, ScaleUsesCorrectAssets) {
|
|
testing::NiceMock<ui::MockResourceBundleDelegate> mock_delegate;
|
|
gfx::ImageSkia image_skia(std::make_unique<TestCursorImageSource>(),
|
|
gfx::Size(25, 25));
|
|
|
|
auto get_pixel_value = [&](float scale) {
|
|
// TODO(b/318592117): don't need to update display when
|
|
// wm::GetCursorData uses ImageSkia instead of SkBitmap.
|
|
// Trigger regeneration of the cursor image.
|
|
UpdateDisplay(base::StringPrintf("300x200*%f", scale));
|
|
|
|
uint32_t* data = static_cast<uint32_t*>(
|
|
GetCursorImage().GetRepresentation(scale).GetBitmap().getPixels());
|
|
return data[0];
|
|
};
|
|
|
|
EXPECT_CALL(mock_delegate, GetImageNamed(testing::_))
|
|
.WillOnce(testing::Return(gfx::Image(image_skia)));
|
|
|
|
ui::ResourceBundle test_bundle(&mock_delegate);
|
|
auto* original =
|
|
ui::ResourceBundle::SwapSharedInstanceForTesting(&test_bundle);
|
|
// Force re-create composited cursor.
|
|
SetCursorCompositionEnabled(false);
|
|
SetCursorCompositionEnabled(true);
|
|
|
|
// The cursor should use 2x resources when dsf > 1.2.
|
|
EXPECT_EQ(SK_ColorWHITE, get_pixel_value(2.4f));
|
|
EXPECT_EQ(SK_ColorWHITE, get_pixel_value(2.f));
|
|
EXPECT_EQ(SK_ColorWHITE, get_pixel_value(1.25f));
|
|
EXPECT_EQ(SK_ColorBLACK, get_pixel_value(1.20f));
|
|
EXPECT_EQ(SK_ColorBLACK, get_pixel_value(1.15f));
|
|
EXPECT_EQ(SK_ColorBLACK, get_pixel_value(1.f));
|
|
EXPECT_EQ(SK_ColorBLACK, get_pixel_value(0.8f));
|
|
|
|
ui::ResourceBundle::SwapSharedInstanceForTesting(original);
|
|
}
|
|
|
|
// Test different properties of the composited cursor with different device
|
|
// scale factors and zoom levels.
|
|
TEST_F(CursorWindowControllerTest, DSF) {
|
|
const auto& cursor_shape_client = aura::client::GetCursorShapeClient();
|
|
|
|
auto cursor_test = [&](ui::Cursor cursor, float size, float cursor_scale) {
|
|
const float dsf =
|
|
display::Screen::GetScreen()->GetPrimaryDisplay().device_scale_factor();
|
|
SCOPED_TRACE(testing::Message() << cursor.type() << " at scale " << dsf
|
|
<< " and size " << size);
|
|
|
|
cursor_window_controller()->SetCursor(cursor);
|
|
const std::optional<ui::CursorData> cursor_data =
|
|
cursor_shape_client.GetCursorData(cursor);
|
|
DCHECK(cursor_data);
|
|
|
|
// Software cursors look blurry if they are resized by the window they are
|
|
// rendered in, instead of by `ImageSkia`. Make sure
|
|
// `CursorWindowController` creates the cursor in a way that a
|
|
// representation for the display's device scale factor can be directly
|
|
// obtained.
|
|
const gfx::ImageSkiaRep& rep = GetCursorImage().GetRepresentation(dsf);
|
|
EXPECT_EQ(rep.scale(), dsf);
|
|
|
|
const gfx::Size kOriginalCursorSize =
|
|
// ImageSkiaRep::GetWidth() uses static_cast<int>.
|
|
gfx::ToFlooredSize(gfx::ConvertSizeToDips(
|
|
gfx::SkISizeToSize(cursor_data->bitmaps[0].dimensions()),
|
|
cursor_scale));
|
|
const gfx::Size kCursorSize =
|
|
size != 0 ? gfx::Size(size, size) : kOriginalCursorSize;
|
|
// Scaling operations and conversions between dp and px can cause rounding
|
|
// errors. We accept rounding errors <= sqrt(1+1).
|
|
EXPECT_LE(DistanceBetweenSizes(GetCursorImage().size(), kCursorSize),
|
|
sqrt(2));
|
|
|
|
// TODO(hferreiro): the cursor hotspot for non-custom cursors cannot be
|
|
// checked, since the software cursor uses
|
|
// `ui::GetSupportedResourceScaleFactorForRescale`, and
|
|
// `CursorLoader::GetCursorData` uses `ui::GetSupportedResourceScaleFactor`,
|
|
// and 2x cursor hotspots are not just twice the 1x hotspots.
|
|
if (cursor.type() == CursorType::kCustom) {
|
|
const gfx::Point kHotspot = gfx::ToFlooredPoint(
|
|
gfx::ConvertPointToDips(cursor_data->hotspot, cursor_scale));
|
|
const float rescale =
|
|
static_cast<float>(kCursorSize.width()) / kOriginalCursorSize.width();
|
|
// Scaling operations and conversions between dp and px can cause rounding
|
|
// errors. We accept rounding errors <= sqrt(1+1).
|
|
EXPECT_LE(
|
|
DistanceBetweenPoints(GetCursorHotPoint(),
|
|
gfx::ScaleToCeiledPoint(kHotspot, rescale)),
|
|
sqrt(2));
|
|
}
|
|
|
|
// The cursor window should have the same size as the cursor.
|
|
EXPECT_EQ(GetCursorWindow()->bounds().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::GetScreen()
|
|
->GetPrimaryDisplay()
|
|
.device_scale_factor();
|
|
|
|
for (const int size : {0, 32, 64, 128}) {
|
|
cursor_manager->SetCursorSize(size == 0 ? ui::CursorSize::kNormal
|
|
: ui::CursorSize::kLarge);
|
|
Shell::Get()->SetLargeCursorSizeInDip(size);
|
|
|
|
// Default cursor.
|
|
cursor_test(CursorType::kPointer, size,
|
|
// Use the nearest resource scale factor.
|
|
ui::GetScaleForResourceScaleFactor(
|
|
ui::GetSupportedResourceScaleFactor(dsf)));
|
|
|
|
// Custom cursor. Custom cursors are always scaled at the device scale
|
|
// factor. See `WebCursor::GetNativeCursor`.
|
|
cursor_test(ui::Cursor::NewCustom(gfx::test::CreateBitmap(/*size=*/20),
|
|
gfx::Point(10, 10), dsf),
|
|
size, dsf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test that cursor compositing is enabled if at least one of the features that
|
|
// use it is enabled.
|
|
TEST_F(CursorWindowControllerTest, ShouldEnableCursorCompositing) {
|
|
PrefService* prefs =
|
|
Shell::Get()->session_controller()->GetActivePrefService();
|
|
|
|
// Cursor compositing is disabled by default.
|
|
SetCursorCompositionEnabled(false);
|
|
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
|
|
|
|
// Enable large cursor, cursor compositing should be enabled.
|
|
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, true);
|
|
EXPECT_TRUE(cursor_window_controller()->is_cursor_compositing_enabled());
|
|
|
|
// Disable large cursor, cursor compositing should be disabled.
|
|
prefs->SetBoolean(prefs::kAccessibilityLargeCursorEnabled, false);
|
|
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
|
|
}
|
|
|
|
TEST_F(CursorWindowControllerTest, CursorColoringSpotCheck) {
|
|
SetCursorCompositionEnabled(false);
|
|
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
|
|
|
|
// Try a few colors to ensure colorizing is working appropriately.
|
|
const struct {
|
|
SkColor cursor_color; // Set the cursor to this color.
|
|
SkColor not_found; // Spot-check: This color shouldn't be in the cursor.
|
|
SkColor found; // Spot-check: This color should be in the cursor.
|
|
gfx::NativeCursor cursor;
|
|
} kTestCases[] = {
|
|
// Cursors should still have white.
|
|
{SK_ColorMAGENTA, SK_ColorBLUE, SK_ColorWHITE, CursorType::kHand},
|
|
{SK_ColorBLUE, SK_ColorMAGENTA, SK_ColorWHITE, CursorType::kCell},
|
|
{SK_ColorGREEN, SK_ColorBLUE, SK_ColorWHITE, CursorType::kNoDrop},
|
|
// Also cursors should still have transparent.
|
|
{SK_ColorRED, SK_ColorGREEN, SK_ColorTRANSPARENT, CursorType::kPointer},
|
|
// The no drop cursor has red in it, check it's still there:
|
|
// Most of the cursor should be colored, but the red part shouldn't be
|
|
// re-colored.
|
|
{SK_ColorBLUE, SK_ColorGREEN, SkColorSetRGB(173, 8, 8),
|
|
CursorType::kNoDrop},
|
|
// Similarly, the copy cursor has green in it.
|
|
{SK_ColorBLUE, SK_ColorRED, SkColorSetRGB(19, 137, 16),
|
|
CursorType::kCopy},
|
|
};
|
|
|
|
for (const auto& test : kTestCases) {
|
|
// Setting a color enables cursor compositing.
|
|
cursor_window_controller()->SetCursorColor(test.cursor_color);
|
|
Shell::Get()->UpdateCursorCompositingEnabled();
|
|
EXPECT_TRUE(cursor_window_controller()->is_cursor_compositing_enabled());
|
|
cursor_window_controller()->SetCursor(test.cursor);
|
|
const SkBitmap* bitmap = GetCursorImage().bitmap();
|
|
// We should find |cursor_color| pixels in the cursor, but no black or
|
|
// |not_found| color pixels. All black pixels are recolored.
|
|
// We should also find |found| color.
|
|
bool has_color = false;
|
|
bool has_not_found_color = false;
|
|
bool has_found_color = false;
|
|
bool has_black = 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;
|
|
else if (color == SK_ColorBLACK)
|
|
has_black = 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);
|
|
EXPECT_FALSE(has_black);
|
|
}
|
|
|
|
// Set back to the default color and ensure cursor compositing is disabled.
|
|
cursor_window_controller()->SetCursorColor(kDefaultCursorColor);
|
|
Shell::Get()->UpdateCursorCompositingEnabled();
|
|
EXPECT_FALSE(cursor_window_controller()->is_cursor_compositing_enabled());
|
|
}
|
|
|
|
} // namespace ash
|