Skip to content

Commit c22fa9c

Browse files
committed
fix: make compositor keyboard layout policy follow layout switches
Two mechanisms replace the wl_keyboard.modifiers sync, which is unreachable for hypr-rdp: modifiers events are delivered only after wl_keyboard.enter, i.e. to the client whose surface holds keyboard focus, and hypr-rdp has no surfaces. The group therefore stayed 0 forever and every modifier change reset the virtual keyboard's layout (Hyprland also silently ignores switchxkblayout for virtual keyboards, so only the virtual keyboard owner can switch its group). - Subscribe to Hyprland socket2 activelayout events and mirror external layout switches (hyprctl switchxkblayout, physical keyboards) onto the virtual keyboard. - Replicate per-key XKB state processing so group toggle options (e.g. grp:alt_shift_toggle) forwarded by the RDP client switch the tracked group in lockstep with the compositor's own processing.
1 parent afec3bd commit c22fa9c

3 files changed

Lines changed: 204 additions & 81 deletions

File tree

src/input/keyboard.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
7885
pub(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+
199250
fn 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");

src/input/rdp.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ impl RdpServerInputHandler for HyprInputHandler {
3333
let Ok(mut state) = self.state.lock() else {
3434
return;
3535
};
36-
state.refresh_keyboard_group(self.keyboard_layout_policy);
37-
3836
let t = state.timestamp();
3937
match event {
4038
KeyboardEvent::Pressed { code, extended } => {

0 commit comments

Comments
 (0)