mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00:04 -04:00
Delegate handling of IME interruptions to integrations to fix virtual keyboard flickering on web (#8078)
* Closes N/A * Partially replaces #7983 * Related: #8045 * [x] I have followed the instructions in the PR template ## Details In #7983, I modified `Memory::request_focus` to interrupt any ongoing IME composition. This fixed a bug where clicking inside an already focused `TextEdit` failed to cancel the active composition, resulting in duplicated text: https://github.com/emilk/egui/pull/8045#issuecomment-4193310616 To avoid introducing API changes in that PR, I ensured the IME state was reset by forcing `PlatformOutput::ime` to `None` for at least one frame. While this works well on desktop platforms, it causes virtual keyboard flickering on the web: https://github.com/emilk/egui/pull/8045#issuecomment-4193035008 In this PR, I delegate the responsibility for handling IME composition interruptions to integrations, allowing each integration to decide how to interrupt compositions in a flexible manner. ### The new field `should_interrupt_composition` on `IMEOutput`. Instead of introducing a new `OutputCommand` variant, this PR adds a new field `should_interrupt_composition` to `IMEOutput`. Interrupting an active composition is only meaningful when IME remains allowed. If IME should be disabled altogether, `PlatformOutput::ime` can simply be set to `None`. Given this, IMO, it is more appropriate to attach the interrupt signal to `IMEOutput` (i.e., the type of `PlatformOutput::ime`).
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -117,21 +117,10 @@ pub struct Memory {
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
popups: ViewportIdMap<OpenPopup>,
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user