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 pub text_node: Option<NodeId>,
254}
255
256impl TextBrush {
257 pub(crate) fn from_id(id: NodeId) -> Self {
258 Self {
259 id,
260 text_node: None,
261 }
262 }
263}
264
265#[derive(Clone, Debug)]
270pub struct CachedContentWidths {
271 inline_box_widths: Box<[u32]>,
276 widths: ContentWidths,
277}
278
279#[derive(Clone, Default)]
280pub struct TextLayout {
281 pub text: String,
282 pub content_widths: Option<CachedContentWidths>,
283 pub layout: parley::layout::Layout<TextBrush>,
284 pub laid_out_at: Option<f32>,
299}
300
301impl TextLayout {
302 pub fn new() -> Self {
303 Default::default()
304 }
305
306 pub fn content_widths(&mut self) -> ContentWidths {
332 let inline_box_widths: Box<[u32]> = self
336 .layout
337 .inline_boxes()
338 .iter()
339 .map(|ibox| ibox.width.to_bits())
340 .collect();
341
342 if let Some(cached) = &self.content_widths
343 && cached.inline_box_widths == inline_box_widths
344 {
345 return cached.widths;
346 }
347
348 let widths = self.layout.calculate_content_widths();
349 self.content_widths = Some(CachedContentWidths {
350 inline_box_widths,
351 widths,
352 });
353 widths
354 }
355}
356
357impl std::fmt::Debug for TextLayout {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 write!(f, "TextLayout")
360 }
361}
362
363pub enum GeneratedTextInputEvent {
365 Input,
366 Select,
367 PreEditChange,
368 Submit,
369}
370
371pub struct TextInputData {
372 pub editor: Box<parley::PlainEditor<TextBrush>>,
374 pub placeholder_editor: Option<Box<parley::PlainEditor<TextBrush>>>,
376 history: TextEditHistory,
379 pub is_multiline: bool,
381 pub scroll_offset: f32,
387 pub layout_width: Option<f32>,
388}
389
390impl Clone for TextInputData {
392 fn clone(&self) -> Self {
393 TextInputData::new(self.is_multiline)
394 }
395}
396
397impl TextInputData {
398 pub fn new(is_multiline: bool) -> Self {
399 let editor = Box::new(parley::PlainEditor::new(16.0));
400 Self {
401 editor,
402 placeholder_editor: None,
403 history: TextEditHistory::default(),
404 is_multiline,
405 scroll_offset: 0.0,
406 layout_width: None,
407 }
408 }
409
410 fn snapshot(&self) -> TextEditSnapshot {
412 let selection = self.editor.raw_selection();
413 TextEditSnapshot {
414 text: self.editor.raw_text().to_string(),
415 anchor: selection.anchor().index(),
416 focus: selection.focus().index(),
417 }
418 }
419
420 fn record_history(&mut self) {
422 let snapshot = self.snapshot();
423 self.history.record(snapshot);
424 }
425
426 fn restore(
432 &mut self,
433 font_ctx: &mut FontContext,
434 layout_ctx: &mut LayoutContext<TextBrush>,
435 snapshot: &TextEditSnapshot,
436 ) {
437 self.history.applying = true;
438 self.editor.set_text(&snapshot.text);
439 let mut driver = self.editor.driver(font_ctx, layout_ctx);
440 let len = snapshot.text.len();
444 let anchor = snapshot.anchor.min(len);
445 let focus = snapshot.focus.min(len);
446 if anchor == focus {
447 driver.move_to_byte(focus);
448 } else {
449 driver.select_byte_range(anchor, focus);
450 }
451 self.history.applying = false;
452 }
453
454 fn apply_history_command(
456 &mut self,
457 font_ctx: &mut FontContext,
458 layout_ctx: &mut LayoutContext<TextBrush>,
459 command: HistoryCommand,
460 ) -> Option<GeneratedTextInputEvent> {
461 let now = self.snapshot();
462 let restore = match command {
463 HistoryCommand::Undo => self.history.undo(now),
464 HistoryCommand::Redo => self.history.redo(now),
465 }?;
466 self.restore(font_ctx, layout_ctx, &restore);
467 Some(GeneratedTextInputEvent::Input)
468 }
469
470 pub fn content_height(&self) -> Option<f32> {
478 self.editor
479 .try_layout()
480 .map(|layout| layout.height() / layout.scale())
481 }
482
483 fn apply_layout_width(&mut self) {
491 let Some(width) = self.layout_width else {
492 return;
493 };
494 self.editor.set_width(Some(width * self.editor.get_scale()));
495 if let Some(placeholder) = self.placeholder_editor.as_mut() {
496 placeholder.set_width(Some(width * placeholder.get_scale()));
497 }
498 }
499
500 pub fn sync_multiline_width(
501 &mut self,
502 font_ctx: &mut FontContext,
503 layout_ctx: &mut LayoutContext<TextBrush>,
504 width: f32,
505 ) {
506 if !self.is_multiline || width <= 0.0 {
507 return;
508 }
509 if self
510 .layout_width
511 .is_some_and(|current| (current - width).abs() < 0.01)
512 {
513 return;
514 }
515 self.layout_width = Some(width);
516 self.apply_layout_width();
517 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
518 if let Some(placeholder) = self.placeholder_editor.as_mut() {
519 placeholder.driver(font_ctx, layout_ctx).refresh_layout();
520 }
521 }
522
523 pub fn set_text(
524 &mut self,
525 font_ctx: &mut FontContext,
526 layout_ctx: &mut LayoutContext<TextBrush>,
527 text: &str,
528 ) {
529 if self.editor.text() != text {
530 self.editor.set_text(text);
531 self.apply_layout_width();
545 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
546 self.editor.driver(font_ctx, layout_ctx).move_to_text_end();
560 }
561 }
562
563 pub fn clamp_scroll_offset(&mut self, content_box_width: f32, content_box_height: f32) {
569 let Some(layout) = self.editor.try_layout() else {
570 return;
571 };
572 let scale = layout.scale();
575
576 let Some(caret) = self.editor.cursor_geometry(1.5) else {
578 return;
579 };
580
581 let (caret_start, caret_end, content, viewport) = if self.is_multiline {
583 (
584 caret.y0 as f32 / scale,
585 caret.y1 as f32 / scale,
586 layout.height() / scale,
587 content_box_height,
588 )
589 } else {
590 (
591 caret.x0 as f32 / scale,
592 caret.x1 as f32 / scale,
593 layout.full_width() / scale,
594 content_box_width,
595 )
596 };
597
598 let mut offset = self.scroll_offset;
599
600 if caret_end > offset + viewport {
602 offset = caret_end - viewport;
603 }
604 if caret_start < offset {
605 offset = caret_start;
606 }
607
608 let max_offset = (content.max(caret_end) - viewport).max(0.0);
612 self.scroll_offset = offset.clamp(0.0, max_offset);
613 }
614
615 pub fn max_scroll_offset(&self, content_box_width: f32, content_box_height: f32) -> f32 {
622 let Some(layout) = self.editor.try_layout() else {
623 return 0.0;
624 };
625 let scale = layout.scale();
626 let (content, viewport) = if self.is_multiline {
627 (layout.height() / scale, content_box_height)
628 } else {
629 (layout.full_width() / scale, content_box_width)
630 };
631 (content - viewport).max(0.0)
632 }
633
634 pub fn scroll_by(
641 &mut self,
642 delta: f32,
643 content_box_width: f32,
644 content_box_height: f32,
645 ) -> f32 {
646 let max_offset = self.max_scroll_offset(content_box_width, content_box_height);
647 if max_offset <= 0.0 {
648 return delta;
649 }
650
651 let new_offset = (self.scroll_offset - delta).clamp(0.0, max_offset);
654 let consumed = self.scroll_offset - new_offset;
655 self.scroll_offset = new_offset;
656 delta - consumed
657 }
658
659 pub(crate) fn apply_keypress_event(
660 &mut self,
661 font_ctx: &mut FontContext,
662 layout_ctx: &mut LayoutContext<TextBrush>,
663 shell_provider: &dyn ShellProvider,
664 event: BlitzKeyEvent,
665 ) -> Option<GeneratedTextInputEvent> {
666 if !event.state.is_pressed() {
668 return None;
669 }
670
671 if let Some(command) = history_command(&event) {
674 return self.apply_history_command(font_ctx, layout_ctx, command);
675 }
676
677 self.record_history();
682
683 let mods = event.modifiers;
684 let shift = mods.contains(Modifiers::SHIFT);
685 let action_mod = mods.contains(ACTION_MOD);
686 let word_mod = mods.contains(Modifiers::ALT);
687 let is_multiline = self.is_multiline;
688 let editor = &mut self.editor;
689 let mut driver = editor.driver(font_ctx, layout_ctx);
690 if let Some(command) = clipboard_command(&event) {
691 match command {
692 ClipboardCommand::Copy => {
693 if let Some(text) = driver.editor.selected_text() {
694 let _ = shell_provider.set_clipboard_text(text.to_owned());
695 }
696 }
697 ClipboardCommand::Cut => {
698 if let Some(text) = driver.editor.selected_text() {
699 let _ = shell_provider.set_clipboard_text(text.to_owned());
700 driver.delete_selection()
701 }
702 }
703 ClipboardCommand::Paste => {
704 let text = shell_provider.get_clipboard_text().unwrap_or_default();
705 driver.insert_or_replace_selection(&text)
706 }
707 }
708
709 return Some(GeneratedTextInputEvent::Input);
710 }
711 match event.key {
712 Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
713 if shift {
714 driver.collapse_selection()
715 } else {
716 driver.select_all()
717 }
718 return Some(GeneratedTextInputEvent::Select);
719 }
720 Key::ArrowLeft => {
721 if action_mod {
722 if shift {
723 driver.select_to_line_start()
724 } else {
725 driver.move_to_line_start()
726 }
727 } else if word_mod {
728 if shift {
729 driver.select_word_left()
730 } else {
731 driver.move_word_left()
732 }
733 } else if shift {
734 driver.select_left()
735 } else {
736 driver.move_left()
737 }
738 return Some(GeneratedTextInputEvent::Select);
739 }
740 Key::ArrowRight => {
741 if action_mod {
742 if shift {
743 driver.select_to_line_end()
744 } else {
745 driver.move_to_line_end()
746 }
747 } else if word_mod {
748 if shift {
749 driver.select_word_right()
750 } else {
751 driver.move_word_right()
752 }
753 } else if shift {
754 driver.select_right()
755 } else {
756 driver.move_right()
757 }
758 return Some(GeneratedTextInputEvent::Select);
759 }
760 Key::ArrowUp => {
761 if action_mod && shift {
762 driver.select_to_text_start()
763 } else if action_mod {
764 driver.move_to_text_start()
765 } else if shift {
766 driver.select_up()
767 } else {
768 driver.move_up()
769 }
770 return Some(GeneratedTextInputEvent::Select);
771 }
772 Key::ArrowDown => {
773 if action_mod && shift {
774 driver.select_to_text_end()
775 } else if action_mod {
776 driver.move_to_text_end()
777 } else if shift {
778 driver.select_down()
779 } else {
780 driver.move_down()
781 }
782 return Some(GeneratedTextInputEvent::Select);
783 }
784 Key::Home => {
785 if action_mod {
786 if shift {
787 driver.select_to_text_start()
788 } else {
789 driver.move_to_text_start()
790 }
791 } else if shift {
792 driver.select_to_line_start()
793 } else {
794 driver.move_to_line_start()
795 }
796 return Some(GeneratedTextInputEvent::Select);
797 }
798 Key::End => {
799 if action_mod {
800 if shift {
801 driver.select_to_text_end()
802 } else {
803 driver.move_to_text_end()
804 }
805 } else if shift {
806 driver.select_to_line_end()
807 } else {
808 driver.move_to_line_end()
809 }
810 return Some(GeneratedTextInputEvent::Select);
811 }
812 Key::Delete => {
813 #[cfg(target_os = "macos")]
814 if mods.contains(Modifiers::SUPER) {
815 if driver.editor.raw_selection().is_collapsed() {
816 driver.select_to_line_end();
817 }
818 driver.delete_selection();
819 } else if mods.contains(Modifiers::ALT) {
820 driver.delete_word();
821 } else {
822 driver.delete();
823 }
824 #[cfg(not(target_os = "macos"))]
825 if action_mod {
826 driver.delete_word();
827 } else {
828 driver.delete();
829 }
830 return Some(GeneratedTextInputEvent::Input);
831 }
832 Key::Backspace => {
833 #[cfg(target_os = "macos")]
834 if mods.contains(Modifiers::SUPER) {
835 if driver.editor.raw_selection().is_collapsed() {
836 driver.select_to_line_start();
837 }
838 driver.delete_selection();
839 } else if mods.contains(Modifiers::ALT) {
840 driver.backdelete_word();
841 } else {
842 driver.backdelete();
843 }
844 #[cfg(not(target_os = "macos"))]
845 if action_mod {
846 driver.backdelete_word();
847 } else {
848 driver.backdelete();
849 }
850 return Some(GeneratedTextInputEvent::Input);
851 }
852
853 Key::Character(c) if c == "\n" => {
854 if is_multiline {
855 driver.insert_or_replace_selection("\n");
856 return Some(GeneratedTextInputEvent::Input);
857 } else {
858 return Some(GeneratedTextInputEvent::Submit);
859 }
860 }
861 Key::Enter => {
862 if is_multiline {
863 driver.insert_or_replace_selection("\n");
864 return Some(GeneratedTextInputEvent::Input);
865 } else {
866 return Some(GeneratedTextInputEvent::Submit);
867 }
868 }
869 Key::Character(s)
870 if !mods.contains(Modifiers::CONTROL) && !mods.contains(Modifiers::SUPER) =>
871 {
872 driver.insert_or_replace_selection(&s);
873 return Some(GeneratedTextInputEvent::Input);
874 }
875 _ => {}
876 };
877
878 None
879 }
880
881 pub(crate) fn apply_apple_standard_keybinding(
882 &mut self,
883 font_ctx: &mut FontContext,
884 layout_ctx: &mut LayoutContext<TextBrush>,
885 shell_provider: &dyn ShellProvider,
886 command: &str,
887 ) -> Option<GeneratedTextInputEvent> {
888 self.record_history();
892
893 let editor = &mut self.editor;
894 let mut driver = editor.driver(font_ctx, layout_ctx);
895 let is_multiline = self.is_multiline;
896
897 match command {
898 "insertBacktab:" => {}
902 "insertContainerBreak:" => {}
904 "insertDoubleQuoteIgnoringSubstitution:" => {
906 driver.insert_or_replace_selection("\"");
907 return Some(GeneratedTextInputEvent::Input);
908 }
909 "insertLineBreak:" => {
911 driver.insert_or_replace_selection("\n");
912 return Some(GeneratedTextInputEvent::Input);
913 }
914 "insertNewline:" => {
916 if is_multiline {
917 driver.insert_or_replace_selection("\n");
918 return Some(GeneratedTextInputEvent::Input);
919 } else {
920 return Some(GeneratedTextInputEvent::Submit);
921 }
922 }
923 "insertNewlineIgnoringFieldEditor:" => {
925 driver.insert_or_replace_selection("\n");
926 return Some(GeneratedTextInputEvent::Input);
927 }
928 "insertParagraphSeparator:" => {
930 driver.insert_or_replace_selection("\n");
931 return Some(GeneratedTextInputEvent::Input);
932 }
933 "insertSingleQuoteIgnoringSubstitution:" => {
934 driver.insert_or_replace_selection("'");
935 return Some(GeneratedTextInputEvent::Input);
936 }
937 "insertTab:" | "insertTabIgnoringFieldEditor:" => {
939 }
941 "insertText:" => {}
943
944 "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {}
950 "deleteForward:" => {}
951 "deleteToBeginningOfLine:" => {
953 if driver.editor.raw_selection().is_collapsed() {
954 driver.select_to_line_start();
955 }
956 driver.delete_selection();
957 return Some(GeneratedTextInputEvent::Input);
958 }
959 "deleteToEndOfLine:" => {
961 if driver.editor.raw_selection().is_collapsed() {
962 driver.select_to_line_end();
963 }
964 driver.delete_selection();
965 return Some(GeneratedTextInputEvent::Input);
966 }
967 "deleteToBeginningOfParagraph:" => {
968 if driver.editor.raw_selection().is_collapsed() {
969 driver.select_to_hard_line_start();
970 }
971 driver.delete_selection();
972 return Some(GeneratedTextInputEvent::Input);
973 }
974
975 "deleteToEndOfParagraph:" => {
977 if driver.editor.raw_selection().is_collapsed() {
978 driver.select_to_hard_line_end();
979 }
980 driver.delete_selection();
981 return Some(GeneratedTextInputEvent::Input);
982 }
983 "deleteWordBackward:" => {}
985 "deleteWordForward:" => {}
987 "yank:" => {
989 if let Some(text) = driver.editor.selected_text() {
990 let _ = shell_provider.set_clipboard_text(text.to_owned());
991 driver.delete_selection();
992 return Some(GeneratedTextInputEvent::Input);
993 }
994 }
995
996 "moveBackward:" => {
1000 driver.move_left(); return Some(GeneratedTextInputEvent::Select);
1002 }
1003
1004 "moveDown:" => {
1006 driver.move_down();
1007 return Some(GeneratedTextInputEvent::Select);
1008 }
1009 "moveForward:" => {
1011 driver.move_right();
1012 return Some(GeneratedTextInputEvent::Select);
1013 } "moveLeft:" => {
1017 driver.move_left();
1018 return Some(GeneratedTextInputEvent::Select);
1019 }
1020 "moveRight:" => {
1022 driver.move_right();
1023 return Some(GeneratedTextInputEvent::Select);
1024 }
1025 "moveUp:" => {
1027 driver.move_up();
1028 return Some(GeneratedTextInputEvent::Select);
1029 }
1030
1031 "moveBackwardAndModifySelection:" => {
1035 driver.select_left(); return Some(GeneratedTextInputEvent::Select);
1037 }
1038 "moveDownAndModifySelection:" => {
1040 driver.select_down();
1041 return Some(GeneratedTextInputEvent::Select);
1042 }
1043 "moveForwardAndModifySelection:" => {
1045 driver.select_right(); return Some(GeneratedTextInputEvent::Select);
1047 }
1048 "moveLeftAndModifySelection:" => {
1050 driver.select_left();
1051 return Some(GeneratedTextInputEvent::Select);
1052 }
1053 "moveRightAndModifySelection:" => {
1055 driver.select_right();
1056 return Some(GeneratedTextInputEvent::Select);
1057 }
1058 "moveUpAndModifySelection:" => {
1060 driver.select_up();
1061 return Some(GeneratedTextInputEvent::Select);
1062 }
1063
1064 "selectAll:" => {
1066 driver.select_all();
1067 return Some(GeneratedTextInputEvent::Select);
1068 }
1069 "selectLine:" => {
1070 driver.move_to_line_start();
1071 driver.select_to_line_end();
1072 return Some(GeneratedTextInputEvent::Select);
1073 }
1074 "selectParagraph:" => {
1075 driver.move_to_hard_line_start();
1076 driver.select_to_hard_line_end();
1077 return Some(GeneratedTextInputEvent::Select);
1078 }
1079 "selectWord:" => {
1080 }
1082
1083 "moveToBeginningOfDocument:" => {
1085 driver.move_to_text_start();
1086 return Some(GeneratedTextInputEvent::Select);
1087 }
1088 "moveToBeginningOfDocumentAndModifySelection:" => {
1089 driver.select_to_text_start();
1090 return Some(GeneratedTextInputEvent::Select);
1091 }
1092 "moveToEndOfDocument:" => {
1093 driver.move_to_text_end();
1094 return Some(GeneratedTextInputEvent::Select);
1095 }
1096 "moveToEndOfDocumentAndModifySelection:" => {
1097 driver.move_to_text_end();
1098 return Some(GeneratedTextInputEvent::Select);
1099 }
1100
1101 "moveParagraphBackwardAndModifySelection:" => {}
1103 "moveParagraphForwardAndModifySelection:" => {}
1104 "moveToBeginningOfParagraph:" => {
1105 driver.move_to_hard_line_start();
1106 return Some(GeneratedTextInputEvent::Select);
1107 }
1108 "moveToBeginningOfParagraphAndModifySelection:" => {
1109 driver.select_to_hard_line_start();
1110 return Some(GeneratedTextInputEvent::Select);
1111 }
1112 "moveToEndOfParagraph:" => {
1113 driver.move_to_hard_line_end();
1114 return Some(GeneratedTextInputEvent::Select);
1115 }
1116 "moveToEndOfParagraphAndModifySelection:" => {
1117 driver.select_to_hard_line_end();
1118 return Some(GeneratedTextInputEvent::Select);
1119 }
1120
1121 "moveToBeginningOfLine:" => {
1123 driver.move_to_line_start();
1124 return Some(GeneratedTextInputEvent::Select);
1125 }
1126 "moveToBeginningOfLineAndModifySelection:" => {
1127 driver.select_to_line_start();
1128 return Some(GeneratedTextInputEvent::Select);
1129 }
1130 "moveToEndOfLine:" => {
1131 driver.move_to_line_end();
1132 return Some(GeneratedTextInputEvent::Select);
1133 }
1134 "moveToEndOfLineAndModifySelection:" => {
1135 driver.select_to_line_end();
1136 return Some(GeneratedTextInputEvent::Select);
1137 }
1138 "moveToLeftEndOfLine:" => {
1139 driver.move_to_text_start();
1140 return Some(GeneratedTextInputEvent::Select);
1141 }
1142 "moveToLeftEndOfLineAndModifySelection:" => {
1143 driver.select_to_line_start();
1144 return Some(GeneratedTextInputEvent::Select);
1145 }
1146 "moveToRightEndOfLine:" => {
1147 driver.move_to_line_end();
1148 return Some(GeneratedTextInputEvent::Select);
1149 }
1150 "moveToRightEndOfLineAndModifySelection:" => {
1151 driver.select_to_line_end();
1152 return Some(GeneratedTextInputEvent::Select);
1153 }
1154
1155 "moveWordBackward:" => {
1157 driver.move_word_left();
1158 return Some(GeneratedTextInputEvent::Select);
1159 }
1160 "moveWordBackwardAndModifySelection:" => {
1161 driver.select_word_left();
1162 return Some(GeneratedTextInputEvent::Select);
1163 }
1164 "moveWordForward:" => {
1165 driver.move_word_right();
1166 return Some(GeneratedTextInputEvent::Select);
1167 }
1168 "moveWordForwardAndModifySelection:" => {
1169 driver.select_word_right();
1170 return Some(GeneratedTextInputEvent::Select);
1171 }
1172 "moveWordLeft:" => {
1173 driver.move_word_left();
1174 return Some(GeneratedTextInputEvent::Select);
1175 }
1176 "moveWordLeftAndModifySelection:" => {
1177 driver.select_word_left();
1178 return Some(GeneratedTextInputEvent::Select);
1179 }
1180 "moveWordRight:" => {
1181 driver.move_word_right();
1182 return Some(GeneratedTextInputEvent::Select);
1183 }
1184 "moveWordRightAndModifySelection:" => {
1185 driver.select_word_right();
1186 return Some(GeneratedTextInputEvent::Select);
1187 }
1188
1189 "scrollPageDown:" => {}
1193 "scrollPageUp:" => {}
1195 "scrollLineDown:" => {}
1197 "scrollLineUp:" => {}
1199 "scrollToBeginningOfDocument:" => {}
1201 "scrollToEndOfDocument:" => {}
1203 "pageDown:" => {}
1205 "pageUp:" => {}
1207 "pageDownAndModifySelection:" => {}
1209 "pageUpAndModifySelection:" => {}
1211 "centerSelectionInVisibleArea:" => {}
1213
1214 "transpose:" => {}
1218 "transposeWords:" => {}
1220
1221 "indent:" => {}
1224
1225 "cancelOperation:" => {}
1228
1229 "quickLookPreviewItems:" => {}
1232
1233 "makeBaseWritingDirectionLeftToRight:" => {}
1235 "makeBaseWritingDirectionNatural:" => {}
1236 "makeBaseWritingDirectionRightToLeft:" => {}
1237 "makeTextWritingDirectionLeftToRight:" => {}
1238 "makeTextWritingDirectionNatural:" => {}
1239 "makeTextWritingDirectionRightToLeft:" => {}
1240
1241 "capitalizeWord:" => {}
1243 "changeCaseOfLetter:" => {}
1244 "lowercaseWord:" => {}
1245 "uppercaseWord:" => {}
1246
1247 "setMark:" => {}
1249 "selectToMark:" => {}
1250 "deleteToMark:" => {}
1251 "swapWithMark:" => {}
1252
1253 "complete:" => {}
1255
1256 "showContextMenuForSelection:" => {}
1258
1259 _ => {}
1261 };
1262
1263 None
1264 }
1265
1266 pub(crate) fn apply_ime_event(
1267 &mut self,
1268 font_ctx: &mut FontContext,
1269 layout_ctx: &mut LayoutContext<TextBrush>,
1270 event: BlitzImeEvent,
1271 ) -> Option<GeneratedTextInputEvent> {
1272 if matches!(event, BlitzImeEvent::Commit(_)) {
1280 self.record_history();
1281 }
1282
1283 let editor = &mut self.editor;
1284 let mut driver = editor.driver(font_ctx, layout_ctx);
1285
1286 match event {
1287 BlitzImeEvent::Enabled => {
1288 None
1290 }
1291 BlitzImeEvent::Disabled => {
1292 driver.clear_compose();
1293 Some(GeneratedTextInputEvent::PreEditChange)
1294 }
1295 BlitzImeEvent::Commit(text) => {
1296 driver.insert_or_replace_selection(&text);
1297 Some(GeneratedTextInputEvent::Input)
1298 }
1299 BlitzImeEvent::Preedit(text, cursor) => {
1300 if text.is_empty() {
1301 driver.clear_compose();
1302 } else {
1303 driver.set_compose(&text, cursor);
1304 }
1305 Some(GeneratedTextInputEvent::PreEditChange)
1306 }
1307 BlitzImeEvent::DeleteSurrounding {
1308 before_bytes,
1309 after_bytes,
1310 } => {
1311 let _ = before_bytes;
1312 let _ = after_bytes;
1313 None
1315 }
1316 }
1317 }
1318}
1319
1320#[cfg(test)]
1321mod content_widths_cache_tests {
1322 use super::*;
1323 use parley::{InlineBox, InlineBoxKind, TextStyle};
1324
1325 fn build_layout(text: &str, inline_box_width: Option<f32>) -> TextLayout {
1328 let mut font_ctx = FontContext::default();
1329 let mut layout_ctx = LayoutContext::new();
1330 let style: TextStyle<'_, '_, TextBrush> = TextStyle::default();
1331 let mut builder = layout_ctx.tree_builder(&mut font_ctx, 1.0, true, &style);
1332 builder.push_text(text);
1333 if let Some(width) = inline_box_width {
1334 builder.push_inline_box(InlineBox {
1335 id: 0,
1336 kind: InlineBoxKind::InFlow,
1337 index: text.len(),
1338 width,
1339 height: 10.0,
1340 });
1341 }
1342
1343 let mut text_layout = TextLayout::new();
1344 text_layout.text = builder.build_into(&mut text_layout.layout);
1345 text_layout
1346 }
1347
1348 #[cfg(target_os = "macos")]
1353 #[test]
1354 fn first_call_matches_an_uncached_computation() {
1355 let mut text_layout = build_layout("the quick brown fox", None);
1356 let expected = text_layout.layout.calculate_content_widths();
1357
1358 let cached = text_layout.content_widths();
1359
1360 assert_eq!(cached.min, expected.min);
1361 assert_eq!(cached.max, expected.max);
1362 assert!(cached.min > 0.0);
1363 assert!(cached.max > cached.min);
1364 }
1365
1366 #[test]
1367 fn text_only_layout_reuses_the_cached_widths() {
1368 let mut text_layout = build_layout("the quick brown fox", None);
1369 text_layout.content_widths();
1370
1371 let poison = ContentWidths {
1374 min: -1.0,
1375 max: -2.0,
1376 };
1377 text_layout.content_widths.as_mut().unwrap().widths = poison;
1378
1379 let second = text_layout.content_widths();
1380 assert_eq!(second.min, poison.min);
1381 assert_eq!(second.max, poison.max);
1382 }
1383
1384 #[test]
1385 fn a_changed_inline_box_width_forces_a_recompute() {
1386 let mut text_layout = build_layout("the quick brown fox", Some(40.0));
1387 let first = text_layout.content_widths();
1388
1389 text_layout.content_widths.as_mut().unwrap().widths = ContentWidths {
1391 min: -1.0,
1392 max: -2.0,
1393 };
1394
1395 text_layout.layout.inline_boxes_mut()[0].width = 400.0;
1398
1399 let second = text_layout.content_widths();
1400 assert!(second.min > 0.0);
1401 assert!(second.max > first.max);
1402 assert_eq!(second.min, 400.0);
1403 }
1404
1405 #[test]
1406 fn an_unchanged_inline_box_width_still_hits_the_cache() {
1407 let mut text_layout = build_layout("the quick brown fox", Some(40.0));
1408 text_layout.content_widths();
1409
1410 let poison = ContentWidths {
1411 min: -1.0,
1412 max: -2.0,
1413 };
1414 text_layout.content_widths.as_mut().unwrap().widths = poison;
1415 text_layout.layout.inline_boxes_mut()[0].width = 40.0;
1417
1418 let second = text_layout.content_widths();
1419 assert_eq!(second.min, poison.min);
1420 assert_eq!(second.max, poison.max);
1421 }
1422
1423 #[test]
1424 fn rebuilding_the_layout_discards_the_cache() {
1425 let mut text_layout = build_layout("the quick brown fox", None);
1426 text_layout.content_widths();
1427 assert!(text_layout.content_widths.is_some());
1428
1429 text_layout.content_widths = None;
1431 let rebuilt = build_layout("a much much much longer run of text", None);
1432 text_layout.layout = rebuilt.layout;
1433 text_layout.text = rebuilt.text;
1434
1435 let widths = text_layout.content_widths();
1436 let expected = text_layout.layout.calculate_content_widths();
1437 assert_eq!(widths.max, expected.max);
1438 }
1439}
1440
1441#[cfg(test)]
1442mod shortcut_tests {
1443 use super::*;
1444 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1445 use blitz_traits::shell::DummyShellProvider;
1446 use keyboard_types::Location;
1447
1448 fn control_event(key: Key, code: Code) -> BlitzKeyEvent {
1449 BlitzKeyEvent {
1450 key,
1451 code,
1452 modifiers: Modifiers::CONTROL,
1453 location: Location::Standard,
1454 is_auto_repeating: false,
1455 is_composing: false,
1456 state: KeyState::Pressed,
1457 text: None,
1458 }
1459 }
1460
1461 #[test]
1462 fn control_character_cut_uses_the_physical_key_code() {
1463 let event = control_event(Key::Character("\u{18}".into()), Code::KeyX);
1464 assert_eq!(clipboard_command(&event), Some(ClipboardCommand::Cut));
1465 }
1466
1467 #[cfg(target_os = "macos")]
1471 #[test]
1472 fn backspace_does_not_depend_on_an_apple_standard_keybinding() {
1473 let mut data = TextInputData::new(false);
1474 let mut font_ctx = FontContext::default();
1475 let mut layout_ctx = LayoutContext::new();
1476 data.set_text(&mut font_ctx, &mut layout_ctx, "typo");
1477 data.editor
1478 .driver(&mut font_ctx, &mut layout_ctx)
1479 .move_to_text_end();
1480 let event = BlitzKeyEvent {
1481 key: Key::Backspace,
1482 code: Code::Backspace,
1483 modifiers: Modifiers::empty(),
1484 location: Location::Standard,
1485 is_auto_repeating: false,
1486 is_composing: false,
1487 state: KeyState::Pressed,
1488 text: None,
1489 };
1490
1491 assert!(matches!(
1492 data.apply_keypress_event(&mut font_ctx, &mut layout_ctx, &DummyShellProvider, event,),
1493 Some(GeneratedTextInputEvent::Input)
1494 ));
1495 assert_eq!(data.editor.raw_text(), "typ");
1496 }
1497}
1498
1499#[cfg(test)]
1505mod history_tests {
1506 use super::*;
1507 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1508 use blitz_traits::shell::DummyShellProvider;
1509 use keyboard_types::Location;
1510
1511 struct Input {
1512 data: TextInputData,
1513 font_ctx: FontContext,
1514 layout_ctx: LayoutContext<TextBrush>,
1515 }
1516
1517 impl Input {
1518 fn new() -> Self {
1519 Self {
1520 data: TextInputData::new(true),
1521 font_ctx: FontContext::default(),
1522 layout_ctx: LayoutContext::new(),
1523 }
1524 }
1525
1526 fn press(&mut self, key: Key, code: Code, modifiers: Modifiers) {
1527 let event = BlitzKeyEvent {
1528 key,
1529 code,
1530 modifiers,
1531 location: Location::Standard,
1532 is_auto_repeating: false,
1533 is_composing: false,
1534 state: KeyState::Pressed,
1535 text: None,
1536 };
1537 self.data.apply_keypress_event(
1538 &mut self.font_ctx,
1539 &mut self.layout_ctx,
1540 &DummyShellProvider,
1541 event,
1542 );
1543 }
1544
1545 fn type_text(&mut self, text: &str) {
1547 for ch in text.chars() {
1548 self.press(
1549 Key::Character(ch.to_string()),
1550 Code::Unidentified,
1551 Modifiers::empty(),
1552 );
1553 }
1554 }
1555
1556 fn undo(&mut self) {
1557 self.press(Key::Character("z".into()), Code::KeyZ, Modifiers::CONTROL);
1558 }
1559
1560 fn redo(&mut self) {
1561 self.press(
1562 Key::Character("z".into()),
1563 Code::KeyZ,
1564 Modifiers::CONTROL | Modifiers::SHIFT,
1565 );
1566 }
1567
1568 fn text(&self) -> &str {
1569 self.data.editor.raw_text()
1570 }
1571 }
1572
1573 #[test]
1575 fn undo_restores_the_text_from_before_the_edit() {
1576 let mut input = Input::new();
1577 input.type_text("first");
1578 input.type_text(" second");
1579
1580 input.undo();
1581
1582 assert_eq!(
1587 input.text(),
1588 "first ",
1589 "undo should remove the most recent word",
1590 );
1591 }
1592
1593 #[test]
1594 fn redo_reapplies_what_undo_removed() {
1595 let mut input = Input::new();
1596 input.type_text("first");
1597 input.type_text(" second");
1598 let full = input.text().to_string();
1599
1600 input.undo();
1601 input.redo();
1602
1603 assert_eq!(input.text(), full, "redo should restore the undone text");
1604 }
1605
1606 #[test]
1611 fn a_run_of_typing_undoes_as_one_word_rather_than_per_character() {
1612 let mut input = Input::new();
1613 input.type_text("hello world");
1614
1615 input.undo();
1616
1617 assert_eq!(
1618 input.text(),
1619 "hello ",
1620 "the burst should end at the space, not at the previous character",
1621 );
1622 }
1623
1624 #[test]
1626 fn repeated_undo_walks_back_through_the_history() {
1627 let mut input = Input::new();
1628 input.type_text("one two three");
1629
1630 input.undo();
1631 assert_eq!(input.text(), "one two ");
1632 input.undo();
1633 assert_eq!(input.text(), "one ");
1634 input.undo();
1635 assert_eq!(input.text(), "");
1636 }
1637
1638 #[test]
1640 fn undo_with_nothing_to_undo_leaves_the_text_alone() {
1641 let mut input = Input::new();
1642 input.type_text("only");
1643
1644 input.undo();
1645 input.undo();
1646 input.undo();
1647
1648 assert_eq!(input.text(), "");
1649 }
1650
1651 #[test]
1653 fn a_fresh_edit_after_an_undo_clears_the_redo_stack() {
1654 let mut input = Input::new();
1655 input.type_text("first");
1656 input.type_text(" second");
1657
1658 input.undo();
1659 assert_eq!(input.text(), "first ");
1660 input.type_text("third");
1661 input.redo();
1662
1663 assert_eq!(
1664 input.text(),
1665 "first third",
1666 "redo must not resurrect a branch that was typed over",
1667 );
1668 }
1669
1670 #[test]
1672 fn control_y_also_redoes() {
1673 let mut input = Input::new();
1674 input.type_text("first");
1675 input.type_text(" second");
1676 let full = input.text().to_string();
1677
1678 input.undo();
1679 input.press(Key::Character("y".into()), Code::KeyY, Modifiers::CONTROL);
1680
1681 assert_eq!(input.text(), full);
1682 }
1683
1684 #[test]
1689 fn the_undo_chord_does_not_type_its_own_character() {
1690 let mut input = Input::new();
1691 input.type_text("text");
1692
1693 input.undo();
1694 input.redo();
1695
1696 assert!(
1697 !input.text().contains('z'),
1698 "the undo chord leaked into the buffer: {:?}",
1699 input.text(),
1700 );
1701 }
1702
1703 #[test]
1705 fn undo_restores_the_selection_along_with_the_text() {
1706 let mut input = Input::new();
1707 input.type_text("alpha");
1708 input.type_text(" beta");
1709
1710 input.undo();
1711
1712 let selection = input.data.editor.raw_selection();
1713 assert_eq!(
1714 selection.focus().index(),
1715 input.text().len(),
1716 "the caret should return to the end of the restored text",
1717 );
1718 }
1719
1720 #[test]
1722 fn the_history_is_capped_at_the_maximum_depth() {
1723 let mut history = TextEditHistory::default();
1724 for i in 0..(MAX_UNDO_DEPTH + 50) {
1725 history.record(TextEditSnapshot {
1726 text: format!("state {i}"),
1727 anchor: 0,
1728 focus: 0,
1729 });
1730 }
1731
1732 assert!(
1733 history.undo.len() <= MAX_UNDO_DEPTH,
1734 "history grew to {} entries, past the {MAX_UNDO_DEPTH} cap",
1735 history.undo.len(),
1736 );
1737 }
1738}
1739
1740#[cfg(test)]
1747mod history_chord_tests {
1748 use super::*;
1749 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1750 use keyboard_types::Location;
1751
1752 fn event(key: Key, code: Code, modifiers: Modifiers) -> BlitzKeyEvent {
1753 BlitzKeyEvent {
1754 key,
1755 code,
1756 modifiers,
1757 location: Location::Standard,
1758 is_auto_repeating: false,
1759 is_composing: false,
1760 state: KeyState::Pressed,
1761 text: None,
1762 }
1763 }
1764
1765 #[test]
1766 fn undo_is_recognised_under_control_and_under_the_platform_modifier() {
1767 for modifiers in [Modifiers::CONTROL, ACTION_MOD] {
1768 assert_eq!(
1769 history_command(&event(Key::Character("z".into()), Code::KeyZ, modifiers)),
1770 Some(HistoryCommand::Undo),
1771 );
1772 }
1773 }
1774
1775 #[test]
1776 fn shift_z_redoes_under_either_modifier() {
1777 for modifiers in [Modifiers::CONTROL, ACTION_MOD] {
1778 assert_eq!(
1779 history_command(&event(
1780 Key::Character("z".into()),
1781 Code::KeyZ,
1782 modifiers | Modifiers::SHIFT,
1783 )),
1784 Some(HistoryCommand::Redo),
1785 );
1786 }
1787 }
1788
1789 #[test]
1791 fn a_remapped_character_still_undoes_by_its_physical_key() {
1792 assert_eq!(
1793 history_command(&event(
1794 Key::Character("w".into()),
1795 Code::KeyZ,
1796 Modifiers::CONTROL,
1797 )),
1798 Some(HistoryCommand::Undo),
1799 );
1800 }
1801
1802 #[test]
1803 fn the_chord_needs_a_modifier() {
1804 assert_eq!(
1805 history_command(&event(
1806 Key::Character("z".into()),
1807 Code::KeyZ,
1808 Modifiers::empty(),
1809 )),
1810 None,
1811 );
1812 }
1813}