SDL: keyboard: Add some SDL keycodes for common Xkb keys

https://github.com/libsdl-org/SDL/commit/6b776a9989d89cac2c5d7d223b8230f021fb0062

From 6b776a9989d89cac2c5d7d223b8230f021fb0062 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sun, 5 Jan 2025 13:30:44 -0500
Subject: [PATCH] keyboard: Add some SDL keycodes for common Xkb keys

Add SDL keycodes for keys found commonly found in the default Xkb layout, such as left tab and compose, and keys frequently used for custom modifiers such as Meta, Hyper, and Level5 Shift.

As these keys aren't Unicode code points and don't have associated scancodes (at least on modern keyboards), they are placed in the new extended key code space, with bit 30 set as a flag.
---
 docs/README-migration.md              |  3 +
 include/SDL3/SDL_keycode.h            | 13 ++++-
 src/events/SDL_keymap.c               | 50 +++++++++++++++++
 src/events/SDL_keysym_to_keycode.c    | 68 ++++++++++++++++++++++
 src/events/SDL_keysym_to_keycode_c.h  | 28 +++++++++
 src/events/SDL_keysym_to_scancode.c   |  1 +
 src/events/SDL_keysym_to_scancode_c.h |  3 +
 src/video/wayland/SDL_waylandevents.c | 81 +++++++++------------------
 src/video/x11/SDL_x11events.c         | 68 ++++++++++++----------
 src/video/x11/SDL_x11keyboard.c       | 66 +++++++++-------------
 src/video/x11/SDL_x11video.h          |  3 +-
 test/checkkeys.c                      |  3 +
 12 files changed, 262 insertions(+), 125 deletions(-)
 create mode 100644 src/events/SDL_keysym_to_keycode.c
 create mode 100644 src/events/SDL_keysym_to_keycode_c.h

diff --git a/docs/README-migration.md b/docs/README-migration.md
index e3924f29ae5ce..cc890fb0715c8 100644
--- a/docs/README-migration.md
+++ b/docs/README-migration.md
@@ -1031,6 +1031,9 @@ The following structures have been removed:
 
 SDL_Keycode is now Uint32 and the SDLK_* constants are now defines instead of an enum, to more clearly reflect that they are a subset of the possible values of an SDL_Keycode.
 
+In addition to the `SDLK_SCANCODE_MASK` bit found on key codes that directly map to scancodes, there is now the
+`SDLK_EXTENDED_MASK` bit used to denote key codes that don't have a corresponding scancode, and aren't a unicode value.
+
 The following symbols have been removed:
 
 * KMOD_RESERVED - No replacement. A bit named "RESERVED" probably shouldn't be used in an app, but if you need it, this was equivalent to KMOD_SCROLL (0x8000) in SDL2.
diff --git a/include/SDL3/SDL_keycode.h b/include/SDL3/SDL_keycode.h
index cb155a5a1c062..3c40e962598ec 100644
--- a/include/SDL3/SDL_keycode.h
+++ b/include/SDL3/SDL_keycode.h
@@ -47,11 +47,15 @@
  * A special exception is the number keys at the top of the keyboard which map
  * to SDLK_0...SDLK_9 on AZERTY layouts.
  *
+ * Keys with the `SDLK_EXTENDED_MASK` bit set do not map to a scancode or
+ * unicode code point.
+ *
  * \since This datatype is available since SDL 3.1.3.
  */
 typedef Uint32 SDL_Keycode;
 
-#define SDLK_SCANCODE_MASK          (1u<<30)
+#define SDLK_EXTENDED_MASK          (1u << 29)
+#define SDLK_SCANCODE_MASK          (1u << 30)
 #define SDL_SCANCODE_TO_KEYCODE(X)  (X | SDLK_SCANCODE_MASK)
 #define SDLK_UNKNOWN                0x00000000u /**< 0 */
 #define SDLK_RETURN                 0x0000000du /**< '\r' */
@@ -302,6 +306,13 @@ typedef Uint32 SDL_Keycode;
 #define SDLK_SOFTRIGHT              0x40000120u /**< SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_SOFTRIGHT) */
 #define SDLK_CALL                   0x40000121u /**< SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_CALL) */
 #define SDLK_ENDCALL                0x40000122u /**< SDL_SCANCODE_TO_KEYCODE(SDL_SCANCODE_ENDCALL) */
