diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index a879f99d..c61b7093 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -68,7 +68,8 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { is_composing.set(true); input_clone.set_value(""); - runner.input.raw.events.push(egui::Event::CompositionStart); + let egui_event = egui::Event::Ime(egui::ImeEvent::Enabled); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } })?; @@ -77,8 +78,9 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { &input, "compositionupdate", move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { - if let Some(event) = event.data().map(egui::Event::CompositionUpdate) { - runner.input.raw.events.push(event); + if let Some(text) = event.data() { + let egui_event = egui::Event::Ime(egui::ImeEvent::Preedit(text)); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } }, @@ -91,8 +93,9 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { is_composing.set(false); input_clone.set_value(""); - if let Some(event) = event.data().map(egui::Event::CompositionEnd) { - runner.input.raw.events.push(event); + if let Some(text) = event.data() { + let egui_event = egui::Event::Ime(egui::ImeEvent::Commit(text)); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 1bba39b3..81840e95 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -95,7 +95,7 @@ pub struct State { pointer_touch_id: Option, /// track ime state - input_method_editor_started: bool, + has_sent_ime_enabled: bool, #[cfg(feature = "accesskit")] accesskit: Option, @@ -136,7 +136,7 @@ impl State { simulate_touch_screen: false, pointer_touch_id: None, - input_method_editor_started: false, + has_sent_ime_enabled: false, #[cfg(feature = "accesskit")] accesskit: None, @@ -342,23 +342,39 @@ impl State { // We use input_method_editor_started to manually insert CompositionStart // between Commits. match ime { - winit::event::Ime::Enabled | winit::event::Ime::Disabled => (), - winit::event::Ime::Commit(text) => { - self.input_method_editor_started = false; + winit::event::Ime::Enabled => { self.egui_input .events - .push(egui::Event::CompositionEnd(text.clone())); + .push(egui::Event::Ime(egui::ImeEvent::Enabled)); + self.has_sent_ime_enabled = true; } - winit::event::Ime::Preedit(text, Some(_)) => { - if !self.input_method_editor_started { - self.input_method_editor_started = true; - self.egui_input.events.push(egui::Event::CompositionStart); + winit::event::Ime::Preedit(_, None) => {} + winit::event::Ime::Preedit(text, Some(_cursor)) => { + if !self.has_sent_ime_enabled { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Enabled)); + self.has_sent_ime_enabled = true; } self.egui_input .events - .push(egui::Event::CompositionUpdate(text.clone())); + .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); + } + winit::event::Ime::Commit(text) => { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Disabled)); + self.has_sent_ime_enabled = false; + } + winit::event::Ime::Disabled => { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Disabled)); + self.has_sent_ime_enabled = false; } - winit::event::Ime::Preedit(_, None) => {} }; EventResponse { @@ -601,7 +617,8 @@ impl State { }); // If we're not yet translating a touch or we're translating this very // touch … - if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap() == touch.id { + if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap_or_default() == touch.id + { // … emit PointerButton resp. PointerMoved events to emulate mouse match touch.phase { winit::event::TouchPhase::Started => { @@ -1531,7 +1548,7 @@ pub fn create_winit_window_builder( // We set sizes and positions in egui:s own ui points, which depends on the egui // zoom_factor and the native pixels per point, so we need to know that here. // We don't know what monitor the window will appear on though, but - // we'll try to fix that after the window is created in the vall to `apply_viewport_builder_to_window`. + // we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`. let native_pixels_per_point = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index f61b9312..8217f4f5 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -445,14 +445,8 @@ pub enum Event { /// * `zoom > 1`: pinch spread Zoom(f32), - /// IME composition start. - CompositionStart, - - /// A new IME candidate is being suggested. - CompositionUpdate(String), - - /// IME composition ended with this final result. - CompositionEnd(String), + /// IME Event + Ime(ImeEvent), /// On touch screens, report this *in addition to* /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] @@ -507,6 +501,25 @@ pub enum Event { }, } +/// IME event. +/// +/// See +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ImeEvent { + /// Notifies when the IME was enabled. + Enabled, + + /// A new IME candidate is being suggested. + Preedit(String), + + /// IME composition ended with this final result. + Commit(String), + + /// Notifies when the IME was disabled. + Disabled, +} + /// Mouse button (or similar for touch input) #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index f022e860..c0b8532d 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -950,47 +950,51 @@ fn events( .. } => check_for_mutating_key_press(os, &mut cursor_range, text, galley, modifiers, *key), - Event::CompositionStart => { - state.has_ime = true; - None - } - - Event::CompositionUpdate(text_mark) => { - // empty prediction can be produced when user press backspace - // or escape during ime. We should clear current text. - if text_mark != "\n" && text_mark != "\r" && state.has_ime { - let mut ccursor = text.delete_selected(&cursor_range); - let start_cursor = ccursor; - if !text_mark.is_empty() { - text.insert_text_at(&mut ccursor, text_mark, char_limit); - } + Event::Ime(ime_event) => match ime_event { + ImeEvent::Enabled => { + state.ime_enabled = true; state.ime_cursor_range = cursor_range; - Some(CCursorRange::two(start_cursor, ccursor)) - } else { None } - } - - Event::CompositionEnd(prediction) => { - // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, - // so do not check `state.has_ime = true` in the following statement. - if prediction != "\n" && prediction != "\r" { - state.has_ime = false; - let mut ccursor; - if !prediction.is_empty() - && cursor_range.secondary.ccursor.index - == state.ime_cursor_range.secondary.ccursor.index - { - ccursor = text.delete_selected(&cursor_range); - text.insert_text_at(&mut ccursor, prediction, char_limit); + ImeEvent::Preedit(text_mark) => { + if text_mark == "\n" || text_mark == "\r" { + None } else { - ccursor = cursor_range.primary.ccursor; + // Empty prediction can be produced when user press backspace + // or escape during IME, so we clear current text. + let mut ccursor = text.delete_selected(&cursor_range); + let start_cursor = ccursor; + if !text_mark.is_empty() { + text.insert_text_at(&mut ccursor, text_mark, char_limit); + } + state.ime_cursor_range = cursor_range; + Some(CCursorRange::two(start_cursor, ccursor)) } - Some(CCursorRange::one(ccursor)) - } else { + } + ImeEvent::Commit(prediction) => { + if prediction == "\n" || prediction == "\r" { + None + } else { + state.ime_enabled = false; + + if !prediction.is_empty() + && cursor_range.secondary.ccursor.index + == state.ime_cursor_range.secondary.ccursor.index + { + let mut ccursor = text.delete_selected(&cursor_range); + text.insert_text_at(&mut ccursor, prediction, char_limit); + Some(CCursorRange::one(ccursor)) + } else { + let ccursor = cursor_range.primary.ccursor; + Some(CCursorRange::one(ccursor)) + } + } + } + ImeEvent::Disabled => { + state.ime_enabled = false; None } - } + }, _ => None, }; diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index d0334da3..ef4a8909 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -42,7 +42,7 @@ pub struct TextEditState { // If IME candidate window is shown on this text edit. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) has_ime: bool, + pub(crate) ime_enabled: bool, // cursor range for IME candidate. #[cfg_attr(feature = "serde", serde(skip))]