From eeff9eb121884daa777de2561f3b63e962ec2469 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 11 Sep 2026 16:02:52 +0800 Subject: [PATCH] feat(linux): measure the active system cursor size --- flutter/linux/CMakeLists.txt | 2 + flutter/linux/cursor_size.h | 106 ++++++++++++++++++++++++++++++++ flutter/linux/my_application.cc | 3 + 3 files changed, 111 insertions(+) create mode 100644 flutter/linux/cursor_size.h diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 56a8dbb70..ed12b41b2 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -53,6 +53,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(XCURSOR REQUIRED IMPORTED_TARGET xcursor) # Wayland protocol for keyboard shortcuts inhibit pkg_check_modules(WAYLAND_CLIENT IMPORTED_TARGET wayland-client) @@ -126,6 +127,7 @@ apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::XCURSOR) target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) diff --git a/flutter/linux/cursor_size.h b/flutter/linux/cursor_size.h new file mode 100644 index 000000000..b3c676628 --- /dev/null +++ b/flutter/linux/cursor_size.h @@ -0,0 +1,106 @@ +#ifndef RUSTDESK_CURSOR_SIZE_H_ +#define RUSTDESK_CURSOR_SIZE_H_ + +#include +#include +#ifdef GDK_WINDOWING_WAYLAND +#include +#endif + +#include +#include +#include + +namespace cursor_size { + +template +double VisibleSize(int width, int height, IsVisible visible) { + int left = width, top = height, right = -1, bottom = -1; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + if (!visible(x, y)) continue; + left = std::min(left, x); + right = std::max(right, x); + top = std::min(top, y); + bottom = std::max(bottom, y); + } + } + if (right < left) throw std::runtime_error("System cursor has no visible pixels"); + return std::max(right - left + 1, bottom - top + 1); +} + +inline double WaylandSize(GtkWidget* view) { + g_autofree gchar* theme = nullptr; + gint size = 0; + g_object_get(gtk_widget_get_settings(view), "gtk-cursor-theme-name", &theme, + "gtk-cursor-theme-size", &size, nullptr); + // GTK uses 24 when the theme size is unset, and loads at the window scale. + constexpr int kDefaultThemeSize = 24; + if (size == 0) size = kDefaultThemeSize; + const int scale = gtk_widget_get_scale_factor(view); + if (size < 0 || scale <= 0 || size > G_MAXINT / scale) { + throw std::runtime_error("Invalid GTK cursor theme size or window scale"); + } + auto* loaded = XcursorLibraryLoadImage("default", theme, size * scale); + // Match GTK's CSS default -> traditional left_ptr name mapping. + if (!loaded) loaded = XcursorLibraryLoadImage("left_ptr", theme, size * scale); + std::unique_ptr image( + loaded, XcursorImageDestroy); + if (!image) throw std::runtime_error("Could not load the GTK system cursor"); + // GTK reduces the buffer scale until both theme dimensions are divisible. + // For example, a 64px theme at 3x is displayed using a buffer scale of 2. + int cursor_scale = scale; + while (image->width % cursor_scale != 0 || image->height % cursor_scale != 0) { + --cursor_scale; + } + constexpr XcursorPixel kAlphaMask = 0xff000000; + return VisibleSize(image->width, image->height, [&](int x, int y) { + return (image->pixels[y * image->width + x] & kAlphaMask) != 0; + }) / cursor_scale; +} + +inline double SystemSize(GtkWidget* view) { + GdkDisplay* display = gtk_widget_get_display(view); +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY(display)) return WaylandSize(view); +#endif + g_autoptr(GdkCursor) cursor = gdk_cursor_new_from_name(display, "default"); + if (!cursor) throw std::runtime_error("Could not load the GDK system cursor"); + g_autoptr(GdkPixbuf) image = gdk_cursor_get_image(cursor); + if (!image) throw std::runtime_error("Could not read the GDK system cursor"); + const auto* pixels = gdk_pixbuf_read_pixels(image); + const int stride = gdk_pixbuf_get_rowstride(image); + const int channels = gdk_pixbuf_get_n_channels(image); + return VisibleSize(gdk_pixbuf_get_width(image), gdk_pixbuf_get_height(image), + [&](int x, int y) { + return !gdk_pixbuf_get_has_alpha(image) || + pixels[y * stride + x * channels + channels - 1] != 0; + }); +} + +inline void Register(FlView* view) { + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + FlMethodChannel* channel = fl_method_channel_new( + fl_engine_get_binary_messenger(fl_view_get_engine(view)), + "org.rustdesk.rustdesk/cursor", FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(channel, + [](FlMethodChannel*, FlMethodCall* call, gpointer data) { + g_autoptr(GError) error = nullptr; + if (g_strcmp0(fl_method_call_get_name(call), "getSystemCursorSize") != 0) { + fl_method_call_respond_not_implemented(call, &error); + } else { + try { + g_autoptr(FlValue) value = fl_value_new_float(SystemSize(GTK_WIDGET(data))); + fl_method_call_respond_success(call, value, &error); + } catch (const std::exception& failure) { + fl_method_call_respond_error(call, "cursor_size", failure.what(), nullptr, &error); + } + } + if (error) g_warning("Cursor size response failed: %s", error->message); + }, view, nullptr); + g_object_set_data_full(G_OBJECT(view), "cursor-size-channel", channel, g_object_unref); +} + +} // namespace cursor_size + +#endif // RUSTDESK_CURSOR_SIZE_H_ diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 210adba96..292f94c9a 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -1,6 +1,7 @@ #include "my_application.h" #include "bump_mouse.h" +#include "cursor_size.h" #include #ifdef GDK_WINDOWING_X11 @@ -94,6 +95,7 @@ static void on_subwindow_created(FlPluginRegistry* registry) { // Set up side button forwarding for sub-windows. if (registry == NULL || !FL_IS_VIEW(registry)) return; FlView* view = FL_VIEW(registry); + cursor_size::Register(view); GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view)); if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) { FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view)); @@ -178,6 +180,7 @@ static void my_application_activate(GApplication* application) { (WindowCreatedCallback)on_subwindow_created); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + cursor_size::Register(view); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); self->host_channel = fl_method_channel_new(