SDL: Added support for multiple distinct keyboards

From f1f24b173c4109bb32fd5d624a849408a624c19c Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 25 Mar 2024 15:18:27 -0700
Subject: [PATCH] Added support for multiple distinct keyboards

---
 src/events/SDL_events.c       |   4 +-
 src/events/SDL_keyboard.c     |   3 -
 src/video/x11/SDL_x11events.c |   4 +-
 test/testcustomcursor.c       |   1 -
 test/testmanymouse.c          | 200 ++++++++++++++++++++++++++++++++--
 5 files changed, 196 insertions(+), 16 deletions(-)

diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 968f53c5d4d9f..93af8aa1f9a5e 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -335,8 +335,8 @@ static void SDL_LogEvent(const SDL_Event *event)
 #undef PRINT_KEYDEV_EVENT
 
 #define PRINT_KEY_EVENT(event)                                                                                                   \
-    (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u state=%s repeat=%s scancode=%u keycode=%u mod=%u)", \
-                       (uint)event->key.timestamp, (uint)event->key.windowID,                                                    \
+    (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u state=%s repeat=%s scancode=%u keycode=%u mod=%u)", \
+                       (uint)event->key.timestamp, (uint)event->key.windowID, (uint)event->key.which,                            \
                        event->key.state == SDL_PRESSED ? "pressed" : "released",                                                 \
                        event->key.repeat ? "true" : "false",                                                                     \
                        (uint)event->key.keysym.scancode,                                                                         \
diff --git a/src/events/SDL_keyboard.c b/src/events/SDL_keyboard.c
index dd14e6c3dac30..f1b2cde892d1e 100644
--- a/src/events/SDL_keyboard.c
+++ b/src/events/SDL_keyboard.c
@@ -945,9 +945,6 @@ static int SDL_SendKeyboardKeyInternal(Uint64 timestamp, Uint32 flags, SDL_Keybo
     Uint8 repeat = SDL_FALSE;
     const Uint8 source = flags & KEYBOARD_SOURCE_MASK;
 
-    /* We currently don't have raw keyboard mode, so all key events are global */
-    keyboardID = SDL_GLOBAL_KEYBOARD_ID;
-
     if (scancode == SDL_SCANCODE_UNKNOWN || scancode >= SDL_NUM_SCANCODES) {
         return 0;
     }
diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 9924c35b17b00..afff85370043c 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -908,7 +908,7 @@ void X11_HandleKeyEvent(SDL_VideoDevice *_this, SDL_WindowData *windowdata, SDL_
         if (xevent->type == KeyPress) {
             /* Don't send the key if it looks like a duplicate of a filtered key sent by an IME */
             if (xevent->xkey.keycode != videodata->filter_code || xevent->xkey.time != videodata->filter_time) {
-                SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, SDL_PRESSED, videodata->key_layout[keycode]);
+                SDL_SendKeyboardKey(0, keyboardID, SDL_PRESSED, videodata->key_layout[keycode]);
             }
             if (*text) {
                 SDL_SendKeyboardText(text);
@@ -918,7 +918,7 @@ void X11_HandleKeyEvent(SDL_VideoDevice *_this, SDL_WindowData *windowdata, SDL_
                 /* We're about to get a repeated key down, ignore the key up */
                 return;
             }
-            SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, SDL_RELEASED, videodata->key_layout[keycode]);
+            SDL_SendKeyboardKey(0, keyboardID, SDL_RELEASED, videodata->key_layout[keycode]);
         }
     }
 
