SDL: wayland: Add support for Mod3 and more esoteric Xkb configurations

From 73ee99978d54006ce94230cbfcbc59ce29ba5a9b Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 25 Dec 2024 13:53:31 -0500
Subject: [PATCH] wayland: Add support for Mod3 and more esoteric Xkb
 configurations

Adds support for Mod3, which is usually Level 5 shift, but can vary, as well as not altering the functionality of the more esoteric modifier keys, such as meta and hyper.
---
 include/SDL3/SDL_keycode.h              |   1 +
 src/events/SDL_keymap.c                 |   4 +-
 src/test/SDL_test_common.c              |   4 +
 src/video/wayland/SDL_waylandevents.c   | 151 +++++++++++++++++-------
 src/video/wayland/SDL_waylandevents_c.h |   3 +-
 5 files changed, 118 insertions(+), 45 deletions(-)

diff --git a/include/SDL3/SDL_keycode.h b/include/SDL3/SDL_keycode.h
index e4d6024eb2817..cb155a5a1c062 100644
--- a/include/SDL3/SDL_keycode.h
+++ b/include/SDL3/SDL_keycode.h
@@ -313,6 +313,7 @@ typedef Uint16 SDL_Keymod;
 #define SDL_KMOD_NONE   0x0000u /**< no modifier is applicable. */
 #define SDL_KMOD_LSHIFT 0x0001u /**< the left Shift key is down. */
 #define SDL_KMOD_RSHIFT 0x0002u /**< the right Shift key is down. */
+#define SDL_KMOD_LEVEL5 0x0004u /**< the Level 5 Shift key is down. */
 #define SDL_KMOD_LCTRL  0x0040u /**< the left Ctrl (Control) key is down. */
 #define SDL_KMOD_RCTRL  0x0080u /**< the right Ctrl (Control) key is down. */
 #define SDL_KMOD_LALT   0x0100u /**< the left Alt key is down. */
