SDL: x11: added hotplug support for XInput2 devices

From 3dfc3b4c8da52f6550afe373bccebfb41b66e413 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 25 Mar 2024 16:32:40 -0700
Subject: [PATCH] x11: added hotplug support for XInput2 devices

---
 src/video/x11/SDL_x11events.c  |   7 ++
 src/video/x11/SDL_x11video.h   |   1 +
 src/video/x11/SDL_x11xinput2.c | 207 ++++++++++++++++++++++++---------
 src/video/x11/SDL_x11xinput2.h |   1 +
 4 files changed, 161 insertions(+), 55 deletions(-)

diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index afff85370043c..2e3a4eb74a762 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -29,6 +29,7 @@
 #include <limits.h> /* For INT_MAX */
 
 #include "SDL_x11video.h"
+#include "SDL_x11pen.h"
 #include "SDL_x11touch.h"
 #include "SDL_x11xinput2.h"
 #include "SDL_x11xfixes.h"
@@ -2033,6 +2034,12 @@ void X11_PumpEvents(SDL_VideoDevice *_this)
             X11_FlashWindow(_this, data->windowlist[i]->window, SDL_FLASH_CANCEL);
         }
     }
+
+    if (data->xinput_hierarchy_changed) {
+        X11_InitPen(_this);
+        X11_Xinput2UpdateDevices(_this, SDL_FALSE);
+        data->xinput_hierarchy_changed = SDL_FALSE;
+    }
 }
 
 int X11_SuspendScreenSaver(SDL_VideoDevice *_this)
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index e199dd51692e4..71a2daf9d35b8 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -111,6 +111,7 @@ struct SDL_VideoData
     Uint32 global_mouse_buttons;
 
     SDL_XInput2DeviceInfo *mouse_device_info;
+    SDL_bool xinput_hierarchy_changed;
 
     int xrandr_event_base;
 
diff --git a/src/video/x11/SDL_x11xinput2.c b/src/video/x11/SDL_x11xinput2.c
index e599e0e5cb0c4..cdc741f143cbb 100644
--- a/src/video/x11/SDL_x11xinput2.c
+++ b/src/video/x11/SDL_x11xinput2.c
@@ -95,59 +95,6 @@ static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window
     return windowdata ? windowdata->window : NULL;
 }
 
-static void xinput2_init_device_list(SDL_VideoData *videodata)
-{
-    XIDeviceInfo *info;
-    int ndevices;
-
-    info = X11_XIQueryDevice(videodata->display, XIAllDevices, &ndevices);
-
-    for (int i = 0; i < ndevices; i++) {
-        XIDeviceInfo *dev = &info[i];
-
-        switch (dev->use) {
-        case XIMasterKeyboard:
-            SDL_AddKeyboard((SDL_KeyboardID)dev->deviceid, dev->name, SDL_FALSE);
-            break;
-        case XISlaveKeyboard:
-            SDL_AddKeyboard((SDL_KeyboardID)dev->deviceid, dev->name, SDL_FALSE);
-            break;
-        case XIMasterPointer:
-            SDL_AddMouse((SDL_MouseID)dev->deviceid, dev->name, SDL_FALSE);
-            break;
-        case XISlavePointer:
-            SDL_AddMouse((SDL_MouseID)dev->deviceid, dev->name, SDL_FALSE);
-            break;
-        default:
-            break;
-        }
-
-#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
-        for (int j = 0; j < dev->num_classes; j++) {
-            SDL_TouchID touchId;
-            SDL_TouchDeviceType touchType;
-            XIAnyClassInfo *class = dev->classes[j];
-            XITouchClassInfo *t = (XITouchClassInfo *)class;
-
-            /* Only touch devices */
-            if (class->type != XITouchClass) {
-                continue;
-            }
-
-            if (t->mode == XIDependentTouch) {
-                touchType = SDL_TOUCH_DEVICE_INDIRECT_RELATIVE;
-            } else { /* XIDirectTouch */
-                touchType = SDL_TOUCH_DEVICE_DIRECT;
-            }
-
-            touchId = t->sourceid;
-            SDL_AddTouch(touchId, touchType, dev->name);
-        }
-#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
-    }
-    X11_XIFreeDeviceInfo(info);
-}
-
 #ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
 static void xinput2_normalize_touch_coordinates(SDL_Window *window, double in_x, double in_y, float *out_x, float *out_y)
 {
@@ -239,7 +186,7 @@ SDL_bool X11_InitXinput2(SDL_VideoDevice *_this)
     XISetMask(mask, XI_HierarchyChanged);
     X11_XISelectEvents(data->display, DefaultRootWindow(data->display), &eventmask, 1);
 
-    xinput2_init_device_list(data);
+    X11_Xinput2UpdateDevices(_this, SDL_TRUE);
 
     return SDL_TRUE;
 #else
@@ -420,7 +367,7 @@ int X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
                 xinput2_remove_device_info(videodata, hierev->info[i].deviceid);
             }
         }