diff --git a/test/testcustomcursor.c b/test/testcustomcursor.c
index 17fbc5faf2f2e..ba0cab050b416 100644
--- a/test/testcustomcursor.c
+++ b/test/testcustomcursor.c
@@ -74,7 +74,6 @@ static const char *cross[] = {
     ". c #ffffff",
     "  c None",
     /* pixels */
-    /* pixels */
     "                                ",
     "                                ",
     "                                ",
diff --git a/test/testmanymouse.c b/test/testmanymouse.c
index ec4dbce5d526e..66b86a490c0c3 100644
--- a/test/testmanymouse.c
+++ b/test/testmanymouse.c
@@ -60,11 +60,56 @@ static const char *arrow[] = {
     "0,0"
 };
 
+static const char *cross[] = {
+    /* width height num_colors chars_per_pixel */
+    "    32    32        3            1",
+    /* colors */
+    "o c #ffffff",
+    ". c #000000",
+    "  c None",
+    /* pixels */
+    "                                ",
+    "                                ",
+    "                                ",
+    "                                ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "    oooooooooooooooooooooooo    ",
+    "    oooooooooooooooooooooooo    ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "               oo               ",
+    "                                ",
+    "                                ",
+    "                                ",
+    "                                ",
+    "0,0"
+};
+
 static SDLTest_CommonState *state;
 static int done;
 
-#define PROP_CURSOR_TEXTURE "cursor_texture"
-#define MAX_MICE    3
+#define PROP_ARROW_CURSOR_TEXTURE "arrow_cursor_texture"
+#define PROP_CROSS_CURSOR_TEXTURE "cross_cursor_texture"
+#define MAX_MICE        3
+#define MAX_KEYBOARDS   3
 #define CURSOR_SIZE 48.0f
 #define MAX_TRAIL   500
 #define TRAIL_SIZE  8.0f
@@ -74,7 +119,8 @@ static SDL_Color colors[] = {
     { 255,   0, 255, 255 }, /* mouse 2, magenta */
     { 255, 255,   0, 255 }, /* mouse 3, yellow */
 };
-SDL_COMPILE_TIME_ASSERT(colors, SDL_arraysize(colors) == MAX_MICE);
+SDL_COMPILE_TIME_ASSERT(mouse_colors, SDL_arraysize(colors) == MAX_MICE);
+SDL_COMPILE_TIME_ASSERT(keyboard_colors, SDL_arraysize(colors) == MAX_KEYBOARDS);
 
 typedef struct
 {
@@ -89,7 +135,17 @@ typedef struct
 
 static MouseState mice[MAX_MICE];
 
-static SDL_Texture *CreateCursor(const char *image[], SDL_Renderer *renderer)
+typedef struct
+{
+    SDL_KeyboardID instance_id;
+    SDL_bool active;
+    Uint8 button_state;
+    SDL_FPoint position;
+} KeyboardState;
+
+static KeyboardState keyboards[MAX_KEYBOARDS];
+
+static SDL_Texture *CreateTexture(const char *image[], SDL_Renderer *renderer)
 {
     SDL_Surface *surface;
     SDL_Palette *palette;
@@ -109,6 +165,9 @@ static SDL_Texture *CreateCursor(const char *image[], SDL_Renderer *renderer)
     palette->colors['.'].r = 0xFF;
     palette->colors['.'].g = 0xFF;
     palette->colors['.'].b = 0xFF;
+    palette->colors['o'].r = 0xFF;
+    palette->colors['o'].g = 0xFF;
+    palette->colors['o'].b = 0xFF;
     palette->colors['X'].r = 0x00;
     palette->colors['X'].g = 0x00;
     palette->colors['X'].b = 0x00;
@@ -271,6 +330,115 @@ static void DrawMouseState(SDL_Window *window, SDL_Renderer *renderer, MouseStat
     SDL_RenderTexture(renderer, cursor, NULL, &rect);
 }
 
+static void HandleKeyboardAdded(SDL_KeyboardID instance_id)
+{
+    SDL_Window *window = state->windows[0];
+    int i, w = 0, h = 0;
+
+    SDL_GetWindowSize(window, &w, &h);
+
+    for (i = 0; i < SDL_arraysize(keyboards); ++i) {
+        KeyboardState *keyboard_state = &keyboards[i];
+        if (!keyboard_state->active) {
+            keyboard_state->instance_id = instance_id;
+            keyboard_state->active = SDL_TRUE;
+            keyboard_state->position.x = w * 0.5f;
+            keyboard_state->position.y = h * 0.5f;
+            return;
+        }
+    }
+}
+
+static void HandleKeyboardRemoved(SDL_KeyboardID instance_id)
+{
+    int i;
+
+    for (i = 0; i < SDL_arraysize(keyboards); ++i) {
+        KeyboardState *keyboard_state = &keyboards[i];
+        if (instance_id == keyboard_state->instance_id) {
+            SDL_zerop(keyboard_state);
+            return;
+        }
+    }
+}
+
+static void ActivateKeyboard(SDL_KeyboardID instance_id)
+{
+    int i;
+
+    for (i = 0; i < SDL_arraysize(keyboards); ++i) {
+        KeyboardState *keyboard_state = &keyboards[i];
+        if (keyboard_state->active && instance_id == keyboard_state->instance_id) {
+            return;
+        }
+    }
+
+    HandleKeyboardAdded(instance_id);
+}
+
+static void HandleKeyboardKeyDown(SDL_KeyboardEvent *event)
+{
+    SDL_Window *window = state->windows[0];
+    int i, w = 0, h = 0;
+
+    SDL_GetWindowSize(window, &w, &h);
+
+    ActivateKeyboard(event->which);
+
+    for (i = 0; i < SDL_arraysize(keyboards); ++i) {
+        KeyboardState *keyboard_state = &keyboards[i];
+        if (!keyboard_state->active) {
+            continue;
+        }
+        if (event->which == keyboard_state->instance_id) {
+            switch (event->keysym.sym) {
+            case SDLK_LEFT:
+                keyboard_state->position.x -= CURSOR_SIZE;
+                if (keyboard_state->position.x < 0.0f) {
+                    keyboard_state->position.x = 0.0f;
+                }
+                break;
+            case SDLK_RIGHT:
+                keyboard_state->position.x += CURSOR_SIZE;
+                if (keyboard_state->position.x > w) {
+                    keyboard_state->position.x = w;
+                }
+                break;
+            case SDLK_UP:
+                keyboard_state->position.y -= CURSOR_SIZE;
+                if (keyboard_state->position.y < 0.0f) {
+                    keyboard_state->position.y = 0.0f;
+                }
+                break;
+            case SDLK_DOWN:
+                keyboard_state->position.y += CURSOR_SIZE;
+                if (keyboard_state->position.y > h) {
+                    keyboard_state->position.y = h;
+                }
+                break;
+            default:
+                break;
+            }
+        }
+    }
+}
+
+static void DrawKeyboardState(SDL_Window *window, SDL_Renderer *renderer, KeyboardState *keyboard_state, SDL_Texture *cursor, SDL_Color *color)
+{
+    SDL_FRect rect;
+
+    if (!keyboard_state->active) {
+        return;
+    }
+
+    rect.x = keyboard_state->position.x - CURSOR_SIZE / 2;
+    rect.y = keyboard_state->position.y - CURSOR_SIZE / 2;
+    rect.w = CURSOR_SIZE;
+    rect.h = CURSOR_SIZE;
+    SDL_SetTextureColorMod(cursor, color->r, color->g, color->b);
+    SDL_RenderTexture(renderer, cursor, NULL, &rect);
+}
+
 static void loop(void)
 {
     int i, j;
@@ -281,6 +449,15 @@ static void loop(void)
         SDLTest_CommonEvent(state, &event, &done);
 
         switch (event.type) {
+        case SDL_EVENT_KEYBOARD_ADDED:
+            /* Wait for events before activating this keyboard */
+            break;
+        case SDL_EVENT_KEYBOARD_REMOVED:
+            HandleKeyboardRemoved(event.kdevice.which);
+            break;
+        case SDL_EVENT_KEY_DOWN:
+            HandleKeyboardKeyDown(&event.key);
+            break;
         case SDL_EVENT_MOUSE_ADDED:
             /* Wait for events before activating this mouse */
             break;
@@ -302,13 +479,18 @@ static void loop(void)
     for (i = 0; i < state->num_windows; ++i) {
         SDL_Window *window = state->windows[i];
         SDL_Renderer *renderer = state->renderers[i];
-        SDL_Texture *cursor = (SDL_Texture *)SDL_GetProperty(SDL_GetRendererProperties(renderer), PROP_CURSOR_TEXTURE, NULL);
+        SDL_Texture *arrow_cursor = (SDL_Texture *)SDL_GetProperty(SDL_GetRendererProperties(renderer), PROP_ARROW_CURSOR_TEXTURE, NULL);
+        SDL_Texture *cross_cursor = (SDL_Texture *)SDL_GetProperty(SDL_GetRendererProperties(renderer), PROP_CROSS_CURSOR_TEXTURE, NULL);
 
         SDL_SetRenderDrawColor(renderer, 128, 128, 128, 255);
         SDL_RenderClear(renderer);
 
         for (j = 0; j < SDL_arraysize(mice); ++j) {
-            DrawMouseState(window, renderer, &mice[j], cursor, &colors[j]);
+            DrawMouseState(window, renderer, &mice[j], arrow_cursor, &colors[j]);
+        }
+
+        for (j = 0; j < SDL_arraysize(keyboards); ++j) {
+            DrawKeyboardState(window, renderer, &keyboards[j], cross_cursor, &colors[j]);
         }
 
         SDL_RenderPresent(renderer);
@@ -349,9 +531,11 @@ int main(int argc, char *argv[])
     /* Create the cursor textures */
     for (i = 0; i < state->num_windows; ++i) {
         SDL_Renderer *renderer = state->renderers[i];
+        SDL_Texture *cursor_arrow = CreateTexture(arrow, renderer);
+        SDL_Texture *cursor_cross = CreateTexture(cross, renderer);
 
-        SDL_Texture *cursor = CreateCursor(arrow, renderer);
-        SDL_SetProperty(SDL_GetRendererProperties(renderer), PROP_CURSOR_TEXTURE, cursor);
+        SDL_SetProperty(SDL_GetRendererProperties(renderer), PROP_ARROW_CURSOR_TEXTURE, cursor_arrow);
+        SDL_SetProperty(SDL_GetRendererProperties(renderer), PROP_CROSS_CURSOR_TEXTURE, cursor_cross);
     }
 
     /* We only get mouse motion for distinct devices when relative mode is enabled */