winit-core: add Ime::DeleteSurroundingText API

This completes the basic API required for e.g. Wayland.
This commit is contained in:
DorotaC 2025-08-02 12:17:27 +02:00 committed by GitHub
parent 120f21a010
commit d7fdfb1bca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 5 deletions

View file

@ -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, .. } => {

View file

@ -879,6 +879,8 @@ impl From<ModifiersState> 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

View file

@ -115,12 +115,42 @@ impl Dispatch<ZwpTextInputV3, TextInputData, WinitState> 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<ZwpTextInputV3, TextInputData, WinitState> for TextInputState {
);
}
},
TextInputEvent::DeleteSurroundingText { .. } => {
// Not handled.
},
_ => {},
}
}
@ -219,6 +246,9 @@ pub struct TextInputDataInner {
/// The preedit to submit on `done`.
pending_preedit: Option<Preedit>,
/// The text around the cursor to delete on `done`
pending_delete: Option<DeleteSurroundingText>,
}
/// The state of the preedit.
@ -229,6 +259,15 @@ struct Preedit {
cursor_end: Option<usize>,
}
/// 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

View file

@ -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