Add keyboard layout support

This adds a keyboard layout switch callback and a query for the
human-readable name of the current layout.

Fixes #1201.
diff --git a/README.md b/README.md
index c30e8a4..377b8bc 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,10 @@
 
 ## Changelog
 
+ - Added `glfwGetKeyboardLayoutName` for querying the name of the current
+   keyboard layout (#1201)
+ - Added `glfwSetKeyboardLayoutCallback` and `GLFWkeyboardlayoutfun` for
+   receiving keyboard layout events (#1201)
  - Added `GLFW_RESIZE_NWSE_CURSOR`, `GLFW_RESIZE_NESW_CURSOR`,
    `GLFW_RESIZE_ALL_CURSOR` and `GLFW_NOT_ALLOWED_CURSOR` cursor shapes (#427)
  - Added `GLFW_RESIZE_EW_CURSOR` alias for `GLFW_HRESIZE_CURSOR` (#427)
diff --git a/docs/input.dox b/docs/input.dox
index 331f971..f3b23cc 100644
--- a/docs/input.dox
+++ b/docs/input.dox
@@ -230,6 +230,32 @@
 ignored.  This matches the behavior of the key callback, meaning the callback
 arguments can always be passed unmodified to this function.
 
+The name of a key will not change unless the keyboard layout changes.
+
+
+@subsection keyboard_layout Keyboard layout
+
+The human-readable name of the current keyboard layout is returned by @ref
+glfwGetKeyboardLayoutName.
+
+If you wish to be notified when the keyboard layout changes, set a keyboard
+layout callback.
+
+@code
+glfwSetKeyboardLayoutCallback(keyboard_layout_callback);
+@endcode
+
+The callback is called when the new layout takes effect for the application,
+which on some platforms may not happen until one of its windows gets input
+focus.
+
+@code
+void keyboard_layout_callback(void)
+{
+    update_keyboard_layout_name(glfwGetKeyboardLayoutName());
+}
+@endcode
+
 
 @section input_mouse Mouse input
 
diff --git a/docs/news.dox b/docs/news.dox
index 4dd7279..ac649c5 100644
--- a/docs/news.dox
+++ b/docs/news.dox
@@ -9,6 +9,15 @@
 
 @subsection features_34 New features in version 3.4
 
+@subsubsection keyboard_layout_34 Keyboard layouts
+
+GLFW can now notify when the keyboard layout has changed with @ref
+glfwSetKeyboardLayoutCallback and provides the human-readable name of the
+current layout with @ref glfwGetKeyboardLayoutName.
+
+For more information, see @ref keyboard_layout.
+
+
 @subsubsection standard_cursors_34 More standard cursors
 
 GLFW now provides the standard cursor shapes @ref GLFW_RESIZE_NWSE_CURSOR and
diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h
index e077a0c..7d36025 100644
--- a/include/GLFW/glfw3.h
+++ b/include/GLFW/glfw3.h
@@ -1270,6 +1270,23 @@
  */
 typedef void (* GLFWerrorfun)(int,const char*);
 
+/*! @brief The function pointer type for keyboard layout callbacks.
+ *
+ *  This is the function pointer type for keyboard layout callbacks.  A keyboard
+ *  layout callback function has the following signature:
+ *  @code
+ *  void callback_name(void);
+ *  @endcode
+ *
+ *  @sa @ref keyboard_layout
+ *  @sa @ref glfwSetKeyboardLayoutCallback
+ *
+ *  @since Added in version 3.4.
+ *
+ *  @ingroup input
+ */
+typedef void (* GLFWkeyboardlayoutfun)(void);
+
 /*! @brief The function pointer type for window position callbacks.
  *
  *  This is the function pointer type for window position callbacks.  A window
@@ -4265,6 +4282,11 @@
  *  non-printable keys are the same across layouts but depend on the application
  *  language and should be localized along with other user interface text.
  *
+ *  The contents of the returned string may change when a keyboard
+ *  layout change event is received.  Set a
+ *  [keyboard layout](@ref keyboard_layout) callback to be notified when the
+ *  layout changes.
+ *
  *  @param[in] key The key to query, or `GLFW_KEY_UNKNOWN`.
  *  @param[in] scancode The scancode of the key to query.
  *  @return The UTF-8 encoded, layout-specific name of the key, or `NULL`.
@@ -4272,9 +4294,6 @@
  *  @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
  *  GLFW_PLATFORM_ERROR.
  *
- *  @remark The contents of the returned string may change when a keyboard
- *  layout change event is received.
- *
  *  @pointer_lifetime The returned string is allocated and freed by GLFW.  You
  *  should not free it yourself.  It is valid until the library is terminated.
  *
@@ -4312,6 +4331,73 @@
  */
 GLFWAPI int glfwGetKeyScancode(int key);
 
+/*! @brief Returns the human-readable name of the current keyboard layout.
+ *
+ *  This function returns the human-readable name, encoded as UTF-8, of the
+ *  current keyboard layout.  On some platforms this may not be updated until
+ *  one of the application's windows gets input focus.
+ *
+ *  The keyboard layout name is intended to be shown to the user during text
+ *  input, especially in full screen applications.
+ *
+ *  The name may be localized into the current operating system UI language.  It
+ *  is provided by the operating system and may not be identical for a given
+ *  layout across platforms.
+ *
+ *  @return The UTF-8 encoded name of the current keyboard layout, or `NULL` if
+ *  an [error](@ref error_handling) occurred.
+ *
+ *  @errors Possible errors include @ref GLFW_PLATFORM_ERROR and @ref
+ *  GLFW_NOT_INITIALIZED.
+ *
+ *  @pointer_lifetime The returned string is allocated and freed by GLFW.  You
+ *  should not free it yourself.  It is valid until the next call to this
+ *  function or the library is terminated.
+ *
+ *  @thread_safety This function must only be called from the main thread.
+ *
+ *  @sa @ref keyboard_layout
+ *  @sa @ref glfwSetKeyboardLayoutCallback
+ *
+ *  @since Added in version 3.4.
+ *
+ *  @ingroup input
+ */
+GLFWAPI const char* glfwGetKeyboardLayoutName(void);
+
+/*! @brief Sets the keyboard layout callback.
+ *
+ *  This function sets the keyboard layout callback, which is called when the
+ *  keyboard layout is changed.  The name of the current layout is returned by
+ *  @ref glfwGetKeyboardLayoutName.
+ *
+ *  On some platforms the keyboard layout event may not arrive until one of the
+ *  application's windows get input focus.  Layout changes may not be reported
+ *  while other applications have input focus.
+ *
+ *  @param[in] callback The new callback, or `NULL` to remove the currently set
+ *  callback.
+ *  @return The previously set callback, or `NULL` if no callback was set or the
+ *  library had not been [initialized](@ref intro_init).
+ *
+ *  @callback_signature
+ *  @code
+ *  void function_name(void)
+ *  @endcode
+ *
+ *  @errors Possible errors include @ref GLFW_NOT_INITIALIZED.
+ *
+ *  @thread_safety This function must only be called from the main thread.
+ *
+ *  @sa @ref keyboard_layout
+ *  @sa @ref glfwGetKeyboardLayoutName
+ *
+ *  @since Added in version 3.4.
+ *
+ *  @ingroup input
+ */
+GLFWAPI GLFWkeyboardlayoutfun glfwSetKeyboardLayoutCallback(GLFWkeyboardlayoutfun callback);
+
 /*! @brief Returns the last reported state of a keyboard key for the specified
  *  window.
  *
diff --git a/src/cocoa_init.m b/src/cocoa_init.m
index 434e5be..d4460fe 100644
--- a/src/cocoa_init.m
+++ b/src/cocoa_init.m
@@ -305,38 +305,6 @@
     }
 }
 
-// Retrieve Unicode data for the current keyboard layout
-//
-static GLFWbool updateUnicodeDataNS(void)
-{
-    if (_glfw.ns.inputSource)
-    {
-        CFRelease(_glfw.ns.inputSource);
-        _glfw.ns.inputSource = NULL;
-        _glfw.ns.unicodeData = nil;
-    }
-
-    _glfw.ns.inputSource = TISCopyCurrentKeyboardLayoutInputSource();
-    if (!_glfw.ns.inputSource)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Cocoa: Failed to retrieve keyboard layout input source");
-        return GLFW_FALSE;
-    }
-
-    _glfw.ns.unicodeData =
-        TISGetInputSourceProperty(_glfw.ns.inputSource,
-                                  kTISPropertyUnicodeKeyLayoutData);
-    if (!_glfw.ns.unicodeData)
-    {
-        _glfwInputError(GLFW_PLATFORM_ERROR,
-                        "Cocoa: Failed to retrieve keyboard layout Unicode data");
-        return GLFW_FALSE;
-    }
-
-    return GLFW_TRUE;
-}
-
 // Load HIToolbox.framework and the TIS symbols we need from it
 //
 static GLFWbool initializeTIS(void)
@@ -354,6 +322,15 @@
     CFStringRef* kPropertyUnicodeKeyLayoutData =
         CFBundleGetDataPointerForName(_glfw.ns.tis.bundle,
                                       CFSTR("kTISPropertyUnicodeKeyLayoutData"));
+    CFStringRef* kPropertyInputSourceID =
+        CFBundleGetDataPointerForName(_glfw.ns.tis.bundle,
+                                      CFSTR("kTISPropertyInputSourceID"));
+    CFStringRef* kPropertyLocalizedName =
+        CFBundleGetDataPointerForName(_glfw.ns.tis.bundle,
+                                      CFSTR("kTISPropertyLocalizedName"));
+    _glfw.ns.tis.CopyCurrentKeyboardInputSource =
+        CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle,
+                                          CFSTR("TISCopyCurrentKeyboardInputSource"));
     _glfw.ns.tis.CopyCurrentKeyboardLayoutInputSource =
         CFBundleGetFunctionPointerForName(_glfw.ns.tis.bundle,
                                           CFSTR("TISCopyCurrentKeyboardLayoutInputSource"));
@@ -365,6 +342,9 @@
                                           CFSTR("LMGetKbdType"));
 
     if (!kPropertyUnicodeKeyLayoutData ||
+        !kPropertyInputSourceID ||
+        !kPropertyLocalizedName ||
+        !TISCopyCurrentKeyboardInputSource ||
         !TISCopyCurrentKeyboardLayoutInputSource ||
         !TISGetInputSourceProperty ||
         !LMGetKbdType)
@@ -376,8 +356,18 @@
 
     _glfw.ns.tis.kPropertyUnicodeKeyLayoutData =
         *kPropertyUnicodeKeyLayoutData;
+    _glfw.ns.tis.kPropertyInputSourceID =
+        *kPropertyInputSourceID;
+    _glfw.ns.tis.kPropertyLocalizedName =
+        *kPropertyLocalizedName;
 
-    return updateUnicodeDataNS();
+    _glfw.ns.inputSource = TISCopyCurrentKeyboardInputSource();
+    _glfw.ns.keyboardLayout = TISCopyCurrentKeyboardLayoutInputSource();
+    _glfw.ns.unicodeData =
+        TISGetInputSourceProperty(_glfw.ns.keyboardLayout,
+                                  kTISPropertyUnicodeKeyLayoutData);
+
+    return GLFW_TRUE;
 }
 
 @interface GLFWHelper : NSObject
@@ -387,7 +377,28 @@
 
 - (void)selectedKeyboardInputSourceChanged:(NSObject* )object
 {
-    updateUnicodeDataNS();
+    // The keyboard layout is needed for Unicode data which is the source of
+    // GLFW key names on Cocoa (the generic input source may not have this)
+    CFRelease(_glfw.ns.keyboardLayout);
+    _glfw.ns.keyboardLayout = TISCopyCurrentKeyboardLayoutInputSource();
+    _glfw.ns.unicodeData =
+        TISGetInputSourceProperty(_glfw.ns.keyboardLayout,
+                                    kTISPropertyUnicodeKeyLayoutData);
+
+    // The generic input source may be something higher level than a keyboard
+    // layout and if so will provide a better layout name than the layout source
+    const TISInputSourceRef source = TISCopyCurrentKeyboardInputSource();
+    const CFStringRef newID =
+        TISGetInputSourceProperty(source, kTISPropertyInputSourceID);
+    const CFStringRef oldID =
+        TISGetInputSourceProperty(_glfw.ns.inputSource, kTISPropertyInputSourceID);
+    const CFComparisonResult result = CFStringCompare(oldID, newID, 0);
+    CFRelease(_glfw.ns.inputSource);
+    _glfw.ns.inputSource = source;
+
+    // Filter events as we may receive more than one per input source switch
+    if (result != kCFCompareEqualTo)
+        _glfwInputKeyboardLayout();
 }
 
 - (void)doNothing:(id)object
@@ -567,11 +578,17 @@
 {
     @autoreleasepool {
 
+    if (_glfw.ns.keyboardLayout)
+    {
+        CFRelease(_glfw.ns.keyboardLayout);
+        _glfw.ns.keyboardLayout = NULL;
+        _glfw.ns.unicodeData = NULL;
+    }
+
     if (_glfw.ns.inputSource)
     {
         CFRelease(_glfw.ns.inputSource);
         _glfw.ns.inputSource = NULL;
-        _glfw.ns.unicodeData = nil;
     }
 
     if (_glfw.ns.eventSource)
@@ -603,6 +620,7 @@
         [NSEvent removeMonitor:_glfw.ns.keyUpMonitor];
 
     free(_glfw.ns.clipboardString);
+    free(_glfw.ns.keyboardLayoutName);
 
     _glfwTerminateNSGL();
     _glfwTerminateJoysticksNS();
diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h
index b18b99c..cf68cfc 100644
--- a/src/cocoa_platform.h
+++ b/src/cocoa_platform.h
@@ -103,6 +103,10 @@
 
 // HIToolbox.framework pointer typedefs
 #define kTISPropertyUnicodeKeyLayoutData _glfw.ns.tis.kPropertyUnicodeKeyLayoutData
+#define kTISPropertyInputSourceID _glfw.ns.tis.kPropertyInputSourceID
+#define kTISPropertyLocalizedName _glfw.ns.tis.kPropertyLocalizedName
+typedef TISInputSourceRef (*PFN_TISCopyCurrentKeyboardInputSource)(void);
+#define TISCopyCurrentKeyboardInputSource _glfw.ns.tis.CopyCurrentKeyboardInputSource
 typedef TISInputSourceRef (*PFN_TISCopyCurrentKeyboardLayoutInputSource)(void);
 #define TISCopyCurrentKeyboardLayoutInputSource _glfw.ns.tis.CopyCurrentKeyboardLayoutInputSource
 typedef void* (*PFN_TISGetInputSourceProperty)(TISInputSourceRef,CFStringRef);
@@ -144,8 +148,9 @@
     id                  delegate;
     GLFWbool            cursorHidden;
     TISInputSourceRef   inputSource;
+    TISInputSourceRef   keyboardLayout;
     IOHIDManagerRef     hidManager;
-    id                  unicodeData;
+    void*               unicodeData;
     id                  helper;
     id                  keyUpMonitor;
     id                  nibObjects;
@@ -154,6 +159,7 @@
     short int           keycodes[256];
     short int           scancodes[GLFW_KEY_LAST + 1];
     char*               clipboardString;
+    char*               keyboardLayoutName;
     CGPoint             cascadePoint;
     // Where to place the cursor when re-enabled
     double              restoreCursorPosX, restoreCursorPosY;
@@ -162,10 +168,13 @@
 
     struct {
         CFBundleRef     bundle;
+        PFN_TISCopyCurrentKeyboardInputSource CopyCurrentKeyboardInputSource;
         PFN_TISCopyCurrentKeyboardLayoutInputSource CopyCurrentKeyboardLayoutInputSource;
         PFN_TISGetInputSourceProperty GetInputSourceProperty;
         PFN_LMGetKbdType GetKbdType;
         CFStringRef     kPropertyUnicodeKeyLayoutData;
+        CFStringRef     kPropertyInputSourceID;
+        CFStringRef     kPropertyLocalizedName;
     } tis;
 
 } _GLFWlibraryNS;
diff --git a/src/cocoa_window.m b/src/cocoa_window.m
index 45837dc..8d7be16 100644
--- a/src/cocoa_window.m
+++ b/src/cocoa_window.m
@@ -1516,6 +1516,12 @@
         return NULL;
     }
 
+    if (!_glfw.ns.unicodeData)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Keyboard Unicode data missing");
+        return NULL;
+    }
+
     const int key = _glfw.ns.keycodes[scancode];
 
     UInt32 deadKeyState = 0;
@@ -1559,6 +1565,26 @@
     return _glfw.ns.scancodes[key];
 }
 
+const char* _glfwPlatformGetKeyboardLayoutName(void)
+{
+    TISInputSourceRef source = TISCopyCurrentKeyboardInputSource();
+    NSString* name = (__bridge NSString*)
+        TISGetInputSourceProperty(source, kTISPropertyLocalizedName);
+    if (!name)
+    {
+        CFRelease(source);
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Cocoa: Failed to retrieve keyboard layout name");
+        return NULL;
+    }
+
+    free(_glfw.ns.keyboardLayoutName);
+    _glfw.ns.keyboardLayoutName = _glfw_strdup([name UTF8String]);
+
+    CFRelease(source);
+    return _glfw.ns.keyboardLayoutName;
+}
+
 int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
                               const GLFWimage* image,
                               int xhot, int yhot)
diff --git a/src/input.c b/src/input.c
index f616309..e4d62ab 100644
--- a/src/input.c
+++ b/src/input.c
@@ -256,6 +256,14 @@
 //////                         GLFW event API                       //////
 //////////////////////////////////////////////////////////////////////////
 
+// Notifies shared code of a keyboard layout change event
+//
+void _glfwInputKeyboardLayout(void)
+{
+    if (_glfw.callbacks.layout)
+        _glfw.callbacks.layout();
+}
+
 // Notifies shared code of a physical key event
 //
 void _glfwInputKey(_GLFWwindow* window, int key, int scancode, int action, int mods)
@@ -626,6 +634,19 @@
     return _glfwPlatformGetKeyScancode(key);
 }
 
+GLFWAPI const char* glfwGetKeyboardLayoutName(void)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
+    return _glfwPlatformGetKeyboardLayoutName();
+}
+
+GLFWAPI GLFWkeyboardlayoutfun glfwSetKeyboardLayoutCallback(GLFWkeyboardlayoutfun cbfun)
+{
+    _GLFW_REQUIRE_INIT_OR_RETURN(NULL);
+    _GLFW_SWAP_POINTERS(_glfw.callbacks.layout, cbfun);
+    return cbfun;
+}
+
 GLFWAPI int glfwGetKey(GLFWwindow* handle, int key)
 {
     _GLFWwindow* window = (_GLFWwindow*) handle;
diff --git a/src/internal.h b/src/internal.h
index 6d7587c..b24e858 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -572,6 +572,7 @@
     struct {
         GLFWmonitorfun  monitor;
         GLFWjoystickfun joystick;
+        GLFWkeyboardlayoutfun layout;
     } callbacks;
 
     // This is defined in the window API's platform.h
@@ -612,6 +613,7 @@
 
 const char* _glfwPlatformGetScancodeName(int scancode);
 int _glfwPlatformGetKeyScancode(int key);
+const char* _glfwPlatformGetKeyboardLayoutName(void);
 
 void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor);
 void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor, int* xpos, int* ypos);
@@ -717,6 +719,7 @@
 void _glfwInputWindowCloseRequest(_GLFWwindow* window);
 void _glfwInputWindowMonitor(_GLFWwindow* window, _GLFWmonitor* monitor);
 
+void _glfwInputKeyboardLayout(void);
 void _glfwInputKey(_GLFWwindow* window,
                    int key, int scancode, int action, int mods);
 void _glfwInputChar(_GLFWwindow* window,
diff --git a/src/null_window.c b/src/null_window.c
index 936400d..106cf19 100644
--- a/src/null_window.c
+++ b/src/null_window.c
@@ -310,6 +310,11 @@
     return -1;
 }
 
+const char* _glfwPlatformGetKeyboardLayoutName(void)
+{
+    return "";
+}
+
 void _glfwPlatformGetRequiredInstanceExtensions(char** extensions)
 {
 }
diff --git a/src/win32_init.c b/src/win32_init.c
index 260e888..54d428a 100644
--- a/src/win32_init.c
+++ b/src/win32_init.c
@@ -602,6 +602,7 @@
                           SPIF_SENDCHANGE);
 
     free(_glfw.win32.clipboardString);
+    free(_glfw.win32.keyboardLayoutName);
     free(_glfw.win32.rawInput);
 
     _glfwTerminateWGL();
diff --git a/src/win32_platform.h b/src/win32_platform.h
index 2b00b00..7b0a89a 100644
--- a/src/win32_platform.h
+++ b/src/win32_platform.h
@@ -331,6 +331,7 @@
     DWORD               foregroundLockTimeout;
     int                 acquiredMonitorCount;
     char*               clipboardString;
+    char*               keyboardLayoutName;
     short int           keycodes[512];
     short int           scancodes[GLFW_KEY_LAST + 1];
     char                keynames[GLFW_KEY_LAST + 1][5];
diff --git a/src/win32_window.c b/src/win32_window.c
index 0ae0998..78eb2cb 100644
--- a/src/win32_window.c
+++ b/src/win32_window.c
@@ -645,6 +645,7 @@
         case WM_INPUTLANGCHANGE:
         {
             _glfwUpdateKeyNamesWin32();
+            _glfwInputKeyboardLayout();
             break;
         }
 
@@ -2042,6 +2043,48 @@
     return _glfw.win32.scancodes[key];
 }
 
+const char* _glfwPlatformGetKeyboardLayoutName(void)
+{
+    WCHAR klid[KL_NAMELENGTH];
+    int size;
+    LCID lcid;
+    WCHAR* language;
+
+    if (!GetKeyboardLayoutNameW(klid))
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Win32: Failed to retrieve keyboard layout name");
+        return NULL;
+    }
+
+    // NOTE: We only care about the language part of the keyboard layout ID
+    lcid = MAKELCID(LANGIDFROMLCID(wcstoul(klid, NULL, 16)), 0);
+
+    size = GetLocaleInfoW(lcid, LOCALE_SLANGUAGE, NULL, 0);
+    if (!size)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Win32: Failed to retrieve keyboard layout name length");
+        return NULL;
+    }
+
+    language = calloc(size, sizeof(WCHAR));
+
+    if (!GetLocaleInfoW(lcid, LOCALE_SLANGUAGE, language, size))
+    {
+        free(language);
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Win32: Failed to translate keyboard layout name");
+        return NULL;
+    }
+
+    free(_glfw.win32.keyboardLayoutName);
+    _glfw.win32.keyboardLayoutName = _glfwCreateUTF8FromWideStringWin32(language);
+    free(language);
+
+    return _glfw.win32.keyboardLayoutName;
+}
+
 int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
                               const GLFWimage* image,
                               int xhot, int yhot)
diff --git a/src/wl_init.c b/src/wl_init.c
index 558ff8a..3cfd8cf 100644
--- a/src/wl_init.c
+++ b/src/wl_init.c
@@ -640,6 +640,12 @@
     if (mask & _glfw.wl.xkb.numLockMask)
         modifiers |= GLFW_MOD_NUM_LOCK;
     _glfw.wl.xkb.modifiers = modifiers;
+
+    if (_glfw.wl.xkb.group != group)
+    {
+        _glfw.wl.xkb.group = group;
+        _glfwInputKeyboardLayout();
+    }
 }
 
 #ifdef WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION
@@ -1101,6 +1107,8 @@
         _glfw_dlsym(_glfw.wl.xkb.handle, "xkb_state_update_mask");
     _glfw.wl.xkb.state_serialize_mods = (PFN_xkb_state_serialize_mods)
         _glfw_dlsym(_glfw.wl.xkb.handle, "xkb_state_serialize_mods");
+    _glfw.wl.xkb.keymap_layout_get_name = (PFN_xkb_keymap_layout_get_name)
+        _glfw_dlsym(_glfw.wl.xkb.handle, "xkb_keymap_layout_get_name");
 
 #ifdef HAVE_XKBCOMMON_COMPOSE_H
     _glfw.wl.xkb.compose_table_new_from_locale = (PFN_xkb_compose_table_new_from_locale)
@@ -1300,6 +1308,8 @@
         free(_glfw.wl.clipboardString);
     if (_glfw.wl.clipboardSendString)
         free(_glfw.wl.clipboardSendString);
+    if (_glfw.wl.keyboardLayoutName)
+        free(_glfw.wl.keyboardLayoutName);
 }
 
 const char* _glfwPlatformGetVersionString(void)
diff --git a/src/wl_platform.h b/src/wl_platform.h
index 542cc78..1069230 100644
--- a/src/wl_platform.h
+++ b/src/wl_platform.h
@@ -117,6 +117,7 @@
 typedef int (* PFN_xkb_state_key_get_syms)(struct xkb_state*, xkb_keycode_t, const xkb_keysym_t**);
 typedef enum xkb_state_component (* PFN_xkb_state_update_mask)(struct xkb_state*, xkb_mod_mask_t, xkb_mod_mask_t, xkb_mod_mask_t, xkb_layout_index_t, xkb_layout_index_t, xkb_layout_index_t);
 typedef xkb_mod_mask_t (* PFN_xkb_state_serialize_mods)(struct xkb_state*, enum xkb_state_component);
+typedef const char * (* PFN_xkb_keymap_layout_get_name)(struct xkb_keymap*,xkb_layout_index_t);
 #define xkb_context_new _glfw.wl.xkb.context_new
 #define xkb_context_unref _glfw.wl.xkb.context_unref
 #define xkb_keymap_new_from_string _glfw.wl.xkb.keymap_new_from_string
@@ -128,6 +129,7 @@
 #define xkb_state_key_get_syms _glfw.wl.xkb.state_key_get_syms
 #define xkb_state_update_mask _glfw.wl.xkb.state_update_mask
 #define xkb_state_serialize_mods _glfw.wl.xkb.state_serialize_mods
+#define xkb_keymap_layout_get_name _glfw.wl.xkb.keymap_layout_get_name
 
 #ifdef HAVE_XKBCOMMON_COMPOSE_H
 typedef struct xkb_compose_table* (* PFN_xkb_compose_table_new_from_locale)(struct xkb_context*, const char*, enum xkb_compose_compile_flags);
@@ -259,6 +261,7 @@
     size_t                      clipboardSize;
     char*                       clipboardSendString;
     size_t                      clipboardSendSize;
+    char*                       keyboardLayoutName;
     int                         timerfd;
     short int                   keycodes[256];
     short int                   scancodes[GLFW_KEY_LAST + 1];
@@ -280,6 +283,7 @@
         xkb_mod_mask_t          capsLockMask;
         xkb_mod_mask_t          numLockMask;
         unsigned int            modifiers;
+        xkb_layout_index_t      group;
 
         PFN_xkb_context_new context_new;
         PFN_xkb_context_unref context_unref;
@@ -292,6 +296,7 @@
         PFN_xkb_state_key_get_syms state_key_get_syms;
         PFN_xkb_state_update_mask state_update_mask;
         PFN_xkb_state_serialize_mods state_serialize_mods;
+        PFN_xkb_keymap_layout_get_name keymap_layout_get_name;
 
 #ifdef HAVE_XKBCOMMON_COMPOSE_H
         PFN_xkb_compose_table_new_from_locale compose_table_new_from_locale;
diff --git a/src/wl_window.c b/src/wl_window.c
index c8dde30..2b1fa78 100644
--- a/src/wl_window.c
+++ b/src/wl_window.c
@@ -1194,6 +1194,29 @@
     return _glfw.wl.scancodes[key];
 }
 
+const char* _glfwPlatformGetKeyboardLayoutName(void)
+{
+    if (!_glfw.wl.xkb.keymap)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: Keymap missing");
+        return NULL;
+    }
+
+    const char* name = xkb_keymap_layout_get_name(_glfw.wl.xkb.keymap,
+                                                  _glfw.wl.xkb.group);
+    if (!name)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "Wayland: Failed to query keyboard layout name");
+        return NULL;
+    }
+
+    free(_glfw.wl.keyboardLayoutName);
+    _glfw.wl.keyboardLayoutName = _glfw_strdup(name);
+    return _glfw.wl.keyboardLayoutName;
+}
+
 int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
                               const GLFWimage* image,
                               int xhot, int yhot)
diff --git a/src/x11_init.c b/src/x11_init.c
index e5bd578..40ad25c 100644
--- a/src/x11_init.c
+++ b/src/x11_init.c
@@ -1153,6 +1153,8 @@
         _glfw_dlsym(_glfw.x11.xlib.handle, "XFreeCursor");
     _glfw.x11.xlib.FreeEventData = (PFN_XFreeEventData)
         _glfw_dlsym(_glfw.x11.xlib.handle, "XFreeEventData");
+    _glfw.x11.xlib.GetAtomName = (PFN_XGetAtomName)
+        _glfw_dlsym(_glfw.x11.xlib.handle, "XGetAtomName");
     _glfw.x11.xlib.GetErrorText = (PFN_XGetErrorText)
         _glfw_dlsym(_glfw.x11.xlib.handle, "XGetErrorText");
     _glfw.x11.xlib.GetEventData = (PFN_XGetEventData)
@@ -1263,6 +1265,8 @@
         _glfw_dlsym(_glfw.x11.xlib.handle, "XVisualIDFromVisual");
     _glfw.x11.xlib.WarpPointer = (PFN_XWarpPointer)
         _glfw_dlsym(_glfw.x11.xlib.handle, "XWarpPointer");
+    _glfw.x11.xkb.AllocKeyboard = (PFN_XkbAllocKeyboard)
+        _glfw_dlsym(_glfw.x11.xlib.handle, "XkbAllocKeyboard");
     _glfw.x11.xkb.FreeKeyboard = (PFN_XkbFreeKeyboard)
         _glfw_dlsym(_glfw.x11.xlib.handle, "XkbFreeKeyboard");
     _glfw.x11.xkb.FreeNames = (PFN_XkbFreeNames)
@@ -1376,6 +1380,9 @@
     free(_glfw.x11.primarySelectionString);
     free(_glfw.x11.clipboardString);
 
+    if (_glfw.x11.keyboardLayoutName)
+        XFree(_glfw.x11.keyboardLayoutName);
+
     XUnregisterIMInstantiateCallback(_glfw.x11.display,
                                      NULL, NULL, NULL,
                                      inputMethodInstantiateCallback,
diff --git a/src/x11_platform.h b/src/x11_platform.h
index fe82a72..9b19e12 100644
--- a/src/x11_platform.h
+++ b/src/x11_platform.h
@@ -76,6 +76,7 @@
 typedef int (* PFN_XFreeColormap)(Display*,Colormap);
 typedef int (* PFN_XFreeCursor)(Display*,Cursor);
 typedef void (* PFN_XFreeEventData)(Display*,XGenericEventCookie*);
+typedef char* (* PFN_XGetAtomName)(Display*,Atom);
 typedef int (* PFN_XGetErrorText)(Display*,int,char*,int);
 typedef Bool (* PFN_XGetEventData)(Display*,XGenericEventCookie*);
 typedef char* (* PFN_XGetICValues)(XIC,...);
@@ -133,6 +134,7 @@
 typedef int (* PFN_XWarpPointer)(Display*,Window,Window,int,int,unsigned int,unsigned int,int,int);
 typedef void (* PFN_XkbFreeKeyboard)(XkbDescPtr,unsigned int,Bool);
 typedef void (* PFN_XkbFreeNames)(XkbDescPtr,unsigned int,Bool);
+typedef XkbDescPtr (* PFN_XkbAllocKeyboard)(void);
 typedef XkbDescPtr (* PFN_XkbGetMap)(Display*,unsigned int,unsigned int);
 typedef Status (* PFN_XkbGetNames)(Display*,unsigned int,XkbDescPtr);
 typedef Status (* PFN_XkbGetState)(Display*,unsigned int,XkbStatePtr);
@@ -176,6 +178,7 @@
 #define XFreeColormap _glfw.x11.xlib.FreeColormap
 #define XFreeCursor _glfw.x11.xlib.FreeCursor
 #define XFreeEventData _glfw.x11.xlib.FreeEventData
+#define XGetAtomName _glfw.x11.xlib.GetAtomName
 #define XGetErrorText _glfw.x11.xlib.GetErrorText
 #define XGetEventData _glfw.x11.xlib.GetEventData
 #define XGetICValues _glfw.x11.xlib.GetICValues
@@ -231,6 +234,7 @@
 #define XUnsetICFocus _glfw.x11.xlib.UnsetICFocus
 #define XVisualIDFromVisual _glfw.x11.xlib.VisualIDFromVisual
 #define XWarpPointer _glfw.x11.xlib.WarpPointer
+#define XkbAllocKeyboard _glfw.x11.xkb.AllocKeyboard
 #define XkbFreeKeyboard _glfw.x11.xkb.FreeKeyboard
 #define XkbFreeNames _glfw.x11.xkb.FreeNames
 #define XkbGetMap _glfw.x11.xkb.GetMap
@@ -442,6 +446,7 @@
     short int       keycodes[256];
     // GLFW key to X11 keycode LUT
     short int       scancodes[GLFW_KEY_LAST + 1];
+    char*           keyboardLayoutName;
     // Where to place the cursor when re-enabled
     double          restoreCursorPosX, restoreCursorPosY;
     // The window whose disabled cursor mode is active
@@ -533,6 +538,7 @@
         PFN_XFreeColormap FreeColormap;
         PFN_XFreeCursor FreeCursor;
         PFN_XFreeEventData FreeEventData;
+        PFN_XGetAtomName GetAtomName;
         PFN_XGetErrorText GetErrorText;
         PFN_XGetEventData GetEventData;
         PFN_XGetICValues GetICValues;
@@ -638,6 +644,7 @@
         int          major;
         int          minor;
         unsigned int group;
+        PFN_XkbAllocKeyboard AllocKeyboard;
         PFN_XkbFreeKeyboard FreeKeyboard;
         PFN_XkbFreeNames FreeNames;
         PFN_XkbGetMap GetMap;
diff --git a/src/x11_window.c b/src/x11_window.c
index d6e6bf3..011e00f 100644
--- a/src/x11_window.c
+++ b/src/x11_window.c
@@ -1184,6 +1184,7 @@
                 (((XkbEvent*) event)->state.changed & XkbGroupStateMask))
             {
                 _glfw.x11.xkb.group = ((XkbEvent*) event)->state.group;
+                _glfwInputKeyboardLayout();
             }
         }
     }
@@ -2920,6 +2921,42 @@
     return _glfw.x11.scancodes[key];
 }
 
+const char* _glfwPlatformGetKeyboardLayoutName(void)
+{
+    if (!_glfw.x11.xkb.available)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "X11: XKB extension required for keyboard layout names");
+        return NULL;
+    }
+
+    XkbStateRec state = {0};
+    XkbGetState(_glfw.x11.display, XkbUseCoreKbd, &state);
+
+    XkbDescPtr desc = XkbAllocKeyboard();
+    if (XkbGetNames(_glfw.x11.display, XkbGroupNamesMask, desc) != Success)
+    {
+        XkbFreeKeyboard(desc, 0, True);
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "X11: Failed to retrieve keyboard layout names");
+        return NULL;
+    }
+
+    const Atom atom = desc->names->groups[state.group];
+    XkbFreeKeyboard(desc, 0, True);
+
+    if (atom == None)
+    {
+        _glfwInputError(GLFW_PLATFORM_ERROR,
+                        "X11: Name missing for current keyboard layout");
+        return NULL;
+    }
+
+    free(_glfw.x11.keyboardLayoutName);
+    _glfw.x11.keyboardLayoutName = XGetAtomName(_glfw.x11.display, atom);
+    return _glfw.x11.keyboardLayoutName;
+}
+
 int _glfwPlatformCreateCursor(_GLFWcursor* cursor,
                               const GLFWimage* image,
                               int xhot, int yhot)
diff --git a/tests/events.c b/tests/events.c
index 86a63df..969b14b 100644
--- a/tests/events.c
+++ b/tests/events.c
@@ -465,6 +465,12 @@
         printf("  %i: \"%s\"\n", i, paths[i]);
 }
 
+static void keyboard_layout_callback(void)
+{
+    printf("%08x at %0.3f: Keyboard layout changed to \'%s\'\n",
+           counter++, glfwGetTime(), glfwGetKeyboardLayoutName());
+}
+
 static void monitor_callback(GLFWmonitor* monitor, int event)
 {
     if (event == GLFW_CONNECTED)
@@ -546,6 +552,7 @@
 
     glfwSetMonitorCallback(monitor_callback);
     glfwSetJoystickCallback(joystick_callback);
+    glfwSetKeyboardLayoutCallback(keyboard_layout_callback);
 
     while ((ch = getopt(argc, argv, "hfn:")) != -1)
     {