@@ -75,13 +75,22 @@ pub(super) struct UnicodeKeyMapping {
7575 pub ( super ) needs_shift : bool ,
7676}
7777
78+ /// `xkb::State` holds a raw pointer without thread affinity; libxkbcommon
79+ /// objects may move between threads as long as access is externally
80+ /// synchronized, which the `InputState` mutex guarantees.
81+ struct SendXkbState ( xkb:: State ) ;
82+
83+ unsafe impl Send for SendXkbState { }
84+
7885pub ( super ) struct KeyboardStateTracker {
7986 modifier_masks_by_key : HashMap < u32 , u32 > ,
8087 unicode_to_keycode : HashMap < u16 , UnicodeKeyMapping > ,
88+ layout_names : Vec < String > ,
8189 pressed_keys : HashSet < u32 > ,
8290 depressed_mods : u32 ,
8391 locked_mods : u32 ,
8492 group : u32 ,
93+ xkb_state : SendXkbState ,
8594 caps_lock_mask : u32 ,
8695 num_lock_mask : u32 ,
8796 scroll_lock_mask : u32 ,
@@ -103,10 +112,12 @@ impl KeyboardStateTracker {
103112 Ok ( Self {
104113 modifier_masks_by_key : build_modifier_masks_by_key ( & keymap) ,
105114 unicode_to_keycode : build_unicode_to_keycode ( & keymap) ,
115+ layout_names : build_layout_names ( & keymap) ,
106116 pressed_keys : HashSet :: new ( ) ,
107117 depressed_mods : 0 ,
108118 locked_mods : 0 ,
109119 group : 0 ,
120+ xkb_state : SendXkbState ( xkb:: State :: new ( & keymap) ) ,
110121 caps_lock_mask : locked_mask_for_key ( & keymap, KEY_CAPSLOCK ) ,
111122 num_lock_mask : locked_mask_for_key ( & keymap, KEY_NUMLOCK ) ,
112123 scroll_lock_mask : locked_mask_for_key ( & keymap, KEY_SCROLLLOCK ) ,
@@ -130,6 +141,22 @@ impl KeyboardStateTracker {
130141 self . pressed_keys . remove ( & evdev_key) ;
131142 }
132143
144+ // Replicate the compositor's per-key XKB processing so group toggle
145+ // options (e.g. grp:alt_shift_toggle) keep the tracked group in sync
146+ // with the group the compositor switches to on the same key stream.
147+ let direction = if pressed {
148+ xkb:: KeyDirection :: Down
149+ } else {
150+ xkb:: KeyDirection :: Up
151+ } ;
152+ self . xkb_state
153+ . 0
154+ . update_key ( xkb:: Keycode :: new ( evdev_key + XKB_KEYCODE_OFFSET ) , direction) ;
155+ self . group = self
156+ . xkb_state
157+ . 0
158+ . serialize_layout ( xkb:: STATE_LAYOUT_EFFECTIVE ) ;
159+
133160 self . depressed_mods = self
134161 . pressed_keys
135162 . iter ( )
@@ -144,9 +171,28 @@ impl KeyboardStateTracker {
144171 }
145172
146173 pub ( super ) fn set_group ( & mut self , group : u32 ) {
174+ if self . group == group {
175+ return ;
176+ }
177+ // Force the locked layout while preserving the current modifier view,
178+ // so later update_key calls keep toggling relative to the new group.
179+ let depressed = self . xkb_state . 0 . serialize_mods ( xkb:: STATE_MODS_DEPRESSED ) ;
180+ let latched = self . xkb_state . 0 . serialize_mods ( xkb:: STATE_MODS_LATCHED ) ;
181+ let locked = self . xkb_state . 0 . serialize_mods ( xkb:: STATE_MODS_LOCKED ) ;
182+ self . xkb_state
183+ . 0
184+ . update_mask ( depressed, latched, locked, 0 , 0 , group) ;
147185 self . group = group;
148186 }
149187
188+ /// Resolve an XKB layout display name (e.g. "Russian") to its group index.
189+ pub ( super ) fn layout_index_by_name ( & self , name : & str ) -> Option < u32 > {
190+ self . layout_names
191+ . iter ( )
192+ . position ( |layout| layout == name)
193+ . map ( |index| index as u32 )
194+ }
195+
150196 pub ( super ) fn send_modifiers ( & self , vk : & ZwpVirtualKeyboardV1 ) {
151197 let state = self . modifier_state ( ) ;
152198 vk. modifiers ( state. depressed , state. latched , state. locked , state. group ) ;
@@ -161,7 +207,6 @@ impl KeyboardStateTracker {
161207 }
162208 }
163209
164- #[ cfg( test) ]
165210 pub ( super ) fn group ( & self ) -> u32 {
166211 self . group
167212 }
@@ -196,6 +241,12 @@ impl KeyboardStateTracker {
196241 }
197242}
198243
244+ fn build_layout_names ( keymap : & xkb:: Keymap ) -> Vec < String > {
245+ ( 0 ..keymap. num_layouts ( ) )
246+ . map ( |layout| keymap. layout_get_name ( layout) . to_owned ( ) )
247+ . collect ( )
248+ }
249+
199250fn compile_xkb_keymap ( keymap_data : & [ u8 ] ) -> Result < xkb:: Keymap > {
200251 let context = xkb:: Context :: new ( xkb:: CONTEXT_NO_FLAGS ) ;
201252 let keymap_text =
@@ -397,6 +448,64 @@ mod tests {
397448 assert_eq ! ( tracker. modifier_state( ) . group, 1 ) ;
398449 }
399450
451+ #[ test]
452+ fn layout_index_by_name_resolves_group_indices ( ) {
453+ let keymap = generate_xkb_keymap_from_names ( & XkbKeymapNames {
454+ layout : Some ( "us,ru" . into ( ) ) ,
455+ ..Default :: default ( )
456+ } )
457+ . expect ( "multi-layout keymap compiles" ) ;
458+ let tracker = KeyboardStateTracker :: new ( & keymap) . expect ( "generated keymap loads" ) ;
459+
460+ assert_eq ! ( tracker. layout_index_by_name( "English (US)" ) , Some ( 0 ) ) ;
461+ assert_eq ! ( tracker. layout_index_by_name( "Russian" ) , Some ( 1 ) ) ;
462+ assert_eq ! ( tracker. layout_index_by_name( "German" ) , None ) ;
463+ }
464+
465+ #[ test]
466+ fn alt_shift_toggles_layout_group ( ) {
467+ let keymap = generate_xkb_keymap_from_names ( & XkbKeymapNames {
468+ layout : Some ( "us,ru" . into ( ) ) ,
469+ options : Some ( "grp:alt_shift_toggle" . into ( ) ) ,
470+ ..Default :: default ( )
471+ } )
472+ . expect ( "multi-layout keymap compiles" ) ;
473+ let mut tracker = KeyboardStateTracker :: new ( & keymap) . expect ( "generated keymap loads" ) ;
474+
475+ // 56 = KEY_LEFTALT, 42 = KEY_LEFTSHIFT
476+ tracker. key ( 56 , true ) ;
477+ tracker. key ( 42 , true ) ;
478+ tracker. key ( 42 , false ) ;
479+ tracker. key ( 56 , false ) ;
480+ assert_eq ! ( tracker. group( ) , 1 ) ;
481+
482+ tracker. key ( 56 , true ) ;
483+ tracker. key ( 42 , true ) ;
484+ tracker. key ( 42 , false ) ;
485+ tracker. key ( 56 , false ) ;
486+ assert_eq ! ( tracker. group( ) , 0 ) ;
487+ }
488+
489+ #[ test]
490+ fn external_group_switch_composes_with_alt_shift_toggle ( ) {
491+ let keymap = generate_xkb_keymap_from_names ( & XkbKeymapNames {
492+ layout : Some ( "us,ru" . into ( ) ) ,
493+ options : Some ( "grp:alt_shift_toggle" . into ( ) ) ,
494+ ..Default :: default ( )
495+ } )
496+ . expect ( "multi-layout keymap compiles" ) ;
497+ let mut tracker = KeyboardStateTracker :: new ( & keymap) . expect ( "generated keymap loads" ) ;
498+
499+ tracker. set_group ( 1 ) ;
500+ assert_eq ! ( tracker. group( ) , 1 ) ;
501+
502+ tracker. key ( 56 , true ) ;
503+ tracker. key ( 42 , true ) ;
504+ tracker. key ( 42 , false ) ;
505+ tracker. key ( 56 , false ) ;
506+ assert_eq ! ( tracker. group( ) , 0 ) ;
507+ }
508+
400509 #[ test]
401510 fn normal_key_does_not_report_modifier_state_change ( ) {
402511 let keymap = generate_xkb_keymap ( ) . expect ( "default keymap compiles" ) ;
0 commit comments