diff --git a/examples/application.rs b/examples/application.rs index e6523c12..5832de16 100644 --- a/examples/application.rs +++ b/examples/application.rs @@ -9,7 +9,7 @@ use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::Arc; #[cfg(all(not(android_platform), not(web_platform)))] use std::time::Instant; -use std::{fmt, mem}; +use std::{cmp, fmt, mem}; use ::tracing::{error, info}; use cursor_icon::CursorIcon; @@ -525,6 +525,26 @@ impl ApplicationHandler for Application { let request_data = window.get_ime_update(); window.window.request_ime_update(ImeRequest::Update(request_data)).unwrap(); }, + Ime::DeleteSurrounding { before_bytes, after_bytes } => { + let (text, cursor) = &window.text_field_contents; + + // To anyone copying this, keep in mind that this doesn't take text selection + // into account. The deletion happens *around* the pre-edit, + // and may remove the whole selection or a part of it. + let delete_start = cursor.saturating_sub(before_bytes); + let delete_end = cmp::min(cursor.saturating_add(after_bytes), text.len()); + if text.is_char_boundary(delete_start) && text.is_char_boundary(delete_end) { + let new_text = { + let mut t = String::from(&text[..delete_start]); + t.push_str(&text[delete_end..]); + t + }; + window.text_field_contents = (new_text, delete_start); + info!("IME deleted bytes: {before_bytes}, {after_bytes}"); + } else { + error!("Buggy IME tried to delete with indices not on char boundary."); + } + }, Ime::Disabled => info!("IME disabled for Window={window_id:?}"), }, WindowEvent::PinchGesture { delta, .. } => { diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 42c37c8d..9b6ddb81 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -879,6 +879,8 @@ impl From for Modifiers { /// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. /// +/// The `Ime` events must be applied in the order they arrive. +/// /// This is also called a "composition event". /// /// Most keypresses using a latin-like keyboard layout simply generate a @@ -935,7 +937,7 @@ pub enum Ime { /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string /// this indicates that preedit was cleared. /// - /// The cursor position is byte-wise indexed. + /// The cursor position is byte-wise indexed, assuming UTF-8. Preedit(String, Option<(usize, usize)>), /// Notifies when text should be inserted into the editor widget. @@ -943,6 +945,20 @@ pub enum Ime { /// Right before this event winit will send empty [`Self::Preedit`] event. Commit(String), + /// Delete text surrounding the cursor or selection. + /// + /// This event does not affect either the pre-edit string. + /// This means that the application must first remove the pre-edit, + /// then execute the deletion, then insert the removed text back. + /// + /// This event assumes text is stored in UTF-8. + DeleteSurrounding { + /// Bytes to remove before the selection + before_bytes: usize, + /// Bytes to remove after the selection + after_bytes: usize, + }, + /// Notifies when the IME was disabled. /// /// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or diff --git a/winit-wayland/src/seat/text_input/mod.rs b/winit-wayland/src/seat/text_input/mod.rs index 3aca8c2f..f505fc70 100644 --- a/winit-wayland/src/seat/text_input/mod.rs +++ b/winit-wayland/src/seat/text_input/mod.rs @@ -115,12 +115,42 @@ impl Dispatch for TextInputState { text_input_data.pending_preedit = None; text_input_data.pending_commit = text; }, + TextInputEvent::DeleteSurroundingText { before_length, after_length } => { + text_input_data.pending_delete = Some(DeleteSurroundingText { + before: before_length as usize, + after: after_length as usize, + }); + }, TextInputEvent::Done { .. } => { let window_id = match text_input_data.surface.as_ref() { Some(surface) => crate::make_wid(surface), None => return, }; + // The events are sent to the user separately, so + // CAUTION: events must always arrive in the order compatible with the application + // order specified by the text-input-v3 protocol: + // + // As of version 1: + // 1. Replace existing preedit string with the cursor. + // 2. Delete requested surrounding text. + // 3. Insert commit string with the cursor at its end. + // 4. Calculate surrounding text to send. + // 5. Insert new preedit text in cursor position. + // 6. Place cursor inside preedit text. + + if let Some(DeleteSurroundingText { before, after }) = + text_input_data.pending_delete + { + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::DeleteSurrounding { + before_bytes: before, + after_bytes: after, + }), + window_id, + ); + } + // Clear preedit, unless all we'll be doing next is sending a new preedit. if text_input_data.pending_commit.is_some() || text_input_data.pending_preedit.is_none() @@ -149,9 +179,6 @@ impl Dispatch for TextInputState { ); } }, - TextInputEvent::DeleteSurroundingText { .. } => { - // Not handled. - }, _ => {}, } } @@ -219,6 +246,9 @@ pub struct TextInputDataInner { /// The preedit to submit on `done`. pending_preedit: Option, + + /// The text around the cursor to delete on `done` + pending_delete: Option, } /// The state of the preedit. @@ -229,6 +259,15 @@ struct Preedit { cursor_end: Option, } +/// The delete request +#[derive(Clone)] +struct DeleteSurroundingText { + /// Bytes before cursor + before: usize, + /// Bytes after cursor + after: usize, +} + /// State change requested by the application. /// /// This is a version that uses text_input abstractions translated from the ones used in diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index e255263d..2976a981 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -83,6 +83,7 @@ changelog entry. - Each platform now has corresponding `WindowAttributes` struct instead of trait extension. - On Wayland, added implementation for `Window::set_window_icon` - Add `Window::request_ime_update` to atomically apply set of IME changes. +- Add `Ime::DeleteSurrounding` to let the input method delete text. ### Changed