diff --git a/src/events/SDL_keymap.c b/src/events/SDL_keymap.c
index 96896b2c08dd4..1aeb57c6c51ef 100644
--- a/src/events/SDL_keymap.c
+++ b/src/events/SDL_keymap.c
@@ -50,8 +50,8 @@ SDL_Keymap *SDL_CreateKeymap(void)
 
 static SDL_Keymod NormalizeModifierStateForKeymap(SDL_Keymod modstate)
 {
-    // The modifiers that affect the keymap are: SHIFT, CAPS, ALT, and MODE
-    modstate &= (SDL_KMOD_SHIFT | SDL_KMOD_CAPS | SDL_KMOD_ALT | SDL_KMOD_MODE);
+    // The modifiers that affect the keymap are: SHIFT, CAPS, ALT, MODE, and LEVEL5
+    modstate &= (SDL_KMOD_SHIFT | SDL_KMOD_CAPS | SDL_KMOD_ALT | SDL_KMOD_MODE | SDL_KMOD_LEVEL5);
 
     // If either right or left Shift are set, set both in the output
     if (modstate & SDL_KMOD_SHIFT) {
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index dd60884e0076c..335d5595dd28d 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -958,6 +958,9 @@ static void SDLTest_PrintModStateFlag(char *text, size_t maxlen, SDL_Keymod flag
     case SDL_KMOD_RSHIFT:
         SDL_snprintfcat(text, maxlen, "RSHIFT");
         break;
+    case SDL_KMOD_LEVEL5:
+        SDL_snprintfcat(text, maxlen, "LEVEL5");
+        break;
     case SDL_KMOD_LCTRL:
         SDL_snprintfcat(text, maxlen, "LCTRL");
         break;
@@ -999,6 +1002,7 @@ static void SDLTest_PrintModState(char *text, size_t maxlen, SDL_Keymod keymod)
     const SDL_Keymod kmod_flags[] = {
         SDL_KMOD_LSHIFT,
         SDL_KMOD_RSHIFT,
+        SDL_KMOD_LEVEL5,
         SDL_KMOD_LCTRL,
         SDL_KMOD_RCTRL,
         SDL_KMOD_LALT,
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 6784bf0803995..b92c84a12eba1 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -68,9 +68,14 @@
 // Weston uses a ratio of 10 units per scroll tick
 #define WAYLAND_WHEEL_AXIS_UNIT 10
 
-// xkbcommon as of 1.4.1 doesn't have a name macro for the mode key
-#ifndef XKB_MOD_NAME_MODE
-#define XKB_MOD_NAME_MODE "Mod5"
+// "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
 
 // Keyboard and mouse names to match XWayland
@@ -1267,28 +1272,46 @@ static void Wayland_keymap_iter(struct xkb_keymap *keymap, xkb_keycode_t key, vo
 {
     Wayland_Keymap *sdlKeymap = (Wayland_Keymap *)data;
     const xkb_keysym_t *syms;
-    SDL_Scancode scancode;
-
-    scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8));
+    const SDL_Scancode scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, (key - 8));
     if (scancode == SDL_SCANCODE_UNKNOWN) {
         return;
     }
 
     if (WAYLAND_xkb_state_key_get_syms(sdlKeymap->state, key, &syms) > 0) {
         uint32_t keycode = SDL_KeySymToUcs4(syms[0]);
+        bool key_is_unknown = false;
 
         if (!keycode) {
-            const SDL_Scancode sc = SDL_GetScancodeFromKeySym(syms[0], key);
+            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;
 
-            // Note: The default SDL scancode table sets this to right alt instead of AltGr/Mode, so handle it separately.
-            if (syms[0] != XKB_KEY_ISO_Level3_Shift) {
+            default:
+            {
+                const SDL_Scancode sc = SDL_GetScancodeFromKeySym(syms[0], key);
                 keycode = SDL_GetKeymapKeycode(NULL, sc, sdlKeymap->modstate);
-            } else {
-                keycode = SDLK_MODE;
+            } break;
             }
         }
 
-        if (!keycode) {
+        if (!keycode && !key_is_unknown) {
             switch (scancode) {
             case SDL_SCANCODE_RETURN:
                 keycode = SDLK_RETURN;
@@ -1326,10 +1349,18 @@ static void Wayland_UpdateKeymap(struct SDL_WaylandInput *input)
         { SDL_KMOD_SHIFT, input->xkb.idx_shift },
         { SDL_KMOD_CAPS, input->xkb.idx_caps },
         { SDL_KMOD_SHIFT | SDL_KMOD_CAPS, input->xkb.idx_shift | input->xkb.idx_caps },
-        { SDL_KMOD_MODE, input->xkb.idx_mode },
-        { SDL_KMOD_MODE | SDL_KMOD_SHIFT, input->xkb.idx_mode | input->xkb.idx_shift },
-        { SDL_KMOD_MODE | SDL_KMOD_CAPS, input->xkb.idx_mode | input->xkb.idx_caps },
-        { SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, input->xkb.idx_mode | input->xkb.idx_shift | input->xkb.idx_caps }
+        { SDL_KMOD_MODE, input->xkb.idx_mod5 },
+        { SDL_KMOD_MODE | SDL_KMOD_SHIFT, input->xkb.idx_mod5 | input->xkb.idx_shift },
+        { SDL_KMOD_MODE | SDL_KMOD_CAPS, input->xkb.idx_mod5 | input->xkb.idx_caps },
+        { SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, input->xkb.idx_mod5 | input->xkb.idx_shift | input->xkb.idx_caps },
+        { SDL_KMOD_LEVEL5, input->xkb.idx_mod3 },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_SHIFT, input->xkb.idx_mod3 | input->xkb.idx_shift },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_CAPS, input->xkb.idx_mod3 | input->xkb.idx_caps },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, input->xkb.idx_mod3 | input->xkb.idx_shift | input->xkb.idx_caps },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE, input->xkb.idx_mod3 | input->xkb.idx_mod5 },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_SHIFT, input->xkb.idx_mod3 | input->xkb.idx_mod5 | input->xkb.idx_shift },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_CAPS, input->xkb.idx_mod3 | input->xkb.idx_mod5 | input->xkb.idx_caps },
+        { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, input->xkb.idx_mod3 | input->xkb.idx_mod5 | input->xkb.idx_shift | input->xkb.idx_caps },
     };
 
     if (!input->keyboard_is_virtual) {
@@ -1350,7 +1381,7 @@ static void Wayland_UpdateKeymap(struct SDL_WaylandInput *input)
         for (int i = 0; i < SDL_arraysize(keymod_masks); ++i) {
             keymap.modstate = keymod_masks[i].sdl_mask;
             WAYLAND_xkb_state_update_mask(keymap.state,
-                                          keymod_masks[i].xkb_mask & (input->xkb.idx_shift | input->xkb.idx_mode), 0, keymod_masks[i].xkb_mask & input->xkb.idx_caps,
+                                          keymod_masks[i].xkb_mask & (input->xkb.idx_shift | input->xkb.idx_mod5 | input->xkb.idx_mod3), 0, keymod_masks[i].xkb_mask & input->xkb.idx_caps,
                                           0, 0, input->xkb.current_group);
             WAYLAND_xkb_keymap_key_for_each(input->xkb.keymap,
                                             Wayland_keymap_iter,
@@ -1413,7 +1444,8 @@ static void keyboard_handle_keymap(void *data, struct wl_keyboard *keyboard,
     input->xkb.idx_ctrl = 1 << GET_MOD_INDEX(CTRL);
     input->xkb.idx_alt = 1 << GET_MOD_INDEX(ALT);
     input->xkb.idx_gui = 1 << GET_MOD_INDEX(LOGO);
-    input->xkb.idx_mode = 1 << GET_MOD_INDEX(MODE);
+    input->xkb.idx_mod3 = 1 << GET_MOD_INDEX(MOD3);
+    input->xkb.idx_mod5 = 1 << GET_MOD_INDEX(MOD5);
     input->xkb.idx_num = 1 << GET_MOD_INDEX(NUM);
     input->xkb.idx_caps = 1 << GET_MOD_INDEX(CAPS);
 #undef GET_MOD_INDEX
@@ -1488,59 +1520,77 @@ 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 SDL_Scancode Wayland_get_scancode_from_key(struct SDL_WaylandInput *input, uint32_t key)
+static void Wayland_get_scancode_from_key(struct SDL_WaylandInput *input, uint32_t keycode, SDL_Scancode *scancode)
 {
-    SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN;
+    const xkb_keysym_t *syms;
 
     if (!input->keyboard_is_virtual) {
-        scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, key - 8);
+        *scancode = SDL_GetScancodeFromTable(SDL_SCANCODE_TABLE_XFREE86_2, keycode);
     } else {
-        const xkb_keysym_t *syms;
-        if (WAYLAND_xkb_keymap_key_get_syms_by_level(input->xkb.keymap, key, input->xkb.current_group, 0, &syms) > 0) {
-            scancode = SDL_GetScancodeFromKeySym(syms[0], key);
+        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);
         }
     }
-
-    return scancode;
 }
 
-static void Wayland_ReconcileModifiers(struct SDL_WaylandInput *input)
+static void Wayland_ReconcileModifiers(struct SDL_WaylandInput *input, bool key_pressed)
 {
-    // Handle pressed modifiers for virtual keyboards that may not send keystrokes.
-    if (input->keyboard_is_virtual) {
+    /* Handle explicit pressed modifier state. This will correct the modifier state
+     * if common modifier keys were remapped and the modifiers presumed to be set
+     * during a key press event were incorrect, or if the modifier was set to the
+     * pressed state via means other than pressing the physical key.
+     */
+    if (!key_pressed) {
         if (input->xkb.wl_pressed_modifiers & input->xkb.idx_shift) {
-            input->pressed_modifiers |= SDL_KMOD_SHIFT;
+            if (!(input->pressed_modifiers & SDL_KMOD_SHIFT)) {
+                input->pressed_modifiers |= SDL_KMOD_SHIFT;
+            }
         } else {
             input->pressed_modifiers &= ~SDL_KMOD_SHIFT;
         }
 
         if (input->xkb.wl_pressed_modifiers & input->xkb.idx_ctrl) {
-            input->pressed_modifiers |= SDL_KMOD_CTRL;
+            if (!(input->pressed_modifiers & SDL_KMOD_CTRL)) {
+                input->pressed_modifiers |= SDL_KMOD_CTRL;
+            }
         } else {
             input->pressed_modifiers &= ~SDL_KMOD_CTRL;
         }
 
         if (input->xkb.wl_pressed_modifiers & input->xkb.idx_alt) {
-            input->pressed_modifiers |= SDL_KMOD_ALT;
+            if (!(input->pressed_modifiers & SDL_KMOD_ALT)) {
+                input->pressed_modifiers |= SDL_KMOD_ALT;
+            }
         } else {
             input->pressed_modifiers &= ~SDL_KMOD_ALT;
         }
 
         if (input->xkb.wl_pressed_modifiers & input->xkb.idx_gui) {
-            input->pressed_modifiers |= SDL_KMOD_GUI;
+            if (!(input->pressed_modifiers & SDL_KMOD_GUI)) {
+                input->pressed_modifiers |= SDL_KMOD_GUI;
+            }
         } else {
             input->pressed_modifiers &= ~SDL_KMOD_GUI;
         }
 
-        if (input->xkb.wl_pressed_modifiers & input->xkb.idx_mode) {
-            input->pressed_modifiers |= SDL_KMOD_MODE;
+        if (input->xkb.wl_pressed_modifiers & input->xkb.idx_mod3) {
+            if (!(input->pressed_modifiers & SDL_KMOD_LEVEL5)) {
+                input->pressed_modifiers |= SDL_KMOD_LEVEL5;
+            }
+        } else {
+            input->pressed_modifiers &= ~SDL_KMOD_LEVEL5;
+        }
+
+        if (input->xkb.wl_pressed_modifiers & input->xkb.idx_mod5) {
+            if (!(input->pressed_modifiers & SDL_KMOD_MODE)) {
+                input->pressed_modifiers |= SDL_KMOD_MODE;
+            }
         } else {
             input->pressed_modifiers &= ~SDL_KMOD_MODE;
         }
     }
 
-    /*
-     * If a latch or lock was activated by a keypress, the latch/lock will
+    /* If a latch or lock was activated by a keypress, the latch/lock will
      * be tied to the specific left/right key that initiated it. Otherwise,
      * the ambiguous left/right combo is used.
      *
@@ -1591,7 +1641,16 @@ static void Wayland_ReconcileModifiers(struct SDL_WaylandInput *input)
         input->locked_modifiers &= ~SDL_KMOD_GUI;
     }
 
-    if (input->xkb.wl_locked_modifiers & input->xkb.idx_mode) {
+    /* The Mod3 modifier corresponds to no particular SDL keycode, so it is
+     * only activated by the backend modifier callback.
+     */
+    if (input->xkb.wl_locked_modifiers & input->xkb.idx_mod3) {
+        input->locked_modifiers |= SDL_KMOD_LEVEL5;
+    } else {
+        input->locked_modifiers &= ~SDL_KMOD_LEVEL5;
+    }
+
+    if (input->xkb.wl_locked_modifiers & input->xkb.idx_mod5) {
         input->locked_modifiers |= SDL_KMOD_MODE;
     } else {
         input->locked_modifiers &= ~SDL_KMOD_MODE;
@@ -1618,6 +1677,12 @@ static void Wayland_HandleModifierKeys(struct SDL_WaylandInput *input, SDL_Scanc
     const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false);
     SDL_Keymod mod;
 
+    /* SDL clients expect modifier state to be activated at the same time as the
+     * source keypress, so we set pressed modifier state with the usual modifier
+     * keys here, as the explicit modifier event won't arrive until after the
+     * keypress event. If this is wrong, it will be corrected when the explicit
+     * modifier state is sent at a later time.
+     */
     switch (keycode) {
     case SDLK_LSHIFT:
         mod = SDL_KMOD_LSHIFT;
@@ -1656,7 +1721,7 @@ static void Wayland_HandleModifierKeys(struct SDL_WaylandInput *input, SDL_Scanc
         input->pressed_modifiers &= ~mod;
     }
 
-    Wayland_ReconcileModifiers(input);
+    Wayland_ReconcileModifiers(input, true);
 }
 
 static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
@@ -1694,7 +1759,9 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
     window->last_focus_event_time_ns = timestamp;
 
     wl_array_for_each (key, keys) {
-        const SDL_Scancode scancode = Wayland_get_scancode_from_key(input, *key + 8);
+        SDL_Scancode scancode;
+
+        Wayland_get_scancode_from_key(input, *key, &scancode);
         const SDL_Keycode keycode = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false);
 
         switch (keycode) {
@@ -1842,7 +1909,7 @@ static void keyboard_handle_key(void *data, struct wl_keyboard *keyboard,
         keyboard_input_get_text(text, input, key, false, &handled_by_ime);
     }
 
-    scancode = Wayland_get_scancode_from_key(input, key + 8);
+    Wayland_get_scancode_from_key(input, key, &scancode);
     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));
@@ -1878,7 +1945,7 @@ static void keyboard_handle_modifiers(void *data, struct wl_keyboard *keyboard,
     input->xkb.wl_pressed_modifiers = mods_depressed;
     input->xkb.wl_locked_modifiers = mods_latched | mods_locked;
 
-    Wayland_ReconcileModifiers(input);
+    Wayland_ReconcileModifiers(input, false);
 
     // If a key is repeating, update the text to apply the modifier.
     if (keyboard_repeat_is_set(&input->keyboard_repeat)) {
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 5073472da4e53..2f11fe5087df6 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -114,7 +114,8 @@ struct SDL_WaylandInput
         uint32_t idx_ctrl;
         uint32_t idx_alt;
         uint32_t idx_gui;
-        uint32_t idx_mode;
+        uint32_t idx_mod3;
+        uint32_t idx_mod5;
         uint32_t idx_num;
         uint32_t idx_caps;