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.
This commit is contained in:
Campbell Barton
2025-07-18 14:07:00 +10:00
parent 4e3b58423c
commit 751cfe4a74
4 changed files with 84 additions and 18 deletions

View File

@@ -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,

View File

@@ -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`.

View File

@@ -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<uint8_t *>(&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<uint8_t *>(&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<const uint32_t *>(bitmap_src);
uint32_t *px_dst = static_cast<uint32_t *>(*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<const uint32_t *>(bitmap_src);
uint32_t *px_dst = static_cast<uint32_t *>(*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);

View File

@@ -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;