mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -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:
@@ -75,11 +75,7 @@ impl TextAgent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let on_composition_start = {
|
let on_composition_start = {
|
||||||
let input = input.clone();
|
|
||||||
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
||||||
input.set_value("");
|
|
||||||
let event = egui::Event::Ime(egui::ImeEvent::Enabled);
|
|
||||||
runner.input.raw.events.push(event);
|
|
||||||
// Repaint moves the text agent into place,
|
// Repaint moves the text agent into place,
|
||||||
// see `move_to` in `AppRunner::handle_platform_output`.
|
// see `move_to` in `AppRunner::handle_platform_output`.
|
||||||
runner.needs_repaint.repaint_asap();
|
runner.needs_repaint.repaint_asap();
|
||||||
|
|||||||
@@ -101,9 +101,6 @@ pub struct State {
|
|||||||
/// Only one touch will be interpreted as pointer at any time.
|
/// Only one touch will be interpreted as pointer at any time.
|
||||||
pointer_touch_id: Option<u64>,
|
pointer_touch_id: Option<u64>,
|
||||||
|
|
||||||
/// track ime state
|
|
||||||
has_sent_ime_enabled: bool,
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
pub accesskit: Option<accesskit_winit::Adapter>,
|
pub accesskit: Option<accesskit_winit::Adapter>,
|
||||||
|
|
||||||
@@ -150,8 +147,6 @@ impl State {
|
|||||||
simulate_touch_screen: false,
|
simulate_touch_screen: false,
|
||||||
pointer_touch_id: None,
|
pointer_touch_id: None,
|
||||||
|
|
||||||
has_sent_ime_enabled: false,
|
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit: None,
|
accesskit: None,
|
||||||
|
|
||||||
@@ -689,17 +684,11 @@ impl State {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
match ime {
|
match ime {
|
||||||
winit::event::Ime::Enabled => {
|
// [`winit::event::Ime::Enabled`] means different things in X11 and
|
||||||
if cfg!(target_os = "linux") {
|
// Wayland, but it doesn't matter to us.
|
||||||
// This event means different things in X11 and Wayland, but we can just
|
// See <https://github.com/rust-windowing/winit/issues/2498>
|
||||||
// ignore it and enable IME on the preedit event.
|
winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
|
||||||
// See <https://github.com/rust-windowing/winit/issues/2498>
|
winit::event::Ime::Preedit(text, _) => {
|
||||||
} else {
|
|
||||||
self.ime_event_enable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
winit::event::Ime::Preedit(text, Some(_cursor)) => {
|
|
||||||
self.ime_event_enable();
|
|
||||||
self.egui_input
|
self.egui_input
|
||||||
.events
|
.events
|
||||||
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
|
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
|
||||||
@@ -708,53 +697,10 @@ impl State {
|
|||||||
self.egui_input
|
self.egui_input
|
||||||
.events
|
.events
|
||||||
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
|
.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.
|
/// Returns `true` if the event was sent to egui.
|
||||||
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
|
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
|
||||||
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
|
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
|
||||||
|
|||||||
@@ -605,15 +605,22 @@ pub enum Event {
|
|||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum ImeEvent {
|
pub enum ImeEvent {
|
||||||
/// Notifies when the IME was enabled.
|
/// Notifies when the IME was enabled.
|
||||||
|
#[deprecated = "No longer used by egui"]
|
||||||
Enabled,
|
Enabled,
|
||||||
|
|
||||||
/// A new IME candidate is being suggested.
|
/// A new IME candidate is being suggested.
|
||||||
|
///
|
||||||
|
/// An empty preedit string indicates that the IME has been dismissed, while
|
||||||
|
/// a non-empty preedit string indicates that the IME is active.
|
||||||
Preedit(String),
|
Preedit(String),
|
||||||
|
|
||||||
/// IME composition ended with this final result.
|
/// IME composition ended with this final result.
|
||||||
|
///
|
||||||
|
/// The IME is considered dismissed after this event.
|
||||||
Commit(String),
|
Commit(String),
|
||||||
|
|
||||||
/// Notifies when the IME was disabled.
|
/// Notifies when the IME was disabled.
|
||||||
|
#[deprecated = "No longer used by egui"]
|
||||||
Disabled,
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ pub struct PlatformOutput {
|
|||||||
/// This is set if, and only if, the user is currently editing text.
|
/// This is set if, and only if, the user is currently editing text.
|
||||||
///
|
///
|
||||||
/// Useful for IME.
|
/// Useful for IME.
|
||||||
|
///
|
||||||
|
/// This field should only be set by the widget that currently owns IME
|
||||||
|
/// events (see [`crate::Memory::owns_ime_events`]).
|
||||||
pub ime: Option<IMEOutput>,
|
pub ime: Option<IMEOutput>,
|
||||||
|
|
||||||
/// The difference in the widget tree since last frame.
|
/// The difference in the widget tree since last frame.
|
||||||
|
|||||||
@@ -116,6 +116,22 @@ pub struct Memory {
|
|||||||
/// (e.g. relative to some other widget).
|
/// (e.g. relative to some other widget).
|
||||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||||
popups: ViewportIdMap<OpenPopup>,
|
popups: ViewportIdMap<OpenPopup>,
|
||||||
|
|
||||||
|
/// When the last IME interruption was made.
|
||||||
|
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||||
|
ime_interruption_time: ImeInterruptionTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
enum ImeInterruptionTime {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// The IME was interrupted in the current frame.
|
||||||
|
ThisFrame,
|
||||||
|
|
||||||
|
/// The IME was interrupted in the previous frame.
|
||||||
|
LastFrame,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Memory {
|
impl Default for Memory {
|
||||||
@@ -133,6 +149,7 @@ impl Default for Memory {
|
|||||||
popups: Default::default(),
|
popups: Default::default(),
|
||||||
everything_is_visible: Default::default(),
|
everything_is_visible: Default::default(),
|
||||||
add_fonts: Default::default(),
|
add_fonts: Default::default(),
|
||||||
|
ime_interruption_time: Default::default(),
|
||||||
};
|
};
|
||||||
slf.interactions.entry(slf.viewport_id).or_default();
|
slf.interactions.entry(slf.viewport_id).or_default();
|
||||||
slf.areas.entry(slf.viewport_id).or_default();
|
slf.areas.entry(slf.viewport_id).or_default();
|
||||||
@@ -761,6 +778,16 @@ impl Memory {
|
|||||||
|
|
||||||
self.areas.entry(self.viewport_id).or_default();
|
self.areas.entry(self.viewport_id).or_default();
|
||||||
|
|
||||||
|
match self.ime_interruption_time {
|
||||||
|
ImeInterruptionTime::ThisFrame => {
|
||||||
|
self.ime_interruption_time = ImeInterruptionTime::LastFrame;
|
||||||
|
}
|
||||||
|
ImeInterruptionTime::LastFrame => {
|
||||||
|
self.ime_interruption_time = ImeInterruptionTime::None;
|
||||||
|
}
|
||||||
|
ImeInterruptionTime::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
// self.interactions is handled elsewhere
|
// self.interactions is handled elsewhere
|
||||||
|
|
||||||
self.options.begin_pass(new_raw_input);
|
self.options.begin_pass(new_raw_input);
|
||||||
@@ -875,9 +902,12 @@ impl Memory {
|
|||||||
|
|
||||||
/// Give keyboard focus to a specific widget.
|
/// Give keyboard focus to a specific widget.
|
||||||
/// See also [`crate::Response::request_focus`].
|
/// See also [`crate::Response::request_focus`].
|
||||||
|
///
|
||||||
|
/// Calling this will interrupt IME composition.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn request_focus(&mut self, id: Id) {
|
pub fn request_focus(&mut self, id: Id) {
|
||||||
self.focus_mut().focused_widget = Some(FocusWidget::new(id));
|
self.focus_mut().focused_widget = Some(FocusWidget::new(id));
|
||||||
|
self.interrupt_ime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surrender keyboard focus for a specific widget.
|
/// Surrender keyboard focus for a specific widget.
|
||||||
@@ -993,6 +1023,36 @@ impl Memory {
|
|||||||
pub(crate) fn focus_mut(&mut self) -> &mut Focus {
|
pub(crate) fn focus_mut(&mut self) -> &mut Focus {
|
||||||
self.focus.entry(self.viewport_id).or_default()
|
self.focus.entry(self.viewport_id).or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the widget owns IME events.
|
||||||
|
///
|
||||||
|
/// A widget should only consume IME events if this returns `true`. At most
|
||||||
|
/// one widget can own IME events for each frame.
|
||||||
|
pub fn owns_ime_events(&self, id: Id) -> bool {
|
||||||
|
let Some(focus) = self.focus() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// We check across two frames because the widget that called
|
||||||
|
// `interrupt_ime` may run after other widgets that call this method
|
||||||
|
// within the same frame.
|
||||||
|
if matches!(
|
||||||
|
self.ime_interruption_time,
|
||||||
|
ImeInterruptionTime::ThisFrame | ImeInterruptionTime::LastFrame
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
focus.focused() == Some(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interrupt the current IME composition, if any.
|
||||||
|
///
|
||||||
|
/// This causes [`Self::owns_ime_events`] to return `false` for all widgets
|
||||||
|
/// for the remainder of this frame and the next frame, giving time
|
||||||
|
/// for the IME to be dismissed (by making `platform_output.ime` be `None`
|
||||||
|
/// for at least one frame).
|
||||||
|
pub fn interrupt_ime(&mut self) {
|
||||||
|
self.ime_interruption_time = ImeInterruptionTime::ThisFrame;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State of an open popup.
|
/// State of an open popup.
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ use crate::{
|
|||||||
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
|
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
|
||||||
os::OperatingSystem,
|
os::OperatingSystem,
|
||||||
output::OutputEvent,
|
output::OutputEvent,
|
||||||
response, text_selection,
|
response,
|
||||||
text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection},
|
text_edit::state::TextEditCursorPurpose,
|
||||||
|
text_selection::{
|
||||||
|
self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
|
||||||
|
},
|
||||||
vec2,
|
vec2,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -858,33 +861,23 @@ impl TextEdit<'_> {
|
|||||||
now - state.last_interaction_time,
|
now - state.last_interaction_time,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if ui.memory(|mem| mem.owns_ime_events(id)) {
|
||||||
// Set IME output (in screen coords) when text is editable and visible
|
// Set IME output (in screen coords) when text is editable and visible
|
||||||
let to_global = ui
|
let to_global = ui
|
||||||
.ctx()
|
.ctx()
|
||||||
.layer_transform_to_global(ui.layer_id())
|
.layer_transform_to_global(ui.layer_id())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
ui.output_mut(|o| {
|
||||||
ui.output_mut(|o| {
|
o.ime = Some(crate::output::IMEOutput {
|
||||||
o.ime = Some(crate::output::IMEOutput {
|
rect: to_global * inner_rect,
|
||||||
rect: to_global * inner_rect,
|
cursor_rect: to_global * primary_cursor_rect,
|
||||||
cursor_rect: to_global * primary_cursor_rect,
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures correct IME behavior when the text input area gains or loses focus.
|
|
||||||
if state.ime_enabled && (response.gained_focus() || response.lost_focus()) {
|
|
||||||
state.ime_enabled = false;
|
|
||||||
if let Some(mut ccursor_range) = state.cursor.char_range() {
|
|
||||||
ccursor_range.secondary.index = ccursor_range.primary.index;
|
|
||||||
state.cursor.set_char_range(Some(ccursor_range));
|
|
||||||
}
|
|
||||||
ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.clone().store(ui.ctx(), id);
|
state.clone().store(ui.ctx(), id);
|
||||||
|
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
@@ -999,6 +992,11 @@ fn events(
|
|||||||
|
|
||||||
let events = ui.input(|i| i.filtered_events(&event_filter));
|
let events = ui.input(|i| i.filtered_events(&event_filter));
|
||||||
|
|
||||||
|
let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id));
|
||||||
|
if !owns_ime_events {
|
||||||
|
state.cursor_purpose = TextEditCursorPurpose::Selection;
|
||||||
|
}
|
||||||
|
|
||||||
for event in &events {
|
for event in &events {
|
||||||
let did_mutate_text = match event {
|
let did_mutate_text = match event {
|
||||||
// First handle events that only changes the selection cursor, not the text:
|
// First handle events that only changes the selection cursor, not the text:
|
||||||
@@ -1126,7 +1124,7 @@ fn events(
|
|||||||
..
|
..
|
||||||
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
|
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
|
||||||
|
|
||||||
Event::Ime(ime_event) => {
|
Event::Ime(ime_event) if owns_ime_events => {
|
||||||
/// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
|
/// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
|
||||||
/// might be emitted from different integrations to signify that
|
/// might be emitted from different integrations to signify that
|
||||||
/// the current IME composition should be cleared.
|
/// the current IME composition should be cleared.
|
||||||
@@ -1160,46 +1158,58 @@ fn events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match ime_event {
|
match ime_event {
|
||||||
ImeEvent::Enabled => {
|
#[expect(deprecated)]
|
||||||
state.ime_enabled = true;
|
ImeEvent::Enabled | ImeEvent::Disabled => None,
|
||||||
state.ime_cursor_range = cursor_range;
|
// Ignore `Preedit`/`Commit` events with empty text when
|
||||||
|
// there is no active IME composition.
|
||||||
|
//
|
||||||
|
// Some integrations may emit these events when there is no
|
||||||
|
// active IME composition (e.g. when `set_ime_allowed` or
|
||||||
|
// `set_ime_cursor_area` is called on `winit`'s `Window` on
|
||||||
|
// Wayland). Without this guard, they would clear any
|
||||||
|
// selected text.
|
||||||
|
//
|
||||||
|
// TODO(umajho): Ideally this would be handled by the
|
||||||
|
// integration, but since this guard is harmless for well-
|
||||||
|
// behaved integrations and also fixes the issue described
|
||||||
|
// above, it is good enough for now.
|
||||||
|
ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text)
|
||||||
|
if composition_text.is_empty()
|
||||||
|
&& !matches!(
|
||||||
|
state.cursor_purpose,
|
||||||
|
TextEditCursorPurpose::ImeComposition
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text)
|
||||||
|
if composition_text == "\n" || composition_text == "\r" =>
|
||||||
|
{
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
ImeEvent::Preedit(preedit_text) => {
|
ImeEvent::Preedit(preedit_text) => {
|
||||||
if preedit_text == "\n" || preedit_text == "\r" {
|
state.cursor_purpose = if preedit_text.is_empty() {
|
||||||
None
|
TextEditCursorPurpose::Selection
|
||||||
} else {
|
} else {
|
||||||
let mut ccursor = clear_preedit_text(text, &cursor_range);
|
TextEditCursorPurpose::ImeComposition
|
||||||
|
};
|
||||||
|
let mut ccursor = clear_preedit_text(text, &cursor_range);
|
||||||
|
|
||||||
let start_cursor = ccursor;
|
let start_cursor = ccursor;
|
||||||
if !preedit_text.is_empty() {
|
if !preedit_text.is_empty() {
|
||||||
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
|
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
|
||||||
}
|
|
||||||
state.ime_cursor_range = cursor_range;
|
|
||||||
Some(CCursorRange::two(start_cursor, ccursor))
|
|
||||||
}
|
}
|
||||||
|
Some(CCursorRange::two(start_cursor, ccursor))
|
||||||
}
|
}
|
||||||
ImeEvent::Commit(commit_text) => {
|
ImeEvent::Commit(commit_text) => {
|
||||||
if commit_text == "\n" || commit_text == "\r" {
|
state.cursor_purpose = TextEditCursorPurpose::Selection;
|
||||||
None
|
let mut ccursor = clear_preedit_text(text, &cursor_range);
|
||||||
} else {
|
|
||||||
state.ime_enabled = false;
|
|
||||||
|
|
||||||
let mut ccursor = clear_preedit_text(text, &cursor_range);
|
if !commit_text.is_empty() {
|
||||||
|
text.insert_text_at(&mut ccursor, commit_text, char_limit);
|
||||||
if !commit_text.is_empty()
|
|
||||||
&& cursor_range.secondary.index
|
|
||||||
== state.ime_cursor_range.secondary.index
|
|
||||||
{
|
|
||||||
text.insert_text_at(&mut ccursor, commit_text, char_limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(CCursorRange::one(ccursor))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ImeEvent::Disabled => {
|
Some(CCursorRange::one(ccursor))
|
||||||
state.ime_enabled = false;
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,18 +37,14 @@ pub struct TextEditState {
|
|||||||
/// Controls the text selection.
|
/// Controls the text selection.
|
||||||
pub cursor: TextCursorState,
|
pub cursor: TextCursorState,
|
||||||
|
|
||||||
|
/// The purpose of the cursor.
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
pub(crate) cursor_purpose: TextEditCursorPurpose,
|
||||||
|
|
||||||
/// Wrapped in Arc for cheaper clones.
|
/// Wrapped in Arc for cheaper clones.
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
pub(crate) undoer: Arc<Mutex<TextEditUndoer>>,
|
pub(crate) undoer: Arc<Mutex<TextEditUndoer>>,
|
||||||
|
|
||||||
// If IME candidate window is shown on this text edit.
|
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
|
||||||
pub(crate) ime_enabled: bool,
|
|
||||||
|
|
||||||
// cursor range for IME candidate.
|
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
|
||||||
pub(crate) ime_cursor_range: CCursorRange,
|
|
||||||
|
|
||||||
// Text offset within the widget area.
|
// Text offset within the widget area.
|
||||||
// Used for sensing and singleline text clipping.
|
// Used for sensing and singleline text clipping.
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
@@ -82,3 +78,13 @@ impl TextEditState {
|
|||||||
self.set_undoer(TextEditUndoer::default());
|
self.set_undoer(TextEditUndoer::default());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub(crate) enum TextEditCursorPurpose {
|
||||||
|
/// The cursor is used for text selection.
|
||||||
|
#[default]
|
||||||
|
Selection,
|
||||||
|
|
||||||
|
/// The cursor is used for IME composition.
|
||||||
|
ImeComposition,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user