-        X11_InitPen(_this);
+        videodata->xinput_hierarchy_changed = SDL_TRUE;
     } break;
 
     case XI_KeyPress:
@@ -735,4 +682,154 @@ void X11_Xinput2UngrabTouch(SDL_VideoDevice *_this, SDL_Window *window)
 #endif
 }
 
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+
+static void AddDeviceID(Uint32 deviceID, Uint32 **list, int *count)
+{
+    int new_count = (*count + 1);
+    Uint32 *new_list = (Uint32 *)SDL_realloc(*list, new_count * sizeof(*new_list));
+    if (!new_list) {
+        /* Oh well, we'll drop this one */
+        return;
+    }
+    new_list[new_count - 1] = deviceID;
+
+    *count = new_count;
+    *list = new_list;
+}
+
+static SDL_bool HasDeviceID(Uint32 deviceID, Uint32 *list, int count)
+{
+    for (int i = 0; i < count; ++i) {
+        if (deviceID == list[i]) {
+            return SDL_TRUE;
+        }
+    }
+    return SDL_FALSE;
+}
+
+#endif // SDL_VIDEO_DRIVER_X11_XINPUT2
+
+void X11_Xinput2UpdateDevices(SDL_VideoDevice *_this, SDL_bool initial_check)
+{
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+    SDL_VideoData *data = _this->driverdata;
+    XIDeviceInfo *info;
+    int ndevices;
+    int old_keyboard_count = 0;
+    SDL_KeyboardID *old_keyboards = NULL;
+    int new_keyboard_count = 0;
+    SDL_KeyboardID *new_keyboards = NULL;
+    int old_mouse_count = 0;
+    SDL_MouseID *old_mice = NULL;
+    int new_mouse_count = 0;
+    SDL_MouseID *new_mice = NULL;
+    int old_touch_count = 0;
+    SDL_TouchID *old_touch_devices64 = NULL;
+    Uint32 *old_touch_devices = NULL;
+    int new_touch_count = 0;
+    Uint32 *new_touch_devices = NULL;
+    SDL_bool send_event = !initial_check;
+
+    info = X11_XIQueryDevice(data->display, XIAllDevices, &ndevices);
+
+    old_keyboards = SDL_GetKeyboards(&old_keyboard_count);
+    old_mice = SDL_GetMice(&old_mouse_count);
+
+    /* SDL_TouchID is 64-bit, but our helper functions take Uint32 */
+    old_touch_devices64 = SDL_GetTouchDevices(&old_touch_count);
+    if (&old_touch_count > 0) {
+        old_touch_devices = (Uint32 *)SDL_malloc(old_touch_count * sizeof(*old_touch_devices));
+        if (old_touch_devices) {
+            for (int i = 0; i < old_touch_count; ++i) {
+                old_touch_devices[i] = (Uint32)old_touch_devices64[i];
+            }
+        }
+    }
+    SDL_free(old_touch_devices64);
+
+    for (int i = 0; i < ndevices; i++) {
+        XIDeviceInfo *dev = &info[i];
+
+        switch (dev->use) {
+        case XIMasterKeyboard:
+        case XISlaveKeyboard:
+            {
+                SDL_KeyboardID keyboardID = (SDL_KeyboardID)dev->deviceid;
+                AddDeviceID(keyboardID, &new_keyboards, &new_keyboard_count);
+                if (!HasDeviceID(keyboardID, old_keyboards, old_keyboard_count)) {
+                    SDL_AddKeyboard(keyboardID, dev->name, send_event);
+                }
+            }
+            break;
+        case XIMasterPointer:
+        case XISlavePointer:
+            {
+                SDL_MouseID mouseID = (SDL_MouseID)dev->deviceid;
+                AddDeviceID(mouseID, &new_mice, &new_mouse_count);
+                if (!HasDeviceID(mouseID, old_mice, old_mouse_count)) {
+                    SDL_AddMouse(mouseID, dev->name, send_event);
+                }
+            }
+            break;
+        default:
+            break;
+        }
+
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
+        for (int j = 0; j < dev->num_classes; j++) {
+            Uint32 touchID;
+            SDL_TouchDeviceType touchType;
+            XIAnyClassInfo *class = dev->classes[j];
+            XITouchClassInfo *t = (XITouchClassInfo *)class;
+
+            /* Only touch devices */
+            if (class->type != XITouchClass) {
+                continue;
+            }
+
+            touchID = (Uint32)t->sourceid;
+            AddDeviceID(touchID, &new_touch_devices, &new_touch_count);
+            if (!HasDeviceID(touchID, old_touch_devices, old_touch_count)) {
+                if (t->mode == XIDependentTouch) {
+                    touchType = SDL_TOUCH_DEVICE_INDIRECT_RELATIVE;
+                } else { /* XIDirectTouch */
+                    touchType = SDL_TOUCH_DEVICE_DIRECT;
+                }
+                SDL_AddTouch(touchID, touchType, dev->name);
+            }
+        }
+#endif // SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
+    }
+
+    for (int i = old_keyboard_count; i--;) {
+        if (!HasDeviceID(old_keyboards[i], new_keyboards, new_keyboard_count)) {
+            SDL_RemoveKeyboard(old_keyboards[i]);
+        }
+    }
+
+    for (int i = old_mouse_count; i--;) {
+        if (!HasDeviceID(old_mice[i], new_mice, new_mouse_count)) {
+            SDL_RemoveMouse(old_mice[i]);
+        }
+    }
+
+    for (int i = old_touch_count; i--;) {
+        if (!HasDeviceID(old_touch_devices[i], new_touch_devices, new_touch_count)) {
+            SDL_DelTouch(old_touch_devices[i]);
+        }
+    }
+
+    SDL_free(old_keyboards);
+    SDL_free(new_keyboards);
+    SDL_free(old_mice);
+    SDL_free(new_mice);
+    SDL_free(old_touch_devices);
+    SDL_free(new_touch_devices);
+
+    X11_XIFreeDeviceInfo(info);
+
+#endif // SDL_VIDEO_DRIVER_X11_XINPUT2
+}
+
 #endif /* SDL_VIDEO_DRIVER_X11 */
diff --git a/src/video/x11/SDL_x11xinput2.h b/src/video/x11/SDL_x11xinput2.h
index b0f749fa5e212..e30f814552851 100644
--- a/src/video/x11/SDL_x11xinput2.h
+++ b/src/video/x11/SDL_x11xinput2.h
@@ -39,5 +39,6 @@ extern void X11_Xinput2SelectTouch(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_Xinput2GrabTouch(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_Xinput2UngrabTouch(SDL_VideoDevice *_this, SDL_Window *window);
 extern SDL_bool X11_Xinput2SelectMouseAndKeyboard(SDL_VideoDevice *_this, SDL_Window *window);
+extern void X11_Xinput2UpdateDevices(SDL_VideoDevice *_this, SDL_bool initial_check);
 
 #endif /* SDL_x11xinput2_h_ */