Skip to main content

rsynko_manager/
text.rs

1use alux_ext::ext;
2use ambassador::delegatable_trait;
3
4/// Specifies storage for the text editor active on the current manager page.
5#[delegatable_trait]
6pub trait TextEditorStateAlg {
7    /// Observes active text and its UTF-8 byte cursor.
8    fn active_text_editor(&self) -> Option<(&str, usize)>;
9    /// Replaces active text and its UTF-8 byte cursor.
10    fn set_active_text_editor(&mut self, text: String, cursor: usize);
11}
12
13/// Derives UTF-8-safe text editing independently of a renderer.
14#[ext(name = TextCursorExt)]
15pub impl<This> This
16where
17    This: TextEditorStateAlg,
18{
19    /// Inserts text at the active cursor and advances past the insertion.
20    fn insert_text(&mut self, insertion: &str) {
21        let Some((text, cursor)) = self.active_text_editor() else {
22            return;
23        };
24        let mut edited = text.to_owned();
25        edited.insert_str(cursor, insertion);
26        self.set_active_text_editor(edited, cursor.saturating_add(insertion.len()));
27    }
28
29    /// Deletes the Unicode scalar immediately preceding the active cursor.
30    fn delete_before_cursor(&mut self) {
31        let Some((text, cursor)) = self.active_text_editor() else {
32            return;
33        };
34        let previous = previous_boundary(text, cursor);
35        let mut edited = text.to_owned();
36        edited.drain(previous..cursor);
37        self.set_active_text_editor(edited, previous);
38    }
39
40    /// Deletes the Unicode scalar at the active cursor.
41    fn delete_at_cursor(&mut self) {
42        let Some((text, cursor)) = self.active_text_editor() else {
43            return;
44        };
45        let next = next_boundary(text, cursor);
46        let mut edited = text.to_owned();
47        edited.drain(cursor..next);
48        self.set_active_text_editor(edited, cursor);
49    }
50
51    /// Moves the active cursor one Unicode scalar to the left.
52    fn move_cursor_left(&mut self) {
53        let Some((text, cursor)) = self.active_text_editor() else {
54            return;
55        };
56        self.set_active_text_editor(text.to_owned(), previous_boundary(text, cursor));
57    }
58
59    /// Moves the active cursor one Unicode scalar to the right.
60    fn move_cursor_right(&mut self) {
61        let Some((text, cursor)) = self.active_text_editor() else {
62            return;
63        };
64        self.set_active_text_editor(text.to_owned(), next_boundary(text, cursor));
65    }
66
67    /// Moves the active cursor to the beginning of its logical line.
68    fn move_cursor_home(&mut self) {
69        let Some((text, cursor)) = self.active_text_editor() else {
70            return;
71        };
72        let home = text[..cursor].rfind('\n').map_or(0, |index| index + 1);
73        self.set_active_text_editor(text.to_owned(), home);
74    }
75
76    /// Moves the active cursor to the end of its logical line.
77    fn move_cursor_end(&mut self) {
78        let Some((text, cursor)) = self.active_text_editor() else {
79            return;
80        };
81        let end = text[cursor..].find('\n').map_or(text.len(), |index| cursor + index);
82        self.set_active_text_editor(text.to_owned(), end);
83    }
84}
85
86fn previous_boundary(text: &str, cursor: usize) -> usize {
87    text[..cursor].char_indices().next_back().map_or(0, |(index, _)| index)
88}
89
90fn next_boundary(text: &str, cursor: usize) -> usize {
91    text[cursor..].chars().next().map_or(cursor, |character| cursor + character.len_utf8())
92}