Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/platform_impl/linux/wayland/seat/keyboard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -93,6 +94,22 @@ impl Dispatch<WlKeyboard, KeyboardData, WinitState> 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);
Expand Down
21 changes: 20 additions & 1 deletion src/platform_impl/linux/wayland/seat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct WinitSeatState {
first_touch_id: Option<i32>,

/// The text input bound on the seat.
text_input: Option<Arc<ZwpTextInputV3>>,
pub(crate) text_input: Option<Arc<ZwpTextInputV3>>,

/// The relative pointer bound on the seat.
relative_pointer: Option<ZwpRelativePointerV1>,
Expand Down Expand Up @@ -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();
}
}
}
}
}

Expand Down
64 changes: 42 additions & 22 deletions src/platform_impl/linux/wayland/seat/text_input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> 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;
Expand Down Expand Up @@ -119,32 +124,43 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> 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 { .. } => {
Expand Down Expand Up @@ -186,9 +202,13 @@ pub struct TextInputDataInner {

/// The preedit to submit on `done`.
pending_preedit: Option<Preedit>,

/// The last preedit sent to the application, for deduplication.
last_sent_preedit: Option<Preedit>,
}

/// The state of the preedit.
#[derive(Clone, PartialEq)]
struct Preedit {
text: String,
cursor_begin: Option<usize>,
Expand Down
8 changes: 8 additions & 0 deletions src/platform_impl/linux/wayland/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
5 changes: 5 additions & 0 deletions src/platform_impl/linux/wayland/window/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down