1use crate::color::Color;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum UndoActionKind {
6 InsertChar,
8 Paste,
10 Backspace,
12 Delete,
14 DeleteWord,
16 Cut,
18 Other,
20}
21
22#[derive(Debug, Clone)]
24pub struct UndoEntry {
25 pub text: String,
26 pub cursor_pos: usize,
27 pub selection_anchor: Option<usize>,
28 pub action_kind: UndoActionKind,
29}
30
31const MAX_UNDO_STACK: usize = 200;
33
34#[derive(Debug, Clone)]
37pub struct TextEditState {
38 pub text: String,
40 pub cursor_pos: usize,
42 pub selection_anchor: Option<usize>,
44 pub scroll_offset: f32,
46 pub scroll_offset_y: f32,
48 pub cursor_blink_timer: f64,
50 pub last_click_time: f64,
52 pub last_click_element: u32,
54 pub preferred_col: Option<usize>,
57 pub no_styles_movement: bool,
60 pub undo_stack: Vec<UndoEntry>,
62 pub redo_stack: Vec<UndoEntry>,
64}
65
66impl Default for TextEditState {
67 fn default() -> Self {
68 Self {
69 text: String::new(),
70 cursor_pos: 0,
71 selection_anchor: None,
72 scroll_offset: 0.0,
73 scroll_offset_y: 0.0,
74 preferred_col: None,
75 no_styles_movement: false,
76 cursor_blink_timer: 0.0,
77 last_click_time: 0.0,
78 last_click_element: 0,
79 undo_stack: Vec::new(),
80 redo_stack: Vec::new(),
81 }
82 }
83}
84
85impl TextEditState {
86 pub fn selection_range(&self) -> Option<(usize, usize)> {
88 self.selection_anchor.map(|anchor| {
89 let start = anchor.min(self.cursor_pos);
90 let end = anchor.max(self.cursor_pos);
91 (start, end)
92 })
93 }
94
95 pub fn selected_text(&self) -> &str {
97 if let Some((start, end)) = self.selection_range() {
98 let byte_start = char_index_to_byte(&self.text, start);
99 let byte_end = char_index_to_byte(&self.text, end);
100 &self.text[byte_start..byte_end]
101 } else {
102 ""
103 }
104 }
105
106 pub fn delete_selection(&mut self) -> bool {
109 if let Some((start, end)) = self.selection_range() {
110 let byte_start = char_index_to_byte(&self.text, start);
111 let byte_end = char_index_to_byte(&self.text, end);
112 self.text.drain(byte_start..byte_end);
113 self.cursor_pos = start;
114 self.selection_anchor = None;
115 true
116 } else {
117 false
118 }
119 }
120
121 pub fn insert_text(&mut self, s: &str, max_length: Option<usize>) {
124 self.delete_selection();
125 let char_count = self.text.chars().count();
126 let insert_count = s.chars().count();
127 let allowed = if let Some(max) = max_length {
128 if char_count >= max {
129 0
130 } else {
131 insert_count.min(max - char_count)
132 }
133 } else {
134 insert_count
135 };
136 if allowed == 0 {
137 return;
138 }
139 let insert_str: String = s.chars().take(allowed).collect();
140 let byte_pos = char_index_to_byte(&self.text, self.cursor_pos);
141 self.text.insert_str(byte_pos, &insert_str);
142 self.cursor_pos += allowed;
143 self.reset_blink();
144 }
145
146 pub fn move_left(&mut self, shift: bool) {
148 if !shift {
149 if let Some((start, _end)) = self.selection_range() {
151 self.cursor_pos = start;
152 self.selection_anchor = None;
153 self.reset_blink();
154 return;
155 }
156 }
157 if self.cursor_pos > 0 {
158 if shift && self.selection_anchor.is_none() {
159 self.selection_anchor = Some(self.cursor_pos);
160 }
161 self.cursor_pos -= 1;
162 if shift {
163 if self.selection_anchor == Some(self.cursor_pos) {
165 self.selection_anchor = None;
166 }
167 }
168 }
169 if !shift {
170 self.selection_anchor = None;
171 }
172 self.reset_blink();
173 }
174
175 pub fn move_right(&mut self, shift: bool) {
177 let len = self.text.chars().count();
178 if !shift {
179 if let Some((_start, end)) = self.selection_range() {
181 self.cursor_pos = end;
182 self.selection_anchor = None;
183 self.reset_blink();
184 return;
185 }
186 }
187 if self.cursor_pos < len {
188 if shift && self.selection_anchor.is_none() {
189 self.selection_anchor = Some(self.cursor_pos);
190 }
191 self.cursor_pos += 1;
192 if shift {
193 if self.selection_anchor == Some(self.cursor_pos) {
194 self.selection_anchor = None;
195 }
196 }
197 }
198 if !shift {
199 self.selection_anchor = None;
200 }
201 self.reset_blink();
202 }
203
204 pub fn move_word_left(&mut self, shift: bool) {
206 if shift && self.selection_anchor.is_none() {
207 self.selection_anchor = Some(self.cursor_pos);
208 }
209 self.cursor_pos = find_word_boundary_left(&self.text, self.cursor_pos);
210 if !shift {
211 self.selection_anchor = None;
212 } else if self.selection_anchor == Some(self.cursor_pos) {
213 self.selection_anchor = None;
214 }
215 self.reset_blink();
216 }
217
218 pub fn move_word_right(&mut self, shift: bool) {
220 if shift && self.selection_anchor.is_none() {
221 self.selection_anchor = Some(self.cursor_pos);
222 }
223 self.cursor_pos = find_word_boundary_right(&self.text, self.cursor_pos);
224 if !shift {
225 self.selection_anchor = None;
226 } else if self.selection_anchor == Some(self.cursor_pos) {
227 self.selection_anchor = None;
228 }
229 self.reset_blink();
230 }
231
232 pub fn move_home(&mut self, shift: bool) {
234 if shift && self.selection_anchor.is_none() {
235 self.selection_anchor = Some(self.cursor_pos);
236 }
237 self.cursor_pos = 0;
238 if !shift {
239 self.selection_anchor = None;
240 } else if self.selection_anchor == Some(0) {
241 self.selection_anchor = None;
242 }
243 self.reset_blink();
244 }
245
246 pub fn move_end(&mut self, shift: bool) {
248 let len = self.text.chars().count();
249 if shift && self.selection_anchor.is_none() {
250 self.selection_anchor = Some(self.cursor_pos);
251 }
252 self.cursor_pos = len;
253 if !shift {
254 self.selection_anchor = None;
255 } else if self.selection_anchor == Some(len) {
256 self.selection_anchor = None;
257 }
258 self.reset_blink();
259 }
260
261 pub fn select_all(&mut self) {
263 let len = self.text.chars().count();
264 if len > 0 {
265 self.selection_anchor = Some(0);
266 self.cursor_pos = len;
267 }
268 self.reset_blink();
269 }
270
271 pub fn backspace(&mut self) {
273 if self.delete_selection() {
274 return;
275 }
276 if self.cursor_pos > 0 {
277 self.cursor_pos -= 1;
278 let byte_pos = char_index_to_byte(&self.text, self.cursor_pos);
279 let next_byte = char_index_to_byte(&self.text, self.cursor_pos + 1);
280 self.text.drain(byte_pos..next_byte);
281 }
282 self.reset_blink();
283 }
284
285 pub fn delete_forward(&mut self) {
287 if self.delete_selection() {
288 return;
289 }
290 let len = self.text.chars().count();
291 if self.cursor_pos < len {
292 let byte_pos = char_index_to_byte(&self.text, self.cursor_pos);
293 let next_byte = char_index_to_byte(&self.text, self.cursor_pos + 1);
294 self.text.drain(byte_pos..next_byte);
295 }
296 self.reset_blink();
297 }
298
299 pub fn backspace_word(&mut self) {
301 if self.delete_selection() {
302 return;
303 }
304 let target = find_word_boundary_left(&self.text, self.cursor_pos);
305 let byte_start = char_index_to_byte(&self.text, target);
306 let byte_end = char_index_to_byte(&self.text, self.cursor_pos);
307 self.text.drain(byte_start..byte_end);
308 self.cursor_pos = target;
309 self.reset_blink();
310 }
311
312 pub fn delete_word_forward(&mut self) {
314 if self.delete_selection() {
315 return;
316 }
317 let target = find_word_delete_boundary_right(&self.text, self.cursor_pos);
318 let byte_start = char_index_to_byte(&self.text, self.cursor_pos);
319 let byte_end = char_index_to_byte(&self.text, target);
320 self.text.drain(byte_start..byte_end);
321 self.reset_blink();
322 }
323
324 pub fn click_to_cursor(&mut self, click_x: f32, char_x_positions: &[f32], shift: bool) {
328 let new_pos = find_nearest_char_boundary(click_x, char_x_positions);
329 if shift {
330 if self.selection_anchor.is_none() {
331 self.selection_anchor = Some(self.cursor_pos);
332 }
333 } else {
334 self.selection_anchor = None;
335 }
336 self.cursor_pos = new_pos;
337 if shift {
338 if self.selection_anchor == Some(self.cursor_pos) {
339 self.selection_anchor = None;
340 }
341 }
342 self.reset_blink();
343 }
344
345 pub fn select_word_at(&mut self, char_pos: usize) {
347 let (start, end) = find_word_at(&self.text, char_pos);
348 if start != end {
349 self.selection_anchor = Some(start);
350 self.cursor_pos = end;
351 }
352 self.reset_blink();
353 }
354
355 pub fn reset_blink(&mut self) {
357 self.cursor_blink_timer = 0.0;
358 }
359
360 pub fn cursor_visible(&self) -> bool {
362 (self.cursor_blink_timer % 1.06) < 0.53
363 }
364
365 pub fn ensure_cursor_visible(&mut self, cursor_x: f32, visible_width: f32) {
368 if cursor_x - self.scroll_offset > visible_width {
369 self.scroll_offset = cursor_x - visible_width;
370 }
371 if cursor_x - self.scroll_offset < 0.0 {
372 self.scroll_offset = cursor_x;
373 }
374 if self.scroll_offset < 0.0 {
376 self.scroll_offset = 0.0;
377 }
378 }
379
380 pub fn ensure_cursor_visible_vertical(&mut self, cursor_line: usize, line_height: f32, visible_height: f32) {
384 let cursor_y = cursor_line as f32 * line_height;
385 let cursor_bottom = cursor_y + line_height;
386 if cursor_bottom - self.scroll_offset_y > visible_height {
387 self.scroll_offset_y = cursor_bottom - visible_height;
388 }
389 if cursor_y - self.scroll_offset_y < 0.0 {
390 self.scroll_offset_y = cursor_y;
391 }
392 if self.scroll_offset_y < 0.0 {
393 self.scroll_offset_y = 0.0;
394 }
395 }
396
397 pub fn push_undo(&mut self, kind: UndoActionKind) {
401 let should_group = matches!(kind, UndoActionKind::InsertChar | UndoActionKind::Backspace | UndoActionKind::Delete);
404 if should_group {
405 if let Some(last) = self.undo_stack.last() {
406 if last.action_kind == kind {
407 self.redo_stack.clear();
410 return;
411 }
412 }
413 }
414
415 self.undo_stack.push(UndoEntry {
416 text: self.text.clone(),
417 cursor_pos: self.cursor_pos,
418 selection_anchor: self.selection_anchor,
419 action_kind: kind,
420 });
421 if self.undo_stack.len() > MAX_UNDO_STACK {
423 self.undo_stack.remove(0);
424 }
425 self.redo_stack.clear();
427 }
428
429 pub fn undo(&mut self) -> bool {
431 if let Some(entry) = self.undo_stack.pop() {
432 self.redo_stack.push(UndoEntry {
434 text: self.text.clone(),
435 cursor_pos: self.cursor_pos,
436 selection_anchor: self.selection_anchor,
437 action_kind: entry.action_kind,
438 });
439 self.text = entry.text;
441 self.cursor_pos = entry.cursor_pos;
442 self.selection_anchor = entry.selection_anchor;
443 self.reset_blink();
444 true
445 } else {
446 false
447 }
448 }
449
450 pub fn redo(&mut self) -> bool {
452 if let Some(entry) = self.redo_stack.pop() {
453 self.undo_stack.push(UndoEntry {
455 text: self.text.clone(),
456 cursor_pos: self.cursor_pos,
457 selection_anchor: self.selection_anchor,
458 action_kind: entry.action_kind,
459 });
460 self.text = entry.text;
462 self.cursor_pos = entry.cursor_pos;
463 self.selection_anchor = entry.selection_anchor;
464 self.reset_blink();
465 true
466 } else {
467 false
468 }
469 }
470
471 pub fn move_line_home(&mut self, shift: bool) {
473 if shift && self.selection_anchor.is_none() {
474 self.selection_anchor = Some(self.cursor_pos);
475 }
476 let target = line_start_char_pos(&self.text, self.cursor_pos);
477 self.cursor_pos = target;
478 if !shift {
479 self.selection_anchor = None;
480 } else if self.selection_anchor == Some(self.cursor_pos) {
481 self.selection_anchor = None;
482 }
483 self.reset_blink();
484 }
485
486 pub fn move_line_end(&mut self, shift: bool) {
488 if shift && self.selection_anchor.is_none() {
489 self.selection_anchor = Some(self.cursor_pos);
490 }
491 let target = line_end_char_pos(&self.text, self.cursor_pos);
492 self.cursor_pos = target;
493 if !shift {
494 self.selection_anchor = None;
495 } else if self.selection_anchor == Some(self.cursor_pos) {
496 self.selection_anchor = None;
497 }
498 self.reset_blink();
499 }
500
501 pub fn move_up(&mut self, shift: bool) {
503 let (line, col) = line_and_column(&self.text, self.cursor_pos);
504 if line == 0 {
505 if shift && self.selection_anchor.is_none() {
507 self.selection_anchor = Some(self.cursor_pos);
508 }
509 self.cursor_pos = 0;
510 if !shift {
511 self.selection_anchor = None;
512 } else if self.selection_anchor == Some(self.cursor_pos) {
513 self.selection_anchor = None;
514 }
515 self.reset_blink();
516 return;
517 }
518 if shift && self.selection_anchor.is_none() {
519 self.selection_anchor = Some(self.cursor_pos);
520 }
521 self.cursor_pos = char_pos_from_line_col(&self.text, line - 1, col);
522 if !shift {
523 self.selection_anchor = None;
524 } else if self.selection_anchor == Some(self.cursor_pos) {
525 self.selection_anchor = None;
526 }
527 self.reset_blink();
528 }
529
530 pub fn move_down(&mut self, shift: bool) {
532 let (line, col) = line_and_column(&self.text, self.cursor_pos);
533 let line_count = self.text.chars().filter(|&c| c == '\n').count() + 1;
534 if line >= line_count - 1 {
535 if shift && self.selection_anchor.is_none() {
537 self.selection_anchor = Some(self.cursor_pos);
538 }
539 self.cursor_pos = self.text.chars().count();
540 if !shift {
541 self.selection_anchor = None;
542 } else if self.selection_anchor == Some(self.cursor_pos) {
543 self.selection_anchor = None;
544 }
545 self.reset_blink();
546 return;
547 }
548 if shift && self.selection_anchor.is_none() {
549 self.selection_anchor = Some(self.cursor_pos);
550 }
551 self.cursor_pos = char_pos_from_line_col(&self.text, line + 1, col);
552 if !shift {
553 self.selection_anchor = None;
554 } else if self.selection_anchor == Some(self.cursor_pos) {
555 self.selection_anchor = None;
556 }
557 self.reset_blink();
558 }
559}
560
561#[cfg(feature = "text-styling")]
565impl TextEditState {
566 fn cursor_len_styled(&self) -> usize {
568 styling::cursor_len(&self.text)
569 }
570
571 pub fn selected_text_styled(&self) -> String {
573 if let Some((start, end)) = self.selection_range() {
574 let stripped = styling::strip_styling(&self.text);
575 let byte_start = char_index_to_byte(&stripped, start);
576 let byte_end = char_index_to_byte(&stripped, end);
577 stripped[byte_start..byte_end].to_string()
578 } else {
579 String::new()
580 }
581 }
582
583 pub fn delete_selection_styled(&mut self) -> bool {
585 if let Some((start, end)) = self.selection_range() {
586 if self.no_styles_movement {
587 let start_cp = styling::cursor_to_content(&self.text, start);
588 let end_cp = styling::cursor_to_content(&self.text, end);
589 if start_cp < end_cp {
590 self.text = styling::delete_content_range(&self.text, start_cp, end_cp);
591 }
592 self.cursor_pos = styling::content_to_cursor(&self.text, start_cp, true);
593 } else {
594 self.text = styling::delete_visual_range(&self.text, start, end);
595 self.cursor_pos = start;
596 }
597 self.selection_anchor = None;
598 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
600 self.text = cleaned;
601 self.cursor_pos = new_pos;
602 self.snap_to_content_pos();
603 if styling::strip_styling(&self.text).is_empty() {
605 self.text = String::new();
606 self.cursor_pos = 0;
607 }
608 true
609 } else {
610 false
611 }
612 }
613
614 pub fn insert_text_styled(&mut self, s: &str, max_length: Option<usize>) {
617 self.delete_selection_styled();
618 let visual_count = self.cursor_len_styled();
619 let insert_cursor_len = styling::cursor_len(s);
620 let allowed = if let Some(max) = max_length {
621 if visual_count >= max {
622 0
623 } else {
624 insert_cursor_len.min(max - visual_count)
625 }
626 } else {
627 insert_cursor_len
628 };
629 if allowed == 0 {
630 return;
631 }
632 let insert_str = if allowed < insert_cursor_len {
634 let stripped = styling::strip_styling(s);
636 let truncated: String = stripped.chars().take(allowed).collect();
637 styling::escape_str(&truncated)
638 } else {
639 s.to_string()
640 };
641 let (new_text, new_cursor) = styling::insert_at_visual(&self.text, self.cursor_pos, &insert_str);
642 self.text = new_text;
643 self.cursor_pos = new_cursor;
644 self.cleanup_after_move();
646 self.reset_blink();
647 }
648
649 pub fn insert_char_styled(&mut self, ch: char, max_length: Option<usize>) {
651 let escaped = styling::escape_char(ch);
652 self.insert_text_styled(&escaped, max_length);
653 }
654
655 pub fn backspace_styled(&mut self) {
657 if self.delete_selection_styled() {
658 return;
659 }
660 if self.no_styles_movement {
661 let cp = styling::cursor_to_content(&self.text, self.cursor_pos);
662 if cp > 0 {
663 self.text = styling::delete_content_range(&self.text, cp - 1, cp);
664 self.cursor_pos = styling::content_to_cursor(&self.text, cp - 1, true);
665 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
666 self.text = cleaned;
667 self.cursor_pos = new_pos;
668 self.snap_to_content_pos();
669 }
670 } else if self.cursor_pos > 0 {
671 self.text = styling::delete_visual_range(&self.text, self.cursor_pos - 1, self.cursor_pos);
672 self.cursor_pos -= 1;
673 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
674 self.text = cleaned;
675 self.cursor_pos = new_pos;
676 }
677 self.preferred_col = None;
678 self.reset_blink();
679 }
680
681 pub fn delete_forward_styled(&mut self) {
683 if self.delete_selection_styled() {
684 return;
685 }
686 if self.no_styles_movement {
687 let cp = styling::cursor_to_content(&self.text, self.cursor_pos);
688 let content_len = styling::strip_styling(&self.text).chars().count();
689 if cp < content_len {
690 self.text = styling::delete_content_range(&self.text, cp, cp + 1);
691 self.cursor_pos = styling::content_to_cursor(&self.text, cp, true);
692 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
693 self.text = cleaned;
694 self.cursor_pos = new_pos;
695 self.snap_to_content_pos();
696 }
697 } else {
698 let vis_len = self.cursor_len_styled();
699 if self.cursor_pos < vis_len {
700 self.text = styling::delete_visual_range(&self.text, self.cursor_pos, self.cursor_pos + 1);
701 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
702 self.text = cleaned;
703 self.cursor_pos = new_pos;
704 }
705 }
706 self.preferred_col = None;
707 self.reset_blink();
708 }
709
710 pub fn backspace_word_styled(&mut self) {
712 if self.delete_selection_styled() {
713 return;
714 }
715 if self.no_styles_movement {
716 let cp = styling::cursor_to_content(&self.text, self.cursor_pos);
717 let stripped = styling::strip_styling(&self.text);
718 let target_cp = find_word_boundary_left(&stripped, cp);
719 if target_cp < cp {
720 self.text = styling::delete_content_range(&self.text, target_cp, cp);
721 self.cursor_pos = styling::content_to_cursor(&self.text, target_cp, true);
722 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
723 self.text = cleaned;
724 self.cursor_pos = new_pos;
725 self.snap_to_content_pos();
726 }
727 } else {
728 let target = styling::find_word_boundary_left_visual(&self.text, self.cursor_pos);
729 self.text = styling::delete_visual_range(&self.text, target, self.cursor_pos);
730 self.cursor_pos = target;
731 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
732 self.text = cleaned;
733 self.cursor_pos = new_pos;
734 }
735 self.preferred_col = None;
736 self.reset_blink();
737 }
738
739 pub fn delete_word_forward_styled(&mut self) {
741 if self.delete_selection_styled() {
742 return;
743 }
744 if self.no_styles_movement {
745 let cp = styling::cursor_to_content(&self.text, self.cursor_pos);
746 let stripped = styling::strip_styling(&self.text);
747 let target_cp = find_word_delete_boundary_right(&stripped, cp);
748 if target_cp > cp {
749 self.text = styling::delete_content_range(&self.text, cp, target_cp);
750 self.cursor_pos = styling::content_to_cursor(&self.text, cp, true);
751 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
752 self.text = cleaned;
753 self.cursor_pos = new_pos;
754 self.snap_to_content_pos();
755 }
756 } else {
757 let target = styling::find_word_delete_boundary_right_visual(&self.text, self.cursor_pos);
758 self.text = styling::delete_visual_range(&self.text, self.cursor_pos, target);
759 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
760 self.text = cleaned;
761 self.cursor_pos = new_pos;
762 }
763 self.preferred_col = None;
764 self.reset_blink();
765 }
766
767 pub fn move_left_styled(&mut self, shift: bool) {
769 if self.no_styles_movement {
770 return self.move_left_content(shift);
771 }
772 if !shift {
773 if let Some((start, _end)) = self.selection_range() {
774 self.cursor_pos = start;
775 self.selection_anchor = None;
776 self.cleanup_after_move();
777 return;
778 }
779 }
780 if self.cursor_pos > 0 {
781 if shift && self.selection_anchor.is_none() {
782 self.selection_anchor = Some(self.cursor_pos);
783 }
784 self.cursor_pos -= 1;
785 if shift {
786 if self.selection_anchor == Some(self.cursor_pos) {
787 self.selection_anchor = None;
788 }
789 }
790 }
791 if !shift {
792 self.selection_anchor = None;
793 }
794 self.cleanup_after_move();
795 }
796
797 fn move_left_content(&mut self, shift: bool) {
800 let cp = styling::cursor_to_content(&self.text, self.cursor_pos);
801 if !shift {
802 if let Some((start, _end)) = self.selection_range() {
803 let sc = styling::cursor_to_content(&self.text, start);
804 self.cursor_pos = styling::content_to_cursor(&self.text, sc, true);
805 self.selection_anchor = None;
806 self.cleanup_after_move();
807 return;
808 }
809 }
810 if cp > 0 {
811 if shift && self.selection_anchor.is_none() {
812 self.selection_anchor = Some(self.cursor_pos);
813 }
814 self.cursor_pos = styling::content_to_cursor(&self.text, cp - 1, true);
815 if shift {
816 if self.selection_anchor == Some(self.cursor_pos) {
817 self.selection_anchor = None;
818 }
819 }
820 }
821 if !shift {
822 self.selection_anchor = None;
823 }
824 self.cleanup_after_move();
825 }
826
827 pub fn move_right_styled(&mut self, shift: bool) {
829 let vis_len = self.cursor_len_styled();
830 if !shift {
831 if let Some((_start, end)) = self.selection_range() {
832 self.cursor_pos = end;
833 self.selection_anchor = None;
834 self.cleanup_after_move();
835 return;
836 }
837 }
838 if self.cursor_pos < vis_len {
839 if shift && self.selection_anchor.is_none() {
840 self.selection_anchor = Some(self.cursor_pos);
841 }
842 self.cursor_pos += 1;
843 if shift {
844 if self.selection_anchor == Some(self.cursor_pos) {
845 self.selection_anchor = None;
846 }
847 }
848 }
849 if !shift {
850 self.selection_anchor = None;
851 }
852 self.cleanup_after_move();
853 }
854
855 pub fn move_word_left_styled(&mut self, shift: bool) {
857 if shift && self.selection_anchor.is_none() {
858 self.selection_anchor = Some(self.cursor_pos);
859 }
860 self.cursor_pos = styling::find_word_boundary_left_visual(&self.text, self.cursor_pos);
861 if !shift {
862 self.selection_anchor = None;
863 } else if self.selection_anchor == Some(self.cursor_pos) {
864 self.selection_anchor = None;
865 }
866 self.cleanup_after_move();
867 }
868
869 pub fn move_word_right_styled(&mut self, shift: bool) {
871 if shift && self.selection_anchor.is_none() {
872 self.selection_anchor = Some(self.cursor_pos);
873 }
874 self.cursor_pos = styling::find_word_boundary_right_visual(&self.text, self.cursor_pos);
875 if !shift {
876 self.selection_anchor = None;
877 } else if self.selection_anchor == Some(self.cursor_pos) {
878 self.selection_anchor = None;
879 }
880 self.cleanup_after_move();
881 }
882
883 pub fn move_home_styled(&mut self, shift: bool) {
885 if shift && self.selection_anchor.is_none() {
886 self.selection_anchor = Some(self.cursor_pos);
887 }
888 self.cursor_pos = 0;
889 if !shift {
890 self.selection_anchor = None;
891 } else if self.selection_anchor == Some(0) {
892 self.selection_anchor = None;
893 }
894 self.cleanup_after_move();
895 }
896
897 pub fn move_end_styled(&mut self, shift: bool) {
899 let vis_len = self.cursor_len_styled();
900 if shift && self.selection_anchor.is_none() {
901 self.selection_anchor = Some(self.cursor_pos);
902 }
903 self.cursor_pos = vis_len;
904 if !shift {
905 self.selection_anchor = None;
906 } else if self.selection_anchor == Some(vis_len) {
907 self.selection_anchor = None;
908 }
909 self.cleanup_after_move();
910 }
911
912 pub fn move_up_styled(&mut self, shift: bool, visual_lines: Option<&[VisualLine]>) {
916 if shift && self.selection_anchor.is_none() {
917 self.selection_anchor = Some(self.cursor_pos);
918 }
919
920 let raw_cursor = styling::cursor_to_raw_for_insertion(&self.text, self.cursor_pos);
921
922 if let Some(vl) = visual_lines {
923 let (line_idx, _raw_col) = cursor_to_visual_pos(vl, raw_cursor);
924
925 let line_start_visual = styling::raw_to_cursor(&self.text, vl[line_idx].global_char_start);
928 let content_start = styling::cursor_to_content(&self.text, line_start_visual);
929 let content_current = styling::cursor_to_content(&self.text, self.cursor_pos);
930 let current_col = content_current.saturating_sub(content_start);
931 let col = self.preferred_col.unwrap_or(current_col);
932
933 if line_idx == 0 {
934 self.cursor_pos = 0;
936 } else {
937 let target = &vl[line_idx - 1];
938 let target_start_visual = styling::raw_to_cursor(&self.text, target.global_char_start);
939 let target_end_visual = styling::raw_to_cursor(
940 &self.text,
941 target.global_char_start + target.char_count,
942 );
943 let target_content_start = styling::cursor_to_content(&self.text, target_start_visual);
944 let target_content_end = styling::cursor_to_content(&self.text, target_end_visual);
945 let target_content_len = target_content_end - target_content_start;
946 let target_col = col.min(target_content_len);
947 self.cursor_pos = styling::content_to_cursor(&self.text, target_content_start + target_col, false);
948 }
949
950 self.preferred_col = Some(col);
951 } else {
952 let (line, _col) = styling::line_and_column_styled(&self.text, self.cursor_pos);
954 let col = self.preferred_col.unwrap_or({
955 let line_start = styling::line_start_visual_styled(&self.text, line);
956 let content_start = styling::cursor_to_content(&self.text, line_start);
957 let content_current = styling::cursor_to_content(&self.text, self.cursor_pos);
958 content_current.saturating_sub(content_start)
959 });
960
961 if line == 0 {
962 self.cursor_pos = 0;
963 } else {
964 let target_start = styling::line_start_visual_styled(&self.text, line - 1);
965 let target_end = styling::line_end_visual_styled(&self.text, line - 1);
966 let target_content_start = styling::cursor_to_content(&self.text, target_start);
967 let target_content_end = styling::cursor_to_content(&self.text, target_end);
968 let target_content_len = target_content_end - target_content_start;
969 let target_col = col.min(target_content_len);
970 self.cursor_pos = styling::content_to_cursor(&self.text, target_content_start + target_col, false);
971 }
972
973 self.preferred_col = Some(col);
974 }
975
976 if !shift {
977 self.selection_anchor = None;
978 } else if self.selection_anchor == Some(self.cursor_pos) {
979 self.selection_anchor = None;
980 }
981 self.reset_blink();
982 }
983
984 pub fn move_down_styled(&mut self, shift: bool, visual_lines: Option<&[VisualLine]>) {
986 if shift && self.selection_anchor.is_none() {
987 self.selection_anchor = Some(self.cursor_pos);
988 }
989
990 let vis_len = self.cursor_len_styled();
991 let raw_cursor = styling::cursor_to_raw_for_insertion(&self.text, self.cursor_pos);
992
993 if let Some(vl) = visual_lines {
994 let (line_idx, _raw_col) = cursor_to_visual_pos(vl, raw_cursor);
995
996 let line_start_visual = styling::raw_to_cursor(&self.text, vl[line_idx].global_char_start);
998 let content_start = styling::cursor_to_content(&self.text, line_start_visual);
999 let content_current = styling::cursor_to_content(&self.text, self.cursor_pos);
1000 let current_col = content_current.saturating_sub(content_start);
1001 let col = self.preferred_col.unwrap_or(current_col);
1002
1003 if line_idx >= vl.len() - 1 {
1004 self.cursor_pos = vis_len;
1006 } else {
1007 let target = &vl[line_idx + 1];
1008 let target_start_visual = styling::raw_to_cursor(&self.text, target.global_char_start);
1009 let target_end_visual = styling::raw_to_cursor(
1010 &self.text,
1011 target.global_char_start + target.char_count,
1012 );
1013 let target_content_start = styling::cursor_to_content(&self.text, target_start_visual);
1014 let target_content_end = styling::cursor_to_content(&self.text, target_end_visual);
1015 let target_content_len = target_content_end - target_content_start;
1016 let target_col = col.min(target_content_len);
1017 self.cursor_pos = styling::content_to_cursor(&self.text, target_content_start + target_col, false);
1018 }
1019
1020 self.preferred_col = Some(col);
1021 } else {
1022 let (line, _col) = styling::line_and_column_styled(&self.text, self.cursor_pos);
1024 let line_count = styling::styled_line_count(&self.text);
1025 let col = self.preferred_col.unwrap_or({
1026 let line_start = styling::line_start_visual_styled(&self.text, line);
1027 let content_start = styling::cursor_to_content(&self.text, line_start);
1028 let content_current = styling::cursor_to_content(&self.text, self.cursor_pos);
1029 content_current.saturating_sub(content_start)
1030 });
1031
1032 if line >= line_count - 1 {
1033 self.cursor_pos = vis_len;
1034 } else {
1035 let target_start = styling::line_start_visual_styled(&self.text, line + 1);
1036 let target_end = styling::line_end_visual_styled(&self.text, line + 1);
1037 let target_content_start = styling::cursor_to_content(&self.text, target_start);
1038 let target_content_end = styling::cursor_to_content(&self.text, target_end);
1039 let target_content_len = target_content_end - target_content_start;
1040 let target_col = col.min(target_content_len);
1041 self.cursor_pos = styling::content_to_cursor(&self.text, target_content_start + target_col, false);
1042 }
1043
1044 self.preferred_col = Some(col);
1045 }
1046
1047 if !shift {
1048 self.selection_anchor = None;
1049 } else if self.selection_anchor == Some(self.cursor_pos) {
1050 self.selection_anchor = None;
1051 }
1052 self.reset_blink();
1053 }
1054
1055 pub fn select_all_styled(&mut self) {
1057 let vis_len = self.cursor_len_styled();
1058 if vis_len > 0 {
1059 self.selection_anchor = Some(0);
1060 self.cursor_pos = vis_len;
1061 self.snap_to_content_pos();
1062 }
1063 self.reset_blink();
1064 }
1065
1066 pub fn click_to_cursor_styled(&mut self, click_visual_pos: usize, shift: bool) {
1069 if shift {
1070 if self.selection_anchor.is_none() {
1071 self.selection_anchor = Some(self.cursor_pos);
1072 }
1073 } else {
1074 self.selection_anchor = None;
1075 }
1076 self.cursor_pos = click_visual_pos;
1077 if shift {
1078 if self.selection_anchor == Some(self.cursor_pos) {
1079 self.selection_anchor = None;
1080 }
1081 }
1082 self.cleanup_after_move();
1083 }
1084
1085 pub fn select_word_at_styled(&mut self, visual_pos: usize) {
1087 let (start, end) = styling::find_word_at_visual(&self.text, visual_pos);
1088 if start != end {
1089 self.selection_anchor = Some(start);
1090 self.cursor_pos = end;
1091 self.snap_to_content_pos();
1092 }
1093 self.reset_blink();
1094 }
1095
1096 fn snap_to_content_pos(&mut self) {
1100 if !self.no_styles_movement { return; }
1101 let cp = styling::cursor_to_content(&self.text, self.cursor_pos);
1102 self.cursor_pos = styling::content_to_cursor(&self.text, cp, true);
1103 if let Some(anchor) = self.selection_anchor {
1104 let ac = styling::cursor_to_content(&self.text, anchor);
1105 self.selection_anchor = Some(
1106 styling::content_to_cursor(&self.text, ac, true),
1107 );
1108 if self.selection_anchor == Some(self.cursor_pos) {
1109 self.selection_anchor = None;
1110 }
1111 }
1112 }
1113
1114 fn cleanup_after_move(&mut self) {
1117 self.snap_to_content_pos();
1120 let (cleaned, new_pos) = styling::cleanup_empty_styles(&self.text, self.cursor_pos);
1121 self.text = cleaned;
1122 self.cursor_pos = new_pos;
1123 self.snap_to_content_pos();
1125 self.preferred_col = None;
1126 self.reset_blink();
1127 }
1128
1129 pub fn cursor_pos_raw(&self) -> usize {
1132 styling::cursor_to_raw_for_insertion(&self.text, self.cursor_pos)
1133 }
1134
1135 pub fn selection_anchor_raw(&self) -> Option<usize> {
1137 self.selection_anchor.map(|a| styling::cursor_to_raw(&self.text, a))
1138 }
1139
1140 pub fn selection_range_raw(&self) -> Option<(usize, usize)> {
1142 self.selection_anchor.map(|anchor| {
1143 let raw_anchor = styling::cursor_to_raw(&self.text, anchor);
1144 let raw_cursor = styling::cursor_to_raw(&self.text, self.cursor_pos);
1145 let start = raw_anchor.min(raw_cursor);
1146 let end = raw_anchor.max(raw_cursor);
1147 (start, end)
1148 })
1149 }
1150}
1151
1152#[derive(Debug, Clone)]
1155pub struct TextInputConfig {
1156 pub placeholder: String,
1158 pub max_length: Option<usize>,
1160 pub is_password: bool,
1162 pub is_multiline: bool,
1164 pub font_size: u16,
1166 pub text_color: Color,
1168 pub placeholder_color: Color,
1170 pub cursor_color: Color,
1172 pub selection_color: Color,
1174 pub line_height: u16,
1176 pub no_styles_movement: bool,
1178 pub font_asset: Option<&'static crate::renderer::FontAsset>,
1180}
1181
1182impl Default for TextInputConfig {
1183 fn default() -> Self {
1184 Self {
1185 placeholder: String::new(),
1186 max_length: None,
1187 is_password: false,
1188 is_multiline: false,
1189 font_size: 0,
1190 text_color: Color::rgba(255.0, 255.0, 255.0, 255.0),
1191 placeholder_color: Color::rgba(128.0, 128.0, 128.0, 255.0),
1192 cursor_color: Color::rgba(255.0, 255.0, 255.0, 255.0),
1193 selection_color: Color::rgba(69.0, 130.0, 181.0, 128.0),
1194 line_height: 0,
1195 no_styles_movement: false,
1196 font_asset: None,
1197 }
1198 }
1199}
1200
1201pub struct TextInputBuilder {
1203 pub(crate) config: TextInputConfig,
1204 pub(crate) on_changed_fn: Option<Box<dyn FnMut(&str) + 'static>>,
1205 pub(crate) on_submit_fn: Option<Box<dyn FnMut(&str) + 'static>>,
1206}
1207
1208impl TextInputBuilder {
1209 pub(crate) fn new() -> Self {
1210 Self {
1211 config: TextInputConfig::default(),
1212 on_changed_fn: None,
1213 on_submit_fn: None,
1214 }
1215 }
1216
1217 #[inline]
1219 pub fn placeholder(&mut self, text: &str) -> &mut Self {
1220 self.config.placeholder = text.to_string();
1221 self
1222 }
1223
1224 #[inline]
1226 pub fn max_length(&mut self, len: usize) -> &mut Self {
1227 self.config.max_length = Some(len);
1228 self
1229 }
1230
1231 #[inline]
1233 pub fn password(&mut self, enabled: bool) -> &mut Self {
1234 self.config.is_password = enabled;
1235 self
1236 }
1237
1238 #[inline]
1240 pub fn multiline(&mut self, enabled: bool) -> &mut Self {
1241 self.config.is_multiline = enabled;
1242 self
1243 }
1244
1245 #[inline]
1249 pub fn font(&mut self, asset: &'static crate::renderer::FontAsset) -> &mut Self {
1250 self.config.font_asset = Some(asset);
1251 self
1252 }
1253
1254 #[inline]
1256 pub fn font_size(&mut self, size: u16) -> &mut Self {
1257 self.config.font_size = size;
1258 self
1259 }
1260
1261 #[inline]
1263 pub fn text_color(&mut self, color: impl Into<Color>) -> &mut Self {
1264 self.config.text_color = color.into();
1265 self
1266 }
1267
1268 #[inline]
1270 pub fn placeholder_color(&mut self, color: impl Into<Color>) -> &mut Self {
1271 self.config.placeholder_color = color.into();
1272 self
1273 }
1274
1275 #[inline]
1277 pub fn cursor_color(&mut self, color: impl Into<Color>) -> &mut Self {
1278 self.config.cursor_color = color.into();
1279 self
1280 }
1281
1282 #[inline]
1284 pub fn selection_color(&mut self, color: impl Into<Color>) -> &mut Self {
1285 self.config.selection_color = color.into();
1286 self
1287 }
1288
1289 #[inline]
1295 pub fn line_height(&mut self, height: u16) -> &mut Self {
1296 self.config.line_height = height;
1297 self
1298 }
1299
1300 #[inline]
1306 pub fn no_styles_movement(&mut self) -> &mut Self {
1307 self.config.no_styles_movement = true;
1308 self
1309 }
1310
1311 #[inline]
1313 pub fn on_changed<F>(&mut self, callback: F) -> &mut Self
1314 where
1315 F: FnMut(&str) + 'static,
1316 {
1317 self.on_changed_fn = Some(Box::new(callback));
1318 self
1319 }
1320
1321 #[inline]
1323 pub fn on_submit<F>(&mut self, callback: F) -> &mut Self
1324 where
1325 F: FnMut(&str) + 'static,
1326 {
1327 self.on_submit_fn = Some(Box::new(callback));
1328 self
1329 }
1330}
1331
1332pub fn char_index_to_byte(s: &str, char_idx: usize) -> usize {
1334 s.char_indices()
1335 .nth(char_idx)
1336 .map(|(byte_pos, _)| byte_pos)
1337 .unwrap_or(s.len())
1338}
1339
1340pub fn line_start_char_pos(text: &str, char_pos: usize) -> usize {
1343 let chars: Vec<char> = text.chars().collect();
1344 let mut i = char_pos;
1345 while i > 0 && chars[i - 1] != '\n' {
1346 i -= 1;
1347 }
1348 i
1349}
1350
1351pub fn line_end_char_pos(text: &str, char_pos: usize) -> usize {
1354 let chars: Vec<char> = text.chars().collect();
1355 let len = chars.len();
1356 let mut i = char_pos;
1357 while i < len && chars[i] != '\n' {
1358 i += 1;
1359 }
1360 i
1361}
1362
1363pub fn line_and_column(text: &str, char_pos: usize) -> (usize, usize) {
1366 let mut line = 0;
1367 let mut col = 0;
1368 for (i, ch) in text.chars().enumerate() {
1369 if i == char_pos {
1370 return (line, col);
1371 }
1372 if ch == '\n' {
1373 line += 1;
1374 col = 0;
1375 } else {
1376 col += 1;
1377 }
1378 }
1379 (line, col)
1380}
1381
1382pub fn char_pos_from_line_col(text: &str, target_line: usize, target_col: usize) -> usize {
1385 let mut line = 0;
1386 let mut col = 0;
1387 for (i, ch) in text.chars().enumerate() {
1388 if line == target_line && col == target_col {
1389 return i;
1390 }
1391 if ch == '\n' {
1392 if line == target_line {
1393 return i;
1395 }
1396 line += 1;
1397 col = 0;
1398 } else {
1399 col += 1;
1400 }
1401 }
1402 text.chars().count()
1404}
1405
1406pub fn split_lines(text: &str) -> Vec<(usize, &str)> {
1409 let mut result = Vec::new();
1410 let mut char_start = 0;
1411 let mut byte_start = 0;
1412 for (byte_idx, ch) in text.char_indices() {
1413 if ch == '\n' {
1414 result.push((char_start, &text[byte_start..byte_idx]));
1415 char_start += text[byte_start..byte_idx].chars().count() + 1; byte_start = byte_idx + 1; }
1418 }
1419 result.push((char_start, &text[byte_start..]));
1421 result
1422}
1423
1424#[derive(Debug, Clone)]
1426pub struct VisualLine {
1427 pub text: String,
1429 pub global_char_start: usize,
1431 pub char_count: usize,
1433}
1434
1435pub fn wrap_lines(
1439 text: &str,
1440 max_width: f32,
1441 font_asset: Option<&'static crate::renderer::FontAsset>,
1442 font_size: u16,
1443 measure_fn: &dyn Fn(&str, &crate::text::TextConfig) -> crate::math::Dimensions,
1444) -> Vec<VisualLine> {
1445 let config = crate::text::TextConfig {
1446 font_asset,
1447 font_size,
1448 ..Default::default()
1449 };
1450
1451 let hard_lines = split_lines(text);
1452 let mut result = Vec::new();
1453
1454 for (global_start, line_text) in hard_lines {
1455 if line_text.is_empty() {
1456 result.push(VisualLine {
1457 text: String::new(),
1458 global_char_start: global_start,
1459 char_count: 0,
1460 });
1461 continue;
1462 }
1463
1464 if max_width <= 0.0 {
1465 result.push(VisualLine {
1467 text: line_text.to_string(),
1468 global_char_start: global_start,
1469 char_count: line_text.chars().count(),
1470 });
1471 continue;
1472 }
1473
1474 let full_width = measure_fn(line_text, &config).width;
1476 if full_width <= max_width {
1477 result.push(VisualLine {
1478 text: line_text.to_string(),
1479 global_char_start: global_start,
1480 char_count: line_text.chars().count(),
1481 });
1482 continue;
1483 }
1484
1485 let chars: Vec<char> = line_text.chars().collect();
1487 let total_chars = chars.len();
1488 let mut line_char_start = 0; while line_char_start < total_chars {
1491 let mut fit_count = 0;
1493
1494 #[cfg(feature = "text-styling")]
1495 {
1496 let mut in_tag_hdr = false;
1500 let mut escaped = false;
1501 for i in 1..=(total_chars - line_char_start) {
1502 let ch = chars[line_char_start + i - 1];
1503 if escaped {
1504 escaped = false;
1505 let substr: String = chars[line_char_start..line_char_start + i].iter().collect();
1507 let w = measure_fn(&substr, &config).width;
1508 if w > max_width { break; }
1509 fit_count = i;
1510 continue;
1511 }
1512 match ch {
1513 '\\' => { escaped = true; }
1514 '{' => { in_tag_hdr = true; fit_count = i; }
1515 '|' if in_tag_hdr => { in_tag_hdr = false; fit_count = i; }
1516 '}' => { fit_count = i; }
1517 _ if in_tag_hdr => { fit_count = i; }
1518 _ => {
1519 let substr: String = chars[line_char_start..line_char_start + i].iter().collect();
1521 let w = measure_fn(&substr, &config).width;
1522 if w > max_width { break; }
1523 fit_count = i;
1524 }
1525 }
1526 }
1527 }
1528
1529 #[cfg(not(feature = "text-styling"))]
1530 {
1531 for i in 1..=(total_chars - line_char_start) {
1532 let substr: String = chars[line_char_start..line_char_start + i].iter().collect();
1533 let w = measure_fn(&substr, &config).width;
1534 if w > max_width {
1535 break;
1536 }
1537 fit_count = i;
1538 }
1539 }
1540
1541 if fit_count == 0 {
1542 #[cfg(feature = "text-styling")]
1546 if chars[line_char_start] == '\\' && line_char_start + 2 <= total_chars {
1547 fit_count = 2;
1548 } else {
1549 fit_count = 1;
1550 }
1551 #[cfg(not(feature = "text-styling"))]
1552 {
1553 fit_count = 1;
1554 }
1555 }
1556
1557 if line_char_start + fit_count < total_chars {
1558 let mut break_at = fit_count;
1560 let mut found_space = false;
1561 for j in (1..=fit_count).rev() {
1562 if chars[line_char_start + j - 1] == ' ' {
1563 break_at = j;
1564 found_space = true;
1565 break;
1566 }
1567 }
1568 #[allow(unused_mut)]
1570 let mut wrap_count = if found_space { break_at } else { fit_count };
1571 #[cfg(feature = "text-styling")]
1573 if wrap_count > 0
1574 && chars[line_char_start + wrap_count - 1] == '\\'
1575 && line_char_start + wrap_count < total_chars
1576 {
1577 if wrap_count > 1 {
1578 wrap_count -= 1; } else {
1580 wrap_count = 2.min(total_chars - line_char_start); }
1582 }
1583 let segment: String = chars[line_char_start..line_char_start + wrap_count].iter().collect();
1584 result.push(VisualLine {
1585 text: segment,
1586 global_char_start: global_start + line_char_start,
1587 char_count: wrap_count,
1588 });
1589 line_char_start += wrap_count;
1590 if found_space && line_char_start < total_chars && chars[line_char_start] == ' ' {
1592 }
1595 } else {
1596 let segment: String = chars[line_char_start..].iter().collect();
1598 let count = total_chars - line_char_start;
1599 result.push(VisualLine {
1600 text: segment,
1601 global_char_start: global_start + line_char_start,
1602 char_count: count,
1603 });
1604 line_char_start = total_chars;
1605 }
1606 }
1607 }
1608
1609 if result.is_empty() {
1611 result.push(VisualLine {
1612 text: String::new(),
1613 global_char_start: 0,
1614 char_count: 0,
1615 });
1616 }
1617
1618 result
1619}
1620
1621pub fn cursor_to_visual_pos(visual_lines: &[VisualLine], cursor_pos: usize) -> (usize, usize) {
1623 for (i, vl) in visual_lines.iter().enumerate() {
1624 let line_end = vl.global_char_start + vl.char_count;
1625 if cursor_pos < line_end || i == visual_lines.len() - 1 {
1626 return (i, cursor_pos.saturating_sub(vl.global_char_start));
1627 }
1628 if cursor_pos == line_end {
1632 if i + 1 < visual_lines.len() {
1634 let next = &visual_lines[i + 1];
1635 if next.global_char_start == line_end {
1636 return (i + 1, 0);
1638 }
1639 return (i, cursor_pos - vl.global_char_start);
1641 }
1642 return (i, cursor_pos - vl.global_char_start);
1643 }
1644 }
1645 (0, 0)
1646}
1647
1648pub fn visual_move_up(visual_lines: &[VisualLine], cursor_pos: usize) -> usize {
1651 let (line, col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1652 if line == 0 {
1653 return 0; }
1655 let target_line = &visual_lines[line - 1];
1656 let new_col = col.min(target_line.char_count);
1657 target_line.global_char_start + new_col
1658}
1659
1660pub fn visual_move_down(visual_lines: &[VisualLine], cursor_pos: usize, text_len: usize) -> usize {
1662 let (line, col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1663 if line >= visual_lines.len() - 1 {
1664 return text_len; }
1666 let target_line = &visual_lines[line + 1];
1667 let new_col = col.min(target_line.char_count);
1668 target_line.global_char_start + new_col
1669}
1670
1671pub fn visual_line_home(visual_lines: &[VisualLine], cursor_pos: usize) -> usize {
1673 let (line, _col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1674 visual_lines[line].global_char_start
1675}
1676
1677pub fn visual_line_end(visual_lines: &[VisualLine], cursor_pos: usize) -> usize {
1679 let (line, _col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1680 visual_lines[line].global_char_start + visual_lines[line].char_count
1681}
1682
1683pub fn find_nearest_char_boundary(click_x: f32, char_x_positions: &[f32]) -> usize {
1686 if char_x_positions.is_empty() {
1687 return 0;
1688 }
1689 let mut best = 0;
1690 let mut best_dist = f32::MAX;
1691 for (i, &x) in char_x_positions.iter().enumerate() {
1692 let dist = (click_x - x).abs();
1693 if dist < best_dist {
1694 best_dist = dist;
1695 best = i;
1696 }
1697 }
1698 best
1699}
1700
1701pub fn find_word_boundary_left(text: &str, pos: usize) -> usize {
1703 if pos == 0 {
1704 return 0;
1705 }
1706 let chars: Vec<char> = text.chars().collect();
1707 let mut i = pos.min(chars.len());
1708 while i > 0 && chars[i - 1].is_whitespace() {
1710 i -= 1;
1711 }
1712 while i > 0 && !chars[i - 1].is_whitespace() {
1714 i -= 1;
1715 }
1716 i
1717}
1718
1719pub fn find_word_boundary_right(text: &str, pos: usize) -> usize {
1722 let chars: Vec<char> = text.chars().collect();
1723 let len = chars.len();
1724 if pos >= len {
1725 return len;
1726 }
1727 let mut i = pos;
1728 while i < len && chars[i].is_whitespace() {
1730 i += 1;
1731 }
1732 while i < len && !chars[i].is_whitespace() {
1734 i += 1;
1735 }
1736 i
1737}
1738
1739pub fn find_word_delete_boundary_right(text: &str, pos: usize) -> usize {
1742 let chars: Vec<char> = text.chars().collect();
1743 let len = chars.len();
1744 if pos >= len {
1745 return len;
1746 }
1747 let mut i = pos;
1748 while i < len && !chars[i].is_whitespace() {
1750 i += 1;
1751 }
1752 while i < len && chars[i].is_whitespace() {
1754 i += 1;
1755 }
1756 i
1757}
1758
1759pub fn find_word_at(text: &str, pos: usize) -> (usize, usize) {
1762 let chars: Vec<char> = text.chars().collect();
1763 let len = chars.len();
1764 if len == 0 || pos >= len {
1765 return (pos, pos);
1766 }
1767 let is_word_char = |c: char| !c.is_whitespace();
1768 if !is_word_char(chars[pos]) {
1769 let mut start = pos;
1771 while start > 0 && !is_word_char(chars[start - 1]) {
1772 start -= 1;
1773 }
1774 let mut end = pos;
1775 while end < len && !is_word_char(chars[end]) {
1776 end += 1;
1777 }
1778 return (start, end);
1779 }
1780 let mut start = pos;
1782 while start > 0 && is_word_char(chars[start - 1]) {
1783 start -= 1;
1784 }
1785 let mut end = pos;
1786 while end < len && is_word_char(chars[end]) {
1787 end += 1;
1788 }
1789 (start, end)
1790}
1791
1792pub fn display_text(text: &str, placeholder: &str, is_password: bool) -> String {
1795 if text.is_empty() {
1796 return placeholder.to_string();
1797 }
1798 if is_password {
1799 "•".repeat(text.chars().count())
1800 } else {
1801 text.to_string()
1802 }
1803}
1804
1805#[cfg(feature = "text-styling")]
1815pub mod styling {
1816 pub fn escape_char(ch: char) -> String {
1819 match ch {
1820 '{' | '}' | '|' | '\\' => format!("\\{}", ch),
1821 _ => ch.to_string(),
1822 }
1823 }
1824
1825 pub fn escape_str(s: &str) -> String {
1827 let mut result = String::with_capacity(s.len());
1828 for ch in s.chars() {
1829 match ch {
1830 '{' | '}' | '|' | '\\' => {
1831 result.push('\\');
1832 result.push(ch);
1833 }
1834 _ => result.push(ch),
1835 }
1836 }
1837 result
1838 }
1839
1840 pub fn cursor_to_raw(raw: &str, visual_pos: usize) -> usize {
1853 if visual_pos == 0 {
1854 return 0;
1855 }
1856
1857 let chars: Vec<char> = raw.chars().collect();
1858 let len = chars.len();
1859 let mut visual = 0usize;
1860 let mut raw_idx = 0usize;
1861 let mut escaped = false;
1862 let mut in_style_def = false;
1863
1864 while raw_idx < len {
1865 let c = chars[raw_idx];
1866
1867 if escaped {
1868 visual += 1;
1870 escaped = false;
1871 raw_idx += 1;
1872 if visual == visual_pos {
1873 return skip_tag_headers(&chars, raw_idx);
1874 }
1875 continue;
1876 }
1877
1878 match c {
1879 '\\' => {
1880 escaped = true;
1881 raw_idx += 1;
1882 }
1883 '{' if !in_style_def => {
1884 in_style_def = true;
1885 raw_idx += 1;
1886 }
1887 '|' if in_style_def => {
1888 in_style_def = false;
1889 if raw_idx + 1 < len && chars[raw_idx + 1] == '}' {
1891 visual += 1;
1893 raw_idx += 1; if visual == visual_pos {
1895 return raw_idx; }
1897 } else {
1899 raw_idx += 1;
1900 }
1901 }
1902 '}' if !in_style_def => {
1903 visual += 1;
1905 raw_idx += 1;
1906 if visual == visual_pos {
1907 return raw_idx;
1909 }
1910 }
1911 _ if in_style_def => {
1912 raw_idx += 1;
1913 }
1914 _ => {
1915 visual += 1;
1917 raw_idx += 1;
1918 if visual == visual_pos {
1919 return skip_tag_headers(&chars, raw_idx);
1922 }
1923 }
1924 }
1925 }
1926
1927 len
1928 }
1929
1930 fn skip_tag_headers(chars: &[char], pos: usize) -> usize {
1933 let len = chars.len();
1934 let mut p = pos;
1935 while p < len && chars[p] == '{' {
1936 let mut j = p + 1;
1937 while j < len && chars[j] != '|' && chars[j] != '}' {
1938 j += 1;
1939 }
1940 if j < len && chars[j] == '|' {
1941 p = j + 1; } else {
1943 break; }
1945 }
1946 p
1947 }
1948
1949 pub fn raw_to_cursor(raw: &str, raw_pos: usize) -> usize {
1952 let chars: Vec<char> = raw.chars().collect();
1953 let len = chars.len();
1954 let mut visual = 0usize;
1955 let mut raw_idx = 0usize;
1956 let mut escaped = false;
1957 let mut in_style_def = false;
1958
1959 while raw_idx < len && raw_idx < raw_pos {
1960 let c = chars[raw_idx];
1961
1962 if escaped {
1963 visual += 1;
1964 escaped = false;
1965 raw_idx += 1;
1966 continue;
1967 }
1968
1969 match c {
1970 '\\' => {
1971 escaped = true;
1972 raw_idx += 1;
1973 }
1974 '{' if !in_style_def => {
1975 in_style_def = true;
1976 raw_idx += 1;
1977 }
1978 '|' if in_style_def => {
1979 in_style_def = false;
1980 if raw_idx + 1 < len && chars[raw_idx + 1] == '}' {
1982 visual += 1; raw_idx += 1; } else {
1986 raw_idx += 1;
1987 }
1988 }
1989 '}' if !in_style_def => {
1990 visual += 1; raw_idx += 1;
1992 }
1993 _ if in_style_def => {
1994 raw_idx += 1;
1995 }
1996 _ => {
1997 visual += 1;
1998 raw_idx += 1;
1999 }
2000 }
2001 }
2002
2003 visual
2004 }
2005
2006 pub fn cursor_len(raw: &str) -> usize {
2009 raw_to_cursor(raw, raw.chars().count())
2010 }
2011
2012 fn enter_empty_tags_at(chars: &[char], pos: usize) -> usize {
2016 let len = chars.len();
2017 let mut p = pos;
2018 while p < len && chars[p] == '{' {
2019 let mut j = p + 1;
2021 while j < len && chars[j] != '|' && chars[j] != '}' {
2022 j += 1;
2023 }
2024 if j < len && chars[j] == '|' {
2025 if j + 1 < len && chars[j + 1] == '}' {
2027 p = j + 1;
2029 } else {
2030 break; }
2032 } else {
2033 break; }
2035 }
2036 p
2037 }
2038
2039 pub fn cursor_to_raw_for_insertion(raw: &str, visual_pos: usize) -> usize {
2043 let pos = cursor_to_raw(raw, visual_pos);
2044 let chars: Vec<char> = raw.chars().collect();
2045 enter_empty_tags_at(&chars, pos)
2047 }
2048
2049 pub fn insert_at_visual(raw: &str, visual_pos: usize, insert: &str) -> (String, usize) {
2053 let raw_pos = cursor_to_raw_for_insertion(raw, visual_pos);
2054 let byte_pos = super::char_index_to_byte(raw, raw_pos);
2055 let mut new_raw = String::with_capacity(raw.len() + insert.len());
2056 new_raw.push_str(&raw[..byte_pos]);
2057 new_raw.push_str(insert);
2058 new_raw.push_str(&raw[byte_pos..]);
2059 let inserted_visual = cursor_len(insert);
2060 (new_raw, visual_pos + inserted_visual)
2061 }
2062
2063 pub fn delete_visual_range(raw: &str, visual_start: usize, visual_end: usize) -> String {
2067 if visual_start >= visual_end {
2068 return raw.to_string();
2069 }
2070
2071 let chars: Vec<char> = raw.chars().collect();
2072 let len = chars.len();
2073 let mut result = String::with_capacity(raw.len());
2074 let mut visual = 0usize;
2075 let mut i = 0;
2076 let mut in_style_def = false;
2077
2078 while i < len {
2079 let c = chars[i];
2080
2081 match c {
2082 '\\' if !in_style_def && i + 1 < len => {
2083 let in_range = visual >= visual_start && visual < visual_end;
2085 if !in_range {
2086 result.push(c);
2087 result.push(chars[i + 1]);
2088 }
2089 visual += 1;
2090 i += 2;
2091 }
2092 '{' if !in_style_def => {
2093 in_style_def = true;
2094 result.push(c); i += 1;
2096 }
2097 '|' if in_style_def => {
2098 in_style_def = false;
2099 result.push(c);
2100 if i + 1 < len && chars[i + 1] == '}' {
2102 visual += 1; }
2104 i += 1;
2105 }
2106 '}' if !in_style_def => {
2107 result.push(c); visual += 1; i += 1;
2110 }
2111 _ if in_style_def => {
2112 result.push(c); i += 1;
2114 }
2115 _ => {
2116 let in_range = visual >= visual_start && visual < visual_end;
2117 if !in_range {
2118 result.push(c);
2119 }
2120 visual += 1;
2121 i += 1;
2122 }
2123 }
2124 }
2125
2126 result
2127 }
2128
2129 pub fn cleanup_empty_styles(raw: &str, cursor_visual_pos: usize) -> (String, usize) {
2135 let chars: Vec<char> = raw.chars().collect();
2136 let len = chars.len();
2137 let mut result = String::with_capacity(raw.len());
2138 let mut i = 0;
2139 let mut visual = 0usize;
2140 let mut escaped = false;
2141 let mut cursor_adj = cursor_visual_pos;
2142
2143 while i < len {
2145 let c = chars[i];
2146
2147 if escaped {
2148 result.push(c);
2149 visual += 1;
2150 escaped = false;
2151 i += 1;
2152 continue;
2153 }
2154
2155 match c {
2156 '\\' => {
2157 escaped = true;
2158 result.push(c);
2159 i += 1;
2160 }
2161 '{' => {
2162 let mut j = i + 1;
2166 let mut style_escaped = false;
2167 let mut found_pipe = false;
2168 while j < len {
2169 if style_escaped {
2170 style_escaped = false;
2171 j += 1;
2172 continue;
2173 }
2174 if chars[j] == '\\' {
2175 style_escaped = true;
2176 j += 1;
2177 continue;
2178 }
2179 if chars[j] == '|' {
2180 found_pipe = true;
2181 j += 1; break;
2183 }
2184 if chars[j] == '{' {
2185 j += 1;
2187 continue;
2188 }
2189 j += 1;
2190 }
2191
2192 if !found_pipe {
2193 result.push(c);
2195 i += 1;
2196 continue;
2197 }
2198
2199 let _content_start_raw = j;
2202 let mut k = j;
2203 let mut content_escaped = false;
2204 let mut has_visible_content = false;
2205 let mut nesting = 1; while k < len && nesting > 0 {
2207 if content_escaped {
2208 has_visible_content = true;
2209 content_escaped = false;
2210 k += 1;
2211 continue;
2212 }
2213 match chars[k] {
2214 '\\' => {
2215 content_escaped = true;
2216 k += 1;
2217 }
2218 '{' => {
2219 nesting += 1;
2221 k += 1;
2222 }
2223 '}' => {
2224 nesting -= 1;
2225 if nesting == 0 {
2226 break; }
2228 k += 1;
2229 }
2230 '|' => {
2231 k += 1;
2233 }
2234 _ => {
2235 has_visible_content = true;
2236 k += 1;
2237 }
2238 }
2239 }
2240
2241 if !has_visible_content && nesting == 0 {
2242 let cursor_is_inside = cursor_visual_pos == visual
2247 || cursor_visual_pos == visual + 1;
2248 if cursor_is_inside {
2249 for idx in i..=k {
2251 result.push(chars[idx]);
2252 }
2253 visual += 2; } else {
2255 if cursor_adj > visual {
2258 cursor_adj = cursor_adj.saturating_sub(2);
2259 }
2260 }
2261 i = k + 1;
2262 } else {
2263 for idx in i..j {
2266 result.push(chars[idx]);
2267 }
2268 i = j;
2270 }
2271 }
2272 '}' => {
2273 result.push(c);
2274 visual += 1; i += 1;
2276 }
2277 _ => {
2278 result.push(c);
2279 visual += 1;
2280 i += 1;
2281 }
2282 }
2283 }
2284
2285 (result, cursor_adj)
2286 }
2287
2288 pub fn visual_char_at(raw: &str, visual_pos: usize) -> Option<char> {
2290 let raw_pos = cursor_to_raw(raw, visual_pos);
2291 let chars: Vec<char> = raw.chars().collect();
2292 if raw_pos >= chars.len() {
2293 return None;
2294 }
2295 if chars[raw_pos] == '\\' && raw_pos + 1 < chars.len() {
2297 Some(chars[raw_pos + 1])
2298 } else {
2299 Some(chars[raw_pos])
2300 }
2301 }
2302
2303 pub fn strip_styling(raw: &str) -> String {
2305 let mut result = String::new();
2306 let mut escaped = false;
2307 let mut in_style_def = false;
2308 for c in raw.chars() {
2309 if escaped {
2310 result.push(c);
2311 escaped = false;
2312 continue;
2313 }
2314 match c {
2315 '\\' => { escaped = true; }
2316 '{' if !in_style_def => { in_style_def = true; }
2317 '|' if in_style_def => { in_style_def = false; }
2318 '}' if !in_style_def => { }
2319 _ if in_style_def => { }
2320 _ => { result.push(c); }
2321 }
2322 }
2323 result
2324 }
2325
2326 pub fn cursor_to_content(raw: &str, cursor_pos: usize) -> usize {
2330 let chars: Vec<char> = raw.chars().collect();
2331 let len = chars.len();
2332 let mut visual = 0usize;
2333 let mut content = 0usize;
2334 let mut escaped = false;
2335 let mut in_style_def = false;
2336
2337 for i in 0..len {
2338 if visual >= cursor_pos {
2339 break;
2340 }
2341 let c = chars[i];
2342
2343 if escaped {
2344 visual += 1;
2345 content += 1;
2346 escaped = false;
2347 continue;
2348 }
2349
2350 match c {
2351 '\\' => { escaped = true; }
2352 '{' if !in_style_def => { in_style_def = true; }
2353 '|' if in_style_def => {
2354 in_style_def = false;
2355 if i + 1 < len && chars[i + 1] == '}' {
2356 visual += 1; }
2358 }
2359 '}' if !in_style_def => {
2360 visual += 1; }
2362 _ if in_style_def => {}
2363 _ => {
2364 visual += 1;
2365 content += 1;
2366 }
2367 }
2368 }
2369
2370 content
2371 }
2372
2373 pub fn content_to_cursor(raw: &str, content_pos: usize, snap_to_content: bool) -> usize {
2382 let chars: Vec<char> = raw.chars().collect();
2383 let len = chars.len();
2384 let mut visual = 0usize;
2385 let mut content = 0usize;
2386 let mut escaped = false;
2387 let mut in_style_def = false;
2388
2389 if snap_to_content {
2390 for i in 0..len {
2392 let c = chars[i];
2393
2394 if escaped {
2395 if content >= content_pos {
2396 return visual;
2397 }
2398 visual += 1;
2399 content += 1;
2400 escaped = false;
2401 continue;
2402 }
2403
2404 match c {
2405 '\\' => { escaped = true; }
2406 '{' if !in_style_def => { in_style_def = true; }
2407 '|' if in_style_def => {
2408 in_style_def = false;
2409 if i + 1 < len && chars[i + 1] == '}' {
2410 visual += 1; }
2412 }
2413 '}' if !in_style_def => {
2414 visual += 1; }
2416 _ if in_style_def => {}
2417 _ => {
2418 if content >= content_pos {
2419 return visual;
2420 }
2421 visual += 1;
2422 content += 1;
2423 }
2424 }
2425 }
2426 } else {
2427 for i in 0..len {
2429 if content >= content_pos {
2430 break;
2431 }
2432 let c = chars[i];
2433
2434 if escaped {
2435 visual += 1;
2436 content += 1;
2437 escaped = false;
2438 continue;
2439 }
2440
2441 match c {
2442 '\\' => { escaped = true; }
2443 '{' if !in_style_def => { in_style_def = true; }
2444 '|' if in_style_def => {
2445 in_style_def = false;
2446 if i + 1 < len && chars[i + 1] == '}' {
2447 visual += 1; }
2449 }
2450 '}' if !in_style_def => {
2451 visual += 1; }
2453 _ if in_style_def => {}
2454 _ => {
2455 visual += 1;
2456 content += 1;
2457 }
2458 }
2459 }
2460 }
2461
2462 visual
2463 }
2464
2465 pub fn delete_content_range(raw: &str, content_start: usize, content_end: usize) -> String {
2468 if content_start >= content_end {
2469 return raw.to_string();
2470 }
2471
2472 let chars: Vec<char> = raw.chars().collect();
2473 let len = chars.len();
2474 let mut result = String::with_capacity(raw.len());
2475 let mut content = 0usize;
2476 let mut i = 0;
2477 let mut in_style_def = false;
2478
2479 while i < len {
2480 let c = chars[i];
2481
2482 match c {
2483 '\\' if !in_style_def && i + 1 < len => {
2484 let in_range = content >= content_start && content < content_end;
2485 if !in_range {
2486 result.push(c);
2487 result.push(chars[i + 1]);
2488 }
2489 content += 1;
2490 i += 2;
2491 }
2492 '{' if !in_style_def => {
2493 in_style_def = true;
2494 result.push(c);
2495 i += 1;
2496 }
2497 '|' if in_style_def => {
2498 in_style_def = false;
2499 result.push(c);
2500 i += 1;
2501 }
2502 '}' if !in_style_def => {
2503 result.push(c);
2504 i += 1;
2505 }
2506 _ if in_style_def => {
2507 result.push(c);
2508 i += 1;
2509 }
2510 _ => {
2511 let in_range = content >= content_start && content < content_end;
2512 if !in_range {
2513 result.push(c);
2514 }
2515 content += 1;
2516 i += 1;
2517 }
2518 }
2519 }
2520
2521 result
2522 }
2523
2524 pub fn find_word_boundary_left_visual(raw: &str, visual_pos: usize) -> usize {
2527 let cp = cursor_to_content(raw, visual_pos);
2528 let stripped = strip_styling(raw);
2529 let boundary = super::find_word_boundary_left(&stripped, cp);
2530 content_to_cursor(raw, boundary, false)
2531 }
2532
2533 pub fn find_word_boundary_right_visual(raw: &str, visual_pos: usize) -> usize {
2536 let cp = cursor_to_content(raw, visual_pos);
2537 let stripped = strip_styling(raw);
2538 let boundary = super::find_word_boundary_right(&stripped, cp);
2539 content_to_cursor(raw, boundary, false)
2540 }
2541
2542 pub fn find_word_delete_boundary_right_visual(raw: &str, visual_pos: usize) -> usize {
2545 let cp = cursor_to_content(raw, visual_pos);
2546 let stripped = strip_styling(raw);
2547 let boundary = super::find_word_delete_boundary_right(&stripped, cp);
2548 content_to_cursor(raw, boundary, false)
2549 }
2550
2551 pub fn find_word_at_visual(raw: &str, visual_pos: usize) -> (usize, usize) {
2554 let cp = cursor_to_content(raw, visual_pos);
2555 let stripped = strip_styling(raw);
2556 let (s, e) = super::find_word_at(&stripped, cp);
2557 (content_to_cursor(raw, s, false), content_to_cursor(raw, e, false))
2558 }
2559
2560 pub fn styled_line_count(raw: &str) -> usize {
2562 raw.chars().filter(|&c| c == '\n').count() + 1
2564 }
2565
2566 pub fn line_and_column_styled(raw: &str, visual_pos: usize) -> (usize, usize) {
2569 let chars: Vec<char> = raw.chars().collect();
2571 let len = chars.len();
2572 let mut visual = 0usize;
2573 let mut line = 0usize;
2574 let mut line_start_visual = 0usize;
2575 let mut escaped = false;
2576 let mut in_style_def = false;
2577
2578 for i in 0..len {
2579 if visual >= visual_pos {
2580 break;
2581 }
2582 let c = chars[i];
2583
2584 if escaped {
2585 visual += 1;
2586 escaped = false;
2587 continue;
2588 }
2589
2590 match c {
2591 '\\' => { escaped = true; }
2592 '\n' => {
2593 visual += 1; line += 1;
2595 line_start_visual = visual;
2596 }
2597 '{' if !in_style_def => { in_style_def = true; }
2598 '|' if in_style_def => {
2599 in_style_def = false;
2600 if i + 1 < len && chars[i + 1] == '}' {
2601 visual += 1; }
2603 }
2604 '}' if !in_style_def => {
2605 visual += 1;
2606 }
2607 _ if in_style_def => {}
2608 _ => {
2609 visual += 1;
2610 }
2611 }
2612 }
2613
2614 (line, visual_pos.saturating_sub(line_start_visual))
2615 }
2616
2617 pub fn line_start_visual_styled(raw: &str, line_idx: usize) -> usize {
2619 if line_idx == 0 {
2620 return 0;
2621 }
2622 let chars: Vec<char> = raw.chars().collect();
2623 let len = chars.len();
2624 let mut visual = 0usize;
2625 let mut line = 0usize;
2626 let mut escaped = false;
2627 let mut in_style_def = false;
2628
2629 for i in 0..len {
2630 let c = chars[i];
2631 if escaped {
2632 visual += 1;
2633 escaped = false;
2634 continue;
2635 }
2636 match c {
2637 '\\' => { escaped = true; }
2638 '\n' => {
2639 visual += 1;
2640 line += 1;
2641 if line == line_idx {
2642 return visual;
2643 }
2644 }
2645 '{' if !in_style_def => { in_style_def = true; }
2646 '|' if in_style_def => {
2647 in_style_def = false;
2648 if i + 1 < len && chars[i + 1] == '}' {
2649 visual += 1;
2650 }
2651 }
2652 '}' if !in_style_def => { visual += 1; }
2653 _ if in_style_def => {}
2654 _ => { visual += 1; }
2655 }
2656 }
2657 visual }
2659
2660 pub fn line_end_visual_styled(raw: &str, line_idx: usize) -> usize {
2662 let chars: Vec<char> = raw.chars().collect();
2663 let len = chars.len();
2664 let mut visual = 0usize;
2665 let mut line = 0usize;
2666 let mut escaped = false;
2667 let mut in_style_def = false;
2668
2669 for i in 0..len {
2670 let c = chars[i];
2671 if escaped {
2672 visual += 1;
2673 escaped = false;
2674 continue;
2675 }
2676 match c {
2677 '\\' => { escaped = true; }
2678 '\n' => {
2679 if line == line_idx {
2680 return visual;
2681 }
2682 visual += 1;
2683 line += 1;
2684 }
2685 '{' if !in_style_def => { in_style_def = true; }
2686 '|' if in_style_def => {
2687 in_style_def = false;
2688 if i + 1 < len && chars[i + 1] == '}' {
2689 visual += 1;
2690 }
2691 }
2692 '}' if !in_style_def => { visual += 1; }
2693 _ if in_style_def => {}
2694 _ => { visual += 1; }
2695 }
2696 }
2697 visual }
2699
2700 #[cfg(test)]
2701 mod tests {
2702 use super::*;
2703
2704 #[test]
2705 fn test_escape_char() {
2706 assert_eq!(escape_char('a'), "a");
2707 assert_eq!(escape_char('{'), "\\{");
2708 assert_eq!(escape_char('}'), "\\}");
2709 assert_eq!(escape_char('|'), "\\|");
2710 assert_eq!(escape_char('\\'), "\\\\");
2711 }
2712
2713 #[test]
2714 fn test_escape_str() {
2715 assert_eq!(escape_str("hello"), "hello");
2716 assert_eq!(escape_str("a{b}c"), "a\\{b\\}c");
2717 assert_eq!(escape_str("x|y\\z"), "x\\|y\\\\z");
2718 }
2719
2720 #[test]
2721 fn test_cursor_to_raw_no_styling() {
2722 assert_eq!(cursor_to_raw("hello", 0), 0);
2724 assert_eq!(cursor_to_raw("hello", 3), 3);
2725 assert_eq!(cursor_to_raw("hello", 5), 5);
2726 }
2727
2728 #[test]
2729 fn test_cursor_to_raw_with_escape() {
2730 let raw = r"hel\{lo";
2732 assert_eq!(cursor_to_raw(raw, 0), 0); assert_eq!(cursor_to_raw(raw, 3), 3); assert_eq!(cursor_to_raw(raw, 4), 5); assert_eq!(cursor_to_raw(raw, 5), 6); assert_eq!(cursor_to_raw(raw, 6), 7); }
2738
2739 #[test]
2740 fn test_cursor_to_raw_with_style() {
2741 let raw = "{red|world}";
2743 assert_eq!(cursor_to_raw(raw, 0), 0); assert_eq!(cursor_to_raw(raw, 1), 6); assert_eq!(cursor_to_raw(raw, 5), 10); assert_eq!(cursor_to_raw(raw, 6), 11); }
2750
2751 #[test]
2752 fn test_cursor_to_raw_mixed() {
2753 let raw = r"hel\{lo{red|world}";
2755 assert_eq!(cursor_to_raw(raw, 0), 0); assert_eq!(cursor_to_raw(raw, 3), 3); assert_eq!(cursor_to_raw(raw, 4), 5); assert_eq!(cursor_to_raw(raw, 6), 12); assert_eq!(cursor_to_raw(raw, 11), 17); assert_eq!(cursor_to_raw(raw, 12), 18); }
2762
2763 #[test]
2764 fn test_raw_to_cursor_no_styling() {
2765 assert_eq!(raw_to_cursor("hello", 0), 0);
2766 assert_eq!(raw_to_cursor("hello", 3), 3);
2767 assert_eq!(raw_to_cursor("hello", 5), 5);
2768 }
2769
2770 #[test]
2771 fn test_raw_to_cursor_with_escape() {
2772 let raw = r"hel\{lo";
2773 assert_eq!(raw_to_cursor(raw, 0), 0);
2774 assert_eq!(raw_to_cursor(raw, 3), 3); assert_eq!(raw_to_cursor(raw, 5), 4); assert_eq!(raw_to_cursor(raw, 7), 6); }
2778
2779 #[test]
2780 fn test_raw_to_cursor_with_style() {
2781 let raw = "{red|world}";
2783 assert_eq!(raw_to_cursor(raw, 0), 0);
2784 assert_eq!(raw_to_cursor(raw, 5), 0); assert_eq!(raw_to_cursor(raw, 6), 1); assert_eq!(raw_to_cursor(raw, 10), 5); assert_eq!(raw_to_cursor(raw, 11), 6); }
2789
2790 #[test]
2791 fn test_cursor_len() {
2792 assert_eq!(cursor_len("hello"), 5);
2793 assert_eq!(cursor_len("{red|world}"), 6); assert_eq!(cursor_len(r"hel\{lo{red|world}"), 12); assert_eq!(cursor_len(r"\\\{"), 2); assert_eq!(cursor_len("{red|}"), 2); }
2798
2799 #[test]
2800 fn test_insert_at_visual() {
2801 let (new, pos) = insert_at_visual("{red|hello}", 3, "XY");
2802 assert_eq!(new, "{red|helXYlo}");
2805 assert_eq!(pos, 5);
2806 }
2807
2808 #[test]
2809 fn test_delete_visual_range() {
2810 let new = delete_visual_range("{red|hello}", 1, 3);
2811 assert_eq!(new, "{red|hlo}");
2813 }
2814
2815 #[test]
2816 fn test_cleanup_empty_styles_removes_empty() {
2817 let (result, _) = cleanup_empty_styles("{red|}", 999);
2818 assert_eq!(result, ""); }
2820
2821 #[test]
2822 fn test_cleanup_empty_styles_keeps_if_cursor_inside() {
2823 let (result, _) = cleanup_empty_styles("{red|}", 0);
2825 assert_eq!(result, "{red|}"); }
2827
2828 #[test]
2829 fn test_cleanup_empty_styles_nonempty_kept() {
2830 let (result, _) = cleanup_empty_styles("{red|hello}", 999);
2831 assert_eq!(result, "{red|hello}");
2832 }
2833
2834 #[test]
2835 fn test_cleanup_preserves_text_after_empty() {
2836 let raw = "something{red|}more";
2839 let (result, _) = cleanup_empty_styles(raw, 0); assert_eq!(result, "somethingmore");
2842 }
2843
2844 #[test]
2845 fn test_cleanup_keeps_empty_when_cursor_at_content() {
2846 let raw = "something{red|}more";
2847 let (result, _) = cleanup_empty_styles(raw, 9);
2849 assert_eq!(result, "something{red|}more");
2850 }
2851
2852 #[test]
2853 fn test_cleanup_nonempty_nested_visual_counting() {
2854 let raw = "{color=red|hello}world";
2857 let (result, new_cursor) = cleanup_empty_styles(raw, 11);
2860 assert_eq!(result, raw);
2861 assert_eq!(new_cursor, 11);
2862
2863 let raw2 = "{color=red|hello}{blue|}";
2865 let (result2, new_cursor2) = cleanup_empty_styles(raw2, 8);
2868 assert_eq!(result2, "{color=red|hello}");
2869 assert_eq!(new_cursor2, 6);
2871 }
2872
2873 #[test]
2874 fn test_cleanup_deeply_nested_nonempty() {
2875 let raw = "aaa{r|{g|{b|xyz}}}end";
2877 let vl = cursor_len(raw);
2879 assert_eq!(vl, 12);
2880 let (result, new_cursor) = cleanup_empty_styles(raw, vl);
2881 assert_eq!(result, raw);
2882 assert_eq!(new_cursor, vl);
2883 }
2884
2885 #[test]
2886 fn test_word_boundary_visual_nested_tags() {
2887 let raw = "aaa{r|{r|{r|bbb}}} ccc";
2891 let vl = cursor_len(raw);
2893 assert_eq!(vl, 13);
2894
2895 let result = find_word_boundary_left_visual(raw, vl);
2897 assert!(result <= vl, "word boundary should not exceed visual len");
2898
2899 for v in 0..=vl {
2901 let _ = find_word_boundary_left_visual(raw, v);
2902 let _ = find_word_boundary_right_visual(raw, v);
2903 }
2904 }
2905
2906 #[test]
2907 fn test_word_boundary_visual_after_cleanup() {
2908 let raw = "aaa{color=red|{color=red|bbb}}} ccc";
2912 let vl = cursor_len(raw);
2913 let (cleaned, cursor) = cleanup_empty_styles(raw, vl);
2915 let cleaned_vl = cursor_len(&cleaned);
2916 assert!(cursor <= cleaned_vl,
2917 "cursor {} should be <= cursor_len {} after cleanup",
2918 cursor, cleaned_vl);
2919
2920 let _ = find_word_boundary_left_visual(&cleaned, cursor);
2922 }
2923
2924 #[test]
2925 fn test_roundtrip_visual_raw() {
2926 let raw = r"hel\{lo{red|world}";
2927 for v in 0..=12 {
2929 let r = cursor_to_raw(raw, v);
2930 let v2 = raw_to_cursor(raw, r);
2931 assert_eq!(v, v2, "visual {} → raw {} → visual {} (expected {})", v, r, v2, v);
2932 }
2933 }
2934
2935 #[test]
2936 fn test_cursor_to_raw_for_insertion_enters_empty_tag() {
2937 let raw = "test{red|}";
2939 assert_eq!(cursor_to_raw(raw, 4), 9);
2941 assert_eq!(cursor_to_raw(raw, 5), 9);
2943 assert_eq!(cursor_to_raw(raw, 6), 10);
2945 assert_eq!(cursor_to_raw_for_insertion(raw, 4), 9);
2947 }
2948
2949 #[test]
2950 fn test_cursor_to_raw_for_insertion_nonempty_tag_not_entered() {
2951 let raw = "test{red|x}";
2953 assert_eq!(cursor_to_raw(raw, 4), 9);
2955 assert_eq!(cursor_to_raw_for_insertion(raw, 4), 9);
2956 }
2957
2958 #[test]
2959 fn test_cursor_to_raw_for_insertion_at_start() {
2960 let raw = "{red|}hello";
2962 assert_eq!(cursor_to_raw(raw, 0), 0);
2964 assert_eq!(cursor_to_raw_for_insertion(raw, 0), 5);
2966 assert_eq!(cursor_to_raw(raw, 1), 5);
2968 assert_eq!(cursor_to_raw(raw, 2), 6);
2970 }
2971
2972 #[test]
2973 fn test_insert_at_visual_enters_empty_tag() {
2974 let raw = "test{red|}";
2976 let (new, pos) = insert_at_visual(raw, 4, "X");
2977 assert_eq!(new, "test{red|X}");
2979 assert_eq!(pos, 5);
2980 }
2981
2982 #[test]
2983 fn test_insert_at_visual_empty_tag_middle() {
2984 let raw = "hello{red|}world";
2986 let (new, pos) = insert_at_visual(raw, 5, "X");
2987 assert_eq!(new, "hello{red|X}world");
2988 assert_eq!(pos, 6);
2989 }
2990
2991 #[test]
2992 fn test_user_scenario_backspace_to_empty_tag() {
2993 let raw = r"hel\{lo{red|world}";
2999 let after_delete = delete_visual_range(raw, 6, 11);
3001 assert_eq!(after_delete, r"hel\{lo{red|}");
3002
3003 let (cleaned, _) = cleanup_empty_styles(&after_delete, 6);
3006 assert_eq!(cleaned, r"hel\{lo{red|}");
3007
3008 let (after_insert, new_pos) = insert_at_visual(&cleaned, 6, "X");
3010 assert_eq!(after_insert, r"hel\{lo{red|X}");
3012 assert_eq!(new_pos, 7);
3013
3014 let empty_again = delete_visual_range(&after_insert, 6, 7);
3017 assert_eq!(empty_again, r"hel\{lo{red|}");
3018 let (after_move, _) = cleanup_empty_styles(&empty_again, 5);
3019 assert_eq!(after_move, r"hel\{lo");
3020 }
3021 }
3022}
3023
3024pub fn compute_char_x_positions(
3028 display_text: &str,
3029 font_asset: Option<&'static crate::renderer::FontAsset>,
3030 font_size: u16,
3031 measure_fn: &dyn Fn(&str, &crate::text::TextConfig) -> crate::math::Dimensions,
3032) -> Vec<f32> {
3033 let char_count = display_text.chars().count();
3034 let mut positions = Vec::with_capacity(char_count + 1);
3035 positions.push(0.0);
3036
3037 let config = crate::text::TextConfig {
3038 font_asset,
3039 font_size,
3040 ..Default::default()
3041 };
3042
3043 #[cfg(feature = "text-styling")]
3044 {
3045 let chars: Vec<char> = display_text.chars().collect();
3050 let mut in_tag_header = false;
3051 let mut escaped = false;
3052 let mut last_width = 0.0f32;
3053
3054 for i in 0..char_count {
3055 let ch = chars[i];
3056 if escaped {
3057 escaped = false;
3058 let byte_end = char_index_to_byte(display_text, i + 1);
3060 let substr = &display_text[..byte_end];
3061 let dims = measure_fn(substr, &config);
3062 last_width = dims.width;
3063 positions.push(last_width);
3064 continue;
3065 }
3066 match ch {
3067 '\\' => {
3068 escaped = true;
3069 positions.push(last_width);
3071 }
3072 '{' => {
3073 in_tag_header = true;
3074 positions.push(last_width);
3075 }
3076 '|' if in_tag_header => {
3077 in_tag_header = false;
3078 positions.push(last_width);
3079 }
3080 '}' => {
3081 positions.push(last_width);
3083 }
3084 _ if in_tag_header => {
3085 positions.push(last_width);
3087 }
3088 _ => {
3089 let byte_end = char_index_to_byte(display_text, i + 1);
3091 let substr = &display_text[..byte_end];
3092 let dims = measure_fn(substr, &config);
3093 last_width = dims.width;
3094 positions.push(last_width);
3095 }
3096 }
3097 }
3098 }
3099
3100 #[cfg(not(feature = "text-styling"))]
3101 {
3102 for i in 1..=char_count {
3103 let byte_end = char_index_to_byte(display_text, i);
3104 let substr = &display_text[..byte_end];
3105 let dims = measure_fn(substr, &config);
3106 positions.push(dims.width);
3107 }
3108 }
3109
3110 positions
3111}
3112
3113#[cfg(test)]
3114mod tests {
3115 use super::*;
3116
3117 #[test]
3118 fn test_char_index_to_byte_ascii() {
3119 let s = "Hello";
3120 assert_eq!(char_index_to_byte(s, 0), 0);
3121 assert_eq!(char_index_to_byte(s, 3), 3);
3122 assert_eq!(char_index_to_byte(s, 5), 5);
3123 }
3124
3125 #[test]
3126 fn test_char_index_to_byte_unicode() {
3127 let s = "Héllo";
3128 assert_eq!(char_index_to_byte(s, 0), 0);
3129 assert_eq!(char_index_to_byte(s, 1), 1); assert_eq!(char_index_to_byte(s, 2), 3); assert_eq!(char_index_to_byte(s, 5), 6);
3132 }
3133
3134 #[test]
3135 fn test_word_boundary_left() {
3136 assert_eq!(find_word_boundary_left("hello world", 11), 6);
3137 assert_eq!(find_word_boundary_left("hello world", 6), 0); assert_eq!(find_word_boundary_left("hello world", 5), 0);
3139 assert_eq!(find_word_boundary_left("hello", 0), 0);
3140 }
3141
3142 #[test]
3143 fn test_word_boundary_right() {
3144 assert_eq!(find_word_boundary_right("hello world", 0), 5); assert_eq!(find_word_boundary_right("hello world", 5), 11); assert_eq!(find_word_boundary_right("hello world", 6), 11);
3147 assert_eq!(find_word_boundary_right("hello", 5), 5);
3148 }
3149
3150 #[test]
3151 fn test_find_word_at() {
3152 assert_eq!(find_word_at("hello world", 2), (0, 5));
3153 assert_eq!(find_word_at("hello world", 7), (6, 11));
3154 assert_eq!(find_word_at("hello world", 5), (5, 6)); }
3156
3157 #[test]
3158 fn test_insert_text() {
3159 let mut state = TextEditState::default();
3160 state.insert_text("Hello", None);
3161 assert_eq!(state.text, "Hello");
3162 assert_eq!(state.cursor_pos, 5);
3163
3164 state.cursor_pos = 5;
3165 state.insert_text(" World", None);
3166 assert_eq!(state.text, "Hello World");
3167 assert_eq!(state.cursor_pos, 11);
3168 }
3169
3170 #[test]
3171 fn test_insert_text_max_length() {
3172 let mut state = TextEditState::default();
3173 state.insert_text("Hello World", Some(5));
3174 assert_eq!(state.text, "Hello");
3175 assert_eq!(state.cursor_pos, 5);
3176
3177 state.insert_text("!", Some(5));
3179 assert_eq!(state.text, "Hello");
3180 }
3181
3182 #[test]
3183 fn test_backspace() {
3184 let mut state = TextEditState::default();
3185 state.text = "Hello".to_string();
3186 state.cursor_pos = 5;
3187 state.backspace();
3188 assert_eq!(state.text, "Hell");
3189 assert_eq!(state.cursor_pos, 4);
3190 }
3191
3192 #[test]
3193 fn test_delete_forward() {
3194 let mut state = TextEditState::default();
3195 state.text = "Hello".to_string();
3196 state.cursor_pos = 0;
3197 state.delete_forward();
3198 assert_eq!(state.text, "ello");
3199 assert_eq!(state.cursor_pos, 0);
3200 }
3201
3202 #[test]
3203 fn test_selection_delete() {
3204 let mut state = TextEditState::default();
3205 state.text = "Hello World".to_string();
3206 state.selection_anchor = Some(0);
3207 state.cursor_pos = 5;
3208 state.delete_selection();
3209 assert_eq!(state.text, " World");
3210 assert_eq!(state.cursor_pos, 0);
3211 assert!(state.selection_anchor.is_none());
3212 }
3213
3214 #[test]
3215 fn test_select_all() {
3216 let mut state = TextEditState::default();
3217 state.text = "Hello".to_string();
3218 state.cursor_pos = 2;
3219 state.select_all();
3220 assert_eq!(state.selection_anchor, Some(0));
3221 assert_eq!(state.cursor_pos, 5);
3222 }
3223
3224 #[test]
3225 fn test_move_left_right() {
3226 let mut state = TextEditState::default();
3227 state.text = "AB".to_string();
3228 state.cursor_pos = 1;
3229
3230 state.move_left(false);
3231 assert_eq!(state.cursor_pos, 0);
3232
3233 state.move_right(false);
3234 assert_eq!(state.cursor_pos, 1);
3235 }
3236
3237 #[test]
3238 fn test_move_with_shift_creates_selection() {
3239 let mut state = TextEditState::default();
3240 state.text = "Hello".to_string();
3241 state.cursor_pos = 2;
3242
3243 state.move_right(true);
3244 assert_eq!(state.cursor_pos, 3);
3245 assert_eq!(state.selection_anchor, Some(2));
3246
3247 state.move_right(true);
3248 assert_eq!(state.cursor_pos, 4);
3249 assert_eq!(state.selection_anchor, Some(2));
3250 }
3251
3252 #[test]
3253 fn test_display_text_normal() {
3254 assert_eq!(display_text("Hello", "Placeholder", false), "Hello");
3255 }
3256
3257 #[test]
3258 fn test_display_text_empty() {
3259 assert_eq!(display_text("", "Placeholder", false), "Placeholder");
3260 }
3261
3262 #[test]
3263 fn test_display_text_password() {
3264 assert_eq!(display_text("pass", "Placeholder", true), "••••");
3265 }
3266
3267 #[test]
3268 fn test_nearest_char_boundary() {
3269 let positions = vec![0.0, 10.0, 20.0, 30.0];
3270 assert_eq!(find_nearest_char_boundary(4.0, &positions), 0);
3271 assert_eq!(find_nearest_char_boundary(6.0, &positions), 1);
3272 assert_eq!(find_nearest_char_boundary(15.0, &positions), 1); assert_eq!(find_nearest_char_boundary(25.0, &positions), 2);
3274 assert_eq!(find_nearest_char_boundary(100.0, &positions), 3);
3275 }
3276
3277 #[test]
3278 fn test_ensure_cursor_visible() {
3279 let mut state = TextEditState::default();
3280 state.scroll_offset = 0.0;
3281
3282 state.ensure_cursor_visible(150.0, 100.0);
3284 assert_eq!(state.scroll_offset, 50.0);
3285
3286 state.ensure_cursor_visible(30.0, 100.0);
3288 assert_eq!(state.scroll_offset, 30.0);
3289 }
3290
3291 #[test]
3292 fn test_backspace_word() {
3293 let mut state = TextEditState::default();
3294 state.text = "hello world".to_string();
3295 state.cursor_pos = 11;
3296 state.backspace_word();
3297 assert_eq!(state.text, "hello ");
3298 assert_eq!(state.cursor_pos, 6);
3299 }
3300
3301 #[test]
3302 fn test_delete_word_forward() {
3303 let mut state = TextEditState::default();
3304 state.text = "hello world".to_string();
3305 state.cursor_pos = 0;
3306 state.delete_word_forward();
3307 assert_eq!(state.text, "world");
3308 assert_eq!(state.cursor_pos, 0);
3309 }
3310
3311 #[test]
3314 fn test_line_start_char_pos() {
3315 assert_eq!(line_start_char_pos("hello\nworld", 0), 0);
3316 assert_eq!(line_start_char_pos("hello\nworld", 3), 0);
3317 assert_eq!(line_start_char_pos("hello\nworld", 5), 0);
3318 assert_eq!(line_start_char_pos("hello\nworld", 6), 6); assert_eq!(line_start_char_pos("hello\nworld", 9), 6);
3320 }
3321
3322 #[test]
3323 fn test_line_end_char_pos() {
3324 assert_eq!(line_end_char_pos("hello\nworld", 0), 5);
3325 assert_eq!(line_end_char_pos("hello\nworld", 3), 5);
3326 assert_eq!(line_end_char_pos("hello\nworld", 6), 11);
3327 assert_eq!(line_end_char_pos("hello\nworld", 9), 11);
3328 }
3329
3330 #[test]
3331 fn test_line_and_column() {
3332 assert_eq!(line_and_column("hello\nworld", 0), (0, 0));
3333 assert_eq!(line_and_column("hello\nworld", 3), (0, 3));
3334 assert_eq!(line_and_column("hello\nworld", 5), (0, 5)); assert_eq!(line_and_column("hello\nworld", 6), (1, 0));
3336 assert_eq!(line_and_column("hello\nworld", 8), (1, 2));
3337 assert_eq!(line_and_column("hello\nworld", 11), (1, 5)); }
3339
3340 #[test]
3341 fn test_line_and_column_three_lines() {
3342 let text = "ab\ncd\nef";
3343 assert_eq!(line_and_column(text, 0), (0, 0));
3344 assert_eq!(line_and_column(text, 2), (0, 2)); assert_eq!(line_and_column(text, 3), (1, 0));
3346 assert_eq!(line_and_column(text, 5), (1, 2)); assert_eq!(line_and_column(text, 6), (2, 0));
3348 assert_eq!(line_and_column(text, 8), (2, 2)); }
3350
3351 #[test]
3352 fn test_char_pos_from_line_col() {
3353 assert_eq!(char_pos_from_line_col("hello\nworld", 0, 0), 0);
3354 assert_eq!(char_pos_from_line_col("hello\nworld", 0, 3), 3);
3355 assert_eq!(char_pos_from_line_col("hello\nworld", 1, 0), 6);
3356 assert_eq!(char_pos_from_line_col("hello\nworld", 1, 3), 9);
3357 assert_eq!(char_pos_from_line_col("ab\ncd", 0, 10), 2); assert_eq!(char_pos_from_line_col("ab\ncd", 1, 10), 5); }
3361
3362 #[test]
3363 fn test_split_lines() {
3364 let lines = split_lines("hello\nworld");
3365 assert_eq!(lines.len(), 2);
3366 assert_eq!(lines[0], (0, "hello"));
3367 assert_eq!(lines[1], (6, "world"));
3368
3369 let lines2 = split_lines("a\nb\nc");
3370 assert_eq!(lines2.len(), 3);
3371 assert_eq!(lines2[0], (0, "a"));
3372 assert_eq!(lines2[1], (2, "b"));
3373 assert_eq!(lines2[2], (4, "c"));
3374
3375 let lines3 = split_lines("no newlines");
3376 assert_eq!(lines3.len(), 1);
3377 assert_eq!(lines3[0], (0, "no newlines"));
3378 }
3379
3380 #[test]
3381 fn test_split_lines_trailing_newline() {
3382 let lines = split_lines("hello\n");
3383 assert_eq!(lines.len(), 2);
3384 assert_eq!(lines[0], (0, "hello"));
3385 assert_eq!(lines[1], (6, ""));
3386 }
3387
3388 #[test]
3389 fn test_move_up_down() {
3390 let mut state = TextEditState::default();
3391 state.text = "hello\nworld".to_string();
3392 state.cursor_pos = 8; state.move_up(false);
3395 assert_eq!(state.cursor_pos, 2); state.move_down(false);
3398 assert_eq!(state.cursor_pos, 8); }
3400
3401 #[test]
3402 fn test_move_up_clamps_column() {
3403 let mut state = TextEditState::default();
3404 state.text = "ab\nhello".to_string();
3405 state.cursor_pos = 7; state.move_up(false);
3408 assert_eq!(state.cursor_pos, 2); }
3410
3411 #[test]
3412 fn test_move_up_from_first_line() {
3413 let mut state = TextEditState::default();
3414 state.text = "hello\nworld".to_string();
3415 state.cursor_pos = 3;
3416
3417 state.move_up(false);
3418 assert_eq!(state.cursor_pos, 0); }
3420
3421 #[test]
3422 fn test_move_down_from_last_line() {
3423 let mut state = TextEditState::default();
3424 state.text = "hello\nworld".to_string();
3425 state.cursor_pos = 8;
3426
3427 state.move_down(false);
3428 assert_eq!(state.cursor_pos, 11); }
3430
3431 #[test]
3432 fn test_move_line_home_end() {
3433 let mut state = TextEditState::default();
3434 state.text = "hello\nworld".to_string();
3435 state.cursor_pos = 8; state.move_line_home(false);
3438 assert_eq!(state.cursor_pos, 6); state.move_line_end(false);
3441 assert_eq!(state.cursor_pos, 11); }
3443
3444 #[test]
3445 fn test_move_up_with_shift_selects() {
3446 let mut state = TextEditState::default();
3447 state.text = "hello\nworld".to_string();
3448 state.cursor_pos = 8;
3449
3450 state.move_up(true);
3451 assert_eq!(state.cursor_pos, 2);
3452 assert_eq!(state.selection_anchor, Some(8));
3453 }
3454
3455 #[test]
3456 fn test_ensure_cursor_visible_vertical() {
3457 let mut state = TextEditState::default();
3458 state.scroll_offset_y = 0.0;
3459
3460 state.ensure_cursor_visible_vertical(5, 20.0, 60.0);
3463 assert_eq!(state.scroll_offset_y, 60.0); state.ensure_cursor_visible_vertical(1, 20.0, 60.0);
3467 assert_eq!(state.scroll_offset_y, 20.0);
3468 }
3469
3470 fn fixed_measure(text: &str, _config: &crate::text::TextConfig) -> crate::math::Dimensions {
3474 crate::math::Dimensions {
3475 width: text.chars().count() as f32 * 10.0,
3476 height: 20.0,
3477 }
3478 }
3479
3480 #[test]
3481 fn test_wrap_lines_no_wrap_needed() {
3482 let lines = wrap_lines("hello", 100.0, None, 16, &fixed_measure);
3483 assert_eq!(lines.len(), 1);
3484 assert_eq!(lines[0].text, "hello");
3485 assert_eq!(lines[0].global_char_start, 0);
3486 assert_eq!(lines[0].char_count, 5);
3487 }
3488
3489 #[test]
3490 fn test_wrap_lines_hard_break() {
3491 let lines = wrap_lines("ab\ncd", 100.0, None, 16, &fixed_measure);
3492 assert_eq!(lines.len(), 2);
3493 assert_eq!(lines[0].text, "ab");
3494 assert_eq!(lines[0].global_char_start, 0);
3495 assert_eq!(lines[1].text, "cd");
3496 assert_eq!(lines[1].global_char_start, 3); }
3498
3499 #[test]
3500 fn test_wrap_lines_word_wrap() {
3501 let lines = wrap_lines("hello world", 60.0, None, 16, &fixed_measure);
3504 assert_eq!(lines.len(), 2);
3505 assert_eq!(lines[0].text, "hello ");
3506 assert_eq!(lines[0].global_char_start, 0);
3507 assert_eq!(lines[0].char_count, 6);
3508 assert_eq!(lines[1].text, "world");
3509 assert_eq!(lines[1].global_char_start, 6);
3510 assert_eq!(lines[1].char_count, 5);
3511 }
3512
3513 #[test]
3514 fn test_wrap_lines_char_level_break() {
3515 let lines = wrap_lines("abcdefghij", 50.0, None, 16, &fixed_measure);
3518 assert_eq!(lines.len(), 2);
3519 assert_eq!(lines[0].text, "abcde");
3520 assert_eq!(lines[0].char_count, 5);
3521 assert_eq!(lines[1].text, "fghij");
3522 assert_eq!(lines[1].global_char_start, 5);
3523 }
3524
3525 #[test]
3526 fn test_cursor_to_visual_pos_simple() {
3527 let lines = vec![
3528 VisualLine { text: "hello ".to_string(), global_char_start: 0, char_count: 6 },
3529 VisualLine { text: "world".to_string(), global_char_start: 6, char_count: 5 },
3530 ];
3531 assert_eq!(cursor_to_visual_pos(&lines, 0), (0, 0));
3532 assert_eq!(cursor_to_visual_pos(&lines, 3), (0, 3));
3533 assert_eq!(cursor_to_visual_pos(&lines, 6), (1, 0)); assert_eq!(cursor_to_visual_pos(&lines, 8), (1, 2));
3535 assert_eq!(cursor_to_visual_pos(&lines, 11), (1, 5));
3536 }
3537
3538 #[test]
3539 fn test_cursor_to_visual_pos_hard_break() {
3540 let lines = vec![
3542 VisualLine { text: "ab".to_string(), global_char_start: 0, char_count: 2 },
3543 VisualLine { text: "cd".to_string(), global_char_start: 3, char_count: 2 },
3544 ];
3545 assert_eq!(cursor_to_visual_pos(&lines, 2), (0, 2)); assert_eq!(cursor_to_visual_pos(&lines, 3), (1, 0)); }
3548
3549 #[test]
3550 fn test_visual_move_up_down() {
3551 let lines = vec![
3552 VisualLine { text: "hello ".to_string(), global_char_start: 0, char_count: 6 },
3553 VisualLine { text: "world".to_string(), global_char_start: 6, char_count: 5 },
3554 ];
3555 assert_eq!(visual_move_up(&lines, 8), 2);
3557 assert_eq!(visual_move_down(&lines, 2, 11), 8);
3559 }
3560
3561 #[test]
3562 fn test_visual_line_home_end() {
3563 let lines = vec![
3564 VisualLine { text: "hello ".to_string(), global_char_start: 0, char_count: 6 },
3565 VisualLine { text: "world".to_string(), global_char_start: 6, char_count: 5 },
3566 ];
3567 assert_eq!(visual_line_home(&lines, 8), 6);
3569 assert_eq!(visual_line_end(&lines, 8), 11);
3570 assert_eq!(visual_line_home(&lines, 3), 0);
3572 assert_eq!(visual_line_end(&lines, 3), 6);
3573 }
3574
3575 #[test]
3576 fn test_undo_basic() {
3577 let mut state = TextEditState::default();
3578 state.text = "hello".to_string();
3579 state.cursor_pos = 5;
3580
3581 state.push_undo(UndoActionKind::Paste);
3583 state.insert_text(" world", None);
3584 assert_eq!(state.text, "hello world");
3585
3586 assert!(state.undo());
3588 assert_eq!(state.text, "hello");
3589 assert_eq!(state.cursor_pos, 5);
3590
3591 assert!(state.redo());
3593 assert_eq!(state.text, "hello world");
3594 assert_eq!(state.cursor_pos, 11);
3595 }
3596
3597 #[test]
3598 fn test_undo_grouping_insert_char() {
3599 let mut state = TextEditState::default();
3600
3601 state.push_undo(UndoActionKind::InsertChar);
3603 state.insert_text("a", None);
3604 state.push_undo(UndoActionKind::InsertChar);
3605 state.insert_text("b", None);
3606 state.push_undo(UndoActionKind::InsertChar);
3607 state.insert_text("c", None);
3608 assert_eq!(state.text, "abc");
3609
3610 assert!(state.undo());
3612 assert_eq!(state.text, "");
3613 assert_eq!(state.cursor_pos, 0);
3614
3615 assert!(!state.undo());
3617 }
3618
3619 #[test]
3620 fn test_undo_grouping_backspace() {
3621 let mut state = TextEditState::default();
3622 state.text = "hello".to_string();
3623 state.cursor_pos = 5;
3624
3625 state.push_undo(UndoActionKind::Backspace);
3627 state.backspace();
3628 state.push_undo(UndoActionKind::Backspace);
3629 state.backspace();
3630 state.push_undo(UndoActionKind::Backspace);
3631 state.backspace();
3632 assert_eq!(state.text, "he");
3633
3634 assert!(state.undo());
3636 assert_eq!(state.text, "hello");
3637 }
3638
3639 #[test]
3640 fn test_undo_different_kinds_not_grouped() {
3641 let mut state = TextEditState::default();
3642
3643 state.push_undo(UndoActionKind::InsertChar);
3645 state.insert_text("abc", None);
3646 state.push_undo(UndoActionKind::Backspace);
3647 state.backspace();
3648 assert_eq!(state.text, "ab");
3649
3650 assert!(state.undo());
3652 assert_eq!(state.text, "abc");
3653
3654 assert!(state.undo());
3656 assert_eq!(state.text, "");
3657 }
3658
3659 #[test]
3660 fn test_redo_cleared_on_new_edit() {
3661 let mut state = TextEditState::default();
3662
3663 state.push_undo(UndoActionKind::Paste);
3664 state.insert_text("hello", None);
3665 state.undo();
3666 assert_eq!(state.text, "");
3667 assert!(!state.redo_stack.is_empty());
3668
3669 state.push_undo(UndoActionKind::Paste);
3671 state.insert_text("world", None);
3672 assert!(state.redo_stack.is_empty());
3673 }
3674
3675 #[test]
3676 fn test_undo_empty_stack() {
3677 let mut state = TextEditState::default();
3678 assert!(!state.undo());
3679 assert!(!state.redo());
3680 }
3681
3682 #[cfg(feature = "text-styling")]
3683 fn make_no_styles_state(raw: &str) -> TextEditState {
3684 let mut s = TextEditState::default();
3685 s.text = raw.to_string();
3686 s.no_styles_movement = true;
3687 s.cursor_pos = 0;
3689 s
3690 }
3691
3692 #[test]
3693 #[cfg(feature = "text-styling")]
3694 fn test_content_to_cursor_no_structural_basic() {
3695 use crate::text_input::styling::content_to_cursor;
3696 assert_eq!(content_to_cursor("a{red|}b", 0, true), 0); assert_eq!(content_to_cursor("a{red|}b", 1, true), 3); assert_eq!(content_to_cursor("a{red|}b", 2, true), 4); }
3701
3702 #[test]
3703 #[cfg(feature = "text-styling")]
3704 fn test_content_to_cursor_no_structural_nested() {
3705 use crate::text_input::styling::content_to_cursor;
3706 assert_eq!(content_to_cursor("a{red|b}{blue|c}", 0, true), 0);
3708 assert_eq!(content_to_cursor("a{red|b}{blue|c}", 1, true), 1); assert_eq!(content_to_cursor("a{red|b}{blue|c}", 2, true), 3); assert_eq!(content_to_cursor("a{red|b}{blue|c}", 3, true), 5); }
3712
3713 #[test]
3714 #[cfg(feature = "text-styling")]
3715 fn test_delete_content_range() {
3716 use crate::text_input::styling::delete_content_range;
3717 assert_eq!(delete_content_range("a{red|b}c", 1, 2), "a{red|}c");
3719 assert_eq!(delete_content_range("a{red|b}c", 0, 1), "{red|b}c");
3721 assert_eq!(delete_content_range("a{red|b}c", 0, 3), "{red|}");
3723 assert_eq!(delete_content_range("abc", 1, 1), "abc");
3725 }
3726
3727 #[test]
3728 #[cfg(feature = "text-styling")]
3729 fn test_no_styles_move_right() {
3730 let mut s = make_no_styles_state("a{red|}b");
3731 s.move_right_styled(false);
3735 assert_eq!(s.text, "ab");
3736 assert_eq!(s.cursor_pos, 1);
3737 s.move_right_styled(false);
3739 assert_eq!(s.cursor_pos, 2);
3740 assert_eq!(styling::cursor_to_content(&s.text, s.cursor_pos), 2);
3741 }
3742
3743 #[test]
3744 #[cfg(feature = "text-styling")]
3745 fn test_no_styles_move_left() {
3746 let mut s = make_no_styles_state("a{red|}b");
3747 s.cursor_pos = styling::content_to_cursor(&s.text, 2, true);
3750 s.move_end_styled(false);
3752 assert_eq!(s.text, "ab");
3753 assert_eq!(s.cursor_pos, 2);
3754 s.move_left_styled(false);
3756 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3757 assert_eq!(cp, 1);
3758 s.move_left_styled(false);
3760 assert_eq!(s.cursor_pos, 0);
3761 }
3762
3763 #[test]
3764 #[cfg(feature = "text-styling")]
3765 fn test_no_styles_move_left_skips_closing_brace() {
3766 let mut s = make_no_styles_state("a{red|b}c");
3767 s.cursor_pos = styling::content_to_cursor(&s.text, 2, true);
3769 assert_eq!(s.cursor_pos, 3);
3771 s.move_left_styled(false);
3773 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3774 assert_eq!(cp, 1);
3775 }
3776
3777 #[test]
3778 #[cfg(feature = "text-styling")]
3779 fn test_no_styles_backspace() {
3780 let mut s = make_no_styles_state("a{red|b}c");
3781 s.cursor_pos = styling::content_to_cursor(&s.text, 2, true);
3783 s.backspace_styled();
3784 let stripped = styling::strip_styling(&s.text);
3786 assert_eq!(stripped, "ac");
3787 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3789 assert_eq!(cp, 1);
3790 }
3791
3792 #[test]
3793 #[cfg(feature = "text-styling")]
3794 fn test_no_styles_delete_forward() {
3795 let mut s = make_no_styles_state("{red|abc}");
3796 s.cursor_pos = styling::content_to_cursor(&s.text, 1, true);
3798 s.delete_forward_styled();
3799 let stripped = styling::strip_styling(&s.text);
3800 assert_eq!(stripped, "ac");
3801 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3802 assert_eq!(cp, 1);
3803 }
3804
3805 #[test]
3806 #[cfg(feature = "text-styling")]
3807 fn test_no_styles_home_end() {
3808 let mut s = make_no_styles_state("{red|}hello{blue|}");
3809 s.move_home_styled(false);
3811 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3812 assert_eq!(cp, 0);
3813 s.move_end_styled(false);
3815 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3816 let content_len = styling::strip_styling(&s.text).chars().count();
3817 assert_eq!(cp, content_len);
3818 }
3819
3820 #[test]
3821 #[cfg(feature = "text-styling")]
3822 fn test_no_styles_select_all_and_delete() {
3823 let mut s = make_no_styles_state("a{red|b}c");
3824 s.select_all_styled();
3825 assert!(s.selection_anchor.is_some());
3826 s.delete_selection_styled();
3828 let stripped = styling::strip_styling(&s.text);
3829 assert!(stripped.is_empty());
3830 }
3831}