1use std::borrow::Cow;
48use std::ops::Range;
49
50use kurbo::{Point, Rect, Size};
51use parley::{BoundingBox, PlainEditor, StyleProperty};
52use peniko::Brush;
53
54use frust_scene::GlyphRun;
55
56use crate::context::TextContext;
57use crate::style::{
58 TextStyle, to_parley_family, to_parley_line_height, to_parley_style, to_parley_weight,
59};
60
61#[derive(Clone, Debug, PartialEq)]
67pub enum EditOp {
68 Insert(String),
70 InsertNewline,
76 Backdelete,
78 Delete,
81 MoveLeft { select: bool },
83 MoveRight { select: bool },
85 MoveUp { select: bool },
87 MoveDown { select: bool },
89 MoveWordLeft { select: bool },
91 MoveWordRight { select: bool },
93 Home { select: bool },
95 End { select: bool },
97 SelectAll,
99 CollapseSelection,
101 MoveToPoint { x: f32, y: f32, select: bool },
103 SelectWordAtPoint { x: f32, y: f32 },
105 Compose {
108 text: String,
109 cursor: Option<(usize, usize)>,
110 },
111 FinishCompose,
113 ClearCompose,
115 ApplyEditingState(EditingStateBytes),
118}
119
120#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct EditingStateBytes {
124 pub text: String,
126 pub base: usize,
128 pub extent: usize,
130 pub composing: Option<Range<usize>>,
133}
134
135#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct EditingState {
142 pub text: String,
144 pub selection_base: i32,
146 pub selection_extent: i32,
148 pub composing_base: i32,
150 pub composing_extent: i32,
152}
153
154pub struct TextEditor {
161 editor: PlainEditor<Brush>,
162 platform_composing: Option<Range<usize>>,
166}
167
168impl TextEditor {
169 pub fn new(style: &TextStyle) -> Self {
186 let mut editor = PlainEditor::<Brush>::new(style.size);
187 editor.set_width(None);
190 let styles = editor.edit_styles();
191 styles.insert(StyleProperty::FontFamily(to_parley_family(&style.family)));
192 styles.insert(StyleProperty::FontWeight(to_parley_weight(style.weight)));
193 styles.insert(StyleProperty::FontStyle(to_parley_style(style.style)));
194 styles.insert(StyleProperty::LetterSpacing(style.letter_spacing));
195 styles.insert(StyleProperty::LineHeight(to_parley_line_height(
196 style.line_height,
197 )));
198 styles.insert(StyleProperty::Brush(Brush::Solid(style.color)));
199 Self {
200 editor,
201 platform_composing: None,
202 }
203 }
204
205 pub fn apply(&mut self, op: EditOp, ctx: &mut TextContext) {
213 let (font_cx, layout_cx) = ctx.driver_contexts();
214
215 match op {
216 EditOp::ApplyEditingState(state) => {
217 if self.editing_state_bytes() == state {
223 return;
224 }
225 if self.editor.raw_text() != state.text {
226 self.editor.set_text(&state.text);
227 }
228 {
229 let mut drv = self.editor.driver(font_cx, layout_cx);
230 drv.select_byte_range(state.base, state.extent);
231 }
232 self.platform_composing = state.composing;
233 }
234 other => {
235 let mut drv = self.editor.driver(font_cx, layout_cx);
236 match other {
237 EditOp::Insert(s) => drv.insert_or_replace_selection(&s),
238 EditOp::InsertNewline => drv.insert_or_replace_selection("\n"),
239 EditOp::Backdelete => drv.backdelete(),
240 EditOp::Delete => drv.delete(),
241 EditOp::MoveLeft { select } => {
242 sel(&mut drv, select, |d| d.move_left(), |d| d.select_left())
243 }
244 EditOp::MoveRight { select } => {
245 sel(&mut drv, select, |d| d.move_right(), |d| d.select_right())
246 }
247 EditOp::MoveUp { select } => {
248 sel(&mut drv, select, |d| d.move_up(), |d| d.select_up())
249 }
250 EditOp::MoveDown { select } => {
251 sel(&mut drv, select, |d| d.move_down(), |d| d.select_down())
252 }
253 EditOp::MoveWordLeft { select } => sel(
254 &mut drv,
255 select,
256 |d| d.move_word_left(),
257 |d| d.select_word_left(),
258 ),
259 EditOp::MoveWordRight { select } => sel(
260 &mut drv,
261 select,
262 |d| d.move_word_right(),
263 |d| d.select_word_right(),
264 ),
265 EditOp::Home { select } => sel(
266 &mut drv,
267 select,
268 |d| d.move_to_line_start(),
269 |d| d.select_to_line_start(),
270 ),
271 EditOp::End { select } => sel(
272 &mut drv,
273 select,
274 |d| d.move_to_line_end(),
275 |d| d.select_to_line_end(),
276 ),
277 EditOp::SelectAll => drv.select_all(),
278 EditOp::CollapseSelection => drv.collapse_selection(),
279 EditOp::MoveToPoint { x, y, select } => {
280 if select {
281 drv.extend_selection_to_point(x, y);
282 } else {
283 drv.move_to_point(x, y);
284 }
285 }
286 EditOp::SelectWordAtPoint { x, y } => drv.select_word_at_point(x, y),
287 EditOp::Compose { text, cursor } => drv.set_compose(&text, cursor),
288 EditOp::FinishCompose => drv.finish_compose(),
289 EditOp::ClearCompose => drv.clear_compose(),
290 EditOp::ApplyEditingState(_) => unreachable!(),
292 }
293 }
294 }
295
296 self.editor.refresh_layout(font_cx, layout_cx);
297 }
298
299 pub fn set_wrap_width(&mut self, width: Option<f32>, ctx: &mut TextContext) {
309 self.editor.set_width(width);
310 let (font_cx, layout_cx) = ctx.driver_contexts();
311 self.editor.refresh_layout(font_cx, layout_cx);
312 }
313
314 pub fn line_count(&self) -> usize {
319 self.editor
320 .try_layout()
321 .map(|layout| layout.len())
322 .unwrap_or(0)
323 }
324
325 pub fn text(&self) -> &str {
327 self.editor.raw_text()
328 }
329
330 pub fn selected_text(&self) -> Option<&str> {
341 let text = self.text();
342 let selection = self.editor.raw_selection();
343 let base = selection.anchor().index();
344 let extent = selection.focus().index();
345
346 if base == extent {
347 return None;
348 }
349
350 let start = base.min(extent);
351 let end = base.max(extent);
352
353 #[cfg(debug_assertions)]
355 assert!(
356 text.is_char_boundary(start) && text.is_char_boundary(end),
357 "selected text byte offsets are not char boundaries: start={}, end={}, text.len()={}",
358 start,
359 end,
360 text.len()
361 );
362
363 #[cfg(not(debug_assertions))]
364 if !text.is_char_boundary(start) || !text.is_char_boundary(end) {
365 return None;
366 }
367
368 Some(&text[start..end])
369 }
370
371 pub fn layout_size(&self) -> Size {
376 match self.editor.try_layout() {
377 Some(layout) => Size::new(layout.width() as f64, layout.height() as f64),
378 None => Size::ZERO,
379 }
380 }
381
382 pub fn editing_state_bytes(&self) -> EditingStateBytes {
384 let selection = self.editor.raw_selection();
385 EditingStateBytes {
386 text: self.editor.raw_text().to_string(),
387 base: selection.anchor().index(),
388 extent: selection.focus().index(),
389 composing: self.composing_range(),
390 }
391 }
392
393 pub fn editing_state_utf16(&self) -> EditingState {
395 let text = self.editor.raw_text();
396 let selection = self.editor.raw_selection();
397 let (composing_base, composing_extent) = match self.composing_range() {
398 Some(range) => (
399 byte_to_utf16(text, range.start) as i32,
400 byte_to_utf16(text, range.end) as i32,
401 ),
402 None => (-1, -1),
403 };
404 EditingState {
405 text: text.to_string(),
406 selection_base: byte_to_utf16(text, selection.anchor().index()) as i32,
407 selection_extent: byte_to_utf16(text, selection.focus().index()) as i32,
408 composing_base,
409 composing_extent,
410 }
411 }
412
413 pub fn cursor_rect(&self, caret_width: f32) -> Option<Rect> {
417 self.editor.cursor_geometry(caret_width).map(bbox_to_rect)
418 }
419
420 pub fn selection_rects(&self) -> Vec<Rect> {
422 self.editor
423 .selection_geometry()
424 .into_iter()
425 .map(|(bbox, _line)| bbox_to_rect(bbox))
426 .collect()
427 }
428
429 pub fn ime_cursor_area(&self) -> Rect {
432 bbox_to_rect(self.editor.ime_cursor_area())
433 }
434
435 pub fn to_scene_runs(&self, origin: Point) -> Vec<GlyphRun> {
440 match self.editor.try_layout() {
441 Some(layout) => crate::convert::layout_to_scene_runs(layout, origin),
442 None => Vec::new(),
443 }
444 }
445
446 fn composing_range(&self) -> Option<Range<usize>> {
449 self.editor
450 .raw_compose()
451 .clone()
452 .or_else(|| self.platform_composing.clone())
453 }
454}
455
456fn sel<'a>(
458 drv: &mut parley::PlainEditorDriver<'a, Brush>,
459 select: bool,
460 mv: impl FnOnce(&mut parley::PlainEditorDriver<'a, Brush>),
461 ext: impl FnOnce(&mut parley::PlainEditorDriver<'a, Brush>),
462) {
463 if select {
464 ext(drv);
465 } else {
466 mv(drv);
467 }
468}
469
470fn bbox_to_rect(bbox: BoundingBox) -> Rect {
473 Rect::new(bbox.x0, bbox.y0, bbox.x1, bbox.y1)
474}
475
476pub fn byte_to_utf16(text: &str, byte_idx: usize) -> usize {
481 let mut boundary = byte_idx.min(text.len());
482 while boundary > 0 && !text.is_char_boundary(boundary) {
483 boundary -= 1;
484 }
485 text[..boundary].chars().map(char::len_utf16).sum()
486}
487
488pub fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize {
493 let mut units = 0usize;
494 for (byte_off, ch) in text.char_indices() {
495 if utf16_idx <= units {
496 return byte_off;
497 }
498 let next = units + ch.len_utf16();
499 if utf16_idx < next {
500 return byte_off;
502 }
503 units = next;
504 }
505 text.len()
506}
507
508pub fn sanitize_paste(text: &str, single_line: bool) -> Cow<'_, str> {
529 if single_line {
530 if text.contains('\r') || text.contains('\n') {
532 let sanitized = text.replace(['\r', '\n'], "");
533 Cow::Owned(sanitized)
534 } else {
535 Cow::Borrowed(text)
536 }
537 } else {
538 if text.contains('\r') {
540 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
542 Cow::Owned(normalized)
543 } else {
544 Cow::Borrowed(text)
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::style::{FontStyle, FontWeight};
553 use peniko::Color;
554
555 fn style() -> TextStyle {
556 TextStyle::new(24.0, Color::BLACK)
557 }
558
559 fn editor() -> (TextEditor, TextContext) {
560 (TextEditor::new(&style()), TextContext::new())
561 }
562
563 const EMOJI: &str = "\u{1F600}";
566 const FAMILY: &str = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}";
568
569 #[test]
570 fn insert_appends_text_and_advances_caret() {
571 let (mut ed, mut cx) = editor();
572 ed.apply(EditOp::Insert("Hi".into()), &mut cx);
573 assert_eq!(ed.text(), "Hi");
574 let st = ed.editing_state_bytes();
575 assert_eq!(st.base, 2);
576 assert_eq!(st.extent, 2);
577 }
578
579 #[test]
580 fn backdelete_over_emoji_removes_whole_code_point() {
581 let (mut ed, mut cx) = editor();
582 ed.apply(EditOp::Insert(format!("a{EMOJI}")), &mut cx);
583 assert_eq!(ed.text(), format!("a{EMOJI}"));
584 ed.apply(EditOp::Backdelete, &mut cx);
585 assert_eq!(ed.text(), "a");
587 }
588
589 #[test]
590 fn backdelete_over_zwj_sequence_removes_a_whole_code_point() {
591 let (mut ed, mut cx) = editor();
592 ed.apply(EditOp::Insert(format!("x{FAMILY}")), &mut cx);
593 let before = ed.text().len();
594 ed.apply(EditOp::Backdelete, &mut cx);
595 let after = ed.text();
596 assert_eq!(before - after.len(), 4, "one 4-byte code point removed");
601 assert!(after.starts_with('x'));
602 }
603
604 #[test]
605 fn forward_delete_removes_following_grapheme() {
606 let (mut ed, mut cx) = editor();
607 ed.apply(EditOp::Insert("abc".into()), &mut cx);
608 ed.apply(EditOp::Home { select: false }, &mut cx);
609 ed.apply(EditOp::Delete, &mut cx);
610 assert_eq!(ed.text(), "bc");
611 }
612
613 #[test]
614 fn move_left_right_adjust_caret_without_selecting() {
615 let (mut ed, mut cx) = editor();
616 ed.apply(EditOp::Insert("abc".into()), &mut cx);
617 ed.apply(EditOp::MoveLeft { select: false }, &mut cx);
618 assert_eq!(ed.editing_state_bytes().extent, 2);
619 let st = ed.editing_state_bytes();
620 assert_eq!(st.base, st.extent, "plain move must collapse the selection");
621 ed.apply(EditOp::MoveRight { select: false }, &mut cx);
622 assert_eq!(ed.editing_state_bytes().extent, 3);
623 }
624
625 #[test]
626 fn move_left_with_select_extends_selection() {
627 let (mut ed, mut cx) = editor();
628 ed.apply(EditOp::Insert("abc".into()), &mut cx);
629 ed.apply(EditOp::MoveLeft { select: true }, &mut cx);
630 let st = ed.editing_state_bytes();
631 assert_ne!(st.base, st.extent, "select variant must extend selection");
632 assert_eq!(st.base, 3);
633 assert_eq!(st.extent, 2);
634 }
635
636 #[test]
637 fn word_move_jumps_over_a_word() {
638 let (mut ed, mut cx) = editor();
639 ed.apply(EditOp::Insert("hello world".into()), &mut cx);
640 ed.apply(EditOp::Home { select: false }, &mut cx);
641 assert_eq!(ed.editing_state_bytes().extent, 0);
642 ed.apply(EditOp::MoveWordRight { select: false }, &mut cx);
643 let after = ed.editing_state_bytes().extent;
644 assert!(
645 after > 0 && after <= 6,
646 "word-right should land near the space, got {after}"
647 );
648 }
649
650 #[test]
651 fn home_and_end_move_to_line_bounds() {
652 let (mut ed, mut cx) = editor();
653 ed.apply(EditOp::Insert("hello".into()), &mut cx);
654 ed.apply(EditOp::Home { select: false }, &mut cx);
655 assert_eq!(ed.editing_state_bytes().extent, 0);
656 ed.apply(EditOp::End { select: false }, &mut cx);
657 assert_eq!(ed.editing_state_bytes().extent, 5);
658 }
659
660 #[test]
661 fn select_all_then_collapse() {
662 let (mut ed, mut cx) = editor();
663 ed.apply(EditOp::Insert("hello".into()), &mut cx);
664 ed.apply(EditOp::SelectAll, &mut cx);
665 let st = ed.editing_state_bytes();
666 assert_eq!(st.base.min(st.extent), 0);
667 assert_eq!(st.base.max(st.extent), 5);
668 ed.apply(EditOp::CollapseSelection, &mut cx);
669 let st = ed.editing_state_bytes();
670 assert_eq!(st.base, st.extent, "collapse must drop the anchor");
671 }
672
673 #[test]
674 fn insert_replaces_selection() {
675 let (mut ed, mut cx) = editor();
676 ed.apply(EditOp::Insert("hello".into()), &mut cx);
677 ed.apply(EditOp::SelectAll, &mut cx);
678 ed.apply(EditOp::Insert("bye".into()), &mut cx);
679 assert_eq!(ed.text(), "bye");
680 }
681
682 #[test]
683 fn compose_lifecycle_commit() {
684 let (mut ed, mut cx) = editor();
685 ed.apply(
686 EditOp::Compose {
687 text: "ni".into(),
688 cursor: Some((2, 2)),
689 },
690 &mut cx,
691 );
692 let st = ed.editing_state_bytes();
694 assert_eq!(st.composing, Some(0..2), "preedit range should be reported");
695 assert_eq!(ed.text(), "ni");
696 ed.apply(EditOp::FinishCompose, &mut cx);
697 let st = ed.editing_state_bytes();
698 assert_eq!(st.composing, None, "commit clears the composing region");
699 assert_eq!(ed.text(), "ni");
700 }
701
702 #[test]
703 fn compose_lifecycle_clear_reverts() {
704 let (mut ed, mut cx) = editor();
705 ed.apply(EditOp::Insert("a".into()), &mut cx);
706 ed.apply(
707 EditOp::Compose {
708 text: "xyz".into(),
709 cursor: None,
710 },
711 &mut cx,
712 );
713 assert_eq!(ed.text(), "axyz");
714 ed.apply(EditOp::ClearCompose, &mut cx);
715 assert_eq!(ed.text(), "a");
717 assert_eq!(ed.editing_state_bytes().composing, None);
718 }
719
720 #[test]
721 fn apply_editing_state_sets_text_selection_and_composing() {
722 let (mut ed, mut cx) = editor();
723 let state = EditingStateBytes {
724 text: "hello".into(),
725 base: 1,
726 extent: 3,
727 composing: Some(0..5),
728 };
729 ed.apply(EditOp::ApplyEditingState(state.clone()), &mut cx);
730 assert_eq!(ed.text(), "hello");
731 assert_eq!(ed.editing_state_bytes(), state);
732 }
733
734 #[test]
735 fn apply_editing_state_is_idempotent_and_short_circuits() {
736 let (mut ed, mut cx) = editor();
737 let state = EditingStateBytes {
738 text: "world".into(),
739 base: 2,
740 extent: 2,
741 composing: None,
742 };
743 ed.apply(EditOp::ApplyEditingState(state.clone()), &mut cx);
744 let first = ed.editing_state_bytes();
745 ed.apply(EditOp::ApplyEditingState(state.clone()), &mut cx);
747 assert_eq!(ed.editing_state_bytes(), first);
748 assert_eq!(ed.editing_state_bytes(), state);
749 }
750
751 #[test]
752 fn apply_editing_state_selection_only_change_keeps_text() {
753 let (mut ed, mut cx) = editor();
754 ed.apply(
755 EditOp::ApplyEditingState(EditingStateBytes {
756 text: "abcd".into(),
757 base: 0,
758 extent: 0,
759 composing: None,
760 }),
761 &mut cx,
762 );
763 ed.apply(
764 EditOp::ApplyEditingState(EditingStateBytes {
765 text: "abcd".into(),
766 base: 1,
767 extent: 3,
768 composing: None,
769 }),
770 &mut cx,
771 );
772 let st = ed.editing_state_bytes();
773 assert_eq!(st.text, "abcd");
774 assert_eq!((st.base, st.extent), (1, 3));
775 }
776
777 #[test]
778 fn editing_state_utf16_converts_offsets_for_emoji() {
779 let (mut ed, mut cx) = editor();
780 ed.apply(
782 EditOp::ApplyEditingState(EditingStateBytes {
783 text: format!("a{EMOJI}"),
784 base: 5,
785 extent: 5,
786 composing: Some(1..5),
787 }),
788 &mut cx,
789 );
790 let st = ed.editing_state_utf16();
791 assert_eq!(st.selection_base, 3, "1 + 2 UTF-16 units");
792 assert_eq!(st.selection_extent, 3);
793 assert_eq!(st.composing_base, 1);
794 assert_eq!(st.composing_extent, 3);
795 }
796
797 #[test]
798 fn editing_state_utf16_reports_minus_one_when_not_composing() {
799 let (mut ed, mut cx) = editor();
800 ed.apply(EditOp::Insert("hi".into()), &mut cx);
801 let st = ed.editing_state_utf16();
802 assert_eq!(st.composing_base, -1);
803 assert_eq!(st.composing_extent, -1);
804 }
805
806 #[test]
807 fn cursor_and_selection_geometry_are_sane() {
808 let (mut ed, mut cx) = editor();
809 ed.apply(EditOp::Insert("hello".into()), &mut cx);
810
811 let caret = ed
812 .cursor_rect(2.0)
813 .expect("caret should exist after typing");
814 assert!(
815 caret.width() > 0.0,
816 "caret width should reflect the requested size"
817 );
818 assert!(caret.height() > 0.0, "caret should have positive height");
819
820 ed.apply(EditOp::SelectAll, &mut cx);
821 let rects = ed.selection_rects();
822 assert!(
823 !rects.is_empty(),
824 "a non-empty selection should yield rects"
825 );
826 for r in &rects {
827 assert!(
828 r.width() > 0.0 && r.height() > 0.0,
829 "selection rect must be non-degenerate: {r:?}"
830 );
831 assert!(r.x1 >= r.x0 && r.y1 >= r.y0, "rect must be ordered: {r:?}");
832 }
833
834 let ime = ed.ime_cursor_area();
835 assert!(
836 ime.x1 >= ime.x0 && ime.y1 >= ime.y0,
837 "ime area must be ordered"
838 );
839 }
840
841 #[test]
842 fn to_scene_runs_translates_by_origin_and_is_empty_when_fresh() {
843 let (mut ed, mut cx) = editor();
844 assert!(ed.to_scene_runs(Point::ORIGIN).is_empty());
846
847 ed.apply(EditOp::Insert("Ag".into()), &mut cx);
848 let runs = ed.to_scene_runs(Point::new(10.0, 20.0));
849 assert!(!runs.is_empty(), "typed text should produce glyph runs");
850 for run in &runs {
851 assert_eq!(run.transform, kurbo::Affine::translate((10.0, 20.0)));
852 }
853 }
854
855 #[test]
858 fn byte_to_utf16_matrix() {
859 assert_eq!(byte_to_utf16("abc", 0), 0);
861 assert_eq!(byte_to_utf16("abc", 2), 2);
862 assert_eq!(byte_to_utf16("é", 0), 0);
864 assert_eq!(byte_to_utf16("é", 2), 1);
865 assert_eq!(byte_to_utf16("é", 1), 0);
867 assert_eq!(byte_to_utf16(EMOJI, 4), 2);
869 assert_eq!(byte_to_utf16(EMOJI, 2), 0, "mid-surrogate byte snaps back");
870 assert_eq!(byte_to_utf16(FAMILY, FAMILY.len()), 11);
872 assert_eq!(byte_to_utf16("abc", 99), 3);
874 }
875
876 #[test]
877 fn utf16_to_byte_matrix() {
878 assert_eq!(utf16_to_byte("abc", 0), 0);
880 assert_eq!(utf16_to_byte("abc", 2), 2);
881 assert_eq!(utf16_to_byte("é", 1), 2);
883 assert_eq!(utf16_to_byte(EMOJI, 2), 4);
885 assert_eq!(
886 utf16_to_byte(EMOJI, 1),
887 0,
888 "mid-surrogate snaps to char start"
889 );
890 assert_eq!(utf16_to_byte(FAMILY, 11), FAMILY.len());
892 assert_eq!(utf16_to_byte("abc", 99), 3);
894 }
895
896 #[test]
897 fn conversion_helpers_round_trip_on_boundaries() {
898 for text in ["", "abc", "é", EMOJI, FAMILY, "aé😀b"] {
899 let mut byte = 0;
900 for ch in text.chars() {
901 let units = byte_to_utf16(text, byte);
902 assert_eq!(
903 utf16_to_byte(text, units),
904 byte,
905 "round-trip failed at byte {byte} in {text:?}"
906 );
907 byte += ch.len_utf8();
908 }
909 let units = byte_to_utf16(text, text.len());
911 assert_eq!(utf16_to_byte(text, units), text.len());
912 }
913 }
914
915 #[test]
918 fn editor_letter_spacing_widens_layout_like_context_layout() {
919 let narrow_style = TextStyle::new(24.0, Color::BLACK);
923 let (mut narrow_ed, mut narrow_cx) = (TextEditor::new(&narrow_style), TextContext::new());
924 narrow_ed.apply(EditOp::Insert("Hello".into()), &mut narrow_cx);
925 let narrow_width = narrow_ed.layout_size().width;
926
927 let mut wide_style = TextStyle::new(24.0, Color::BLACK);
928 wide_style.letter_spacing = 20.0;
929 let (mut wide_ed, mut wide_cx) = (TextEditor::new(&wide_style), TextContext::new());
930 wide_ed.apply(EditOp::Insert("Hello".into()), &mut wide_cx);
931 let wide_width = wide_ed.layout_size().width;
932
933 assert!(
934 wide_width > narrow_width,
935 "the editor's own layout should widen with extra letter-spacing \
936 the same way TextContext::layout's does: {narrow_width} vs {wide_width}"
937 );
938 }
939
940 #[test]
943 fn single_line_mode_keeps_one_line_and_grows_horizontally() {
944 let (mut ed, mut cx) = editor();
947 ed.apply(
948 EditOp::Insert("the quick brown fox jumps over the lazy dog".into()),
949 &mut cx,
950 );
951 assert_eq!(ed.line_count(), 1, "single-line mode never wraps");
952 let one_line_h = ed.layout_size().height;
953 assert!(one_line_h > 0.0);
954 assert!(
955 ed.layout_size().width > 100.0,
956 "single line grows horizontally"
957 );
958 }
959
960 #[test]
961 fn wrap_width_produces_multiple_lines_and_grows_vertically() {
962 let (mut ed, mut cx) = editor();
963 ed.apply(
964 EditOp::Insert("the quick brown fox jumps over the lazy dog".into()),
965 &mut cx,
966 );
967 let single_h = ed.layout_size().height;
968
969 ed.set_wrap_width(Some(60.0), &mut cx);
972 assert!(
973 ed.line_count() > 1,
974 "narrow wrap width splits the run into multiple lines, got {}",
975 ed.line_count()
976 );
977 assert!(
978 ed.layout_size().height > single_h,
979 "wrapped multi-line layout is taller than the single line: {single_h} -> {}",
980 ed.layout_size().height
981 );
982
983 ed.set_wrap_width(None, &mut cx);
985 assert_eq!(ed.line_count(), 1, "None restores single-line mode");
986 }
987
988 #[test]
989 fn insert_newline_splits_into_two_lines() {
990 let (mut ed, mut cx) = editor();
991 ed.set_wrap_width(Some(400.0), &mut cx);
994 ed.apply(EditOp::Insert("ab".into()), &mut cx);
995 ed.apply(EditOp::InsertNewline, &mut cx);
996 ed.apply(EditOp::Insert("cd".into()), &mut cx);
997 assert_eq!(ed.text(), "ab\ncd");
998 assert_eq!(ed.line_count(), 2, "the newline creates a second line");
999 assert_eq!(ed.editing_state_bytes().extent, 5);
1001 }
1002
1003 #[test]
1004 fn move_up_down_navigate_across_a_newline() {
1005 let (mut ed, mut cx) = editor();
1006 ed.set_wrap_width(Some(400.0), &mut cx);
1007 ed.apply(EditOp::Insert("abc".into()), &mut cx);
1008 ed.apply(EditOp::InsertNewline, &mut cx);
1009 ed.apply(EditOp::Insert("xyz".into()), &mut cx);
1010 let bottom = ed.editing_state_bytes().extent;
1012 assert_eq!(bottom, 7);
1013
1014 ed.apply(EditOp::MoveUp { select: false }, &mut cx);
1016 let up = ed.editing_state_bytes().extent;
1017 assert!(up <= 3, "move-up crosses the newline onto line 1, got {up}");
1018
1019 ed.apply(EditOp::MoveDown { select: false }, &mut cx);
1021 let down = ed.editing_state_bytes().extent;
1022 assert!(
1023 down >= 4,
1024 "move-down crosses the newline back onto line 2, got {down}"
1025 );
1026 }
1027
1028 #[test]
1029 fn utf16_selection_indices_are_correct_across_a_newline() {
1030 let (mut ed, mut cx) = editor();
1031 ed.set_wrap_width(Some(400.0), &mut cx);
1032 ed.apply(
1035 EditOp::ApplyEditingState(EditingStateBytes {
1036 text: format!("a{EMOJI}\nb"),
1037 base: 0,
1038 extent: format!("a{EMOJI}\nb").len(),
1039 composing: None,
1040 }),
1041 &mut cx,
1042 );
1043 assert_eq!(ed.line_count(), 2, "the newline still splits the layout");
1044 let st = ed.editing_state_utf16();
1045 assert_eq!(st.selection_base, 0);
1047 assert_eq!(
1048 st.selection_extent, 5,
1049 "the newline counts as exactly one UTF-16 unit"
1050 );
1051 }
1052
1053 #[test]
1054 fn editor_applies_weight_and_style_edit_defaults_without_panicking() {
1055 let mut style = TextStyle::new(24.0, Color::BLACK);
1056 style.weight = FontWeight::BOLD;
1057 style.style = FontStyle::Italic;
1058 let (mut ed, mut cx) = (TextEditor::new(&style), TextContext::new());
1059 ed.apply(EditOp::Insert("Hi".into()), &mut cx);
1060 assert!(
1061 ed.layout_size().width > 0.0,
1062 "bold+italic edit-style defaults should still shape text"
1063 );
1064 }
1065
1066 #[test]
1069 fn selected_text_returns_none_when_collapsed() {
1070 let (mut ed, mut cx) = editor();
1071 ed.apply(EditOp::Insert("hello".into()), &mut cx);
1072 ed.apply(EditOp::Home { select: false }, &mut cx);
1073 assert_eq!(ed.selected_text(), None);
1075 }
1076
1077 #[test]
1078 fn selected_text_returns_slice_with_forward_selection() {
1079 let (mut ed, mut cx) = editor();
1080 ed.apply(EditOp::Insert("hello".into()), &mut cx);
1081 ed.apply(EditOp::Home { select: false }, &mut cx);
1082 ed.apply(EditOp::MoveRight { select: true }, &mut cx);
1083 ed.apply(EditOp::MoveRight { select: true }, &mut cx);
1084 assert_eq!(ed.selected_text(), Some("he"));
1086 }
1087
1088 #[test]
1089 fn selected_text_returns_same_slice_with_backward_selection() {
1090 let (mut ed, mut cx) = editor();
1091 ed.apply(EditOp::Insert("hello".into()), &mut cx);
1092 ed.apply(EditOp::End { select: false }, &mut cx);
1093 ed.apply(EditOp::MoveLeft { select: true }, &mut cx);
1094 ed.apply(EditOp::MoveLeft { select: true }, &mut cx);
1095 assert_eq!(ed.selected_text(), Some("lo"));
1097 }
1098
1099 #[test]
1100 fn selected_text_spans_multi_byte_emoji_correctly() {
1101 let (mut ed, mut cx) = editor();
1102 ed.apply(EditOp::Insert(format!("a{EMOJI}b")), &mut cx);
1104 ed.apply(EditOp::SelectAll, &mut cx);
1106 let selected = ed.selected_text().expect("full selection should exist");
1108 assert_eq!(selected, format!("a{EMOJI}b"));
1109 }
1110
1111 #[test]
1112 fn selected_text_can_select_just_the_emoji() {
1113 let (mut ed, mut cx) = editor();
1114 ed.apply(EditOp::Insert(format!("a{EMOJI}b")), &mut cx);
1116 ed.apply(EditOp::Home { select: false }, &mut cx);
1118 ed.apply(EditOp::MoveRight { select: false }, &mut cx);
1119 ed.apply(EditOp::MoveRight { select: true }, &mut cx);
1120 assert_eq!(ed.selected_text(), Some(EMOJI));
1122 }
1123
1124 #[test]
1127 fn sanitize_paste_single_line_removes_crlf() {
1128 let result = sanitize_paste("a\r\nb\n", true);
1129 assert_eq!(result, Cow::Borrowed::<str>("ab") as Cow<str>);
1130 assert_eq!(result.as_ref(), "ab");
1131 }
1132
1133 #[test]
1134 fn sanitize_paste_single_line_removes_lone_cr() {
1135 let result = sanitize_paste("a\rb", true);
1136 assert_eq!(result.as_ref(), "ab");
1137 }
1138
1139 #[test]
1140 fn sanitize_paste_single_line_removes_lone_lf() {
1141 let result = sanitize_paste("a\nb", true);
1142 assert_eq!(result.as_ref(), "ab");
1143 }
1144
1145 #[test]
1146 fn sanitize_paste_multi_line_normalizes_crlf_and_cr() {
1147 let result = sanitize_paste("a\r\nb\rc", false);
1148 assert_eq!(result.as_ref(), "a\nb\nc");
1149 }
1150
1151 #[test]
1152 fn sanitize_paste_multi_line_keeps_existing_lf() {
1153 let result = sanitize_paste("a\nb\nc", false);
1154 assert_eq!(result, Cow::Borrowed("a\nb\nc"));
1155 }
1156
1157 #[test]
1158 fn sanitize_paste_returns_borrowed_when_unchanged() {
1159 let text = "hello world";
1160 let result_single = sanitize_paste(text, true);
1161 assert_eq!(result_single, Cow::Borrowed(text));
1162
1163 let result_multi = sanitize_paste(text, false);
1164 assert_eq!(result_multi, Cow::Borrowed(text));
1165 }
1166
1167 #[test]
1168 fn sanitize_paste_single_line_multiple_newlines() {
1169 let result = sanitize_paste("line1\nline2\nline3", true);
1170 assert_eq!(result.as_ref(), "line1line2line3");
1171 }
1172
1173 #[test]
1174 fn sanitize_paste_multi_line_complex_mixed_endings() {
1175 let result = sanitize_paste("a\r\nb\rc\nd", false);
1177 assert_eq!(result.as_ref(), "a\nb\nc\nd");
1178 }
1179
1180 #[test]
1181 fn sanitize_paste_empty_string() {
1182 let result_single = sanitize_paste("", true);
1183 assert_eq!(result_single, Cow::Borrowed(""));
1184
1185 let result_multi = sanitize_paste("", false);
1186 assert_eq!(result_multi, Cow::Borrowed(""));
1187 }
1188
1189 #[test]
1190 fn sanitize_paste_only_newlines_single_line() {
1191 let result = sanitize_paste("\r\n\n\r", true);
1192 assert_eq!(result.as_ref(), "");
1193 }
1194
1195 #[test]
1196 fn sanitize_paste_preserves_other_whitespace() {
1197 let text = "a \t b";
1198 let result = sanitize_paste(text, true);
1199 assert_eq!(result.as_ref(), text);
1200 }
1201}