1use astrelis_core::math::Vec2;
31
32#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct TextCursor {
35 pub position: usize,
37 pub visual_x: f32,
39 pub line: usize,
41 pub column: usize,
43}
44
45impl TextCursor {
46 pub fn new() -> Self {
48 Self {
49 position: 0,
50 visual_x: 0.0,
51 line: 0,
52 column: 0,
53 }
54 }
55
56 pub fn at_position(position: usize) -> Self {
58 Self {
59 position,
60 visual_x: 0.0,
61 line: 0,
62 column: 0,
63 }
64 }
65}
66
67impl Default for TextCursor {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
75pub struct TextSelection {
76 pub start: usize,
78 pub end: usize,
80}
81
82impl TextSelection {
83 pub fn new(start: usize, end: usize) -> Self {
85 Self { start, end }
86 }
87
88 pub fn range(&self) -> (usize, usize) {
90 if self.start <= self.end {
91 (self.start, self.end)
92 } else {
93 (self.end, self.start)
94 }
95 }
96
97 pub fn len(&self) -> usize {
99 let (min, max) = self.range();
100 max - min
101 }
102
103 pub fn is_empty(&self) -> bool {
105 self.start == self.end
106 }
107
108 pub fn contains(&self, position: usize) -> bool {
110 let (min, max) = self.range();
111 position >= min && position < max
112 }
113}
114
115pub struct TextEditor {
117 text: String,
119 cursor: TextCursor,
121 selection: Option<TextSelection>,
123 history: Vec<String>,
125 history_position: usize,
127}
128
129impl TextEditor {
130 pub fn new(text: impl Into<String>) -> Self {
132 let text = text.into();
133 Self {
134 text: text.clone(),
135 cursor: TextCursor::new(),
136 selection: None,
137 history: vec![text],
138 history_position: 0,
139 }
140 }
141
142 pub fn text(&self) -> &str {
144 &self.text
145 }
146
147 pub fn cursor(&self) -> &TextCursor {
149 &self.cursor
150 }
151
152 pub fn selection(&self) -> Option<&TextSelection> {
154 self.selection.as_ref()
155 }
156
157 pub fn has_selection(&self) -> bool {
159 self.selection.as_ref().is_some_and(|sel| !sel.is_empty())
160 }
161
162 pub fn set_cursor(&mut self, position: usize) {
164 self.cursor.position = position.min(self.text.len());
165 self.clear_selection();
166 self.update_cursor_position();
167 }
168
169 pub fn move_cursor_start(&mut self) {
171 self.set_cursor(0);
172 }
173
174 pub fn move_cursor_end(&mut self) {
176 self.set_cursor(self.text.len());
177 }
178
179 pub fn move_cursor_left(&mut self) {
181 if self.cursor.position > 0 {
182 let mut pos = self.cursor.position - 1;
184 while pos > 0 && !self.text.is_char_boundary(pos) {
185 pos -= 1;
186 }
187 self.set_cursor(pos);
188 }
189 }
190
191 pub fn move_cursor_right(&mut self) {
193 if self.cursor.position < self.text.len() {
194 let mut pos = self.cursor.position + 1;
196 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
197 pos += 1;
198 }
199 self.set_cursor(pos);
200 }
201 }
202
203 pub fn select(&mut self, start: usize, end: usize) {
205 self.selection = Some(TextSelection::new(
206 start.min(self.text.len()),
207 end.min(self.text.len()),
208 ));
209 self.cursor.position = end.min(self.text.len());
210 }
211
212 pub fn select_all(&mut self) {
214 self.select(0, self.text.len());
215 }
216
217 pub fn clear_selection(&mut self) {
219 self.selection = None;
220 }
221
222 pub fn insert_char(&mut self, c: char) {
224 if self.has_selection() {
226 self.delete_selection();
227 }
228
229 self.text.insert(self.cursor.position, c);
231 self.cursor.position += c.len_utf8();
232 self.update_cursor_position();
233 self.push_history();
234 }
235
236 pub fn insert_str(&mut self, s: &str) {
238 if self.has_selection() {
240 self.delete_selection();
241 }
242
243 self.text.insert_str(self.cursor.position, s);
245 self.cursor.position += s.len();
246 self.update_cursor_position();
247 self.push_history();
248 }
249
250 pub fn delete_char(&mut self) {
252 if self.has_selection() {
253 self.delete_selection();
254 } else if self.cursor.position > 0 {
255 let mut pos = self.cursor.position - 1;
257 while pos > 0 && !self.text.is_char_boundary(pos) {
258 pos -= 1;
259 }
260
261 self.text.drain(pos..self.cursor.position);
262 self.cursor.position = pos;
263 self.update_cursor_position();
264 self.push_history();
265 }
266 }
267
268 pub fn delete_char_forward(&mut self) {
270 if self.has_selection() {
271 self.delete_selection();
272 } else if self.cursor.position < self.text.len() {
273 let mut pos = self.cursor.position + 1;
275 while pos < self.text.len() && !self.text.is_char_boundary(pos) {
276 pos += 1;
277 }
278
279 self.text.drain(self.cursor.position..pos);
280 self.update_cursor_position();
281 self.push_history();
282 }
283 }
284
285 pub fn delete_selection(&mut self) {
287 if let Some(sel) = self.selection {
288 let (start, end) = sel.range();
289 self.text.drain(start..end);
290 self.cursor.position = start;
291 self.clear_selection();
292 self.update_cursor_position();
293 self.push_history();
294 }
295 }
296
297 pub fn selected_text(&self) -> Option<&str> {
299 self.selection.as_ref().map(|sel| {
300 let (start, end) = sel.range();
301 &self.text[start..end]
302 })
303 }
304
305 pub fn replace_selection(&mut self, text: &str) {
307 if self.has_selection() {
308 self.delete_selection();
309 }
310 self.insert_str(text);
311 }
312
313 pub fn undo(&mut self) -> bool {
315 if self.history_position > 0 {
316 self.history_position -= 1;
317 self.text = self.history[self.history_position].clone();
318 self.cursor.position = self.cursor.position.min(self.text.len());
319 self.clear_selection();
320 self.update_cursor_position();
321 true
322 } else {
323 false
324 }
325 }
326
327 pub fn redo(&mut self) -> bool {
329 if self.history_position < self.history.len() - 1 {
330 self.history_position += 1;
331 self.text = self.history[self.history_position].clone();
332 self.cursor.position = self.cursor.position.min(self.text.len());
333 self.clear_selection();
334 self.update_cursor_position();
335 true
336 } else {
337 false
338 }
339 }
340
341 pub fn hit_test(&self, _pos: Vec2, _char_width: f32) -> usize {
346 self.cursor.position
352 }
353
354 pub fn selection_rects(
359 &self,
360 _line_height: f32,
361 _char_width: f32,
362 ) -> Vec<(f32, f32, f32, f32)> {
363 if let Some(sel) = self.selection {
364 if !sel.is_empty() {
365 let (start, end) = sel.range();
366 vec![(
369 start as f32 * _char_width,
370 0.0,
371 (end - start) as f32 * _char_width,
372 _line_height,
373 )]
374 } else {
375 vec![]
376 }
377 } else {
378 vec![]
379 }
380 }
381
382 fn update_cursor_position(&mut self) {
385 let text_before = &self.text[..self.cursor.position];
387 self.cursor.line = text_before.matches('\n').count();
388
389 if let Some(line_start) = text_before.rfind('\n') {
391 let line_text = &text_before[line_start + 1..];
392 self.cursor.column = line_text.chars().count();
393 } else {
394 self.cursor.column = text_before.chars().count();
395 }
396 }
397
398 fn push_history(&mut self) {
399 self.history.truncate(self.history_position + 1);
401
402 self.history.push(self.text.clone());
404 self.history_position = self.history.len() - 1;
405
406 const MAX_HISTORY: usize = 100;
408 if self.history.len() > MAX_HISTORY {
409 self.history.remove(0);
410 self.history_position -= 1;
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_text_cursor_default() {
421 let cursor = TextCursor::default();
422 assert_eq!(cursor.position, 0);
423 assert_eq!(cursor.line, 0);
424 assert_eq!(cursor.column, 0);
425 }
426
427 #[test]
428 fn test_text_selection_range() {
429 let sel = TextSelection::new(5, 10);
430 assert_eq!(sel.range(), (5, 10));
431 assert_eq!(sel.len(), 5);
432 assert!(!sel.is_empty());
433
434 let sel = TextSelection::new(10, 5);
436 assert_eq!(sel.range(), (5, 10));
437 assert_eq!(sel.len(), 5);
438 }
439
440 #[test]
441 fn test_text_selection_contains() {
442 let sel = TextSelection::new(5, 10);
443 assert!(sel.contains(5));
444 assert!(sel.contains(7));
445 assert!(!sel.contains(10)); assert!(!sel.contains(3));
447 }
448
449 #[test]
450 fn test_editor_new() {
451 let editor = TextEditor::new("Hello");
452 assert_eq!(editor.text(), "Hello");
453 assert_eq!(editor.cursor().position, 0);
454 assert!(!editor.has_selection());
455 }
456
457 #[test]
458 fn test_editor_insert_char() {
459 let mut editor = TextEditor::new("Hello");
460 editor.move_cursor_end();
461 editor.insert_char('!');
462 assert_eq!(editor.text(), "Hello!");
463 assert_eq!(editor.cursor().position, 6);
464 }
465
466 #[test]
467 fn test_editor_insert_str() {
468 let mut editor = TextEditor::new("Hello");
469 editor.move_cursor_end();
470 editor.insert_str(", World");
471 assert_eq!(editor.text(), "Hello, World");
472 }
473
474 #[test]
475 fn test_editor_delete_char() {
476 let mut editor = TextEditor::new("Hello");
477 editor.move_cursor_end();
478 editor.delete_char();
479 assert_eq!(editor.text(), "Hell");
480 assert_eq!(editor.cursor().position, 4);
481 }
482
483 #[test]
484 fn test_editor_delete_char_forward() {
485 let mut editor = TextEditor::new("Hello");
486 editor.set_cursor(0);
487 editor.delete_char_forward();
488 assert_eq!(editor.text(), "ello");
489 assert_eq!(editor.cursor().position, 0);
490 }
491
492 #[test]
493 fn test_editor_selection() {
494 let mut editor = TextEditor::new("Hello, World");
495 editor.select(0, 5);
496 assert!(editor.has_selection());
497 assert_eq!(editor.selected_text(), Some("Hello"));
498 }
499
500 #[test]
501 fn test_editor_delete_selection() {
502 let mut editor = TextEditor::new("Hello, World");
503 editor.select(0, 5);
504 editor.delete_selection();
505 assert_eq!(editor.text(), ", World");
506 assert!(!editor.has_selection());
507 }
508
509 #[test]
510 fn test_editor_replace_selection() {
511 let mut editor = TextEditor::new("Hello, World");
512 editor.select(7, 12);
513 editor.replace_selection("Universe");
514 assert_eq!(editor.text(), "Hello, Universe");
515 }
516
517 #[test]
518 fn test_editor_select_all() {
519 let mut editor = TextEditor::new("Hello");
520 editor.select_all();
521 assert_eq!(editor.selected_text(), Some("Hello"));
522 }
523
524 #[test]
525 fn test_editor_cursor_movement() {
526 let mut editor = TextEditor::new("Hello");
527
528 editor.move_cursor_end();
529 assert_eq!(editor.cursor().position, 5);
530
531 editor.move_cursor_left();
532 assert_eq!(editor.cursor().position, 4);
533
534 editor.move_cursor_right();
535 assert_eq!(editor.cursor().position, 5);
536
537 editor.move_cursor_start();
538 assert_eq!(editor.cursor().position, 0);
539 }
540
541 #[test]
542 fn test_editor_undo_redo() {
543 let mut editor = TextEditor::new("Hello");
544 editor.move_cursor_end();
545 editor.insert_char('!');
546 assert_eq!(editor.text(), "Hello!");
547
548 editor.undo();
549 assert_eq!(editor.text(), "Hello");
550
551 editor.redo();
552 assert_eq!(editor.text(), "Hello!");
553 }
554
555 #[test]
556 fn test_editor_utf8() {
557 let mut editor = TextEditor::new("Hello 世界");
558 editor.move_cursor_end();
559 editor.insert_char('!');
560 assert_eq!(editor.text(), "Hello 世界!");
561
562 editor.delete_char();
563 assert_eq!(editor.text(), "Hello 世界");
564
565 editor.delete_char();
566 assert_eq!(editor.text(), "Hello 世");
567 }
568
569 #[test]
570 fn test_cursor_position_multiline() {
571 let mut editor = TextEditor::new("Hello\nWorld");
572 editor.set_cursor(6); assert_eq!(editor.cursor().line, 1);
574 assert_eq!(editor.cursor().column, 0);
575
576 editor.move_cursor_end();
577 assert_eq!(editor.cursor().line, 1);
578 assert_eq!(editor.cursor().column, 5);
579 }
580}