From f32727ddca01a033a7af072ac1a5d504b9178c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:09:07 +0800 Subject: [PATCH] Improve IME, and restrict mac-specific workaround (#7973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes N/A * [x] I have followed the instructions in the PR template My PR that fixes the macOS backspacing issue (#7810) unfortunately breaks text selection on Wayland (Fedora KDE Plasma Desktop 43 [Wayland, with or without IBus]). I had actually tested on a Wayland setup but failed to notice that :( Windows and Linux+X11 (Debian 13 [Cinnamon 6.4.10 + X11 + fcitx5 5.1.2]) are not affected. This PR fixes the issue by restricting the macOS fix to macOS-only.
Here is the correct behavior on Wayland after this PR (and before #7810 is applied) ![2026-03-13 5 25 24 PM](https://github.com/user-attachments/assets/3b0831c1-1d96-4003-9109-4bfe68e06d40)
Here is the buggy behavior on Wayland before this PR ![2026-03-13 5 31 58 PM](https://github.com/user-attachments/assets/c6d69382-0104-4e38-ad47-2d431f83f1fa)
## Cause of the Wayland issue On Wayland, `winit` constantly emits `winit::event::Ime::Preedit("", None)` events. PR #7810 added these lines for handling `winit::event::Ime::Preedit(_, None)` in `egui-winit` without considering the `target_os`: https://github.com/emilk/egui/blob/14afefa2521d1baaf4fd02105eec2d3727a7ac36/crates/egui-winit/src/lib.rs#L619-L621 As a result, while text is being selected, `egui-winit` receives these `winit::event::Ime::Preedit("", None)` events from `winit` and forwards them to `egui` as `egui::ImeEvent::Preedit("")`. `egui` then clears the current text selection, because it currently does not distinguish between IME pre-edit text and selected text. --------- Co-authored-by: lucasmerlin --- crates/egui-winit/src/lib.rs | 47 +++++++++------ crates/egui/src/widgets/text_edit/builder.rs | 62 ++++++++++++-------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 243ed119a..234a9989b 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -548,23 +548,23 @@ impl State { /// /// | Setup | Events in Order | /// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | - /// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("测试")` | - /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("测试")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | - /// | c-windows11-ms_pinyin | `Predict("测试", Some(…))` -> `Predict("", None)` -> `Commit("测试")` -> `Disabled` | + /// | a-macos15-apple_shuangpin | `Preedit("", None)` -> `Commit("测试")` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", None)` -> `Commit("测试")` -> `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Preedit("测试", Some(…))` -> `Preedit("", None)` -> `Commit("测试")` -> `Disabled` | /// - /// #### Situation: pressed backspace to delete the last character in the prediction + /// #### Situation: pressed backspace to delete the last character in the composition /// /// | Setup | Events in Order | - /// | a-macos15-apple_shuangpin | `Predict("", None)` | - /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | - /// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` | + /// | a-macos15-apple_shuangpin | `Preedit("", None)` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` -> `Commit("")` -> `Disabled` | /// - /// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce" + /// #### Situation: clicked somewhere else while there is an active composition with the pre-edit text "ce" /// /// | Setup | Events in Order | /// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- | /// | a-macos15-apple_shuangpin | nothing emitted | - /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` (duplicate) -> `Preedit("", None)` (duplicate until `TextEdit` blurred) | /// | c-windows11-ms_pinyin | nothing emitted | fn on_ime(&mut self, ime: &winit::event::Ime) { // // code for inspecting ime events emitted by winit: @@ -610,15 +610,26 @@ impl State { self.ime_event_disable(); } winit::event::Ime::Preedit(_, None) => { - // we need to emit this on macOS, since winit doesn't emit - // `Predict("", Some(0, 0))` before this event on macOS when the - // user deletes the last character in the prediction with the - // backspace key. Without this, only `egui::ImeEvent::Disabled` - // is emitted here, leading to the last character being left in - // TextEdit in such situation. - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); + if cfg!(target_os = "macos") { + // On macOS, when the user presses backspace to delete the + // last character in an IME composition, `winit` only emits + // `winit::event::Ime::Preedit("", None)` without a + // preceding `winit::event::Ime::Preedit("", Some(0, 0))`. + // + // The current implementation of `egui::TextEdit` relies on + // receiving an `egui::ImeEvent::Preedit("")` to remove the + // last character in the composition in this case, so we + // emit it here. + // + // This is guarded to macOS-only, as applying it on other + // platforms is unnecessary and can cause undesired + // behavior. + // See: https://github.com/emilk/egui/pull/7973 + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); + } + self.ime_event_disable(); } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index fbf25babf..b9fdb1cbe 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1066,26 +1066,36 @@ fn events( } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), Event::Ime(ime_event) => { - /// Empty prediction can be produced with [`ImeEvent::Preedit`] - /// or [`ImeEvent::Commit`] when user press backspace or escape - /// during IME, so this function should be called in both cases - /// to clear current text. + /// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")` + /// might be emitted from different integrations to signify that + /// the current IME composition should be cleared. /// - /// Example platforms where only `ImeEvent::Preedit("")` of - /// those two events is emitted when the last character in the - /// prediction is deleted: - /// - macOS 15.7.3. - /// - Debian13 with gnome48 and wayland. + /// Example integrations where only `ImeEvent::Preedit("")` of + /// those two events is emitted when the last character is + /// deleted with a backspace: + /// - `egui-winit` on macOS 15.7.3. + /// - `egui-winit` on Debian13 with gnome48 and wayland. /// - /// An example platform where only `ImeEvent::Commit("")` of - /// those two events is emitted when the last character in the - /// prediction is deleted: - /// - Safari 26.2 (on macOS 15.7.3). - fn clear_prediction( + /// An example integration where only `ImeEvent::Commit("")` of + /// those two events is emitted when the last character is + /// deleted with a backspace: + /// - `eframe`'s web integration on Safari 26.2 (on macOS + /// 15.7.3). + /// + /// ## Note + /// + /// The term “pre-edit string” is used by X11 and Wayland, and + /// we use “pre-edit text” and “pre-edit range” here in the + /// same manner. + /// See: + /// + /// We previously referred to “pre-edit text” as “prediction”, + /// which is not standard and can mean different things. + fn clear_preedit_text( text: &mut dyn TextBuffer, - cursor_range: &CCursorRange, + preedit_range: &CCursorRange, ) -> CCursor { - text.delete_selected(cursor_range) + text.delete_selected(preedit_range) } match ime_event { @@ -1094,33 +1104,33 @@ fn events( state.ime_cursor_range = cursor_range; None } - ImeEvent::Preedit(text_mark) => { - if text_mark == "\n" || text_mark == "\r" { + ImeEvent::Preedit(preedit_text) => { + if preedit_text == "\n" || preedit_text == "\r" { None } else { - let mut ccursor = clear_prediction(text, &cursor_range); + let mut ccursor = clear_preedit_text(text, &cursor_range); let start_cursor = ccursor; - if !text_mark.is_empty() { - text.insert_text_at(&mut ccursor, text_mark, char_limit); + if !preedit_text.is_empty() { + text.insert_text_at(&mut ccursor, preedit_text, char_limit); } state.ime_cursor_range = cursor_range; Some(CCursorRange::two(start_cursor, ccursor)) } } - ImeEvent::Commit(prediction) => { - if prediction == "\n" || prediction == "\r" { + ImeEvent::Commit(commit_text) => { + if commit_text == "\n" || commit_text == "\r" { None } else { state.ime_enabled = false; - let mut ccursor = clear_prediction(text, &cursor_range); + let mut ccursor = clear_preedit_text(text, &cursor_range); - if !prediction.is_empty() + if !commit_text.is_empty() && cursor_range.secondary.index == state.ime_cursor_range.secondary.index { - text.insert_text_at(&mut ccursor, prediction, char_limit); + text.insert_text_at(&mut ccursor, commit_text, char_limit); } Some(CCursorRange::one(ccursor))