Skip to main content

beuvy_runtime/input/
edit.rs

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}"); // café with combining accent
543        assert_eq!(state.caret(), 6);
544        assert!(state.move_left(false));
545        assert_eq!(state.caret(), 3); // before "é" grapheme
546        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}