1use crate::{color::Color, elements, engine};
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 drag_select: bool,
1166 pub font_size: u16,
1168 pub text_color: Color,
1170 pub placeholder_color: Color,
1172 pub cursor_color: Color,
1174 pub selection_color: Color,
1176 pub line_height: u16,
1178 pub no_styles_movement: bool,
1180 pub font_asset: Option<&'static crate::renderer::FontAsset>,
1182 pub scrollbar: Option<engine::ScrollbarConfig>,
1184}
1185
1186impl Default for TextInputConfig {
1187 fn default() -> Self {
1188 Self {
1189 placeholder: String::new(),
1190 max_length: None,
1191 is_password: false,
1192 is_multiline: false,
1193 drag_select: false,
1194 font_size: 0,
1195 text_color: Color::rgba(255.0, 255.0, 255.0, 255.0),
1196 placeholder_color: Color::rgba(128.0, 128.0, 128.0, 255.0),
1197 cursor_color: Color::rgba(255.0, 255.0, 255.0, 255.0),
1198 selection_color: Color::rgba(69.0, 130.0, 181.0, 128.0),
1199 line_height: 0,
1200 no_styles_movement: false,
1201 font_asset: None,
1202 scrollbar: None,
1203 }
1204 }
1205}
1206
1207pub struct TextInputBuilder {
1209 pub(crate) config: TextInputConfig,
1210 pub(crate) on_changed_fn: Option<Box<dyn FnMut(&str) + 'static>>,
1211 pub(crate) on_submit_fn: Option<Box<dyn FnMut(&str) + 'static>>,
1212}
1213
1214impl TextInputBuilder {
1215 pub(crate) fn new() -> Self {
1216 Self {
1217 config: TextInputConfig::default(),
1218 on_changed_fn: None,
1219 on_submit_fn: None,
1220 }
1221 }
1222
1223 #[inline]
1225 pub fn placeholder(&mut self, text: &str) -> &mut Self {
1226 self.config.placeholder = text.to_string();
1227 self
1228 }
1229
1230 #[inline]
1232 pub fn max_length(&mut self, len: usize) -> &mut Self {
1233 self.config.max_length = Some(len);
1234 self
1235 }
1236
1237 #[inline]
1239 pub fn password(&mut self) -> &mut Self {
1240 self.config.is_password = true;
1241 self
1242 }
1243
1244 #[inline]
1246 pub fn multiline(&mut self) -> &mut Self {
1247 self.config.is_multiline = true;
1248 self
1249 }
1250
1251 #[inline]
1253 pub fn drag_select(&mut self) -> &mut Self {
1254 self.config.drag_select = true;
1255 self
1256 }
1257
1258 #[inline]
1262 pub fn font(&mut self, asset: &'static crate::renderer::FontAsset) -> &mut Self {
1263 self.config.font_asset = Some(asset);
1264 self
1265 }
1266
1267 #[inline]
1269 pub fn font_size(&mut self, size: u16) -> &mut Self {
1270 self.config.font_size = size;
1271 self
1272 }
1273
1274 #[inline]
1276 pub fn text_color(&mut self, color: impl Into<Color>) -> &mut Self {
1277 self.config.text_color = color.into();
1278 self
1279 }
1280
1281 #[inline]
1283 pub fn placeholder_color(&mut self, color: impl Into<Color>) -> &mut Self {
1284 self.config.placeholder_color = color.into();
1285 self
1286 }
1287
1288 #[inline]
1290 pub fn cursor_color(&mut self, color: impl Into<Color>) -> &mut Self {
1291 self.config.cursor_color = color.into();
1292 self
1293 }
1294
1295 #[inline]
1297 pub fn selection_color(&mut self, color: impl Into<Color>) -> &mut Self {
1298 self.config.selection_color = color.into();
1299 self
1300 }
1301
1302 #[inline]
1308 pub fn line_height(&mut self, height: u16) -> &mut Self {
1309 self.config.line_height = height;
1310 self
1311 }
1312
1313 #[inline]
1315 pub fn scrollbar(
1316 &mut self,
1317 f: impl for<'a> FnOnce(&'a mut elements::ScrollbarBuilder) -> &'a mut elements::ScrollbarBuilder,
1318 ) -> &mut Self {
1319 let mut builder = elements::ScrollbarBuilder {
1320 config: self.config.scrollbar.unwrap_or_default(),
1321 };
1322 f(&mut builder);
1323 self.config.scrollbar = Some(builder.config);
1324 self
1325 }
1326
1327 #[inline]
1333 pub fn no_styles_movement(&mut self) -> &mut Self {
1334 self.config.no_styles_movement = true;
1335 self
1336 }
1337
1338 #[inline]
1340 pub fn on_changed<F>(&mut self, callback: F) -> &mut Self
1341 where
1342 F: FnMut(&str) + 'static,
1343 {
1344 self.on_changed_fn = Some(Box::new(callback));
1345 self
1346 }
1347
1348 #[inline]
1350 pub fn on_submit<F>(&mut self, callback: F) -> &mut Self
1351 where
1352 F: FnMut(&str) + 'static,
1353 {
1354 self.on_submit_fn = Some(Box::new(callback));
1355 self
1356 }
1357}
1358
1359pub fn char_index_to_byte(s: &str, char_idx: usize) -> usize {
1361 s.char_indices()
1362 .nth(char_idx)
1363 .map(|(byte_pos, _)| byte_pos)
1364 .unwrap_or(s.len())
1365}
1366
1367pub fn line_start_char_pos(text: &str, char_pos: usize) -> usize {
1370 let chars: Vec<char> = text.chars().collect();
1371 let mut i = char_pos;
1372 while i > 0 && chars[i - 1] != '\n' {
1373 i -= 1;
1374 }
1375 i
1376}
1377
1378pub fn line_end_char_pos(text: &str, char_pos: usize) -> usize {
1381 let chars: Vec<char> = text.chars().collect();
1382 let len = chars.len();
1383 let mut i = char_pos;
1384 while i < len && chars[i] != '\n' {
1385 i += 1;
1386 }
1387 i
1388}
1389
1390pub fn line_and_column(text: &str, char_pos: usize) -> (usize, usize) {
1393 let mut line = 0;
1394 let mut col = 0;
1395 for (i, ch) in text.chars().enumerate() {
1396 if i == char_pos {
1397 return (line, col);
1398 }
1399 if ch == '\n' {
1400 line += 1;
1401 col = 0;
1402 } else {
1403 col += 1;
1404 }
1405 }
1406 (line, col)
1407}
1408
1409pub fn char_pos_from_line_col(text: &str, target_line: usize, target_col: usize) -> usize {
1412 let mut line = 0;
1413 let mut col = 0;
1414 for (i, ch) in text.chars().enumerate() {
1415 if line == target_line && col == target_col {
1416 return i;
1417 }
1418 if ch == '\n' {
1419 if line == target_line {
1420 return i;
1422 }
1423 line += 1;
1424 col = 0;
1425 } else {
1426 col += 1;
1427 }
1428 }
1429 text.chars().count()
1431}
1432
1433pub fn split_lines(text: &str) -> Vec<(usize, &str)> {
1436 let mut result = Vec::new();
1437 let mut char_start = 0;
1438 let mut byte_start = 0;
1439 for (byte_idx, ch) in text.char_indices() {
1440 if ch == '\n' {
1441 result.push((char_start, &text[byte_start..byte_idx]));
1442 char_start += text[byte_start..byte_idx].chars().count() + 1; byte_start = byte_idx + 1; }
1445 }
1446 result.push((char_start, &text[byte_start..]));
1448 result
1449}
1450
1451#[derive(Debug, Clone)]
1453pub struct VisualLine {
1454 pub text: String,
1456 pub global_char_start: usize,
1458 pub char_count: usize,
1460}
1461
1462pub fn wrap_lines(
1466 text: &str,
1467 max_width: f32,
1468 font_asset: Option<&'static crate::renderer::FontAsset>,
1469 font_size: u16,
1470 measure_fn: &dyn Fn(&str, &crate::text::TextConfig) -> crate::math::Dimensions,
1471) -> Vec<VisualLine> {
1472 let config = crate::text::TextConfig {
1473 font_asset,
1474 font_size,
1475 ..Default::default()
1476 };
1477
1478 let hard_lines = split_lines(text);
1479 let mut result = Vec::new();
1480
1481 for (global_start, line_text) in hard_lines {
1482 if line_text.is_empty() {
1483 result.push(VisualLine {
1484 text: String::new(),
1485 global_char_start: global_start,
1486 char_count: 0,
1487 });
1488 continue;
1489 }
1490
1491 if max_width <= 0.0 {
1492 result.push(VisualLine {
1494 text: line_text.to_string(),
1495 global_char_start: global_start,
1496 char_count: line_text.chars().count(),
1497 });
1498 continue;
1499 }
1500
1501 let full_width = measure_fn(line_text, &config).width;
1503 if full_width <= max_width {
1504 result.push(VisualLine {
1505 text: line_text.to_string(),
1506 global_char_start: global_start,
1507 char_count: line_text.chars().count(),
1508 });
1509 continue;
1510 }
1511
1512 let chars: Vec<char> = line_text.chars().collect();
1514 let total_chars = chars.len();
1515 let mut line_char_start = 0; while line_char_start < total_chars {
1518 let mut fit_count = 0;
1520
1521 #[cfg(feature = "text-styling")]
1522 {
1523 let mut in_tag_hdr = false;
1527 let mut escaped = false;
1528 for i in 1..=(total_chars - line_char_start) {
1529 let ch = chars[line_char_start + i - 1];
1530 if escaped {
1531 escaped = false;
1532 let substr: String = chars[line_char_start..line_char_start + i].iter().collect();
1534 let w = measure_fn(&substr, &config).width;
1535 if w > max_width { break; }
1536 fit_count = i;
1537 continue;
1538 }
1539 match ch {
1540 '\\' => { escaped = true; }
1541 '{' => { in_tag_hdr = true; fit_count = i; }
1542 '|' if in_tag_hdr => { in_tag_hdr = false; fit_count = i; }
1543 '}' => { fit_count = i; }
1544 _ if in_tag_hdr => { fit_count = i; }
1545 _ => {
1546 let substr: String = chars[line_char_start..line_char_start + i].iter().collect();
1548 let w = measure_fn(&substr, &config).width;
1549 if w > max_width { break; }
1550 fit_count = i;
1551 }
1552 }
1553 }
1554 }
1555
1556 #[cfg(not(feature = "text-styling"))]
1557 {
1558 for i in 1..=(total_chars - line_char_start) {
1559 let substr: String = chars[line_char_start..line_char_start + i].iter().collect();
1560 let w = measure_fn(&substr, &config).width;
1561 if w > max_width {
1562 break;
1563 }
1564 fit_count = i;
1565 }
1566 }
1567
1568 if fit_count == 0 {
1569 #[cfg(feature = "text-styling")]
1573 if chars[line_char_start] == '\\' && line_char_start + 2 <= total_chars {
1574 fit_count = 2;
1575 } else {
1576 fit_count = 1;
1577 }
1578 #[cfg(not(feature = "text-styling"))]
1579 {
1580 fit_count = 1;
1581 }
1582 }
1583
1584 if line_char_start + fit_count < total_chars {
1585 let mut break_at = fit_count;
1587 let mut found_space = false;
1588 for j in (1..=fit_count).rev() {
1589 if chars[line_char_start + j - 1] == ' ' {
1590 break_at = j;
1591 found_space = true;
1592 break;
1593 }
1594 }
1595 #[allow(unused_mut)]
1597 let mut wrap_count = if found_space { break_at } else { fit_count };
1598 #[cfg(feature = "text-styling")]
1600 if wrap_count > 0
1601 && chars[line_char_start + wrap_count - 1] == '\\'
1602 && line_char_start + wrap_count < total_chars
1603 {
1604 if wrap_count > 1 {
1605 wrap_count -= 1; } else {
1607 wrap_count = 2.min(total_chars - line_char_start); }
1609 }
1610 let segment: String = chars[line_char_start..line_char_start + wrap_count].iter().collect();
1611 result.push(VisualLine {
1612 text: segment,
1613 global_char_start: global_start + line_char_start,
1614 char_count: wrap_count,
1615 });
1616 line_char_start += wrap_count;
1617 if found_space && line_char_start < total_chars && chars[line_char_start] == ' ' {
1619 }
1622 } else {
1623 let segment: String = chars[line_char_start..].iter().collect();
1625 let count = total_chars - line_char_start;
1626 result.push(VisualLine {
1627 text: segment,
1628 global_char_start: global_start + line_char_start,
1629 char_count: count,
1630 });
1631 line_char_start = total_chars;
1632 }
1633 }
1634 }
1635
1636 if result.is_empty() {
1638 result.push(VisualLine {
1639 text: String::new(),
1640 global_char_start: 0,
1641 char_count: 0,
1642 });
1643 }
1644
1645 result
1646}
1647
1648pub fn cursor_to_visual_pos(visual_lines: &[VisualLine], cursor_pos: usize) -> (usize, usize) {
1650 for (i, vl) in visual_lines.iter().enumerate() {
1651 let line_end = vl.global_char_start + vl.char_count;
1652 if cursor_pos < line_end || i == visual_lines.len() - 1 {
1653 return (i, cursor_pos.saturating_sub(vl.global_char_start));
1654 }
1655 if cursor_pos == line_end {
1659 if i + 1 < visual_lines.len() {
1661 let next = &visual_lines[i + 1];
1662 if next.global_char_start == line_end {
1663 return (i + 1, 0);
1665 }
1666 return (i, cursor_pos - vl.global_char_start);
1668 }
1669 return (i, cursor_pos - vl.global_char_start);
1670 }
1671 }
1672 (0, 0)
1673}
1674
1675pub fn visual_move_up(visual_lines: &[VisualLine], cursor_pos: usize) -> usize {
1678 let (line, col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1679 if line == 0 {
1680 return 0; }
1682 let target_line = &visual_lines[line - 1];
1683 let new_col = col.min(target_line.char_count);
1684 target_line.global_char_start + new_col
1685}
1686
1687pub fn visual_move_down(visual_lines: &[VisualLine], cursor_pos: usize, text_len: usize) -> usize {
1689 let (line, col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1690 if line >= visual_lines.len() - 1 {
1691 return text_len; }
1693 let target_line = &visual_lines[line + 1];
1694 let new_col = col.min(target_line.char_count);
1695 target_line.global_char_start + new_col
1696}
1697
1698pub fn visual_line_home(visual_lines: &[VisualLine], cursor_pos: usize) -> usize {
1700 let (line, _col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1701 visual_lines[line].global_char_start
1702}
1703
1704pub fn visual_line_end(visual_lines: &[VisualLine], cursor_pos: usize) -> usize {
1706 let (line, _col) = cursor_to_visual_pos(visual_lines, cursor_pos);
1707 visual_lines[line].global_char_start + visual_lines[line].char_count
1708}
1709
1710pub fn find_nearest_char_boundary(click_x: f32, char_x_positions: &[f32]) -> usize {
1713 if char_x_positions.is_empty() {
1714 return 0;
1715 }
1716 let mut best = 0;
1717 let mut best_dist = f32::MAX;
1718 for (i, &x) in char_x_positions.iter().enumerate() {
1719 let dist = (click_x - x).abs();
1720 if dist < best_dist {
1721 best_dist = dist;
1722 best = i;
1723 }
1724 }
1725 best
1726}
1727
1728pub fn find_word_boundary_left(text: &str, pos: usize) -> usize {
1730 if pos == 0 {
1731 return 0;
1732 }
1733 let chars: Vec<char> = text.chars().collect();
1734 let mut i = pos.min(chars.len());
1735 while i > 0 && chars[i - 1].is_whitespace() {
1737 i -= 1;
1738 }
1739 while i > 0 && !chars[i - 1].is_whitespace() {
1741 i -= 1;
1742 }
1743 i
1744}
1745
1746pub fn find_word_boundary_right(text: &str, pos: usize) -> usize {
1749 let chars: Vec<char> = text.chars().collect();
1750 let len = chars.len();
1751 if pos >= len {
1752 return len;
1753 }
1754 let mut i = pos;
1755 while i < len && chars[i].is_whitespace() {
1757 i += 1;
1758 }
1759 while i < len && !chars[i].is_whitespace() {
1761 i += 1;
1762 }
1763 i
1764}
1765
1766pub fn find_word_delete_boundary_right(text: &str, pos: usize) -> usize {
1769 let chars: Vec<char> = text.chars().collect();
1770 let len = chars.len();
1771 if pos >= len {
1772 return len;
1773 }
1774 let mut i = pos;
1775 while i < len && !chars[i].is_whitespace() {
1777 i += 1;
1778 }
1779 while i < len && chars[i].is_whitespace() {
1781 i += 1;
1782 }
1783 i
1784}
1785
1786pub fn find_word_at(text: &str, pos: usize) -> (usize, usize) {
1789 let chars: Vec<char> = text.chars().collect();
1790 let len = chars.len();
1791 if len == 0 || pos >= len {
1792 return (pos, pos);
1793 }
1794 let is_word_char = |c: char| !c.is_whitespace();
1795 if !is_word_char(chars[pos]) {
1796 let mut start = pos;
1798 while start > 0 && !is_word_char(chars[start - 1]) {
1799 start -= 1;
1800 }
1801 let mut end = pos;
1802 while end < len && !is_word_char(chars[end]) {
1803 end += 1;
1804 }
1805 return (start, end);
1806 }
1807 let mut start = pos;
1809 while start > 0 && is_word_char(chars[start - 1]) {
1810 start -= 1;
1811 }
1812 let mut end = pos;
1813 while end < len && is_word_char(chars[end]) {
1814 end += 1;
1815 }
1816 (start, end)
1817}
1818
1819pub fn display_text(text: &str, placeholder: &str, is_password: bool) -> String {
1822 if text.is_empty() {
1823 return placeholder.to_string();
1824 }
1825 if is_password {
1826 "•".repeat(text.chars().count())
1827 } else {
1828 text.to_string()
1829 }
1830}
1831
1832#[cfg(feature = "text-styling")]
1842pub mod styling {
1843 pub fn escape_char(ch: char) -> String {
1846 match ch {
1847 '{' | '}' | '|' | '\\' => format!("\\{}", ch),
1848 _ => ch.to_string(),
1849 }
1850 }
1851
1852 pub fn escape_str(s: &str) -> String {
1854 let mut result = String::with_capacity(s.len());
1855 for ch in s.chars() {
1856 match ch {
1857 '{' | '}' | '|' | '\\' => {
1858 result.push('\\');
1859 result.push(ch);
1860 }
1861 _ => result.push(ch),
1862 }
1863 }
1864 result
1865 }
1866
1867 pub fn cursor_to_raw(raw: &str, visual_pos: usize) -> usize {
1880 if visual_pos == 0 {
1881 return 0;
1882 }
1883
1884 let chars: Vec<char> = raw.chars().collect();
1885 let len = chars.len();
1886 let mut visual = 0usize;
1887 let mut raw_idx = 0usize;
1888 let mut escaped = false;
1889 let mut in_style_def = false;
1890
1891 while raw_idx < len {
1892 let c = chars[raw_idx];
1893
1894 if escaped {
1895 visual += 1;
1897 escaped = false;
1898 raw_idx += 1;
1899 if visual == visual_pos {
1900 return skip_tag_headers(&chars, raw_idx);
1901 }
1902 continue;
1903 }
1904
1905 match c {
1906 '\\' => {
1907 escaped = true;
1908 raw_idx += 1;
1909 }
1910 '{' if !in_style_def => {
1911 in_style_def = true;
1912 raw_idx += 1;
1913 }
1914 '|' if in_style_def => {
1915 in_style_def = false;
1916 if raw_idx + 1 < len && chars[raw_idx + 1] == '}' {
1918 visual += 1;
1920 raw_idx += 1; if visual == visual_pos {
1922 return raw_idx; }
1924 } else {
1926 raw_idx += 1;
1927 }
1928 }
1929 '}' if !in_style_def => {
1930 visual += 1;
1932 raw_idx += 1;
1933 if visual == visual_pos {
1934 return raw_idx;
1936 }
1937 }
1938 _ if in_style_def => {
1939 raw_idx += 1;
1940 }
1941 _ => {
1942 visual += 1;
1944 raw_idx += 1;
1945 if visual == visual_pos {
1946 return skip_tag_headers(&chars, raw_idx);
1949 }
1950 }
1951 }
1952 }
1953
1954 len
1955 }
1956
1957 fn skip_tag_headers(chars: &[char], pos: usize) -> usize {
1960 let len = chars.len();
1961 let mut p = pos;
1962 while p < len && chars[p] == '{' {
1963 let mut j = p + 1;
1964 while j < len && chars[j] != '|' && chars[j] != '}' {
1965 j += 1;
1966 }
1967 if j < len && chars[j] == '|' {
1968 p = j + 1; } else {
1970 break; }
1972 }
1973 p
1974 }
1975
1976 pub fn raw_to_cursor(raw: &str, raw_pos: usize) -> usize {
1979 let chars: Vec<char> = raw.chars().collect();
1980 let len = chars.len();
1981 let mut visual = 0usize;
1982 let mut raw_idx = 0usize;
1983 let mut escaped = false;
1984 let mut in_style_def = false;
1985
1986 while raw_idx < len && raw_idx < raw_pos {
1987 let c = chars[raw_idx];
1988
1989 if escaped {
1990 visual += 1;
1991 escaped = false;
1992 raw_idx += 1;
1993 continue;
1994 }
1995
1996 match c {
1997 '\\' => {
1998 escaped = true;
1999 raw_idx += 1;
2000 }
2001 '{' if !in_style_def => {
2002 in_style_def = true;
2003 raw_idx += 1;
2004 }
2005 '|' if in_style_def => {
2006 in_style_def = false;
2007 if raw_idx + 1 < len && chars[raw_idx + 1] == '}' {
2009 visual += 1; raw_idx += 1; } else {
2013 raw_idx += 1;
2014 }
2015 }
2016 '}' if !in_style_def => {
2017 visual += 1; raw_idx += 1;
2019 }
2020 _ if in_style_def => {
2021 raw_idx += 1;
2022 }
2023 _ => {
2024 visual += 1;
2025 raw_idx += 1;
2026 }
2027 }
2028 }
2029
2030 visual
2031 }
2032
2033 pub fn cursor_len(raw: &str) -> usize {
2036 raw_to_cursor(raw, raw.chars().count())
2037 }
2038
2039 fn enter_empty_tags_at(chars: &[char], pos: usize) -> usize {
2043 let len = chars.len();
2044 let mut p = pos;
2045 while p < len && chars[p] == '{' {
2046 let mut j = p + 1;
2048 while j < len && chars[j] != '|' && chars[j] != '}' {
2049 j += 1;
2050 }
2051 if j < len && chars[j] == '|' {
2052 if j + 1 < len && chars[j + 1] == '}' {
2054 p = j + 1;
2056 } else {
2057 break; }
2059 } else {
2060 break; }
2062 }
2063 p
2064 }
2065
2066 pub fn cursor_to_raw_for_insertion(raw: &str, visual_pos: usize) -> usize {
2070 let pos = cursor_to_raw(raw, visual_pos);
2071 let chars: Vec<char> = raw.chars().collect();
2072 enter_empty_tags_at(&chars, pos)
2074 }
2075
2076 pub fn insert_at_visual(raw: &str, visual_pos: usize, insert: &str) -> (String, usize) {
2080 let raw_pos = cursor_to_raw_for_insertion(raw, visual_pos);
2081 let byte_pos = super::char_index_to_byte(raw, raw_pos);
2082 let mut new_raw = String::with_capacity(raw.len() + insert.len());
2083 new_raw.push_str(&raw[..byte_pos]);
2084 new_raw.push_str(insert);
2085 new_raw.push_str(&raw[byte_pos..]);
2086 let inserted_visual = cursor_len(insert);
2087 (new_raw, visual_pos + inserted_visual)
2088 }
2089
2090 pub fn delete_visual_range(raw: &str, visual_start: usize, visual_end: usize) -> String {
2094 if visual_start >= visual_end {
2095 return raw.to_string();
2096 }
2097
2098 let chars: Vec<char> = raw.chars().collect();
2099 let len = chars.len();
2100 let mut result = String::with_capacity(raw.len());
2101 let mut visual = 0usize;
2102 let mut i = 0;
2103 let mut in_style_def = false;
2104
2105 while i < len {
2106 let c = chars[i];
2107
2108 match c {
2109 '\\' if !in_style_def && i + 1 < len => {
2110 let in_range = visual >= visual_start && visual < visual_end;
2112 if !in_range {
2113 result.push(c);
2114 result.push(chars[i + 1]);
2115 }
2116 visual += 1;
2117 i += 2;
2118 }
2119 '{' if !in_style_def => {
2120 in_style_def = true;
2121 result.push(c); i += 1;
2123 }
2124 '|' if in_style_def => {
2125 in_style_def = false;
2126 result.push(c);
2127 if i + 1 < len && chars[i + 1] == '}' {
2129 visual += 1; }
2131 i += 1;
2132 }
2133 '}' if !in_style_def => {
2134 result.push(c); visual += 1; i += 1;
2137 }
2138 _ if in_style_def => {
2139 result.push(c); i += 1;
2141 }
2142 _ => {
2143 let in_range = visual >= visual_start && visual < visual_end;
2144 if !in_range {
2145 result.push(c);
2146 }
2147 visual += 1;
2148 i += 1;
2149 }
2150 }
2151 }
2152
2153 result
2154 }
2155
2156 pub fn cleanup_empty_styles(raw: &str, cursor_visual_pos: usize) -> (String, usize) {
2162 let chars: Vec<char> = raw.chars().collect();
2163 let len = chars.len();
2164 let mut result = String::with_capacity(raw.len());
2165 let mut i = 0;
2166 let mut visual = 0usize;
2167 let mut escaped = false;
2168 let mut cursor_adj = cursor_visual_pos;
2169
2170 while i < len {
2172 let c = chars[i];
2173
2174 if escaped {
2175 result.push(c);
2176 visual += 1;
2177 escaped = false;
2178 i += 1;
2179 continue;
2180 }
2181
2182 match c {
2183 '\\' => {
2184 escaped = true;
2185 result.push(c);
2186 i += 1;
2187 }
2188 '{' => {
2189 let mut j = i + 1;
2193 let mut style_escaped = false;
2194 let mut found_pipe = false;
2195 while j < len {
2196 if style_escaped {
2197 style_escaped = false;
2198 j += 1;
2199 continue;
2200 }
2201 if chars[j] == '\\' {
2202 style_escaped = true;
2203 j += 1;
2204 continue;
2205 }
2206 if chars[j] == '|' {
2207 found_pipe = true;
2208 j += 1; break;
2210 }
2211 if chars[j] == '{' {
2212 j += 1;
2214 continue;
2215 }
2216 j += 1;
2217 }
2218
2219 if !found_pipe {
2220 result.push(c);
2222 i += 1;
2223 continue;
2224 }
2225
2226 let _content_start_raw = j;
2229 let mut k = j;
2230 let mut content_escaped = false;
2231 let mut has_visible_content = false;
2232 let mut nesting = 1; while k < len && nesting > 0 {
2234 if content_escaped {
2235 has_visible_content = true;
2236 content_escaped = false;
2237 k += 1;
2238 continue;
2239 }
2240 match chars[k] {
2241 '\\' => {
2242 content_escaped = true;
2243 k += 1;
2244 }
2245 '{' => {
2246 nesting += 1;
2248 k += 1;
2249 }
2250 '}' => {
2251 nesting -= 1;
2252 if nesting == 0 {
2253 break; }
2255 k += 1;
2256 }
2257 '|' => {
2258 k += 1;
2260 }
2261 _ => {
2262 has_visible_content = true;
2263 k += 1;
2264 }
2265 }
2266 }
2267
2268 if !has_visible_content && nesting == 0 {
2269 let cursor_is_inside = cursor_visual_pos == visual
2274 || cursor_visual_pos == visual + 1;
2275 if cursor_is_inside {
2276 for idx in i..=k {
2278 result.push(chars[idx]);
2279 }
2280 visual += 2; } else {
2282 if cursor_adj > visual {
2285 cursor_adj = cursor_adj.saturating_sub(2);
2286 }
2287 }
2288 i = k + 1;
2289 } else {
2290 for idx in i..j {
2293 result.push(chars[idx]);
2294 }
2295 i = j;
2297 }
2298 }
2299 '}' => {
2300 result.push(c);
2301 visual += 1; i += 1;
2303 }
2304 _ => {
2305 result.push(c);
2306 visual += 1;
2307 i += 1;
2308 }
2309 }
2310 }
2311
2312 (result, cursor_adj)
2313 }
2314
2315 pub fn visual_char_at(raw: &str, visual_pos: usize) -> Option<char> {
2317 let raw_pos = cursor_to_raw(raw, visual_pos);
2318 let chars: Vec<char> = raw.chars().collect();
2319 if raw_pos >= chars.len() {
2320 return None;
2321 }
2322 if chars[raw_pos] == '\\' && raw_pos + 1 < chars.len() {
2324 Some(chars[raw_pos + 1])
2325 } else {
2326 Some(chars[raw_pos])
2327 }
2328 }
2329
2330 pub fn strip_styling(raw: &str) -> String {
2332 let mut result = String::new();
2333 let mut escaped = false;
2334 let mut in_style_def = false;
2335 for c in raw.chars() {
2336 if escaped {
2337 result.push(c);
2338 escaped = false;
2339 continue;
2340 }
2341 match c {
2342 '\\' => { escaped = true; }
2343 '{' if !in_style_def => { in_style_def = true; }
2344 '|' if in_style_def => { in_style_def = false; }
2345 '}' if !in_style_def => { }
2346 _ if in_style_def => { }
2347 _ => { result.push(c); }
2348 }
2349 }
2350 result
2351 }
2352
2353 pub fn cursor_to_content(raw: &str, cursor_pos: usize) -> usize {
2357 let chars: Vec<char> = raw.chars().collect();
2358 let len = chars.len();
2359 let mut visual = 0usize;
2360 let mut content = 0usize;
2361 let mut escaped = false;
2362 let mut in_style_def = false;
2363
2364 for i in 0..len {
2365 if visual >= cursor_pos {
2366 break;
2367 }
2368 let c = chars[i];
2369
2370 if escaped {
2371 visual += 1;
2372 content += 1;
2373 escaped = false;
2374 continue;
2375 }
2376
2377 match c {
2378 '\\' => { escaped = true; }
2379 '{' if !in_style_def => { in_style_def = true; }
2380 '|' if in_style_def => {
2381 in_style_def = false;
2382 if i + 1 < len && chars[i + 1] == '}' {
2383 visual += 1; }
2385 }
2386 '}' if !in_style_def => {
2387 visual += 1; }
2389 _ if in_style_def => {}
2390 _ => {
2391 visual += 1;
2392 content += 1;
2393 }
2394 }
2395 }
2396
2397 content
2398 }
2399
2400 pub fn content_to_cursor(raw: &str, content_pos: usize, snap_to_content: bool) -> usize {
2409 let chars: Vec<char> = raw.chars().collect();
2410 let len = chars.len();
2411 let mut visual = 0usize;
2412 let mut content = 0usize;
2413 let mut escaped = false;
2414 let mut in_style_def = false;
2415
2416 if snap_to_content {
2417 for i in 0..len {
2419 let c = chars[i];
2420
2421 if escaped {
2422 if content >= content_pos {
2423 return visual;
2424 }
2425 visual += 1;
2426 content += 1;
2427 escaped = false;
2428 continue;
2429 }
2430
2431 match c {
2432 '\\' => { escaped = true; }
2433 '{' if !in_style_def => { in_style_def = true; }
2434 '|' if in_style_def => {
2435 in_style_def = false;
2436 if i + 1 < len && chars[i + 1] == '}' {
2437 visual += 1; }
2439 }
2440 '}' if !in_style_def => {
2441 visual += 1; }
2443 _ if in_style_def => {}
2444 _ => {
2445 if content >= content_pos {
2446 return visual;
2447 }
2448 visual += 1;
2449 content += 1;
2450 }
2451 }
2452 }
2453 } else {
2454 for i in 0..len {
2456 if content >= content_pos {
2457 break;
2458 }
2459 let c = chars[i];
2460
2461 if escaped {
2462 visual += 1;
2463 content += 1;
2464 escaped = false;
2465 continue;
2466 }
2467
2468 match c {
2469 '\\' => { escaped = true; }
2470 '{' if !in_style_def => { in_style_def = true; }
2471 '|' if in_style_def => {
2472 in_style_def = false;
2473 if i + 1 < len && chars[i + 1] == '}' {
2474 visual += 1; }
2476 }
2477 '}' if !in_style_def => {
2478 visual += 1; }
2480 _ if in_style_def => {}
2481 _ => {
2482 visual += 1;
2483 content += 1;
2484 }
2485 }
2486 }
2487 }
2488
2489 visual
2490 }
2491
2492 pub fn delete_content_range(raw: &str, content_start: usize, content_end: usize) -> String {
2495 if content_start >= content_end {
2496 return raw.to_string();
2497 }
2498
2499 let chars: Vec<char> = raw.chars().collect();
2500 let len = chars.len();
2501 let mut result = String::with_capacity(raw.len());
2502 let mut content = 0usize;
2503 let mut i = 0;
2504 let mut in_style_def = false;
2505
2506 while i < len {
2507 let c = chars[i];
2508
2509 match c {
2510 '\\' if !in_style_def && i + 1 < len => {
2511 let in_range = content >= content_start && content < content_end;
2512 if !in_range {
2513 result.push(c);
2514 result.push(chars[i + 1]);
2515 }
2516 content += 1;
2517 i += 2;
2518 }
2519 '{' if !in_style_def => {
2520 in_style_def = true;
2521 result.push(c);
2522 i += 1;
2523 }
2524 '|' if in_style_def => {
2525 in_style_def = false;
2526 result.push(c);
2527 i += 1;
2528 }
2529 '}' if !in_style_def => {
2530 result.push(c);
2531 i += 1;
2532 }
2533 _ if in_style_def => {
2534 result.push(c);
2535 i += 1;
2536 }
2537 _ => {
2538 let in_range = content >= content_start && content < content_end;
2539 if !in_range {
2540 result.push(c);
2541 }
2542 content += 1;
2543 i += 1;
2544 }
2545 }
2546 }
2547
2548 result
2549 }
2550
2551 pub fn find_word_boundary_left_visual(raw: &str, visual_pos: usize) -> usize {
2554 let cp = cursor_to_content(raw, visual_pos);
2555 let stripped = strip_styling(raw);
2556 let boundary = super::find_word_boundary_left(&stripped, cp);
2557 content_to_cursor(raw, boundary, false)
2558 }
2559
2560 pub fn find_word_boundary_right_visual(raw: &str, visual_pos: usize) -> usize {
2563 let cp = cursor_to_content(raw, visual_pos);
2564 let stripped = strip_styling(raw);
2565 let boundary = super::find_word_boundary_right(&stripped, cp);
2566 content_to_cursor(raw, boundary, false)
2567 }
2568
2569 pub fn find_word_delete_boundary_right_visual(raw: &str, visual_pos: usize) -> usize {
2572 let cp = cursor_to_content(raw, visual_pos);
2573 let stripped = strip_styling(raw);
2574 let boundary = super::find_word_delete_boundary_right(&stripped, cp);
2575 content_to_cursor(raw, boundary, false)
2576 }
2577
2578 pub fn find_word_at_visual(raw: &str, visual_pos: usize) -> (usize, usize) {
2581 let cp = cursor_to_content(raw, visual_pos);
2582 let stripped = strip_styling(raw);
2583 let (s, e) = super::find_word_at(&stripped, cp);
2584 (content_to_cursor(raw, s, false), content_to_cursor(raw, e, false))
2585 }
2586
2587 pub fn styled_line_count(raw: &str) -> usize {
2589 raw.chars().filter(|&c| c == '\n').count() + 1
2591 }
2592
2593 pub fn line_and_column_styled(raw: &str, visual_pos: usize) -> (usize, usize) {
2596 let chars: Vec<char> = raw.chars().collect();
2598 let len = chars.len();
2599 let mut visual = 0usize;
2600 let mut line = 0usize;
2601 let mut line_start_visual = 0usize;
2602 let mut escaped = false;
2603 let mut in_style_def = false;
2604
2605 for i in 0..len {
2606 if visual >= visual_pos {
2607 break;
2608 }
2609 let c = chars[i];
2610
2611 if escaped {
2612 visual += 1;
2613 escaped = false;
2614 continue;
2615 }
2616
2617 match c {
2618 '\\' => { escaped = true; }
2619 '\n' => {
2620 visual += 1; line += 1;
2622 line_start_visual = visual;
2623 }
2624 '{' if !in_style_def => { in_style_def = true; }
2625 '|' if in_style_def => {
2626 in_style_def = false;
2627 if i + 1 < len && chars[i + 1] == '}' {
2628 visual += 1; }
2630 }
2631 '}' if !in_style_def => {
2632 visual += 1;
2633 }
2634 _ if in_style_def => {}
2635 _ => {
2636 visual += 1;
2637 }
2638 }
2639 }
2640
2641 (line, visual_pos.saturating_sub(line_start_visual))
2642 }
2643
2644 pub fn line_start_visual_styled(raw: &str, line_idx: usize) -> usize {
2646 if line_idx == 0 {
2647 return 0;
2648 }
2649 let chars: Vec<char> = raw.chars().collect();
2650 let len = chars.len();
2651 let mut visual = 0usize;
2652 let mut line = 0usize;
2653 let mut escaped = false;
2654 let mut in_style_def = false;
2655
2656 for i in 0..len {
2657 let c = chars[i];
2658 if escaped {
2659 visual += 1;
2660 escaped = false;
2661 continue;
2662 }
2663 match c {
2664 '\\' => { escaped = true; }
2665 '\n' => {
2666 visual += 1;
2667 line += 1;
2668 if line == line_idx {
2669 return visual;
2670 }
2671 }
2672 '{' if !in_style_def => { in_style_def = true; }
2673 '|' if in_style_def => {
2674 in_style_def = false;
2675 if i + 1 < len && chars[i + 1] == '}' {
2676 visual += 1;
2677 }
2678 }
2679 '}' if !in_style_def => { visual += 1; }
2680 _ if in_style_def => {}
2681 _ => { visual += 1; }
2682 }
2683 }
2684 visual }
2686
2687 pub fn line_end_visual_styled(raw: &str, line_idx: usize) -> usize {
2689 let chars: Vec<char> = raw.chars().collect();
2690 let len = chars.len();
2691 let mut visual = 0usize;
2692 let mut line = 0usize;
2693 let mut escaped = false;
2694 let mut in_style_def = false;
2695
2696 for i in 0..len {
2697 let c = chars[i];
2698 if escaped {
2699 visual += 1;
2700 escaped = false;
2701 continue;
2702 }
2703 match c {
2704 '\\' => { escaped = true; }
2705 '\n' => {
2706 if line == line_idx {
2707 return visual;
2708 }
2709 visual += 1;
2710 line += 1;
2711 }
2712 '{' if !in_style_def => { in_style_def = true; }
2713 '|' if in_style_def => {
2714 in_style_def = false;
2715 if i + 1 < len && chars[i + 1] == '}' {
2716 visual += 1;
2717 }
2718 }
2719 '}' if !in_style_def => { visual += 1; }
2720 _ if in_style_def => {}
2721 _ => { visual += 1; }
2722 }
2723 }
2724 visual }
2726
2727 #[cfg(test)]
2728 mod tests {
2729 use super::*;
2730
2731 #[test]
2732 fn test_escape_char() {
2733 assert_eq!(escape_char('a'), "a");
2734 assert_eq!(escape_char('{'), "\\{");
2735 assert_eq!(escape_char('}'), "\\}");
2736 assert_eq!(escape_char('|'), "\\|");
2737 assert_eq!(escape_char('\\'), "\\\\");
2738 }
2739
2740 #[test]
2741 fn test_escape_str() {
2742 assert_eq!(escape_str("hello"), "hello");
2743 assert_eq!(escape_str("a{b}c"), "a\\{b\\}c");
2744 assert_eq!(escape_str("x|y\\z"), "x\\|y\\\\z");
2745 }
2746
2747 #[test]
2748 fn test_cursor_to_raw_no_styling() {
2749 assert_eq!(cursor_to_raw("hello", 0), 0);
2751 assert_eq!(cursor_to_raw("hello", 3), 3);
2752 assert_eq!(cursor_to_raw("hello", 5), 5);
2753 }
2754
2755 #[test]
2756 fn test_cursor_to_raw_with_escape() {
2757 let raw = r"hel\{lo";
2759 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); }
2765
2766 #[test]
2767 fn test_cursor_to_raw_with_style() {
2768 let raw = "{red|world}";
2770 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); }
2777
2778 #[test]
2779 fn test_cursor_to_raw_mixed() {
2780 let raw = r"hel\{lo{red|world}";
2782 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); }
2789
2790 #[test]
2791 fn test_raw_to_cursor_no_styling() {
2792 assert_eq!(raw_to_cursor("hello", 0), 0);
2793 assert_eq!(raw_to_cursor("hello", 3), 3);
2794 assert_eq!(raw_to_cursor("hello", 5), 5);
2795 }
2796
2797 #[test]
2798 fn test_raw_to_cursor_with_escape() {
2799 let raw = r"hel\{lo";
2800 assert_eq!(raw_to_cursor(raw, 0), 0);
2801 assert_eq!(raw_to_cursor(raw, 3), 3); assert_eq!(raw_to_cursor(raw, 5), 4); assert_eq!(raw_to_cursor(raw, 7), 6); }
2805
2806 #[test]
2807 fn test_raw_to_cursor_with_style() {
2808 let raw = "{red|world}";
2810 assert_eq!(raw_to_cursor(raw, 0), 0);
2811 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); }
2816
2817 #[test]
2818 fn test_cursor_len() {
2819 assert_eq!(cursor_len("hello"), 5);
2820 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); }
2825
2826 #[test]
2827 fn test_insert_at_visual() {
2828 let (new, pos) = insert_at_visual("{red|hello}", 3, "XY");
2829 assert_eq!(new, "{red|helXYlo}");
2832 assert_eq!(pos, 5);
2833 }
2834
2835 #[test]
2836 fn test_delete_visual_range() {
2837 let new = delete_visual_range("{red|hello}", 1, 3);
2838 assert_eq!(new, "{red|hlo}");
2840 }
2841
2842 #[test]
2843 fn test_cleanup_empty_styles_removes_empty() {
2844 let (result, _) = cleanup_empty_styles("{red|}", 999);
2845 assert_eq!(result, ""); }
2847
2848 #[test]
2849 fn test_cleanup_empty_styles_keeps_if_cursor_inside() {
2850 let (result, _) = cleanup_empty_styles("{red|}", 0);
2852 assert_eq!(result, "{red|}"); }
2854
2855 #[test]
2856 fn test_cleanup_empty_styles_nonempty_kept() {
2857 let (result, _) = cleanup_empty_styles("{red|hello}", 999);
2858 assert_eq!(result, "{red|hello}");
2859 }
2860
2861 #[test]
2862 fn test_cleanup_preserves_text_after_empty() {
2863 let raw = "something{red|}more";
2866 let (result, _) = cleanup_empty_styles(raw, 0); assert_eq!(result, "somethingmore");
2869 }
2870
2871 #[test]
2872 fn test_cleanup_keeps_empty_when_cursor_at_content() {
2873 let raw = "something{red|}more";
2874 let (result, _) = cleanup_empty_styles(raw, 9);
2876 assert_eq!(result, "something{red|}more");
2877 }
2878
2879 #[test]
2880 fn test_cleanup_nonempty_nested_visual_counting() {
2881 let raw = "{color=red|hello}world";
2884 let (result, new_cursor) = cleanup_empty_styles(raw, 11);
2887 assert_eq!(result, raw);
2888 assert_eq!(new_cursor, 11);
2889
2890 let raw2 = "{color=red|hello}{blue|}";
2892 let (result2, new_cursor2) = cleanup_empty_styles(raw2, 8);
2895 assert_eq!(result2, "{color=red|hello}");
2896 assert_eq!(new_cursor2, 6);
2898 }
2899
2900 #[test]
2901 fn test_cleanup_deeply_nested_nonempty() {
2902 let raw = "aaa{r|{g|{b|xyz}}}end";
2904 let vl = cursor_len(raw);
2906 assert_eq!(vl, 12);
2907 let (result, new_cursor) = cleanup_empty_styles(raw, vl);
2908 assert_eq!(result, raw);
2909 assert_eq!(new_cursor, vl);
2910 }
2911
2912 #[test]
2913 fn test_word_boundary_visual_nested_tags() {
2914 let raw = "aaa{r|{r|{r|bbb}}} ccc";
2918 let vl = cursor_len(raw);
2920 assert_eq!(vl, 13);
2921
2922 let result = find_word_boundary_left_visual(raw, vl);
2924 assert!(result <= vl, "word boundary should not exceed visual len");
2925
2926 for v in 0..=vl {
2928 let _ = find_word_boundary_left_visual(raw, v);
2929 let _ = find_word_boundary_right_visual(raw, v);
2930 }
2931 }
2932
2933 #[test]
2934 fn test_word_boundary_visual_after_cleanup() {
2935 let raw = "aaa{color=red|{color=red|bbb}}} ccc";
2939 let vl = cursor_len(raw);
2940 let (cleaned, cursor) = cleanup_empty_styles(raw, vl);
2942 let cleaned_vl = cursor_len(&cleaned);
2943 assert!(cursor <= cleaned_vl,
2944 "cursor {} should be <= cursor_len {} after cleanup",
2945 cursor, cleaned_vl);
2946
2947 let _ = find_word_boundary_left_visual(&cleaned, cursor);
2949 }
2950
2951 #[test]
2952 fn test_roundtrip_visual_raw() {
2953 let raw = r"hel\{lo{red|world}";
2954 for v in 0..=12 {
2956 let r = cursor_to_raw(raw, v);
2957 let v2 = raw_to_cursor(raw, r);
2958 assert_eq!(v, v2, "visual {} → raw {} → visual {} (expected {})", v, r, v2, v);
2959 }
2960 }
2961
2962 #[test]
2963 fn test_cursor_to_raw_for_insertion_enters_empty_tag() {
2964 let raw = "test{red|}";
2966 assert_eq!(cursor_to_raw(raw, 4), 9);
2968 assert_eq!(cursor_to_raw(raw, 5), 9);
2970 assert_eq!(cursor_to_raw(raw, 6), 10);
2972 assert_eq!(cursor_to_raw_for_insertion(raw, 4), 9);
2974 }
2975
2976 #[test]
2977 fn test_cursor_to_raw_for_insertion_nonempty_tag_not_entered() {
2978 let raw = "test{red|x}";
2980 assert_eq!(cursor_to_raw(raw, 4), 9);
2982 assert_eq!(cursor_to_raw_for_insertion(raw, 4), 9);
2983 }
2984
2985 #[test]
2986 fn test_cursor_to_raw_for_insertion_at_start() {
2987 let raw = "{red|}hello";
2989 assert_eq!(cursor_to_raw(raw, 0), 0);
2991 assert_eq!(cursor_to_raw_for_insertion(raw, 0), 5);
2993 assert_eq!(cursor_to_raw(raw, 1), 5);
2995 assert_eq!(cursor_to_raw(raw, 2), 6);
2997 }
2998
2999 #[test]
3000 fn test_insert_at_visual_enters_empty_tag() {
3001 let raw = "test{red|}";
3003 let (new, pos) = insert_at_visual(raw, 4, "X");
3004 assert_eq!(new, "test{red|X}");
3006 assert_eq!(pos, 5);
3007 }
3008
3009 #[test]
3010 fn test_insert_at_visual_empty_tag_middle() {
3011 let raw = "hello{red|}world";
3013 let (new, pos) = insert_at_visual(raw, 5, "X");
3014 assert_eq!(new, "hello{red|X}world");
3015 assert_eq!(pos, 6);
3016 }
3017
3018 #[test]
3019 fn test_user_scenario_backspace_to_empty_tag() {
3020 let raw = r"hel\{lo{red|world}";
3026 let after_delete = delete_visual_range(raw, 6, 11);
3028 assert_eq!(after_delete, r"hel\{lo{red|}");
3029
3030 let (cleaned, _) = cleanup_empty_styles(&after_delete, 6);
3033 assert_eq!(cleaned, r"hel\{lo{red|}");
3034
3035 let (after_insert, new_pos) = insert_at_visual(&cleaned, 6, "X");
3037 assert_eq!(after_insert, r"hel\{lo{red|X}");
3039 assert_eq!(new_pos, 7);
3040
3041 let empty_again = delete_visual_range(&after_insert, 6, 7);
3044 assert_eq!(empty_again, r"hel\{lo{red|}");
3045 let (after_move, _) = cleanup_empty_styles(&empty_again, 5);
3046 assert_eq!(after_move, r"hel\{lo");
3047 }
3048 }
3049}
3050
3051pub fn compute_char_x_positions(
3055 display_text: &str,
3056 font_asset: Option<&'static crate::renderer::FontAsset>,
3057 font_size: u16,
3058 measure_fn: &dyn Fn(&str, &crate::text::TextConfig) -> crate::math::Dimensions,
3059) -> Vec<f32> {
3060 let char_count = display_text.chars().count();
3061 let mut positions = Vec::with_capacity(char_count + 1);
3062 positions.push(0.0);
3063
3064 let config = crate::text::TextConfig {
3065 font_asset,
3066 font_size,
3067 ..Default::default()
3068 };
3069
3070 #[cfg(feature = "text-styling")]
3071 {
3072 let chars: Vec<char> = display_text.chars().collect();
3077 let mut in_tag_header = false;
3078 let mut escaped = false;
3079 let mut last_width = 0.0f32;
3080
3081 for i in 0..char_count {
3082 let ch = chars[i];
3083 if escaped {
3084 escaped = false;
3085 let byte_end = char_index_to_byte(display_text, i + 1);
3087 let substr = &display_text[..byte_end];
3088 let dims = measure_fn(substr, &config);
3089 last_width = dims.width;
3090 positions.push(last_width);
3091 continue;
3092 }
3093 match ch {
3094 '\\' => {
3095 escaped = true;
3096 positions.push(last_width);
3098 }
3099 '{' => {
3100 in_tag_header = true;
3101 positions.push(last_width);
3102 }
3103 '|' if in_tag_header => {
3104 in_tag_header = false;
3105 positions.push(last_width);
3106 }
3107 '}' => {
3108 positions.push(last_width);
3110 }
3111 _ if in_tag_header => {
3112 positions.push(last_width);
3114 }
3115 _ => {
3116 let byte_end = char_index_to_byte(display_text, i + 1);
3118 let substr = &display_text[..byte_end];
3119 let dims = measure_fn(substr, &config);
3120 last_width = dims.width;
3121 positions.push(last_width);
3122 }
3123 }
3124 }
3125 }
3126
3127 #[cfg(not(feature = "text-styling"))]
3128 {
3129 for i in 1..=char_count {
3130 let byte_end = char_index_to_byte(display_text, i);
3131 let substr = &display_text[..byte_end];
3132 let dims = measure_fn(substr, &config);
3133 positions.push(dims.width);
3134 }
3135 }
3136
3137 positions
3138}
3139
3140#[cfg(test)]
3141mod tests {
3142 use super::*;
3143
3144 #[test]
3145 fn test_char_index_to_byte_ascii() {
3146 let s = "Hello";
3147 assert_eq!(char_index_to_byte(s, 0), 0);
3148 assert_eq!(char_index_to_byte(s, 3), 3);
3149 assert_eq!(char_index_to_byte(s, 5), 5);
3150 }
3151
3152 #[test]
3153 fn test_char_index_to_byte_unicode() {
3154 let s = "Héllo";
3155 assert_eq!(char_index_to_byte(s, 0), 0);
3156 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);
3159 }
3160
3161 #[test]
3162 fn test_word_boundary_left() {
3163 assert_eq!(find_word_boundary_left("hello world", 11), 6);
3164 assert_eq!(find_word_boundary_left("hello world", 6), 0); assert_eq!(find_word_boundary_left("hello world", 5), 0);
3166 assert_eq!(find_word_boundary_left("hello", 0), 0);
3167 }
3168
3169 #[test]
3170 fn test_word_boundary_right() {
3171 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);
3174 assert_eq!(find_word_boundary_right("hello", 5), 5);
3175 }
3176
3177 #[test]
3178 fn test_find_word_at() {
3179 assert_eq!(find_word_at("hello world", 2), (0, 5));
3180 assert_eq!(find_word_at("hello world", 7), (6, 11));
3181 assert_eq!(find_word_at("hello world", 5), (5, 6)); }
3183
3184 #[test]
3185 fn test_insert_text() {
3186 let mut state = TextEditState::default();
3187 state.insert_text("Hello", None);
3188 assert_eq!(state.text, "Hello");
3189 assert_eq!(state.cursor_pos, 5);
3190
3191 state.cursor_pos = 5;
3192 state.insert_text(" World", None);
3193 assert_eq!(state.text, "Hello World");
3194 assert_eq!(state.cursor_pos, 11);
3195 }
3196
3197 #[test]
3198 fn test_insert_text_max_length() {
3199 let mut state = TextEditState::default();
3200 state.insert_text("Hello World", Some(5));
3201 assert_eq!(state.text, "Hello");
3202 assert_eq!(state.cursor_pos, 5);
3203
3204 state.insert_text("!", Some(5));
3206 assert_eq!(state.text, "Hello");
3207 }
3208
3209 #[test]
3210 fn test_backspace() {
3211 let mut state = TextEditState::default();
3212 state.text = "Hello".to_string();
3213 state.cursor_pos = 5;
3214 state.backspace();
3215 assert_eq!(state.text, "Hell");
3216 assert_eq!(state.cursor_pos, 4);
3217 }
3218
3219 #[test]
3220 fn test_delete_forward() {
3221 let mut state = TextEditState::default();
3222 state.text = "Hello".to_string();
3223 state.cursor_pos = 0;
3224 state.delete_forward();
3225 assert_eq!(state.text, "ello");
3226 assert_eq!(state.cursor_pos, 0);
3227 }
3228
3229 #[test]
3230 fn test_selection_delete() {
3231 let mut state = TextEditState::default();
3232 state.text = "Hello World".to_string();
3233 state.selection_anchor = Some(0);
3234 state.cursor_pos = 5;
3235 state.delete_selection();
3236 assert_eq!(state.text, " World");
3237 assert_eq!(state.cursor_pos, 0);
3238 assert!(state.selection_anchor.is_none());
3239 }
3240
3241 #[test]
3242 fn test_select_all() {
3243 let mut state = TextEditState::default();
3244 state.text = "Hello".to_string();
3245 state.cursor_pos = 2;
3246 state.select_all();
3247 assert_eq!(state.selection_anchor, Some(0));
3248 assert_eq!(state.cursor_pos, 5);
3249 }
3250
3251 #[test]
3252 fn test_move_left_right() {
3253 let mut state = TextEditState::default();
3254 state.text = "AB".to_string();
3255 state.cursor_pos = 1;
3256
3257 state.move_left(false);
3258 assert_eq!(state.cursor_pos, 0);
3259
3260 state.move_right(false);
3261 assert_eq!(state.cursor_pos, 1);
3262 }
3263
3264 #[test]
3265 fn test_move_with_shift_creates_selection() {
3266 let mut state = TextEditState::default();
3267 state.text = "Hello".to_string();
3268 state.cursor_pos = 2;
3269
3270 state.move_right(true);
3271 assert_eq!(state.cursor_pos, 3);
3272 assert_eq!(state.selection_anchor, Some(2));
3273
3274 state.move_right(true);
3275 assert_eq!(state.cursor_pos, 4);
3276 assert_eq!(state.selection_anchor, Some(2));
3277 }
3278
3279 #[test]
3280 fn test_display_text_normal() {
3281 assert_eq!(display_text("Hello", "Placeholder", false), "Hello");
3282 }
3283
3284 #[test]
3285 fn test_display_text_empty() {
3286 assert_eq!(display_text("", "Placeholder", false), "Placeholder");
3287 }
3288
3289 #[test]
3290 fn test_display_text_password() {
3291 assert_eq!(display_text("pass", "Placeholder", true), "••••");
3292 }
3293
3294 #[test]
3295 fn test_nearest_char_boundary() {
3296 let positions = vec![0.0, 10.0, 20.0, 30.0];
3297 assert_eq!(find_nearest_char_boundary(4.0, &positions), 0);
3298 assert_eq!(find_nearest_char_boundary(6.0, &positions), 1);
3299 assert_eq!(find_nearest_char_boundary(15.0, &positions), 1); assert_eq!(find_nearest_char_boundary(25.0, &positions), 2);
3301 assert_eq!(find_nearest_char_boundary(100.0, &positions), 3);
3302 }
3303
3304 #[test]
3305 fn test_ensure_cursor_visible() {
3306 let mut state = TextEditState::default();
3307 state.scroll_offset = 0.0;
3308
3309 state.ensure_cursor_visible(150.0, 100.0);
3311 assert_eq!(state.scroll_offset, 50.0);
3312
3313 state.ensure_cursor_visible(30.0, 100.0);
3315 assert_eq!(state.scroll_offset, 30.0);
3316 }
3317
3318 #[test]
3319 fn test_backspace_word() {
3320 let mut state = TextEditState::default();
3321 state.text = "hello world".to_string();
3322 state.cursor_pos = 11;
3323 state.backspace_word();
3324 assert_eq!(state.text, "hello ");
3325 assert_eq!(state.cursor_pos, 6);
3326 }
3327
3328 #[test]
3329 fn test_delete_word_forward() {
3330 let mut state = TextEditState::default();
3331 state.text = "hello world".to_string();
3332 state.cursor_pos = 0;
3333 state.delete_word_forward();
3334 assert_eq!(state.text, "world");
3335 assert_eq!(state.cursor_pos, 0);
3336 }
3337
3338 #[test]
3341 fn test_line_start_char_pos() {
3342 assert_eq!(line_start_char_pos("hello\nworld", 0), 0);
3343 assert_eq!(line_start_char_pos("hello\nworld", 3), 0);
3344 assert_eq!(line_start_char_pos("hello\nworld", 5), 0);
3345 assert_eq!(line_start_char_pos("hello\nworld", 6), 6); assert_eq!(line_start_char_pos("hello\nworld", 9), 6);
3347 }
3348
3349 #[test]
3350 fn test_line_end_char_pos() {
3351 assert_eq!(line_end_char_pos("hello\nworld", 0), 5);
3352 assert_eq!(line_end_char_pos("hello\nworld", 3), 5);
3353 assert_eq!(line_end_char_pos("hello\nworld", 6), 11);
3354 assert_eq!(line_end_char_pos("hello\nworld", 9), 11);
3355 }
3356
3357 #[test]
3358 fn test_line_and_column() {
3359 assert_eq!(line_and_column("hello\nworld", 0), (0, 0));
3360 assert_eq!(line_and_column("hello\nworld", 3), (0, 3));
3361 assert_eq!(line_and_column("hello\nworld", 5), (0, 5)); assert_eq!(line_and_column("hello\nworld", 6), (1, 0));
3363 assert_eq!(line_and_column("hello\nworld", 8), (1, 2));
3364 assert_eq!(line_and_column("hello\nworld", 11), (1, 5)); }
3366
3367 #[test]
3368 fn test_line_and_column_three_lines() {
3369 let text = "ab\ncd\nef";
3370 assert_eq!(line_and_column(text, 0), (0, 0));
3371 assert_eq!(line_and_column(text, 2), (0, 2)); assert_eq!(line_and_column(text, 3), (1, 0));
3373 assert_eq!(line_and_column(text, 5), (1, 2)); assert_eq!(line_and_column(text, 6), (2, 0));
3375 assert_eq!(line_and_column(text, 8), (2, 2)); }
3377
3378 #[test]
3379 fn test_char_pos_from_line_col() {
3380 assert_eq!(char_pos_from_line_col("hello\nworld", 0, 0), 0);
3381 assert_eq!(char_pos_from_line_col("hello\nworld", 0, 3), 3);
3382 assert_eq!(char_pos_from_line_col("hello\nworld", 1, 0), 6);
3383 assert_eq!(char_pos_from_line_col("hello\nworld", 1, 3), 9);
3384 assert_eq!(char_pos_from_line_col("ab\ncd", 0, 10), 2); assert_eq!(char_pos_from_line_col("ab\ncd", 1, 10), 5); }
3388
3389 #[test]
3390 fn test_split_lines() {
3391 let lines = split_lines("hello\nworld");
3392 assert_eq!(lines.len(), 2);
3393 assert_eq!(lines[0], (0, "hello"));
3394 assert_eq!(lines[1], (6, "world"));
3395
3396 let lines2 = split_lines("a\nb\nc");
3397 assert_eq!(lines2.len(), 3);
3398 assert_eq!(lines2[0], (0, "a"));
3399 assert_eq!(lines2[1], (2, "b"));
3400 assert_eq!(lines2[2], (4, "c"));
3401
3402 let lines3 = split_lines("no newlines");
3403 assert_eq!(lines3.len(), 1);
3404 assert_eq!(lines3[0], (0, "no newlines"));
3405 }
3406
3407 #[test]
3408 fn test_split_lines_trailing_newline() {
3409 let lines = split_lines("hello\n");
3410 assert_eq!(lines.len(), 2);
3411 assert_eq!(lines[0], (0, "hello"));
3412 assert_eq!(lines[1], (6, ""));
3413 }
3414
3415 #[test]
3416 fn test_move_up_down() {
3417 let mut state = TextEditState::default();
3418 state.text = "hello\nworld".to_string();
3419 state.cursor_pos = 8; state.move_up(false);
3422 assert_eq!(state.cursor_pos, 2); state.move_down(false);
3425 assert_eq!(state.cursor_pos, 8); }
3427
3428 #[test]
3429 fn test_move_up_clamps_column() {
3430 let mut state = TextEditState::default();
3431 state.text = "ab\nhello".to_string();
3432 state.cursor_pos = 7; state.move_up(false);
3435 assert_eq!(state.cursor_pos, 2); }
3437
3438 #[test]
3439 fn test_move_up_from_first_line() {
3440 let mut state = TextEditState::default();
3441 state.text = "hello\nworld".to_string();
3442 state.cursor_pos = 3;
3443
3444 state.move_up(false);
3445 assert_eq!(state.cursor_pos, 0); }
3447
3448 #[test]
3449 fn test_move_down_from_last_line() {
3450 let mut state = TextEditState::default();
3451 state.text = "hello\nworld".to_string();
3452 state.cursor_pos = 8;
3453
3454 state.move_down(false);
3455 assert_eq!(state.cursor_pos, 11); }
3457
3458 #[test]
3459 fn test_move_line_home_end() {
3460 let mut state = TextEditState::default();
3461 state.text = "hello\nworld".to_string();
3462 state.cursor_pos = 8; state.move_line_home(false);
3465 assert_eq!(state.cursor_pos, 6); state.move_line_end(false);
3468 assert_eq!(state.cursor_pos, 11); }
3470
3471 #[test]
3472 fn test_move_up_with_shift_selects() {
3473 let mut state = TextEditState::default();
3474 state.text = "hello\nworld".to_string();
3475 state.cursor_pos = 8;
3476
3477 state.move_up(true);
3478 assert_eq!(state.cursor_pos, 2);
3479 assert_eq!(state.selection_anchor, Some(8));
3480 }
3481
3482 #[test]
3483 fn test_ensure_cursor_visible_vertical() {
3484 let mut state = TextEditState::default();
3485 state.scroll_offset_y = 0.0;
3486
3487 state.ensure_cursor_visible_vertical(5, 20.0, 60.0);
3490 assert_eq!(state.scroll_offset_y, 60.0); state.ensure_cursor_visible_vertical(1, 20.0, 60.0);
3494 assert_eq!(state.scroll_offset_y, 20.0);
3495 }
3496
3497 fn fixed_measure(text: &str, _config: &crate::text::TextConfig) -> crate::math::Dimensions {
3501 crate::math::Dimensions {
3502 width: text.chars().count() as f32 * 10.0,
3503 height: 20.0,
3504 }
3505 }
3506
3507 #[test]
3508 fn test_wrap_lines_no_wrap_needed() {
3509 let lines = wrap_lines("hello", 100.0, None, 16, &fixed_measure);
3510 assert_eq!(lines.len(), 1);
3511 assert_eq!(lines[0].text, "hello");
3512 assert_eq!(lines[0].global_char_start, 0);
3513 assert_eq!(lines[0].char_count, 5);
3514 }
3515
3516 #[test]
3517 fn test_wrap_lines_hard_break() {
3518 let lines = wrap_lines("ab\ncd", 100.0, None, 16, &fixed_measure);
3519 assert_eq!(lines.len(), 2);
3520 assert_eq!(lines[0].text, "ab");
3521 assert_eq!(lines[0].global_char_start, 0);
3522 assert_eq!(lines[1].text, "cd");
3523 assert_eq!(lines[1].global_char_start, 3); }
3525
3526 #[test]
3527 fn test_wrap_lines_word_wrap() {
3528 let lines = wrap_lines("hello world", 60.0, None, 16, &fixed_measure);
3531 assert_eq!(lines.len(), 2);
3532 assert_eq!(lines[0].text, "hello ");
3533 assert_eq!(lines[0].global_char_start, 0);
3534 assert_eq!(lines[0].char_count, 6);
3535 assert_eq!(lines[1].text, "world");
3536 assert_eq!(lines[1].global_char_start, 6);
3537 assert_eq!(lines[1].char_count, 5);
3538 }
3539
3540 #[test]
3541 fn test_wrap_lines_char_level_break() {
3542 let lines = wrap_lines("abcdefghij", 50.0, None, 16, &fixed_measure);
3545 assert_eq!(lines.len(), 2);
3546 assert_eq!(lines[0].text, "abcde");
3547 assert_eq!(lines[0].char_count, 5);
3548 assert_eq!(lines[1].text, "fghij");
3549 assert_eq!(lines[1].global_char_start, 5);
3550 }
3551
3552 #[test]
3553 fn test_cursor_to_visual_pos_simple() {
3554 let lines = vec![
3555 VisualLine { text: "hello ".to_string(), global_char_start: 0, char_count: 6 },
3556 VisualLine { text: "world".to_string(), global_char_start: 6, char_count: 5 },
3557 ];
3558 assert_eq!(cursor_to_visual_pos(&lines, 0), (0, 0));
3559 assert_eq!(cursor_to_visual_pos(&lines, 3), (0, 3));
3560 assert_eq!(cursor_to_visual_pos(&lines, 6), (1, 0)); assert_eq!(cursor_to_visual_pos(&lines, 8), (1, 2));
3562 assert_eq!(cursor_to_visual_pos(&lines, 11), (1, 5));
3563 }
3564
3565 #[test]
3566 fn test_cursor_to_visual_pos_hard_break() {
3567 let lines = vec![
3569 VisualLine { text: "ab".to_string(), global_char_start: 0, char_count: 2 },
3570 VisualLine { text: "cd".to_string(), global_char_start: 3, char_count: 2 },
3571 ];
3572 assert_eq!(cursor_to_visual_pos(&lines, 2), (0, 2)); assert_eq!(cursor_to_visual_pos(&lines, 3), (1, 0)); }
3575
3576 #[test]
3577 fn test_visual_move_up_down() {
3578 let lines = vec![
3579 VisualLine { text: "hello ".to_string(), global_char_start: 0, char_count: 6 },
3580 VisualLine { text: "world".to_string(), global_char_start: 6, char_count: 5 },
3581 ];
3582 assert_eq!(visual_move_up(&lines, 8), 2);
3584 assert_eq!(visual_move_down(&lines, 2, 11), 8);
3586 }
3587
3588 #[test]
3589 fn test_visual_line_home_end() {
3590 let lines = vec![
3591 VisualLine { text: "hello ".to_string(), global_char_start: 0, char_count: 6 },
3592 VisualLine { text: "world".to_string(), global_char_start: 6, char_count: 5 },
3593 ];
3594 assert_eq!(visual_line_home(&lines, 8), 6);
3596 assert_eq!(visual_line_end(&lines, 8), 11);
3597 assert_eq!(visual_line_home(&lines, 3), 0);
3599 assert_eq!(visual_line_end(&lines, 3), 6);
3600 }
3601
3602 #[test]
3603 fn test_undo_basic() {
3604 let mut state = TextEditState::default();
3605 state.text = "hello".to_string();
3606 state.cursor_pos = 5;
3607
3608 state.push_undo(UndoActionKind::Paste);
3610 state.insert_text(" world", None);
3611 assert_eq!(state.text, "hello world");
3612
3613 assert!(state.undo());
3615 assert_eq!(state.text, "hello");
3616 assert_eq!(state.cursor_pos, 5);
3617
3618 assert!(state.redo());
3620 assert_eq!(state.text, "hello world");
3621 assert_eq!(state.cursor_pos, 11);
3622 }
3623
3624 #[test]
3625 fn test_undo_grouping_insert_char() {
3626 let mut state = TextEditState::default();
3627
3628 state.push_undo(UndoActionKind::InsertChar);
3630 state.insert_text("a", None);
3631 state.push_undo(UndoActionKind::InsertChar);
3632 state.insert_text("b", None);
3633 state.push_undo(UndoActionKind::InsertChar);
3634 state.insert_text("c", None);
3635 assert_eq!(state.text, "abc");
3636
3637 assert!(state.undo());
3639 assert_eq!(state.text, "");
3640 assert_eq!(state.cursor_pos, 0);
3641
3642 assert!(!state.undo());
3644 }
3645
3646 #[test]
3647 fn test_undo_grouping_backspace() {
3648 let mut state = TextEditState::default();
3649 state.text = "hello".to_string();
3650 state.cursor_pos = 5;
3651
3652 state.push_undo(UndoActionKind::Backspace);
3654 state.backspace();
3655 state.push_undo(UndoActionKind::Backspace);
3656 state.backspace();
3657 state.push_undo(UndoActionKind::Backspace);
3658 state.backspace();
3659 assert_eq!(state.text, "he");
3660
3661 assert!(state.undo());
3663 assert_eq!(state.text, "hello");
3664 }
3665
3666 #[test]
3667 fn test_undo_different_kinds_not_grouped() {
3668 let mut state = TextEditState::default();
3669
3670 state.push_undo(UndoActionKind::InsertChar);
3672 state.insert_text("abc", None);
3673 state.push_undo(UndoActionKind::Backspace);
3674 state.backspace();
3675 assert_eq!(state.text, "ab");
3676
3677 assert!(state.undo());
3679 assert_eq!(state.text, "abc");
3680
3681 assert!(state.undo());
3683 assert_eq!(state.text, "");
3684 }
3685
3686 #[test]
3687 fn test_redo_cleared_on_new_edit() {
3688 let mut state = TextEditState::default();
3689
3690 state.push_undo(UndoActionKind::Paste);
3691 state.insert_text("hello", None);
3692 state.undo();
3693 assert_eq!(state.text, "");
3694 assert!(!state.redo_stack.is_empty());
3695
3696 state.push_undo(UndoActionKind::Paste);
3698 state.insert_text("world", None);
3699 assert!(state.redo_stack.is_empty());
3700 }
3701
3702 #[test]
3703 fn test_undo_empty_stack() {
3704 let mut state = TextEditState::default();
3705 assert!(!state.undo());
3706 assert!(!state.redo());
3707 }
3708
3709 #[cfg(feature = "text-styling")]
3710 fn make_no_styles_state(raw: &str) -> TextEditState {
3711 let mut s = TextEditState::default();
3712 s.text = raw.to_string();
3713 s.no_styles_movement = true;
3714 s.cursor_pos = 0;
3716 s
3717 }
3718
3719 #[test]
3720 #[cfg(feature = "text-styling")]
3721 fn test_content_to_cursor_no_structural_basic() {
3722 use crate::text_input::styling::content_to_cursor;
3723 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); }
3728
3729 #[test]
3730 #[cfg(feature = "text-styling")]
3731 fn test_content_to_cursor_no_structural_nested() {
3732 use crate::text_input::styling::content_to_cursor;
3733 assert_eq!(content_to_cursor("a{red|b}{blue|c}", 0, true), 0);
3735 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); }
3739
3740 #[test]
3741 #[cfg(feature = "text-styling")]
3742 fn test_delete_content_range() {
3743 use crate::text_input::styling::delete_content_range;
3744 assert_eq!(delete_content_range("a{red|b}c", 1, 2), "a{red|}c");
3746 assert_eq!(delete_content_range("a{red|b}c", 0, 1), "{red|b}c");
3748 assert_eq!(delete_content_range("a{red|b}c", 0, 3), "{red|}");
3750 assert_eq!(delete_content_range("abc", 1, 1), "abc");
3752 }
3753
3754 #[test]
3755 #[cfg(feature = "text-styling")]
3756 fn test_no_styles_move_right() {
3757 let mut s = make_no_styles_state("a{red|}b");
3758 s.move_right_styled(false);
3762 assert_eq!(s.text, "ab");
3763 assert_eq!(s.cursor_pos, 1);
3764 s.move_right_styled(false);
3766 assert_eq!(s.cursor_pos, 2);
3767 assert_eq!(styling::cursor_to_content(&s.text, s.cursor_pos), 2);
3768 }
3769
3770 #[test]
3771 #[cfg(feature = "text-styling")]
3772 fn test_no_styles_move_left() {
3773 let mut s = make_no_styles_state("a{red|}b");
3774 s.cursor_pos = styling::content_to_cursor(&s.text, 2, true);
3777 s.move_end_styled(false);
3779 assert_eq!(s.text, "ab");
3780 assert_eq!(s.cursor_pos, 2);
3781 s.move_left_styled(false);
3783 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3784 assert_eq!(cp, 1);
3785 s.move_left_styled(false);
3787 assert_eq!(s.cursor_pos, 0);
3788 }
3789
3790 #[test]
3791 #[cfg(feature = "text-styling")]
3792 fn test_no_styles_move_left_skips_closing_brace() {
3793 let mut s = make_no_styles_state("a{red|b}c");
3794 s.cursor_pos = styling::content_to_cursor(&s.text, 2, true);
3796 assert_eq!(s.cursor_pos, 3);
3798 s.move_left_styled(false);
3800 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3801 assert_eq!(cp, 1);
3802 }
3803
3804 #[test]
3805 #[cfg(feature = "text-styling")]
3806 fn test_no_styles_backspace() {
3807 let mut s = make_no_styles_state("a{red|b}c");
3808 s.cursor_pos = styling::content_to_cursor(&s.text, 2, true);
3810 s.backspace_styled();
3811 let stripped = styling::strip_styling(&s.text);
3813 assert_eq!(stripped, "ac");
3814 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3816 assert_eq!(cp, 1);
3817 }
3818
3819 #[test]
3820 #[cfg(feature = "text-styling")]
3821 fn test_no_styles_delete_forward() {
3822 let mut s = make_no_styles_state("{red|abc}");
3823 s.cursor_pos = styling::content_to_cursor(&s.text, 1, true);
3825 s.delete_forward_styled();
3826 let stripped = styling::strip_styling(&s.text);
3827 assert_eq!(stripped, "ac");
3828 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3829 assert_eq!(cp, 1);
3830 }
3831
3832 #[test]
3833 #[cfg(feature = "text-styling")]
3834 fn test_no_styles_home_end() {
3835 let mut s = make_no_styles_state("{red|}hello{blue|}");
3836 s.move_home_styled(false);
3838 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3839 assert_eq!(cp, 0);
3840 s.move_end_styled(false);
3842 let cp = styling::cursor_to_content(&s.text, s.cursor_pos);
3843 let content_len = styling::strip_styling(&s.text).chars().count();
3844 assert_eq!(cp, content_len);
3845 }
3846
3847 #[test]
3848 #[cfg(feature = "text-styling")]
3849 fn test_no_styles_select_all_and_delete() {
3850 let mut s = make_no_styles_state("a{red|b}c");
3851 s.select_all_styled();
3852 assert!(s.selection_anchor.is_some());
3853 s.delete_selection_styled();
3855 let stripped = styling::strip_styling(&s.text);
3856 assert!(stripped.is_empty());
3857 }
3858}