Improve IME support with new `Event::Ime` (#4358)

* Closes #4354 

Fix: can't repeat input chinese words

AND

For Windows :
ImeEnable
ImeDisable

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
rustbasic 2024-04-22 19:43:07 +09:00 committed by GitHub
parent 587bc2034a
commit 436c671331
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 99 additions and 62 deletions

View File

@ -68,7 +68,8 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
is_composing.set(true); is_composing.set(true);
input_clone.set_value(""); 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(); runner.needs_repaint.repaint_asap();
} }
})?; })?;
@ -77,8 +78,9 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
&input, &input,
"compositionupdate", "compositionupdate",
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) { if let Some(text) = event.data() {
runner.input.raw.events.push(event); let egui_event = egui::Event::Ime(egui::ImeEvent::Preedit(text));
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
} }
}, },
@ -91,8 +93,9 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
is_composing.set(false); is_composing.set(false);
input_clone.set_value(""); input_clone.set_value("");
if let Some(event) = event.data().map(egui::Event::CompositionEnd) { if let Some(text) = event.data() {
runner.input.raw.events.push(event); let egui_event = egui::Event::Ime(egui::ImeEvent::Commit(text));
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap(); runner.needs_repaint.repaint_asap();
} }
} }

View File

@ -95,7 +95,7 @@ pub struct State {
pointer_touch_id: Option<u64>, pointer_touch_id: Option<u64>,
/// track ime state /// track ime state
input_method_editor_started: bool, has_sent_ime_enabled: bool,
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>, accesskit: Option<accesskit_winit::Adapter>,
@ -136,7 +136,7 @@ impl State {
simulate_touch_screen: false, simulate_touch_screen: false,
pointer_touch_id: None, pointer_touch_id: None,
input_method_editor_started: false, has_sent_ime_enabled: false,
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit: None, accesskit: None,
@ -342,23 +342,39 @@ impl State {
// We use input_method_editor_started to manually insert CompositionStart // We use input_method_editor_started to manually insert CompositionStart
// between Commits. // between Commits.
match ime { match ime {
winit::event::Ime::Enabled | winit::event::Ime::Disabled => (), winit::event::Ime::Enabled => {
winit::event::Ime::Commit(text) => {
self.input_method_editor_started = false;
self.egui_input self.egui_input
.events .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);
}
self.egui_input
.events
.push(egui::Event::CompositionUpdate(text.clone()));
} }
winit::event::Ime::Preedit(_, None) => {} 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::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;
}
}; };
EventResponse { EventResponse {
@ -601,7 +617,8 @@ impl State {
}); });
// If we're not yet translating a touch or we're translating this very // If we're not yet translating a touch or we're translating this very
// touch … // 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 // … emit PointerButton resp. PointerMoved events to emulate mouse
match touch.phase { match touch.phase {
winit::event::TouchPhase::Started => { winit::event::TouchPhase::Started => {
@ -1531,7 +1548,7 @@ pub fn create_winit_window_builder<T>(
// We set sizes and positions in egui:s own ui points, which depends on the egui // 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. // 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 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 let native_pixels_per_point = event_loop
.primary_monitor() .primary_monitor()
.or_else(|| event_loop.available_monitors().next()) .or_else(|| event_loop.available_monitors().next())

View File

@ -445,14 +445,8 @@ pub enum Event {
/// * `zoom > 1`: pinch spread /// * `zoom > 1`: pinch spread
Zoom(f32), Zoom(f32),
/// IME composition start. /// IME Event
CompositionStart, Ime(ImeEvent),
/// A new IME candidate is being suggested.
CompositionUpdate(String),
/// IME composition ended with this final result.
CompositionEnd(String),
/// On touch screens, report this *in addition to* /// On touch screens, report this *in addition to*
/// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`]
@ -507,6 +501,25 @@ pub enum Event {
}, },
} }
/// IME event.
///
/// See <https://docs.rs/winit/latest/winit/event/enum.Ime.html>
#[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) /// Mouse button (or similar for touch input)
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]

View File

@ -950,15 +950,18 @@ fn events(
.. ..
} => check_for_mutating_key_press(os, &mut cursor_range, text, galley, modifiers, *key), } => check_for_mutating_key_press(os, &mut cursor_range, text, galley, modifiers, *key),
Event::CompositionStart => { Event::Ime(ime_event) => match ime_event {
state.has_ime = true; ImeEvent::Enabled => {
state.ime_enabled = true;
state.ime_cursor_range = cursor_range;
None None
} }
ImeEvent::Preedit(text_mark) => {
Event::CompositionUpdate(text_mark) => { if text_mark == "\n" || text_mark == "\r" {
// empty prediction can be produced when user press backspace None
// or escape during ime. We should clear current text. } else {
if text_mark != "\n" && text_mark != "\r" && state.has_ime { // 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 mut ccursor = text.delete_selected(&cursor_range);
let start_cursor = ccursor; let start_cursor = ccursor;
if !text_mark.is_empty() { if !text_mark.is_empty() {
@ -966,31 +969,32 @@ fn events(
} }
state.ime_cursor_range = cursor_range; state.ime_cursor_range = cursor_range;
Some(CCursorRange::two(start_cursor, ccursor)) Some(CCursorRange::two(start_cursor, ccursor))
} else { }
}
ImeEvent::Commit(prediction) => {
if prediction == "\n" || prediction == "\r" {
None None
} } else {
} state.ime_enabled = false;
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() if !prediction.is_empty()
&& cursor_range.secondary.ccursor.index && cursor_range.secondary.ccursor.index
== state.ime_cursor_range.secondary.ccursor.index == state.ime_cursor_range.secondary.ccursor.index
{ {
ccursor = text.delete_selected(&cursor_range); let mut ccursor = text.delete_selected(&cursor_range);
text.insert_text_at(&mut ccursor, prediction, char_limit); text.insert_text_at(&mut ccursor, prediction, char_limit);
} else {
ccursor = cursor_range.primary.ccursor;
}
Some(CCursorRange::one(ccursor)) Some(CCursorRange::one(ccursor))
} else { } else {
let ccursor = cursor_range.primary.ccursor;
Some(CCursorRange::one(ccursor))
}
}
}
ImeEvent::Disabled => {
state.ime_enabled = false;
None None
} }
} },
_ => None, _ => None,
}; };

View File

@ -42,7 +42,7 @@ pub struct TextEditState {
// If IME candidate window is shown on this text edit. // If IME candidate window is shown on this text edit.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) has_ime: bool, pub(crate) ime_enabled: bool,
// cursor range for IME candidate. // cursor range for IME candidate.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]