+#define SDLK_LEFT_TAB               0x20000001u /**< Extended key Left Tab */
+#define SDLK_LEVEL5_SHIFT           0x20000002u /**< Extended key Level 5 Shift */
+#define SDLK_MULTI_KEY_COMPOSE      0x20000003u /**< Extended key Multi-key Compose */
+#define SDLK_LMETA                  0x20000004u /**< Extended key Left Meta */
+#define SDLK_RMETA                  0x20000005u /**< Extended key Right Meta */
+#define SDLK_LHYPER                 0x20000006u /**< Extended key Left Hyper */
+#define SDLK_RHYPER                 0x20000007u /**< Extended key Right Hyper */
 
 /**
  * Valid key modifiers (possibly OR'd together).
diff --git a/src/events/SDL_keymap.c b/src/events/SDL_keymap.c
index 1aeb57c6c51ef..5c3f784c6c7b8 100644
--- a/src/events/SDL_keymap.c
+++ b/src/events/SDL_keymap.c
@@ -189,6 +189,18 @@ static const SDL_Keycode shifted_default_symbols[] = {
     SDLK_QUESTION
 };
 
+static const struct
+{
+    SDL_Keycode keycode;
+    SDL_Scancode scancode;
+} extended_default_symbols[] = {
+    { SDLK_LEFT_TAB, SDL_SCANCODE_TAB },
+    { SDLK_MULTI_KEY_COMPOSE, SDL_SCANCODE_APPLICATION }, // Sun keyboards
+    { SDLK_LMETA, SDL_SCANCODE_LGUI },
+    { SDLK_RMETA, SDL_SCANCODE_RGUI },
+    { SDLK_RHYPER, SDL_SCANCODE_APPLICATION }
+};
+
 static SDL_Keycode SDL_GetDefaultKeyFromScancode(SDL_Scancode scancode, SDL_Keymod modstate)
 {
     if (((int)scancode) < SDL_SCANCODE_UNKNOWN || scancode >= SDL_SCANCODE_COUNT) {
@@ -600,6 +612,16 @@ static SDL_Scancode SDL_GetDefaultScancodeFromKey(SDL_Keycode key, SDL_Keymod *m
         return SDL_SCANCODE_UNKNOWN;
     }
 
+    if (key & SDLK_EXTENDED_MASK) {
+        for (int i = 0; i < SDL_arraysize(extended_default_symbols); ++i) {
+            if (extended_default_symbols[i].keycode == key) {
+                return extended_default_symbols[i].scancode;
+            }
+        }
+
+        return SDL_SCANCODE_UNKNOWN;
+    }
+
     if (key & SDLK_SCANCODE_MASK) {
         return (SDL_Scancode)(key & ~SDLK_SCANCODE_MASK);
     }
@@ -932,6 +954,16 @@ static const char *SDL_scancode_names[SDL_SCANCODE_COUNT] =
     /* 290 */ "EndCall",
 };
 
+static const char *SDL_extended_key_names[] = {
+    "LeftTab",         /* 0x01 SDLK_LEFT_TAB */
+    "Level5Shift",     /* 0x02 SDLK_LEVEL5_SHIFT */
+    "MultiKeyCompose", /* 0x03 SDLK_MULTI_KEY_COMPOSE */
+    "Left Meta",       /* 0x04 SDLK_LMETA */
+    "Right Meta",      /* 0x05 SDLK_RMETA */
+    "Left Hyper",      /* 0x06 SDLK_LHYPER */
+    "Right Hyper"      /* 0x07 SDLK_RHYPER */
+};
+
 bool SDL_SetScancodeName(SDL_Scancode scancode, const char *name)
 {
     if (((int)scancode) < SDL_SCANCODE_UNKNOWN || scancode >= SDL_SCANCODE_COUNT) {
@@ -990,6 +1022,17 @@ const char *SDL_GetKeyName(SDL_Keycode key)
         return SDL_GetScancodeName((SDL_Scancode)(key & ~SDLK_SCANCODE_MASK));
     }
 
+    if (key & SDLK_EXTENDED_MASK) {
+        const SDL_Keycode idx = (key & ~SDLK_EXTENDED_MASK);
+        if (idx > 0 && (idx - 1) < SDL_arraysize(SDL_extended_key_names)) {
+            return SDL_extended_key_names[idx - 1];
+        }
+
+        // Key out of name index bounds.
+        SDL_InvalidParamError("key");
+        return "";
+    }
+
     switch (key) {
     case SDLK_RETURN:
         return SDL_GetScancodeName(SDL_SCANCODE_RETURN);
@@ -1087,5 +1130,12 @@ SDL_Keycode SDL_GetKeyFromName(const char *name)
         return key;
     }
 
+    // Check the extended key names
+    for (SDL_Keycode i = 0; i < SDL_arraysize(SDL_extended_key_names); ++i) {
+        if (SDL_strcasecmp(name, SDL_extended_key_names[i]) == 0) {
+            return (i + 1) | SDLK_EXTENDED_MASK;
+        }
+    }
+
     return SDL_GetKeyFromScancode(SDL_GetScancodeFromName(name), SDL_KMOD_NONE, false);
 }
