Skip to main content

kanban_core/
input.rs

1pub struct InputState {
2    buffer: String,
3    cursor_byte_offset: usize,
4}
5
6impl InputState {
7    pub fn new() -> Self {
8        Self {
9            buffer: String::new(),
10            cursor_byte_offset: 0,
11        }
12    }
13
14    pub fn insert_char(&mut self, c: char) {
15        self.buffer.insert(self.cursor_byte_offset, c);
16        self.cursor_byte_offset += c.len_utf8();
17    }
18
19    pub fn backspace(&mut self) {
20        if self.cursor_byte_offset > 0 {
21            let prev = self.buffer[..self.cursor_byte_offset]
22                .chars()
23                .next_back()
24                .unwrap();
25            self.cursor_byte_offset -= prev.len_utf8();
26            self.buffer.remove(self.cursor_byte_offset);
27        }
28    }
29
30    pub fn delete(&mut self) {
31        if self.cursor_byte_offset < self.buffer.len() {
32            self.buffer.remove(self.cursor_byte_offset);
33        }
34    }
35
36    pub fn move_left(&mut self) {
37        if self.cursor_byte_offset > 0 {
38            let prev = self.buffer[..self.cursor_byte_offset]
39                .chars()
40                .next_back()
41                .unwrap();
42            self.cursor_byte_offset -= prev.len_utf8();
43        }
44    }
45
46    pub fn move_right(&mut self) {
47        if self.cursor_byte_offset < self.buffer.len() {
48            let next = self.buffer[self.cursor_byte_offset..]
49                .chars()
50                .next()
51                .unwrap();
52            self.cursor_byte_offset += next.len_utf8();
53        }
54    }
55
56    pub fn move_home(&mut self) {
57        self.cursor_byte_offset = 0;
58    }
59
60    pub fn move_end(&mut self) {
61        self.cursor_byte_offset = self.buffer.len();
62    }
63
64    pub fn clear(&mut self) {
65        self.buffer.clear();
66        self.cursor_byte_offset = 0;
67    }
68
69    pub fn set(&mut self, text: String) {
70        self.buffer = text;
71        self.cursor_byte_offset = self.buffer.len();
72    }
73
74    pub fn is_empty(&self) -> bool {
75        self.buffer.is_empty()
76    }
77
78    pub fn as_str(&self) -> &str {
79        &self.buffer
80    }
81
82    pub fn cursor_byte_offset(&self) -> usize {
83        self.cursor_byte_offset
84    }
85}
86
87impl Default for InputState {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_new_is_empty() {
99        let input = InputState::new();
100        assert!(input.is_empty());
101        assert_eq!(input.cursor_byte_offset(), 0);
102        assert_eq!(input.as_str(), "");
103    }
104
105    #[test]
106    fn test_insert_char_at_end() {
107        let mut input = InputState::new();
108        input.insert_char('a');
109        input.insert_char('b');
110        input.insert_char('c');
111        assert_eq!(input.as_str(), "abc");
112        assert_eq!(input.cursor_byte_offset(), 3);
113    }
114
115    #[test]
116    fn test_insert_char_at_beginning() {
117        let mut input = InputState::new();
118        input.insert_char('b');
119        input.move_home();
120        input.insert_char('a');
121        assert_eq!(input.as_str(), "ab");
122        assert_eq!(input.cursor_byte_offset(), 1);
123    }
124
125    #[test]
126    fn test_insert_char_at_middle() {
127        let mut input = InputState::new();
128        input.insert_char('a');
129        input.insert_char('c');
130        input.move_left();
131        input.insert_char('b');
132        assert_eq!(input.as_str(), "abc");
133        assert_eq!(input.cursor_byte_offset(), 2);
134    }
135
136    #[test]
137    fn test_backspace_at_start_is_noop() {
138        let mut input = InputState::new();
139        input.backspace();
140        assert_eq!(input.as_str(), "");
141        assert_eq!(input.cursor_byte_offset(), 0);
142
143        input.insert_char('a');
144        input.move_home();
145        input.backspace();
146        assert_eq!(input.as_str(), "a");
147        assert_eq!(input.cursor_byte_offset(), 0);
148    }
149
150    #[test]
151    fn test_backspace_removes_previous_char() {
152        let mut input = InputState::new();
153        input.insert_char('a');
154        input.insert_char('b');
155        input.insert_char('c');
156        input.move_left();
157        input.backspace();
158        assert_eq!(input.as_str(), "ac");
159        assert_eq!(input.cursor_byte_offset(), 1);
160    }
161
162    #[test]
163    fn test_backspace_at_end() {
164        let mut input = InputState::new();
165        input.insert_char('a');
166        input.insert_char('b');
167        input.backspace();
168        assert_eq!(input.as_str(), "a");
169        assert_eq!(input.cursor_byte_offset(), 1);
170    }
171
172    #[test]
173    fn test_delete_at_end_is_noop() {
174        let mut input = InputState::new();
175        input.delete();
176        assert_eq!(input.as_str(), "");
177
178        input.insert_char('a');
179        input.delete();
180        assert_eq!(input.as_str(), "a");
181        assert_eq!(input.cursor_byte_offset(), 1);
182    }
183
184    #[test]
185    fn test_delete_removes_char_at_cursor() {
186        let mut input = InputState::new();
187        input.insert_char('a');
188        input.insert_char('b');
189        input.insert_char('c');
190        input.move_home();
191        input.delete();
192        assert_eq!(input.as_str(), "bc");
193        assert_eq!(input.cursor_byte_offset(), 0);
194    }
195
196    #[test]
197    fn test_move_left_at_zero_is_noop() {
198        let input = InputState::new();
199        assert_eq!(input.cursor_byte_offset(), 0);
200
201        let mut input = InputState::new();
202        input.insert_char('a');
203        input.move_home();
204        input.move_left();
205        assert_eq!(input.cursor_byte_offset(), 0);
206    }
207
208    #[test]
209    fn test_move_left() {
210        let mut input = InputState::new();
211        input.insert_char('a');
212        input.insert_char('b');
213        input.move_left();
214        assert_eq!(input.cursor_byte_offset(), 1);
215        input.move_left();
216        assert_eq!(input.cursor_byte_offset(), 0);
217    }
218
219    #[test]
220    fn test_move_right_at_end_is_noop() {
221        let mut input = InputState::new();
222        input.move_right();
223        assert_eq!(input.cursor_byte_offset(), 0);
224
225        input.insert_char('a');
226        input.move_right();
227        assert_eq!(input.cursor_byte_offset(), 1);
228    }
229
230    #[test]
231    fn test_move_right() {
232        let mut input = InputState::new();
233        input.insert_char('a');
234        input.insert_char('b');
235        input.move_home();
236        input.move_right();
237        assert_eq!(input.cursor_byte_offset(), 1);
238        input.move_right();
239        assert_eq!(input.cursor_byte_offset(), 2);
240    }
241
242    #[test]
243    fn test_move_home() {
244        let mut input = InputState::new();
245        input.insert_char('a');
246        input.insert_char('b');
247        input.insert_char('c');
248        assert_eq!(input.cursor_byte_offset(), 3);
249        input.move_home();
250        assert_eq!(input.cursor_byte_offset(), 0);
251    }
252
253    #[test]
254    fn test_move_end() {
255        let mut input = InputState::new();
256        input.insert_char('a');
257        input.insert_char('b');
258        input.move_home();
259        assert_eq!(input.cursor_byte_offset(), 0);
260        input.move_end();
261        assert_eq!(input.cursor_byte_offset(), 2);
262    }
263
264    #[test]
265    fn test_clear() {
266        let mut input = InputState::new();
267        input.insert_char('a');
268        input.insert_char('b');
269        input.clear();
270        assert!(input.is_empty());
271        assert_eq!(input.cursor_byte_offset(), 0);
272        assert_eq!(input.as_str(), "");
273    }
274
275    #[test]
276    fn test_set() {
277        let mut input = InputState::new();
278        input.set("hello".to_string());
279        assert_eq!(input.as_str(), "hello");
280        assert_eq!(input.cursor_byte_offset(), 5);
281    }
282
283    #[test]
284    fn test_is_empty() {
285        let mut input = InputState::new();
286        assert!(input.is_empty());
287        input.insert_char('x');
288        assert!(!input.is_empty());
289        input.backspace();
290        assert!(input.is_empty());
291    }
292
293    #[test]
294    fn test_as_str() {
295        let mut input = InputState::new();
296        assert_eq!(input.as_str(), "");
297        input.set("test".to_string());
298        assert_eq!(input.as_str(), "test");
299    }
300
301    // Multi-byte character tests
302
303    #[test]
304    fn test_insert_multibyte_char() {
305        let mut input = InputState::new();
306        input.insert_char('a');
307        input.insert_char('\u{00e9}'); // e-acute, 2 bytes
308        input.insert_char('b');
309        assert_eq!(input.as_str(), "a\u{00e9}b");
310        assert_eq!(input.cursor_byte_offset(), 4); // 1 + 2 + 1
311
312        let mut input = InputState::new();
313        input.insert_char('\u{4e16}'); // CJK character, 3 bytes
314        assert_eq!(input.cursor_byte_offset(), 3);
315
316        let mut input = InputState::new();
317        input.insert_char('\u{1f600}'); // emoji, 4 bytes
318        assert_eq!(input.cursor_byte_offset(), 4);
319    }
320
321    #[test]
322    fn test_backspace_multibyte() {
323        let mut input = InputState::new();
324        input.insert_char('a');
325        input.insert_char('\u{00e9}');
326        input.insert_char('b');
327        input.backspace();
328        assert_eq!(input.as_str(), "a\u{00e9}");
329        assert_eq!(input.cursor_byte_offset(), 3);
330        input.backspace();
331        assert_eq!(input.as_str(), "a");
332        assert_eq!(input.cursor_byte_offset(), 1);
333    }
334
335    #[test]
336    fn test_delete_multibyte() {
337        let mut input = InputState::new();
338        input.insert_char('a');
339        input.insert_char('\u{00e9}');
340        input.insert_char('b');
341        input.move_home();
342        input.move_right(); // past 'a'
343        input.delete(); // delete e-acute
344        assert_eq!(input.as_str(), "ab");
345        assert_eq!(input.cursor_byte_offset(), 1);
346    }
347
348    #[test]
349    fn test_move_left_multibyte() {
350        let mut input = InputState::new();
351        input.insert_char('a');
352        input.insert_char('\u{00e9}'); // 2 bytes
353        input.insert_char('b');
354        // cursor at 4 (end)
355        input.move_left(); // back over 'b' (1 byte)
356        assert_eq!(input.cursor_byte_offset(), 3);
357        input.move_left(); // back over e-acute (2 bytes)
358        assert_eq!(input.cursor_byte_offset(), 1);
359        input.move_left(); // back over 'a' (1 byte)
360        assert_eq!(input.cursor_byte_offset(), 0);
361    }
362
363    #[test]
364    fn test_move_right_multibyte() {
365        let mut input = InputState::new();
366        input.insert_char('a');
367        input.insert_char('\u{00e9}'); // 2 bytes
368        input.insert_char('b');
369        input.move_home();
370        input.move_right(); // past 'a'
371        assert_eq!(input.cursor_byte_offset(), 1);
372        input.move_right(); // past e-acute (2 bytes)
373        assert_eq!(input.cursor_byte_offset(), 3);
374        input.move_right(); // past 'b'
375        assert_eq!(input.cursor_byte_offset(), 4);
376    }
377
378    #[test]
379    fn test_mixed_ascii_and_multibyte() {
380        let mut input = InputState::new();
381        // Build: "h\u{00e9}llo\u{1f600}"
382        input.insert_char('h');
383        input.insert_char('l');
384        input.insert_char('l');
385        input.insert_char('o');
386        // Insert e-acute after 'h': move to position 1
387        input.move_home();
388        input.move_right();
389        input.insert_char('\u{00e9}');
390        assert_eq!(input.as_str(), "h\u{00e9}llo");
391        // Append emoji
392        input.move_end();
393        input.insert_char('\u{1f600}');
394        assert_eq!(input.as_str(), "h\u{00e9}llo\u{1f600}");
395        // Navigate back and delete the e-acute
396        input.move_home();
397        input.move_right(); // past 'h'
398        input.delete(); // remove e-acute
399        assert_eq!(input.as_str(), "hllo\u{1f600}");
400        // Backspace the emoji from the end
401        input.move_end();
402        input.backspace();
403        assert_eq!(input.as_str(), "hllo");
404    }
405}