ui: Support lottie-based cursor logic
- Unify cursor image scaling for both software cursor and hardware cursor. They both use wm::GetCursorData now. - Support creating bitmaps from lottie cursor resources for both animated cursor (i.e. throbber) and non-animated cursor. go/lottie-based-chromeos-cursor-op BUG=b:293186112, 1064361 Change-Id: Iba1a836f1162786811077b9862a0538a260ffe30 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5015598 Reviewed-by: Mitsuru Oshima <oshima@chromium.org> Commit-Queue: Yichen Zhou <yichenz@chromium.org> Reviewed-by: Florin Malita <fmalita@chromium.org> Reviewed-by: Xiyuan Xia <xiyuan@chromium.org> Cr-Commit-Position: refs/heads/main@{#1252886}
This commit is contained in:
@ -22,6 +22,7 @@
|
||||
#include "base/command_line.h"
|
||||
#include "base/metrics/histogram_macros.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "third_party/abseil-cpp/absl/types/optional.h"
|
||||
#include "ui/accessibility/accessibility_features.h"
|
||||
#include "ui/aura/env.h"
|
||||
#include "ui/aura/window.h"
|
||||
@ -97,36 +98,28 @@ SkBitmap GetColorAdjustedBitmap(const gfx::ImageSkiaRep& image_rep,
|
||||
return recolored;
|
||||
}
|
||||
|
||||
std::vector<gfx::ImageSkia> GetCursorImages(ui::CursorSize cursor_size,
|
||||
ui::mojom::CursorType type,
|
||||
float scale_factor,
|
||||
gfx::Point& out_hotspot) {
|
||||
std::vector<gfx::ImageSkia> GetCursorImages(
|
||||
ui::CursorSize cursor_size,
|
||||
ui::mojom::CursorType type,
|
||||
int target_cursor_size_in_dip,
|
||||
float dsf,
|
||||
gfx::Point* out_hotspot_in_physical_pixels) {
|
||||
std::vector<gfx::ImageSkia> images;
|
||||
int resource_id;
|
||||
if (!wm::GetCursorDataFor(cursor_size, type, scale_factor, &resource_id,
|
||||
&out_hotspot)) {
|
||||
// Rotation is handled in viz (for aura::Window based cursor)
|
||||
// or fast ink canvas (for fast ink based cursor), so don't do any
|
||||
// rotation here.
|
||||
absl::optional<ui::CursorData> cursor_data = wm::GetCursorData(
|
||||
type, cursor_size, dsf,
|
||||
cursor_size == ui::CursorSize::kLarge
|
||||
? std::make_optional(target_cursor_size_in_dip * dsf)
|
||||
: absl::nullopt,
|
||||
display::Display::ROTATE_0);
|
||||
if (!cursor_data) {
|
||||
return images;
|
||||
}
|
||||
gfx::ImageSkia* image =
|
||||
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(resource_id);
|
||||
const int frame_height = image->height();
|
||||
// Assume frame width equals to frame height.
|
||||
const int frame_width = frame_height;
|
||||
const int total_width = image->width();
|
||||
const int frame_count = total_width / frame_width;
|
||||
|
||||
if (frame_count == 1) {
|
||||
images.push_back(*image);
|
||||
} else if (frame_count > 1) {
|
||||
// For animated cursor, the input image is a sesquence of frames which
|
||||
// needs to be cut into a list of images.
|
||||
images.resize(frame_count);
|
||||
for (int frame = 0; frame < frame_count; ++frame) {
|
||||
const int x_offset = frame_width * frame;
|
||||
gfx::ImageSkia cropped = gfx::ImageSkiaOperations::CreateTiledImage(
|
||||
*image, x_offset, 0, frame_width, frame_height);
|
||||
images[frame] = std::move(cropped);
|
||||
}
|
||||
*out_hotspot_in_physical_pixels = cursor_data->hotspot;
|
||||
for (const auto& bitmap : cursor_data->bitmaps) {
|
||||
images.push_back(gfx::ImageSkia::CreateFromBitmap(bitmap, dsf));
|
||||
}
|
||||
return images;
|
||||
}
|
||||
@ -536,19 +529,22 @@ void CursorWindowController::UpdateCursorImage() {
|
||||
return;
|
||||
}
|
||||
|
||||
float cursor_scale;
|
||||
std::vector<gfx::ImageSkia> images;
|
||||
gfx::Point hot_point_in_physical_pixels;
|
||||
|
||||
if (cursor_.type() == ui::mojom::CursorType::kCustom) {
|
||||
// Custom cursor.
|
||||
SkBitmap bitmap = cursor_.custom_bitmap();
|
||||
gfx::Point hotspot = cursor_.custom_hotspot();
|
||||
if (bitmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
cursor_scale = cursor_.image_scale_factor();
|
||||
float cursor_scale = cursor_.image_scale_factor();
|
||||
|
||||
// Custom cursor's bitmap is already rotated. Revert the rotation because
|
||||
// software cursor's rotation is handled by viz.
|
||||
// TODO(b/320398214): Custom cursor's scaling and rotation
|
||||
// should be handled in ash.
|
||||
const display::Display::Rotation inverted_rotation =
|
||||
static_cast<display::Display::Rotation>(
|
||||
(4 - static_cast<int>(display_.rotation())) % 4);
|
||||
@ -556,39 +552,39 @@ void CursorWindowController::UpdateCursorImage() {
|
||||
&hotspot);
|
||||
images.push_back(gfx::ImageSkia::CreateFromBitmap(bitmap, cursor_scale));
|
||||
hot_point_in_physical_pixels = hotspot;
|
||||
} else {
|
||||
// Do not use the device scale factor, as the cursor will be scaled
|
||||
// by compositor. HW cursor will not be scaled by display zoom, so the
|
||||
// physical size will be inconsistent.
|
||||
cursor_scale = ui::GetScaleForResourceScaleFactor(
|
||||
ui::GetSupportedResourceScaleFactorForRescale(
|
||||
display_.device_scale_factor()));
|
||||
|
||||
images = GetCursorImages(cursor_size_, cursor_.type(), cursor_scale,
|
||||
hot_point_in_physical_pixels);
|
||||
// Use `gfx::ToFlooredPoint` as `ImageSkiaRep::GetWidth` is implemented as
|
||||
// `return static_cast<int>(pixel_width() / scale());`.
|
||||
hot_point_ = gfx::ToFlooredPoint(
|
||||
gfx::ConvertPointToDips(hot_point_in_physical_pixels, cursor_scale));
|
||||
|
||||
// Rescale cursor size. This is used with the combination of accessibility
|
||||
// large cursor. We don't need to care about the case where cursor
|
||||
// compositing is disabled as we always use cursor compositing if
|
||||
// accessibility large cursor is enabled.
|
||||
if (cursor_size_ == ui::CursorSize::kLarge &&
|
||||
large_cursor_size_in_dip_ != images[0].size().height()) {
|
||||
const float rescale = static_cast<float>(large_cursor_size_in_dip_) /
|
||||
static_cast<float>(images[0].size().height());
|
||||
hot_point_ = gfx::ScaleToCeiledPoint(hot_point_, rescale);
|
||||
for (size_t i = 0; i < images.size(); ++i) {
|
||||
images[i] = gfx::ImageSkiaOperations::CreateResizedImage(
|
||||
images[i], skia::ImageOperations::ResizeMethod::RESIZE_BEST,
|
||||
gfx::ScaleToCeiledSize(images[i].size(), rescale));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard cursor.
|
||||
const float dsf = display_.device_scale_factor();
|
||||
|
||||
images =
|
||||
GetCursorImages(cursor_size_, cursor_.type(), large_cursor_size_in_dip_,
|
||||
dsf, &hot_point_in_physical_pixels);
|
||||
if (images.empty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Use `gfx::ToFlooredPoint` as `ImageSkiaRep::GetWidth` is implemented as
|
||||
// `return static_cast<int>(pixel_width() / scale());`.
|
||||
hot_point_ = gfx::ToFlooredPoint(
|
||||
gfx::ConvertPointToDips(hot_point_in_physical_pixels, cursor_scale));
|
||||
|
||||
// Rescale cursor size. This is used with the combination of accessibility
|
||||
// large cursor. We don't need to care about the case where cursor
|
||||
// compositing is disabled as we always use cursor compositing if
|
||||
// accessibility large cursor is enabled.
|
||||
if (cursor_size_ == ui::CursorSize::kLarge &&
|
||||
large_cursor_size_in_dip_ != images[0].size().width()) {
|
||||
const float rescale = static_cast<float>(large_cursor_size_in_dip_) /
|
||||
static_cast<float>(images[0].size().width());
|
||||
hot_point_ = gfx::ScaleToCeiledPoint(hot_point_, rescale);
|
||||
for (size_t i = 0; i < images.size(); ++i) {
|
||||
images[i] = gfx::ImageSkiaOperations::CreateResizedImage(
|
||||
images[i], skia::ImageOperations::ResizeMethod::RESIZE_BEST,
|
||||
gfx::ScaleToCeiledSize(images[i].size(), rescale));
|
||||
}
|
||||
hot_point_ = gfx::ToFlooredPoint(
|
||||
gfx::ConvertPointToDips(hot_point_in_physical_pixels, dsf));
|
||||
}
|
||||
|
||||
if (cursor_color_ != kDefaultCursorColor) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include "ash/display/cursor_window_controller.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include "ash/accessibility/accessibility_controller.h"
|
||||
@ -49,6 +50,22 @@
|
||||
|
||||
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 {
|
||||
@ -236,6 +253,11 @@ TEST_F(CursorWindowControllerTest, ScaleUsesCorrectAssets) {
|
||||
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];
|
||||
@ -294,7 +316,10 @@ TEST_F(CursorWindowControllerTest, DSF) {
|
||||
cursor_scale));
|
||||
const gfx::Size kCursorSize =
|
||||
size != 0 ? gfx::Size(size, size) : kOriginalCursorSize;
|
||||
EXPECT_EQ(GetCursorImage().size(), kCursorSize);
|
||||
// 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
|
||||
@ -306,8 +331,12 @@ TEST_F(CursorWindowControllerTest, DSF) {
|
||||
gfx::ConvertPointToDips(cursor_data->hotspot, cursor_scale));
|
||||
const float rescale =
|
||||
static_cast<float>(kCursorSize.width()) / kOriginalCursorSize.width();
|
||||
EXPECT_EQ(GetCursorHotPoint(),
|
||||
gfx::ScaleToCeiledPoint(kHotspot, rescale));
|
||||
// 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.
|
||||
|
@ -88,11 +88,13 @@ component("wm") {
|
||||
]
|
||||
|
||||
deps = [
|
||||
"//cc/paint",
|
||||
"//skia",
|
||||
"//ui/base/ime",
|
||||
"//ui/compositor_extra",
|
||||
"//ui/events/platform",
|
||||
"//ui/gfx/animation",
|
||||
"//ui/lottie",
|
||||
"//ui/resources",
|
||||
]
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
include_rules = [
|
||||
"+cc/paint/skottie_wrapper.h",
|
||||
"+third_party/skia",
|
||||
"+ui/aura",
|
||||
"+ui/color",
|
||||
"+ui/compositor",
|
||||
"+ui/compositor_extra",
|
||||
"+ui/gfx",
|
||||
"+ui/lottie/animation.h",
|
||||
"+ui/platform_window",
|
||||
"+ui/resources/grit/ui_resources.h",
|
||||
"+skia/ext/image_operations.h",
|
||||
]
|
||||
|
@ -109,7 +109,7 @@ absl::optional<ui::CursorData> CursorLoader::GetCursorData(
|
||||
// TODO(https://crbug.com/1193775): use the actual `rotation_` if that makes
|
||||
// sense for the current use cases of `GetCursorData` (e.g. Chrome Remote
|
||||
// Desktop, WebRTC and VideoRecordingWatcher).
|
||||
return wm::GetCursorData(type, size_, resource_scale_,
|
||||
return wm::GetCursorData(type, size_, resource_scale_, absl::nullopt,
|
||||
display::Display::ROTATE_0);
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ scoped_refptr<ui::PlatformCursor> CursorLoader::CursorFromType(
|
||||
scoped_refptr<ui::PlatformCursor> CursorLoader::LoadCursorFromAsset(
|
||||
CursorType type) {
|
||||
absl::optional<ui::CursorData> cursor_data =
|
||||
wm::GetCursorData(type, size_, resource_scale_, rotation_);
|
||||
wm::GetCursorData(type, size_, resource_scale_, absl::nullopt, rotation_);
|
||||
if (!cursor_data) {
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ TEST(CursorLoaderTest, GetCursorData) {
|
||||
ASSERT_TRUE(cursor_loader_data);
|
||||
const auto cursor_data =
|
||||
GetCursorData(cursor.type(), cursor_size, resource_scale,
|
||||
display.panel_rotation());
|
||||
absl::nullopt, display.panel_rotation());
|
||||
ASSERT_TRUE(cursor_data);
|
||||
ASSERT_EQ(cursor_loader_data->bitmaps.size(),
|
||||
cursor_data->bitmaps.size());
|
||||
|
@ -5,10 +5,16 @@
|
||||
#include "ui/wm/core/cursor_util.h"
|
||||
|
||||
#include <cfloat>
|
||||
#include <memory>
|
||||
|
||||
#include "base/check_op.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/containers/flat_map.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/notreached.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "cc/paint/skottie_wrapper.h"
|
||||
#include "skia/ext/image_operations.h"
|
||||
#include "third_party/abseil-cpp/absl/types/optional.h"
|
||||
#include "third_party/skia/include/core/SkBitmap.h"
|
||||
#include "ui/base/cursor/cursor.h"
|
||||
@ -16,11 +22,16 @@
|
||||
#include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
|
||||
#include "ui/base/resource/resource_bundle.h"
|
||||
#include "ui/base/resource/resource_scale_factor.h"
|
||||
#include "ui/display/display_transform.h"
|
||||
#include "ui/gfx/canvas.h"
|
||||
#include "ui/gfx/geometry/point.h"
|
||||
#include "ui/gfx/geometry/size.h"
|
||||
#include "ui/gfx/geometry/skia_conversions.h"
|
||||
#include "ui/gfx/geometry/transform.h"
|
||||
#include "ui/gfx/image/image_skia.h"
|
||||
#include "ui/gfx/image/image_skia_rep.h"
|
||||
#include "ui/gfx/skbitmap_operations.h"
|
||||
#include "ui/lottie/animation.h"
|
||||
#include "ui/resources/grit/ui_resources.h"
|
||||
|
||||
namespace wm {
|
||||
@ -29,6 +40,15 @@ namespace {
|
||||
|
||||
using ::ui::mojom::CursorType;
|
||||
|
||||
using AnimationCache =
|
||||
base::flat_map<CursorType, std::unique_ptr<lottie::Animation>>;
|
||||
|
||||
// Get a cache for lottie animations.
|
||||
AnimationCache& GetAnimationCache() {
|
||||
static base::NoDestructor<AnimationCache> cache;
|
||||
return *cache;
|
||||
}
|
||||
|
||||
// Converts the SkBitmap to use a different alpha type. Returns true if bitmap
|
||||
// was modified, otherwise returns false.
|
||||
bool ConvertSkBitmapAlphaType(SkBitmap* bitmap, SkAlphaType alpha_type) {
|
||||
@ -51,11 +71,179 @@ bool ConvertSkBitmapAlphaType(SkBitmap* bitmap, SkAlphaType alpha_type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only rotate the cursor's hotpoint. |hotpoint_in_out| is used as
|
||||
// both input and output. |cursor_bitmap_width| and |cursor_bitmap_height|
|
||||
// should be the width and height of the cursor before bitmap rotation.
|
||||
void RotateCursorHotpoint(display::Display::Rotation rotation,
|
||||
int cursor_bitmap_width,
|
||||
int cursor_bitmap_height,
|
||||
gfx::Point* hotpoint_in_out) {
|
||||
switch (rotation) {
|
||||
case display::Display::ROTATE_0:
|
||||
break;
|
||||
case display::Display::ROTATE_90:
|
||||
hotpoint_in_out->SetPoint(cursor_bitmap_height - hotpoint_in_out->y(),
|
||||
hotpoint_in_out->x());
|
||||
break;
|
||||
case display::Display::ROTATE_180:
|
||||
hotpoint_in_out->SetPoint(cursor_bitmap_width - hotpoint_in_out->x(),
|
||||
cursor_bitmap_height - hotpoint_in_out->y());
|
||||
break;
|
||||
case display::Display::ROTATE_270:
|
||||
hotpoint_in_out->SetPoint(hotpoint_in_out->y(),
|
||||
cursor_bitmap_width - hotpoint_in_out->x());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate the cursor's bitmap and hotpoint.
|
||||
// |bitmap_in_out| and |hotpoint_in_out| are used as
|
||||
// both input and output.
|
||||
void RotateCursorBitmapAndHotpoint(display::Display::Rotation rotation,
|
||||
SkBitmap* bitmap_in_out,
|
||||
gfx::Point* hotpoint_in_out) {
|
||||
if (hotpoint_in_out) {
|
||||
RotateCursorHotpoint(rotation, bitmap_in_out->width(),
|
||||
bitmap_in_out->height(), hotpoint_in_out);
|
||||
}
|
||||
|
||||
// SkBitmapOperations::Rotate() needs the bitmap to have premultiplied alpha.
|
||||
DCHECK(rotation == display::Display::ROTATE_0 ||
|
||||
bitmap_in_out->info().alphaType() != kUnpremul_SkAlphaType);
|
||||
|
||||
switch (rotation) {
|
||||
case display::Display::ROTATE_0:
|
||||
break;
|
||||
case display::Display::ROTATE_90:
|
||||
*bitmap_in_out = SkBitmapOperations::Rotate(
|
||||
*bitmap_in_out, SkBitmapOperations::ROTATION_90_CW);
|
||||
break;
|
||||
case display::Display::ROTATE_180:
|
||||
*bitmap_in_out = SkBitmapOperations::Rotate(
|
||||
*bitmap_in_out, SkBitmapOperations::ROTATION_180_CW);
|
||||
break;
|
||||
case display::Display::ROTATE_270:
|
||||
*bitmap_in_out = SkBitmapOperations::Rotate(
|
||||
*bitmap_in_out, SkBitmapOperations::ROTATION_270_CW);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create bitmaps from bitmap pixels. |image_rep| has a scale and holds
|
||||
// bitmap pixels for that scale.
|
||||
void CreateBitmapsFromPixels(const gfx::ImageSkiaRep& image_rep,
|
||||
float scale,
|
||||
display::Display::Rotation rotation,
|
||||
bool is_animated,
|
||||
std::vector<SkBitmap>* bitmaps_out,
|
||||
gfx::Point* hotspot_in_out) {
|
||||
CHECK(bitmaps_out->empty());
|
||||
|
||||
SkBitmap bitmap = image_rep.GetBitmap();
|
||||
if (!is_animated) {
|
||||
// Non-animated cursor.
|
||||
ScaleAndRotateCursorBitmapAndHotpoint(scale / image_rep.scale(), rotation,
|
||||
&bitmap, hotspot_in_out);
|
||||
|
||||
bitmaps_out->push_back(std::move(bitmap));
|
||||
} else {
|
||||
// Animated cursor.
|
||||
|
||||
// The image is assumed to be a concatenation of animation frames from
|
||||
// left to right. Also, each frame is assumed to be square (width ==
|
||||
// height).
|
||||
const int frame_width = bitmap.height();
|
||||
const int frame_height = frame_width;
|
||||
const int total_width = bitmap.width();
|
||||
CHECK_EQ(total_width % frame_width, 0);
|
||||
const int frame_count = total_width / frame_width;
|
||||
CHECK_GT(frame_count, 0);
|
||||
|
||||
for (int frame = 0; frame < frame_count; ++frame) {
|
||||
const int x_offset = frame_width * frame;
|
||||
SkBitmap cropped = SkBitmapOperations::CreateTiledBitmap(
|
||||
bitmap, x_offset, 0, frame_width, frame_height);
|
||||
ScaleAndRotateCursorBitmapAndHotpoint(
|
||||
scale / image_rep.scale(), rotation, &cropped,
|
||||
frame == 0 ? hotspot_in_out : nullptr);
|
||||
|
||||
bitmaps_out->push_back(std::move(cropped));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create bitmaps from static lottie. |image_rep| is unscaled and contains
|
||||
// a paint record for the lottie animation.
|
||||
void CreateBitmapsFromStaticLottie(const gfx::ImageSkiaRep& image_rep,
|
||||
const gfx::Size& scaled_size,
|
||||
float scale,
|
||||
const gfx::Transform& rotation_transform,
|
||||
std::vector<SkBitmap>* bitmaps_out) {
|
||||
CHECK(bitmaps_out->empty());
|
||||
|
||||
// Non-animated cursor.
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(scaled_size.width(), scaled_size.height());
|
||||
bitmap.eraseColor(SK_ColorTRANSPARENT);
|
||||
|
||||
SkCanvas canvas(bitmap);
|
||||
canvas.concat(TransformToSkM44(rotation_transform));
|
||||
canvas.scale(scale, scale);
|
||||
image_rep.GetPaintRecord().Playback(&canvas);
|
||||
bitmap.setImmutable();
|
||||
|
||||
bitmaps_out->push_back(std::move(bitmap));
|
||||
}
|
||||
|
||||
// Create bitmaps from animated lottie.
|
||||
void CreateBitmapsFromAnimatedLottie(int resource_id,
|
||||
const gfx::Size& scaled_size,
|
||||
float scale,
|
||||
const gfx::Transform& rotation_transform,
|
||||
CursorType type,
|
||||
std::vector<SkBitmap>* bitmaps_out) {
|
||||
CHECK(bitmaps_out->empty());
|
||||
|
||||
AnimationCache& cursor_animations = GetAnimationCache();
|
||||
if (!base::Contains(cursor_animations, type)) {
|
||||
std::optional<std::vector<uint8_t>> lottie_bytes =
|
||||
ui::ResourceBundle::GetSharedInstance().GetLottieData(resource_id);
|
||||
scoped_refptr<cc::SkottieWrapper> skottie =
|
||||
cc::SkottieWrapper::CreateSerializable(std::move(*lottie_bytes));
|
||||
cursor_animations[type] = std::make_unique<lottie::Animation>(skottie);
|
||||
}
|
||||
lottie::Animation* animation = cursor_animations[type].get();
|
||||
const float cursor_animation_duration_in_second =
|
||||
animation->GetAnimationDuration().InSecondsF();
|
||||
|
||||
// Target frame rate for animated cursor.
|
||||
const int kAnimatedCursorFramePerSecond = 60;
|
||||
const int frames =
|
||||
kAnimatedCursorFramePerSecond * cursor_animation_duration_in_second;
|
||||
|
||||
for (int i = 0; i < frames; i++) {
|
||||
float t = static_cast<float>(i) / frames;
|
||||
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(scaled_size.width(), scaled_size.height());
|
||||
bitmap.eraseColor(SK_ColorTRANSPARENT);
|
||||
|
||||
cc::SkiaPaintCanvas paint_canvas(bitmap);
|
||||
gfx::Canvas canvas(&paint_canvas, scale);
|
||||
canvas.Transform(rotation_transform);
|
||||
animation->PaintFrame(&canvas, t, scaled_size);
|
||||
bitmap.setImmutable();
|
||||
|
||||
bitmaps_out->push_back(std::move(bitmap));
|
||||
}
|
||||
}
|
||||
|
||||
struct CursorResourceData {
|
||||
CursorType type;
|
||||
int id;
|
||||
gfx::Point hotspot_1x;
|
||||
gfx::Point hotspot_2x;
|
||||
bool is_animated = false;
|
||||
};
|
||||
|
||||
// Cursor resource data indexed by CursorType. Make sure to respect the order
|
||||
@ -65,7 +253,11 @@ constexpr absl::optional<CursorResourceData> kNormalCursorResourceData[] = {
|
||||
{{CursorType::kCross, IDR_AURA_CURSOR_CROSSHAIR, {12, 12}, {24, 24}}},
|
||||
{{CursorType::kHand, IDR_AURA_CURSOR_HAND, {9, 4}, {19, 8}}},
|
||||
{{CursorType::kIBeam, IDR_AURA_CURSOR_IBEAM, {12, 12}, {24, 25}}},
|
||||
{{CursorType::kWait, IDR_AURA_CURSOR_THROBBER, {7, 7}, {14, 14}}},
|
||||
{{CursorType::kWait,
|
||||
IDR_AURA_CURSOR_THROBBER,
|
||||
{7, 7},
|
||||
{14, 14},
|
||||
/*is_animated=*/true}},
|
||||
{{CursorType::kHelp, IDR_AURA_CURSOR_HELP, {4, 4}, {8, 9}}},
|
||||
{{CursorType::kEastResize,
|
||||
IDR_AURA_CURSOR_EAST_RESIZE,
|
||||
@ -137,7 +329,11 @@ constexpr absl::optional<CursorResourceData> kNormalCursorResourceData[] = {
|
||||
{{CursorType::kCell, IDR_AURA_CURSOR_CELL, {11, 11}, {24, 23}}},
|
||||
{{CursorType::kContextMenu, IDR_AURA_CURSOR_CONTEXT_MENU, {4, 4}, {8, 9}}},
|
||||
{{CursorType::kAlias, IDR_AURA_CURSOR_ALIAS, {8, 6}, {15, 11}}},
|
||||
{{CursorType::kProgress, IDR_AURA_CURSOR_THROBBER, {7, 7}, {14, 14}}},
|
||||
{{CursorType::kProgress,
|
||||
IDR_AURA_CURSOR_THROBBER,
|
||||
{7, 7},
|
||||
{14, 14},
|
||||
/*is_animated=*/true}},
|
||||
{{CursorType::kNoDrop, IDR_AURA_CURSOR_NO_DROP, {9, 9}, {18, 18}}},
|
||||
{{CursorType::kCopy, IDR_AURA_CURSOR_COPY, {9, 9}, {18, 18}}},
|
||||
/*CursorType::kNone*/ {},
|
||||
@ -183,7 +379,8 @@ constexpr absl::optional<CursorResourceData> kLargeCursorResourceData[] = {
|
||||
// TODO(https://crbug.com/336867): create IDR_AURA_CURSOR_BIG_THROBBER.
|
||||
IDR_AURA_CURSOR_THROBBER,
|
||||
{7, 7},
|
||||
{14, 14}}},
|
||||
{14, 14},
|
||||
/*is_animated=*/true}},
|
||||
{{CursorType::kHelp, IDR_AURA_CURSOR_BIG_HELP, {10, 11}, {20, 22}}},
|
||||
{{CursorType::kEastResize,
|
||||
IDR_AURA_CURSOR_BIG_EAST_RESIZE,
|
||||
@ -265,7 +462,8 @@ constexpr absl::optional<CursorResourceData> kLargeCursorResourceData[] = {
|
||||
// TODO(https://crbug.com/336867): create IDR_AURA_CURSOR_BIG_THROBBER.
|
||||
IDR_AURA_CURSOR_THROBBER,
|
||||
{7, 7},
|
||||
{14, 14}}},
|
||||
{14, 14},
|
||||
/*is_animated=*/true}},
|
||||
{{CursorType::kNoDrop, IDR_AURA_CURSOR_BIG_NO_DROP, {10, 10}, {20, 20}}},
|
||||
{{CursorType::kCopy, IDR_AURA_CURSOR_BIG_COPY, {21, 11}, {42, 22}}},
|
||||
/*CursorType::kNone*/ {},
|
||||
@ -311,15 +509,18 @@ absl::optional<ui::CursorData> GetCursorData(
|
||||
CursorType type,
|
||||
ui::CursorSize size,
|
||||
float scale,
|
||||
absl::optional<int> target_cursor_size_in_px,
|
||||
display::Display::Rotation rotation) {
|
||||
DCHECK_NE(type, CursorType::kNone);
|
||||
DCHECK_NE(type, CursorType::kCustom);
|
||||
|
||||
int resource_id;
|
||||
gfx::Point hotspot;
|
||||
if (!GetCursorDataFor(size, type, scale, &resource_id, &hotspot)) {
|
||||
bool is_animated;
|
||||
if (!GetCursorDataFor(size, type, scale, &resource_id, &hotspot,
|
||||
&is_animated)) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
DCHECK_NE(type, CursorType::kNone);
|
||||
|
||||
std::vector<SkBitmap> bitmaps;
|
||||
const gfx::ImageSkia* image =
|
||||
@ -328,34 +529,41 @@ absl::optional<ui::CursorData> GetCursorData(
|
||||
ui::GetSupportedResourceScaleFactorForRescale(scale));
|
||||
const gfx::ImageSkiaRep& image_rep = image->GetRepresentation(resource_scale);
|
||||
CHECK_EQ(image_rep.scale(), resource_scale);
|
||||
SkBitmap bitmap = image_rep.GetBitmap();
|
||||
|
||||
// The image is assumed to be a concatenation of animation frames from left
|
||||
// to right. Also, each frame is assumed to be square (width == height).
|
||||
const int frame_width = bitmap.height();
|
||||
const int frame_height = frame_width;
|
||||
const int total_width = bitmap.width();
|
||||
CHECK_EQ(total_width % frame_width, 0);
|
||||
const int frame_count = total_width / frame_width;
|
||||
CHECK_GT(frame_count, 0);
|
||||
if (target_cursor_size_in_px) {
|
||||
// If `target_cursor_size_in_px` presents, use it to calculate scale.
|
||||
// Use `image_rep.GetHeight()` as cursor dp size. An animated bitmap
|
||||
// is composed of horizontally tiled frames so its width could not
|
||||
// be used as cursor size.
|
||||
int cursor_size_in_dp = image_rep.GetHeight();
|
||||
scale = static_cast<float>(target_cursor_size_in_px.value()) /
|
||||
static_cast<float>(cursor_size_in_dp);
|
||||
}
|
||||
|
||||
if (frame_count == 1) {
|
||||
ScaleAndRotateCursorBitmapAndHotpoint(scale / image_rep.scale(), rotation,
|
||||
&bitmap, &hotspot);
|
||||
bitmaps.push_back(std::move(bitmap));
|
||||
if (!image_rep.unscaled()) {
|
||||
// Bitmap-based cursor image.
|
||||
CreateBitmapsFromPixels(image_rep, scale, rotation, is_animated, &bitmaps,
|
||||
&hotspot);
|
||||
} else {
|
||||
// Animated cursor.
|
||||
bitmaps.resize(frame_count);
|
||||
for (int frame = 0; frame < frame_count; ++frame) {
|
||||
const int x_offset = frame_width * frame;
|
||||
SkBitmap cropped = SkBitmapOperations::CreateTiledBitmap(
|
||||
bitmap, x_offset, 0, frame_width, frame_height);
|
||||
ScaleAndRotateCursorBitmapAndHotpoint(scale / image_rep.scale(), rotation,
|
||||
&cropped,
|
||||
frame == 0 ? &hotspot : nullptr);
|
||||
bitmaps[frame] = std::move(cropped);
|
||||
const gfx::Size scaled_size = ScaleToRoundedSize(
|
||||
gfx::Size(image_rep.GetWidth(), image_rep.GetHeight()), scale);
|
||||
const gfx::Transform rotation_transform = display::CreateRotationTransform(
|
||||
rotation, gfx::SizeF(scaled_size.width(), scaled_size.height()));
|
||||
hotspot = gfx::ScaleToFlooredPoint(hotspot, scale);
|
||||
RotateCursorHotpoint(rotation, scaled_size.width(), scaled_size.height(),
|
||||
&hotspot);
|
||||
|
||||
if (!is_animated) {
|
||||
// Non-animated lottie cursor.
|
||||
CreateBitmapsFromStaticLottie(image_rep, scaled_size, scale,
|
||||
rotation_transform, &bitmaps);
|
||||
} else {
|
||||
// Animated lottie cursor.
|
||||
CreateBitmapsFromAnimatedLottie(resource_id, scaled_size, scale,
|
||||
rotation_transform, type, &bitmaps);
|
||||
}
|
||||
}
|
||||
|
||||
return ui::CursorData(std::move(bitmaps), std::move(hotspot), scale);
|
||||
}
|
||||
|
||||
@ -363,66 +571,41 @@ void ScaleAndRotateCursorBitmapAndHotpoint(float scale,
|
||||
display::Display::Rotation rotation,
|
||||
SkBitmap* bitmap,
|
||||
gfx::Point* hotpoint) {
|
||||
// SkBitmapOperations::Rotate() needs the bitmap to have premultiplied alpha,
|
||||
// so convert bitmap alpha type if we are going to rotate.
|
||||
bool was_converted = false;
|
||||
if (rotation != display::Display::ROTATE_0 &&
|
||||
bitmap->info().alphaType() == kUnpremul_SkAlphaType) {
|
||||
ConvertSkBitmapAlphaType(bitmap, kPremul_SkAlphaType);
|
||||
was_converted = true;
|
||||
}
|
||||
|
||||
switch (rotation) {
|
||||
case display::Display::ROTATE_0:
|
||||
break;
|
||||
case display::Display::ROTATE_90:
|
||||
if (hotpoint) {
|
||||
hotpoint->SetPoint(bitmap->height() - hotpoint->y(), hotpoint->x());
|
||||
}
|
||||
*bitmap = SkBitmapOperations::Rotate(
|
||||
*bitmap, SkBitmapOperations::ROTATION_90_CW);
|
||||
break;
|
||||
case display::Display::ROTATE_180:
|
||||
if (hotpoint) {
|
||||
hotpoint->SetPoint(bitmap->width() - hotpoint->x(),
|
||||
bitmap->height() - hotpoint->y());
|
||||
}
|
||||
*bitmap = SkBitmapOperations::Rotate(
|
||||
*bitmap, SkBitmapOperations::ROTATION_180_CW);
|
||||
break;
|
||||
case display::Display::ROTATE_270:
|
||||
if (hotpoint) {
|
||||
hotpoint->SetPoint(hotpoint->y(), bitmap->width() - hotpoint->x());
|
||||
}
|
||||
*bitmap = SkBitmapOperations::Rotate(
|
||||
*bitmap, SkBitmapOperations::ROTATION_270_CW);
|
||||
break;
|
||||
}
|
||||
|
||||
if (was_converted) {
|
||||
ConvertSkBitmapAlphaType(bitmap, kUnpremul_SkAlphaType);
|
||||
}
|
||||
|
||||
if (scale < FLT_EPSILON) {
|
||||
NOTREACHED() << "Scale must be larger than 0.";
|
||||
scale = 1.0f;
|
||||
}
|
||||
|
||||
if (scale == 1.0f)
|
||||
// SkBitmapOperations::Rotate() and skia::ImageOperations::Resize()
|
||||
// need the bitmap to have premultiplied alpha, so convert bitmap alpha type
|
||||
// if we are going to rotate or scale.
|
||||
bool was_converted = false;
|
||||
if ((rotation != display::Display::ROTATE_0 || scale != 1.0f) &&
|
||||
bitmap->info().alphaType() == kUnpremul_SkAlphaType) {
|
||||
ConvertSkBitmapAlphaType(bitmap, kPremul_SkAlphaType);
|
||||
was_converted = true;
|
||||
}
|
||||
|
||||
RotateCursorBitmapAndHotpoint(rotation, bitmap, hotpoint);
|
||||
|
||||
if (scale == 1.0f) {
|
||||
if (was_converted) {
|
||||
ConvertSkBitmapAlphaType(bitmap, kUnpremul_SkAlphaType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
gfx::Size scaled_size = gfx::ScaleToFlooredSize(
|
||||
gfx::Size(bitmap->width(), bitmap->height()), scale);
|
||||
|
||||
// TODO(crbug.com/919866): skia::ImageOperations::Resize() doesn't support
|
||||
// unpremultiplied alpha bitmaps.
|
||||
SkBitmap scaled_bitmap;
|
||||
scaled_bitmap.setInfo(
|
||||
bitmap->info().makeWH(scaled_size.width(), scaled_size.height()));
|
||||
if (scaled_bitmap.tryAllocPixels()) {
|
||||
bitmap->pixmap().scalePixels(
|
||||
scaled_bitmap.pixmap(),
|
||||
{SkFilterMode::kLinear, SkMipmapMode::kNearest});
|
||||
// Use RESIZE_BEST to avoid blurry large cursor on external displays.
|
||||
// See crbug.com/1229231.
|
||||
scaled_bitmap =
|
||||
skia::ImageOperations::Resize(*bitmap, skia::ImageOperations::RESIZE_BEST,
|
||||
scaled_size.width(), scaled_size.height());
|
||||
|
||||
if (was_converted) {
|
||||
ConvertSkBitmapAlphaType(&scaled_bitmap, kUnpremul_SkAlphaType);
|
||||
}
|
||||
|
||||
*bitmap = scaled_bitmap;
|
||||
@ -435,7 +618,8 @@ bool GetCursorDataFor(ui::CursorSize cursor_size,
|
||||
CursorType type,
|
||||
float scale_factor,
|
||||
int* resource_id,
|
||||
gfx::Point* point) {
|
||||
gfx::Point* point,
|
||||
bool* is_animated) {
|
||||
DCHECK_NE(type, CursorType::kCustom);
|
||||
|
||||
// TODO(https://crbug.com/1270302: temporary check until GetCursorDataFor is
|
||||
@ -461,6 +645,7 @@ bool GetCursorDataFor(ui::CursorSize cursor_size,
|
||||
ui::k200Percent) {
|
||||
*point = resource->hotspot_2x;
|
||||
}
|
||||
*is_animated = resource->is_animated;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -24,13 +24,17 @@ struct CursorData;
|
||||
namespace wm {
|
||||
|
||||
// Returns the cursor data corresponding to `type` and the rest of the
|
||||
// parameters. It will load the cursor resource and then scale the bitmap and
|
||||
// hotspot to match `scale`.
|
||||
// parameters. If `target_cursor_size_in_px` presents, it will load the cursor
|
||||
// resource and scale the bitmap and hotspot to match
|
||||
// `target_cursor_size_in_px`, if not it will load the cursor resource and
|
||||
// scale the bitmap and hotspot to match `scale`. The bitmap and hotspot are
|
||||
// both in physical pixels.
|
||||
COMPONENT_EXPORT(UI_WM)
|
||||
absl::optional<ui::CursorData> GetCursorData(
|
||||
ui::mojom::CursorType type,
|
||||
ui::CursorSize size,
|
||||
float scale,
|
||||
absl::optional<int> target_cursor_size_in_px,
|
||||
display::Display::Rotation rotation);
|
||||
|
||||
// Scale and rotate the cursor's bitmap and hotpoint.
|
||||
@ -43,14 +47,16 @@ void ScaleAndRotateCursorBitmapAndHotpoint(float scale,
|
||||
gfx::Point* hotpoint_in_out);
|
||||
|
||||
// Returns data about the cursor `type`. The IDR will be placed in `resource_id`
|
||||
// and the hotspot in `point`. Returns false if resource data for `type` isn't
|
||||
// and the hotspot in `point`. If `is_animated` is true it means the resource
|
||||
// should be animated. Returns false if resource data for `type` isn't
|
||||
// available.
|
||||
COMPONENT_EXPORT(UI_WM)
|
||||
bool GetCursorDataFor(ui::CursorSize cursor_size,
|
||||
ui::mojom::CursorType type,
|
||||
float scale_factor,
|
||||
int* resource_id,
|
||||
gfx::Point* point);
|
||||
gfx::Point* point,
|
||||
bool* is_animated);
|
||||
|
||||
} // namespace wm
|
||||
|
||||
|
@ -103,8 +103,8 @@ TEST(CursorUtil, GetCursorData) {
|
||||
for (const auto& test : kTestCases) {
|
||||
SCOPED_TRACE(test.cursor);
|
||||
constexpr auto kDefaultRotation = display::Display::ROTATE_0;
|
||||
const auto pointer_data =
|
||||
GetCursorData(test.cursor, size, scale, kDefaultRotation);
|
||||
const auto pointer_data = GetCursorData(
|
||||
test.cursor, size, scale, absl::nullopt, kDefaultRotation);
|
||||
ASSERT_TRUE(pointer_data);
|
||||
ASSERT_GT(pointer_data->bitmaps.size(), 0u);
|
||||
EXPECT_EQ(gfx::SkISizeToSize(pointer_data->bitmaps[0].dimensions()),
|
||||
|
Reference in New Issue
Block a user