Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/keyboard_group.rs (26.2K)
1 // SPDX-FileCopyrightText: © 2026 The River Developers
2 // SPDX-License-Identifier: GPL-3.0-only
3
4 use crate::ffi;
5 use crate::seat::Seat;
6 use crate::keyboard::KeyboardConfig;
7 use crate::xkb_bindings::XkbBinding;
8 use crate::server::wl_listener_remove;
9 use std::collections::HashMap;
10
11 #[derive(Clone, Debug, PartialEq, Eq)]
12 pub enum KeyConsumer {
13 Builtin,
14 Binding(*mut XkbBinding),
15 CceBinding(crate::config::Keybind),
16 /// A chord bound through the GlobalShortcuts portal backend (see
17 /// `global_shortcuts`): the press and the release are both reported on
18 /// the status socket's `shortcuts` topic and neither reaches the client.
19 PortalShortcut { session: String, id: String },
20 EnsureEaten,
21 ImGrab,
22 Focus,
23 }
24
25 pub struct Press {
26 pub consumer: KeyConsumer,
27 pub count: u32,
28 }
29
30 pub struct KeyboardGroup {
31 pub ref_count: u32,
32 pub seat: *mut Seat,
33 pub link: ffi::wl_list, // Seat.keyboard_groups
34 pub virtual_device: bool,
35 pub config: KeyboardConfig,
36 pub wlr_keyboard: ffi::wlr_keyboard,
37 pub modifiers_old: u32,
38 pub pressed: HashMap<u32, Press>,
39 pub key_listener: ffi::wl_listener,
40 pub modifiers_listener: ffi::wl_listener,
41 pub keyboards: ffi::wl_list, // list of keyboards in this group
42 }
43
44 impl KeyboardGroup {
45 pub unsafe fn create(
46 seat: *mut Seat,
47 config: KeyboardConfig,
48 virtual_device: bool,
49 ) -> Result<*mut Self, &'static str> {
50 let mut group = Box::new(Self {
51 ref_count: 1,
52 seat,
53 link: std::mem::zeroed(),
54 virtual_device,
55 config,
56 wlr_keyboard: std::mem::zeroed(),
57 modifiers_old: 0,
58 pressed: HashMap::new(),
59 key_listener: std::mem::zeroed(),
60 modifiers_listener: std::mem::zeroed(),
61 keyboards: std::mem::zeroed(),
62 });
63
64 ffi::wl_list_init(&mut group.keyboards);
65 ffi::wl_list_init(&mut group.link);
66
67 // Add to seat.keyboard_groups
68 let seat_groups_head = &mut (*seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
69 crate::server::wl_list_insert((*seat_groups_head).prev, &mut group.link as *mut ffi::wl_list as *mut crate::server::WlList);
70
71 let group_ptr = Box::into_raw(group);
72
73 ffi::river_wlr_keyboard_init(
74 &mut (*group_ptr).wlr_keyboard,
75 Some(led_update),
76 b"river.KeyboardGroup\0".as_ptr() as *const _,
77 );
78
79 ffi::river_wlr_keyboard_set_data(&mut (*group_ptr).wlr_keyboard, group_ptr as *mut _);
80
81 if !config.keymap.is_null() {
82 ffi::wlr_keyboard_set_keymap(&mut (*group_ptr).wlr_keyboard, config.keymap);
83 }
84 ffi::wlr_keyboard_set_repeat_info(&mut (*group_ptr).wlr_keyboard, config.repeat_rate, config.repeat_delay);
85
86 let key_listener_ptr = &mut (*group_ptr).key_listener as *mut ffi::wl_listener as *mut crate::server::WlListener;
87 (*key_listener_ptr).notify = Some(handle_group_key);
88 let key_signal = ffi::river_wlr_keyboard_get_key_signal(&mut (*group_ptr).wlr_keyboard);
89 crate::server::wl_signal_add(key_signal, &mut (*group_ptr).key_listener);
90
91 let modifiers_listener_ptr = &mut (*group_ptr).modifiers_listener as *mut ffi::wl_listener as *mut crate::server::WlListener;
92 (*modifiers_listener_ptr).notify = Some(handle_group_modifiers);
93 let modifiers_signal = ffi::river_wlr_keyboard_get_modifiers_signal(&mut (*group_ptr).wlr_keyboard);
94 crate::server::wl_signal_add(modifiers_signal, &mut (*group_ptr).modifiers_listener);
95
96 if !config.keymap.is_null() {
97 ffi::xkb_keymap_ref(config.keymap);
98 }
99
100 Ok(group_ptr)
101 }
102
103 pub unsafe fn ref_group(&mut self) -> *mut Self {
104 self.ref_count += 1;
105 self
106 }
107
108 pub unsafe fn unref(&mut self, to_release: &[u32]) {
109 for &keycode in to_release {
110 let mut event = ffi::wlr_keyboard_key_event {
111 time_msec: crate::util::msec_timestamp(),
112 keycode,
113 update_state: true,
114 state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED,
115 };
116 self.process_key(&mut event);
117 }
118
119 self.ref_count -= 1;
120 if self.ref_count > 0 {
121 return;
122 }
123
124 crate::server::wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut crate::server::WlList);
125 wl_listener_remove(&mut self.key_listener);
126 wl_listener_remove(&mut self.modifiers_listener);
127
128 // If the currently active keyboard of a seat is destroyed, we need to set a new active keyboard.
129 let active_wlr_kbd = ffi::river_wlr_seat_get_keyboard((*self.seat).wlr_seat);
130 if active_wlr_kbd == &mut self.wlr_keyboard {
131 let seat_groups_head = &mut (*self.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
132 let first_node = (*seat_groups_head).next;
133 if first_node != seat_groups_head {
134 let other_group = crate::container_of!(first_node, KeyboardGroup, link);
135 ffi::wlr_seat_set_keyboard((*self.seat).wlr_seat, &mut (*other_group).wlr_keyboard);
136 } else {
137 ffi::wlr_seat_set_keyboard((*self.seat).wlr_seat, std::ptr::null_mut());
138 }
139 }
140
141 ffi::wlr_keyboard_finish(&mut self.wlr_keyboard);
142
143 if !self.config.keymap.is_null() {
144 ffi::xkb_keymap_unref(self.config.keymap);
145 }
146
147 let _boxed = Box::from_raw(self);
148 }
149
150 pub unsafe fn match_config(&self, config: *mut KeyboardConfig) -> bool {
151 if self.config.repeat_rate != (*config).repeat_rate {
152 return false;
153 }
154 if self.config.repeat_delay != (*config).repeat_delay {
155 return false;
156 }
157 if self.config.keymap == (*config).keymap {
158 return true;
159 }
160 if self.config.keymap.is_null() || (*config).keymap.is_null() {
161 return false;
162 }
163
164 let a_string_ptr = ffi::xkb_keymap_get_as_string(self.config.keymap, ffi::xkb_keymap_format_XKB_KEYMAP_FORMAT_TEXT_V1);
165 if a_string_ptr.is_null() {
166 return false;
167 }
168 let b_string_ptr = ffi::xkb_keymap_get_as_string((*config).keymap, ffi::xkb_keymap_format_XKB_KEYMAP_FORMAT_TEXT_V1);
169 if b_string_ptr.is_null() {
170 libc::free(a_string_ptr as *mut _);
171 return false;
172 }
173
174 let a_str = std::ffi::CStr::from_ptr(a_string_ptr);
175 let b_str = std::ffi::CStr::from_ptr(b_string_ptr);
176 let matched = a_str == b_str;
177
178 libc::free(a_string_ptr as *mut _);
179 libc::free(b_string_ptr as *mut _);
180
181 if matched {
182 ffi::xkb_keymap_unref((*config).keymap);
183 (*config).keymap = self.config.keymap;
184 ffi::xkb_keymap_ref((*config).keymap);
185 }
186
187 matched
188 }
189
190 pub unsafe fn process_key(&mut self, event: *const ffi::wlr_keyboard_key_event) {
191 if let Some(key) = self.pressed.get_mut(&(*event).keycode) {
192 assert!(key.count > 0);
193 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
194 key.count += 1;
195 } else {
196 key.count -= 1;
197 if key.count == 0 {
198 let mut key_event = ffi::wlr_keyboard_key_event {
199 time_msec: (*event).time_msec,
200 keycode: (*event).keycode,
201 update_state: true,
202 state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED,
203 };
204 ffi::wlr_keyboard_notify_key(&mut self.wlr_keyboard, &mut key_event);
205 }
206 }
207 } else if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
208 if self.pressed.len() < 32 {
209 let mut key_event = ffi::wlr_keyboard_key_event {
210 time_msec: (*event).time_msec,
211 keycode: (*event).keycode,
212 update_state: true,
213 state: ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED,
214 };
215 ffi::wlr_keyboard_notify_key(&mut self.wlr_keyboard, &mut key_event);
216 }
217 }
218 }
219
220 pub unsafe fn process_modifiers(&mut self, modifiers: ffi::wlr_keyboard_modifiers) {
221 ffi::wlr_keyboard_notify_modifiers(
222 &mut self.wlr_keyboard,
223 modifiers.depressed,
224 modifiers.latched,
225 modifiers.locked,
226 modifiers.group,
227 );
228 }
229
230 pub unsafe fn process_keymap(&mut self, keymap: *mut ffi::xkb_keymap) {
231 ffi::wlr_keyboard_set_keymap(&mut self.wlr_keyboard, keymap);
232 }
233
234 pub unsafe fn get_input_method_grab(&self) -> *mut ffi::wlr_input_method_keyboard_grab_v2 {
235 if self.virtual_device {
236 return std::ptr::null_mut();
237 }
238 let input_method = (*self.seat).relay.input_method;
239 if !input_method.is_null() {
240 return (*input_method).keyboard_grab;
241 }
242 std::ptr::null_mut()
243 }
244
245 pub unsafe fn send_state(&mut self) {
246 let keymap = self.config.keymap;
247 if keymap.is_null() {
248 return;
249 }
250 let layout_index = self.wlr_keyboard.modifiers.group;
251 let layout_name = ffi::xkb_keymap_layout_get_name(keymap, layout_index);
252 let caps_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Caps Lock\0".as_ptr() as *const _);
253 let capslock = if caps_idx != ffi::XKB_MOD_INVALID {
254 let caps_mask = 1 << caps_idx;
255 (self.wlr_keyboard.modifiers.locked & caps_mask) != 0
256 } else {
257 false
258 };
259 let num_idx = ffi::xkb_keymap_mod_get_index(keymap, b"Num Lock\0".as_ptr() as *const _);
260 let numlock = if num_idx != ffi::XKB_MOD_INVALID {
261 let num_mask = 1 << num_idx;
262 (self.wlr_keyboard.modifiers.locked & num_mask) != 0
263 } else {
264 false
265 };
266
267 let server = (*self.seat).server;
268 let keyboards_head = &mut (*server).xkb_config.keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
269 let mut curr = (*keyboards_head).next;
270 while curr != keyboards_head {
271 let next = (*curr).next;
272 let xkb_kbd = crate::container_of!(curr, crate::xkb_keyboard::XkbKeyboard, link);
273 let parent_dev = (*xkb_kbd).parent_device;
274 let kbd = (*parent_dev).destroy_data as *mut crate::keyboard::Keyboard;
275 if !kbd.is_null() && (*kbd).group == self as *mut KeyboardGroup {
276 (*xkb_kbd).send_state(layout_index, layout_name, capslock, numlock);
277 }
278 curr = next;
279 }
280 }
281 }
282
283 unsafe fn handle_builtin_binding(seat: *mut Seat, keysym: u32, modifiers: u32) -> bool {
284 match keysym {
285 ffi::XKB_KEY_XF86Switch_VT_1..=ffi::XKB_KEY_XF86Switch_VT_12 => {
286 log::debug!("switch VT keysym received");
287 let server = (*seat).server;
288 let session = (*server).session;
289 if !session.is_null() {
290 let vt = keysym - ffi::XKB_KEY_XF86Switch_VT_1 + 1;
291 log::info!("switching to VT {}", vt);
292 ffi::wlr_session_change_vt(session, vt);
293 }
294 true
295 }
296 // Plain Escape closes any open in-surface status menu — the keyboard
297 // twin of the click-away dismiss in cursor.rs, consumed the same way
298 // a builtin is (the release is eaten with the press via the consumer
299 // map), so the focused window never sees it. Gated on a menu
300 // actually being open and on NO modifiers: a chorded Escape stays a
301 // bindable/forwardable key, and with nothing expanded this arm never
302 // fires at all.
303 ffi::XKB_KEY_Escape if modifiers == 0 => {
304 let server = (*seat).server;
305 if !(*server).wm.any_expanded_status_segment(std::ptr::null_mut()) {
306 return false;
307 }
308 log::debug!("Escape dismisses the open status menu");
309 if let Some(ref sender) = (*server).wm.status_sender {
310 sender.send_menu_dismiss("-");
311 }
312 true
313 }
314 _ => false,
315 }
316 }
317
318 unsafe extern "C" fn led_update(wlr_keyboard: *mut ffi::wlr_keyboard, leds: u32) {
319 let group = ffi::river_wlr_keyboard_get_data(wlr_keyboard) as *mut KeyboardGroup;
320 if group.is_null() {
321 return;
322 }
323
324 let keyboards_head = &mut (*group).keyboards as *mut ffi::wl_list as *mut crate::server::WlList;
325 let mut curr = (*keyboards_head).next;
326 while curr != keyboards_head {
327 let next = (*curr).next;
328 let keyboard = crate::container_of!(curr, crate::keyboard::Keyboard, group_link);
329 ffi::wlr_keyboard_led_update((*keyboard).wlr_keyboard, leds);
330 curr = next;
331 }
332 }
333
334 unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
335 let group = &mut *crate::container_of!(listener, KeyboardGroup, key_listener);
336 let event = data as *mut ffi::wlr_keyboard_key_event;
337
338 let xkb_state = group.wlr_keyboard.xkb_state;
339 if xkb_state.is_null() {
340 log::error!("no xkb_state available");
341 return;
342 }
343
344 // Keys are activity for the idle timeouts (and the idle-notify clients)
345 // exactly as pointer events are; before 2026-09-16 only tablet, touch
346 // and gestures counted, so idle-notify clients never saw a key.
347 if !group.seat.is_null() {
348 (*group.seat).handle_activity();
349 }
350
351 // A real key press ends an emulated view drag (see `cursor::ViewDrag`):
352 // the drag holds Space and a pointer button down on the client's behalf,
353 // and a key pressed on top of that reaches the app as a chord nobody
354 // asked for. The drag's own synthetic Space goes straight to the seat and
355 // never passes through here.
356 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
357 && !group.seat.is_null()
358 && (*group.seat).cursor.view_drag.is_some()
359 {
360 (*group.seat).cursor.end_view_drag("key");
361 }
362 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
363 && !group.seat.is_null()
364 && (*group.seat).cursor.popup_wheel.is_some()
365 {
366 (*group.seat).cursor.end_popup_wheel("key");
367 }
368 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
369 && !group.seat.is_null()
370 && (*group.seat).cursor.hscroll_shift.is_some()
371 {
372 (*group.seat).cursor.end_hscroll_shift("key");
373 }
374
375 // Cancel active binding repeats
376 let seat_groups_head = &mut (*group.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
377 let mut curr_g = (*seat_groups_head).next;
378 while curr_g != seat_groups_head {
379 let next_g = (*curr_g).next;
380 let g = crate::container_of!(curr_g, KeyboardGroup, link);
381 for press in (*g).pressed.values() {
382 if let KeyConsumer::Binding(binding) = press.consumer {
383 if !binding.is_null() {
384 (*binding).stop_repeat();
385 }
386 }
387 }
388 curr_g = next_g;
389 }
390
391 let consumer: KeyConsumer = if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED {
392 if let Some(kv) = group.pressed.remove(&(*event).keycode) {
393 assert!(kv.count == 0);
394 kv.consumer
395 } else {
396 KeyConsumer::Focus
397 }
398 } else {
399 let xkb_keycode = (*event).keycode + 8;
400 let modifiers = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
401
402 let mut matched_builtin = false;
403 let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
404 let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
405 log::debug!("handle_group_key keycode={}, xkb_keycode={}, num_syms={}", (*event).keycode, xkb_keycode, num_syms);
406 if num_syms > 0 && !syms_ptr.is_null() {
407 let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
408 for &sym in syms {
409 log::debug!(" keysym={:#x}", sym);
410 if handle_builtin_binding(group.seat, sym, modifiers) {
411 matched_builtin = true;
412 break;
413 }
414 }
415 }
416
417 if matched_builtin {
418 KeyConsumer::Builtin
419 } else if let Some(kb) = match_cce_keybind(&(*(*group.seat).server).wm, xkb_keycode, modifiers, xkb_state) {
420 log::debug!("matched CCE monolithic keybind: {:?}", kb);
421 KeyConsumer::CceBinding(kb)
422 } else if let Some((session, id)) = match_portal_shortcut(&(*(*group.seat).server).wm, xkb_keycode, modifiers, xkb_state) {
423 log::debug!("matched portal shortcut {} {}", session, id);
424 KeyConsumer::PortalShortcut { session, id }
425 } else if let Some(binding) = (*group.seat).match_xkb_binding(xkb_keycode, &mut group.wlr_keyboard) {
426 log::debug!("matched xkb binding");
427 (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
428 KeyConsumer::Binding(if (*binding).sent_pressed {
429 std::ptr::null_mut()
430 } else {
431 binding
432 })
433 } else if (*group.seat).xkb_bindings_seat.ensure_next_key_eaten {
434 let mut has_non_modifier = false;
435 let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
436 let num_syms = ffi::xkb_state_key_get_syms(xkb_state, xkb_keycode, &mut syms_ptr);
437 if num_syms > 0 && !syms_ptr.is_null() {
438 let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
439 for &sym in syms {
440 if !crate::keyboard::keysym_is_modifier(sym) {
441 has_non_modifier = true;
442 break;
443 }
444 }
445 }
446 if has_non_modifier {
447 (*group.seat).xkb_bindings_seat.ensure_next_key_eaten = false;
448 KeyConsumer::EnsureEaten
449 } else {
450 KeyConsumer::Focus
451 }
452 } else if !group.get_input_method_grab().is_null() {
453 KeyConsumer::ImGrab
454 } else {
455 KeyConsumer::Focus
456 }
457 };
458
459 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
460 group.pressed.insert(
461 (*event).keycode,
462 Press {
463 consumer: consumer.clone(),
464 count: 1,
465 },
466 );
467 }
468
469 match consumer {
470 KeyConsumer::Builtin => {}
471 KeyConsumer::CceBinding(kb) => {
472 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
473 let wm = &mut (*(*group.seat).server).wm;
474 // A key carries no pointer position: the overview toggle's
475 // exit must land on the focused window, not on whatever the
476 // pointer was left hovering (or the empty desktop under it).
477 // Same substitution the control socket makes.
478 let action = if kb.action == crate::config::Action::Overview {
479 wm.overview_action_pointerless()
480 } else {
481 kb.action
482 };
483 log::info!("executing CCE monolithic action: {:?}", action);
484 wm.execute_action(&action, kb.command.as_deref());
485 }
486 }
487 KeyConsumer::PortalShortcut { session, id } => {
488 // Both edges go out: the portal has a Deactivated signal, and
489 // the release comes back here through the consumer map with the
490 // same variant the press recorded.
491 let pressed = (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED;
492 if let Some(ref sender) = (*(*group.seat).server).wm.status_sender {
493 sender.send_shortcut_event(&format!(
494 "{} {} {} {}",
495 if pressed { "activated" } else { "deactivated" },
496 session,
497 id,
498 (*event).time_msec
499 ));
500 }
501 }
502 KeyConsumer::Binding(binding) => {
503 if !binding.is_null() {
504 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
505 (*binding).pressed();
506 } else {
507 (*binding).released();
508 }
509 }
510 }
511 KeyConsumer::EnsureEaten => {
512 if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
513 (*group.seat).xkb_bindings_seat.scheduled_ate_unbound_key = true;
514 (*(*group.seat).server).wm.dirty_windowing();
515 }
516 }
517 KeyConsumer::ImGrab => {
518 let grab = group.get_input_method_grab();
519 if !grab.is_null() {
520 ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
521 ffi::wlr_input_method_keyboard_grab_v2_send_key(grab, (*event).time_msec, (*event).keycode, (*event).state);
522 }
523 }
524 KeyConsumer::Focus => {
525 // Overlay AND Popup: desktop chrome (docks, the cce-cloud
526 // launcher) stays keyboard-interactive in overview — only world
527 // windows (spatial thumbnails) have their presses eaten.
528 let is_overlay_mode = (*group.seat).focus_is_chrome();
529
530 if (*(*group.seat).server).wm.mode != crate::window_manager::WindowManagerMode::Overview
531 || is_overlay_mode
532 || (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_RELEASED
533 {
534 ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
535 ffi::wlr_seat_keyboard_notify_key((*group.seat).wlr_seat, (*event).time_msec, (*event).keycode, (*event).state);
536 }
537 }
538 }
539
540 group.send_state();
541 }
542
543 pub unsafe fn match_cce_keybind(
544 wm: &crate::window_manager::WindowManager,
545 keycode: u32,
546 modifiers: u32,
547 xkb_state: *mut ffi::xkb_state,
548 ) -> Option<crate::config::Keybind> {
549 match_chord(keycode, modifiers, xkb_state, |mods, sym| {
550 wm.keybinds.iter().find(|kb| kb.mods == mods && kb.keysym == sym).cloned()
551 })
552 }
553
554 /// The portal-bound chords (`global_shortcuts`), matched exactly like the
555 /// config keybinds but consulted after them. Returns `(session, id)`.
556 pub unsafe fn match_portal_shortcut(
557 wm: &crate::window_manager::WindowManager,
558 keycode: u32,
559 modifiers: u32,
560 xkb_state: *mut ffi::xkb_state,
561 ) -> Option<(String, String)> {
562 if wm.portal_shortcuts.is_empty() {
563 return None;
564 }
565 match_chord(keycode, modifiers, xkb_state, |mods, sym| {
566 wm.portal_shortcuts
567 .iter()
568 .find(|s| s.mods == mods && s.keysym == sym)
569 .map(|s| (s.session.clone(), s.id.clone()))
570 })
571 }
572
573 /// Chord lookup shared by every (mods, keysym) table. `probe` is asked
574 /// twice over: first with the keycode's level-0 keysyms against the raw
575 /// modifier mask (so `super+shift+h` matches on `h`, not `H`), then with the
576 /// keysyms of the level the modifiers actually select against the mask with
577 /// the consumed modifiers removed (so a bind on a shifted symbol like
578 /// `plus` still fires). The first hit wins.
579 unsafe fn match_chord<T>(
580 keycode: u32,
581 modifiers: u32,
582 xkb_state: *mut ffi::xkb_state,
583 probe: impl Fn(u32, u32) -> Option<T>,
584 ) -> Option<T> {
585 if xkb_state.is_null() {
586 return None;
587 }
588 let keymap = ffi::xkb_state_get_keymap(xkb_state);
589 if keymap.is_null() {
590 return None;
591 }
592 let layout = ffi::xkb_state_key_get_layout(xkb_state, keycode);
593
594 let mut syms_ptr: *const ffi::xkb_keysym_t = std::ptr::null();
595 let num_syms = ffi::xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, 0, &mut syms_ptr);
596 if num_syms > 0 && !syms_ptr.is_null() {
597 let syms = std::slice::from_raw_parts(syms_ptr, num_syms as usize);
598 for &sym in syms {
599 if let Some(hit) = probe(modifiers, sym) {
600 return Some(hit);
601 }
602 }
603 }
604
605 let level = ffi::xkb_state_key_get_level(xkb_state, keycode, layout);
606 let mut syms_ptr_level: *const ffi::xkb_keysym_t = std::ptr::null();
607 let num_syms_level = ffi::xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, level, &mut syms_ptr_level);
608 if num_syms_level > 0 && !syms_ptr_level.is_null() {
609 let syms = std::slice::from_raw_parts(syms_ptr_level, num_syms_level as usize);
610 let consumed = ffi::xkb_state_key_get_consumed_mods2(xkb_state, keycode, ffi::xkb_consumed_mode_XKB_CONSUMED_MODE_XKB);
611 let modifiers_translated = modifiers & !consumed;
612 for &sym in syms {
613 if let Some(hit) = probe(modifiers_translated, sym) {
614 return Some(hit);
615 }
616 }
617 }
618
619 None
620 }
621
622 unsafe extern "C" fn handle_group_modifiers(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
623 let group = &mut *crate::container_of!(listener, KeyboardGroup, modifiers_listener);
624
625 let old = group.modifiers_old;
626 let new = ffi::wlr_keyboard_get_modifiers(&mut group.wlr_keyboard);
627 let watched = (*group.seat).xkb_bindings_seat.requested_mods_watched;
628 if (old & watched) != (new & watched) {
629 (*group.seat).xkb_bindings_seat.scheduled_mods_update = Some(crate::xkb_bindings::XkbBindingsSeatModsUpdate {
630 old,
631 new,
632 });
633 (*(*group.seat).server).wm.dirty_windowing();
634 }
635 group.modifiers_old = new;
636
637 let grab = group.get_input_method_grab();
638 if !grab.is_null() {
639 ffi::wlr_input_method_keyboard_grab_v2_set_keyboard(grab, &mut group.wlr_keyboard);
640 ffi::wlr_input_method_keyboard_grab_v2_send_modifiers(grab, &mut group.wlr_keyboard.modifiers);
641 } else {
642 ffi::wlr_seat_set_keyboard((*group.seat).wlr_seat, &mut group.wlr_keyboard);
643 ffi::wlr_seat_keyboard_notify_modifiers((*group.seat).wlr_seat, &mut group.wlr_keyboard.modifiers);
644 }
645 // Window-adjust mode (Super held) reads the seat keyboard's mask, which
646 // is this group's — the DEVICE keyboard has no keymap on the DRM
647 // backend (see keyboard::should_set_keymap), so its own modifiers
648 // signal never fires there; this one does for every real key.
649 (*(*group.seat).server).wm.refresh_adjust_held();
650
651 group.send_state();
652 }