gpui: Introduce `PlatformKeyboardLayout` trait for human-friendly keyboard layout names (#29049)

张小白 created

This PR adds a new `PlatformKeyboardLayout` trait with two methods:
`id(&self) -> &str` and `name(&self) -> &str`. The `id()` method returns
a unique identifier for the keyboard layout, while `name()` provides a
human-readable name. This distinction is especially important on
Windows, where the `id` and `name` can be quite different. For example,
the French layout has an `id` of `0000040C`, which is not
human-readable, whereas the `name` would simply be `French`. Currently,
the existing `keyboard_layout()` method returns what's essentially the
same as `id()` in this new design.

This PR implements the `name()` method for both Windows and macOS. On
Linux, for now, `name()` still returns the same value as `id()`.

Release Notes:

- N/A

Change summary

Cargo.lock                                        | 14 +++
crates/gpui/Cargo.toml                            |  3 
crates/gpui/examples/input.rs                     |  2 
crates/gpui/src/app.rs                            | 18 ++--
crates/gpui/src/platform.rs                       | 10 ++
crates/gpui/src/platform/linux/headless/client.rs |  7 +
crates/gpui/src/platform/linux/platform.rs        | 28 ++++++-
crates/gpui/src/platform/linux/wayland/client.rs  | 23 +++--
crates/gpui/src/platform/linux/x11/client.rs      | 21 +++--
crates/gpui/src/platform/mac/platform.rs          | 62 ++++++++++++----
crates/gpui/src/platform/test/platform.rs         | 22 ++++-
crates/gpui/src/platform/windows/platform.rs      | 44 +++++++++++
crates/language_tools/src/key_context_view.rs     |  2 
crates/settings/src/keymap_file.rs                |  3 
crates/workspace/src/workspace.rs                 |  2 
crates/zed/src/zed.rs                             |  4 
16 files changed, 197 insertions(+), 68 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -6171,6 +6171,7 @@ dependencies = [
  "windows 0.61.1",
  "windows-core 0.61.0",
  "windows-numerics",
+ "windows-registry 0.5.1",
  "workspace-hack",
  "x11-clipboard",
  "x11rb",
@@ -11930,7 +11931,7 @@ dependencies = [
  "wasm-bindgen-futures",
  "wasm-streams",
  "web-sys",
- "windows-registry",
+ "windows-registry 0.2.0",
 ]
 
 [[package]]
@@ -17044,6 +17045,17 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-registry"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
+dependencies = [
+ "windows-link",
+ "windows-result 0.3.2",
+ "windows-strings 0.4.0",
+]
+
 [[package]]
 name = "windows-result"
 version = "0.1.2"

crates/gpui/Cargo.toml 🔗

@@ -66,7 +66,7 @@ x11 = [
     "x11-clipboard",
     "filedescriptor",
     "open",
-    "scap"
+    "scap",
 ]
 
 
@@ -220,6 +220,7 @@ rand.workspace = true
 windows.workspace = true
 windows-core = "0.61"
 windows-numerics = "0.2"
+windows-registry = "0.5"
 
 [dev-dependencies]
 backtrace = "0.3"

crates/gpui/examples/input.rs 🔗

@@ -635,7 +635,7 @@ impl Render for InputExample {
                     .flex()
                     .flex_row()
                     .justify_between()
-                    .child(format!("Keyboard {}", cx.keyboard_layout()))
+                    .child(format!("Keyboard {}", cx.keyboard_layout().name()))
                     .child(
                         div()
                             .border_1()

crates/gpui/src/app.rs 🔗

@@ -35,10 +35,10 @@ use crate::{
     AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
     EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke,
     LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay,
-    Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
-    ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
-    Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, current_platform, hash,
-    init_app_menus,
+    PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
+    RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
+    Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
+    WindowInvalidator, current_platform, hash, init_app_menus,
 };
 
 mod async_context;
@@ -248,7 +248,7 @@ pub struct App {
     pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
     pub(crate) focus_handles: Arc<FocusMap>,
     pub(crate) keymap: Rc<RefCell<Keymap>>,
-    pub(crate) keyboard_layout: SharedString,
+    pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
     pub(crate) global_action_listeners:
         FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
     pending_effects: VecDeque<Effect>,
@@ -289,7 +289,7 @@ impl App {
 
         let text_system = Arc::new(TextSystem::new(platform.text_system()));
         let entities = EntityMap::new();
-        let keyboard_layout = SharedString::from(platform.keyboard_layout());
+        let keyboard_layout = platform.keyboard_layout();
 
         let app = Rc::new_cyclic(|this| AppCell {
             app: RefCell::new(App {
@@ -345,7 +345,7 @@ impl App {
             move || {
                 if let Some(app) = app.upgrade() {
                     let cx = &mut app.borrow_mut();
-                    cx.keyboard_layout = SharedString::from(cx.platform.keyboard_layout());
+                    cx.keyboard_layout = cx.platform.keyboard_layout();
                     cx.keyboard_layout_observers
                         .clone()
                         .retain(&(), move |callback| (callback)(cx));
@@ -387,8 +387,8 @@ impl App {
     }
 
     /// Get the id of the current keyboard layout
-    pub fn keyboard_layout(&self) -> &SharedString {
-        &self.keyboard_layout
+    pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
+        self.keyboard_layout.as_ref()
     }
 
     /// Invokes a handler when the current keyboard layout changes

crates/gpui/src/platform.rs 🔗

@@ -214,7 +214,7 @@ pub(crate) trait Platform: 'static {
     fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
     fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
     fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
-    fn keyboard_layout(&self) -> String;
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
 
     fn compositor_name(&self) -> &'static str {
         ""
@@ -1634,3 +1634,11 @@ impl From<String> for ClipboardString {
         }
     }
 }
+
+/// A trait for platform-specific keyboard layouts
+pub trait PlatformKeyboardLayout {
+    /// Get the keyboard layout ID, which should be unique to the layout
+    fn id(&self) -> &str;
+    /// Get the keyboard layout display name
+    fn name(&self) -> &str;
+}

crates/gpui/src/platform/linux/headless/client.rs 🔗

@@ -9,7 +9,8 @@ use util::ResultExt;
 use crate::platform::linux::LinuxClient;
 use crate::platform::{LinuxCommon, PlatformWindow};
 use crate::{
-    AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, ScreenCaptureSource, WindowParams,
+    AnyWindowHandle, CursorStyle, DisplayId, LinuxKeyboardLayout, PlatformDisplay,
+    PlatformKeyboardLayout, ScreenCaptureSource, WindowParams,
 };
 
 pub struct HeadlessClientState {
@@ -50,8 +51,8 @@ impl LinuxClient for HeadlessClient {
         f(&mut self.0.borrow_mut().common)
     }
 
-    fn keyboard_layout(&self) -> String {
-        "unknown".to_string()
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
+        Box::new(LinuxKeyboardLayout::new("unknown".to_string()))
     }
 
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
-    Pixels, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Point, Result,
-    ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
+    Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
+    Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
 };
 
 #[cfg(any(feature = "wayland", feature = "x11"))]
@@ -46,7 +46,7 @@ const FILE_PICKER_PORTAL_MISSING: &str =
 pub trait LinuxClient {
     fn compositor_name(&self) -> &'static str;
     fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
-    fn keyboard_layout(&self) -> String;
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
     #[allow(unused)]
     fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
@@ -138,7 +138,7 @@ impl<P: LinuxClient + 'static> Platform for P {
         self.with_common(|common| common.text_system.clone())
     }
 
-    fn keyboard_layout(&self) -> String {
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
         self.keyboard_layout()
     }
 
@@ -858,6 +858,26 @@ impl crate::Modifiers {
     }
 }
 
+pub(crate) struct LinuxKeyboardLayout {
+    id: String,
+}
+
+impl PlatformKeyboardLayout for LinuxKeyboardLayout {
+    fn id(&self) -> &str {
+        &self.id
+    }
+
+    fn name(&self) -> &str {
+        &self.id
+    }
+}
+
+impl LinuxKeyboardLayout {
+    pub(crate) fn new(id: String) -> Self {
+        Self { id }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -66,8 +66,10 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
 use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
 use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
 
-use super::display::WaylandDisplay;
-use super::window::{ImeInput, WaylandWindowStatePtr};
+use super::{
+    display::WaylandDisplay,
+    window::{ImeInput, WaylandWindowStatePtr},
+};
 
 use crate::platform::linux::{
     LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
@@ -83,11 +85,11 @@ use crate::platform::linux::{
 use crate::platform::{PlatformWindow, blade::BladeContext};
 use crate::{
     AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
-    FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, Modifiers,
-    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
-    MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, SCROLL_LINES,
-    ScaledPixels, ScreenCaptureSource, ScrollDelta, ScrollWheelEvent, Size, TouchPhase,
-    WindowParams, point, px, size,
+    FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
+    LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
+    MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
+    PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource,
+    ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
 };
 
 /// Used to convert evdev scancode to xkb scancode
@@ -587,9 +589,9 @@ impl WaylandClient {
 }
 
 impl LinuxClient for WaylandClient {
-    fn keyboard_layout(&self) -> String {
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
         let state = self.0.borrow();
-        if let Some(keymap_state) = &state.keymap_state {
+        let id = if let Some(keymap_state) = &state.keymap_state {
             let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
             keymap_state
                 .get_keymap()
@@ -597,7 +599,8 @@ impl LinuxClient for WaylandClient {
                 .to_string()
         } else {
             "unknown".to_string()
-        }
+        };
+        Box::new(LinuxKeyboardLayout::new(id))
     }
 
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -59,9 +59,10 @@ use crate::platform::{
 };
 use crate::{
     AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
-    Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform, PlatformDisplay,
-    PlatformInput, Point, RequestFrameOptions, ScaledPixels, ScreenCaptureSource, ScrollDelta,
-    Size, TouchPhase, WindowParams, X11Window, modifiers_from_xinput_info, point, px,
+    LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
+    PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
+    ScaledPixels, ScreenCaptureSource, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
+    modifiers_from_xinput_info, point, px,
 };
 
 /// Value for DeviceId parameters which selects all devices.
@@ -1282,14 +1283,16 @@ impl LinuxClient for X11Client {
         f(&mut self.0.borrow_mut().common)
     }
 
-    fn keyboard_layout(&self) -> String {
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
         let state = self.0.borrow();
         let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
-        state
-            .xkb
-            .get_keymap()
-            .layout_get_name(layout_idx)
-            .to_string()
+        Box::new(LinuxKeyboardLayout::new(
+            state
+                .xkb
+                .get_keymap()
+                .layout_get_name(layout_idx)
+                .to_string(),
+        ))
     }
 
     fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -7,9 +7,9 @@ use super::{
 use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
     CursorStyle, ForegroundExecutor, Image, ImageFormat, Keymap, MacDispatcher, MacDisplay,
-    MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem,
-    PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance,
-    WindowParams, hash,
+    MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
+    PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
+    SemanticVersion, Task, WindowAppearance, WindowParams, hash,
 };
 use anyhow::{Context as _, anyhow};
 use block::ConcreteBlock;
@@ -825,20 +825,8 @@ impl Platform for MacPlatform {
         self.0.lock().validate_menu_command = Some(callback);
     }
 
-    fn keyboard_layout(&self) -> String {
-        unsafe {
-            let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
-
-            let input_source_id: *mut Object = TISGetInputSourceProperty(
-                current_keyboard,
-                kTISPropertyInputSourceID as *const c_void,
-            );
-            let input_source_id: *const std::os::raw::c_char =
-                msg_send![input_source_id, UTF8String];
-            let input_source_id = CStr::from_ptr(input_source_id).to_str().unwrap();
-
-            input_source_id.to_string()
-        }
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
+        Box::new(MacKeyboardLayout::new())
     }
 
     fn app_path(&self) -> Result<PathBuf> {
@@ -1501,6 +1489,7 @@ unsafe extern "C" {
     pub(super) fn LMGetKbdType() -> u16;
     pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
     pub(super) static kTISPropertyInputSourceID: CFStringRef;
+    pub(super) static kTISPropertyLocalizedName: CFStringRef;
 }
 
 mod security {
@@ -1590,6 +1579,45 @@ impl UTType {
     }
 }
 
+struct MacKeyboardLayout {
+    id: String,
+    name: String,
+}
+
+impl PlatformKeyboardLayout for MacKeyboardLayout {
+    fn id(&self) -> &str {
+        &self.id
+    }
+
+    fn name(&self) -> &str {
+        &self.name
+    }
+}
+
+impl MacKeyboardLayout {
+    fn new() -> Self {
+        unsafe {
+            let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
+
+            let id: *mut Object = TISGetInputSourceProperty(
+                current_keyboard,
+                kTISPropertyInputSourceID as *const c_void,
+            );
+            let id: *const std::os::raw::c_char = msg_send![id, UTF8String];
+            let id = CStr::from_ptr(id).to_str().unwrap().to_string();
+
+            let name: *mut Object = TISGetInputSourceProperty(
+                current_keyboard,
+                kTISPropertyLocalizedName as *const c_void,
+            );
+            let name: *const std::os::raw::c_char = msg_send![name, UTF8String];
+            let name = CStr::from_ptr(name).to_str().unwrap().to_string();
+
+            Self { id, name }
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::ClipboardItem;

crates/gpui/src/platform/test/platform.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
-    ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformTextSystem,
-    ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay,
-    TestWindow, WindowAppearance, WindowParams, size,
+    ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
+    PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task,
+    TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
 };
 use anyhow::Result;
 use collections::VecDeque;
@@ -223,8 +223,8 @@ impl Platform for TestPlatform {
         self.text_system.clone()
     }
 
-    fn keyboard_layout(&self) -> String {
-        "zed.keyboard.example".to_string()
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
+        Box::new(TestKeyboardLayout)
     }
 
     fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
@@ -431,3 +431,15 @@ impl Drop for TestPlatform {
         }
     }
 }
+
+struct TestKeyboardLayout;
+
+impl PlatformKeyboardLayout for TestKeyboardLayout {
+    fn id(&self) -> &str {
+        "zed.keyboard.example"
+    }
+
+    fn name(&self) -> &str {
+        "zed.keyboard.example"
+    }
+}

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -297,8 +297,12 @@ impl Platform for WindowsPlatform {
         self.text_system.clone()
     }
 
-    fn keyboard_layout(&self) -> String {
-        "unknown".into()
+    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
+        Box::new(
+            KeyboardLayout::new()
+                .log_err()
+                .unwrap_or(KeyboardLayout::unknown()),
+        )
     }
 
     fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
@@ -836,6 +840,42 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
     Ok(ui_settings.AutoHideScrollBars()?)
 }
 
+struct KeyboardLayout {
+    id: String,
+    name: String,
+}
+
+impl PlatformKeyboardLayout for KeyboardLayout {
+    fn id(&self) -> &str {
+        &self.id
+    }
+
+    fn name(&self) -> &str {
+        &self.name
+    }
+}
+
+impl KeyboardLayout {
+    fn new() -> Result<Self> {
+        let mut buffer = [0u16; KL_NAMELENGTH as usize];
+        unsafe { GetKeyboardLayoutNameW(&mut buffer)? };
+        let id = HSTRING::from_wide(&buffer).to_string();
+        let entry = windows_registry::LOCAL_MACHINE.open(format!(
+            "System\\CurrentControlSet\\Control\\Keyboard Layouts\\{}",
+            id
+        ))?;
+        let name = entry.get_hstring("Layout Text")?.to_string();
+        Ok(Self { id, name })
+    }
+
+    fn unknown() -> Self {
+        Self {
+            id: "unknown".to_string(),
+            name: "unknown".to_string(),
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::{ClipboardItem, read_from_clipboard, write_to_clipboard};

crates/language_tools/src/key_context_view.rs 🔗

@@ -173,7 +173,7 @@ impl Item for KeyContextView {
 impl Render for KeyContextView {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
         use itertools::Itertools;
-        let key_equivalents = get_key_equivalents(cx.keyboard_layout());
+        let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
         v_flex()
             .id("key-context-view")
             .overflow_scroll()

crates/settings/src/keymap_file.rs 🔗

@@ -195,7 +195,8 @@ impl KeymapFile {
     }
 
     pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
-        let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
+        let key_equivalents =
+            crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
 
         if content.is_empty() {
             return KeymapFileLoadResult::Success {

crates/workspace/src/workspace.rs 🔗

@@ -5439,7 +5439,7 @@ impl Render for Workspace {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let mut context = KeyContext::new_with_defaults();
         context.add("Workspace");
-        context.set("keyboard_layout", cx.keyboard_layout().clone());
+        context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
         let centered_layout = self.centered_layout
             && self.center.panes().len() == 1
             && self.active_item(cx).is_some();

crates/zed/src/zed.rs 🔗

@@ -1224,9 +1224,9 @@ pub fn handle_keymap_file_changes(
     })
     .detach();
 
-    let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout());
+    let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
     cx.on_keyboard_layout_change(move |cx| {
-        let next_mapping = settings::get_key_equivalents(cx.keyboard_layout());
+        let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
         if next_mapping != current_mapping {
             current_mapping = next_mapping;
             keyboard_layout_tx.unbounded_send(()).ok();