feat: add custom cursor interface on windows (flutter/engine#36143)
* feat: initial custom cursor and cache support * opt: add cursor validity check * opt: format code * opt: rename more suitable name * opt: key, method name * opt: code * feat: add unittests * opt: change to ref * opt: docs * opt: format code * opt: code * opt: correct code and comments * opt: rgba -> bgra * opt: name * opt: create valid/compatible mask of bitmap & mask * opt: null check * opt: split cursor functions into configurable ones * fix: EncodableValue type * opt: doc for custom cursor key * update: cursor doc * opt: docs * opt: refactor vector cache to unordered_map * opt: code * opt: unifrom key value * opt: uniform key_iter to name_iter * opt: docs * fix: get width out of range
This commit is contained in:
@@ -3181,6 +3181,7 @@ FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plu
|
||||
FILE: ../../../flutter/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc
|
||||
FILE: ../../../flutter/shell/platform/windows/cursor_handler.cc
|
||||
FILE: ../../../flutter/shell/platform/windows/cursor_handler.h
|
||||
FILE: ../../../flutter/shell/platform/windows/cursor_handler_unittests.cc
|
||||
FILE: ../../../flutter/shell/platform/windows/direct_manipulation.cc
|
||||
FILE: ../../../flutter/shell/platform/windows/direct_manipulation.h
|
||||
FILE: ../../../flutter/shell/platform/windows/direct_manipulation_unittests.cc
|
||||
|
||||
@@ -176,6 +176,7 @@ executable("flutter_windows_unittests") {
|
||||
# Common Windows test sources.
|
||||
sources = [
|
||||
"accessibility_bridge_windows_unittests.cc",
|
||||
"cursor_handler_unittests.cc",
|
||||
"direct_manipulation_unittests.cc",
|
||||
"dpi_utils_unittests.cc",
|
||||
"flutter_project_bundle_unittests.cc",
|
||||
|
||||
@@ -11,9 +11,35 @@
|
||||
static constexpr char kChannelName[] = "flutter/mousecursor";
|
||||
|
||||
static constexpr char kActivateSystemCursorMethod[] = "activateSystemCursor";
|
||||
|
||||
static constexpr char kKindKey[] = "kind";
|
||||
|
||||
// This method allows creating a custom cursor with rawBGRA buffer, returns a
|
||||
// string to identify the cursor.
|
||||
static constexpr char kCreateCustomCursorMethod[] =
|
||||
"createCustomCursor/windows";
|
||||
// A string, the custom cursor's name.
|
||||
static constexpr char kCustomCursorNameKey[] = "name";
|
||||
// A list of bytes, the custom cursor's rawBGRA buffer.
|
||||
static constexpr char kCustomCursorBufferKey[] = "buffer";
|
||||
// A double, the x coordinate of the custom cursor's hotspot, starting from
|
||||
// left.
|
||||
static constexpr char kCustomCursorHotXKey[] = "hotX";
|
||||
// A double, the y coordinate of the custom cursor's hotspot, starting from top.
|
||||
static constexpr char kCustomCursorHotYKey[] = "hotY";
|
||||
// An int value for the width of the custom cursor.
|
||||
static constexpr char kCustomCursorWidthKey[] = "width";
|
||||
// An int value for the height of the custom cursor.
|
||||
static constexpr char kCustomCursorHeightKey[] = "height";
|
||||
|
||||
// This method also has an argument `kCustomCursorNameKey` for the name
|
||||
// of the cursor to activate.
|
||||
static constexpr char kSetCustomCursorMethod[] = "setCustomCursor/windows";
|
||||
|
||||
// This method also has an argument `kCustomCursorNameKey` for the name
|
||||
// of the cursor to delete.
|
||||
static constexpr char kDeleteCustomCursorMethod[] =
|
||||
"deleteCustomCursor/windows";
|
||||
|
||||
namespace flutter {
|
||||
|
||||
CursorHandler::CursorHandler(BinaryMessenger* messenger,
|
||||
@@ -45,9 +71,192 @@ void CursorHandler::HandleMethodCall(
|
||||
const auto& kind = std::get<std::string>(kind_iter->second);
|
||||
delegate_->UpdateFlutterCursor(kind);
|
||||
result->Success();
|
||||
} else if (method.compare(kCreateCustomCursorMethod) == 0) {
|
||||
const auto& arguments = std::get<EncodableMap>(*method_call.arguments());
|
||||
auto name_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorNameKey)));
|
||||
if (name_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument name while trying to customize system cursor");
|
||||
return;
|
||||
}
|
||||
auto name = std::get<std::string>(name_iter->second);
|
||||
auto buffer_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorBufferKey)));
|
||||
if (buffer_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument buffer while trying to customize system cursor");
|
||||
return;
|
||||
}
|
||||
auto buffer = std::get<std::vector<uint8_t>>(buffer_iter->second);
|
||||
auto width_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorWidthKey)));
|
||||
if (width_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument width while trying to customize system cursor");
|
||||
return;
|
||||
}
|
||||
auto width = std::get<int>(width_iter->second);
|
||||
auto height_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorHeightKey)));
|
||||
if (height_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument height while trying to customize system cursor");
|
||||
return;
|
||||
}
|
||||
auto height = std::get<int>(height_iter->second);
|
||||
auto hot_x_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorHotXKey)));
|
||||
if (hot_x_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument hotX while trying to customize system cursor");
|
||||
return;
|
||||
}
|
||||
auto hot_x = std::get<double>(hot_x_iter->second);
|
||||
auto hot_y_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorHotYKey)));
|
||||
if (hot_y_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument hotY while trying to customize system cursor");
|
||||
return;
|
||||
}
|
||||
auto hot_y = std::get<double>(hot_y_iter->second);
|
||||
HCURSOR cursor = GetCursorFromBuffer(buffer, hot_x, hot_y, width, height);
|
||||
if (cursor == nullptr) {
|
||||
result->Error("Argument error",
|
||||
"Argument must contains a valid rawBGRA bitmap");
|
||||
return;
|
||||
}
|
||||
// Push the cursor into the cache map.
|
||||
custom_cursors_.emplace(name, std::move(cursor));
|
||||
result->Success(flutter::EncodableValue(std::move(name)));
|
||||
} else if (method.compare(kSetCustomCursorMethod) == 0) {
|
||||
const auto& arguments = std::get<EncodableMap>(*method_call.arguments());
|
||||
auto name_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorNameKey)));
|
||||
if (name_iter == arguments.end()) {
|
||||
result->Error("Argument error",
|
||||
"Missing argument key while trying to set a custom cursor");
|
||||
return;
|
||||
}
|
||||
auto name = std::get<std::string>(name_iter->second);
|
||||
if (custom_cursors_.find(name) == custom_cursors_.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"The custom cursor identified by the argument key cannot be found");
|
||||
return;
|
||||
}
|
||||
HCURSOR cursor = custom_cursors_[name];
|
||||
delegate_->SetFlutterCursor(cursor);
|
||||
result->Success();
|
||||
} else if (method.compare(kDeleteCustomCursorMethod) == 0) {
|
||||
const auto& arguments = std::get<EncodableMap>(*method_call.arguments());
|
||||
auto name_iter =
|
||||
arguments.find(EncodableValue(std::string(kCustomCursorNameKey)));
|
||||
if (name_iter == arguments.end()) {
|
||||
result->Error(
|
||||
"Argument error",
|
||||
"Missing argument key while trying to delete a custom cursor");
|
||||
return;
|
||||
}
|
||||
auto name = std::get<std::string>(name_iter->second);
|
||||
auto it = custom_cursors_.find(name);
|
||||
// If the specified cursor name is not found, the deletion is a noop and
|
||||
// returns success.
|
||||
if (it != custom_cursors_.end()) {
|
||||
DeleteObject(it->second);
|
||||
custom_cursors_.erase(it);
|
||||
}
|
||||
result->Success();
|
||||
} else {
|
||||
result->NotImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
HCURSOR GetCursorFromBuffer(const std::vector<uint8_t>& buffer,
|
||||
double hot_x,
|
||||
double hot_y,
|
||||
int width,
|
||||
int height) {
|
||||
HCURSOR cursor = nullptr;
|
||||
HDC display_dc = GetDC(NULL);
|
||||
// Flutter should returns rawBGRA, which has 8bits * 4channels.
|
||||
BITMAPINFO bmi;
|
||||
memset(&bmi, 0, sizeof(bmi));
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = width;
|
||||
bmi.bmiHeader.biHeight = -height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
bmi.bmiHeader.biSizeImage = width * height * 4;
|
||||
// Create the pixmap DIB section
|
||||
uint8_t* pixels = 0;
|
||||
HBITMAP bitmap =
|
||||
CreateDIBSection(display_dc, &bmi, DIB_RGB_COLORS, (void**)&pixels, 0, 0);
|
||||
ReleaseDC(0, display_dc);
|
||||
if (!bitmap || !pixels) {
|
||||
return nullptr;
|
||||
}
|
||||
int bytes_per_line = width * 4;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
memcpy(pixels + y * bytes_per_line, &buffer[bytes_per_line * y],
|
||||
bytes_per_line);
|
||||
}
|
||||
HBITMAP mask;
|
||||
GetMaskBitmaps(bitmap, mask);
|
||||
ICONINFO icon_info;
|
||||
icon_info.fIcon = 0;
|
||||
icon_info.xHotspot = hot_x;
|
||||
icon_info.yHotspot = hot_y;
|
||||
icon_info.hbmMask = mask;
|
||||
icon_info.hbmColor = bitmap;
|
||||
cursor = CreateIconIndirect(&icon_info);
|
||||
DeleteObject(mask);
|
||||
DeleteObject(bitmap);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
void GetMaskBitmaps(HBITMAP bitmap, HBITMAP& mask_bitmap) {
|
||||
HDC h_dc = ::GetDC(NULL);
|
||||
HDC h_main_dc = ::CreateCompatibleDC(h_dc);
|
||||
HDC h_and_mask_dc = ::CreateCompatibleDC(h_dc);
|
||||
|
||||
// Get the dimensions of the source bitmap
|
||||
BITMAP bm;
|
||||
::GetObject(bitmap, sizeof(BITMAP), &bm);
|
||||
mask_bitmap = ::CreateCompatibleBitmap(h_dc, bm.bmWidth, bm.bmHeight);
|
||||
|
||||
// Select the bitmaps to DC
|
||||
HBITMAP h_old_main_bitmap = (HBITMAP)::SelectObject(h_main_dc, bitmap);
|
||||
HBITMAP h_old_and_mask_bitmap =
|
||||
(HBITMAP)::SelectObject(h_and_mask_dc, mask_bitmap);
|
||||
|
||||
// Scan each pixel of the souce bitmap and create the masks
|
||||
COLORREF main_bit_pixel;
|
||||
for (int x = 0; x < bm.bmWidth; ++x) {
|
||||
for (int y = 0; y < bm.bmHeight; ++y) {
|
||||
main_bit_pixel = ::GetPixel(h_main_dc, x, y);
|
||||
if (main_bit_pixel == RGB(0, 0, 0)) {
|
||||
::SetPixel(h_and_mask_dc, x, y, RGB(255, 255, 255));
|
||||
} else {
|
||||
::SetPixel(h_and_mask_dc, x, y, RGB(0, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
::SelectObject(h_main_dc, h_old_main_bitmap);
|
||||
::SelectObject(h_and_mask_dc, h_old_and_mask_bitmap);
|
||||
|
||||
::DeleteDC(h_and_mask_dc);
|
||||
::DeleteDC(h_main_dc);
|
||||
|
||||
::ReleaseDC(NULL, h_dc);
|
||||
}
|
||||
|
||||
} // namespace flutter
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CURSOR_HANDLER_H_
|
||||
#define FLUTTER_SHELL_PLATFORM_WINDOWS_CURSOR_HANDLER_H_
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "flutter/shell/platform/common/client_wrapper/include/flutter/binary_messenger.h"
|
||||
#include "flutter/shell/platform/common/client_wrapper/include/flutter/encodable_value.h"
|
||||
#include "flutter/shell/platform/common/client_wrapper/include/flutter/method_channel.h"
|
||||
@@ -30,8 +32,21 @@ class CursorHandler {
|
||||
|
||||
// The delegate for cursor updates.
|
||||
WindowBindingHandler* delegate_;
|
||||
|
||||
// The cache map for custom cursors.
|
||||
std::unordered_map<std::string, HCURSOR> custom_cursors_;
|
||||
};
|
||||
|
||||
// Create a cursor from a rawBGRA buffer and the cursor info.
|
||||
HCURSOR GetCursorFromBuffer(const std::vector<uint8_t>& buffer,
|
||||
double hot_x,
|
||||
double hot_y,
|
||||
int width,
|
||||
int height);
|
||||
|
||||
// Get the corresponding mask bitmap from the source bitmap.
|
||||
void GetMaskBitmaps(HBITMAP bitmap, HBITMAP& mask_bitmap);
|
||||
|
||||
} // namespace flutter
|
||||
|
||||
#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_CURSOR_HANDLER_H_
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2013 The Flutter 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 "flutter/shell/platform/windows/cursor_handler.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace flutter {
|
||||
namespace testing {
|
||||
TEST(CursorHandlerTest, CreateDummyCursor) {
|
||||
// Create a 4x4 rawBGRA dummy cursor buffer.
|
||||
std::vector<uint8_t> buffer;
|
||||
for (int i = 0; i < 4 * 4 * 4; i++) {
|
||||
buffer.push_back(0);
|
||||
}
|
||||
// Create the cursor from buffer provided above.
|
||||
auto cursor = GetCursorFromBuffer(buffer, 0.0, 0.0, 4, 4);
|
||||
// Expect cursor is not null.
|
||||
EXPECT_NE(cursor, nullptr);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace flutter
|
||||
@@ -105,6 +105,11 @@ void FlutterWindow::UpdateFlutterCursor(const std::string& cursor_name) {
|
||||
current_cursor_ = GetCursorByName(cursor_name);
|
||||
}
|
||||
|
||||
void FlutterWindow::SetFlutterCursor(HCURSOR cursor) {
|
||||
current_cursor_ = cursor;
|
||||
::SetCursor(current_cursor_);
|
||||
}
|
||||
|
||||
void FlutterWindow::OnWindowResized() {
|
||||
// Blocking the raster thread until DWM flushes alleviates glitches where
|
||||
// previous size surface is stretched over current size view.
|
||||
|
||||
@@ -131,6 +131,9 @@ class FlutterWindow : public Window, public WindowBindingHandler {
|
||||
// |FlutterWindowBindingHandler|
|
||||
void UpdateFlutterCursor(const std::string& cursor_name) override;
|
||||
|
||||
// |FlutterWindowBindingHandler|
|
||||
void SetFlutterCursor(HCURSOR cursor) override;
|
||||
|
||||
// |FlutterWindowBindingHandler|
|
||||
void OnWindowResized() override;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class MockWindowBindingHandler : public WindowBindingHandler {
|
||||
MOCK_METHOD0(OnWindowResized, void());
|
||||
MOCK_METHOD0(GetPhysicalWindowBounds, PhysicalWindowBounds());
|
||||
MOCK_METHOD1(UpdateFlutterCursor, void(const std::string& cursor_name));
|
||||
MOCK_METHOD1(SetFlutterCursor, void(HCURSOR cursor_name));
|
||||
MOCK_METHOD1(OnCursorRectUpdated, void(const Rect& rect));
|
||||
MOCK_METHOD0(OnResetImeComposing, void());
|
||||
MOCK_METHOD3(OnBitmapSurfaceUpdated,
|
||||
|
||||
@@ -72,6 +72,9 @@ class WindowBindingHandler {
|
||||
// content. See mouse_cursor.dart for the values and meanings of cursor_name.
|
||||
virtual void UpdateFlutterCursor(const std::string& cursor_name) = 0;
|
||||
|
||||
// Sets the cursor directly from a cursor handle.
|
||||
virtual void SetFlutterCursor(HCURSOR cursor) = 0;
|
||||
|
||||
// Invoked when the cursor/composing rect has been updated in the framework.
|
||||
virtual void OnCursorRectUpdated(const Rect& rect) = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user