diff --git a/src/events/SDL_keysym_to_keycode.c b/src/events/SDL_keysym_to_keycode.c
new file mode 100644
index 0000000000000..7cbfcb62fecec
--- /dev/null
+++ b/src/events/SDL_keysym_to_keycode.c
@@ -0,0 +1,68 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#if defined(SDL_VIDEO_DRIVER_WAYLAND) || defined(SDL_VIDEO_DRIVER_X11)
+
+#include "SDL_keyboard_c.h"
+#include "SDL_keysym_to_scancode_c.h"
+#include "imKStoUCS.h"
+
+
+// Extended key code mappings
+static const struct
+{
+    Uint32 keysym;
+    SDL_Keycode keycode;
+} keysym_to_keycode_table[] = {
+    { 0xfe03, SDLK_MODE }, // XK_ISO_Level3_Shift
+    { 0xfe11, SDLK_LEVEL5_SHIFT }, // XK_ISO_Level5_Shift
+    { 0xfe20, SDLK_LEFT_TAB }, // XK_ISO_Left_Tab
+    { 0xff20, SDLK_MULTI_KEY_COMPOSE }, // XK_Multi_key
+    { 0xffe7, SDLK_LMETA }, // XK_Meta_L
+    { 0xffe8, SDLK_RMETA }, // XK_Meta_R
+    { 0xffed, SDLK_LHYPER }, // XK_Hyper_L
+    { 0xffee, SDLK_RHYPER }, // XK_Hyper_R
+};
+
+SDL_Keycode SDL_GetKeyCodeFromKeySym(Uint32 keysym, Uint32 keycode, SDL_Keymod modifiers)
+{
+    SDL_Keycode sdl_keycode = SDL_KeySymToUcs4(keysym);
+
+    if (!sdl_keycode) {
+        for (int i = 0; i < SDL_arraysize(keysym_to_keycode_table); ++i) {
+            if (keysym == keysym_to_keycode_table[i].keysym) {
+                return keysym_to_keycode_table[i].keycode;
+            }
+        }
+    }
+
+    if (!sdl_keycode) {
+        const SDL_Scancode scancode = SDL_GetScancodeFromKeySym(keysym, keycode);
+        if (scancode != SDL_SCANCODE_UNKNOWN) {
+            sdl_keycode = SDL_GetKeymapKeycode(NULL, scancode, modifiers);
+        }
+    }
+
+    return sdl_keycode;
+}
+
+#endif // SDL_VIDEO_DRIVER_WAYLAND || SDL_VIDEO_DRIVER_X11
diff --git a/src/events/SDL_keysym_to_keycode_c.h b/src/events/SDL_keysym_to_keycode_c.h
new file mode 100644
index 0000000000000..2321d2022b768
--- /dev/null
+++ b/src/events/SDL_keysym_to_keycode_c.h
@@ -0,0 +1,28 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_keysym_to_keycode_c_h_
+#define SDL_keysym_to_keycode_c_h_
+
+// Convert a keysym to an SDL key code
+extern SDL_Keycode SDL_GetKeyCodeFromKeySym(Uint32 keysym, Uint32 keycode, SDL_Keymod modifiers);
+
+#endif // SDL_keysym_to_scancode_c_h_
diff --git a/src/events/SDL_keysym_to_scancode.c b/src/events/SDL_keysym_to_scancode.c
index 8067e05c59b54..8d43ca3509e6c 100644
--- a/src/events/SDL_keysym_to_scancode.c
+++ b/src/events/SDL_keysym_to_scancode.c
@@ -45,6 +45,7 @@ static const struct {
     { 0xFF62, SDL_SCANCODE_EXECUTE },  // XK_Execute
     { 0xFFEE, SDL_SCANCODE_APPLICATION },  // XK_Hyper_R
     { 0xFE03, SDL_SCANCODE_RALT },  // XK_ISO_Level3_Shift
+    { 0xFE20, SDL_SCANCODE_TAB },  // XK_ISO_Left_Tab
     { 0xFFEB, SDL_SCANCODE_LGUI },  // XK_Super_L
     { 0xFFEC, SDL_SCANCODE_RGUI },  // XK_Super_R
     { 0xFF7E, SDL_SCANCODE_MODE },  // XK_Mode_switch
diff --git a/src/events/SDL_keysym_to_scancode_c.h b/src/events/SDL_keysym_to_scancode_c.h
index 8d0e2143af261..2e890fda81755 100644
--- a/src/events/SDL_keysym_to_scancode_c.h
+++ b/src/events/SDL_keysym_to_scancode_c.h
@@ -25,4 +25,7 @@
 // This function only correctly maps letters and numbers for keyboards in US QWERTY layout
 extern SDL_Scancode SDL_GetScancodeFromKeySym(Uint32 keysym, Uint32 keycode);
 
+// Convert a keysym to an extended SDL key code
+extern SDL_Keycode SDL_GetExtendedKeyCodeFromKeySym(Uint32 keysym);
+
 #endif // SDL_keysym_to_scancode_c_h_
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index b92c84a12eba1..36e2933970763 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -26,6 +26,7 @@
 #include "../../core/unix/SDL_poll.h"
 #include "../../events/SDL_events_c.h"
 #include "../../events/SDL_scancode_tables_c.h"
+#include "../../events/SDL_keysym_to_keycode_c.h"
 #include "../../core/linux/SDL_system_theme.h"
 #include "../SDL_sysvideo.h"
 
@@ -68,16 +69,14 @@
 // Weston uses a ratio of 10 units per scroll tick
 #define WAYLAND_WHEEL_AXIS_UNIT 10
 
-// "Mod5" is typically level 3 shift, which SDL calls SDL_KMOD_MODE (AltGr).
-#ifndef XKB_MOD_NAME_MOD5
-#define XKB_MOD_NAME_MOD5 "Mod5"
-#endif
-
-// "Mod3" is typically level 5 shift, but is often remapped.
 #ifndef XKB_MOD_NAME_MOD3
 #define XKB_MOD_NAME_MOD3 "Mod3"
 #endif
 
+#ifndef XKB_MOD_NAME_MOD5
+#define XKB_MOD_NAME_MOD5 "Mod5"
+#endif
+
 // Keyboard and mouse names to match XWayland
 #define WAYLAND_DEFAULT_KEYBOARD_NAME "Virtual core keyboard"
 #define WAYLAND_DEFAULT_POINTER_NAME "Virtual core pointer"
@@ -1278,40 +1277,9 @@ static void Wayland_keymap_iter(struct xkb_keymap *keymap, xkb_keycode_t key, vo
     }
 
     if (WAYLAND_xkb_state_key_get_syms(sdlKeymap->state, key, &syms) > 0) {
-        uint32_t keycode = SDL_KeySymToUcs4(syms[0]);
-        bool key_is_unknown = false;
+        SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(syms[0], key, sdlKeymap->modstate);
 
         if (!keycode) {
-            switch (syms[0]) {
-            // The default SDL scancode table sets this to right alt instead of AltGr/Mode, so handle it separately.
-            case XKB_KEY_ISO_Level3_Shift:
-                keycode = SDLK_MODE;
-                break;
-
-            /* The default SDL scancode table sets Meta L/R to the GUI keys, and Hyper R to app menu, which is
-             * correct as far as physical key placement goes, but these keys are functionally distinct from the
-             * default keycodes SDL returns for the scancodes, so they are set to unknown.
-             *
-             * SDL has no scancode mapping for Hyper L or Level 5 Shift.
-             */
-            case XKB_KEY_Meta_L:
-            case XKB_KEY_Meta_R:
-            case XKB_KEY_Hyper_L:
-            case XKB_KEY_Hyper_R:
-            case XKB_KEY_ISO_Level5_Shift:
-                keycode = SDLK_UNKNOWN;
-                key_is_unknown = true;
-                break;
-
-            default:
-            {
-                const SDL_Scancode sc = SDL_GetScancodeFromKeySym(syms[0], key);
-                keycode = SDL_GetKeymapKeycode(NULL, sc, sdlKeymap->modstate);
-            } break;
-            }
-        }
-
-        if (!keycode && !key_is_unknown) {
             switch (scancode) {
             case SDL_SCANCODE_RETURN:
                 keycode = SDLK_RETURN;
@@ -1322,9 +1290,6 @@ static void Wayland_keymap_iter(struct xkb_keymap *keymap, xkb_keycode_t key, vo
             case SDL_SCANCODE_BACKSPACE:
                 keycode = SDLK_BACKSPACE;
                 break;
-            case SDL_SCANCODE_TAB:
-                keycode = SDLK_TAB;
-                break;
             case SDL_SCANCODE_DELETE:
                 keycode = SDLK_DELETE;
                 break;
@@ -1520,17 +1485,20 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard,
  * Virtual keyboards can have arbitrary layouts, arbitrary scancodes/keycodes, etc...
  * Key presses from these devices must be looked up by their keysym value.
  */
-static void Wayland_get_scancode_from_key(struct SDL_WaylandInput *input, uint32_t keycode, SDL_Scancode *scancode)
+static SDL_Scancode Wayland_GetScancodeForKey(struct SDL_WaylandInput *input, uint32_t key)
 {
-    const xkb_keysym_t *syms;
+    SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN;
 
     if (!input->keyboard_is_virtual) {
-        *scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, keycode);
+        scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, key);
     } else {
-        if (WAYLAND_xkb_keymap_key_get_syms_by_level(input->xkb.keymap, keycode + 8, input->xkb.current_group, 0, &syms) > 0) {
-            *scancode = SDL_GetScancodeFromKeySym(syms[0], keycode + 8);
+        const xkb_keysym_t *syms;
+        if (WAYLAND_xkb_keymap_key_get_syms_by_level(input->xkb.keymap, key + 8, input->xkb.current_group, 0, &syms) > 0) {
+            scancode = SDL_GetScancodeFromKeySym(syms[0], key);
         }
     }
+
+    return scancode;
 }
 
 static void Wayland_ReconcileModifiers(struct SDL_WaylandInput *input, bool key_pressed)
@@ -1573,6 +1541,9 @@ static void Wayland_ReconcileModifiers(struct SDL_WaylandInput *input, bool key_
             input->pressed_modifiers &= ~SDL_KMOD_GUI;
         }
 
+        /* Note: This is not backwards: in the default keymap, Mod5 is typically
+         *       level 3 shift, and Mod3 is typically level 5 shift.
+         */
         if (input->xkb.wl_pressed_modifiers & input->xkb.idx_mod3) {
             if (!(input->pressed_modifiers & SDL_KMOD_LEVEL5)) {
                 input->pressed_modifiers |= SDL_KMOD_LEVEL5;
@@ -1641,9 +1612,7 @@ static void Wayland_ReconcileModifiers(struct SDL_WaylandInput *input, bool key_
         input->locked_modifiers &= ~SDL_KMOD_GUI;
     }
 
-    /* The Mod3 modifier corresponds to no particular SDL keycode, so it is
-     * only activated by the backend modifier callback.
-     */
+    // As above, this is correct: Mod3 is typically level 5 shift, and Mod5 is typically level 3 shift.
     if (input->xkb.wl_locked_modifiers & input->xkb.idx_mod3) {
         input->locked_modifiers |= SDL_KMOD_LEVEL5;
     } else {
@@ -1711,6 +1680,9 @@ static void Wayland_HandleModifierKeys(struct SDL_WaylandInput *input, SDL_Scanc
     case SDLK_MODE:
         mod = SDL_KMOD_MODE;
         break;
+    case SDLK_LEVEL5_SHIFT:
+        mod = SDL_KMOD_LEVEL5;
+        break;
     default:
         return;
     }
@@ -1759,9 +1731,7 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
     window->last_focus_event_time_ns = timestamp;
 
     wl_array_for_each (key, keys) {
-        SDL_Scancode scancode;
-
-        Wayland_get_scancode_from_key(input, *key, &scancode);
+        const SDL_Scancode scancode = Wayland_GetScancodeForKey(input, *key);
         const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false);
 
         switch (keycode) {
@@ -1774,6 +1744,7 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
         case SDLK_LGUI:
         case SDLK_RGUI:
         case SDLK_MODE:
+        case SDLK_LEVEL5_SHIFT:
             Wayland_HandleModifierKeys(input, scancode, true);
             SDL_SendKeyboardKeyIgnoreModifiers(timestamp, input->keyboard_id, *key, scancode, true);
             break;
@@ -1883,7 +1854,6 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard,
 {
     struct SDL_WaylandInput *input = data;
     enum wl_keyboard_key_state state = state_w;
-    SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN;
     char text[8];
     bool has_text = false;
     bool handled_by_ime = false;
@@ -1909,10 +1879,11 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard,
         keyboard_input_get_text(text, input, key, false, &handled_by_ime);
     }
 
-    Wayland_get_scancode_from_key(input, key, &scancode);
+    const SDL_Scancode scancode = Wayland_GetScancodeForKey(input, key);
     Wayland_HandleModifierKeys(input, scancode, state == WL_KEYBOARD_KEY_STATE_PRESSED);
     Uint64 timestamp = Wayland_GetKeyboardTimestamp(input, time);
-    SDL_SendKeyboardKeyIgnoreModifiers(timestamp, input->keyboard_id, key, scancode, (state == WL_KEYBOARD_KEY_STATE_PRESSED));
+
+    SDL_SendKeyboardKeyIgnoreModifiers(timestamp, input->keyboard_id, key, scancode, state == WL_KEYBOARD_KEY_STATE_PRESSED);
 
     if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
         if (has_text && !(SDL_GetModState() & SDL_KMOD_CTRL)) {
diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 8cdce7184ab87..3641cf9c35db0 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -246,79 +246,83 @@ static void X11_HandleGenericEvent(SDL_VideoDevice *_this, XEvent *xev)
 }
 #endif // SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS
 
-static void X11_ReconcileModifiers(SDL_VideoData *viddata)
+static void X11_UpdateSystemKeyModifiers(SDL_VideoData *viddata)
 {
     Window junk_window;
     int x, y;
-    Uint32 xk_modifiers = 0;
 
-    X11_XQueryPointer(viddata->display, DefaultRootWindow(viddata->display), &junk_window, &junk_window, &x, &y, &x, &y, &xk_modifiers);
+    X11_XQueryPointer(viddata->display, DefaultRootWindow(viddata->display), &junk_window, &junk_window, &x, &y, &x, &y, &viddata->xkb.xkb_modifiers);
+}
+
+static void X11_ReconcileModifiers(SDL_VideoData *viddata)
+{
+    const Uint32 xk_modifiers = viddata->xkb.xkb_modifiers;
 
     /* If a modifier was activated by a keypress, it will be tied to the
      * specific left/right key that initiated it. Otherwise, the ambiguous
      * left/right combo is used.
      */
     if (xk_modifiers & ShiftMask) {
-        if (!(viddata->xkb.active_modifiers & SDL_KMOD_SHIFT)) {
-            viddata->xkb.active_modifiers |= SDL_KMOD_SHIFT;
+        if (!(viddata->xkb.sdl_modifiers & SDL_KMOD_SHIFT)) {
+            viddata->xkb.sdl_modifiers |= SDL_KMOD_SHIFT;
         }
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_SHIFT;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_SHIFT;
     }
 
     if (xk_modifiers & ControlMask) {
-        if (!(viddata->xkb.active_modifiers & SDL_KMOD_CTRL)) {
-            viddata->xkb.active_modifiers |= SDL_KMOD_CTRL;
+        if (!(viddata->xkb.sdl_modifiers & SDL_KMOD_CTRL)) {
+            viddata->xkb.sdl_modifiers |= SDL_KMOD_CTRL;
         }
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_CTRL;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_CTRL;
     }
 
     // Mod1 is used for the Alt keys
     if (xk_modifiers & Mod1Mask) {
-        if (!(viddata->xkb.active_modifiers & SDL_KMOD_ALT)) {
-            viddata->xkb.active_modifiers |= SDL_KMOD_ALT;
+        if (!(viddata->xkb.sdl_modifiers & SDL_KMOD_ALT)) {
+            viddata->xkb.sdl_modifiers |= SDL_KMOD_ALT;
         }
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_ALT;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_ALT;
     }
 
     // Mod4 is used for the Super (aka GUI/Logo) keys.
     if (xk_modifiers & Mod4Mask) {
-        if (!(viddata->xkb.active_modifiers & SDL_KMOD_GUI)) {
-            viddata->xkb.active_modifiers |= SDL_KMOD_GUI;
+        if (!(viddata->xkb.sdl_modifiers & SDL_KMOD_GUI)) {
+            viddata->xkb.sdl_modifiers |= SDL_KMOD_GUI;
         }
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_GUI;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_GUI;
     }
 
     // Mod3 is typically Level 5 shift.
     if (xk_modifiers & Mod3Mask) {
-        viddata->xkb.active_modifiers |= SDL_KMOD_LEVEL5;
+        viddata->xkb.sdl_modifiers |= SDL_KMOD_LEVEL5;
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_LEVEL5;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_LEVEL5;
     }
 
     // Mod5 is typically Level 3 shift (aka AltGr).
     if (xk_modifiers & Mod5Mask) {
-        viddata->xkb.active_modifiers |= SDL_KMOD_MODE;
+        viddata->xkb.sdl_modifiers |= SDL_KMOD_MODE;
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_MODE;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_MODE;
     }
 
     if (xk_modifiers & viddata->xkb.numlock_mask) {
-        viddata->xkb.active_modifiers |= SDL_KMOD_NUM;
+        viddata->xkb.sdl_modifiers |= SDL_KMOD_NUM;
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_NUM;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_NUM;
     }
 
     if (xk_modifiers & viddata->xkb.scrolllock_mask) {
-        viddata->xkb.active_modifiers |= SDL_KMOD_SCROLL;
+        viddata->xkb.sdl_modifiers |= SDL_KMOD_SCROLL;
     } else {
-        viddata->xkb.active_modifiers &= ~SDL_KMOD_SCROLL;
+        viddata->xkb.sdl_modifiers &= ~SDL_KMOD_SCROLL;
     }
 
-    SDL_SetModState(viddata->xkb.active_modifiers);
+    SDL_SetModState(viddata->xkb.sdl_modifiers);
 }
 
 static void X11_HandleModifierKeys(SDL_VideoData *viddata, SDL_Scancode scancode, bool pressed, bool reconcile)
@@ -360,14 +364,17 @@ static void X11_HandleModifierKeys(SDL_VideoData *viddata, SDL_Scancode scancode
     case SDLK_MODE:
         mod = SDL_KMOD_MODE;
         break;
+    case SDLK_LEVEL5_SHIFT:
+        mod = SDL_KMOD_LEVEL5;
+        break;
     default:
-        return;
+        break;
     }
 
     if (pressed) {
-        viddata->xkb.active_modifiers |= mod;
+        viddata->xkb.sdl_modifiers |= mod;
     } else {
-        viddata->xkb.active_modifiers &= ~mod;
+        viddata->xkb.sdl_modifiers &= ~mod;
     }
 
     if (reconcile) {
@@ -403,6 +410,7 @@ void X11_ReconcileKeyboardState(SDL_VideoDevice *_this)
             case SDLK_LGUI:
             case SDLK_RGUI:
             case SDLK_MODE:
+            case SDLK_LEVEL5_SHIFT:
                 X11_HandleModifierKeys(videodata, scancode, true, false);
                 SDL_SendKeyboardKeyIgnoreModifiers(0, SDL_GLOBAL_KEYBOARD_ID, keycode, scancode, true);
                 break;
@@ -415,6 +423,7 @@ void X11_ReconcileKeyboardState(SDL_VideoDevice *_this)
         }
     }
 
+    X11_UpdateSystemKeyModifiers(videodata);
     X11_ReconcileModifiers(videodata);
 }
 
@@ -901,6 +910,7 @@ void X11_HandleKeyEvent(SDL_VideoDevice *_this, SDL_WindowData *windowdata, SDL_
 #endif // DEBUG SCANCODES
 
     text[0] = '\0';
+    X11_UpdateSystemKeyModifiers(videodata);
 
     if (SDL_TextInputActive(windowdata->window)) {
 #if defined(HAVE_IBUS_IBUS_H) || defined(HAVE_FCITX)
@@ -924,13 +934,12 @@ void X11_HandleKeyEvent(SDL_VideoDevice *_this, SDL_WindowData *windowdata, SDL_
             SDL_Scancode scancode = videodata->key_layout[orig_keycode];
             videodata->filter_code = orig_keycode;
             videodata->filter_time = xevent->xkey.time;
-
             if (orig_event_type == KeyPress) {
                 X11_HandleModifierKeys(videodata, scancode, true, true);
                 SDL_SendKeyboardKeyIgnoreModifiers(timestamp, keyboardID, orig_keycode, scancode, true);
             } else {
                 X11_HandleModifierKeys(videodata, scancode, false, true);
-                SDL_SendKeyboardKey(timestamp, keyboardID, orig_keycode, scancode, false);
+                SDL_SendKeyboardKeyIgnoreModifiers(timestamp, keyboardID, orig_keycode, scancode, false);
             }
 #endif
             return;
@@ -1183,6 +1192,7 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
                     if (X11_XkbGetState(videodata->display, XkbUseCoreKbd, &state) == Success) {
                         if (state.group != videodata->xkb.current_group) {
                             // Only rebuild the keymap if the layout has changed.
+                            videodata->xkb.current_group = state.group;
                             X11_UpdateKeymap(_this, true);
                         }
                     }
diff --git a/src/video/x11/SDL_x11keyboard.c b/src/video/x11/SDL_x11keyboard.c
index f192d77b8b523..d1d60f30c546f 100644
--- a/src/video/x11/SDL_x11keyboard.c
+++ b/src/video/x11/SDL_x11keyboard.c
@@ -32,6 +32,7 @@
 
 #include "../../events/imKStoUCS.h"
 #include "../../events/SDL_keysym_to_scancode_c.h"
+#include "../../events/SDL_keysym_to_keycode_c.h"
 
 #ifdef X_HAVE_UTF8_STRING
 #include <locale.h>
@@ -280,7 +281,7 @@ bool X11_InitKeyboard(SDL_VideoDevice *_this)
             if (scancode == data->key_layout[i]) {
                 continue;
             }
-            if ((SDL_GetKeymapKeycode(NULL, scancode, SDL_KMOD_NONE) & SDLK_SCANCODE_MASK) && X11_ScancodeIsRemappable(scancode)) {
+            if ((SDL_GetKeymapKeycode(NULL, scancode, SDL_KMOD_NONE) & (SDLK_SCANCODE_MASK | SDLK_EXTENDED_MASK)) && X11_ScancodeIsRemappable(scancode)) {
                 // Not a character key and the scancode is safe to remap
 #ifdef DEBUG_KEYBOARD
                 SDL_Log("Changing scancode, was %d (%s), now %d (%s)\n", data->key_layout[i], SDL_GetScancodeName(data->key_layout[i]), scancode, SDL_GetScancodeName(scancode));
@@ -402,11 +403,8 @@ void X11_UpdateKeymap(SDL_VideoDevice *_this, bool send_event)
     };
 
     SDL_VideoData *data = _this->internal;
-    int i;
     SDL_Scancode scancode;
-    SDL_Keymap *keymap;
-
-    keymap = SDL_CreateKeymap();
+    SDL_Keymap *keymap = SDL_CreateKeymap();
 
 #ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
     if (data->xkb.desc_ptr) {
@@ -420,9 +418,7 @@ void X11_UpdateKeymap(SDL_VideoDevice *_this, bool send_event)
 #endif
 
     for (int m = 0; m < SDL_arraysize(keymod_masks); ++m) {
-        for (i = 0; i < SDL_arraysize(data->key_layout); i++) {
-            SDL_Keycode keycode;
-
+        for (int i = 0; i < SDL_arraysize(data->key_layout); ++i) {
             // Make sure this is a valid scancode
             scancode = data->key_layout[i];
             if (scancode == SDL_SCANCODE_UNKNOWN) {
@@ -430,40 +426,32 @@ void X11_UpdateKeymap(SDL_VideoDevice *_this, bool send_event)
             }
 
             const KeySym keysym = X11_KeyCodeToSym(_this, i, data->xkb.current_group, keymod_masks[m].xkb_mask);
-            bool key_is_unknown = false;
 
-            switch (keysym) {
-            // The default SDL scancode table sets this to right alt instead of AltGr/Mode, so handle it separately.
-            case XK_ISO_Level3_Shift:
-                keycode = SDLK_MODE;
-                break;
-
-            /* The default SDL scancode table sets Meta L/R to the GUI keys, and Hyper R to app menu, which is
-             * correct as far as physical key placement goes, but these keys are functionally distinct from the
-             * default keycodes SDL returns for the scancodes, so they are set to unknown.
-             *
-             * SDL has no scancode mapping for Hyper L or Level 5 Shift, and they are usually mapped to something
-             * else, like Caps Lock, so just pass through the unknown keycode.
-             */
-            case XK_Meta_L:
-            case XK_Meta_R:
-            case XK_Hyper_L:
-            case XK_Hyper_R:
-            case XK_ISO_Level5_Shift:
-                keycode = SDLK_UNKNOWN;
-                key_is_unknown = true;
-                break;
-
-            default:
-                keycode = SDL_KeySymToUcs4(keysym);
-                break;
-            }
+            if (keysym != NoSymbol) {
+                SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(keysym, i, keymod_masks[m].sdl_mask);
+
+                if (!keycode) {
+                    switch (scancode) {
+                    case SDL_SCANCODE_RETURN:
+                        keycode = SDLK_RETURN;
+                        break;
+                    case SDL_SCANCODE_ESCAPE:
+              

(Patch may be truncated, please check the link at the top of this post.)