ferroui_base/input/text_input/
text_input_method_edit.rs1use super::{TextInputMethodError, TextSelection};
2
3#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct TextInputMethodEdit {
12 pub range: TextSelection,
13 pub text: String,
14 pub selection: TextSelection,
15}
16
17impl TextInputMethodEdit {
18 pub fn new(range: TextSelection, text: impl Into<String>, selection: TextSelection) -> Self {
19 Self {
20 range,
21 text: text.into(),
22 selection,
23 }
24 }
25
26 pub fn apply_to(&self, current: &str) -> Result<String, TextInputMethodError> {
29 if self.range.start > self.range.end {
30 return Err(TextInputMethodError::InvalidRange);
31 }
32 let start = byte_offset(current, self.range.start)?;
33 let end = byte_offset(current, self.range.end)?;
34 let capacity = current
35 .len()
36 .checked_sub(end - start)
37 .and_then(|length| length.checked_add(self.text.len()))
38 .ok_or(TextInputMethodError::InvalidRange)?;
39 let mut result = String::with_capacity(capacity);
40 result.push_str(¤t[..start]);
41 result.push_str(&self.text);
42 result.push_str(¤t[end..]);
43 if result.encode_utf16().count() > i32::MAX as usize {
44 return Err(TextInputMethodError::InvalidRange);
45 }
46 validate_selection(&result, self.selection)?;
47 Ok(result)
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct TextInputMethodState {
55 pub text: String,
56 pub selection: TextSelection,
57}
58
59pub(crate) fn validate_selection(
60 text: &str,
61 selection: TextSelection,
62) -> Result<(), TextInputMethodError> {
63 byte_offset(text, selection.start)?;
64 byte_offset(text, selection.end)?;
65 Ok(())
66}
67
68fn byte_offset(text: &str, position: i32) -> Result<usize, TextInputMethodError> {
69 let position = usize::try_from(position).map_err(|_| TextInputMethodError::InvalidRange)?;
70 let mut units = 0;
71 for (byte, character) in text.char_indices() {
72 if units == position {
73 return Ok(byte);
74 }
75 units += character.len_utf16();
76 if units > position {
77 return Err(TextInputMethodError::InvalidRange);
78 }
79 }
80 (units == position)
81 .then_some(text.len())
82 .ok_or(TextInputMethodError::InvalidRange)
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn exact_edit_replaces_utf16_range_and_keeps_directed_result_selection() {
91 let edit =
92 TextInputMethodEdit::new(TextSelection::new(1, 3), "xy", TextSelection::new(3, 1));
93 assert_eq!(edit.apply_to("a😀b").unwrap(), "axyb");
94 }
95
96 #[test]
97 fn exact_edit_deletion_is_not_an_empty_input_no_op() {
98 let edit = TextInputMethodEdit::new(TextSelection::new(0, 4), "", TextSelection::new(0, 0));
99 assert_eq!(edit.apply_to("a😀b").unwrap(), "");
100 }
101
102 #[test]
103 fn exact_edit_rejects_surrogate_interiors_and_out_of_bounds_without_clamping() {
104 for range in [
105 TextSelection::new(-1, 0),
106 TextSelection::new(2, 3),
107 TextSelection::new(3, 2),
108 TextSelection::new(0, 5),
109 ] {
110 let edit = TextInputMethodEdit::new(range, "x", TextSelection::new(0, 0));
111 assert_eq!(
112 edit.apply_to("a😀b"),
113 Err(TextInputMethodError::InvalidRange)
114 );
115 }
116 let edit =
117 TextInputMethodEdit::new(TextSelection::new(0, 0), "😀", TextSelection::new(1, 1));
118 assert_eq!(edit.apply_to(""), Err(TextInputMethodError::InvalidRange));
119 }
120}