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#include "SDL_internal.h"
22
23#ifdef SDL_VIDEO_DRIVER_X11
24
25#include <X11/cursorfont.h>
26#include "SDL_x11video.h"
27#include "SDL_x11mouse.h"
28#include "SDL_x11xinput2.h"
29#include "SDL_x11xtest.h"
30#include "../SDL_video_c.h"
31#include "../../events/SDL_mouse_c.h"
32
33struct SDL_CursorData
34{
35 Cursor cursor;
36};
37
38// FIXME: Find a better place to put this...
39static Cursor x11_empty_cursor = None;
40static bool x11_cursor_visible = true;
41
42static SDL_Cursor *sys_cursors[SDL_HITTEST_RESIZE_LEFT + 1];
43
44static Display *GetDisplay(void)
45{
46 return SDL_GetVideoDevice()->internal->display;
47}
48
49static Cursor X11_CreateEmptyCursor(void)
50{
51 if (x11_empty_cursor == None) {
52 Display *display = GetDisplay();
53 char data[1];
54 XColor color;
55 Pixmap pixmap;
56
57 SDL_zeroa(data);
58 color.red = color.green = color.blue = 0;
59 pixmap = X11_XCreateBitmapFromData(display, DefaultRootWindow(display),
60 data, 1, 1);
61 if (pixmap) {
62 x11_empty_cursor = X11_XCreatePixmapCursor(display, pixmap, pixmap,
63 &color, &color, 0, 0);
64 X11_XFreePixmap(display, pixmap);
65 }
66 }
67 return x11_empty_cursor;
68}
69
70static void X11_DestroyEmptyCursor(void)
71{
72 if (x11_empty_cursor != None) {
73 X11_XFreeCursor(GetDisplay(), x11_empty_cursor);
74 x11_empty_cursor = None;
75 }
76}
77
78static SDL_Cursor *X11_CreateCursorAndData(Cursor x11_cursor)
79{
80 SDL_Cursor *cursor = (SDL_Cursor *)SDL_calloc(1, sizeof(*cursor));
81 if (cursor) {
82 SDL_CursorData *data = (SDL_CursorData *)SDL_calloc(1, sizeof(*data));
83 if (!data) {
84 SDL_free(cursor);
85 return NULL;
86 }
87 data->cursor = x11_cursor;
88 cursor->internal = data;
89 }
90 return cursor;
91}
92
93#ifdef SDL_VIDEO_DRIVER_X11_XCURSOR
94static Cursor X11_CreateXCursorCursor(SDL_Surface *surface, int hot_x, int hot_y)
95{
96 Display *display = GetDisplay();
97 Cursor cursor = None;
98 XcursorImage *image;
99
100 image = X11_XcursorImageCreate(surface->w, surface->h);
101 if (!image) {
102 SDL_OutOfMemory();
103 return None;
104 }
105 image->xhot = hot_x;
106 image->yhot = hot_y;
107 image->delay = 0;
108
109 SDL_assert(surface->format == SDL_PIXELFORMAT_ARGB8888);
110 SDL_assert(surface->pitch == surface->w * 4);
111 SDL_memcpy(image->pixels, surface->pixels, (size_t)surface->h * surface->pitch);
112
113 cursor = X11_XcursorImageLoadCursor(display, image);
114
115 X11_XcursorImageDestroy(image);
116
117 return cursor;
118}
119#endif // SDL_VIDEO_DRIVER_X11_XCURSOR
120
121static Cursor X11_CreatePixmapCursor(SDL_Surface *surface, int hot_x, int hot_y)
122{
123 Display *display = GetDisplay();
124 XColor fg, bg;
125 Cursor cursor = None;
126 Uint32 *ptr;
127 Uint8 *data_bits, *mask_bits;
128 Pixmap data_pixmap, mask_pixmap;
129 int x, y;
130 unsigned int rfg, gfg, bfg, rbg, gbg, bbg, fgBits, bgBits;
131 size_t width_bytes = ((surface->w + 7) & ~((size_t)7)) / 8;
132
133 data_bits = SDL_calloc(1, surface->h * width_bytes);
134 if (!data_bits) {
135 return None;
136 }
137
138 mask_bits = SDL_calloc(1, surface->h * width_bytes);
139 if (!mask_bits) {
140 SDL_free(data_bits);
141 return None;
142 }
143
144 // Code below assumes ARGB pixel format
145 SDL_assert(surface->format == SDL_PIXELFORMAT_ARGB8888);
146
147 rfg = gfg = bfg = rbg = gbg = bbg = fgBits = bgBits = 0;
148 for (y = 0; y < surface->h; ++y) {
149 ptr = (Uint32 *)((Uint8 *)surface->pixels + y * surface->pitch);
150 for (x = 0; x < surface->w; ++x) {
151 int alpha = (*ptr >> 24) & 0xff;
152 int red = (*ptr >> 16) & 0xff;
153 int green = (*ptr >> 8) & 0xff;
154 int blue = (*ptr >> 0) & 0xff;
155 if (alpha > 25) {
156 mask_bits[y * width_bytes + x / 8] |= (0x01 << (x % 8));
157
158 if ((red + green + blue) > 0x40) {
159 fgBits++;
160 rfg += red;
161 gfg += green;
162 bfg += blue;
163 data_bits[y * width_bytes + x / 8] |= (0x01 << (x % 8));
164 } else {
165 bgBits++;
166 rbg += red;
167 gbg += green;
168 bbg += blue;
169 }
170 }
171 ++ptr;
172 }
173 }
174
175 if (fgBits) {
176 fg.red = rfg * 257 / fgBits;
177 fg.green = gfg * 257 / fgBits;
178 fg.blue = bfg * 257 / fgBits;
179 } else {
180 fg.red = fg.green = fg.blue = 0;
181 }
182
183 if (bgBits) {
184 bg.red = rbg * 257 / bgBits;
185 bg.green = gbg * 257 / bgBits;
186 bg.blue = bbg * 257 / bgBits;
187 } else {
188 bg.red = bg.green = bg.blue = 0;
189 }
190
191 data_pixmap = X11_XCreateBitmapFromData(display, DefaultRootWindow(display),
192 (char *)data_bits,
193 surface->w, surface->h);
194 mask_pixmap = X11_XCreateBitmapFromData(display, DefaultRootWindow(display),
195 (char *)mask_bits,
196 surface->w, surface->h);
197 cursor = X11_XCreatePixmapCursor(display, data_pixmap, mask_pixmap,
198 &fg, &bg, hot_x, hot_y);
199 X11_XFreePixmap(display, data_pixmap);
200 X11_XFreePixmap(display, mask_pixmap);
201 SDL_free(data_bits);
202 SDL_free(mask_bits);
203
204 return cursor;
205}
206
207static SDL_Cursor *X11_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
208{
209 Cursor x11_cursor = None;
210
211#ifdef SDL_VIDEO_DRIVER_X11_XCURSOR
212 if (SDL_X11_HAVE_XCURSOR) {
213 x11_cursor = X11_CreateXCursorCursor(surface, hot_x, hot_y);
214 }
215#endif
216 if (x11_cursor == None) {
217 x11_cursor = X11_CreatePixmapCursor(surface, hot_x, hot_y);
218 }
219 return X11_CreateCursorAndData(x11_cursor);
220}
221
222static unsigned int GetLegacySystemCursorShape(SDL_SystemCursor id)
223{
224 switch (id) {
225 // X Font Cursors reference:
226 // http://tronche.com/gui/x/xlib/appendix/b/
227 case SDL_SYSTEM_CURSOR_DEFAULT: return XC_left_ptr;
228 case SDL_SYSTEM_CURSOR_TEXT: return XC_xterm;
229 case SDL_SYSTEM_CURSOR_WAIT: return XC_watch;
230 case SDL_SYSTEM_CURSOR_CROSSHAIR: return XC_tcross;
231 case SDL_SYSTEM_CURSOR_PROGRESS: return XC_watch;
232 case SDL_SYSTEM_CURSOR_NWSE_RESIZE: return XC_top_left_corner;
233 case SDL_SYSTEM_CURSOR_NESW_RESIZE: return XC_top_right_corner;
234 case SDL_SYSTEM_CURSOR_EW_RESIZE: return XC_sb_h_double_arrow;
235 case SDL_SYSTEM_CURSOR_NS_RESIZE: return XC_sb_v_double_arrow;
236 case SDL_SYSTEM_CURSOR_MOVE: return XC_fleur;
237 case SDL_SYSTEM_CURSOR_NOT_ALLOWED: return XC_pirate;
238 case SDL_SYSTEM_CURSOR_POINTER: return XC_hand2;
239 case SDL_SYSTEM_CURSOR_NW_RESIZE: return XC_top_left_corner;
240 case SDL_SYSTEM_CURSOR_N_RESIZE: return XC_top_side;
241 case SDL_SYSTEM_CURSOR_NE_RESIZE: return XC_top_right_corner;
242 case SDL_SYSTEM_CURSOR_E_RESIZE: return XC_right_side;
243 case SDL_SYSTEM_CURSOR_SE_RESIZE: return XC_bottom_right_corner;
244 case SDL_SYSTEM_CURSOR_S_RESIZE: return XC_bottom_side;
245 case SDL_SYSTEM_CURSOR_SW_RESIZE: return XC_bottom_left_corner;
246 case SDL_SYSTEM_CURSOR_W_RESIZE: return XC_left_side;
247 case SDL_SYSTEM_CURSOR_COUNT: break; // so the compiler might notice if an enum value is missing here.
248 }
249
250 SDL_assert(0);
251 return 0;
252}
253
254static SDL_Cursor *X11_CreateSystemCursor(SDL_SystemCursor id)
255{
256 SDL_Cursor *cursor = NULL;
257 Display *dpy = GetDisplay();
258 Cursor x11_cursor = None;
259
260#ifdef SDL_VIDEO_DRIVER_X11_XCURSOR
261 if (SDL_X11_HAVE_XCURSOR) {
262 x11_cursor = X11_XcursorLibraryLoadCursor(dpy, SDL_GetCSSCursorName(id, NULL));
263 }
264#endif
265
266 if (x11_cursor == None) {
267 x11_cursor = X11_XCreateFontCursor(dpy, GetLegacySystemCursorShape(id));
268 }
269
270 if (x11_cursor != None) {
271 cursor = X11_CreateCursorAndData(x11_cursor);
272 }
273
274 return cursor;
275}
276
277static SDL_Cursor *X11_CreateDefaultCursor(void)
278{
279 SDL_SystemCursor id = SDL_GetDefaultSystemCursor();
280 return X11_CreateSystemCursor(id);
281}
282
283static void X11_FreeCursor(SDL_Cursor *cursor)
284{
285 Cursor x11_cursor = cursor->internal->cursor;
286
287 if (x11_cursor != None) {
288 X11_XFreeCursor(GetDisplay(), x11_cursor);
289 }
290 SDL_free(cursor->internal);
291 SDL_free(cursor);
292}
293
294static bool X11_ShowCursor(SDL_Cursor *cursor)
295{
296 Cursor x11_cursor = 0;
297
298 if (cursor) {
299 x11_cursor = cursor->internal->cursor;
300 } else {
301 x11_cursor = X11_CreateEmptyCursor();
302 }
303
304 // FIXME: Is there a better way than this?
305 {
306 SDL_VideoDevice *video = SDL_GetVideoDevice();
307 Display *display = GetDisplay();
308 SDL_Window *window;
309
310 x11_cursor_visible = !!cursor;
311
312 for (window = video->windows; window; window = window->next) {
313 SDL_WindowData *data = window->internal;
314 if (data) {
315 if (x11_cursor != None) {
316 X11_XDefineCursor(display, data->xwindow, x11_cursor);
317 } else {
318 X11_XUndefineCursor(display, data->xwindow);
319 }
320 }
321 }
322 X11_XFlush(display);
323 }
324 return true;
325}
326
327static void X11_WarpMouseInternal(Window xwindow, float x, float y)
328{
329 SDL_VideoData *videodata = SDL_GetVideoDevice()->internal;
330 Display *display = videodata->display;
331 bool warp_hack = false;
332
333 // XWayland will only warp the cursor if it is hidden, so this workaround is required.
334 if (videodata->is_xwayland && x11_cursor_visible) {
335 warp_hack = true;
336 }
337
338 if (warp_hack) {
339 X11_ShowCursor(NULL);
340 }
341#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
342 int deviceid = 0;
343 if (X11_Xinput2IsInitialized()) {
344 /* It seems XIWarpPointer() doesn't work correctly on multi-head setups:
345 * https://developer.blender.org/rB165caafb99c6846e53d11c4e966990aaffc06cea
346 */
347 if (ScreenCount(display) == 1) {
348 X11_XIGetClientPointer(display, None, &deviceid);
349 }
350 }
351 if (deviceid != 0) {
352 SDL_assert(SDL_X11_HAVE_XINPUT2);
353 X11_XIWarpPointer(display, deviceid, None, xwindow, 0.0, 0.0, 0, 0, x, y);
354 } else
355#endif
356 {
357 X11_XWarpPointer(display, None, xwindow, 0, 0, 0, 0, (int)x, (int)y);
358 }
359
360 if (warp_hack) {
361 X11_ShowCursor(SDL_GetCursor());
362 }
363 X11_XSync(display, False);
364 videodata->global_mouse_changed = true;
365}
366
367static bool X11_WarpMouse(SDL_Window *window, float x, float y)
368{
369 SDL_WindowData *data = window->internal;
370
371 if (X11_WarpMouseXTest(SDL_GetVideoDevice(), window, x, y)) {
372 return true;
373 }
374
375#ifdef SDL_VIDEO_DRIVER_X11_XFIXES
376 // If we have no barrier, we need to warp
377 if (data->pointer_barrier_active == false) {
378 X11_WarpMouseInternal(data->xwindow, x, y);
379 }
380#else
381 X11_WarpMouseInternal(data->xwindow, x, y);
382#endif
383 return true;
384}
385
386static bool X11_WarpMouseGlobal(float x, float y)
387{
388 if (X11_WarpMouseXTest(SDL_GetVideoDevice(), NULL, x, y)) {
389 return true;
390 }
391
392 X11_WarpMouseInternal(DefaultRootWindow(GetDisplay()), x, y);
393 return true;
394}
395
396static bool X11_SetRelativeMouseMode(bool enabled)
397{
398 if (!X11_Xinput2IsInitialized()) {
399 return SDL_Unsupported();
400 }
401 return true;
402}
403
404static bool X11_CaptureMouse(SDL_Window *window)
405{
406 Display *display = GetDisplay();
407 SDL_Window *mouse_focus = SDL_GetMouseFocus();
408
409 if (window) {
410 SDL_WindowData *data = window->internal;
411
412 /* If XInput2 is handling the pointer input, non-confinement grabs will always fail with 'AlreadyGrabbed',
413 * since the pointer is being grabbed by XInput2.
414 */
415 if (!data->xinput2_mouse_enabled || data->mouse_grabbed) {
416 const unsigned int mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask;
417 Window confined = (data->mouse_grabbed ? data->xwindow : None);
418 const int rc = X11_XGrabPointer(display, data->xwindow, False,
419 mask, GrabModeAsync, GrabModeAsync,
420 confined, None, CurrentTime);
421 if (rc != GrabSuccess) {
422 return SDL_SetError("X server refused mouse capture");
423 }
424
425 if (data->mouse_grabbed) {
426 // XGrabPointer can warp the cursor when confining, so update the coordinates.
427 data->videodata->global_mouse_changed = true;
428 }
429 }
430 } else if (mouse_focus) {
431 SDL_UpdateWindowGrab(mouse_focus);
432 } else {
433 X11_XUngrabPointer(display, CurrentTime);
434 }
435
436 X11_XSync(display, False);
437
438 return true;
439}
440
441static SDL_MouseButtonFlags X11_GetGlobalMouseState(float *x, float *y)
442{
443 SDL_VideoData *videodata = SDL_GetVideoDevice()->internal;
444 SDL_DisplayID *displays;
445 Display *display = GetDisplay();
446 int i;
447
448 // !!! FIXME: should we XSync() here first?
449
450 if (!X11_Xinput2IsInitialized()) {
451 videodata->global_mouse_changed = true;
452 }
453
454 // check if we have this cached since XInput last saw the mouse move.
455 // !!! FIXME: can we just calculate this from XInput's events?
456 if (videodata->global_mouse_changed) {
457 displays = SDL_GetDisplays(NULL);
458 if (displays) {
459 for (i = 0; displays[i]; ++i) {
460 SDL_DisplayData *data = SDL_GetDisplayDriverData(displays[i]);
461 if (data) {
462 Window root, child;
463 int rootx, rooty, winx, winy;
464 unsigned int mask;
465 if (X11_XQueryPointer(display, RootWindow(display, data->screen), &root, &child, &rootx, &rooty, &winx, &winy, &mask)) {
466 XWindowAttributes root_attrs;
467 SDL_MouseButtonFlags buttons = 0;
468 buttons |= (mask & Button1Mask) ? SDL_BUTTON_LMASK : 0;
469 buttons |= (mask & Button2Mask) ? SDL_BUTTON_MMASK : 0;
470 buttons |= (mask & Button3Mask) ? SDL_BUTTON_RMASK : 0;
471 // Use the SDL state for the extended buttons - it's better than nothing
472 buttons |= (SDL_GetMouseState(NULL, NULL) & (SDL_BUTTON_X1MASK | SDL_BUTTON_X2MASK));
473 /* SDL_DisplayData->x,y point to screen origin, and adding them to mouse coordinates relative to root window doesn't do the right thing
474 * (observed on dual monitor setup with primary display being the rightmost one - mouse was offset to the right).
475 *
476 * Adding root position to root-relative coordinates seems to be a better way to get absolute position. */
477 X11_XGetWindowAttributes(display, root, &root_attrs);
478 videodata->global_mouse_position.x = root_attrs.x + rootx;
479 videodata->global_mouse_position.y = root_attrs.y + rooty;
480 videodata->global_mouse_buttons = buttons;
481 videodata->global_mouse_changed = false;
482 break;
483 }
484 }
485 }
486 SDL_free(displays);
487 }
488 }
489
490 SDL_assert(!videodata->global_mouse_changed); // The pointer wasn't on any X11 screen?!
491
492 *x = (float)videodata->global_mouse_position.x;
493 *y = (float)videodata->global_mouse_position.y;
494 return videodata->global_mouse_buttons;
495}
496
497void X11_InitMouse(SDL_VideoDevice *_this)
498{
499 SDL_Mouse *mouse = SDL_GetMouse();
500
501 mouse->CreateCursor = X11_CreateCursor;
502 mouse->CreateSystemCursor = X11_CreateSystemCursor;
503 mouse->ShowCursor = X11_ShowCursor;
504 mouse->FreeCursor = X11_FreeCursor;
505 mouse->WarpMouse = X11_WarpMouse;
506 mouse->WarpMouseGlobal = X11_WarpMouseGlobal;
507 mouse->SetRelativeMouseMode = X11_SetRelativeMouseMode;
508 mouse->CaptureMouse = X11_CaptureMouse;
509 mouse->GetGlobalMouseState = X11_GetGlobalMouseState;
510
511 SDL_HitTestResult r = SDL_HITTEST_NORMAL;
512 while (r <= SDL_HITTEST_RESIZE_LEFT) {
513 switch (r) {
514 case SDL_HITTEST_NORMAL: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT); break;
515 case SDL_HITTEST_DRAGGABLE: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT); break;
516 case SDL_HITTEST_RESIZE_TOPLEFT: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_NW_RESIZE); break;
517 case SDL_HITTEST_RESIZE_TOP: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_N_RESIZE); break;
518 case SDL_HITTEST_RESIZE_TOPRIGHT: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_NE_RESIZE); break;
519 case SDL_HITTEST_RESIZE_RIGHT: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_E_RESIZE); break;
520 case SDL_HITTEST_RESIZE_BOTTOMRIGHT: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_SE_RESIZE); break;
521 case SDL_HITTEST_RESIZE_BOTTOM: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_S_RESIZE); break;
522 case SDL_HITTEST_RESIZE_BOTTOMLEFT: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_SW_RESIZE); break;
523 case SDL_HITTEST_RESIZE_LEFT: sys_cursors[r] = X11_CreateSystemCursor(SDL_SYSTEM_CURSOR_W_RESIZE); break;
524 }
525 r++;
526 }
527
528 SDL_SetDefaultCursor(X11_CreateDefaultCursor());
529}
530
531void X11_QuitMouse(SDL_VideoDevice *_this)
532{
533 SDL_VideoData *data = _this->internal;
534 SDL_XInput2DeviceInfo *i;
535 SDL_XInput2DeviceInfo *next;
536 int j;
537
538 for (j = 0; j < SDL_arraysize(sys_cursors); j++) {
539 X11_FreeCursor(sys_cursors[j]);
540 sys_cursors[j] = NULL;
541 }
542
543 for (i = data->mouse_device_info; i; i = next) {
544 next = i->next;
545 SDL_free(i);
546 }
547 data->mouse_device_info = NULL;
548
549 X11_DestroyEmptyCursor();
550}
551
552void X11_SetHitTestCursor(SDL_HitTestResult rc)
553{
554 if (rc == SDL_HITTEST_NORMAL || rc == SDL_HITTEST_DRAGGABLE) {
555 SDL_SetCursor(NULL);
556 } else {
557 X11_ShowCursor(sys_cursors[rc]);
558 }
559}
560
561#endif // SDL_VIDEO_DRIVER_X11
562