// 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 #include #include #include #include #include #include #include #include #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(*v)) return std::get(*v); if (std::holds_alternative(*v)) return std::get(*v); if (std::holds_alternative(*v)) { return static_cast(std::get(*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(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& 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(bitmap.GetWidth())); int height = ClampSize(static_cast(bitmap.GetHeight())); HBITMAP mask = CreateBitmap(width, height, 1, 1, nullptr); ICONINFO info = {}; info.fIcon = FALSE; // FALSE = cursor (uses hotspot) info.xHotspot = static_cast(hot_x); info.yHotspot = static_cast(hot_y); info.hbmMask = mask; info.hbmColor = hbitmap; cursor = reinterpret_cast(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>( registrar->messenger(), "custom_cursor", &flutter::StandardMethodCodec::GetInstance()); auto plugin = std::make_unique(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& method_call, std::unique_ptr> result) { const auto* args = std::get_if(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>(*buf_v)) { result->Error("bad_args", "registerCursor"); return; } const std::string key = std::get(*key_v); const auto& bytes = std::get>(*buf_v); const int hot_x = static_cast(GetDouble(*args, "hotspotX", 0) + 0.5); const int hot_y = static_cast(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(*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(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