From 7c3c87abf69f78cae8cc30d6cd7eb536f4b9bd38 Mon Sep 17 00:00:00 2001 From: TheDaemoness Date: Tue, 26 May 2020 20:51:34 -0700 Subject: [PATCH] Initial implementation of proper HiDPI cursor support on Wayland. --- glfw/source-info.json | 1 + glfw/wl_cursors.c | 176 ++++++++++++++++++++++++++++++++++++++++++ glfw/wl_cursors.h | 31 ++++++++ glfw/wl_init.c | 46 +++++------ glfw/wl_platform.h | 20 +++-- glfw/wl_window.c | 57 ++++++++++---- 6 files changed, 283 insertions(+), 48 deletions(-) create mode 100644 glfw/wl_cursors.c create mode 100644 glfw/wl_cursors.h diff --git a/glfw/source-info.json b/glfw/source-info.json index a2edff35d..571f87939 100644 --- a/glfw/source-info.json +++ b/glfw/source-info.json @@ -78,6 +78,7 @@ "wl_init.c", "wl_monitor.c", "wl_window.c", + "wl_cursors.c", "posix_thread.c", "xkb_glfw.c", "dbus_glfw.c", diff --git a/glfw/wl_cursors.c b/glfw/wl_cursors.c new file mode 100644 index 000000000..7395a1b3a --- /dev/null +++ b/glfw/wl_cursors.c @@ -0,0 +1,176 @@ +// Future devs supporting whatever Wayland protocol stabilizes for cursor selection: see _themeAdd. + +#include "wl_cursors.h" + +#include "internal.h" + +#include +#include +#include +#include +#include + +typedef struct { + struct wl_cursor_theme *theme; + int px; + int refcount; +} _themeData; + +struct _wlCursorThemeManager { + size_t count; + /** Pointer to the head of an unsorted array of themes with no sentinel. + * + * The lack of sort (and thus forcing a linear search) is intentional; + * in most cases, users are likely to have 1-2 different cursor sizes loaded. + * For those cases, we get no benefit from sorting and added constant overheads. + * + * Don't change this to a flexible array member becuase that complicates growing/shrinking. + */ + _themeData *themes; +}; + +static void +_themeInit(_themeData *dest, const char *name, int px) { + dest->px = px; + dest->refcount = 1; + if(_glfw.wl.shm) { + dest->theme = wl_cursor_theme_load(name, px, _glfw.wl.shm); + if(!dest->theme) { + _glfwInputError(GLFW_PLATFORM_ERROR, + "Wayland: Unable to load cursor theme"); + } + } else { + dest->theme = NULL; + } +} + +static struct wl_cursor_theme* +_themeAdd(int px, _wlCursorThemeManager *manager) { + ++manager->count; + _themeData *temp = realloc(manager->themes, sizeof(_themeData)*manager->count); + if(!temp) { + _glfwInputError(GLFW_OUT_OF_MEMORY, + "OOM during cursor theme management."); + return NULL; + } else { + manager->themes = temp; + _themeInit(manager->themes + manager->count-1, + getenv("XCURSOR_THEME"), + px); + return manager->themes[manager->count-1].theme; + } +} + +//WARNING: No input safety checks. +static inline void _themeInc(_themeData + *theme) { + ++(theme->refcount); +} + +//WARNING: No input safety checks. +// In particular, doesn't check if theme is actually managed by the manager. +static void +_themeDec(_themeData *theme, _wlCursorThemeManager *manager) { + if(--(theme->refcount) == 0) { + wl_cursor_theme_destroy(theme->theme); + if(--(manager->count) > 0) { + const _themeData *last_theme = (manager->themes)+(manager->count); + *theme = *last_theme; + _themeData *temp = realloc(manager->themes, (manager->count)*sizeof(_themeData)); + //^ The chances of this failing are very slim, but one never knows. + if(temp) { + manager->themes = temp; + } + // We're shrinking here, so it's not catastrophic if realloc fails. + } else { + free(manager->themes); + manager->themes = NULL; + } + } +} + +static _wlCursorThemeManager _default = {0, NULL}; + +_wlCursorThemeManager* +_wlCursorThemeManagerDefault() { + return &_default; +} + +void +_wlCursorThemeManagerDestroy(_wlCursorThemeManager *manager) { + if(manager) { + for(size_t i = 0; i < manager->count; ++i) { + wl_cursor_theme_destroy(manager->themes[i].theme); + } + free(manager->themes); + } +} + +static struct wl_cursor_theme* +_wlCursorThemeManagerGet(_wlCursorThemeManager *manager, int px) { + _themeData *themedata = NULL; + for(size_t i = 0; i < manager->count; ++i) { + _themeData + *temp = manager->themes+i; + if(temp->px == px) { + themedata = temp; + break; + } + } + if(themedata != NULL) { + _themeInc(themedata); + return themedata->theme; + } else { + return _themeAdd(px, manager); + } +} + +struct wl_cursor_theme* +_wlCursorThemeManage(_wlCursorThemeManager *manager, struct wl_cursor_theme *theme, int px) { + //WARNING: Multiple returns. + if(manager == NULL) { + return NULL; + } + if(theme != NULL) { + // Search for the provided theme in the manager. + _themeData *themedata = NULL; + for(size_t i = 0; i < manager->count; ++i) { + _themeData *temp = manager->themes+i; + if(temp->theme == theme) { + themedata = temp; + break; + } + } + if(themedata != NULL) { + // Search succeeded. Check if we can avoid unnecessary operations. + if(themedata->px == px) { + return theme; + } else { + _themeDec(themedata, manager); + } + } else { + _glfwInputError(GLFW_PLATFORM_ERROR, + "Wayland internal: managed theme isn't in the provided manager"); + return theme; + //^ This is probably the sanest behavior for this situation: do nothing. + } + } + if(px > 0) { + return _wlCursorThemeManagerGet(manager, px); + } else { + return NULL; + } +} + +int +_wlCursorPxFromScale(int scale) { + const char *envStr = getenv("XCURSOR_SIZE"); + if(envStr != NULL) { + const int retval = atoi(envStr); + //^ atoi here is fine since 0 is an invalid value. + if(retval > 0 && retval <= INT_MAX/scale) { + return retval*scale; + } + } + return 32*scale; +} diff --git a/glfw/wl_cursors.h b/glfw/wl_cursors.h new file mode 100644 index 000000000..b5c0c5235 --- /dev/null +++ b/glfw/wl_cursors.h @@ -0,0 +1,31 @@ +// Declarations for a HiDPI-aware cursor theme manager. + +#include + +typedef struct _wlCursorThemeManager _wlCursorThemeManager; + +/** Returns a pointer to a wlCursorThemeManagerInstance. + * Repeatedly calling this function will return the same instance. + * + * The retrieved instance must be destroyed with _wlCursorThemeManagerDestroy. + */ +_wlCursorThemeManager* _wlCursorThemeManagerDefault(void); + +/** Set a wl_cursor_theme pointer variable to a pointer to a managed cursor theme. + * Pass the desired px as the third argument. + * Returns a pointer to a managed theme, or NULL if the desired px is 0 or an error occurs. + * + * The passed theme pointer must either be NULL or a pointer to a theme managed by the passed manager. + * The provided pointer may be invalidated if it's non-NULL. + */ +struct wl_cursor_theme* +_wlCursorThemeManage(_wlCursorThemeManager*, struct wl_cursor_theme*, int); + +/** Helper method to determine the appropriate size in pixels for a given scale. + * + * Reads XCURSOR_SIZE if it's set and is valid, else defaults to 32*scale. + */ + +void _wlCursorThemeManagerDestroy(_wlCursorThemeManager*); + +int _wlCursorPxFromScale(int); diff --git a/glfw/wl_init.c b/glfw/wl_init.c index 49136bf03..9069f4515 100644 --- a/glfw/wl_init.c +++ b/glfw/wl_init.c @@ -140,14 +140,20 @@ static void pointerHandleLeave(void* data UNUSED, _glfw.wl.cursorPreviousShape = GLFW_INVALID_CURSOR; } -static void setCursor(GLFWCursorShape shape) +static void setCursor(GLFWCursorShape shape, _GLFWwindow* window) { struct wl_buffer* buffer; struct wl_cursor* cursor; struct wl_cursor_image* image; struct wl_surface* surface = _glfw.wl.cursorSurface; + const int scale = window->wl.scale; - cursor = _glfwLoadCursor(shape); + window->wl.cursorTheme = _wlCursorThemeManage( + _glfw.wl.cursorThemeManager, + window->wl.cursorTheme, + _wlCursorPxFromScale(scale) + ); + cursor = _glfwLoadCursor(shape, window->wl.cursorTheme); if (!cursor) return; // TODO: handle animated cursors too. image = cursor->images[0]; @@ -160,8 +166,9 @@ static void setCursor(GLFWCursorShape shape) return; wl_pointer_set_cursor(_glfw.wl.pointer, _glfw.wl.pointerSerial, surface, - image->hotspot_x, - image->hotspot_y); + image->hotspot_x / scale, + image->hotspot_y / scale); + wl_surface_set_buffer_scale(surface, scale); wl_surface_attach(surface, buffer, 0, 0); wl_surface_damage(surface, 0, 0, image->width, image->height); @@ -225,7 +232,7 @@ static void pointerHandleMotion(void* data UNUSED, assert(0); } if (_glfw.wl.cursorPreviousShape != cursorShape) - setCursor(cursorShape); + setCursor(cursorShape, window); } static void pointerHandleButton(void* data UNUSED, @@ -775,26 +782,14 @@ int _glfwPlatformInit(void) if (_glfw.wl.shm) { - const char *cursorTheme = getenv("XCURSOR_THEME"), *cursorSizeStr = getenv("XCURSOR_SIZE"); - char *cursorSizeEnd; - int cursorSize = 32; - if (cursorSizeStr) - { - errno = 0; - long cursorSizeLong = strtol(cursorSizeStr, &cursorSizeEnd, 10); - if (!*cursorSizeEnd && !errno && cursorSizeLong > 0 && cursorSizeLong <= INT_MAX) - cursorSize = (int)cursorSizeLong; - } - _glfw.wl.cursorTheme = wl_cursor_theme_load(cursorTheme, cursorSize, _glfw.wl.shm); - if (!_glfw.wl.cursorTheme) - { - _glfwInputError(GLFW_PLATFORM_ERROR, - "Wayland: Unable to load default cursor theme"); - return false; - } - _glfw.wl.cursorSurface = + _glfw.wl.cursorThemeManager = _wlCursorThemeManagerDefault(); + _glfw.wl.cursorSurface = wl_compositor_create_surface(_glfw.wl.compositor); } + else + { + _glfw.wl.cursorThemeManager = NULL; + } return true; } @@ -814,8 +809,6 @@ void _glfwPlatformTerminate(void) glfw_xkb_release(&_glfw.wl.xkb); glfw_dbus_terminate(&_glfw.wl.dbus); - if (_glfw.wl.cursorTheme) - wl_cursor_theme_destroy(_glfw.wl.cursorTheme); if (_glfw.wl.cursor.handle) { _glfw_dlclose(_glfw.wl.cursor.handle); @@ -824,6 +817,9 @@ void _glfwPlatformTerminate(void) if (_glfw.wl.cursorSurface) wl_surface_destroy(_glfw.wl.cursorSurface); + if (_glfw.wl.cursorThemeManager) { + _wlCursorThemeManagerDestroy(_glfw.wl.cursorThemeManager); + } if (_glfw.wl.subcompositor) wl_subcompositor_destroy(_glfw.wl.subcompositor); if (_glfw.wl.compositor) diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index 4d17ac39e..43ec31d0e 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -76,7 +76,10 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR #define _GLFW_PLATFORM_CONTEXT_STATE #define _GLFW_PLATFORM_LIBRARY_CONTEXT_STATE -struct wl_cursor_image { +#include "wl_cursors.h" +//^ Includes + +/**struct wl_cursor_image { uint32_t width; uint32_t height; uint32_t hotspot_x; @@ -87,7 +90,7 @@ struct wl_cursor { unsigned int image_count; struct wl_cursor_image** images; char* name; -}; +};*/ typedef struct wl_cursor_theme* (* PFN_wl_cursor_theme_load)(const char*, int, struct wl_shm*); typedef void (* PFN_wl_cursor_theme_destroy)(struct wl_cursor_theme*); typedef struct wl_cursor* (* PFN_wl_cursor_theme_get_cursor)(struct wl_cursor_theme*, const char*); @@ -148,6 +151,7 @@ typedef struct _GLFWwindowWayland _GLFWcursor* currentCursor; double cursorPosX, cursorPosY; + struct wl_cursor_theme* cursorTheme; char* title; char appId[256]; @@ -236,7 +240,6 @@ typedef struct _GLFWlibraryWayland int compositorVersion; int seatVersion; - struct wl_cursor_theme* cursorTheme; struct wl_surface* cursorSurface; GLFWCursorShape cursorPreviousShape; uint32_t pointerSerial; @@ -278,6 +281,8 @@ typedef struct _GLFWlibraryWayland size_t dataOffersCounter; _GLFWWaylandDataOffer dataOffers[8]; char* primarySelectionString; + + _wlCursorThemeManager* cursorThemeManager; } _GLFWlibraryWayland; // Wayland-specific per-monitor data @@ -291,8 +296,7 @@ typedef struct _GLFWmonitorWayland int x; int y; int scale; - -} _GLFWmonitorWayland; + } _GLFWmonitorWayland; // Wayland-specific per-cursor data // @@ -303,6 +307,10 @@ typedef struct _GLFWcursorWayland int width, height; int xhot, yhot; int currentImage; + /** The scale of the cursor, or 0 if the cursor should be loaded late, or -1 if the cursor variable itself is unused. */ + int scale; + /** Cursor shape stored to allow late cursor loading in setCursorImage. */ + GLFWCursorShape shape; } _GLFWcursorWayland; @@ -310,5 +318,5 @@ void _glfwAddOutputWayland(uint32_t name, uint32_t version); void _glfwSetupWaylandDataDevice(void); void _glfwSetupWaylandPrimarySelectionDevice(void); void animateCursorImage(id_type timer_id, void *data); -struct wl_cursor* _glfwLoadCursor(GLFWCursorShape); +struct wl_cursor* _glfwLoadCursor(GLFWCursorShape, struct wl_cursor_theme*); void destroy_data_offer(_GLFWWaylandDataOffer*); diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 5ce48bc16..2c3df3297 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -73,6 +73,9 @@ static bool checkScaleChange(_GLFWwindow* window) { window->wl.scale = scale; wl_surface_set_buffer_scale(window->wl.surface, scale); + window->wl.cursorTheme = _wlCursorThemeManage(_glfw.wl.cursorThemeManager, + window->wl.cursorTheme, + _wlCursorPxFromScale(scale)); return true; } if (window->wl.monitorsCount > 0 && !window->wl.initial_scale_notified) { @@ -680,17 +683,26 @@ static bool createXdgSurface(_GLFWwindow* window) } static void -setCursorImage(_GLFWcursorWayland* cursorWayland) +setCursorImage(_GLFWwindow* window, _GLFWcursorWayland* cursorWayland) { struct wl_cursor_image* image; struct wl_buffer* buffer; struct wl_surface* surface = _glfw.wl.cursorSurface; + const int scale = window->wl.scale; - if (!cursorWayland->cursor) { + if (cursorWayland->scale < 0) { buffer = cursorWayland->buffer; toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 0); } else { + if(cursorWayland->scale != scale) { + struct wl_cursor *newCursor = _glfwLoadCursor(cursorWayland->shape, window->wl.cursorTheme); + if(newCursor != NULL) { + cursorWayland->cursor = newCursor; + } else { + _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: late cursor load failed; proceeding with existing cursor"); + } + } image = cursorWayland->cursor->images[cursorWayland->currentImage]; buffer = wl_cursor_image_get_buffer(image); if (image->delay) { @@ -711,8 +723,9 @@ setCursorImage(_GLFWcursorWayland* cursorWayland) wl_pointer_set_cursor(_glfw.wl.pointer, _glfw.wl.pointerSerial, surface, - cursorWayland->xhot, - cursorWayland->yhot); + cursorWayland->xhot / scale, + cursorWayland->yhot / scale); + wl_surface_set_buffer_scale(surface, scale); wl_surface_attach(surface, buffer, 0, 0); wl_surface_damage(surface, 0, 0, cursorWayland->width, cursorWayland->height); @@ -728,7 +741,7 @@ incrementCursorImage(_GLFWwindow* window) { cursor->wl.currentImage += 1; cursor->wl.currentImage %= cursor->wl.cursor->image_count; - setCursorImage(&cursor->wl); + setCursorImage(window, &cursor->wl); toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, cursor->wl.cursor->image_count > 1); return; } @@ -804,24 +817,24 @@ handleEvents(monotonic_t timeout) } static struct wl_cursor* -try_cursor_names(int arg_count, ...) { +try_cursor_names(struct wl_cursor_theme* theme, int arg_count, ...) { struct wl_cursor* ans = NULL; va_list ap; va_start(ap, arg_count); for (int i = 0; i < arg_count && !ans; i++) { const char *name = va_arg(ap, const char *); - ans = wl_cursor_theme_get_cursor(_glfw.wl.cursorTheme, name); + ans = wl_cursor_theme_get_cursor(theme, name); } va_end(ap); return ans; } -struct wl_cursor* _glfwLoadCursor(GLFWCursorShape shape) +struct wl_cursor* _glfwLoadCursor(GLFWCursorShape shape, struct wl_cursor_theme* theme) { static bool warnings[GLFW_INVALID_CURSOR] = {0}; #define NUMARGS(...) (sizeof((const char*[]){__VA_ARGS__})/sizeof(const char*)) #define C(name, ...) case name: { \ - ans = try_cursor_names(NUMARGS(__VA_ARGS__), __VA_ARGS__); \ + ans = try_cursor_names(theme, NUMARGS(__VA_ARGS__), __VA_ARGS__); \ if (!ans && !warnings[name]) {\ _glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Could not find standard cursor: %s", #name); \ warnings[name] = true; \ @@ -901,6 +914,7 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window, } window->wl.currentCursor = NULL; + // Don't set window->wl.cursorTheme to NULL here. window->wl.monitors = calloc(1, sizeof(_GLFWmonitor*)); window->wl.monitorsCount = 0; @@ -911,6 +925,12 @@ int _glfwPlatformCreateWindow(_GLFWwindow* window, void _glfwPlatformDestroyWindow(_GLFWwindow* window) { + if(window->wl.cursorTheme) { + _wlCursorThemeManage(_glfw.wl.cursorThemeManager, + window->wl.cursorTheme, + 0); + window->wl.cursorTheme = NULL; + } if (window == _glfw.wl.pointerFocus) { _glfw.wl.pointerFocus = NULL; @@ -929,6 +949,7 @@ void _glfwPlatformDestroyWindow(_GLFWwindow* window) window->context.destroy(window); destroyDecorations(window); + if (window->wl.xdg.decoration) zxdg_toplevel_decoration_v1_destroy(window->wl.xdg.decoration); @@ -1317,17 +1338,19 @@ int _glfwPlatformCreateCursor(_GLFWcursor* cursor, cursor->wl.height = image->height; cursor->wl.xhot = xhot; cursor->wl.yhot = yhot; + cursor->wl.scale = -1; + cursor->wl.shape = GLFW_INVALID_CURSOR; return true; } int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape) { - struct wl_cursor* standardCursor; - - standardCursor = _glfwLoadCursor(shape); - if (!standardCursor) return false; - cursor->wl.cursor = standardCursor; + // Don't actually load the cursor at this point, + // because there's not enough info to be properly HiDPI aware. + cursor->wl.cursor = NULL; cursor->wl.currentImage = 0; + cursor->wl.scale = 0; + cursor->wl.shape = shape; return true; } @@ -1470,10 +1493,10 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor) if (window->cursorMode == GLFW_CURSOR_NORMAL) { if (cursor) - setCursorImage(&cursor->wl); + setCursorImage(window, &cursor->wl); else { - defaultCursor = _glfwLoadCursor(GLFW_ARROW_CURSOR); + defaultCursor = _glfwLoadCursor(GLFW_ARROW_CURSOR, window->wl.cursorTheme); if (!defaultCursor) return; _GLFWcursorWayland cursorWayland = { defaultCursor, @@ -1482,7 +1505,7 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor) 0, 0, 0 }; - setCursorImage(&cursorWayland); + setCursorImage(window, &cursorWayland); } } else if (window->cursorMode == GLFW_CURSOR_DISABLED)