// Copyright (c) 2026 Hans Kokx. // SPDX-License-Identifier: MIT #include "include/custom_cursor/custom_cursor_plugin.h" #include #include #include #include #include #include #include struct _CustomCursorPlugin { GObject parent_instance; FlPluginRegistrar* registrar; std::unordered_map* cache; }; G_DEFINE_TYPE(CustomCursorPlugin, custom_cursor_plugin, g_object_get_type()) static GdkWindow* get_gdk_window(CustomCursorPlugin* self) { if (self->registrar == nullptr) return nullptr; FlView* view = fl_plugin_registrar_get_view(self->registrar); if (view == nullptr) return nullptr; GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view)); return gtk_widget_get_window(toplevel); } static double value_to_double(FlValue* v, double fallback) { if (v == nullptr) return fallback; switch (fl_value_get_type(v)) { case FL_VALUE_TYPE_INT: return static_cast(fl_value_get_int(v)); case FL_VALUE_TYPE_FLOAT: return fl_value_get_float(v); default: return fallback; } } static int clamp_size(int size) { return size < 1 ? 1 : (size > 128 ? 128 : size); } static FlMethodResponse* register_cursor(CustomCursorPlugin* self, FlValue* args) { FlValue* key_v = fl_value_lookup_string(args, "name"); FlValue* buf_v = fl_value_lookup_string(args, "png"); FlValue* hot_x_v = fl_value_lookup_string(args, "hotspotX"); FlValue* hot_y_v = fl_value_lookup_string(args, "hotspotY"); if (key_v == nullptr || buf_v == nullptr) { return FL_METHOD_RESPONSE( fl_method_error_response_new("bad_args", "registerCursor", nullptr)); } std::string key = fl_value_get_string(key_v); const uint8_t* bytes = fl_value_get_uint8_list(buf_v); size_t length = fl_value_get_length(buf_v); double dpr = value_to_double(fl_value_lookup_string(args, "devicePixelRatio"), 1.0); if (dpr <= 0) dpr = 1.0; g_autoptr(GdkPixbufLoader) loader = gdk_pixbuf_loader_new(); if (gdk_pixbuf_loader_write(loader, bytes, length, nullptr) == FALSE) { return FL_METHOD_RESPONSE( fl_method_error_response_new("decode", "bad png", nullptr)); } if (gdk_pixbuf_loader_close(loader, nullptr) == FALSE) { return FL_METHOD_RESPONSE( fl_method_error_response_new("decode", "bad png", nullptr)); } GdkPixbuf* raw = gdk_pixbuf_loader_get_pixbuf(loader); if (raw == nullptr) { return FL_METHOD_RESPONSE( fl_method_error_response_new("decode", "bad png", nullptr)); } GdkPixbuf* pixbuf = gdk_pixbuf_copy(raw); int width = clamp_size(gdk_pixbuf_get_width(pixbuf)); int height = clamp_size(gdk_pixbuf_get_height(pixbuf)); if (width != gdk_pixbuf_get_width(pixbuf) || height != gdk_pixbuf_get_height(pixbuf)) { g_autoptr(GdkPixbuf) scaled = gdk_pixbuf_scale_simple(pixbuf, width, height, GDK_INTERP_BILINEAR); if (scaled != nullptr) { g_object_unref(pixbuf); pixbuf = g_object_ref(scaled); } } GdkDisplay* display = gdk_display_get_default(); double hot_x = value_to_double(hot_x_v, 0) / dpr; double hot_y = value_to_double(hot_y_v, 0) / dpr; cairo_surface_t* surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, static_cast(dpr + 0.5), nullptr); GdkCursor* cursor = gdk_cursor_new_from_surface(display, surface, hot_x, hot_y); cairo_surface_destroy(surface); g_object_unref(pixbuf); auto it = self->cache->find(key); if (it != self->cache->end()) { g_object_unref(it->second); it->second = cursor; } else { self->cache->insert(std::make_pair(key, cursor)); } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } static void reset_to_default_cursor(CustomCursorPlugin* self) { GdkWindow* window = get_gdk_window(self); if (window != nullptr) gdk_window_set_cursor(window, nullptr); } static FlMethodResponse* set_cursor(CustomCursorPlugin* self, FlValue* args) { FlValue* key_v = fl_value_lookup_string(args, "name"); if (key_v != nullptr) { std::string key = fl_value_get_string(key_v); auto it = self->cache->find(key); GdkWindow* window = get_gdk_window(self); if (it != self->cache->end() && window != nullptr) { gdk_window_set_cursor(window, it->second); } else if (it == self->cache->end()) { // Unknown name: revert to the system default cursor. reset_to_default_cursor(self); } } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } static FlMethodResponse* system_cursor_scale(CustomCursorPlugin* self, FlValue* args) { return FL_METHOD_RESPONSE(fl_method_success_response_new( fl_value_new_float(1.0))); } static FlMethodResponse* system_direction(CustomCursorPlugin* self, FlValue* args) { return FL_METHOD_RESPONSE(fl_method_success_response_new( fl_value_new_string("ltr"))); } static void custom_cursor_plugin_handle_method_call( CustomCursorPlugin* self, FlMethodCall* method_call) { const gchar* method = fl_method_call_get_name(method_call); FlValue* args = fl_method_call_get_args(method_call); if (strcmp(method, "registerCursor") == 0) { auto* response = register_cursor(self, args); fl_method_call_respond(method_call, FL_METHOD_RESPONSE(response), nullptr); g_object_unref(response); } else if (strcmp(method, "setCursor") == 0) { auto* response = set_cursor(self, args); fl_method_call_respond(method_call, FL_METHOD_RESPONSE(response), nullptr); g_object_unref(response); } else if (strcmp(method, "systemCursorScale") == 0) { auto* response = system_cursor_scale(self, args); fl_method_call_respond(method_call, FL_METHOD_RESPONSE(response), nullptr); g_object_unref(response); } else if (strcmp(method, "registerScaleObserver") == 0) { fl_method_call_respond( method_call, FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)), nullptr); } else if (strcmp(method, "systemDirection") == 0) { auto* response = system_direction(self, args); fl_method_call_respond(method_call, FL_METHOD_RESPONSE(response), nullptr); g_object_unref(response); } else if (strcmp(method, "registerDirectionObserver") == 0) { fl_method_call_respond( method_call, FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)), nullptr); } else if (strcmp(method, "resetCursor") == 0) { reset_to_default_cursor(self); fl_method_call_respond( method_call, FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)), nullptr); } else { fl_method_call_respond( method_call, FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()), nullptr); } } static void custom_cursor_plugin_dispose(GObject* object) { CustomCursorPlugin* self = CUSTOM_CURSOR_PLUGIN(object); if (self->cache != nullptr) { for (auto& entry : *self->cache) { g_object_unref(entry.second); } delete self->cache; self->cache = nullptr; } if (self->registrar != nullptr) { g_clear_object(&self->registrar); } G_OBJECT_CLASS(custom_cursor_plugin_parent_class)->dispose(object); } static void custom_cursor_plugin_class_init( CustomCursorPluginClass* klass) { G_OBJECT_CLASS(klass)->dispose = custom_cursor_plugin_dispose; } static void custom_cursor_plugin_init(CustomCursorPlugin* self) { self->cache = new std::unordered_map(); self->registrar = nullptr; } static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { CustomCursorPlugin* plugin = CUSTOM_CURSOR_PLUGIN(user_data); custom_cursor_plugin_handle_method_call(plugin, method_call); } void custom_cursor_plugin_register_with_registrar( FlPluginRegistrar* registrar) { CustomCursorPlugin* plugin = CUSTOM_CURSOR_PLUGIN( g_object_new(custom_cursor_plugin_get_type(), nullptr)); plugin->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); g_autoptr(FlMethodChannel) channel = fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), "custom_cursor", FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler( channel, method_call_cb, g_object_ref(plugin), g_object_unref); g_object_unref(plugin); }