1#[path = "edit_boundary.rs"]
2mod boundary;
3#[path = "edit_word.rs"]
4mod word;
5
6use boundary::{clamp_boundary, next_boundary, previous_boundary};
7use core::ops::Range;
8use word::{
9 next_word_boundary, previous_word_boundary, previous_word_delete_boundary,
10 surrounding_word_bounds,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct DisplayText {
15 pub text: String,
16 pub is_placeholder: bool,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20pub struct PreeditState {
21 pub text: String,
22 pub cursor: Option<(usize, usize)>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum SelectionDirection {
27 #[default]
28 None,
29 Forward,
30 Backward,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct TextEditState {
35 committed: String,
36 focus: usize,
37 anchor: usize,
38 direction: SelectionDirection,
39 preedit: Option<PreeditState>,
40}
41
42impl Default for TextEditState {
43 fn default() -> Self {
44 Self {
45 committed: String::new(),
46 focus: 0,
47 anchor: 0,
48 direction: SelectionDirection::None,
49 preedit: None,
50 }
51 }
52}
53
54impl TextEditState {
55 pub fn with_text(text: impl Into<String>) -> Self {
56 let committed = text.into();
57 let len = committed.len();
58 Self {
59 committed,
60 focus: len,
61 anchor: len,
62 direction: SelectionDirection::None,
63 preedit: None,
64 }
65 }
66
67 pub fn committed(&self) -> &str {
68 &self.committed
69 }
70
71 pub fn caret(&self) -> usize {
72 self.focus
73 }
74
75 pub fn selection_anchor(&self) -> Option<usize> {
76 self.has_selection().then_some(self.anchor)
77 }
78
79 pub fn selection_direction(&self) -> SelectionDirection {
80 self.direction
81 }
82
83 pub fn preedit(&self) -> Option<&PreeditState> {
84 self.preedit.as_ref()
85 }
86
87 pub fn has_selection(&self) -> bool {
88 self.anchor != self.focus
89 }
90
91 pub fn selection_range(&self) -> Option<Range<usize>> {
92 self.has_selection().then(|| {
93 let start = self.anchor.min(self.focus);
94 let end = self.anchor.max(self.focus);
95 start..end
96 })
97 }
98
99 pub fn clear_selection(&mut self) {
100 self.anchor = self.focus;
101 self.direction = SelectionDirection::None;
102 }
103
104 pub fn select_all(&mut self) -> bool {
105 self.preedit = None;
106 if self.committed.is_empty() {
107 return false;
108 }
109 let changed = self.selection_range() != Some(0..self.committed.len());
110 self.anchor = 0;
111 self.focus = self.committed.len();
112 self.direction = SelectionDirection::Forward;
113 changed
114 }
115
116 pub fn set_text(&mut self, text: impl Into<String>) {
117 self.committed = text.into();
118 self.focus = self.committed.len();
119 self.anchor = self.focus;
120 self.direction = SelectionDirection::None;
121 self.preedit = None;
122 }
123
124 pub fn set_caret(&mut self, caret: usize, extend_selection: bool) {
125 let caret = clamp_boundary(&self.committed, caret);
126 if extend_selection {
127 if self.direction == SelectionDirection::None {
128 self.anchor = self.focus;
129 }
130 self.focus = caret;
131 self.direction = if caret >= self.anchor {
132 SelectionDirection::Forward
133 } else {
134 SelectionDirection::Backward
135 };
136 } else {
137 self.focus = caret;
138 self.anchor = caret;
139 self.direction = SelectionDirection::None;
140 }
141 }
142
143 pub fn collapse_selection_to_start(&mut self) -> bool {
144 let Some(range) = self.selection_range() else {
145 return false;
146 };
147 self.focus = range.start;
148 self.anchor = self.focus;
149 self.direction = SelectionDirection::None;
150 true
151 }
152
153 pub fn collapse_selection_to_end(&mut self) -> bool {
154 let Some(range) = self.selection_range() else {
155 return false;
156 };
157 self.focus = range.end;
158 self.anchor = self.focus;
159 self.direction = SelectionDirection::None;
160 true
161 }
162
163 pub fn move_left(&mut self, extend_selection: bool) -> bool {
164 if !extend_selection && self.collapse_selection_to_start() {
165 self.preedit = None;
166 return true;
167 }
168 let Some(previous) = previous_boundary(&self.committed, self.focus) else {
169 return false;
170 };
171 self.set_caret(previous, extend_selection);
172 self.preedit = None;
173 true
174 }
175
176 pub fn move_right(&mut self, extend_selection: bool) -> bool {
177 if !extend_selection && self.collapse_selection_to_end() {
178 self.preedit = None;
179 return true;
180 }
181 let Some(next) = next_boundary(&self.committed, self.focus) else {
182 return false;
183 };
184 self.set_caret(next, extend_selection);
185 self.preedit = None;
186 true
187 }
188
189 pub fn move_home(&mut self, extend_selection: bool) -> bool {
190 if self.focus == 0 && (!extend_selection || (self.anchor == 0 && self.focus == 0)) {
191 return false;
192 }
193 self.set_caret(0, extend_selection);
194 self.preedit = None;
195 true
196 }
197
198 pub fn move_end(&mut self, extend_selection: bool) -> bool {
199 let end = self.committed.len();
200 if self.focus == end && (!extend_selection || (self.anchor == end && self.focus == end)) {
201 return false;
202 }
203 self.set_caret(end, extend_selection);
204 self.preedit = None;
205 true
206 }
207
208 pub fn move_word_left(&mut self, extend_selection: bool) -> bool {
209 if !extend_selection && self.collapse_selection_to_start() {
210 self.preedit = None;
211 return true;
212 }
213 let target = previous_word_boundary(&self.committed, self.focus);
214 if target == self.focus {
215 return false;
216 }
217 self.set_caret(target, extend_selection);
218 self.preedit = None;
219 true
220 }
221
222 pub fn move_word_right(&mut self, extend_selection: bool) -> bool {
223 if !extend_selection && self.collapse_selection_to_end() {
224 self.preedit = None;
225 return true;
226 }
227 let target = next_word_boundary(&self.committed, self.focus);
228 if target == self.focus {
229 return false;
230 }
231 self.set_caret(target, extend_selection);
232 self.preedit = None;
233 true
234 }
235
236 pub fn replace_selection(&mut self, text: &str) -> bool {
237 if let Some(range) = self.selection_range() {
238 self.committed.replace_range(range.clone(), text);
239 self.focus = range.start + text.len();
240 self.anchor = self.focus;
241 self.direction = SelectionDirection::None;
242 self.preedit = None;
243 return true;
244 }
245 false
246 }
247
248 pub fn insert_text(&mut self, text: &str) -> bool {
249 if text.is_empty() {
250 return false;
251 }
252 let preedit_was_active = self.preedit.is_some();
253 if !self.replace_selection(text) {
254 self.committed.insert_str(self.focus, text);
255 self.focus += text.len();
256 self.anchor = self.focus;
257 self.direction = SelectionDirection::None;
258 if !preedit_was_active {
259 self.preedit = None;
260 }
261 }
262 true
263 }
264
265 pub fn backspace(&mut self) -> bool {
266 self.preedit = None;
267 if self.replace_selection("") {
268 return true;
269 }
270 let Some(previous) = previous_boundary(&self.committed, self.focus) else {
271 return false;
272 };
273 self.committed.replace_range(previous..self.focus, "");
274 self.focus = previous;
275 self.anchor = self.focus;
276 self.direction = SelectionDirection::None;
277 true
278 }
279
280 pub fn delete_forward(&mut self) -> bool {
281 self.preedit = None;
282 if self.replace_selection("") {
283 return true;
284 }
285 let Some(next) = next_boundary(&self.committed, self.focus) else {
286 return false;
287 };
288 self.committed.replace_range(self.focus..next, "");
289 self.anchor = self.focus;
290 self.direction = SelectionDirection::None;
291 true
292 }
293
294 pub fn backspace_word(&mut self) -> bool {
295 self.preedit = None;
296 if self.replace_selection("") {
297 return true;
298 }
299 let previous = previous_word_delete_boundary(&self.committed, self.focus);
300 if previous == self.focus {
301 return false;
302 }
303 self.committed.replace_range(previous..self.focus, "");
304 self.focus = previous;
305 self.anchor = self.focus;
306 self.direction = SelectionDirection::None;
307 true
308 }
309
310 pub fn delete_word_forward(&mut self) -> bool {
311 self.preedit = None;
312 if self.replace_selection("") {
313 return true;
314 }
315 let next = next_word_boundary(&self.committed, self.focus);
316 if next == self.focus {
317 return false;
318 }
319 self.committed.replace_range(self.focus..next, "");
320 self.anchor = self.focus;
321 self.direction = SelectionDirection::None;
322 true
323 }
324
325 pub fn set_preedit(&mut self, text: impl Into<String>, cursor: Option<(usize, usize)>) {
326 let text = text.into();
327 self.anchor = self.focus;
328 self.direction = SelectionDirection::None;
329 if text.is_empty() && cursor.is_none() {
330 self.preedit = None;
331 } else {
332 let clamped_cursor =
333 cursor.map(|(start, end)| (start.min(text.len()), end.min(text.len())));
334 self.preedit = Some(PreeditState {
335 text,
336 cursor: clamped_cursor,
337 });
338 }
339 }
340
341 pub fn clear_preedit(&mut self) {
342 self.preedit = None;
343 }
344
345 pub fn commit_preedit_text(&mut self, text: &str) -> bool {
346 self.preedit = None;
347 self.insert_text(text)
348 }
349
350 pub fn normalize_text(&mut self, text: impl Into<String>) -> bool {
351 let text = text.into();
352 if self.committed == text && !self.has_selection() && self.preedit.is_none() {
353 return false;
354 }
355 self.set_text(text);
356 true
357 }
358
359 pub fn display_text<'a>(&'a self, placeholder: &'a str) -> (&'a str, bool) {
360 if self.committed.is_empty() {
361 (placeholder, true)
362 } else {
363 (&self.committed, false)
364 }
365 }
366
367 pub fn display_text_string(&self, placeholder: &str) -> DisplayText {
368 if let Some(preedit) = self.preedit.as_ref() {
369 let mut text = self.committed.clone();
370 text.insert_str(self.focus, &preedit.text);
371 return DisplayText {
372 text,
373 is_placeholder: false,
374 };
375 }
376 let (text, is_placeholder) = self.display_text(placeholder);
377 DisplayText {
378 text: text.to_string(),
379 is_placeholder,
380 }
381 }
382
383 pub fn display_caret_byte(&self) -> usize {
384 if let Some(preedit) = self.preedit.as_ref() {
385 return self.focus
386 + preedit
387 .cursor
388 .map(|(start, _)| start.min(preedit.text.len()))
389 .unwrap_or(preedit.text.len());
390 }
391 if self.committed.is_empty() {
392 0
393 } else {
394 self.focus
395 }
396 }
397
398 pub fn select_word_at(&mut self, byte: usize) -> bool {
399 if self.committed.is_empty() {
400 return false;
401 }
402 let caret = clamp_boundary(&self.committed, byte);
403 let (start, end) = surrounding_word_bounds(&self.committed, caret);
404 if start == end {
405 return false;
406 }
407 self.anchor = start;
408 self.focus = end;
409 self.direction = SelectionDirection::Forward;
410 self.preedit = None;
411 true
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn insert_and_backspace_follow_utf8_boundaries() {
421 let mut state = TextEditState::with_text("A中");
422 assert!(state.backspace());
423 assert_eq!(state.committed(), "A");
424 assert!(state.insert_text("文"));
425 assert_eq!(state.committed(), "A文");
426 }
427
428 #[test]
429 fn selection_replacement_updates_caret() {
430 let mut state = TextEditState::with_text("hello");
431 state.set_caret(1, false);
432 state.set_caret(4, true);
433 assert_eq!(state.selection_range(), Some(1..4));
434 assert_eq!(state.selection_direction(), SelectionDirection::Forward);
435 assert!(state.insert_text("i"));
436 assert_eq!(state.committed(), "hio");
437 assert_eq!(state.caret(), 2);
438 assert!(!state.has_selection());
439 }
440
441 #[test]
442 fn movement_collapses_selection_when_not_extending() {
443 let mut state = TextEditState::with_text("hello");
444 state.set_caret(1, false);
445 state.set_caret(4, true);
446 assert!(state.move_left(false));
447 assert_eq!(state.caret(), 1);
448 assert!(!state.has_selection());
449 assert!(state.move_right(false));
450 assert_eq!(state.caret(), 2);
451 }
452
453 #[test]
454 fn preedit_display_overrides_placeholder_and_committed() {
455 let mut state = TextEditState::default();
456 assert_eq!(state.display_text("hint"), ("hint", true));
457 state.set_text("abc");
458 assert_eq!(state.display_text("hint"), ("abc", false));
459 state.set_preedit("拼音", Some(("拼".len(), "拼".len())));
460 assert_eq!(
461 state.display_text_string("hint"),
462 DisplayText {
463 text: "abc拼音".to_string(),
464 is_placeholder: false
465 }
466 );
467 assert_eq!(state.display_caret_byte(), "abc拼".len());
468 assert!(state.commit_preedit_text("中文"));
469 assert_eq!(state.committed(), "abc中文");
470 assert_eq!(state.preedit(), None);
471 }
472
473 #[test]
474 fn select_all_selects_committed_text() {
475 let mut state = TextEditState::with_text("hello");
476
477 assert!(state.select_all());
478 assert_eq!(state.selection_range(), Some(0..5));
479 assert_eq!(state.selection_direction(), SelectionDirection::Forward);
480 assert!(!state.select_all());
481 }
482
483 #[test]
484 fn word_navigation_and_deletion_follow_word_boundaries() {
485 let mut state = TextEditState::with_text("alpha beta-gamma");
486 assert!(state.move_word_left(false));
487 assert_eq!(state.caret(), 11);
488 assert!(state.backspace_word());
489 assert_eq!(state.committed(), "alpha gamma");
490 assert_eq!(state.caret(), 6);
491 assert!(state.delete_word_forward());
492 assert_eq!(state.committed(), "alpha ");
493 }
494
495 #[test]
496 fn select_word_at_expands_to_surrounding_word() {
497 let mut state = TextEditState::with_text("hello world");
498 assert!(state.select_word_at(7));
499 assert_eq!(state.selection_range(), Some(6..11));
500 }
501
502 #[test]
503 fn selection_direction_tracks_forward_and_backward() {
504 let mut state = TextEditState::with_text("hello");
505 state.set_caret(1, false);
506 state.set_caret(4, true);
507 assert_eq!(state.selection_direction(), SelectionDirection::Forward);
508
509 let mut state = TextEditState::with_text("hello");
510 state.set_caret(4, false);
511 state.set_caret(1, true);
512 assert_eq!(state.selection_direction(), SelectionDirection::Backward);
513 assert_eq!(state.selection_range(), Some(1..4));
514 }
515
516 #[test]
517 fn backward_selection_collapses_to_start() {
518 let mut state = TextEditState::with_text("hello");
519 state.set_caret(4, false);
520 state.set_caret(1, true);
521 assert_eq!(state.selection_direction(), SelectionDirection::Backward);
522 assert!(state.move_left(false));
523 assert_eq!(state.caret(), 1);
524 assert!(!state.has_selection());
525 }
526
527 #[test]
528 fn grapheme_boundary_does_not_split_emoji() {
529 let mut state = TextEditState::with_text("a😀b");
530 assert_eq!(state.caret(), "a😀b".len());
531
532 assert!(state.move_left(false));
533 assert_eq!(state.caret(), "a😀".len());
534
535 assert!(state.backspace());
536 assert_eq!(state.committed(), "ab");
537 assert_eq!(state.caret(), "a".len());
538 }
539
540 #[test]
541 fn grapheme_boundary_handles_combining_marks() {
542 let mut state = TextEditState::with_text("cafe\u{0301}"); assert_eq!(state.caret(), 6);
544 assert!(state.move_left(false));
545 assert_eq!(state.caret(), 3); assert!(state.backspace());
547 assert_eq!(state.committed(), "cae\u{0301}");
548 assert_eq!(state.caret(), 2);
549 }
550
551 #[test]
552 fn preedit_cursor_is_clamped() {
553 let mut state = TextEditState::with_text("hello");
554 state.set_preedit("xy", Some((5, 5)));
555 assert_eq!(state.preedit().unwrap().cursor, Some((2, 2)));
556 state.set_preedit("xy", Some((0, 0)));
557 assert_eq!(state.preedit().unwrap().cursor, Some((0, 0)));
558 }
559
560 #[test]
561 fn next_boundary_at_last_grapheme_returns_text_end() {
562 let mut state = TextEditState::with_text("ab");
563 state.set_caret(1, false);
564 assert!(state.move_right(false));
565 assert_eq!(state.caret(), 2);
566 }
567
568 #[test]
569 fn delete_at_caret_before_last_grapheme_deletes_last_char() {
570 let mut state = TextEditState::with_text("abc");
571 state.set_caret(2, false);
572 assert!(state.delete_forward());
573 assert_eq!(state.committed(), "ab");
574 }
575}