From 4cb013faa1e3b2852bd69ddaf30f2fa0f631e2ac Mon Sep 17 00:00:00 2001 From: Raymond Thurman Date: Wed, 14 Oct 2026 12:06:38 -0500 Subject: [PATCH] Stop Space and Enter from resetting the X11 input context (#1179) Space and Enter count as a click on the focused canvas. request_focus always interrupts IME, and on X11 winit then destroys the XIM context. fcitx swallows every later key. Ignore that keyboard click. Pointer clicks, accessibility clicks, and drags still focus the canvas. --- crates/ui-egui/src/canvas.rs | 61 ++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 7c2385615..1047c7ef2 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1241,9 +1241,20 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { if app.canvas.balloon.is_some() && ui.input(|i| i.pointer.any_pressed()) && resp.contains_pointer() && !in_balloon { deselect_balloon(app, ui.ctx()); } - // Focus: the canvas takes keyboard focus on click and keeps Tab/arrows. - if resp.clicked() || resp.drag_started() || app.canvas.want_focus { + // Space and Enter also set `Response::clicked` on a focused click-sensing widget + // (egui's fake primary click). `request_focus` always interrupts IME; on X11 that + // destroys the XIM context and fcitx then drops every later key (#1169). A pointer + // click, an accessibility click, or a drag still takes focus, including a click on + // a canvas that already has it, so a composition commits. `want_focus` focuses only + // while the canvas does not, and does not interrupt an editor that already has it. + let keyboard_click = ui.memory(|m| m.has_focus(resp.id)) && ui.input(|i| i.key_pressed(egui::Key::Space) || i.key_pressed(egui::Key::Enter)); + let pointer_click = resp.clicked_by(egui::PointerButton::Primary); + let accesskit_click = resp.clicked() && !pointer_click && !keyboard_click; + let focus_click = pointer_click || accesskit_click || resp.drag_started(); + if focus_click || (app.canvas.want_focus && !resp.has_focus()) { resp.request_focus(); + } + if focus_click || app.canvas.want_focus { app.canvas.want_focus = false; } if resp.has_focus() { @@ -3768,6 +3779,52 @@ mod tests { let _ = collapse_white_space(&[], &[], 1.0, 1.0); } + /// #1169: Space and Enter count as `Response::clicked` on the focused canvas, and + /// `request_focus` always interrupts IME. On X11 that recreates the XIM context and + /// fcitx drops every later key. Those keys still type, and they must not set + /// `should_interrupt_composition`. A real pointer click still does, so a composition + /// commits when the caret moves. + #[test] + fn space_and_enter_on_the_canvas_do_not_interrupt_ime() { + let mut app = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default()); + app.run("document.setText", json!({"text": "The"})).unwrap(); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + for _ in 0..8 { + h.step(); + } + assert!(h.state().canvas.focused, "the canvas takes focus when the window opens"); + let key = |key| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }; + let ime_interrupted = + |h: &egui_kittest::Harness<'_, WordApp>| h.output().platform_output.ime.as_ref().map(|ime| ime.should_interrupt_composition); + h.event(key(egui::Key::Space)); + h.event(egui::Event::Text(" ".into())); + h.step(); + assert_eq!(ime_interrupted(&h), Some(false), "space must not recreate the XIM context"); + let text = h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body); + assert!(text.contains("The") && text.contains(' '), "the space is still inserted: {text:?}"); + assert!(h.state().canvas.focused, "the canvas keeps the keyboard"); + h.event(key(egui::Key::Enter)); + h.step(); + assert_eq!(ime_interrupted(&h), Some(false), "enter must not recreate the XIM context"); + assert!(h.state().canvas.focused, "enter leaves the canvas focused"); + + let at = h.state().canvas.page_rects.first().map(|r| r.center()).expect("a page"); + h.event(egui::Event::PointerMoved(at)); + h.step(); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }); + h.step(); + } + assert_eq!(ime_interrupted(&h), Some(true), "a click still interrupts composition"); + } + fn page_at_index(rects: &[Rect], p: Pos2) -> Option { nearest_page(rects, p) }