diff --git a/src/event.rs b/src/event.rs index e0afca44fa..ad16ac19a4 100644 --- a/src/event.rs +++ b/src/event.rs @@ -886,7 +886,22 @@ pub enum Ime { /// Notifies when text should be inserted into the editor widget. /// /// Right before this event winit will send empty [`Self::Preedit`] event. - Commit(String), + Commit { + content: String, + /// If selection is Some, the selection / cursor position should be updated after + /// placing the `content`. + selection: Option<(usize, usize)>, + /// If compose_region is Some, the compose region should be updated after replacing the + /// text. + compose_region: Option<(usize, usize)>, + }, + + /// Notifies when the text around the cursor should be deleted. + /// If `before_length` and `after_length` are [usize::MAX], the entire text should be deleted. + DeleteSurroundingText { + before_length: usize, + after_length: usize, + }, /// Notifies when the IME was disabled. /// diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 6d8f1e188f..faa455d2f7 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -469,6 +469,35 @@ impl EventLoop { } } } + InputEvent::TextEvent(ime_state) => { + let events = [ + // Send a preedit event so the application knows to expect a commit event + event::Ime::Preedit("".to_string(), None), + // Delete all of the current text + event::Ime::DeleteSurroundingText { + before_length: usize::MAX, + after_length: usize::MAX, + }, + // Replace the previously deleted text with our updated text, and set the cursor and compose region + event::Ime::Commit { + content: ime_state.text.to_string(), + selection: Some((ime_state.selection.start, ime_state.selection.end)), + compose_region: ime_state.compose_region.map(|region| (region.start, region.end)), + } + ]; + + events.into_iter().for_each(|event| { + sticky_exit_callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Ime(event), + }, + self.window_target(), + control_flow, + callback, + ); + }); + } _ => { warn!("Unknown android_activity input event {event:?}") } @@ -901,10 +930,28 @@ impl Window { pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} - pub fn set_ime_allowed(&self, _allowed: bool) {} + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + self.app.show_soft_input(true); + } else { + self.app.hide_soft_input(true); + } + } pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + pub fn set_ime_surrounding_text(&self, text: String, selection: (usize, usize)) { + self.app + .set_text_input_state(android_activity::input::TextInputState { + text, + selection: android_activity::input::TextSpan { + start: selection.0, + end: selection.1, + }, + compose_region: None, + }); + } + pub fn focus_window(&self) {} pub fn request_user_attention(&self, _request_type: Option) {} @@ -987,6 +1034,7 @@ impl Window { pub struct OsError; use std::fmt::{self, Display, Formatter}; + impl Display for OsError { fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { write!(fmt, "Android OS Error") diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 71875be61a..3cebf15411 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -306,6 +306,10 @@ impl Inner { warn!("`Window::set_ime_allowed` is ignored on iOS") } + pub fn set_ime_surrounding_text(&self, _text: String, _selection: (usize, usize)) { + warn!("`Window::set_ime_surrounding_text` is ignored on iOS") + } + pub fn focus_window(&self) { warn!("`Window::set_focus` is ignored on iOS") } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index ae6d762f28..ec3a056757 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -545,6 +545,9 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.set_ime_purpose(purpose)) } + #[inline] + pub fn set_ime_surrounding_text(&self, _text: String, _selection: (usize, usize)) {} + #[inline] pub fn focus_window(&self) { match self { diff --git a/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/src/platform_impl/linux/wayland/seat/text_input/mod.rs index cff18dffeb..51ba851d82 100644 --- a/src/platform_impl/linux/wayland/seat/text_input/mod.rs +++ b/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -143,9 +143,14 @@ impl Dispatch for TextInputState { // Send `Commit`. if let Some(text) = text_input_data.pending_commit.take() { - state - .events_sink - .push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id); + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Commit { + content: text, + selection: None, + compose_region: None, + }), + window_id, + ); } // Send preedit. diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 5d030f89d2..c2704f7814 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -653,7 +653,11 @@ impl EventProcessor { let event = Event::WindowEvent { window_id, - event: WindowEvent::Ime(Ime::Commit(written)), + event: WindowEvent::Ime(Ime::Commit { + content: written, + selection: None, + compose_region: None, + }), }; self.is_composing = false; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index eba06c19a7..6a64d89c24 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -436,7 +436,11 @@ declare_class!( // Commit only if we have marked text. if self.hasMarkedText() && self.is_ime_enabled() && !is_control { self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); - self.queue_event(WindowEvent::Ime(Ime::Commit(string))); + self.queue_event(WindowEvent::Ime(Ime::Commit { + content: string, + selection: None, + compose_region: None, + })); self.state.ime_state.set(ImeState::Commited); } } diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index f174753a84..4b2fe89392 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -1199,6 +1199,9 @@ impl WinitWindow { #[inline] pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] + pub fn set_ime_surrounding_text(&self, _text: String, _selection: (usize, usize)) {} + #[inline] pub fn focus_window(&self) { let is_minimized = self.isMiniaturized(); diff --git a/src/platform_impl/orbital/event_loop.rs b/src/platform_impl/orbital/event_loop.rs index 3cfc2b86f6..89e17e39f5 100644 --- a/src/platform_impl/orbital/event_loop.rs +++ b/src/platform_impl/orbital/event_loop.rs @@ -354,7 +354,11 @@ impl EventLoop { }); event_handler(event::Event::WindowEvent { window_id: RootWindowId(window_id), - event: event::WindowEvent::Ime(Ime::Commit(character.into())), + event: event::WindowEvent::Ime(Ime::Commit { + content: character.into(), + selection: None, + compose_region: None, + }), }); } EventOption::Mouse(MouseEvent { x, y }) => { diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index 11ec6c7ee4..e64c3c51da 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -333,6 +333,9 @@ impl Window { #[inline] pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] + pub fn set_ime_surrounding_text(&self, _text: String, _selection: (usize, usize)) {} + #[inline] pub fn focus_window(&self) {} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index c267f8d6ff..11db77c441 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -366,6 +366,9 @@ impl Window { // Currently not implemented } + #[inline] + pub fn set_ime_surrounding_text(&self, _text: String, _selection: (usize, usize)) {} + #[inline] pub fn focus_window(&self) { self.inner.dispatch(|inner| { diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 9856fca362..baa14422b6 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -1323,7 +1323,11 @@ unsafe fn public_window_callback_inner( }); userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: WindowEvent::Ime(Ime::Commit(text)), + event: WindowEvent::Ime(Ime::Commit { + content: text, + selection: None, + compose_region: None, + }), }); } } @@ -1363,7 +1367,11 @@ unsafe fn public_window_callback_inner( }); userdata.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: WindowEvent::Ime(Ime::Commit(text)), + event: WindowEvent::Ime(Ime::Commit { + content: text, + selection: None, + compose_region: None, + }), }); } } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 1a11b2b667..0c9e4a3577 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -761,6 +761,9 @@ impl Window { #[inline] pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + #[inline] + pub fn set_ime_surrounding_text(&self, _text: String, _selection: (usize, usize)) {} + #[inline] pub fn request_user_attention(&self, request_type: Option) { let window = self.window.clone(); diff --git a/src/window.rs b/src/window.rs index 017501f6b1..2f274f4b92 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1111,6 +1111,16 @@ impl Window { self.window.set_ime_purpose(purpose); } + /// Sets the surrounding text for IME. + /// + /// ## Platform-specific + /// - **Android**: should be set when a textfield is focused, so the keyboard has context + /// for autocomplete. If this is not set, the text will be cleared when the user starts typing. + /// - **iOS / Web / Windows / X11 / macOS / Orbital:** Unsupported. + pub fn set_ime_surrounding_text(&self, text: String, selection: (usize, usize)) { + self.window.set_ime_surrounding_text(text, selection); + } + /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. ///