From 751cfe4a74fbb74a24da5ba3813cb4d9e6633d95 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Fri, 18 Jul 2025 14:07:00 +1000 Subject: [PATCH] GHOST/Wayland: invert cursor color, pre-multiply alpha - Cursors in wayland are expected to use pre-multiplied alpha. Note that cursor generators create straight alpha, pre multiplier in the wayland backend. - Invert cursor colors on Wayland since dark cursors are often default, this could use dark theme settings in the future. --- intern/ghost/GHOST_C-api.h | 2 +- intern/ghost/GHOST_Types.h | 8 +- intern/ghost/intern/GHOST_SystemWayland.cc | 73 ++++++++++++++++--- .../windowmanager/intern/wm_cursors.cc | 19 ++++- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/intern/ghost/GHOST_C-api.h b/intern/ghost/GHOST_C-api.h index 3f10222b2d5..75354fe6248 100644 --- a/intern/ghost/GHOST_C-api.h +++ b/intern/ghost/GHOST_C-api.h @@ -334,7 +334,7 @@ extern GHOST_TSuccess GHOST_HasCursorShape(GHOST_WindowHandle windowhandle, * \param mask: The mask for 1bpp cursor, nullptr if RGBA cursor. * \param size: The width & height of the cursor. * \param hot_spot: The X,Y coordinates of the cursor hot-spot. - * \param can_invert_color: Let macOS invert cursor color to match platform convention. + * \param can_invert_color: Let the cursor colors be inverted to match platform convention. * \return Indication of success. */ extern GHOST_TSuccess GHOST_SetCustomCursorShape(GHOST_WindowHandle windowhandle, diff --git a/intern/ghost/GHOST_Types.h b/intern/ghost/GHOST_Types.h index 1ea35d6fced..92e08692097 100644 --- a/intern/ghost/GHOST_Types.h +++ b/intern/ghost/GHOST_Types.h @@ -86,15 +86,19 @@ typedef struct GHOST_CursorGenerator { * The generator must guarantee the resulting size (dimensions written to `r_bitmap_size`) * never exceeds `cursor_size_max`. * \param r_hot_spot: The cursor hot-spot. + * \param r_can_invert_color: When true, the call it can be inverted too much dark themes. + * * \return the bitmap data or null if it could not be generated. - * Must be at least: `sizeof(uint8_t[4]) * r_bitmap_size[0] * r_bitmap_size[1]` allocated bytes. + * - The color is "straight" (alpha is not pre-multiplied). + * - At least: `sizeof(uint8_t[4]) * r_bitmap_size[0] * r_bitmap_size[1]` allocated bytes. */ uint8_t *(*generate_fn)(const struct GHOST_CursorGenerator *cursor_generator, int cursor_size, int cursor_size_max, uint8_t *(*alloc_fn)(size_t size), int r_bitmap_size[2], - int r_hot_spot[2]); + int r_hot_spot[2], + bool *r_can_invert_color); /** * Called once GHOST has finished with this object, * Typically this would free `user_data`. diff --git a/intern/ghost/intern/GHOST_SystemWayland.cc b/intern/ghost/intern/GHOST_SystemWayland.cc index bef9b97c6f3..37924b4aafe 100644 --- a/intern/ghost/intern/GHOST_SystemWayland.cc +++ b/intern/ghost/intern/GHOST_SystemWayland.cc @@ -481,6 +481,10 @@ struct GWL_Cursor { * See #update_cursor_scale. */ int theme_size = 0; + /** + * Prefer dark theme cursors where possible. + */ + bool use_dark_theme = false; }; /** \} */ @@ -1913,6 +1917,26 @@ static uint32_t round_up_uint(const uint32_t x, const uint32_t multiple) return ((x + multiple - 1) / multiple) * multiple; } +static uint32_t rgba_straight_to_premul(uint32_t rgba_uint) +{ + uint8_t *rgba = reinterpret_cast(&rgba_uint); + const uint32_t alpha = uint32_t(rgba[3]); + rgba[0] = uint8_t(((alpha * rgba[0]) + (0xff / 2)) / 0xff); + rgba[1] = uint8_t(((alpha * rgba[1]) + (0xff / 2)) / 0xff); + rgba[2] = uint8_t(((alpha * rgba[2]) + (0xff / 2)) / 0xff); + return rgba_uint; +} + +static uint32_t rgba_straight_to_premul_inverted(uint32_t rgba_uint) +{ + uint8_t *rgba = reinterpret_cast(&rgba_uint); + const uint32_t alpha = uint32_t(rgba[3]); + rgba[0] = uint8_t(((alpha * (0xff - rgba[0])) + (0xff / 2)) / 0xff); + rgba[1] = uint8_t(((alpha * (0xff - rgba[1])) + (0xff / 2)) / 0xff); + rgba[2] = uint8_t(((alpha * (0xff - rgba[2])) + (0xff / 2)) / 0xff); + return rgba_uint; +} + #ifdef WITH_GHOST_WAYLAND_LIBDECOR static const char *strchr_or_end(const char *str, const char ch) { @@ -6066,6 +6090,10 @@ static void gwl_seat_capability_pointer_enable(GWL_Seat *seat) seat->cursor.theme_size = int(value); } } + + /* TODO: detect this from the system. + * We *could* have weak support based on checking for known themes. */ + seat->cursor.use_dark_theme = true; } } @@ -8622,6 +8650,7 @@ static wl_buffer *ghost_wl_buffer_from_cursor_generator(const GHOST_CursorGenera size_t *buffer_data_size_p, const int cursor_size, const int cursor_size_max, + const int use_dark_theme, const int scale, int r_bitmap_size[2], int r_hot_spot[2]) @@ -8634,6 +8663,7 @@ static wl_buffer *ghost_wl_buffer_from_cursor_generator(const GHOST_CursorGenera int bitmap_size_src[2]; int hot_spot[2]; + bool can_invert_color = false; uint8_t *bitmap_src = cg.generate_fn( &cg, @@ -8641,12 +8671,15 @@ static wl_buffer *ghost_wl_buffer_from_cursor_generator(const GHOST_CursorGenera cursor_size_max, [](size_t size) -> uint8_t * { return new uint8_t[size]; }, bitmap_size_src, - hot_spot); + hot_spot, + &can_invert_color); if (bitmap_src == nullptr) { return nullptr; } + const bool invert_color = can_invert_color && use_dark_theme; + /* There is no need to adjust the hot-spot when resizing. */ int bitmap_size_dst[2] = { int(round_up_uint(bitmap_size_src[0], scale)), @@ -8662,12 +8695,21 @@ static wl_buffer *ghost_wl_buffer_from_cursor_generator(const GHOST_CursorGenera /* NOTE: the copy could be skipped in trivial cases. * Since it's such a small amount of data it hardly seems worth it. */ if (is_trivial_copy) { - /* RGBA color, direct copy. */ + /* RGBA color. */ const uint32_t *px_src = reinterpret_cast(bitmap_src); uint32_t *px_dst = static_cast(*buffer_data_p); - for (int y = 0; y < bitmap_size_src[1]; y++) { - for (int x = 0; x < bitmap_size_src[0]; x++) { - *px_dst++ = *px_src++; + if (invert_color) { + for (int y = 0; y < bitmap_size_src[1]; y++) { + for (int x = 0; x < bitmap_size_src[0]; x++) { + *px_dst++ = rgba_straight_to_premul_inverted(*px_src++); + } + } + } + else { + for (int y = 0; y < bitmap_size_src[1]; y++) { + for (int x = 0; x < bitmap_size_src[0]; x++) { + *px_dst++ = rgba_straight_to_premul(*px_src++); + } } } } @@ -8675,13 +8717,21 @@ static wl_buffer *ghost_wl_buffer_from_cursor_generator(const GHOST_CursorGenera /* RGBA color, copy into an expanded buffer. */ const uint32_t *px_src = reinterpret_cast(bitmap_src); uint32_t *px_dst = static_cast(*buffer_data_p); - for (int y = 0; y < bitmap_size_dst[1]; y++) { - for (int x = 0; x < bitmap_size_dst[0]; x++) { - if (x >= bitmap_size_src[0] || y >= bitmap_size_src[1]) { - *px_dst++ = 0x0; + if (invert_color) { + for (int y = 0; y < bitmap_size_dst[1]; y++) { + for (int x = 0; x < bitmap_size_dst[0]; x++) { + *px_dst++ = (x >= bitmap_size_src[0] || y >= bitmap_size_src[1]) ? + 0x0 : + rgba_straight_to_premul_inverted(*px_src++); } - else { - *px_dst++ = *px_src++; + } + } + else { + for (int y = 0; y < bitmap_size_dst[1]; y++) { + for (int x = 0; x < bitmap_size_dst[0]; x++) { + *px_dst++ = (x >= bitmap_size_src[0] || y >= bitmap_size_src[1]) ? + 0x0 : + rgba_straight_to_premul(*px_src++); } } } @@ -8744,6 +8794,7 @@ GHOST_TSuccess GHOST_SystemWayland::cursor_shape_custom_set(const GHOST_CursorGe &cursor.custom_data_size, cursor_size, cursor_size_max, + cursor.use_dark_theme, scale, bitmap_size, hot_spot); diff --git a/source/blender/windowmanager/intern/wm_cursors.cc b/source/blender/windowmanager/intern/wm_cursors.cc index 6d0496a1e72..982c77ca36c 100644 --- a/source/blender/windowmanager/intern/wm_cursors.cc +++ b/source/blender/windowmanager/intern/wm_cursors.cc @@ -63,6 +63,9 @@ struct BCursor { * A factor (0-1) from the top-left corner of the image (not of the document size). */ blender::float2 hotspot; + /** + * By default cursors are "light", allow dark themes to invert. + */ bool can_invert; }; @@ -172,7 +175,7 @@ static int wm_cursor_size(const wmWindow *win) } /** - * Flip and RGBA byte buffer in-place. + * Flip an RGBA byte buffer in-place. */ static void cursor_bitmap_rgba_flip_y(uint8_t *buffer, const size_t size[2]) { @@ -286,7 +289,8 @@ static bool window_set_custom_cursor_generator(wmWindow *win, const BCursor &cur const int cursor_size_max, uint8_t *(*alloc_fn)(size_t size), int r_bitmap_size[2], - int r_hot_spot[2]) -> uint8_t * { + int r_hot_spot[2], + bool *r_can_invert_color) -> uint8_t * { const BCursor &cursor = *(const BCursor *)(cursor_generator->user_data); /* Currently SVG uses the `cursor_size` as the maximum. */ UNUSED_VARS(cursor_size_max); @@ -305,6 +309,8 @@ static bool window_set_custom_cursor_generator(wmWindow *win, const BCursor &cur r_hot_spot[0] = int(cursor.hotspot[0] * (bitmap_size[0] - 1)); r_hot_spot[1] = int(cursor.hotspot[1] * (bitmap_size[1] - 1)); + *r_can_invert_color = cursor.can_invert; + return bitmap_rgba; }; @@ -805,7 +811,8 @@ static bool wm_cursor_text_generator(wmWindow *win, const std::string &text, int const int cursor_size_max, uint8_t *(*alloc_fn)(size_t size), int r_bitmap_size[2], - int r_hot_spot[2]) -> uint8_t * { + int r_hot_spot[2], + bool *r_can_invert_color) -> uint8_t * { const WMCursorText &cursor_text = *(const WMCursorText *)(cursor_generator->user_data); int bitmap_size[2]; @@ -826,6 +833,9 @@ static bool wm_cursor_text_generator(wmWindow *win, const std::string &text, int r_hot_spot[0] = bitmap_size[0] / 2; r_hot_spot[1] = bitmap_size[1] / 2; + /* Always use a dark background, not optional. */ + *r_can_invert_color = false; + return bitmap_rgba; }; @@ -877,7 +887,8 @@ static bool wm_cursor_text_pixmap(wmWindow *win, const std::string &text, int fo nullptr, bitmap_size, hot_spot, - true); + /* Always use a black background. */ + false); MEM_freeN(bitmap_rgba); return (success == GHOST_kSuccess) ? true : false;