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

232 lines
8.4 KiB
C++

// Copyright (c) 2026 Hans Kokx.
// SPDX-License-Identifier: MIT
#include "include/custom_cursor/custom_cursor_plugin.h"
#include <flutter_linux/flutter_linux.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>
#include <cstring>
#include <string>
#include <unordered_map>
#include <cmath>
struct _CustomCursorPlugin {
GObject parent_instance;
FlPluginRegistrar* registrar;
std::unordered_map<std::string, GdkCursor*>* 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<double>(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<int>(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<std::string, GdkCursor*>();
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);
}