835183a0c3
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
215 lines
7.3 KiB
C++
215 lines
7.3 KiB
C++
// Copyright (c) 2026 Hans Kokx.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "include/custom_cursor/custom_cursor_plugin.h"
|
|
|
|
// This must be included before many other Windows headers.
|
|
#include <windows.h>
|
|
|
|
#include <gdiplus.h>
|
|
|
|
#include <flutter/method_channel.h>
|
|
#include <flutter/plugin_registrar_windows.h>
|
|
#include <flutter/standard_method_codec.h>
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#pragma comment(lib, "gdiplus.lib")
|
|
|
|
namespace custom_cursor {
|
|
|
|
using flutter::EncodableMap;
|
|
using flutter::EncodableValue;
|
|
|
|
namespace {
|
|
|
|
// Look up [key] in [map] or return null.
|
|
const EncodableValue* ValueOrNull(const EncodableMap& map, const char* key) {
|
|
auto it = map.find(EncodableValue(key));
|
|
return it == map.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
double GetDouble(const EncodableMap& map, const char* key, double fallback) {
|
|
const auto* v = ValueOrNull(map, key);
|
|
if (v == nullptr) return fallback;
|
|
if (std::holds_alternative<double>(*v)) return std::get<double>(*v);
|
|
if (std::holds_alternative<int>(*v)) return std::get<int>(*v);
|
|
if (std::holds_alternative<int64_t>(*v)) {
|
|
return static_cast<double>(std::get<int64_t>(*v));
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
// Clamp a dimension to the browser/native cursor max size (128px).
|
|
int ClampSize(int size) {
|
|
return size < 1 ? 1 : (size > 128 ? 128 : size);
|
|
}
|
|
|
|
// Clear the custom cursor: drop the window-class cursor and show the system
|
|
// default (arrow) immediately. A WM_SETCURSOR is posted afterwards so the
|
|
// window re-evaluates the cursor right away instead of waiting for the next
|
|
// mouse move.
|
|
void ResetToDefaultCursor(flutter::PluginRegistrarWindows* registrar) {
|
|
HWND hwnd = registrar->GetView() ? registrar->GetView()->GetNativeWindow()
|
|
: nullptr;
|
|
if (hwnd != nullptr) {
|
|
SetClassLongPtr(hwnd, GCLP_HCURSOR, 0);
|
|
PostMessage(
|
|
hwnd, WM_SETCURSOR, reinterpret_cast<WPARAM>(hwnd),
|
|
MAKELPARAM(HTCLIENT, WM_MOUSEMOVE));
|
|
}
|
|
SetCursor(LoadCursor(nullptr, IDC_ARROW));
|
|
}
|
|
|
|
// Decode PNG [bytes] into an HCURSOR with the given hotspot (device pixels)
|
|
// and clamp width/height to 128px max.
|
|
HCURSOR CursorFromPng(const std::vector<uint8_t>& bytes, int hot_x,
|
|
int hot_y) {
|
|
HGLOBAL global = GlobalAlloc(GMEM_MOVEABLE, bytes.size());
|
|
if (global == nullptr) return nullptr;
|
|
void* mem = GlobalLock(global);
|
|
memcpy(mem, bytes.data(), bytes.size());
|
|
GlobalUnlock(global);
|
|
|
|
IStream* stream = nullptr;
|
|
if (CreateStreamOnHGlobal(global, TRUE, &stream) != S_OK) {
|
|
GlobalFree(global);
|
|
return nullptr;
|
|
}
|
|
|
|
HCURSOR cursor = nullptr;
|
|
{
|
|
Gdiplus::Bitmap bitmap(stream);
|
|
if (bitmap.GetLastStatus() == Gdiplus::Ok) {
|
|
HBITMAP hbitmap = nullptr;
|
|
bitmap.GetHBITMAP(Gdiplus::Color(0, 0, 0, 0), &hbitmap);
|
|
if (hbitmap != nullptr) {
|
|
// Clamp the cursor bitmap to 128x128 max (browser/native limit).
|
|
int width = ClampSize(static_cast<int>(bitmap.GetWidth()));
|
|
int height = ClampSize(static_cast<int>(bitmap.GetHeight()));
|
|
|
|
HBITMAP mask =
|
|
CreateBitmap(width, height, 1, 1, nullptr);
|
|
ICONINFO info = {};
|
|
info.fIcon = FALSE; // FALSE = cursor (uses hotspot)
|
|
info.xHotspot = static_cast<DWORD>(hot_x);
|
|
info.yHotspot = static_cast<DWORD>(hot_y);
|
|
info.hbmMask = mask;
|
|
info.hbmColor = hbitmap;
|
|
cursor = reinterpret_cast<HCURSOR>(CreateIconIndirect(&info));
|
|
DeleteObject(hbitmap);
|
|
DeleteObject(mask);
|
|
}
|
|
}
|
|
}
|
|
stream->Release(); // also frees the HGLOBAL (TRUE above)
|
|
return cursor;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// static
|
|
void CustomCursorPlugin::RegisterWithRegistrar(
|
|
flutter::PluginRegistrarWindows* registrar) {
|
|
auto channel = std::make_unique<flutter::MethodChannel<EncodableValue>>(
|
|
registrar->messenger(), "custom_cursor",
|
|
&flutter::StandardMethodCodec::GetInstance());
|
|
|
|
auto plugin = std::make_unique<CustomCursorPlugin>(registrar);
|
|
|
|
channel->SetMethodCallHandler(
|
|
[plugin_pointer = plugin.get()](const auto& call, auto result) {
|
|
plugin_pointer->HandleMethodCall(call, std::move(result));
|
|
});
|
|
|
|
registrar->AddPlugin(std::move(plugin));
|
|
}
|
|
|
|
CustomCursorPlugin::CustomCursorPlugin(
|
|
flutter::PluginRegistrarWindows* registrar)
|
|
: registrar_(registrar) {
|
|
Gdiplus::GdiplusStartupInput input;
|
|
Gdiplus::GdiplusStartup(&gdiplus_token_, &input, nullptr);
|
|
}
|
|
|
|
CustomCursorPlugin::~CustomCursorPlugin() {
|
|
for (auto& entry : cursors_) {
|
|
if (entry.second != nullptr) DestroyCursor(entry.second);
|
|
}
|
|
if (gdiplus_token_ != 0) Gdiplus::GdiplusShutdown(gdiplus_token_);
|
|
}
|
|
|
|
void CustomCursorPlugin::HandleMethodCall(
|
|
const flutter::MethodCall<EncodableValue>& method_call,
|
|
std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {
|
|
const auto* args = std::get_if<EncodableMap>(method_call.arguments());
|
|
const std::string& method = method_call.method_name();
|
|
|
|
if (method == "registerCursor" && args != nullptr) {
|
|
const auto* key_v = ValueOrNull(*args, "name");
|
|
const auto* buf_v = ValueOrNull(*args, "png");
|
|
if (key_v == nullptr || buf_v == nullptr ||
|
|
!std::holds_alternative<std::vector<uint8_t>>(*buf_v)) {
|
|
result->Error("bad_args", "registerCursor");
|
|
return;
|
|
}
|
|
const std::string key = std::get<std::string>(*key_v);
|
|
const auto& bytes = std::get<std::vector<uint8_t>>(*buf_v);
|
|
const int hot_x = static_cast<int>(GetDouble(*args, "hotspotX", 0) + 0.5);
|
|
const int hot_y = static_cast<int>(GetDouble(*args, "hotspotY", 0) + 0.5);
|
|
|
|
HCURSOR cursor = CursorFromPng(bytes, hot_x, hot_y);
|
|
if (cursor == nullptr) {
|
|
result->Error("decode", "bad png");
|
|
return;
|
|
}
|
|
auto it = cursors_.find(key);
|
|
if (it != cursors_.end()) {
|
|
DestroyCursor(it->second);
|
|
it->second = cursor;
|
|
} else {
|
|
cursors_[key] = cursor;
|
|
}
|
|
result->Success();
|
|
} else if (method == "setCursor" && args != nullptr) {
|
|
const auto* key_v = ValueOrNull(*args, "name");
|
|
if (key_v != nullptr) {
|
|
auto it = cursors_.find(std::get<std::string>(*key_v));
|
|
if (it != cursors_.end()) {
|
|
// Make it the active cursor AND the window-class cursor so Windows
|
|
// keeps drawing it on subsequent WM_SETCURSOR (mouse moves).
|
|
SetCursor(it->second);
|
|
HWND hwnd = registrar_->GetView()
|
|
? registrar_->GetView()->GetNativeWindow()
|
|
: nullptr;
|
|
if (hwnd != nullptr) {
|
|
SetClassLongPtr(hwnd, GCLP_HCURSOR,
|
|
reinterpret_cast<LONG_PTR>(it->second));
|
|
}
|
|
} else {
|
|
// Unknown name: revert to the system default (arrow) cursor.
|
|
ResetToDefaultCursor(registrar_);
|
|
}
|
|
}
|
|
result->Success();
|
|
} else if (method == "resetCursor") {
|
|
ResetToDefaultCursor(registrar_);
|
|
result->Success();
|
|
} else if (method == "systemCursorScale") {
|
|
result->Success(EncodableValue(1.0)); // No global cursor scale on Windows
|
|
} else if (method == "registerScaleObserver") {
|
|
result->Success(); // No-op: no system scale observer on Windows
|
|
} else if (method == "systemDirection") {
|
|
result->Success(EncodableValue("ltr")); // No system RTL direction on Windows
|
|
} else if (method == "registerDirectionObserver") {
|
|
result->Success(); // No-op: no system direction observer on Windows
|
|
} else {
|
|
result->NotImplemented();
|
|
}
|
|
}
|
|
|
|
} // namespace custom_cursor
|