835183a0c3
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
232 lines
8.4 KiB
C++
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);
|
|
}
|