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))