From 4a8fe6ba37a651f5d59d5e7be0a06b68cdfbcce5 Mon Sep 17 00:00:00 2001 From: acarl005 <2yinyang2@gmail.com> Date: Fri, 11 Sep 2026 14:52:49 -0700 Subject: [PATCH 1/2] windows: refresh LAYOUT_CACHE on WM_INPUTLANGCHANGE Add LayoutCache::refresh_current_layout, which unconditionally recomputes the current layout instead of trusting an existing cache entry keyed by HKL. Handle WM_INPUTLANGCHANGE in the window procedure to call it, since Windows can reuse an HKL value after a layout is unloaded and a different one is loaded in its place, which would otherwise leave LayoutCache::get_current_layout serving stale key mappings. DefWindowProc is still invoked afterwards per the WM_INPUTLANGCHANGE docs, so the message still propagates to first-level child windows. --- src/platform_impl/windows/event_loop.rs | 30 +++++++++---- src/platform_impl/windows/keyboard_layout.rs | 46 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index a5597e8f3d..4d6fa7fdc9 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -53,14 +53,15 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, - WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, - WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, - WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, - WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, - WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, - WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, - WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, - WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUTLANGCHANGE, + WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, + WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, + WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, + WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, + WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, + WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, + WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, + WS_VISIBLE, }; use crate::dpi::{PhysicalPosition, PhysicalSize}; @@ -1778,6 +1779,19 @@ unsafe fn public_window_callback_inner( } }, + // The active input layout changed (e.g. the user switched keyboard layout/IME, or a + // layout-switching tool such as Punto Switcher activated one). `LAYOUT_CACHE` is keyed + // by `HKL`, and Windows can reuse an `HKL` value after the layout it identified is + // unloaded and a different one is loaded in its place, so re-derive the layout instead + // of trusting whatever is already cached for it. + WM_INPUTLANGCHANGE => { + LAYOUT_CACHE.lock().unwrap().refresh_current_layout(); + // Per https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-inputlangchange, + // an application should pass this to `DefWindowProc` so it also reaches + // first-level child windows. + result = ProcResult::DefWindowProc(wparam); + }, + WM_LBUTTONDOWN => { use crate::event::ElementState::Pressed; use crate::event::MouseButton::Left; diff --git a/src/platform_impl/windows/keyboard_layout.rs b/src/platform_impl/windows/keyboard_layout.rs index 5340e43d8b..db19bc91ec 100644 --- a/src/platform_impl/windows/keyboard_layout.rs +++ b/src/platform_impl/windows/keyboard_layout.rs @@ -272,6 +272,20 @@ impl LayoutCache { } } + /// Unconditionally recomputes the current layout, replacing any existing cache entry for it. + /// + /// Unlike [`Self::get_current_layout`], this doesn't trust an existing cache entry for the + /// current HKL. Windows can reuse an `HKL` value after the layout it identified is unloaded + /// and a different one is loaded in its place, which would otherwise leave + /// [`Self::get_current_layout`] serving key mappings for the wrong layout. Callers that + /// learn the input layout changed (e.g. on `WM_INPUTLANGCHANGE`) should call this instead. + pub fn refresh_current_layout(&mut self) -> (u64, &Layout) { + let locale_id = unsafe { GetKeyboardLayout(0) } as u64; + let layout = Self::prepare_layout(locale_id); + self.layouts.insert(locale_id, layout); + (locale_id, self.layouts.get(&locale_id).unwrap()) + } + pub fn get_agnostic_mods(&mut self) -> ModifiersState { let (_, layout) = self.get_current_layout(); let filter_out_altgr = layout.has_alt_graph && key_pressed(VK_RMENU); @@ -984,3 +998,35 @@ fn vkey_to_non_char_key( _ => Key::Unidentified(native_code), } } + +#[cfg(test)] +mod tests { + use super::*; + + /// `get_current_layout` trusts an existing cache entry for the current `HKL` without + /// re-checking it, which is the right choice for the hot keystroke path but would serve + /// stale data if Windows reused the `HKL` value for a different layout. `refresh_current_layout` + /// exists precisely to bypass that trust, so this proves it actually recomputes the entry + /// rather than returning the cached one unchanged, by planting an entry that could not have + /// come from a real `prepare_layout(locale_id)` call and confirming it gets replaced. + #[test] + fn refresh_current_layout_replaces_a_stale_entry() { + let mut cache = LayoutCache::default(); + let (locale_id, _) = cache.get_current_layout(); + + let poisoned_hkl = !locale_id; + cache.layouts.get_mut(&locale_id).unwrap().hkl = poisoned_hkl; + assert_eq!( + cache.get_current_layout().1.hkl, + poisoned_hkl, + "sanity check: get_current_layout must trust the existing entry" + ); + + let (refreshed_id, refreshed_layout) = cache.refresh_current_layout(); + assert_eq!(refreshed_id, locale_id); + assert_eq!( + refreshed_layout.hkl, locale_id, + "refresh_current_layout must recompute the entry instead of trusting the stale one" + ); + } +} From 2727280599eddf0ba53143c39ecbb7445fa23e40 Mon Sep 17 00:00:00 2001 From: "warp-agent-staging[bot]" <240773466+warp-agent-staging[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:42:27 +0000 Subject: [PATCH 2/2] Address review feedback --- src/platform_impl/windows/event_loop.rs | 3 --- src/platform_impl/windows/keyboard_layout.rs | 24 ++------------------ 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 4d6fa7fdc9..d11ba17498 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -1786,9 +1786,6 @@ unsafe fn public_window_callback_inner( // of trusting whatever is already cached for it. WM_INPUTLANGCHANGE => { LAYOUT_CACHE.lock().unwrap().refresh_current_layout(); - // Per https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-inputlangchange, - // an application should pass this to `DefWindowProc` so it also reaches - // first-level child windows. result = ProcResult::DefWindowProc(wparam); }, diff --git a/src/platform_impl/windows/keyboard_layout.rs b/src/platform_impl/windows/keyboard_layout.rs index db19bc91ec..db7c39fd4f 100644 --- a/src/platform_impl/windows/keyboard_layout.rs +++ b/src/platform_impl/windows/keyboard_layout.rs @@ -272,13 +272,6 @@ impl LayoutCache { } } - /// Unconditionally recomputes the current layout, replacing any existing cache entry for it. - /// - /// Unlike [`Self::get_current_layout`], this doesn't trust an existing cache entry for the - /// current HKL. Windows can reuse an `HKL` value after the layout it identified is unloaded - /// and a different one is loaded in its place, which would otherwise leave - /// [`Self::get_current_layout`] serving key mappings for the wrong layout. Callers that - /// learn the input layout changed (e.g. on `WM_INPUTLANGCHANGE`) should call this instead. pub fn refresh_current_layout(&mut self) -> (u64, &Layout) { let locale_id = unsafe { GetKeyboardLayout(0) } as u64; let layout = Self::prepare_layout(locale_id); @@ -1003,12 +996,6 @@ fn vkey_to_non_char_key( mod tests { use super::*; - /// `get_current_layout` trusts an existing cache entry for the current `HKL` without - /// re-checking it, which is the right choice for the hot keystroke path but would serve - /// stale data if Windows reused the `HKL` value for a different layout. `refresh_current_layout` - /// exists precisely to bypass that trust, so this proves it actually recomputes the entry - /// rather than returning the cached one unchanged, by planting an entry that could not have - /// come from a real `prepare_layout(locale_id)` call and confirming it gets replaced. #[test] fn refresh_current_layout_replaces_a_stale_entry() { let mut cache = LayoutCache::default(); @@ -1016,17 +1003,10 @@ mod tests { let poisoned_hkl = !locale_id; cache.layouts.get_mut(&locale_id).unwrap().hkl = poisoned_hkl; - assert_eq!( - cache.get_current_layout().1.hkl, - poisoned_hkl, - "sanity check: get_current_layout must trust the existing entry" - ); + assert_eq!(cache.get_current_layout().1.hkl, poisoned_hkl); let (refreshed_id, refreshed_layout) = cache.refresh_current_layout(); assert_eq!(refreshed_id, locale_id); - assert_eq!( - refreshed_layout.hkl, locale_id, - "refresh_current_layout must recompute the entry instead of trusting the stale one" - ); + assert_eq!(refreshed_layout.hkl, locale_id); } }