Skip to main content

rgx/input/
editor.rs

1use unicode_width::UnicodeWidthStr;
2
3#[derive(Debug, Clone)]
4pub struct Editor {
5    content: String,
6    cursor: usize,
7    scroll_offset: usize,
8}
9
10impl Editor {
11    pub fn new() -> Self {
12        Self {
13            content: String::new(),
14            cursor: 0,
15            scroll_offset: 0,
16        }
17    }
18
19    pub fn with_content(content: String) -> Self {
20        let cursor = content.len();
21        Self {
22            content,
23            cursor,
24            scroll_offset: 0,
25        }
26    }
27
28    pub fn content(&self) -> &str {
29        &self.content
30    }
31
32    pub fn cursor(&self) -> usize {
33        self.cursor
34    }
35
36    pub fn scroll_offset(&self) -> usize {
37        self.scroll_offset
38    }
39
40    pub fn visual_cursor(&self) -> usize {
41        let before_cursor = &self.content[..self.cursor];
42        UnicodeWidthStr::width(before_cursor).saturating_sub(self.scroll_offset)
43    }
44
45    pub fn insert_char(&mut self, c: char) {
46        self.content.insert(self.cursor, c);
47        self.cursor += c.len_utf8();
48    }
49
50    pub fn delete_back(&mut self) {
51        if self.cursor > 0 {
52            let prev = self.prev_char_boundary();
53            self.content.drain(prev..self.cursor);
54            self.cursor = prev;
55        }
56    }
57
58    pub fn delete_forward(&mut self) {
59        if self.cursor < self.content.len() {
60            let next = self.next_char_boundary();
61            self.content.drain(self.cursor..next);
62        }
63    }
64
65    pub fn move_left(&mut self) {
66        if self.cursor > 0 {
67            self.cursor = self.prev_char_boundary();
68        }
69    }
70
71    pub fn move_right(&mut self) {
72        if self.cursor < self.content.len() {
73            self.cursor = self.next_char_boundary();
74        }
75    }
76
77    pub fn move_home(&mut self) {
78        self.cursor = 0;
79        self.scroll_offset = 0;
80    }
81
82    pub fn move_end(&mut self) {
83        self.cursor = self.content.len();
84    }
85
86    pub fn update_scroll(&mut self, visible_width: usize) {
87        let visual = UnicodeWidthStr::width(&self.content[..self.cursor]);
88        if visual < self.scroll_offset {
89            self.scroll_offset = visual;
90        } else if visual >= self.scroll_offset + visible_width {
91            self.scroll_offset = visual - visible_width + 1;
92        }
93    }
94
95    fn prev_char_boundary(&self) -> usize {
96        let mut pos = self.cursor - 1;
97        while !self.content.is_char_boundary(pos) {
98            pos -= 1;
99        }
100        pos
101    }
102
103    fn next_char_boundary(&self) -> usize {
104        let mut pos = self.cursor + 1;
105        while pos < self.content.len() && !self.content.is_char_boundary(pos) {
106            pos += 1;
107        }
108        pos
109    }
110}
111
112impl Default for Editor {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_insert_and_content() {
124        let mut editor = Editor::new();
125        editor.insert_char('h');
126        editor.insert_char('i');
127        assert_eq!(editor.content(), "hi");
128        assert_eq!(editor.cursor(), 2);
129    }
130
131    #[test]
132    fn test_delete_back() {
133        let mut editor = Editor::with_content("hello".to_string());
134        editor.delete_back();
135        assert_eq!(editor.content(), "hell");
136    }
137
138    #[test]
139    fn test_cursor_movement() {
140        let mut editor = Editor::with_content("hello".to_string());
141        editor.move_left();
142        assert_eq!(editor.cursor(), 4);
143        editor.move_home();
144        assert_eq!(editor.cursor(), 0);
145        editor.move_end();
146        assert_eq!(editor.cursor(), 5);
147    }
148}