Files
custom_cursor/windows/custom_cursor_plugin.cpp
hans 835183a0c3 Initial commit
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
2026-09-01 15:04:28 +02:00

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