diff --git a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs index 871df785e3..1397c2d2fb 100644 --- a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs +++ b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -16,6 +16,7 @@ use crate::event::{ElementState, WindowEvent}; use crate::keyboard::ModifiersState; use crate::platform_impl::common::xkb::Context; use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::seat::text_input::ZwpTextInputV3Ext; use crate::platform_impl::wayland::state::WinitState; use crate::platform_impl::wayland::{self, WindowId}; @@ -93,6 +94,22 @@ impl Dispatch for WinitState { window_id, ); } + + // Proactively enable text_input when keyboard focus enters a window. + // The compositor may not have received `enable()` from the initial + // `set_ime_allowed(true)` call if the window didn't yet have keyboard + // focus at that time. + let text_input = seat_state.text_input.clone(); + if let Some(window) = state.windows.get_mut().get(&window_id) { + let window = window.lock().unwrap(); + if let Some(text_input) = &text_input { + if window.ime_allowed() { + text_input.enable(); + text_input.set_content_type_by_purpose(window.ime_purpose()); + text_input.commit(); + } + } + } }, WlKeyboardEvent::Leave { surface, .. } => { let window_id = wayland::make_wid(&surface); diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs index 1306699029..811891c453 100644 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -44,7 +44,7 @@ pub struct WinitSeatState { first_touch_id: Option, /// The text input bound on the seat. - text_input: Option>, + pub(crate) text_input: Option>, /// The relative pointer bound on the seat. relative_pointer: Option, @@ -136,6 +136,25 @@ impl SeatHandler for WinitState { queue_handle, TextInputData::default(), ))); + + // Proactively register the new text_input with every existing window, + // and enable it for any window that already has `ime_allowed = true`. + // + // Without this, the `enter` handler in text_input/mod.rs would be the + // only code path that calls `text_input.enable()` + `commit()`, but + // some compositors only send `enter` *after* receiving an `enable()` + // request — causing a protocol deadlock. + if let Some(text_input) = &seat_state.text_input { + for (_, window_mutex) in self.windows.get_mut() { + let mut window = window_mutex.lock().unwrap(); + window.text_input_entered(text_input); + if window.ime_allowed() { + text_input.enable(); + text_input.set_content_type_by_purpose(window.ime_purpose()); + text_input.commit(); + } + } + } } } diff --git a/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/src/platform_impl/linux/wayland/seat/text_input/mod.rs index 45a0f6c9fd..56a0fd9259 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/mod.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -69,14 +69,19 @@ impl Dispatch for TextInputState { None => return, }; + // NOTE: We do NOT call `enable()` + `commit()` here because + // those have already been sent proactively — in + // `new_capability`, `WlKeyboard::enter`, and + // `set_ime_allowed` — before the compositor sends `enter`. + // Calling them again here causes a double-activation cycle + // that makes the compositor re-send preedit events, leading + // to severe lag and repeated SetMarkedText dispatches. + + window.text_input_entered(text_input); + if window.ime_allowed() { - text_input.enable(); - text_input.set_content_type_by_purpose(window.ime_purpose()); - text_input.commit(); state.events_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); } - - window.text_input_entered(text_input); }, TextInputEvent::Leave { surface } => { text_input_data.surface = None; @@ -119,32 +124,43 @@ impl Dispatch for TextInputState { None => return, }; - // Clear preedit, unless all we'll be doing next is sending a new preedit. - if text_input_data.pending_commit.is_some() - || text_input_data.pending_preedit.is_none() - { + // Take pending state so it's consumed exactly once. + let pending_commit = text_input_data.pending_commit.take(); + let pending_preedit = text_input_data.pending_preedit.take(); + + // Send `Commit` (with a preedit-clear first, if there is any + // preedit to clear). + if let Some(text) = pending_commit { state.events_sink.push_window_event( WindowEvent::Ime(Ime::Preedit(String::new(), None)), window_id, ); - } - - // Send `Commit`. - if let Some(text) = text_input_data.pending_commit.take() { state .events_sink .push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id); - } - // Send preedit. - if let Some(preedit) = text_input_data.pending_preedit.take() { - let cursor_range = - preedit.cursor_begin.map(|b| (b, preedit.cursor_end.unwrap_or(b))); + // After a commit the preedit is gone, so clear the last sent. + text_input_data.last_sent_preedit = None; + } - state.events_sink.push_window_event( - WindowEvent::Ime(Ime::Preedit(preedit.text, cursor_range)), - window_id, - ); + // Deduplicate: skip if the preedit hasn't changed since last time. + // fcitx5/KWin may send multiple identical done events per keystroke; + // sending all of them floods Warp with SetMarkedText dispatches, + // each triggering a re-render + set_ime_cursor_area + commit() + // that can overwhelm the compositor. + if let Some(ref preedit) = pending_preedit { + if text_input_data.last_sent_preedit.as_ref() != Some(preedit) { + let cursor_range = preedit + .cursor_begin + .map(|b| (b, preedit.cursor_end.unwrap_or(b))); + + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(preedit.text.clone(), cursor_range)), + window_id, + ); + + text_input_data.last_sent_preedit = Some(preedit.clone()); + } } }, TextInputEvent::DeleteSurroundingText { .. } => { @@ -186,9 +202,13 @@ pub struct TextInputDataInner { /// The preedit to submit on `done`. pending_preedit: Option, + + /// The last preedit sent to the application, for deduplication. + last_sent_preedit: Option, } /// The state of the preedit. +#[derive(Clone, PartialEq)] struct Preedit { text: String, cursor_begin: Option, diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 761ea7ffa5..fff4a69131 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -176,6 +176,14 @@ impl Window { let window_id = super::make_wid(&surface); state.windows.get_mut().insert(window_id, window_state.clone()); + // Register existing seat text_inputs so that a subsequent + // `set_ime_allowed(true)` can immediately enable them. + for seat_state in state.seats.values() { + if let Some(text_input) = &seat_state.text_input { + window_state.lock().unwrap().text_input_entered(text_input); + } + } + let window_requests = WindowRequests { redraw_requested: AtomicBool::new(true), closed: AtomicBool::new(false), diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 69430d79b0..70693b45a9 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -1054,6 +1054,11 @@ impl WindowState { self.reload_transparency_hint(); } + /// Returns the number of registered text inputs. + pub fn text_inputs_len(&self) -> usize { + self.text_inputs.len() + } + /// Register text input on the top-level. #[inline] pub fn text_input_entered(&mut self, text_input: &ZwpTextInputV3) {