1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#ifdef SDL_VIDEO_DRIVER_WAYLAND
25
26#include "../SDL_sysvideo.h"
27#include "../SDL_video_c.h"
28
29#include "../../events/SDL_mouse_c.h"
30#include "SDL_waylandvideo.h"
31#include "../SDL_pixels_c.h"
32#include "SDL_waylandevents_c.h"
33
34#include "wayland-cursor.h"
35#include "SDL_waylandmouse.h"
36#include "SDL_waylandshmbuffer.h"
37
38#include "cursor-shape-v1-client-protocol.h"
39#include "pointer-constraints-unstable-v1-client-protocol.h"
40#include "viewporter-client-protocol.h"
41
42#include "../../SDL_hints_c.h"
43
44static SDL_Cursor *sys_cursors[SDL_HITTEST_RESIZE_LEFT + 1];
45
46static bool Wayland_SetRelativeMouseMode(bool enabled);
47
48typedef struct
49{
50 struct Wayland_SHMBuffer shmBuffer;
51 double scale;
52 struct wl_list node;
53} Wayland_ScaledCustomCursor;
54
55typedef struct
56{
57 SDL_Surface *sdl_cursor_surface;
58 int hot_x;
59 int hot_y;
60 struct wl_list scaled_cursor_cache;
61} Wayland_CustomCursor;
62
63typedef struct
64{
65 struct wl_buffer *wl_buffer;
66 Uint32 duration;
67} Wayland_SystemCursorFrame;
68
69typedef struct
70{
71 Wayland_SystemCursorFrame *frames;
72 struct wl_callback *frame_callback;
73 Uint64 last_frame_callback_time_ms;
74 Uint64 current_frame_time_ms;
75 Uint32 total_duration;
76 int num_frames;
77 int current_frame;
78 SDL_SystemCursor id;
79} Wayland_SystemCursor;
80
81struct SDL_CursorData
82{
83 union
84 {
85 Wayland_CustomCursor custom;
86 Wayland_SystemCursor system;
87 } cursor_data;
88
89 struct wl_surface *surface;
90 struct wp_viewport *viewport;
91
92 bool is_system_cursor;
93};
94
95static int dbus_cursor_size;
96static char *dbus_cursor_theme;
97
98static void Wayland_FreeCursorThemes(SDL_VideoData *vdata)
99{
100 for (int i = 0; i < vdata->num_cursor_themes; i += 1) {
101 WAYLAND_wl_cursor_theme_destroy(vdata->cursor_themes[i].theme);
102 }
103 vdata->num_cursor_themes = 0;
104 SDL_free(vdata->cursor_themes);
105 vdata->cursor_themes = NULL;
106}
107
108#ifdef SDL_USE_LIBDBUS
109
110#include "../../core/linux/SDL_dbus.h"
111
112#define CURSOR_NODE "org.freedesktop.portal.Desktop"
113#define CURSOR_PATH "/org/freedesktop/portal/desktop"
114#define CURSOR_INTERFACE "org.freedesktop.portal.Settings"
115#define CURSOR_NAMESPACE "org.gnome.desktop.interface"
116#define CURSOR_SIGNAL_NAME "SettingChanged"
117#define CURSOR_SIZE_KEY "cursor-size"
118#define CURSOR_THEME_KEY "cursor-theme"
119
120static DBusMessage *Wayland_ReadDBusProperty(SDL_DBusContext *dbus, const char *key)
121{
122 static const char *iface = "org.gnome.desktop.interface";
123
124 DBusMessage *reply = NULL;
125 DBusMessage *msg = dbus->message_new_method_call(CURSOR_NODE,
126 CURSOR_PATH,
127 CURSOR_INTERFACE,
128 "Read"); // Method
129
130 if (msg) {
131 if (dbus->message_append_args(msg, DBUS_TYPE_STRING, &iface, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID)) {
132 reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, NULL);
133 }
134 dbus->message_unref(msg);
135 }
136
137 return reply;
138}
139
140static bool Wayland_ParseDBusReply(SDL_DBusContext *dbus, DBusMessage *reply, int type, void *value)
141{
142 DBusMessageIter iter[3];
143
144 dbus->message_iter_init(reply, &iter[0]);
145 if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_VARIANT) {
146 return false;
147 }
148
149 dbus->message_iter_recurse(&iter[0], &iter[1]);
150 if (dbus->message_iter_get_arg_type(&iter[1]) != DBUS_TYPE_VARIANT) {
151 return false;
152 }
153
154 dbus->message_iter_recurse(&iter[1], &iter[2]);
155 if (dbus->message_iter_get_arg_type(&iter[2]) != type) {
156 return false;
157 }
158
159 dbus->message_iter_get_basic(&iter[2], value);
160
161 return true;
162}
163
164static DBusHandlerResult Wayland_DBusCursorMessageFilter(DBusConnection *conn, DBusMessage *msg, void *data)
165{
166 SDL_DBusContext *dbus = SDL_DBus_GetContext();
167 SDL_VideoData *vdata = (SDL_VideoData *)data;
168
169 if (dbus->message_is_signal(msg, CURSOR_INTERFACE, CURSOR_SIGNAL_NAME)) {
170 DBusMessageIter signal_iter, variant_iter;
171 const char *namespace, *key;
172
173 dbus->message_iter_init(msg, &signal_iter);
174 // Check if the parameters are what we expect
175 if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
176 goto not_our_signal;
177 }
178 dbus->message_iter_get_basic(&signal_iter, &namespace);
179 if (SDL_strcmp(CURSOR_NAMESPACE, namespace) != 0) {
180 goto not_our_signal;
181 }
182 if (!dbus->message_iter_next(&signal_iter)) {
183 goto not_our_signal;
184 }
185 if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
186 goto not_our_signal;
187 }
188 dbus->message_iter_get_basic(&signal_iter, &key);
189 if (SDL_strcmp(CURSOR_SIZE_KEY, key) == 0) {
190 int new_cursor_size;
191
192 if (!dbus->message_iter_next(&signal_iter)) {
193 goto not_our_signal;
194 }
195 if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_VARIANT) {
196 goto not_our_signal;
197 }
198 dbus->message_iter_recurse(&signal_iter, &variant_iter);
199 if (dbus->message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_INT32) {
200 goto not_our_signal;
201 }
202 dbus->message_iter_get_basic(&variant_iter, &new_cursor_size);
203
204 if (dbus_cursor_size != new_cursor_size) {
205 dbus_cursor_size = new_cursor_size;
206 SDL_SetCursor(NULL); // Force cursor update
207 }
208 } else if (SDL_strcmp(CURSOR_THEME_KEY, key) == 0) {
209 const char *new_cursor_theme = NULL;
210
211 if (!dbus->message_iter_next(&signal_iter)) {
212 goto not_our_signal;
213 }
214 if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_VARIANT) {
215 goto not_our_signal;
216 }
217 dbus->message_iter_recurse(&signal_iter, &variant_iter);
218 if (dbus->message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_STRING) {
219 goto not_our_signal;
220 }
221 dbus->message_iter_get_basic(&variant_iter, &new_cursor_theme);
222
223 if (!dbus_cursor_theme || !new_cursor_theme || SDL_strcmp(dbus_cursor_theme, new_cursor_theme) != 0) {
224 SDL_free(dbus_cursor_theme);
225 if (new_cursor_theme) {
226 dbus_cursor_theme = SDL_strdup(new_cursor_theme);
227 } else {
228 dbus_cursor_theme = NULL;
229 }
230
231 // Purge the current cached themes and force a cursor refresh.
232 Wayland_FreeCursorThemes(vdata);
233 SDL_SetCursor(NULL);
234 }
235 } else {
236 goto not_our_signal;
237 }
238
239 return DBUS_HANDLER_RESULT_HANDLED;
240 }
241
242not_our_signal:
243 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
244}
245
246static void Wayland_DBusInitCursorProperties(SDL_VideoData *vdata)
247{
248 DBusMessage *reply;
249 SDL_DBusContext *dbus = SDL_DBus_GetContext();
250 bool add_filter = false;
251
252 if (!dbus) {
253 return;
254 }
255
256 if ((reply = Wayland_ReadDBusProperty(dbus, CURSOR_SIZE_KEY))) {
257 if (Wayland_ParseDBusReply(dbus, reply, DBUS_TYPE_INT32, &dbus_cursor_size)) {
258 add_filter = true;
259 }
260 dbus->message_unref(reply);
261 }
262
263 if ((reply = Wayland_ReadDBusProperty(dbus, CURSOR_THEME_KEY))) {
264 const char *temp = NULL;
265 if (Wayland_ParseDBusReply(dbus, reply, DBUS_TYPE_STRING, &temp)) {
266 add_filter = true;
267
268 if (temp) {
269 dbus_cursor_theme = SDL_strdup(temp);
270 }
271 }
272 dbus->message_unref(reply);
273 }
274
275 // Only add the filter if at least one of the settings we want is present.
276 if (add_filter) {
277 dbus->bus_add_match(dbus->session_conn,
278 "type='signal', interface='" CURSOR_INTERFACE "',"
279 "member='" CURSOR_SIGNAL_NAME "', arg0='" CURSOR_NAMESPACE "'",
280 NULL);
281 dbus->connection_add_filter(dbus->session_conn, &Wayland_DBusCursorMessageFilter, vdata, NULL);
282 dbus->connection_flush(dbus->session_conn);
283 }
284}
285
286static void Wayland_DBusFinishCursorProperties(void)
287{
288 SDL_free(dbus_cursor_theme);
289 dbus_cursor_theme = NULL;
290}
291
292#endif
293
294static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time);
295struct wl_callback_listener cursor_frame_listener = {
296 cursor_frame_done
297};
298
299static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
300{
301 SDL_CursorData *c = (SDL_CursorData *)data;
302
303 const Uint64 now = SDL_GetTicks();
304 const Uint64 elapsed = (now - c->cursor_data.system.last_frame_callback_time_ms) % c->cursor_data.system.total_duration;
305 Uint64 advance = 0;
306 int next = c->cursor_data.system.current_frame;
307
308 wl_callback_destroy(cb);
309 c->cursor_data.system.frame_callback = wl_surface_frame(c->surface);
310 wl_callback_add_listener(c->cursor_data.system.frame_callback, &cursor_frame_listener, data);
311
312 c->cursor_data.system.current_frame_time_ms += elapsed;
313
314 // Calculate the next frame based on the elapsed duration.
315 for (Uint64 t = c->cursor_data.system.frames[next].duration; t <= c->cursor_data.system.current_frame_time_ms; t += c->cursor_data.system.frames[next].duration) {
316 next = (next + 1) % c->cursor_data.system.num_frames;
317 advance = t;
318
319 // Make sure we don't end up in an infinite loop if a cursor has frame durations of 0.
320 if (!c->cursor_data.system.frames[next].duration) {
321 break;
322 }
323 }
324
325 c->cursor_data.system.current_frame_time_ms -= advance;
326 c->cursor_data.system.last_frame_callback_time_ms = now;
327 c->cursor_data.system.current_frame = next;
328 wl_surface_attach(c->surface, c->cursor_data.system.frames[next].wl_buffer, 0, 0);
329 if (wl_surface_get_version(c->surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
330 wl_surface_damage_buffer(c->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
331 } else {
332 wl_surface_damage(c->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
333 }
334 wl_surface_commit(c->surface);
335}
336
337static bool Wayland_GetSystemCursor(SDL_VideoData *vdata, SDL_CursorData *cdata, int *scale, int *dst_size, int *hot_x, int *hot_y)
338{
339 struct wl_cursor_theme *theme = NULL;
340 struct wl_cursor *cursor;
341 const char *css_name = "default";
342 const char *fallback_name = NULL;
343 double scale_factor = 1.0;
344 int theme_size = dbus_cursor_size;
345
346 // Fallback envvar if the DBus properties don't exist
347 if (theme_size <= 0) {
348 const char *xcursor_size = SDL_getenv("XCURSOR_SIZE");
349 if (xcursor_size) {
350 theme_size = SDL_atoi(xcursor_size);
351 }
352 }
353 if (theme_size <= 0) {
354 theme_size = 24;
355 }
356 // First, find the appropriate theme based on the current scale...
357 SDL_Window *focus = SDL_GetMouse()->focus;
358 if (focus) {
359 // TODO: Use the fractional scale once GNOME supports viewports on cursor surfaces.
360 scale_factor = SDL_ceil(focus->internal->scale_factor);
361 }
362
363 const int scaled_size = (int)SDL_lround(theme_size * scale_factor);
364 for (int i = 0; i < vdata->num_cursor_themes; ++i) {
365 if (vdata->cursor_themes[i].size == scaled_size) {
366 theme = vdata->cursor_themes[i].theme;
367 break;
368 }
369 }
370 if (!theme) {
371 const char *xcursor_theme = dbus_cursor_theme;
372
373 SDL_WaylandCursorTheme *new_cursor_themes = SDL_realloc(vdata->cursor_themes,
374 sizeof(SDL_WaylandCursorTheme) * (vdata->num_cursor_themes + 1));
375 if (!new_cursor_themes) {
376 return false;
377 }
378 vdata->cursor_themes = new_cursor_themes;
379
380 // Fallback envvar if the DBus properties don't exist
381 if (!xcursor_theme) {
382 xcursor_theme = SDL_getenv("XCURSOR_THEME");
383 }
384
385 theme = WAYLAND_wl_cursor_theme_load(xcursor_theme, scaled_size, vdata->shm);
386 vdata->cursor_themes[vdata->num_cursor_themes].size = scaled_size;
387 vdata->cursor_themes[vdata->num_cursor_themes++].theme = theme;
388 }
389
390 css_name = SDL_GetCSSCursorName(cdata->cursor_data.system.id, &fallback_name);
391 cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, css_name);
392 if (!cursor && fallback_name) {
393 cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, fallback_name);
394 }
395
396 // Fallback to the default cursor if the chosen one wasn't found
397 if (!cursor) {
398 cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "default");
399 }
400 // Try the old X11 name as a last resort
401 if (!cursor) {
402 cursor = WAYLAND_wl_cursor_theme_get_cursor(theme, "left_ptr");
403 }
404 if (!cursor) {
405 return false;
406 }
407
408 if (cdata->cursor_data.system.num_frames != cursor->image_count) {
409 SDL_free(cdata->cursor_data.system.frames);
410 cdata->cursor_data.system.frames = SDL_calloc(cursor->image_count, sizeof(Wayland_SystemCursorFrame));
411 if (!cdata->cursor_data.system.frames) {
412 return false;
413 }
414 }
415
416 // ... Set the cursor data, finally.
417 cdata->cursor_data.system.num_frames = cursor->image_count;
418 cdata->cursor_data.system.total_duration = 0;
419 for (int i = 0; i < cursor->image_count; ++i) {
420 cdata->cursor_data.system.frames[i].wl_buffer = WAYLAND_wl_cursor_image_get_buffer(cursor->images[i]);
421 cdata->cursor_data.system.frames[i].duration = cursor->images[i]->delay;
422 cdata->cursor_data.system.total_duration += cursor->images[i]->delay;
423 }
424
425 *scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0;
426
427 if (scaled_size != cursor->images[0]->width) {
428 /* If the cursor size isn't an exact match for the target size, use a viewport
429 * to avoid a possible "Buffer size is not divisible by scale" protocol error.
430 *
431 * If viewports are unavailable, find an integer scale that works.
432 */
433 if (vdata->viewporter) {
434 // A scale of 0 indicates that a viewport set to the destination size should be used.
435 *scale = 0;
436 } else {
437 for (; *scale > 1; --*scale) {
438 if (cursor->images[0]->width % *scale == 0) {
439 break;
440 }
441 }
442 // Set the scale factor to the new value for the hotspot calculations.
443 scale_factor = *scale;
444 }
445 }
446
447 *dst_size = (int)SDL_lround(cursor->images[0]->width / scale_factor);
448
449 *hot_x = (int)SDL_lround(cursor->images[0]->hotspot_x / scale_factor);
450 *hot_y = (int)SDL_lround(cursor->images[0]->hotspot_y / scale_factor);
451
452 return true;
453}
454
455static Wayland_ScaledCustomCursor *Wayland_CacheScaledCustomCursor(SDL_CursorData *cdata, double scale)
456{
457 Wayland_ScaledCustomCursor *cache = NULL;
458
459 // Is this cursor already cached at the target scale?
460 if (!WAYLAND_wl_list_empty(&cdata->cursor_data.custom.scaled_cursor_cache)) {
461 Wayland_ScaledCustomCursor *c = NULL;
462 wl_list_for_each (c, &cdata->cursor_data.custom.scaled_cursor_cache, node) {
463 if (c->scale == scale) {
464 cache = c;
465 break;
466 }
467 }
468 }
469
470 if (!cache) {
471 cache = SDL_calloc(1, sizeof(Wayland_ScaledCustomCursor));
472 if (!cache) {
473 return NULL;
474 }
475
476 SDL_Surface *surface = SDL_GetSurfaceImage(cdata->cursor_data.custom.sdl_cursor_surface, (float)scale);
477 if (!surface) {
478 SDL_free(cache);
479 return NULL;
480 }
481
482 // Allocate the shared memory buffer for this cursor.
483 if (!Wayland_AllocSHMBuffer(surface->w, surface->h, &cache->shmBuffer)) {
484 SDL_free(cache);
485 SDL_DestroySurface(surface);
486 return NULL;
487 }
488
489 // Wayland requires premultiplied alpha for its surfaces.
490 SDL_PremultiplyAlpha(surface->w, surface->h,
491 surface->format, surface->pixels, surface->pitch,
492 SDL_PIXELFORMAT_ARGB8888, cache->shmBuffer.shm_data, surface->w * 4, true);
493
494 cache->scale = scale;
495 WAYLAND_wl_list_insert(&cdata->cursor_data.custom.scaled_cursor_cache, &cache->node);
496 SDL_DestroySurface(surface);
497 }
498
499 return cache;
500}
501
502static bool Wayland_GetCustomCursor(SDL_Cursor *cursor, struct wl_buffer **buffer, int *scale, int *dst_width, int *dst_height, int *hot_x, int *hot_y)
503{
504 SDL_VideoDevice *vd = SDL_GetVideoDevice();
505 SDL_VideoData *wd = vd->internal;
506 SDL_CursorData *data = cursor->internal;
507 SDL_Window *focus = SDL_GetMouseFocus();
508 double scale_factor = 1.0;
509
510 if (focus && SDL_SurfaceHasAlternateImages(data->cursor_data.custom.sdl_cursor_surface)) {
511 scale_factor = focus->internal->scale_factor;
512 }
513
514 // Only use fractional scale values if viewports are available.
515 if (!wd->viewporter) {
516 scale_factor = SDL_ceil(scale_factor);
517 }
518
519 Wayland_ScaledCustomCursor *c = Wayland_CacheScaledCustomCursor(data, scale_factor);
520 if (!c) {
521 return false;
522 }
523
524 *buffer = c->shmBuffer.wl_buffer;
525 *scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0;
526 *dst_width = data->cursor_data.custom.sdl_cursor_surface->w;
527 *dst_height = data->cursor_data.custom.sdl_cursor_surface->h;
528 *hot_x = data->cursor_data.custom.hot_x;
529 *hot_y = data->cursor_data.custom.hot_y;
530
531 return true;
532}
533
534static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
535{
536 SDL_VideoDevice *vd = SDL_GetVideoDevice();
537 SDL_VideoData *wd = vd->internal;
538
539 SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
540 if (cursor) {
541 SDL_CursorData *data = SDL_calloc(1, sizeof(*data));
542 if (!data) {
543 SDL_free(cursor);
544 return NULL;
545 }
546 cursor->internal = data;
547 WAYLAND_wl_list_init(&data->cursor_data.custom.scaled_cursor_cache);
548 data->cursor_data.custom.hot_x = hot_x;
549 data->cursor_data.custom.hot_y = hot_y;
550 data->surface = wl_compositor_create_surface(wd->compositor);
551
552 data->cursor_data.custom.sdl_cursor_surface = surface;
553 ++surface->refcount;
554
555 // If the cursor has only one size, just prepare it now.
556 if (!SDL_SurfaceHasAlternateImages(surface)) {
557 Wayland_CacheScaledCustomCursor(data, 1.0);
558 }
559 }
560
561 return cursor;
562}
563
564static SDL_Cursor *Wayland_CreateSystemCursor(SDL_SystemCursor id)
565{
566 SDL_VideoData *data = SDL_GetVideoDevice()->internal;
567 SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
568 if (cursor) {
569 SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata));
570 if (!cdata) {
571 SDL_free(cursor);
572 return NULL;
573 }
574 cursor->internal = cdata;
575
576 /* The surface is only necessary if the cursor shape manager is not present.
577 *
578 * Note that we can't actually set any other cursor properties, as this
579 * is window-specific. See Wayland_GetSystemCursor for the rest!
580 */
581 if (!data->cursor_shape_manager) {
582 cdata->surface = wl_compositor_create_surface(data->compositor);
583 wl_surface_set_user_data(cdata->surface, NULL);
584 }
585
586 cdata->cursor_data.system.id = id;
587 cdata->is_system_cursor = true;
588 }
589
590 return cursor;
591}
592
593static SDL_Cursor *Wayland_CreateDefaultCursor(void)
594{
595 SDL_SystemCursor id = SDL_GetDefaultSystemCursor();
596 return Wayland_CreateSystemCursor(id);
597}
598
599static void Wayland_FreeCursorData(SDL_CursorData *d)
600{
601 SDL_VideoDevice *vd = SDL_GetVideoDevice();
602 struct SDL_WaylandInput *input = vd->internal->input;
603
604 if (input->current_cursor == d) {
605 input->current_cursor = NULL;
606 }
607
608 // Buffers for system cursors must not be destroyed.
609 if (d->is_system_cursor) {
610 if (d->cursor_data.system.frame_callback) {
611 wl_callback_destroy(d->cursor_data.system.frame_callback);
612 }
613 SDL_free(d->cursor_data.system.frames);
614 } else {
615 Wayland_ScaledCustomCursor *c, *temp;
616 wl_list_for_each_safe(c, temp, &d->cursor_data.custom.scaled_cursor_cache, node) {
617 Wayland_ReleaseSHMBuffer(&c->shmBuffer);
618 SDL_free(c);
619 }
620
621 SDL_DestroySurface(d->cursor_data.custom.sdl_cursor_surface);
622 }
623
624 if (d->viewport) {
625 wp_viewport_destroy(d->viewport);
626 d->viewport = NULL;
627 }
628
629 if (d->surface) {
630 wl_surface_destroy(d->surface);
631 d->surface = NULL;
632 }
633}
634
635static void Wayland_FreeCursor(SDL_Cursor *cursor)
636{
637 if (!cursor) {
638 return;
639 }
640
641 // Probably not a cursor we own
642 if (!cursor->internal) {
643 return;
644 }
645
646 Wayland_FreeCursorData(cursor->internal);
647
648 SDL_free(cursor->internal);
649 SDL_free(cursor);
650}
651
652static void Wayland_SetSystemCursorShape(struct SDL_WaylandInput *input, SDL_SystemCursor id)
653{
654 Uint32 shape;
655
656 switch (id) {
657 case SDL_SYSTEM_CURSOR_DEFAULT:
658 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
659 break;
660 case SDL_SYSTEM_CURSOR_TEXT:
661 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT;
662 break;
663 case SDL_SYSTEM_CURSOR_WAIT:
664 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT;
665 break;
666 case SDL_SYSTEM_CURSOR_CROSSHAIR:
667 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR;
668 break;
669 case SDL_SYSTEM_CURSOR_PROGRESS:
670 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS;
671 break;
672 case SDL_SYSTEM_CURSOR_NWSE_RESIZE:
673 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE;
674 break;
675 case SDL_SYSTEM_CURSOR_NESW_RESIZE:
676 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE;
677 break;
678 case SDL_SYSTEM_CURSOR_EW_RESIZE:
679 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE;
680 break;
681 case SDL_SYSTEM_CURSOR_NS_RESIZE:
682 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE;
683 break;
684 case SDL_SYSTEM_CURSOR_MOVE:
685 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL;
686 break;
687 case SDL_SYSTEM_CURSOR_NOT_ALLOWED:
688 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED;
689 break;
690 case SDL_SYSTEM_CURSOR_POINTER:
691 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER;
692 break;
693 case SDL_SYSTEM_CURSOR_NW_RESIZE:
694 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE;
695 break;
696 case SDL_SYSTEM_CURSOR_N_RESIZE:
697 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE;
698 break;
699 case SDL_SYSTEM_CURSOR_NE_RESIZE:
700 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE;
701 break;
702 case SDL_SYSTEM_CURSOR_E_RESIZE:
703 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE;
704 break;
705 case SDL_SYSTEM_CURSOR_SE_RESIZE:
706 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE;
707 break;
708 case SDL_SYSTEM_CURSOR_S_RESIZE:
709 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE;
710 break;
711 case SDL_SYSTEM_CURSOR_SW_RESIZE:
712 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE;
713 break;
714 case SDL_SYSTEM_CURSOR_W_RESIZE:
715 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE;
716 break;
717 default:
718 SDL_assert(0); // Should never be here...
719 shape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
720 }
721
722 wp_cursor_shape_device_v1_set_shape(input->cursor_shape, input->pointer_enter_serial, shape);
723}
724
725static bool Wayland_ShowCursor(SDL_Cursor *cursor)
726{
727 SDL_VideoDevice *vd = SDL_GetVideoDevice();
728 SDL_VideoData *d = vd->internal;
729 struct SDL_WaylandInput *input = d->input;
730 struct wl_pointer *pointer = d->pointer;
731 struct wl_buffer *buffer = NULL;
732 int scale = 1;
733 int dst_width = 0;
734 int dst_height = 0;
735 int hot_x;
736 int hot_y;
737
738 if (!pointer) {
739 return false;
740 }
741
742 // Stop the frame callback for old animated cursors.
743 if (input->current_cursor && input->current_cursor->is_system_cursor &&
744 input->current_cursor->cursor_data.system.frame_callback) {
745 wl_callback_destroy(input->current_cursor->cursor_data.system.frame_callback);
746 input->current_cursor->cursor_data.system.frame_callback = NULL;
747 }
748
749 if (cursor) {
750 SDL_CursorData *data = cursor->internal;
751
752 if (data->is_system_cursor) {
753 // If the cursor shape protocol is supported, the compositor will draw nicely scaled cursors for us, so nothing more to do.
754 if (input->cursor_shape) {
755 Wayland_SetSystemCursorShape(input, data->cursor_data.system.id);
756 input->current_cursor = data;
757 return true;
758 }
759
760 if (!Wayland_GetSystemCursor(d, data, &scale, &dst_width, &hot_x, &hot_y)) {
761 return false;
762 }
763
764 dst_height = dst_width;
765 wl_surface_attach(data->surface, data->cursor_data.system.frames[0].wl_buffer, 0, 0);
766
767 // If more than one frame is available, create a frame callback to run the animation.
768 if (data->cursor_data.system.num_frames > 1) {
769 data->cursor_data.system.last_frame_callback_time_ms = SDL_GetTicks();
770 data->cursor_data.system.current_frame_time_ms = 0;
771 data->cursor_data.system.current_frame = 0;
772 data->cursor_data.system.frame_callback = wl_surface_frame(data->surface);
773 wl_callback_add_listener(data->cursor_data.system.frame_callback, &cursor_frame_listener, data);
774 }
775 } else {
776 if (!Wayland_GetCustomCursor(cursor, &buffer, &scale, &dst_width, &dst_height, &hot_x, &hot_y)) {
777 return false;
778 }
779
780 wl_surface_attach(data->surface, buffer, 0, 0);
781 }
782
783 // A scale value of 0 indicates that a viewport with the returned destination size should be used.
784 if (!scale) {
785 if (!data->viewport) {
786 data->viewport = wp_viewporter_get_viewport(d->viewporter, data->surface);
787 }
788 wl_surface_set_buffer_scale(data->surface, 1);
789 wp_viewport_set_source(data->viewport, wl_fixed_from_int(-1), wl_fixed_from_int(-1), wl_fixed_from_int(-1), wl_fixed_from_int(-1));
790 wp_viewport_set_destination(data->viewport, dst_width, dst_height);
791 } else {
792 if (data->viewport) {
793 wp_viewport_destroy(data->viewport);
794 data->viewport = NULL;
795 }
796 wl_surface_set_buffer_scale(data->surface, scale);
797 }
798
799 wl_pointer_set_cursor(pointer, input->pointer_enter_serial, data->surface, hot_x, hot_y);
800
801 if (wl_surface_get_version(data->surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) {
802 wl_surface_damage_buffer(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
803 } else {
804 wl_surface_damage(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
805 }
806
807 wl_surface_commit(data->surface);
808 input->current_cursor = data;
809 } else {
810 input->current_cursor = NULL;
811 wl_pointer_set_cursor(pointer, input->pointer_enter_serial, NULL, 0, 0);
812 }
813
814 return true;
815}
816
817static bool Wayland_WarpMouse(SDL_Window *window, float x, float y)
818{
819 SDL_VideoDevice *vd = SDL_GetVideoDevice();
820 SDL_VideoData *d = vd->internal;
821 SDL_WindowData *wind = window->internal;
822 struct SDL_WaylandInput *input = d->input;
823
824 if (d->pointer_constraints) {
825 const bool toggle_lock = !wind->locked_pointer;
826
827 /* The pointer confinement protocol allows setting a hint to warp the pointer,
828 * but only when the pointer is locked.
829 *
830 * Lock the pointer, set the position hint, unlock, and hope for the best.
831 */
832 if (toggle_lock) {
833 Wayland_input_lock_pointer(input, window);
834 }
835 if (wind->locked_pointer) {
836 const wl_fixed_t f_x = wl_fixed_from_double(x / wind->pointer_scale.x);
837 const wl_fixed_t f_y = wl_fixed_from_double(y / wind->pointer_scale.y);
838 zwp_locked_pointer_v1_set_cursor_position_hint(wind->locked_pointer, f_x, f_y);
839 wl_surface_commit(wind->surface);
840 }
841 if (toggle_lock) {
842 Wayland_input_unlock_pointer(input, window);
843 }
844
845 /* NOTE: There is a pending warp event under discussion that should replace this when available.
846 * https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/340
847 */
848 SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, false, x, y);
849 } else {
850 return SDL_SetError("wayland: mouse warp failed; compositor lacks support for the required zwp_pointer_confinement_v1 protocol");
851 }
852
853 return true;
854}
855
856static bool Wayland_WarpMouseGlobal(float x, float y)
857{
858 SDL_VideoDevice *vd = SDL_GetVideoDevice();
859 SDL_VideoData *d = vd->internal;
860 struct SDL_WaylandInput *input = d->input;
861 SDL_WindowData *wind = input->pointer_focus;
862
863 // If the client wants the coordinates warped to within the focused window, just convert the coordinates to relative.
864 if (wind) {
865 SDL_Window *window = wind->sdlwindow;
866 return Wayland_WarpMouse(window, x - (float)window->x, y - (float)window->y);
867 }
868
869 return SDL_SetError("wayland: can't warp the mouse when a window does not have focus");
870}
871
872static bool Wayland_SetRelativeMouseMode(bool enabled)
873{
874 SDL_VideoDevice *vd = SDL_GetVideoDevice();
875 SDL_VideoData *data = vd->internal;
876
877 if (enabled) {
878 return Wayland_input_enable_relative_pointer(data->input);
879 } else {
880 return Wayland_input_disable_relative_pointer(data->input);
881 }
882}
883
884/* Wayland doesn't support getting the true global cursor position, but it can
885 * be faked well enough for what most applications use it for: querying the
886 * global cursor coordinates and transforming them to the window-relative
887 * coordinates manually.
888 *
889 * The global position is derived by taking the cursor position relative to the
890 * toplevel window, and offsetting it by the origin of the output the window is
891 * currently considered to be on. The cursor position and button state when the
892 * cursor is outside an application window are unknown, but this gives 'correct'
893 * coordinates when the window has focus, which is good enough for most
894 * applications.
895 */
896static SDL_MouseButtonFlags SDLCALL Wayland_GetGlobalMouseState(float *x, float *y)
897{
898 SDL_Window *focus = SDL_GetMouseFocus();
899 SDL_MouseButtonFlags result = 0;
900
901 if (focus) {
902 SDL_VideoData *viddata = SDL_GetVideoDevice()->internal;
903 int off_x, off_y;
904
905 result = viddata->input->buttons_pressed;
906 SDL_RelativeToGlobalForWindow(focus, focus->x, focus->y, &off_x, &off_y);
907 *x += off_x;
908 *y += off_y;
909 }
910
911 return result;
912}
913
914#if 0 // TODO RECONNECT: See waylandvideo.c for more information!
915static void Wayland_RecreateCursor(SDL_Cursor *cursor, SDL_VideoData *vdata)
916{
917 SDL_CursorData *cdata = cursor->internal;
918
919 // Probably not a cursor we own
920 if (cdata == NULL) {
921 return;
922 }
923
924 Wayland_FreeCursorData(cdata);
925
926 // We're not currently freeing this, so... yolo?
927 if (cdata->shm_data != NULL) {
928 void *old_data_pointer = cdata->shm_data;
929 int stride = cdata->w * 4;
930
931 create_buffer_from_shm(cdata, cdata->w, cdata->h, WL_SHM_FORMAT_ARGB8888);
932
933 SDL_memcpy(cdata->shm_data, old_data_pointer, stride * cdata->h);
934 }
935 cdata->surface = wl_compositor_create_surface(vdata->compositor);
936 wl_surface_set_user_data(cdata->surface, NULL);
937}
938
939void Wayland_RecreateCursors(void)
940{
941 SDL_Cursor *cursor;
942 SDL_Mouse *mouse = SDL_GetMouse();
943 SDL_VideoData *vdata = SDL_GetVideoDevice()->internal;
944
945 if (vdata && vdata->cursor_themes) {
946 SDL_free(vdata->cursor_themes);
947 vdata->cursor_themes = NULL;
948 vdata->num_cursor_themes = 0;
949 }
950
951 if (mouse == NULL) {
952 return;
953 }
954
955 for (cursor = mouse->cursors; cursor != NULL; cursor = cursor->next) {
956 Wayland_RecreateCursor(cursor, vdata);
957 }
958 if (mouse->def_cursor) {
959 Wayland_RecreateCursor(mouse->def_cursor, vdata);
960 }
961 if (mouse->cur_cursor) {
962 Wayland_RecreateCursor(mouse->cur_cursor, vdata);
963 if (mouse->cursor_shown) {
964 Wayland_ShowCursor(mouse->cur_cursor);
965 }
966 }
967}
968#endif // 0
969
970void Wayland_InitMouse(void)
971{
972 SDL_Mouse *mouse = SDL_GetMouse();
973 SDL_VideoDevice *vd = SDL_GetVideoDevice();
974 SDL_VideoData *d = vd->internal;
975
976 mouse->CreateCursor = Wayland_CreateCursor;
977 mouse->CreateSystemCursor = Wayland_CreateSystemCursor;
978 mouse->ShowCursor = Wayland_ShowCursor;
979 mouse->FreeCursor = Wayland_FreeCursor;
980 mouse->WarpMouse = Wayland_WarpMouse;
981 mouse->WarpMouseGlobal = Wayland_WarpMouseGlobal;
982 mouse->SetRelativeMouseMode = Wayland_SetRelativeMouseMode;
983 mouse->GetGlobalMouseState = Wayland_GetGlobalMouseState;
984
985 SDL_HitTestResult r = SDL_HITTEST_NORMAL;
986 while (r <= SDL_HITTEST_RESIZE_LEFT) {
987 switch (r) {
988 case SDL_HITTEST_NORMAL:
989 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
990 break;
991 case SDL_HITTEST_DRAGGABLE:
992 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
993 break;
994 case SDL_HITTEST_RESIZE_TOPLEFT:
995 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_NW_RESIZE);
996 break;
997 case SDL_HITTEST_RESIZE_TOP:
998 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_N_RESIZE);
999 break;
1000 case SDL_HITTEST_RESIZE_TOPRIGHT:
1001 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_NE_RESIZE);
1002 break;
1003 case SDL_HITTEST_RESIZE_RIGHT:
1004 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_E_RESIZE);
1005 break;
1006 case SDL_HITTEST_RESIZE_BOTTOMRIGHT:
1007 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_SE_RESIZE);
1008 break;
1009 case SDL_HITTEST_RESIZE_BOTTOM:
1010 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_S_RESIZE);
1011 break;
1012 case SDL_HITTEST_RESIZE_BOTTOMLEFT:
1013 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_SW_RESIZE);
1014 break;
1015 case SDL_HITTEST_RESIZE_LEFT:
1016 sys_cursors[r] = Wayland_CreateSystemCursor(SDL_SYSTEM_CURSOR_W_RESIZE);
1017 break;
1018 }
1019 r++;
1020 }
1021
1022#ifdef SDL_USE_LIBDBUS
1023 /* The DBus cursor properties are only needed when manually loading themes and cursors.
1024 * If the cursor shape protocol is present, the compositor will handle it internally.
1025 */
1026 if (!d->cursor_shape_manager) {
1027 Wayland_DBusInitCursorProperties(d);
1028 }
1029#endif
1030
1031 SDL_SetDefaultCursor(Wayland_CreateDefaultCursor());
1032}
1033
1034void Wayland_FiniMouse(SDL_VideoData *data)
1035{
1036 Wayland_FreeCursorThemes(data);
1037
1038#ifdef SDL_USE_LIBDBUS
1039 Wayland_DBusFinishCursorProperties();
1040#endif
1041
1042 for (int i = 0; i < SDL_arraysize(sys_cursors); i++) {
1043 Wayland_FreeCursor(sys_cursors[i]);
1044 sys_cursors[i] = NULL;
1045 }
1046}
1047
1048void Wayland_SetHitTestCursor(SDL_HitTestResult rc)
1049{
1050 if (rc == SDL_HITTEST_NORMAL || rc == SDL_HITTEST_DRAGGABLE) {
1051 SDL_SetCursor(NULL);
1052 } else {
1053 Wayland_ShowCursor(sys_cursors[rc]);
1054 }
1055}
1056
1057#endif // SDL_VIDEO_DRIVER_WAYLAND
1058