From 3037968223aa29d31a61dc85e65bb36c9044d855 Mon Sep 17 00:00:00 2001 From: Wanten Date: Mon, 2 Mar 2026 17:59:02 +0800 Subject: [PATCH 1/5] feat: input-method-v1 --- src/wayland/input_method_v1/mod.rs | 1142 ++++++++++++++++++++++++++++ src/wayland/mod.rs | 1 + 2 files changed, 1143 insertions(+) create mode 100644 src/wayland/input_method_v1/mod.rs diff --git a/src/wayland/input_method_v1/mod.rs b/src/wayland/input_method_v1/mod.rs new file mode 100644 index 000000000000..2d0337759fc0 --- /dev/null +++ b/src/wayland/input_method_v1/mod.rs @@ -0,0 +1,1142 @@ +//! Utilities for text-input-v1 and input-method-v1 support. +//! +//! This module wires together the unstable `text-input-v1`, +//! `input-method-v1` and `input-panel-v1` protocols. +//! +//! The high-level integration pattern is: +//! - Implement [`InputMethodV1Handler`] for your compositor state. +//! - Create globals through [`InputMethodV1ManagerState::new`]. +//! - Delegate protocol dispatch with [`delegate_input_method_manager_v1!`]. + +use std::fmt; +use std::sync::{Arc, Mutex}; + +use tracing::warn; +use wayland_protocols::wp::input_method::zv1::server::{ + zwp_input_method_context_v1::{self, ZwpInputMethodContextV1}, + zwp_input_method_v1::{self, ZwpInputMethodV1}, + zwp_input_panel_surface_v1::{self, ZwpInputPanelSurfaceV1}, + zwp_input_panel_v1::{self, ZwpInputPanelV1}, +}; +use wayland_protocols::wp::text_input::zv1::server::{ + zwp_text_input_manager_v1::{self, ZwpTextInputManagerV1}, + zwp_text_input_v1::{self, ZwpTextInputV1}, +}; +use wayland_server::{ + backend::{ClientId, GlobalId}, + protocol::{ + wl_keyboard::{self, KeymapFormat, WlKeyboard}, + wl_seat::WlSeat, + wl_surface::WlSurface, + }, + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; + +use crate::input::{ + keyboard::{ + GrabStartData as KeyboardGrabStartData, KeyboardGrab, KeyboardHandle, KeyboardInnerHandle, + ModifiersState, + }, + Seat, SeatHandler, +}; +use crate::utils::{Logical, Point, Rectangle, SERIAL_COUNTER}; +use crate::wayland::seat::keyboard::for_each_focused_kbds; + +const INPUT_METHOD_VERSION: u32 = 1; +const TEXT_INPUT_VERSION: u32 = 1; +const INPUT_PANEL_VERSION: u32 = 1; + +/// Parent surface and geometry used to place an IME popup. +#[derive(Debug, Clone)] +pub struct PopupParent { + /// The parent `wl_surface` the popup is associated with. + pub surface: WlSurface, + /// Parent geometry in compositor logical coordinates. + pub location: Rectangle, +} + +/// Handle to an input panel popup surface (`zwp_input_panel_surface_v1`). +#[derive(Debug, Clone)] +pub struct PopupSurface { + surface_role: ZwpInputPanelSurfaceV1, + surface: WlSurface, + rectangle: Arc>>, + location: Arc>>, + parent: Option, +} + +impl PopupSurface { + fn new(surface_role: ZwpInputPanelSurfaceV1, surface: WlSurface, parent: Option) -> Self { + Self { + surface_role, + surface, + rectangle: Arc::new(Mutex::new(Rectangle::default())), + location: Arc::new(Mutex::new(Point::default())), + parent, + } + } + + /// Returns `true` if both the role object and `wl_surface` are still alive. + pub fn alive(&self) -> bool { + self.surface_role.is_alive() && self.surface.is_alive() + } + + /// Access the underlying `wl_surface`. + pub fn wl_surface(&self) -> &WlSurface { + &self.surface + } + + /// Access the current popup parent, if any. + pub fn get_parent(&self) -> Option<&PopupParent> { + self.parent.as_ref() + } + + /// Update the popup parent. + pub fn set_parent(&mut self, parent: Option) { + self.parent = parent; + } + + /// Popup location relative to its parent. + pub fn location(&self) -> Point { + *self.location.lock().unwrap() + } + + /// Set popup location relative to its parent. + pub fn set_location(&self, location: Point) { + *self.location.lock().unwrap() = location; + } + + /// Current text cursor rectangle associated with the popup. + pub fn text_input_rectangle(&self) -> Rectangle { + *self.rectangle.lock().unwrap() + } + + /// Set the text cursor rectangle associated with the popup. + pub fn set_text_input_rectangle(&self, rect: Rectangle) { + *self.rectangle.lock().unwrap() = rect; + } +} + +impl PartialEq for PopupSurface { + fn eq(&self, other: &Self) -> bool { + self.surface_role == other.surface_role + } +} + +/// Callbacks used by the v1 input method helpers to manage popup lifecycle. +pub trait InputMethodV1Handler { + /// A new popup should be added to compositor state. + fn new_popup(&mut self, surface: PopupSurface); + /// A popup should be removed from compositor state. + fn dismiss_popup(&mut self, surface: PopupSurface); + /// Popup location or geometry changed. + fn popup_repositioned(&mut self, surface: PopupSurface); + /// Returns geometry for a parent surface used for popup placement. + fn parent_geometry(&self, parent: &WlSurface) -> Rectangle; +} + +#[derive(Clone, Default, Debug)] +struct TextInputV1State { + active: bool, + seat: Option, + surface: Option, + serial: u32, + cursor_rectangle: Rectangle, +} + +#[derive(Default)] +struct InputMethodV1KeyboardGrabState { + grab: Option, +} + +#[derive(Default, Clone)] +struct InputMethodV1KeyboardGrab { + inner: Arc>, +} + +impl KeyboardGrab for InputMethodV1KeyboardGrab +where + D: SeatHandler + 'static, +{ + fn input( + &mut self, + _data: &mut D, + _handle: &mut KeyboardInnerHandle<'_, D>, + keycode: crate::backend::input::Keycode, + key_state: crate::backend::input::KeyState, + modifiers: Option, + serial: crate::utils::Serial, + time: u32, + ) { + let Some(kbd) = self.inner.lock().unwrap().grab.clone() else { + return; + }; + + let serial = serial.0; + kbd.key(serial, time, keycode.raw() - 8, key_state.into()); + + if let Some(serialized) = modifiers.map(|m| m.serialized) { + kbd.modifiers( + serial, + serialized.depressed, + serialized.latched, + serialized.locked, + serialized.layout_effective, + ); + } + } + + fn set_focus( + &mut self, + data: &mut D, + handle: &mut KeyboardInnerHandle<'_, D>, + focus: Option<::KeyboardFocus>, + serial: crate::utils::Serial, + ) { + handle.set_focus(data, focus, serial) + } + + fn start_data(&self) -> &KeyboardGrabStartData { + &KeyboardGrabStartData { focus: None } + } + + fn unset(&mut self, _data: &mut D) {} +} + +#[derive(Default)] +struct ActiveContext { + context: Option, + text_input: Option, + seat: Option, + surface: Option, +} + +#[derive(Default)] +struct InputMethodV1Inner { + input_method: Option, + text_inputs: Vec, + active: ActiveContext, + keyboard_grab: InputMethodV1KeyboardGrab, + popup: Option, + popup_enabled: bool, + popup_visible: bool, +} + +#[derive(Default, Clone)] +/// Internal handle shared between text-input-v1 and input-method-v1 objects. +pub struct InputMethodV1Handle { + inner: Arc>, +} + +impl fmt::Debug for InputMethodV1Handle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InputMethodV1Handle").finish() + } +} + +impl InputMethodV1Handle { + fn add_text_input(&self, text_input: &ZwpTextInputV1) { + self.inner.lock().unwrap().text_inputs.push(text_input.clone()); + } + + fn remove_text_input(&self, text_input: &ZwpTextInputV1) { + self.inner + .lock() + .unwrap() + .text_inputs + .retain(|ti| ti.id() != text_input.id()); + } + + fn hide_popup(&self, state: &mut D) { + let (popup, im) = { + let mut inner = self.inner.lock().unwrap(); + if !inner.popup_visible { + return; + } + inner.popup_visible = false; + let popup = inner.popup.clone(); + let im = inner.input_method.clone(); + if let Some(p) = inner.popup.as_mut() { + p.set_parent(None); + } + (popup, im) + }; + + let (Some(popup), Some(im)) = (popup, im) else { + return; + }; + let data = im.data::>().unwrap(); + (data.dismiss_popup)(state, popup); + } + + fn refresh_popup(&self, state: &mut D) { + let (mut popup, im, parent_surface, text_rect, was_visible, enabled) = { + let inner = self.inner.lock().unwrap(); + ( + inner.popup.clone(), + inner.input_method.clone(), + inner.active.surface.clone(), + inner + .active + .text_input + .as_ref() + .and_then(|ti| ti.data::()) + .map(|ud| ud.state.lock().unwrap().cursor_rectangle) + .unwrap_or_default(), + inner.popup_visible, + inner.popup_enabled, + ) + }; + + let (Some(mut popup_value), Some(im), Some(parent_surface)) = (popup.take(), im, parent_surface) + else { + self.hide_popup(state); + return; + }; + + if !enabled { + self.hide_popup(state); + return; + } + + let data = im.data::>().unwrap(); + let parent = PopupParent { + location: (data.popup_geometry_callback)(state, &parent_surface), + surface: parent_surface, + }; + + popup_value.set_parent(Some(parent)); + popup_value.set_text_input_rectangle(text_rect); + popup_value.set_location((text_rect.loc.x, text_rect.loc.y + text_rect.size.h).into()); + + { + let mut inner = self.inner.lock().unwrap(); + inner.popup = Some(popup_value.clone()); + inner.popup_visible = true; + } + + if was_visible { + (data.popup_repositioned)(state, popup_value); + } else { + (data.new_popup)(state, popup_value); + } + } + + fn deactivate_if_active_for( + &self, + state: &mut D, + text_input: &ZwpTextInputV1, + ) { + let mut inner = self.inner.lock().unwrap(); + if inner + .active + .text_input + .as_ref() + .is_some_and(|ti| ti.id() == text_input.id()) + { + if let (Some(im), Some(ctx)) = (inner.input_method.as_ref(), inner.active.context.as_ref()) { + im.deactivate(ctx); + } + inner.active = ActiveContext::default(); + drop(inner); + self.hide_popup(state); + } + } +} + +#[derive(Debug)] +/// User data attached to `zwp_text_input_v1`. +pub struct TextInputUserData { + handle: InputMethodV1Handle, + state: Arc>, +} + +#[derive(Debug)] +/// User data attached to `zwp_input_method_v1`. +pub struct InputMethodUserData { + handle: InputMethodV1Handle, + popup_geometry_callback: fn(&D, &WlSurface) -> Rectangle, + new_popup: fn(&mut D, PopupSurface), + popup_repositioned: fn(&mut D, PopupSurface), + dismiss_popup: fn(&mut D, PopupSurface), +} + +#[derive(Debug)] +/// User data attached to `zwp_input_method_context_v1`. +pub struct InputMethodContextUserData { + handle: InputMethodV1Handle, +} + +#[derive(Debug)] +/// User data attached to keyboard resources grabbed by input-method-v1. +pub struct InputMethodKeyboardUserData { + handle: InputMethodV1Handle, + keyboard_handle: KeyboardHandle, +} + +#[derive(Debug)] +/// User data attached to `zwp_input_panel_v1`. +pub struct InputPanelUserData { + handle: InputMethodV1Handle, +} + +#[derive(Debug)] +/// User data attached to `zwp_input_panel_surface_v1`. +pub struct InputPanelSurfaceUserData { + handle: InputMethodV1Handle, +} + +#[derive(Clone)] +/// Global data shared by input-method-v1, text-input-v1 and input-panel-v1 globals. +pub struct InputMethodManagerGlobalData { + filter: Arc Fn(&'c Client) -> bool + Send + Sync>, + handle: InputMethodV1Handle, +} + +impl fmt::Debug for InputMethodManagerGlobalData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InputMethodManagerGlobalData").finish() + } +} + +/// State of text-input-v1 + input-method-v1 protocols. +#[derive(Debug)] +pub struct InputMethodV1ManagerState { + input_method_global: GlobalId, + text_input_global: GlobalId, + input_panel_global: GlobalId, +} + +impl InputMethodV1ManagerState { + /// Creates globals for `zwp_input_method_v1`, `zwp_input_panel_v1` and + /// `zwp_text_input_manager_v1`. + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch>, + D: Dispatch, + D: Dispatch>, + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: SeatHandler + InputMethodV1Handler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let filter = InputMethodManagerGlobalData { + filter: Arc::new(filter), + handle: InputMethodV1Handle::default(), + }; + + let input_method_global = + display.create_global::(INPUT_METHOD_VERSION, filter.clone()); + let input_panel_global = + display.create_global::(INPUT_PANEL_VERSION, filter.clone()); + let text_input_global = + display.create_global::(TEXT_INPUT_VERSION, filter); + + Self { + input_method_global, + text_input_global, + input_panel_global, + } + } + + /// Returns the global id of `zwp_input_method_v1`. + pub fn input_method_global(&self) -> GlobalId { + self.input_method_global.clone() + } + + /// Returns the global id of `zwp_text_input_manager_v1`. + pub fn text_input_global(&self) -> GlobalId { + self.text_input_global.clone() + } + + /// Returns the global id of `zwp_input_panel_v1`. + pub fn input_panel_global(&self) -> GlobalId { + self.input_panel_global.clone() + } +} + +impl GlobalDispatch for InputMethodV1ManagerState +where + D: GlobalDispatch, + D: Dispatch>, + D: SeatHandler + InputMethodV1Handler, + D: 'static, +{ + fn bind( + _state: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data: &InputMethodManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init( + resource, + InputMethodUserData { + handle: data.handle.clone(), + popup_geometry_callback: D::parent_geometry, + new_popup: D::new_popup, + popup_repositioned: D::popup_repositioned, + dismiss_popup: D::dismiss_popup, + }, + ); + + let mut inner = data.handle.inner.lock().unwrap(); + let _ = inner.input_method.take(); + inner.input_method = Some(instance); + } + + fn can_view(client: Client, global_data: &InputMethodManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl GlobalDispatch for InputMethodV1ManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: SeatHandler, + D: 'static, +{ + fn bind( + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data: &InputMethodManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init( + resource, + InputPanelUserData { + handle: data.handle.clone(), + }, + ); + } + + fn can_view(client: Client, global_data: &InputMethodManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl GlobalDispatch for InputMethodV1ManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: 'static, +{ + fn bind( + _: &mut D, + _: &DisplayHandle, + _: &Client, + resource: New, + data: &InputMethodManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, data.handle.clone()); + } + + fn can_view(client: Client, global_data: &InputMethodManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for InputMethodV1ManagerState +where + D: Dispatch, + D: Dispatch, + D: 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _resource: &ZwpTextInputManagerV1, + request: zwp_text_input_manager_v1::Request, + handle: &InputMethodV1Handle, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + if let zwp_text_input_manager_v1::Request::CreateTextInput { id } = request { + let state = Arc::new(Mutex::new(TextInputV1State::default())); + let text_input = data_init.init( + id, + TextInputUserData { + handle: handle.clone(), + state, + }, + ); + handle.add_text_input(&text_input); + } + } +} + +impl Dispatch for InputMethodV1ManagerState +where + D: Dispatch, + D: Dispatch, + D: Dispatch>, + D: SeatHandler + InputMethodV1Handler, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwpTextInputV1, + request: zwp_text_input_v1::Request, + data: &TextInputUserData, + dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_text_input_v1::Request::Activate { seat, surface } => { + let mut ti_state = data.state.lock().unwrap(); + ti_state.active = true; + ti_state.seat = Some(seat.clone()); + ti_state.surface = Some(surface.clone()); + drop(ti_state); + + resource.enter(&surface); + + let mut inner = data.handle.inner.lock().unwrap(); + let Some(im) = inner.input_method.clone() else { + return; + }; + + if let Some(ctx) = inner.active.context.take() { + im.deactivate(&ctx); + } + + let Some(client) = im.client() else { + return; + }; + + let Ok(context) = client.create_resource::( + dhandle, + 1, + InputMethodContextUserData { + handle: data.handle.clone(), + }, + ) else { + return; + }; + + im.activate(&context); + + inner.active.context = Some(context); + inner.active.text_input = Some(resource.clone()); + inner.active.seat = Some(seat); + inner.active.surface = Some(surface); + + if let Some(kbd_handle) = Seat::::from_resource(inner.active.seat.as_ref().unwrap()) + .and_then(|s| s.get_keyboard()) + { + kbd_handle.unset_grab(state); + } + drop(inner); + data.handle.refresh_popup(state); + } + zwp_text_input_v1::Request::Deactivate { seat } => { + let mut ti_state = data.state.lock().unwrap(); + ti_state.active = false; + if ti_state.seat.as_ref().is_some_and(|s| s != &seat) { + return; + } + drop(ti_state); + + resource.leave(); + data.handle.deactivate_if_active_for(state, resource); + } + zwp_text_input_v1::Request::ShowInputPanel => { + resource.input_panel_state(1); + } + zwp_text_input_v1::Request::HideInputPanel => { + resource.input_panel_state(0); + } + zwp_text_input_v1::Request::Reset => { + if let Some(ctx) = data.handle.inner.lock().unwrap().active.context.as_ref() { + ctx.reset(); + } + } + zwp_text_input_v1::Request::SetSurroundingText { text, cursor, anchor } => { + if let Some(ctx) = data.handle.inner.lock().unwrap().active.context.as_ref() { + ctx.surrounding_text(text, cursor, anchor); + } + } + zwp_text_input_v1::Request::SetContentType { hint, purpose } => { + if let Some(ctx) = data.handle.inner.lock().unwrap().active.context.as_ref() { + let hint = u32::from(hint.into_result().unwrap_or(zwp_text_input_v1::ContentHint::None)); + let purpose = u32::from( + purpose + .into_result() + .unwrap_or(zwp_text_input_v1::ContentPurpose::Normal), + ); + ctx.content_type(hint, purpose); + } + } + zwp_text_input_v1::Request::SetPreferredLanguage { language } => { + if let Some(ctx) = data.handle.inner.lock().unwrap().active.context.as_ref() { + ctx.preferred_language(language); + } + } + zwp_text_input_v1::Request::CommitState { serial } => { + data.state.lock().unwrap().serial = serial; + if let Some(ctx) = data.handle.inner.lock().unwrap().active.context.as_ref() { + ctx.commit_state(serial); + } + } + zwp_text_input_v1::Request::InvokeAction { button, index } => { + if let Some(ctx) = data.handle.inner.lock().unwrap().active.context.as_ref() { + ctx.invoke_action(button, index); + } + } + zwp_text_input_v1::Request::SetCursorRectangle { x, y, width, height } => { + data.state.lock().unwrap().cursor_rectangle = + Rectangle::new((x, y).into(), (width, height).into()); + data.handle.refresh_popup(state); + } + _ => {} + } + } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZwpTextInputV1, data: &TextInputUserData) { + data.handle.deactivate_if_active_for(state, resource); + data.handle.remove_text_input(resource); + + let maybe_seat = data.state.lock().unwrap().seat.clone(); + if let Some(seat) = maybe_seat.and_then(|s| Seat::::from_resource(&s)) { + if let Some(keyboard) = seat.get_keyboard() { + keyboard.unset_grab(state); + } + } + } +} + +impl Dispatch, D> for InputMethodV1ManagerState +where + D: Dispatch>, + D: SeatHandler + InputMethodV1Handler, + D: 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _resource: &ZwpInputMethodV1, + _request: zwp_input_method_v1::Request, + _data: &InputMethodUserData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ZwpInputMethodV1, + data: &InputMethodUserData, + ) { + let mut inner = data.handle.inner.lock().unwrap(); + if inner + .input_method + .as_ref() + .is_some_and(|im| im.id() == resource.id()) + { + inner.input_method = None; + inner.active = ActiveContext::default(); + drop(inner); + data.handle.hide_popup(state); + } + } +} + +impl Dispatch for InputMethodV1ManagerState +where + D: Dispatch, + D: Dispatch>, + D: SeatHandler, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZwpInputMethodContextV1, + request: zwp_input_method_context_v1::Request, + data: &InputMethodContextUserData, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_input_method_context_v1::Request::CommitString { serial, text } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.commit_string(serial, text); + } + } + zwp_input_method_context_v1::Request::PreeditString { serial, text, commit } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.preedit_string(serial, text, commit); + } + } + zwp_input_method_context_v1::Request::PreeditStyling { index, length, style } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + let style = style + .try_into() + .unwrap_or(zwp_text_input_v1::PreeditStyle::Default); + ti.preedit_styling(index, length, style); + } + } + zwp_input_method_context_v1::Request::PreeditCursor { index } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.preedit_cursor(index); + } + } + zwp_input_method_context_v1::Request::DeleteSurroundingText { index, length } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.delete_surrounding_text(index, length); + } + } + zwp_input_method_context_v1::Request::CursorPosition { index, anchor } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.cursor_position(index, anchor); + } + } + zwp_input_method_context_v1::Request::ModifiersMap { map } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.modifiers_map(map.to_vec()); + } + } + zwp_input_method_context_v1::Request::Keysym { + serial, + time, + sym, + state, + modifiers, + } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.keysym(serial, time, sym, state, modifiers); + } + } + zwp_input_method_context_v1::Request::Language { serial, language } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + ti.language(serial, language); + } + } + zwp_input_method_context_v1::Request::TextDirection { serial, direction } => { + if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { + let direction = direction + .try_into() + .unwrap_or(zwp_text_input_v1::TextDirection::Auto); + ti.text_direction(serial, direction); + } + } + zwp_input_method_context_v1::Request::GrabKeyboard { keyboard } => { + let inner = data.handle.inner.lock().unwrap(); + let Some(seat_resource) = inner.active.seat.clone() else { + return; + }; + let Some(seat) = Seat::::from_resource(&seat_resource) else { + return; + }; + let Some(keyboard_handle) = seat.get_keyboard() else { + return; + }; + + keyboard_handle.set_grab(state, inner.keyboard_grab.clone(), SERIAL_COUNTER.next_serial()); + + let instance = data_init.init( + keyboard, + InputMethodKeyboardUserData { + handle: data.handle.clone(), + keyboard_handle: keyboard_handle.clone(), + }, + ); + + inner.keyboard_grab.inner.lock().unwrap().grab = Some(instance.clone()); + + let guard = keyboard_handle.arc.internal.lock().unwrap(); + instance.repeat_info(guard.repeat_rate, guard.repeat_delay); + let keymap_file = keyboard_handle.arc.keymap.lock().unwrap(); + let res = keymap_file.with_fd(false, |fd, size| { + instance.keymap(KeymapFormat::XkbV1, fd, size as u32); + }); + if let Err(err) = res { + warn!(err = ?err, "failed to send v1 IME keymap"); + } else { + let mods = guard.mods_state.serialized; + instance.modifiers( + SERIAL_COUNTER.next_serial().0, + mods.depressed, + mods.latched, + mods.locked, + mods.layout_effective, + ); + } + } + zwp_input_method_context_v1::Request::Key { + serial, + time, + key, + state, + } => { + let inner = data.handle.inner.lock().unwrap(); + let Some(seat_resource) = inner.active.seat.as_ref() else { + return; + }; + let Some(surface) = inner.active.surface.as_ref() else { + return; + }; + let Some(seat) = Seat::::from_resource(seat_resource) else { + return; + }; + for_each_focused_kbds(&seat, surface, |kbd| { + let key_state = if state == 1 { + wl_keyboard::KeyState::Pressed + } else { + wl_keyboard::KeyState::Released + }; + kbd.key(serial, time, key, key_state); + }); + } + zwp_input_method_context_v1::Request::Modifiers { + serial, + mods_depressed, + mods_latched, + mods_locked, + group, + } => { + let inner = data.handle.inner.lock().unwrap(); + let Some(seat_resource) = inner.active.seat.as_ref() else { + return; + }; + let Some(surface) = inner.active.surface.as_ref() else { + return; + }; + let Some(seat) = Seat::::from_resource(seat_resource) else { + return; + }; + for_each_focused_kbds(&seat, surface, |kbd| { + kbd.modifiers(serial, mods_depressed, mods_latched, mods_locked, group); + }); + } + _ => {} + } + } + + fn destroyed( + _state: &mut D, + _client: ClientId, + _resource: &ZwpInputMethodContextV1, + data: &InputMethodContextUserData, + ) { + data.handle.inner.lock().unwrap().active.context = None; + } +} + +impl Dispatch, D> for InputMethodV1ManagerState +where + D: Dispatch>, + D: SeatHandler + 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _resource: &WlKeyboard, + _request: wl_keyboard::Request, + _data: &InputMethodKeyboardUserData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } + + fn destroyed( + state: &mut D, + _client: ClientId, + _resource: &WlKeyboard, + data: &InputMethodKeyboardUserData, + ) { + data.handle + .inner + .lock() + .unwrap() + .keyboard_grab + .inner + .lock() + .unwrap() + .grab = None; + data.keyboard_handle.unset_grab(state); + } +} + +impl Dispatch for InputMethodV1ManagerState +where + D: Dispatch, + D: Dispatch, + D: SeatHandler + InputMethodV1Handler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZwpInputPanelV1, + request: zwp_input_panel_v1::Request, + data: &InputPanelUserData, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + if let zwp_input_panel_v1::Request::GetInputPanelSurface { id, surface } = request { + let panel_surface = data_init.init( + id, + InputPanelSurfaceUserData { + handle: data.handle.clone(), + }, + ); + + let parent = { + let inner = data.handle.inner.lock().unwrap(); + inner.active.surface.as_ref().map(|s| PopupParent { + surface: s.clone(), + location: Rectangle::default(), + }) + }; + + let popup = PopupSurface::new(panel_surface, surface, parent); + { + let mut inner = data.handle.inner.lock().unwrap(); + inner.popup = Some(popup); + inner.popup_visible = false; + } + data.handle.refresh_popup(state); + } + } +} + +impl Dispatch for InputMethodV1ManagerState +where + D: Dispatch, + D: SeatHandler + InputMethodV1Handler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZwpInputPanelSurfaceV1, + request: zwp_input_panel_surface_v1::Request, + data: &InputPanelSurfaceUserData, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwp_input_panel_surface_v1::Request::SetOverlayPanel => { + data.handle.inner.lock().unwrap().popup_enabled = true; + data.handle.refresh_popup(state); + } + zwp_input_panel_surface_v1::Request::SetToplevel { .. } => { + data.handle.inner.lock().unwrap().popup_enabled = true; + data.handle.refresh_popup(state); + } + _ => {} + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + _resource: &ZwpInputPanelSurfaceV1, + data: &InputPanelSurfaceUserData, + ) { + { + let mut inner = data.handle.inner.lock().unwrap(); + inner.popup = None; + inner.popup_enabled = false; + } + data.handle.hide_popup(state); + } +} + +#[allow(missing_docs)] +#[macro_export] +macro_rules! delegate_input_method_manager_v1 { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + const _: () = { + use $crate::{ + reexports::{ + wayland_protocols::wp::{ + input_method::zv1::server::{ + zwp_input_method_context_v1::ZwpInputMethodContextV1, + zwp_input_method_v1::ZwpInputMethodV1, + zwp_input_panel_surface_v1::ZwpInputPanelSurfaceV1, + zwp_input_panel_v1::ZwpInputPanelV1, + }, + text_input::zv1::server::{ + zwp_text_input_manager_v1::ZwpTextInputManagerV1, + zwp_text_input_v1::ZwpTextInputV1, + }, + }, + wayland_server::{ + delegate_dispatch, delegate_global_dispatch, + protocol::wl_keyboard::WlKeyboard, + }, + }, + wayland::input_method_v1::{ + InputMethodContextUserData, InputMethodKeyboardUserData, + InputMethodManagerGlobalData, InputMethodUserData, InputMethodV1Handle, + InputMethodV1ManagerState, InputPanelSurfaceUserData, InputPanelUserData, + TextInputUserData, + }, + }; + + delegate_global_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpInputMethodV1: InputMethodManagerGlobalData] => InputMethodV1ManagerState + ); + + delegate_global_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpInputPanelV1: InputMethodManagerGlobalData] => InputMethodV1ManagerState + ); + + delegate_global_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpTextInputManagerV1: InputMethodManagerGlobalData] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpInputMethodV1: InputMethodUserData] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpInputMethodContextV1: InputMethodContextUserData] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [WlKeyboard: InputMethodKeyboardUserData] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpInputPanelV1: InputPanelUserData] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpInputPanelSurfaceV1: InputPanelSurfaceUserData] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpTextInputManagerV1: InputMethodV1Handle] => InputMethodV1ManagerState + ); + + delegate_dispatch!( + $(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? + $ty: [ZwpTextInputV1: TextInputUserData] => InputMethodV1ManagerState + ); + }; + }; +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 5d26926d2248..23b368978aec 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -68,6 +68,7 @@ pub mod idle_notify; pub mod image_capture_source; pub mod image_copy_capture; pub mod input_method; +pub mod input_method_v1; pub mod keyboard_shortcuts_inhibit; pub mod output; pub mod pointer_constraints; From a32ef770b18b83745452e1a48615d8809cb25517 Mon Sep 17 00:00:00 2001 From: Wanten Date: Mon, 2 Mar 2026 21:08:07 +0800 Subject: [PATCH 2/5] fix(input-method-v1): release keyboard grab when deactivating text input --- src/wayland/input_method_v1/mod.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/wayland/input_method_v1/mod.rs b/src/wayland/input_method_v1/mod.rs index 2d0337759fc0..ea8c24cf9cf4 100644 --- a/src/wayland/input_method_v1/mod.rs +++ b/src/wayland/input_method_v1/mod.rs @@ -334,12 +334,19 @@ impl InputMethodV1Handle { .as_ref() .is_some_and(|ti| ti.id() == text_input.id()) { + let active_seat = inner.active.seat.clone(); if let (Some(im), Some(ctx)) = (inner.input_method.as_ref(), inner.active.context.as_ref()) { im.deactivate(ctx); } inner.active = ActiveContext::default(); drop(inner); self.hide_popup(state); + + if let Some(seat) = active_seat.and_then(|s| Seat::::from_resource(&s)) { + if let Some(keyboard) = seat.get_keyboard() { + keyboard.unset_grab(state); + } + } } } } @@ -928,12 +935,22 @@ where } fn destroyed( - _state: &mut D, + state: &mut D, _client: ClientId, _resource: &ZwpInputMethodContextV1, data: &InputMethodContextUserData, ) { - data.handle.inner.lock().unwrap().active.context = None; + let active_seat = { + let mut inner = data.handle.inner.lock().unwrap(); + inner.active.context = None; + inner.active.seat.clone() + }; + + if let Some(seat) = active_seat.and_then(|s| Seat::::from_resource(&s)) { + if let Some(keyboard) = seat.get_keyboard() { + keyboard.unset_grab(state); + } + } } } From cc2cf59f5715aa2017a1612fc8d3ad346d61a2d9 Mon Sep 17 00:00:00 2001 From: Wanten Date: Mon, 2 Mar 2026 21:11:40 +0800 Subject: [PATCH 3/5] fix: harden input-method-v1 resource lifecycle handling --- src/wayland/input_method_v1/mod.rs | 131 +++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 25 deletions(-) diff --git a/src/wayland/input_method_v1/mod.rs b/src/wayland/input_method_v1/mod.rs index ea8c24cf9cf4..d01064575bf2 100644 --- a/src/wayland/input_method_v1/mod.rs +++ b/src/wayland/input_method_v1/mod.rs @@ -40,11 +40,13 @@ use crate::input::{ Seat, SeatHandler, }; use crate::utils::{Logical, Point, Rectangle, SERIAL_COUNTER}; +use crate::wayland::compositor; use crate::wayland::seat::keyboard::for_each_focused_kbds; const INPUT_METHOD_VERSION: u32 = 1; const TEXT_INPUT_VERSION: u32 = 1; const INPUT_PANEL_VERSION: u32 = 1; +const INPUT_PANEL_SURFACE_ROLE: &str = "zwp_input_panel_surface_v1"; /// Parent surface and geometry used to place an IME popup. #[derive(Debug, Clone)] @@ -748,16 +750,37 @@ where resource: &ZwpInputMethodV1, data: &InputMethodUserData, ) { - let mut inner = data.handle.inner.lock().unwrap(); - if inner - .input_method - .as_ref() - .is_some_and(|im| im.id() == resource.id()) + let active_seat = { + let inner = data.handle.inner.lock().unwrap(); + if !inner + .input_method + .as_ref() + .is_some_and(|im| im.id() == resource.id()) + { + return; + } + inner.active.seat.clone() + }; + + data.handle.hide_popup(state); + { + let mut inner = data.handle.inner.lock().unwrap(); + if !inner + .input_method + .as_ref() + .is_some_and(|im| im.id() == resource.id()) + { + return; + } inner.input_method = None; inner.active = ActiveContext::default(); - drop(inner); - data.handle.hide_popup(state); + } + + if let Some(seat) = active_seat.and_then(|s| Seat::::from_resource(&s)) { + if let Some(keyboard) = seat.get_keyboard() { + keyboard.unset_grab(state); + } } } } @@ -772,12 +795,24 @@ where fn request( state: &mut D, _client: &Client, - _resource: &ZwpInputMethodContextV1, + resource: &ZwpInputMethodContextV1, request: zwp_input_method_context_v1::Request, data: &InputMethodContextUserData, _dhandle: &DisplayHandle, data_init: &mut DataInit<'_, D>, ) { + let is_active_context = { + let inner = data.handle.inner.lock().unwrap(); + inner + .active + .context + .as_ref() + .is_some_and(|ctx| ctx.id() == resource.id()) + }; + if !is_active_context { + return; + } + match request { zwp_input_method_context_v1::Request::CommitString { serial, text } => { if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { @@ -937,13 +972,23 @@ where fn destroyed( state: &mut D, _client: ClientId, - _resource: &ZwpInputMethodContextV1, + resource: &ZwpInputMethodContextV1, data: &InputMethodContextUserData, ) { let active_seat = { let mut inner = data.handle.inner.lock().unwrap(); - inner.active.context = None; - inner.active.seat.clone() + let is_active_context = inner + .active + .context + .as_ref() + .is_some_and(|ctx| ctx.id() == resource.id()); + if is_active_context { + let active_seat = inner.active.seat.clone(); + inner.active = ActiveContext::default(); + active_seat + } else { + None + } }; if let Some(seat) = active_seat.and_then(|s| Seat::::from_resource(&s)) { @@ -973,19 +1018,22 @@ where fn destroyed( state: &mut D, _client: ClientId, - _resource: &WlKeyboard, + resource: &WlKeyboard, data: &InputMethodKeyboardUserData, ) { - data.handle - .inner - .lock() - .unwrap() - .keyboard_grab - .inner - .lock() - .unwrap() - .grab = None; - data.keyboard_handle.unset_grab(state); + let should_unset = { + let inner = data.handle.inner.lock().unwrap(); + let mut grab = inner.keyboard_grab.inner.lock().unwrap(); + if grab.grab.as_ref().is_some_and(|g| g.id() == resource.id()) { + grab.grab = None; + true + } else { + false + } + }; + if should_unset { + data.keyboard_handle.unset_grab(state); + } } } @@ -998,13 +1046,21 @@ where fn request( state: &mut D, _client: &Client, - _resource: &ZwpInputPanelV1, + resource: &ZwpInputPanelV1, request: zwp_input_panel_v1::Request, data: &InputPanelUserData, _dhandle: &DisplayHandle, data_init: &mut DataInit<'_, D>, ) { if let zwp_input_panel_v1::Request::GetInputPanelSurface { id, surface } = request { + if compositor::give_role(&surface, INPUT_PANEL_SURFACE_ROLE).is_err() + && compositor::get_role(&surface) != Some(INPUT_PANEL_SURFACE_ROLE) + { + // Protocol requires this raise an error, but doesn't define an error enum. + resource.post_error(0u32, "Surface already has a role."); + return; + } + let panel_surface = data_init.init( id, InputPanelSurfaceUserData { @@ -1020,10 +1076,13 @@ where }) }; + data.handle.hide_popup(state); + let popup = PopupSurface::new(panel_surface, surface, parent); { let mut inner = data.handle.inner.lock().unwrap(); inner.popup = Some(popup); + inner.popup_enabled = false; inner.popup_visible = false; } data.handle.refresh_popup(state); @@ -1039,12 +1098,23 @@ where fn request( state: &mut D, _client: &Client, - _resource: &ZwpInputPanelSurfaceV1, + resource: &ZwpInputPanelSurfaceV1, request: zwp_input_panel_surface_v1::Request, data: &InputPanelSurfaceUserData, _dhandle: &DisplayHandle, _data_init: &mut DataInit<'_, D>, ) { + let is_active_popup_surface = { + let inner = data.handle.inner.lock().unwrap(); + inner + .popup + .as_ref() + .is_some_and(|popup| popup.surface_role.id() == resource.id()) + }; + if !is_active_popup_surface { + return; + } + match request { zwp_input_panel_surface_v1::Request::SetOverlayPanel => { data.handle.inner.lock().unwrap().popup_enabled = true; @@ -1061,9 +1131,20 @@ where fn destroyed( state: &mut D, _client: ClientId, - _resource: &ZwpInputPanelSurfaceV1, + resource: &ZwpInputPanelSurfaceV1, data: &InputPanelSurfaceUserData, ) { + let is_active_popup_surface = { + let inner = data.handle.inner.lock().unwrap(); + inner + .popup + .as_ref() + .is_some_and(|popup| popup.surface_role.id() == resource.id()) + }; + if !is_active_popup_surface { + return; + } + { let mut inner = data.handle.inner.lock().unwrap(); inner.popup = None; From 3709968aceece55e375f7eeafa5d574aaec27bce Mon Sep 17 00:00:00 2001 From: Wanten Date: Tue, 3 Mar 2026 07:46:51 +0800 Subject: [PATCH 4/5] docs(input_method_v1): fix rustdoc macro link reference --- src/wayland/input_method_v1/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wayland/input_method_v1/mod.rs b/src/wayland/input_method_v1/mod.rs index d01064575bf2..7108ce1c27b1 100644 --- a/src/wayland/input_method_v1/mod.rs +++ b/src/wayland/input_method_v1/mod.rs @@ -6,7 +6,7 @@ //! The high-level integration pattern is: //! - Implement [`InputMethodV1Handler`] for your compositor state. //! - Create globals through [`InputMethodV1ManagerState::new`]. -//! - Delegate protocol dispatch with [`delegate_input_method_manager_v1!`]. +//! - Delegate protocol dispatch with [`macro@crate::delegate_input_method_manager_v1`]. use std::fmt; use std::sync::{Arc, Mutex}; From 48e87517a16128dc8f1a0ecafaf85bbd44446dd3 Mon Sep 17 00:00:00 2001 From: Wanten Date: Mon, 16 Mar 2026 17:32:39 +0800 Subject: [PATCH 5/5] fix(input-method-v1): init keyboard object on inactive grab --- src/wayland/input_method_v1/mod.rs | 71 +++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/src/wayland/input_method_v1/mod.rs b/src/wayland/input_method_v1/mod.rs index 7108ce1c27b1..8c2464dd0edb 100644 --- a/src/wayland/input_method_v1/mod.rs +++ b/src/wayland/input_method_v1/mod.rs @@ -380,7 +380,7 @@ pub struct InputMethodContextUserData { /// User data attached to keyboard resources grabbed by input-method-v1. pub struct InputMethodKeyboardUserData { handle: InputMethodV1Handle, - keyboard_handle: KeyboardHandle, + keyboard_handle: Option>, } #[derive(Debug)] @@ -809,22 +809,28 @@ where .as_ref() .is_some_and(|ctx| ctx.id() == resource.id()) }; - if !is_active_context { - return; - } match request { zwp_input_method_context_v1::Request::CommitString { serial, text } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.commit_string(serial, text); } } zwp_input_method_context_v1::Request::PreeditString { serial, text, commit } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.preedit_string(serial, text, commit); } } zwp_input_method_context_v1::Request::PreeditStyling { index, length, style } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { let style = style .try_into() @@ -833,21 +839,33 @@ where } } zwp_input_method_context_v1::Request::PreeditCursor { index } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.preedit_cursor(index); } } zwp_input_method_context_v1::Request::DeleteSurroundingText { index, length } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.delete_surrounding_text(index, length); } } zwp_input_method_context_v1::Request::CursorPosition { index, anchor } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.cursor_position(index, anchor); } } zwp_input_method_context_v1::Request::ModifiersMap { map } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.modifiers_map(map.to_vec()); } @@ -859,16 +877,25 @@ where state, modifiers, } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.keysym(serial, time, sym, state, modifiers); } } zwp_input_method_context_v1::Request::Language { serial, language } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { ti.language(serial, language); } } zwp_input_method_context_v1::Request::TextDirection { serial, direction } => { + if !is_active_context { + return; + } if let Some(ti) = data.handle.inner.lock().unwrap().active.text_input.as_ref() { let direction = direction .try_into() @@ -878,17 +905,11 @@ where } zwp_input_method_context_v1::Request::GrabKeyboard { keyboard } => { let inner = data.handle.inner.lock().unwrap(); - let Some(seat_resource) = inner.active.seat.clone() else { - return; - }; - let Some(seat) = Seat::::from_resource(&seat_resource) else { - return; - }; - let Some(keyboard_handle) = seat.get_keyboard() else { - return; - }; - - keyboard_handle.set_grab(state, inner.keyboard_grab.clone(), SERIAL_COUNTER.next_serial()); + let seat_resource = inner.active.seat.clone(); + let keyboard_handle = seat_resource + .as_ref() + .and_then(|seat| Seat::::from_resource(seat)) + .and_then(|seat| seat.get_keyboard()); let instance = data_init.init( keyboard, @@ -897,7 +918,17 @@ where keyboard_handle: keyboard_handle.clone(), }, ); + if !is_active_context { + // Ignore grabs from inactive contexts, but keep the object initialized. + return; + } + let Some(keyboard_handle) = keyboard_handle else { + warn!("input-method-v1 GrabKeyboard without an active seat/keyboard"); + return; + }; + + keyboard_handle.set_grab(state, inner.keyboard_grab.clone(), SERIAL_COUNTER.next_serial()); inner.keyboard_grab.inner.lock().unwrap().grab = Some(instance.clone()); let guard = keyboard_handle.arc.internal.lock().unwrap(); @@ -925,6 +956,9 @@ where key, state, } => { + if !is_active_context { + return; + } let inner = data.handle.inner.lock().unwrap(); let Some(seat_resource) = inner.active.seat.as_ref() else { return; @@ -951,6 +985,9 @@ where mods_locked, group, } => { + if !is_active_context { + return; + } let inner = data.handle.inner.lock().unwrap(); let Some(seat_resource) = inner.active.seat.as_ref() else { return; @@ -1032,7 +1069,9 @@ where } }; if should_unset { - data.keyboard_handle.unset_grab(state); + if let Some(keyboard_handle) = data.keyboard_handle.as_ref() { + keyboard_handle.unset_grab(state); + } } } }