From 0ddd54b5e42083234e2dbee9c75b3e41ebddb819 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 2 Dec 2022 05:08:28 +0800 Subject: [PATCH] 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 --- .../ci/licenses_golden/licenses_flutter | 1 + .../flutter/shell/platform/windows/BUILD.gn | 1 + .../shell/platform/windows/cursor_handler.cc | 211 +++++++++++++++++- .../shell/platform/windows/cursor_handler.h | 15 ++ .../windows/cursor_handler_unittests.cc | 27 +++ .../shell/platform/windows/flutter_window.cc | 5 + .../shell/platform/windows/flutter_window.h | 3 + .../testing/mock_window_binding_handler.h | 1 + .../platform/windows/window_binding_handler.h | 3 + 9 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index ca52a8fdd0..bb63196688 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -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 diff --git a/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn index 8e6af332d7..53b997f584 100644 --- a/engine/src/flutter/shell/platform/windows/BUILD.gn +++ b/engine/src/flutter/shell/platform/windows/BUILD.gn @@ -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", diff --git a/engine/src/flutter/shell/platform/windows/cursor_handler.cc b/engine/src/flutter/shell/platform/windows/cursor_handler.cc index 9e43b051be..1b9cf488a2 100644 --- a/engine/src/flutter/shell/platform/windows/cursor_handler.cc +++ b/engine/src/flutter/shell/platform/windows/cursor_handler.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(kind_iter->second); delegate_->UpdateFlutterCursor(kind); result->Success(); + } else if (method.compare(kCreateCustomCursorMethod) == 0) { + const auto& arguments = std::get(*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(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>(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(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(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(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(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(*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(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(*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(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& 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 diff --git a/engine/src/flutter/shell/platform/windows/cursor_handler.h b/engine/src/flutter/shell/platform/windows/cursor_handler.h index 7aa75e9dc9..759ade7e67 100644 --- a/engine/src/flutter/shell/platform/windows/cursor_handler.h +++ b/engine/src/flutter/shell/platform/windows/cursor_handler.h @@ -5,6 +5,8 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_CURSOR_HANDLER_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_CURSOR_HANDLER_H_ +#include + #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 custom_cursors_; }; +// Create a cursor from a rawBGRA buffer and the cursor info. +HCURSOR GetCursorFromBuffer(const std::vector& 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_ diff --git a/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc b/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc new file mode 100644 index 0000000000..05d43f33f7 --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc @@ -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 +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { +TEST(CursorHandlerTest, CreateDummyCursor) { + // Create a 4x4 rawBGRA dummy cursor buffer. + std::vector 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 diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.cc b/engine/src/flutter/shell/platform/windows/flutter_window.cc index c8d3e6a23e..165fd52413 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_window.cc @@ -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. diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.h b/engine/src/flutter/shell/platform/windows/flutter_window.h index 5af03bad9c..b8f3183bcb 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window.h +++ b/engine/src/flutter/shell/platform/windows/flutter_window.h @@ -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; diff --git a/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h b/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h index 6a191b0e18..dcbbc9e15f 100644 --- a/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h +++ b/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h @@ -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, diff --git a/engine/src/flutter/shell/platform/windows/window_binding_handler.h b/engine/src/flutter/shell/platform/windows/window_binding_handler.h index 13d04eb604..47b8ecf0e0 100644 --- a/engine/src/flutter/shell/platform/windows/window_binding_handler.h +++ b/engine/src/flutter/shell/platform/windows/window_binding_handler.h @@ -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;