diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index e3d4f8860..20b904244 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -56,8 +56,13 @@ impl TextAgent { let input = input.clone(); move |event: web_sys::InputEvent, runner: &mut AppRunner| { let text = input.value(); - // Fix android virtual keyboard Gboard - // This removes the virtual keyboard's suggestion. + // Workaround for an Android Gboard issue: after typing a word, + // the user has to delete invisible characters (whose count + // matches the length of the current suggestion) before actual + // characters are deleted, unless the focus has been reset. + // + // this issue appears to have been fixed in Gboard sometime + // between versions 14.7.09 and 17.0.12. if !event.is_composing() { input.blur().ok(); input.focus().ok(); @@ -132,6 +137,12 @@ impl TextAgent { let Some(ime) = ime else { return Ok(()) }; + if ime.should_interrupt_composition { + // no-op for now: currently, the text agent is sizeless, so any + // click shifts focus to the canvas, which naturally interrupts the + // composition. + } + let mut canvas_rect = super::canvas_content_rect(canvas); // Fix for safari with virtual keyboard flapping position if is_mobile_safari() { diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 99a9894f3..3526f92be 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1049,7 +1049,8 @@ impl State { self.set_cursor_icon(window, cursor_icon); let allow_ime = ime.is_some(); - if self.allow_ime != allow_ime { + let is_toggling_ime = self.allow_ime != allow_ime; + if is_toggling_ime { self.allow_ime = allow_ime; #[cfg(target_os = "windows")] if !self.allow_ime { @@ -1066,6 +1067,14 @@ impl State { } if let Some(ime) = ime { + if !is_toggling_ime && ime.should_interrupt_composition { + // TODO(umajho): use a more proper way to interrupt composition + // if `winit` provides one in the future. + + window.set_ime_allowed(false); + window.set_ime_allowed(true); + } + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); let ime_rect_px = pixels_per_point * ime.rect; if self.ime_rect_px != Some(ime_rect_px) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 809f5dac5..433446648 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2612,6 +2612,12 @@ impl ContextImpl { let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); + if self.memory.should_interrupt_ime() + && let Some(ime) = &mut platform_output.ime + { + ime.should_interrupt_composition = true; + } + { profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index ea3ff4eec..77a0eee63 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -79,6 +79,9 @@ pub struct IMEOutput { /// /// This is a very thin rectangle. pub cursor_rect: crate::Rect, + + /// Whether any ongoing IME composition should be interrupted. + pub should_interrupt_composition: bool, } /// Commands that the egui integration should execute at the end of a frame. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 34e0fe319..8d109c254 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -117,21 +117,10 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] popups: ViewportIdMap, - /// When the last IME interruption was made. + /// Whether to inform the backend to interrupt any ongoing IME composition + /// this pass. #[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, + requested_interrupt_ime: bool, } impl Default for Memory { @@ -149,7 +138,7 @@ impl Default for Memory { popups: Default::default(), everything_is_visible: Default::default(), add_fonts: Default::default(), - ime_interruption_time: Default::default(), + requested_interrupt_ime: Default::default(), }; slf.interactions.entry(slf.viewport_id).or_default(); slf.areas.entry(slf.viewport_id).or_default(); @@ -778,15 +767,7 @@ impl Memory { 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.requested_interrupt_ime = false; // self.interactions is handled elsewhere @@ -1028,30 +1009,22 @@ impl Memory { /// /// A widget should only consume IME events if this returns `true`. At most /// one widget can own IME events for each frame. + #[inline(always)] 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) + // Note: Even if the IME is being interrupted in the current frame, we + // should not return `false` here, since we still need + // `PlatformOutput::ime` to be set in such cases. + + self.has_focus(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; + self.requested_interrupt_ime = true; + } + + pub(crate) fn should_interrupt_ime(&self) -> bool { + self.requested_interrupt_ime } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 9905a2a55..8ba188443 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -871,6 +871,7 @@ impl TextEdit<'_> { o.ime = Some(crate::output::IMEOutput { rect: to_global * inner_rect, cursor_rect: to_global * primary_cursor_rect, + should_interrupt_composition: false, }); }); }