mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Improve IME handling, add public method owns_ime_events on Memory (#7983)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Depends on #7967 * Closes #7485 * Should fix #7906 (This issue doesn't seem to have been resolved, but the author closed it; I personally don't have the environment to verify whether it is fixed.) * Replaces #4137, #4896, and partially #7810 * [x] I have followed the instructions in the PR template This PR started as a fix for #7485, but has since evolved into a broader rewrite of IME-related logic. ## Overview This PR primarily introduces a new public method, `owns_ime_events`, on [`Memory`], and refactors parts of [`TextEdit`] to integrate with it. Previously, each [`TextEdit`] widget independently determined whether to handle IME events and stored its own IME-related state. This approach made ownership-handling fragmented and was therefore error-prone. With this PR: - IME event ownership is centralized, ensuring that at most a single widget owns IME events per frame. - [`PlatformOutput`]'s `ime` field can be set to `None` for at least one frame when IME composition is interrupted, allowing the IME to be properly dismissed. ## Details Two new public methods are introduced on [`Memory`]: - `fn owns_ime_events(&self, id: Id) -> bool`: check IME event ownership for the current frame for the widget with the given `id`. - `fn interrupt_ime(&mut self)`: interrupt the current IME composition, if any. Since the newly added methods on [`Memory`] are public, other widgets can also participate in IME handling without risking ownership conflicts of IME events. I also added an internal (`pub(crate)`) field on [`TextEditState`], called `cursor_purpose`, to distinguish the role of the [`TextEdit`] cursor. Additionally, `egui::ImeEvent::Enabled` and `egui::ImeEvent::Disabled` have been removed, as they are no longer used anywhere. ## Demonstrations ### Windows: The Korean IME text duplication bug fixed in #4137 does not reappear. <table> <thead> <tr> <th></th> <th>With this PR</th> <th>Without this PR</th> </tr> </thead> <tbody> <tr> <th>Behavior</th> <td>Correct (no regression)</td> <td>Correct</td> </tr> <tr> <th>Screencast</th> <td>  </td> <td>  </td> </tr> </tbody> </table> ### Windows: Chinese and Japanese IMEs now behave more consistently with the Korean IME in similar scenarios. This change does not matter much, as composition is rarely interrupted mid-process with these IMEs in typical usage. <table> <thead> <tr> <th></th> <th>With this PR</th> <th>Without this PR</th> </tr> </thead> <tbody> <tr> <th>Behavior</th> <td>Composition can be interrupted by clicking (like Korean IMEs)</td> <td>Composition can not interrupted by clicking</td> </tr> <tr> <th>Screencast (Builtin Chinese IME)</th> <td>  </td> <td>  </td> </tr> <tr> <th>Screencast (Builtin Japanese IME)</th> <td>  </td> <td>  </td> </tr> </tbody> </table> ### macOS: was buggy, still buggy Likely due to this upstream bug in `winit`: https://github.com/rust-windowing/winit/issues/4432 Once `winit` is updated to a version that includes the fix, the behavior should become correct with this PR. <table> <thead> <tr> <th></th> <th>With this PR</th> <th>Without this PR</th> </tr> </thead> <tbody> <tr> <th>Behavior</th> <td>Buggy as before</td> <td>Buggy: Characters are duplicated</td> </tr> <tr> <th>Screencast</th> <td>  </td> <td>  </td> </tr> </tbody> </table> ### Wayland + iBus: Korean IME duplication bug fixed <table> <thead> <tr> <th></th> <th>With this PR</th> <th>Without this PR</th> </tr> </thead> <tbody> <tr> <th>Behavior</th> <td>Correct</td> <td>Buggy: Characters are duplicated</td> </tr> <tr> <th>Screencast</th> <td>  </td> <td>  </td> </tr> </tbody> </table> ### Wayland + iBus: #7485 is fixed <table> <thead> <tr> <th></th> <th>With this PR</th> <th>Without this PR</th> </tr> </thead> <tbody> <tr> <th>Behavior</th> <td>Correct</td> <td>Buggy: Only a single ASCII character can be typed after <code>TextEdit</code> is focused</td> </tr> <tr> <th>Screencast</th> <td>  </td> <td>  </td> </tr> </tbody> </table> ### Wayland + iBus: selection is also not broken This PR does not reintroduce the selection bug fixed in #7973. <table> <thead> <tr> <th></th> <th>With this PR</th> </tr> </thead> <tbody> <tr> <th>Behavior</th> <td>Correct</td> </tr> <tr> <th>Screencast</th> <td>  </td> </tr> </tbody> </table> ### X11 + Fcitx5: IME composition can be interrupted But due to #7975, the experience is still subpar. (Uncommitted text is lost after interruption.) <table> <thead> <tr> <th></th> <th>With this PR</th> <th>Without this PR</th> </tr> </thead> <tbody> <tr> <th>Screencast</th> <td>  </td> <td>  </td> </tr> </tbody> </table> [`Memory`]: https://docs.rs/egui/latest/egui/struct.Memory.html [`TextEdit`]: https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEdit.html [`PlatformOutput`]: https://docs.rs/egui/latest/egui/struct.PlatformOutput.html [`TextEditState`]: https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEditState.html
This commit is contained in:
@@ -101,9 +101,6 @@ pub struct State {
|
||||
/// Only one touch will be interpreted as pointer at any time.
|
||||
pointer_touch_id: Option<u64>,
|
||||
|
||||
/// track ime state
|
||||
has_sent_ime_enabled: bool,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit: Option<accesskit_winit::Adapter>,
|
||||
|
||||
@@ -150,8 +147,6 @@ impl State {
|
||||
simulate_touch_screen: false,
|
||||
pointer_touch_id: None,
|
||||
|
||||
has_sent_ime_enabled: false,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit: None,
|
||||
|
||||
@@ -689,17 +684,11 @@ impl State {
|
||||
// }
|
||||
|
||||
match ime {
|
||||
winit::event::Ime::Enabled => {
|
||||
if cfg!(target_os = "linux") {
|
||||
// This event means different things in X11 and Wayland, but we can just
|
||||
// ignore it and enable IME on the preedit event.
|
||||
// See <https://github.com/rust-windowing/winit/issues/2498>
|
||||
} else {
|
||||
self.ime_event_enable();
|
||||
}
|
||||
}
|
||||
winit::event::Ime::Preedit(text, Some(_cursor)) => {
|
||||
self.ime_event_enable();
|
||||
// [`winit::event::Ime::Enabled`] means different things in X11 and
|
||||
// Wayland, but it doesn't matter to us.
|
||||
// See <https://github.com/rust-windowing/winit/issues/2498>
|
||||
winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
|
||||
winit::event::Ime::Preedit(text, _) => {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
|
||||
@@ -708,53 +697,10 @@ impl State {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
|
||||
self.ime_event_disable();
|
||||
}
|
||||
winit::event::Ime::Disabled => {
|
||||
self.ime_event_disable();
|
||||
}
|
||||
winit::event::Ime::Preedit(_, None) => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ime_event_enable(&mut self) {
|
||||
if !self.has_sent_ime_enabled {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Ime(egui::ImeEvent::Enabled));
|
||||
self.has_sent_ime_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ime_event_disable(&mut self) {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Ime(egui::ImeEvent::Disabled));
|
||||
self.has_sent_ime_enabled = false;
|
||||
}
|
||||
|
||||
/// Returns `true` if the event was sent to egui.
|
||||
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
|
||||
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
|
||||
|
||||
Reference in New Issue
Block a user