1use blitz_traits::{
2 events::{BlitzImeEvent, BlitzKeyEvent},
3 node_id::NodeId,
4 shell::ShellProvider,
5};
6use keyboard_types::{Code, Key, Modifiers};
7use parley::{ContentWidths, FontContext, LayoutContext};
8
9use crate::util::{ACTION_MOD, has_clipboard_modifier};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12enum ClipboardCommand {
13 Copy,
14 Cut,
15 Paste,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19enum HistoryCommand {
20 Undo,
21 Redo,
22}
23
24fn history_command(event: &BlitzKeyEvent) -> Option<HistoryCommand> {
30 if !has_clipboard_modifier(event.modifiers) {
31 return None;
32 }
33 let shift = event.modifiers.contains(Modifiers::SHIFT);
34 let is = |code: Code, ch: &str| {
35 event.code == code || matches!(&event.key, Key::Character(c) if c.eq_ignore_ascii_case(ch))
36 };
37
38 if is(Code::KeyZ, "z") {
39 return Some(if shift {
40 HistoryCommand::Redo
41 } else {
42 HistoryCommand::Undo
43 });
44 }
45 if is(Code::KeyY, "y") {
46 return Some(HistoryCommand::Redo);
47 }
48 None
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
63struct TextEditSnapshot {
64 text: String,
65 anchor: usize,
68 focus: usize,
69}
70
71#[derive(Debug, Default)]
89pub struct TextEditHistory {
90 undo: Vec<TextEditSnapshot>,
93 redo: Vec<TextEditSnapshot>,
95 current: Option<TextEditSnapshot>,
98 burst: Option<TextEditSnapshot>,
102 applying: bool,
105}
106
107const MAX_UNDO_DEPTH: usize = 200;
110
111impl TextEditHistory {
112 fn continues_burst(previous: &TextEditSnapshot, next: &TextEditSnapshot) -> bool {
124 if next.text.len() <= previous.text.len() {
126 return false;
127 }
128 if previous.anchor != previous.focus || next.anchor != next.focus {
129 return false;
130 }
131 let caret = previous.focus;
134 if caret > previous.text.len() || next.focus <= caret {
135 return false;
136 }
137 let added = next.focus - caret;
138 if next.text.len() != previous.text.len() + added {
139 return false;
140 }
141 if previous.text.get(..caret) != next.text.get(..caret) {
142 return false;
143 }
144 if previous.text.get(caret..) != next.text.get(next.focus..) {
145 return false;
146 }
147
148 !next.text[caret..next.focus]
150 .chars()
151 .any(|c| c.is_whitespace())
152 }
153
154 fn record(&mut self, snapshot: TextEditSnapshot) {
161 if self.applying {
162 return;
163 }
164
165 let Some(previous) = self.current.clone() else {
166 self.current = Some(snapshot);
169 return;
170 };
171 if previous == snapshot {
172 return;
173 }
174
175 self.redo.clear();
180
181 let burst = self.burst.as_ref().unwrap_or(&previous);
186 if Self::continues_burst(burst, &snapshot) {
187 self.burst = Some(burst.clone());
190 self.current = Some(snapshot);
191 return;
192 }
193
194 let entry = self.burst.take().unwrap_or(previous);
196 self.current = Some(snapshot);
197 self.undo.push(entry);
198 if self.undo.len() > MAX_UNDO_DEPTH {
199 self.undo.remove(0);
200 }
201 }
202
203 fn undo(&mut self, now: TextEditSnapshot) -> Option<TextEditSnapshot> {
205 if let Some(burst) = self.burst.take() {
209 if burst != now {
210 self.undo.push(burst);
211 }
212 }
213 let restore = self.undo.pop()?;
214 self.redo.push(now);
215 self.current = Some(restore.clone());
216 Some(restore)
217 }
218
219 fn redo(&mut self, now: TextEditSnapshot) -> Option<TextEditSnapshot> {
221 let restore = self.redo.pop()?;
222 self.undo.push(now);
223 self.burst = None;
225 self.current = Some(restore.clone());
226 Some(restore)
227 }
228}
229
230fn clipboard_command(event: &BlitzKeyEvent) -> Option<ClipboardCommand> {
231 if !has_clipboard_modifier(event.modifiers) {
232 return None;
233 }
234 match event.code {
235 Code::KeyC => Some(ClipboardCommand::Copy),
236 Code::KeyX => Some(ClipboardCommand::Cut),
237 Code::KeyV => Some(ClipboardCommand::Paste),
238 _ => match &event.key {
239 Key::Character(c) if c.eq_ignore_ascii_case("c") => Some(ClipboardCommand::Copy),
240 Key::Character(c) if c.eq_ignore_ascii_case("x") => Some(ClipboardCommand::Cut),
241 Key::Character(c) if c.eq_ignore_ascii_case("v") => Some(ClipboardCommand::Paste),
242 _ => None,
243 },
244 }
245}
246
247#[derive(Debug, Clone, Copy, Default, PartialEq)]
248pub struct TextBrush {
250 pub id: NodeId,
252}
253
254impl TextBrush {
255 pub(crate) fn from_id(id: NodeId) -> Self {
256 Self { id }
257 }
258}
259
260#[derive(Clone, Debug)]
265pub struct CachedContentWidths {
266 inline_box_widths: Box<[u32]>,
271 widths: ContentWidths,
272}
273
274#[derive(Clone, Default)]
275pub struct TextLayout {
276 pub text: String,
277 pub content_widths: Option<CachedContentWidths>,
278 pub layout: parley::layout::Layout<TextBrush>,
279 pub laid_out_at: Option<f32>,
294}
295
296impl TextLayout {
297 pub fn new() -> Self {
298 Default::default()
299 }
300
301 pub fn content_widths(&mut self) -> ContentWidths {
327 let inline_box_widths: Box<[u32]> = self
331 .layout
332 .inline_boxes()
333 .iter()
334 .map(|ibox| ibox.width.to_bits())
335 .collect();
336
337 if let Some(cached) = &self.content_widths
338 && cached.inline_box_widths == inline_box_widths
339 {
340 return cached.widths;
341 }
342
343 let widths = self.layout.calculate_content_widths();
344 self.content_widths = Some(CachedContentWidths {
345 inline_box_widths,
346 widths,
347 });
348 widths
349 }
350}
351
352impl std::fmt::Debug for TextLayout {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 write!(f, "TextLayout")
355 }
356}
357
358pub enum GeneratedTextInputEvent {
360 Input,
361 Select,
362 PreEditChange,
363 Submit,
364}
365
366pub struct TextInputData {
367 pub editor: Box<parley::PlainEditor<TextBrush>>,
369 pub placeholder_editor: Option<Box<parley::PlainEditor<TextBrush>>>,
371 history: TextEditHistory,
374 pub is_multiline: bool,
376 pub scroll_offset: f32,
382 pub layout_width: Option<f32>,
383}
384
385impl Clone for TextInputData {
387 fn clone(&self) -> Self {
388 TextInputData::new(self.is_multiline)
389 }
390}
391
392impl TextInputData {
393 pub fn new(is_multiline: bool) -> Self {
394 let editor = Box::new(parley::PlainEditor::new(16.0));
395 Self {
396 editor,
397 placeholder_editor: None,
398 history: TextEditHistory::default(),
399 is_multiline,
400 scroll_offset: 0.0,
401 layout_width: None,
402 }
403 }
404
405 fn snapshot(&self) -> TextEditSnapshot {
407 let selection = self.editor.raw_selection();
408 TextEditSnapshot {
409 text: self.editor.raw_text().to_string(),
410 anchor: selection.anchor().index(),
411 focus: selection.focus().index(),
412 }
413 }
414
415 fn record_history(&mut self) {
417 let snapshot = self.snapshot();
418 self.history.record(snapshot);
419 }
420
421 fn restore(
427 &mut self,
428 font_ctx: &mut FontContext,
429 layout_ctx: &mut LayoutContext<TextBrush>,
430 snapshot: &TextEditSnapshot,
431 ) {
432 self.history.applying = true;
433 self.editor.set_text(&snapshot.text);
434 let mut driver = self.editor.driver(font_ctx, layout_ctx);
435 let len = snapshot.text.len();
439 let anchor = snapshot.anchor.min(len);
440 let focus = snapshot.focus.min(len);
441 if anchor == focus {
442 driver.move_to_byte(focus);
443 } else {
444 driver.select_byte_range(anchor, focus);
445 }
446 self.history.applying = false;
447 }
448
449 fn apply_history_command(
451 &mut self,
452 font_ctx: &mut FontContext,
453 layout_ctx: &mut LayoutContext<TextBrush>,
454 command: HistoryCommand,
455 ) -> Option<GeneratedTextInputEvent> {
456 let now = self.snapshot();
457 let restore = match command {
458 HistoryCommand::Undo => self.history.undo(now),
459 HistoryCommand::Redo => self.history.redo(now),
460 }?;
461 self.restore(font_ctx, layout_ctx, &restore);
462 Some(GeneratedTextInputEvent::Input)
463 }
464
465 pub fn content_height(&self) -> Option<f32> {
473 self.editor
474 .try_layout()
475 .map(|layout| layout.height() / layout.scale())
476 }
477
478 fn apply_layout_width(&mut self) {
486 let Some(width) = self.layout_width else {
487 return;
488 };
489 self.editor.set_width(Some(width * self.editor.get_scale()));
490 if let Some(placeholder) = self.placeholder_editor.as_mut() {
491 placeholder.set_width(Some(width * placeholder.get_scale()));
492 }
493 }
494
495 pub fn sync_multiline_width(
496 &mut self,
497 font_ctx: &mut FontContext,
498 layout_ctx: &mut LayoutContext<TextBrush>,
499 width: f32,
500 ) {
501 if !self.is_multiline || width <= 0.0 {
502 return;
503 }
504 if self
505 .layout_width
506 .is_some_and(|current| (current - width).abs() < 0.01)
507 {
508 return;
509 }
510 self.layout_width = Some(width);
511 self.apply_layout_width();
512 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
513 if let Some(placeholder) = self.placeholder_editor.as_mut() {
514 placeholder.driver(font_ctx, layout_ctx).refresh_layout();
515 }
516 }
517
518 pub fn set_text(
519 &mut self,
520 font_ctx: &mut FontContext,
521 layout_ctx: &mut LayoutContext<TextBrush>,
522 text: &str,
523 ) {
524 if self.editor.text() != text {
525 self.editor.set_text(text);
526 self.apply_layout_width();
540 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
541 self.editor.driver(font_ctx, layout_ctx).move_to_text_end();
555 }
556 }
557
558 pub fn clamp_scroll_offset(&mut self, content_box_width: f32, content_box_height: f32) {
564 let Some(layout) = self.editor.try_layout() else {
565 return;
566 };
567 let scale = layout.scale();
570
571 let Some(caret) = self.editor.cursor_geometry(1.5) else {
573 return;
574 };
575
576 let (caret_start, caret_end, content, viewport) = if self.is_multiline {
578 (
579 caret.y0 as f32 / scale,
580 caret.y1 as f32 / scale,
581 layout.height() / scale,
582 content_box_height,
583 )
584 } else {
585 (
586 caret.x0 as f32 / scale,
587 caret.x1 as f32 / scale,
588 layout.full_width() / scale,
589 content_box_width,
590 )
591 };
592
593 let mut offset = self.scroll_offset;
594
595 if caret_end > offset + viewport {
597 offset = caret_end - viewport;
598 }
599 if caret_start < offset {
600 offset = caret_start;
601 }
602
603 let max_offset = (content.max(caret_end) - viewport).max(0.0);
607 self.scroll_offset = offset.clamp(0.0, max_offset);
608 }
609
610 pub fn max_scroll_offset(&self, content_box_width: f32, content_box_height: f32) -> f32 {
617 let Some(layout) = self.editor.try_layout() else {
618 return 0.0;
619 };
620 let scale = layout.scale();
621 let (content, viewport) = if self.is_multiline {
622 (layout.height() / scale, content_box_height)
623 } else {
624 (layout.full_width() / scale, content_box_width)
625 };
626 (content - viewport).max(0.0)
627 }
628
629 pub fn scroll_by(
636 &mut self,
637 delta: f32,
638 content_box_width: f32,
639 content_box_height: f32,
640 ) -> f32 {
641 let max_offset = self.max_scroll_offset(content_box_width, content_box_height);
642 if max_offset <= 0.0 {
643 return delta;
644 }
645
646 let new_offset = (self.scroll_offset - delta).clamp(0.0, max_offset);
649 let consumed = self.scroll_offset - new_offset;
650 self.scroll_offset = new_offset;
651 delta - consumed
652 }
653
654 pub(crate) fn apply_keypress_event(
655 &mut self,
656 font_ctx: &mut FontContext,
657 layout_ctx: &mut LayoutContext<TextBrush>,
658 shell_provider: &dyn ShellProvider,
659 event: BlitzKeyEvent,
660 ) -> Option<GeneratedTextInputEvent> {
661 if !event.state.is_pressed() {
663 return None;
664 }
665
666 if let Some(command) = history_command(&event) {
669 return self.apply_history_command(font_ctx, layout_ctx, command);
670 }
671
672 self.record_history();
677
678 let mods = event.modifiers;
679 let shift = mods.contains(Modifiers::SHIFT);
680 let action_mod = mods.contains(ACTION_MOD);
681 let word_mod = mods.contains(Modifiers::ALT);
682 let is_multiline = self.is_multiline;
683 let editor = &mut self.editor;
684 let mut driver = editor.driver(font_ctx, layout_ctx);
685 if let Some(command) = clipboard_command(&event) {
686 match command {
687 ClipboardCommand::Copy => {
688 if let Some(text) = driver.editor.selected_text() {
689 let _ = shell_provider.set_clipboard_text(text.to_owned());
690 }
691 }
692 ClipboardCommand::Cut => {
693 if let Some(text) = driver.editor.selected_text() {
694 let _ = shell_provider.set_clipboard_text(text.to_owned());
695 driver.delete_selection()
696 }
697 }
698 ClipboardCommand::Paste => {
699 let text = shell_provider.get_clipboard_text().unwrap_or_default();
700 driver.insert_or_replace_selection(&text)
701 }
702 }
703
704 return Some(GeneratedTextInputEvent::Input);
705 }
706 match event.key {
707 Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
708 if shift {
709 driver.collapse_selection()
710 } else {
711 driver.select_all()
712 }
713 return Some(GeneratedTextInputEvent::Select);
714 }
715 Key::ArrowLeft => {
716 if action_mod {
717 if shift {
718 driver.select_to_line_start()
719 } else {
720 driver.move_to_line_start()
721 }
722 } else if word_mod {
723 if shift {
724 driver.select_word_left()
725 } else {
726 driver.move_word_left()
727 }
728 } else if shift {
729 driver.select_left()
730 } else {
731 driver.move_left()
732 }
733 return Some(GeneratedTextInputEvent::Select);
734 }
735 Key::ArrowRight => {
736 if action_mod {
737 if shift {
738 driver.select_to_line_end()
739 } else {
740 driver.move_to_line_end()
741 }
742 } else if word_mod {
743 if shift {
744 driver.select_word_right()
745 } else {
746 driver.move_word_right()
747 }
748 } else if shift {
749 driver.select_right()
750 } else {
751 driver.move_right()
752 }
753 return Some(GeneratedTextInputEvent::Select);
754 }
755 Key::ArrowUp => {
756 if action_mod && shift {
757 driver.select_to_text_start()
758 } else if action_mod {
759 driver.move_to_text_start()
760 } else if shift {
761 driver.select_up()
762 } else {
763 driver.move_up()
764 }
765 return Some(GeneratedTextInputEvent::Select);
766 }
767 Key::ArrowDown => {
768 if action_mod && shift {
769 driver.select_to_text_end()
770 } else if action_mod {
771 driver.move_to_text_end()
772 } else if shift {
773 driver.select_down()
774 } else {
775 driver.move_down()
776 }
777 return Some(GeneratedTextInputEvent::Select);
778 }
779 Key::Home => {
780 if action_mod {
781 if shift {
782 driver.select_to_text_start()
783 } else {
784 driver.move_to_text_start()
785 }
786 } else if shift {
787 driver.select_to_line_start()
788 } else {
789 driver.move_to_line_start()
790 }
791 return Some(GeneratedTextInputEvent::Select);
792 }
793 Key::End => {
794 if action_mod {
795 if shift {
796 driver.select_to_text_end()
797 } else {
798 driver.move_to_text_end()
799 }
800 } else if shift {
801 driver.select_to_line_end()
802 } else {
803 driver.move_to_line_end()
804 }
805 return Some(GeneratedTextInputEvent::Select);
806 }
807 Key::Delete => {
808 #[cfg(target_os = "macos")]
809 if mods.contains(Modifiers::SUPER) {
810 if driver.editor.raw_selection().is_collapsed() {
811 driver.select_to_line_end();
812 }
813 driver.delete_selection();
814 } else if mods.contains(Modifiers::ALT) {
815 driver.delete_word();
816 } else {
817 driver.delete();
818 }
819 #[cfg(not(target_os = "macos"))]
820 if action_mod {
821 driver.delete_word();
822 } else {
823 driver.delete();
824 }
825 return Some(GeneratedTextInputEvent::Input);
826 }
827 Key::Backspace => {
828 #[cfg(target_os = "macos")]
829 if mods.contains(Modifiers::SUPER) {
830 if driver.editor.raw_selection().is_collapsed() {
831 driver.select_to_line_start();
832 }
833 driver.delete_selection();
834 } else if mods.contains(Modifiers::ALT) {
835 driver.backdelete_word();
836 } else {
837 driver.backdelete();
838 }
839 #[cfg(not(target_os = "macos"))]
840 if action_mod {
841 driver.backdelete_word();
842 } else {
843 driver.backdelete();
844 }
845 return Some(GeneratedTextInputEvent::Input);
846 }
847
848 Key::Character(c) if c == "\n" => {
849 if is_multiline {
850 driver.insert_or_replace_selection("\n");
851 return Some(GeneratedTextInputEvent::Input);
852 } else {
853 return Some(GeneratedTextInputEvent::Submit);
854 }
855 }
856 Key::Enter => {
857 if is_multiline {
858 driver.insert_or_replace_selection("\n");
859 return Some(GeneratedTextInputEvent::Input);
860 } else {
861 return Some(GeneratedTextInputEvent::Submit);
862 }
863 }
864 Key::Character(s)
865 if !mods.contains(Modifiers::CONTROL) && !mods.contains(Modifiers::SUPER) =>
866 {
867 driver.insert_or_replace_selection(&s);
868 return Some(GeneratedTextInputEvent::Input);
869 }
870 _ => {}
871 };
872
873 None
874 }
875
876 pub(crate) fn apply_apple_standard_keybinding(
877 &mut self,
878 font_ctx: &mut FontContext,
879 layout_ctx: &mut LayoutContext<TextBrush>,
880 shell_provider: &dyn ShellProvider,
881 command: &str,
882 ) -> Option<GeneratedTextInputEvent> {
883 self.record_history();
887
888 let editor = &mut self.editor;
889 let mut driver = editor.driver(font_ctx, layout_ctx);
890 let is_multiline = self.is_multiline;
891
892 match command {
893 "insertBacktab:" => {}
897 "insertContainerBreak:" => {}
899 "insertDoubleQuoteIgnoringSubstitution:" => {
901 driver.insert_or_replace_selection("\"");
902 return Some(GeneratedTextInputEvent::Input);
903 }
904 "insertLineBreak:" => {
906 driver.insert_or_replace_selection("\n");
907 return Some(GeneratedTextInputEvent::Input);
908 }
909 "insertNewline:" => {
911 if is_multiline {
912 driver.insert_or_replace_selection("\n");
913 return Some(GeneratedTextInputEvent::Input);
914 } else {
915 return Some(GeneratedTextInputEvent::Submit);
916 }
917 }
918 "insertNewlineIgnoringFieldEditor:" => {
920 driver.insert_or_replace_selection("\n");
921 return Some(GeneratedTextInputEvent::Input);
922 }
923 "insertParagraphSeparator:" => {
925 driver.insert_or_replace_selection("\n");
926 return Some(GeneratedTextInputEvent::Input);
927 }
928 "insertSingleQuoteIgnoringSubstitution:" => {
929 driver.insert_or_replace_selection("'");
930 return Some(GeneratedTextInputEvent::Input);
931 }
932 "insertTab:" | "insertTabIgnoringFieldEditor:" => {
934 }
936 "insertText:" => {}
938
939 "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {}
945 "deleteForward:" => {}
946 "deleteToBeginningOfLine:" => {
948 if driver.editor.raw_selection().is_collapsed() {
949 driver.select_to_line_start();
950 }
951 driver.delete_selection();
952 return Some(GeneratedTextInputEvent::Input);
953 }
954 "deleteToEndOfLine:" => {
956 if driver.editor.raw_selection().is_collapsed() {
957 driver.select_to_line_end();
958 }
959 driver.delete_selection();
960 return Some(GeneratedTextInputEvent::Input);
961 }
962 "deleteToBeginningOfParagraph:" => {
963 if driver.editor.raw_selection().is_collapsed() {
964 driver.select_to_hard_line_start();
965 }
966 driver.delete_selection();
967 return Some(GeneratedTextInputEvent::Input);
968 }
969
970 "deleteToEndOfParagraph:" => {
972 if driver.editor.raw_selection().is_collapsed() {
973 driver.select_to_hard_line_end();
974 }
975 driver.delete_selection();
976 return Some(GeneratedTextInputEvent::Input);
977 }
978 "deleteWordBackward:" => {}
980 "deleteWordForward:" => {}
982 "yank:" => {
984 if let Some(text) = driver.editor.selected_text() {
985 let _ = shell_provider.set_clipboard_text(text.to_owned());
986 driver.delete_selection();
987 return Some(GeneratedTextInputEvent::Input);
988 }
989 }
990
991 "moveBackward:" => {
995 driver.move_left(); return Some(GeneratedTextInputEvent::Select);
997 }
998
999 "moveDown:" => {
1001 driver.move_down();
1002 return Some(GeneratedTextInputEvent::Select);
1003 }
1004 "moveForward:" => {
1006 driver.move_right();
1007 return Some(GeneratedTextInputEvent::Select);
1008 } "moveLeft:" => {
1012 driver.move_left();
1013 return Some(GeneratedTextInputEvent::Select);
1014 }
1015 "moveRight:" => {
1017 driver.move_right();
1018 return Some(GeneratedTextInputEvent::Select);
1019 }
1020 "moveUp:" => {
1022 driver.move_up();
1023 return Some(GeneratedTextInputEvent::Select);
1024 }
1025
1026 "moveBackwardAndModifySelection:" => {
1030 driver.select_left(); return Some(GeneratedTextInputEvent::Select);
1032 }
1033 "moveDownAndModifySelection:" => {
1035 driver.select_down();
1036 return Some(GeneratedTextInputEvent::Select);
1037 }
1038 "moveForwardAndModifySelection:" => {
1040 driver.select_right(); return Some(GeneratedTextInputEvent::Select);
1042 }
1043 "moveLeftAndModifySelection:" => {
1045 driver.select_left();
1046 return Some(GeneratedTextInputEvent::Select);
1047 }
1048 "moveRightAndModifySelection:" => {
1050 driver.select_right();
1051 return Some(GeneratedTextInputEvent::Select);
1052 }
1053 "moveUpAndModifySelection:" => {
1055 driver.select_up();
1056 return Some(GeneratedTextInputEvent::Select);
1057 }
1058
1059 "selectAll:" => {
1061 driver.select_all();
1062 return Some(GeneratedTextInputEvent::Select);
1063 }
1064 "selectLine:" => {
1065 driver.move_to_line_start();
1066 driver.select_to_line_end();
1067 return Some(GeneratedTextInputEvent::Select);
1068 }
1069 "selectParagraph:" => {
1070 driver.move_to_hard_line_start();
1071 driver.select_to_hard_line_end();
1072 return Some(GeneratedTextInputEvent::Select);
1073 }
1074 "selectWord:" => {
1075 }
1077
1078 "moveToBeginningOfDocument:" => {
1080 driver.move_to_text_start();
1081 return Some(GeneratedTextInputEvent::Select);
1082 }
1083 "moveToBeginningOfDocumentAndModifySelection:" => {
1084 driver.select_to_text_start();
1085 return Some(GeneratedTextInputEvent::Select);
1086 }
1087 "moveToEndOfDocument:" => {
1088 driver.move_to_text_end();
1089 return Some(GeneratedTextInputEvent::Select);
1090 }
1091 "moveToEndOfDocumentAndModifySelection:" => {
1092 driver.move_to_text_end();
1093 return Some(GeneratedTextInputEvent::Select);
1094 }
1095
1096 "moveParagraphBackwardAndModifySelection:" => {}
1098 "moveParagraphForwardAndModifySelection:" => {}
1099 "moveToBeginningOfParagraph:" => {
1100 driver.move_to_hard_line_start();
1101 return Some(GeneratedTextInputEvent::Select);
1102 }
1103 "moveToBeginningOfParagraphAndModifySelection:" => {
1104 driver.select_to_hard_line_start();
1105 return Some(GeneratedTextInputEvent::Select);
1106 }
1107 "moveToEndOfParagraph:" => {
1108 driver.move_to_hard_line_end();
1109 return Some(GeneratedTextInputEvent::Select);
1110 }
1111 "moveToEndOfParagraphAndModifySelection:" => {
1112 driver.select_to_hard_line_end();
1113 return Some(GeneratedTextInputEvent::Select);
1114 }
1115
1116 "moveToBeginningOfLine:" => {
1118 driver.move_to_line_start();
1119 return Some(GeneratedTextInputEvent::Select);
1120 }
1121 "moveToBeginningOfLineAndModifySelection:" => {
1122 driver.select_to_line_start();
1123 return Some(GeneratedTextInputEvent::Select);
1124 }
1125 "moveToEndOfLine:" => {
1126 driver.move_to_line_end();
1127 return Some(GeneratedTextInputEvent::Select);
1128 }
1129 "moveToEndOfLineAndModifySelection:" => {
1130 driver.select_to_line_end();
1131 return Some(GeneratedTextInputEvent::Select);
1132 }
1133 "moveToLeftEndOfLine:" => {
1134 driver.move_to_text_start();
1135 return Some(GeneratedTextInputEvent::Select);
1136 }
1137 "moveToLeftEndOfLineAndModifySelection:" => {
1138 driver.select_to_line_start();
1139 return Some(GeneratedTextInputEvent::Select);
1140 }
1141 "moveToRightEndOfLine:" => {
1142 driver.move_to_line_end();
1143 return Some(GeneratedTextInputEvent::Select);
1144 }
1145 "moveToRightEndOfLineAndModifySelection:" => {
1146 driver.select_to_line_end();
1147 return Some(GeneratedTextInputEvent::Select);
1148 }
1149
1150 "moveWordBackward:" => {
1152 driver.move_word_left();
1153 return Some(GeneratedTextInputEvent::Select);
1154 }
1155 "moveWordBackwardAndModifySelection:" => {
1156 driver.select_word_left();
1157 return Some(GeneratedTextInputEvent::Select);
1158 }
1159 "moveWordForward:" => {
1160 driver.move_word_right();
1161 return Some(GeneratedTextInputEvent::Select);
1162 }
1163 "moveWordForwardAndModifySelection:" => {
1164 driver.select_word_right();
1165 return Some(GeneratedTextInputEvent::Select);
1166 }
1167 "moveWordLeft:" => {
1168 driver.move_word_left();
1169 return Some(GeneratedTextInputEvent::Select);
1170 }
1171 "moveWordLeftAndModifySelection:" => {
1172 driver.select_word_left();
1173 return Some(GeneratedTextInputEvent::Select);
1174 }
1175 "moveWordRight:" => {
1176 driver.move_word_right();
1177 return Some(GeneratedTextInputEvent::Select);
1178 }
1179 "moveWordRightAndModifySelection:" => {
1180 driver.select_word_right();
1181 return Some(GeneratedTextInputEvent::Select);
1182 }
1183
1184 "scrollPageDown:" => {}
1188 "scrollPageUp:" => {}
1190 "scrollLineDown:" => {}
1192 "scrollLineUp:" => {}
1194 "scrollToBeginningOfDocument:" => {}
1196 "scrollToEndOfDocument:" => {}
1198 "pageDown:" => {}
1200 "pageUp:" => {}
1202 "pageDownAndModifySelection:" => {}
1204 "pageUpAndModifySelection:" => {}
1206 "centerSelectionInVisibleArea:" => {}
1208
1209 "transpose:" => {}
1213 "transposeWords:" => {}
1215
1216 "indent:" => {}
1219
1220 "cancelOperation:" => {}
1223
1224 "quickLookPreviewItems:" => {}
1227
1228 "makeBaseWritingDirectionLeftToRight:" => {}
1230 "makeBaseWritingDirectionNatural:" => {}
1231 "makeBaseWritingDirectionRightToLeft:" => {}
1232 "makeTextWritingDirectionLeftToRight:" => {}
1233 "makeTextWritingDirectionNatural:" => {}
1234 "makeTextWritingDirectionRightToLeft:" => {}
1235
1236 "capitalizeWord:" => {}
1238 "changeCaseOfLetter:" => {}
1239 "lowercaseWord:" => {}
1240 "uppercaseWord:" => {}
1241
1242 "setMark:" => {}
1244 "selectToMark:" => {}
1245 "deleteToMark:" => {}
1246 "swapWithMark:" => {}
1247
1248 "complete:" => {}
1250
1251 "showContextMenuForSelection:" => {}
1253
1254 _ => {}
1256 };
1257
1258 None
1259 }
1260
1261 pub(crate) fn apply_ime_event(
1262 &mut self,
1263 font_ctx: &mut FontContext,
1264 layout_ctx: &mut LayoutContext<TextBrush>,
1265 event: BlitzImeEvent,
1266 ) -> Option<GeneratedTextInputEvent> {
1267 if matches!(event, BlitzImeEvent::Commit(_)) {
1275 self.record_history();
1276 }
1277
1278 let editor = &mut self.editor;
1279 let mut driver = editor.driver(font_ctx, layout_ctx);
1280
1281 match event {
1282 BlitzImeEvent::Enabled => {
1283 None
1285 }
1286 BlitzImeEvent::Disabled => {
1287 driver.clear_compose();
1288 Some(GeneratedTextInputEvent::PreEditChange)
1289 }
1290 BlitzImeEvent::Commit(text) => {
1291 driver.insert_or_replace_selection(&text);
1292 Some(GeneratedTextInputEvent::Input)
1293 }
1294 BlitzImeEvent::Preedit(text, cursor) => {
1295 if text.is_empty() {
1296 driver.clear_compose();
1297 } else {
1298 driver.set_compose(&text, cursor);
1299 }
1300 Some(GeneratedTextInputEvent::PreEditChange)
1301 }
1302 BlitzImeEvent::DeleteSurrounding {
1303 before_bytes,
1304 after_bytes,
1305 } => {
1306 let _ = before_bytes;
1307 let _ = after_bytes;
1308 None
1310 }
1311 }
1312 }
1313}
1314
1315#[cfg(test)]
1316mod content_widths_cache_tests {
1317 use super::*;
1318 use parley::{InlineBox, InlineBoxKind, TextStyle};
1319
1320 fn build_layout(text: &str, inline_box_width: Option<f32>) -> TextLayout {
1323 let mut font_ctx = FontContext::default();
1324 let mut layout_ctx = LayoutContext::new();
1325 let style: TextStyle<'_, '_, TextBrush> = TextStyle::default();
1326 let mut builder = layout_ctx.tree_builder(&mut font_ctx, 1.0, true, &style);
1327 builder.push_text(text);
1328 if let Some(width) = inline_box_width {
1329 builder.push_inline_box(InlineBox {
1330 id: 0,
1331 kind: InlineBoxKind::InFlow,
1332 index: text.len(),
1333 width,
1334 height: 10.0,
1335 });
1336 }
1337
1338 let mut text_layout = TextLayout::new();
1339 text_layout.text = builder.build_into(&mut text_layout.layout);
1340 text_layout
1341 }
1342
1343 #[test]
1344 fn first_call_matches_an_uncached_computation() {
1345 let mut text_layout = build_layout("the quick brown fox", None);
1346 let expected = text_layout.layout.calculate_content_widths();
1347
1348 let cached = text_layout.content_widths();
1349
1350 assert_eq!(cached.min, expected.min);
1351 assert_eq!(cached.max, expected.max);
1352 assert!(cached.min > 0.0);
1353 assert!(cached.max > cached.min);
1354 }
1355
1356 #[test]
1357 fn text_only_layout_reuses_the_cached_widths() {
1358 let mut text_layout = build_layout("the quick brown fox", None);
1359 text_layout.content_widths();
1360
1361 let poison = ContentWidths {
1364 min: -1.0,
1365 max: -2.0,
1366 };
1367 text_layout.content_widths.as_mut().unwrap().widths = poison;
1368
1369 let second = text_layout.content_widths();
1370 assert_eq!(second.min, poison.min);
1371 assert_eq!(second.max, poison.max);
1372 }
1373
1374 #[test]
1375 fn a_changed_inline_box_width_forces_a_recompute() {
1376 let mut text_layout = build_layout("the quick brown fox", Some(40.0));
1377 let first = text_layout.content_widths();
1378
1379 text_layout.content_widths.as_mut().unwrap().widths = ContentWidths {
1381 min: -1.0,
1382 max: -2.0,
1383 };
1384
1385 text_layout.layout.inline_boxes_mut()[0].width = 400.0;
1388
1389 let second = text_layout.content_widths();
1390 assert!(second.min > 0.0);
1391 assert!(second.max > first.max);
1392 assert_eq!(second.min, 400.0);
1393 }
1394
1395 #[test]
1396 fn an_unchanged_inline_box_width_still_hits_the_cache() {
1397 let mut text_layout = build_layout("the quick brown fox", Some(40.0));
1398 text_layout.content_widths();
1399
1400 let poison = ContentWidths {
1401 min: -1.0,
1402 max: -2.0,
1403 };
1404 text_layout.content_widths.as_mut().unwrap().widths = poison;
1405 text_layout.layout.inline_boxes_mut()[0].width = 40.0;
1407
1408 let second = text_layout.content_widths();
1409 assert_eq!(second.min, poison.min);
1410 assert_eq!(second.max, poison.max);
1411 }
1412
1413 #[test]
1414 fn rebuilding_the_layout_discards_the_cache() {
1415 let mut text_layout = build_layout("the quick brown fox", None);
1416 text_layout.content_widths();
1417 assert!(text_layout.content_widths.is_some());
1418
1419 text_layout.content_widths = None;
1421 let rebuilt = build_layout("a much much much longer run of text", None);
1422 text_layout.layout = rebuilt.layout;
1423 text_layout.text = rebuilt.text;
1424
1425 let widths = text_layout.content_widths();
1426 let expected = text_layout.layout.calculate_content_widths();
1427 assert_eq!(widths.max, expected.max);
1428 }
1429}
1430
1431#[cfg(test)]
1432mod shortcut_tests {
1433 use super::*;
1434 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1435 use blitz_traits::shell::DummyShellProvider;
1436 use keyboard_types::Location;
1437
1438 fn control_event(key: Key, code: Code) -> BlitzKeyEvent {
1439 BlitzKeyEvent {
1440 key,
1441 code,
1442 modifiers: Modifiers::CONTROL,
1443 location: Location::Standard,
1444 is_auto_repeating: false,
1445 is_composing: false,
1446 state: KeyState::Pressed,
1447 text: None,
1448 }
1449 }
1450
1451 #[test]
1452 fn control_character_cut_uses_the_physical_key_code() {
1453 let event = control_event(Key::Character("\u{18}".into()), Code::KeyX);
1454 assert_eq!(clipboard_command(&event), Some(ClipboardCommand::Cut));
1455 }
1456
1457 #[test]
1458 fn backspace_does_not_depend_on_an_apple_standard_keybinding() {
1459 let mut data = TextInputData::new(false);
1460 let mut font_ctx = FontContext::default();
1461 let mut layout_ctx = LayoutContext::new();
1462 data.set_text(&mut font_ctx, &mut layout_ctx, "typo");
1463 data.editor
1464 .driver(&mut font_ctx, &mut layout_ctx)
1465 .move_to_text_end();
1466 let event = BlitzKeyEvent {
1467 key: Key::Backspace,
1468 code: Code::Backspace,
1469 modifiers: Modifiers::empty(),
1470 location: Location::Standard,
1471 is_auto_repeating: false,
1472 is_composing: false,
1473 state: KeyState::Pressed,
1474 text: None,
1475 };
1476
1477 assert!(matches!(
1478 data.apply_keypress_event(&mut font_ctx, &mut layout_ctx, &DummyShellProvider, event,),
1479 Some(GeneratedTextInputEvent::Input)
1480 ));
1481 assert_eq!(data.editor.raw_text(), "typ");
1482 }
1483}
1484
1485#[cfg(test)]
1491mod history_tests {
1492 use super::*;
1493 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1494 use blitz_traits::shell::DummyShellProvider;
1495 use keyboard_types::Location;
1496
1497 struct Input {
1498 data: TextInputData,
1499 font_ctx: FontContext,
1500 layout_ctx: LayoutContext<TextBrush>,
1501 }
1502
1503 impl Input {
1504 fn new() -> Self {
1505 Self {
1506 data: TextInputData::new(true),
1507 font_ctx: FontContext::default(),
1508 layout_ctx: LayoutContext::new(),
1509 }
1510 }
1511
1512 fn press(&mut self, key: Key, code: Code, modifiers: Modifiers) {
1513 let event = BlitzKeyEvent {
1514 key,
1515 code,
1516 modifiers,
1517 location: Location::Standard,
1518 is_auto_repeating: false,
1519 is_composing: false,
1520 state: KeyState::Pressed,
1521 text: None,
1522 };
1523 self.data.apply_keypress_event(
1524 &mut self.font_ctx,
1525 &mut self.layout_ctx,
1526 &DummyShellProvider,
1527 event,
1528 );
1529 }
1530
1531 fn type_text(&mut self, text: &str) {
1533 for ch in text.chars() {
1534 self.press(
1535 Key::Character(ch.to_string()),
1536 Code::Unidentified,
1537 Modifiers::empty(),
1538 );
1539 }
1540 }
1541
1542 fn undo(&mut self) {
1543 self.press(Key::Character("z".into()), Code::KeyZ, Modifiers::CONTROL);
1544 }
1545
1546 fn redo(&mut self) {
1547 self.press(
1548 Key::Character("z".into()),
1549 Code::KeyZ,
1550 Modifiers::CONTROL | Modifiers::SHIFT,
1551 );
1552 }
1553
1554 fn text(&self) -> &str {
1555 self.data.editor.raw_text()
1556 }
1557 }
1558
1559 #[test]
1561 fn undo_restores_the_text_from_before_the_edit() {
1562 let mut input = Input::new();
1563 input.type_text("first");
1564 input.type_text(" second");
1565
1566 input.undo();
1567
1568 assert_eq!(
1573 input.text(),
1574 "first ",
1575 "undo should remove the most recent word",
1576 );
1577 }
1578
1579 #[test]
1580 fn redo_reapplies_what_undo_removed() {
1581 let mut input = Input::new();
1582 input.type_text("first");
1583 input.type_text(" second");
1584 let full = input.text().to_string();
1585
1586 input.undo();
1587 input.redo();
1588
1589 assert_eq!(input.text(), full, "redo should restore the undone text");
1590 }
1591
1592 #[test]
1597 fn a_run_of_typing_undoes_as_one_word_rather_than_per_character() {
1598 let mut input = Input::new();
1599 input.type_text("hello world");
1600
1601 input.undo();
1602
1603 assert_eq!(
1604 input.text(),
1605 "hello ",
1606 "the burst should end at the space, not at the previous character",
1607 );
1608 }
1609
1610 #[test]
1612 fn repeated_undo_walks_back_through_the_history() {
1613 let mut input = Input::new();
1614 input.type_text("one two three");
1615
1616 input.undo();
1617 assert_eq!(input.text(), "one two ");
1618 input.undo();
1619 assert_eq!(input.text(), "one ");
1620 input.undo();
1621 assert_eq!(input.text(), "");
1622 }
1623
1624 #[test]
1626 fn undo_with_nothing_to_undo_leaves_the_text_alone() {
1627 let mut input = Input::new();
1628 input.type_text("only");
1629
1630 input.undo();
1631 input.undo();
1632 input.undo();
1633
1634 assert_eq!(input.text(), "");
1635 }
1636
1637 #[test]
1639 fn a_fresh_edit_after_an_undo_clears_the_redo_stack() {
1640 let mut input = Input::new();
1641 input.type_text("first");
1642 input.type_text(" second");
1643
1644 input.undo();
1645 assert_eq!(input.text(), "first ");
1646 input.type_text("third");
1647 input.redo();
1648
1649 assert_eq!(
1650 input.text(),
1651 "first third",
1652 "redo must not resurrect a branch that was typed over",
1653 );
1654 }
1655
1656 #[test]
1658 fn control_y_also_redoes() {
1659 let mut input = Input::new();
1660 input.type_text("first");
1661 input.type_text(" second");
1662 let full = input.text().to_string();
1663
1664 input.undo();
1665 input.press(Key::Character("y".into()), Code::KeyY, Modifiers::CONTROL);
1666
1667 assert_eq!(input.text(), full);
1668 }
1669
1670 #[test]
1675 fn the_undo_chord_does_not_type_its_own_character() {
1676 let mut input = Input::new();
1677 input.type_text("text");
1678
1679 input.undo();
1680 input.redo();
1681
1682 assert!(
1683 !input.text().contains('z'),
1684 "the undo chord leaked into the buffer: {:?}",
1685 input.text(),
1686 );
1687 }
1688
1689 #[test]
1691 fn undo_restores_the_selection_along_with_the_text() {
1692 let mut input = Input::new();
1693 input.type_text("alpha");
1694 input.type_text(" beta");
1695
1696 input.undo();
1697
1698 let selection = input.data.editor.raw_selection();
1699 assert_eq!(
1700 selection.focus().index(),
1701 input.text().len(),
1702 "the caret should return to the end of the restored text",
1703 );
1704 }
1705
1706 #[test]
1708 fn the_history_is_capped_at_the_maximum_depth() {
1709 let mut history = TextEditHistory::default();
1710 for i in 0..(MAX_UNDO_DEPTH + 50) {
1711 history.record(TextEditSnapshot {
1712 text: format!("state {i}"),
1713 anchor: 0,
1714 focus: 0,
1715 });
1716 }
1717
1718 assert!(
1719 history.undo.len() <= MAX_UNDO_DEPTH,
1720 "history grew to {} entries, past the {MAX_UNDO_DEPTH} cap",
1721 history.undo.len(),
1722 );
1723 }
1724}
1725
1726#[cfg(test)]
1733mod history_chord_tests {
1734 use super::*;
1735 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1736 use keyboard_types::Location;
1737
1738 fn event(key: Key, code: Code, modifiers: Modifiers) -> BlitzKeyEvent {
1739 BlitzKeyEvent {
1740 key,
1741 code,
1742 modifiers,
1743 location: Location::Standard,
1744 is_auto_repeating: false,
1745 is_composing: false,
1746 state: KeyState::Pressed,
1747 text: None,
1748 }
1749 }
1750
1751 #[test]
1752 fn undo_is_recognised_under_control_and_under_the_platform_modifier() {
1753 for modifiers in [Modifiers::CONTROL, ACTION_MOD] {
1754 assert_eq!(
1755 history_command(&event(Key::Character("z".into()), Code::KeyZ, modifiers)),
1756 Some(HistoryCommand::Undo),
1757 );
1758 }
1759 }
1760
1761 #[test]
1762 fn shift_z_redoes_under_either_modifier() {
1763 for modifiers in [Modifiers::CONTROL, ACTION_MOD] {
1764 assert_eq!(
1765 history_command(&event(
1766 Key::Character("z".into()),
1767 Code::KeyZ,
1768 modifiers | Modifiers::SHIFT,
1769 )),
1770 Some(HistoryCommand::Redo),
1771 );
1772 }
1773 }
1774
1775 #[test]
1777 fn a_remapped_character_still_undoes_by_its_physical_key() {
1778 assert_eq!(
1779 history_command(&event(
1780 Key::Character("w".into()),
1781 Code::KeyZ,
1782 Modifiers::CONTROL,
1783 )),
1784 Some(HistoryCommand::Undo),
1785 );
1786 }
1787
1788 #[test]
1789 fn the_chord_needs_a_modifier() {
1790 assert_eq!(
1791 history_command(&event(
1792 Key::Character("z".into()),
1793 Code::KeyZ,
1794 Modifiers::empty(),
1795 )),
1796 None,
1797 );
1798 }
1799}