1use blitz_traits::{
2 events::{BlitzImeEvent, BlitzKeyEvent},
3 shell::ShellProvider,
4};
5use keyboard_types::{Code, Key, Modifiers};
6use parley::{ContentWidths, FontContext, LayoutContext};
7
8use crate::util::{ACTION_MOD, has_clipboard_modifier};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11enum ClipboardCommand {
12 Copy,
13 Cut,
14 Paste,
15}
16
17fn clipboard_command(event: &BlitzKeyEvent) -> Option<ClipboardCommand> {
18 if !has_clipboard_modifier(event.modifiers) {
19 return None;
20 }
21 match event.code {
22 Code::KeyC => Some(ClipboardCommand::Copy),
23 Code::KeyX => Some(ClipboardCommand::Cut),
24 Code::KeyV => Some(ClipboardCommand::Paste),
25 _ => match &event.key {
26 Key::Character(c) if c.eq_ignore_ascii_case("c") => Some(ClipboardCommand::Copy),
27 Key::Character(c) if c.eq_ignore_ascii_case("x") => Some(ClipboardCommand::Cut),
28 Key::Character(c) if c.eq_ignore_ascii_case("v") => Some(ClipboardCommand::Paste),
29 _ => None,
30 },
31 }
32}
33
34#[derive(Debug, Clone, Copy, Default, PartialEq)]
35pub struct TextBrush {
37 pub id: usize,
39}
40
41impl TextBrush {
42 pub(crate) fn from_id(id: usize) -> Self {
43 Self { id }
44 }
45}
46
47#[derive(Clone, Debug)]
52pub struct CachedContentWidths {
53 inline_box_widths: Box<[u32]>,
58 widths: ContentWidths,
59}
60
61#[derive(Clone, Default)]
62pub struct TextLayout {
63 pub text: String,
64 pub content_widths: Option<CachedContentWidths>,
65 pub layout: parley::layout::Layout<TextBrush>,
66}
67
68impl TextLayout {
69 pub fn new() -> Self {
70 Default::default()
71 }
72
73 pub fn content_widths(&mut self) -> ContentWidths {
99 let inline_box_widths: Box<[u32]> = self
103 .layout
104 .inline_boxes()
105 .iter()
106 .map(|ibox| ibox.width.to_bits())
107 .collect();
108
109 if let Some(cached) = &self.content_widths
110 && cached.inline_box_widths == inline_box_widths
111 {
112 return cached.widths;
113 }
114
115 let widths = self.layout.calculate_content_widths();
116 self.content_widths = Some(CachedContentWidths {
117 inline_box_widths,
118 widths,
119 });
120 widths
121 }
122}
123
124impl std::fmt::Debug for TextLayout {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "TextLayout")
127 }
128}
129
130pub enum GeneratedTextInputEvent {
132 Input,
133 Select,
134 PreEditChange,
135 Submit,
136}
137
138pub struct TextInputData {
139 pub editor: Box<parley::PlainEditor<TextBrush>>,
141 pub placeholder_editor: Option<Box<parley::PlainEditor<TextBrush>>>,
143 pub is_multiline: bool,
145 pub scroll_offset: f32,
151 pub layout_width: Option<f32>,
152}
153
154impl Clone for TextInputData {
156 fn clone(&self) -> Self {
157 TextInputData::new(self.is_multiline)
158 }
159}
160
161impl TextInputData {
162 pub fn new(is_multiline: bool) -> Self {
163 let editor = Box::new(parley::PlainEditor::new(16.0));
164 Self {
165 editor,
166 placeholder_editor: None,
167 is_multiline,
168 scroll_offset: 0.0,
169 layout_width: None,
170 }
171 }
172
173 pub fn sync_multiline_width(
174 &mut self,
175 font_ctx: &mut FontContext,
176 layout_ctx: &mut LayoutContext<TextBrush>,
177 width: f32,
178 ) {
179 if !self.is_multiline || width <= 0.0 {
180 return;
181 }
182 if self
183 .layout_width
184 .is_some_and(|current| (current - width).abs() < 0.01)
185 {
186 return;
187 }
188 self.layout_width = Some(width);
189 self.editor.set_width(Some(width));
190 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
191 if let Some(placeholder) = self.placeholder_editor.as_mut() {
192 placeholder.set_width(Some(width));
193 placeholder.driver(font_ctx, layout_ctx).refresh_layout();
194 }
195 }
196
197 pub fn set_text(
198 &mut self,
199 font_ctx: &mut FontContext,
200 layout_ctx: &mut LayoutContext<TextBrush>,
201 text: &str,
202 ) {
203 if self.editor.text() != text {
204 self.editor.set_text(text);
205 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
206 }
207 }
208
209 pub fn clamp_scroll_offset(&mut self, content_box_width: f32, content_box_height: f32) {
215 let Some(layout) = self.editor.try_layout() else {
216 return;
217 };
218 let scale = layout.scale();
221
222 let Some(caret) = self.editor.cursor_geometry(1.5) else {
224 return;
225 };
226
227 let (caret_start, caret_end, content, viewport) = if self.is_multiline {
229 (
230 caret.y0 as f32 / scale,
231 caret.y1 as f32 / scale,
232 layout.height() / scale,
233 content_box_height,
234 )
235 } else {
236 (
237 caret.x0 as f32 / scale,
238 caret.x1 as f32 / scale,
239 layout.full_width() / scale,
240 content_box_width,
241 )
242 };
243
244 let mut offset = self.scroll_offset;
245
246 if caret_end > offset + viewport {
248 offset = caret_end - viewport;
249 }
250 if caret_start < offset {
251 offset = caret_start;
252 }
253
254 let max_offset = (content.max(caret_end) - viewport).max(0.0);
258 self.scroll_offset = offset.clamp(0.0, max_offset);
259 }
260
261 pub fn max_scroll_offset(&self, content_box_width: f32, content_box_height: f32) -> f32 {
268 let Some(layout) = self.editor.try_layout() else {
269 return 0.0;
270 };
271 let scale = layout.scale();
272 let (content, viewport) = if self.is_multiline {
273 (layout.height() / scale, content_box_height)
274 } else {
275 (layout.full_width() / scale, content_box_width)
276 };
277 (content - viewport).max(0.0)
278 }
279
280 pub fn scroll_by(
287 &mut self,
288 delta: f32,
289 content_box_width: f32,
290 content_box_height: f32,
291 ) -> f32 {
292 let max_offset = self.max_scroll_offset(content_box_width, content_box_height);
293 if max_offset <= 0.0 {
294 return delta;
295 }
296
297 let new_offset = (self.scroll_offset - delta).clamp(0.0, max_offset);
300 let consumed = self.scroll_offset - new_offset;
301 self.scroll_offset = new_offset;
302 delta - consumed
303 }
304
305 pub(crate) fn apply_keypress_event(
306 &mut self,
307 font_ctx: &mut FontContext,
308 layout_ctx: &mut LayoutContext<TextBrush>,
309 shell_provider: &dyn ShellProvider,
310 event: BlitzKeyEvent,
311 ) -> Option<GeneratedTextInputEvent> {
312 if !event.state.is_pressed() {
314 return None;
315 }
316
317 let mods = event.modifiers;
318 let shift = mods.contains(Modifiers::SHIFT);
319 let action_mod = mods.contains(ACTION_MOD);
320 let word_mod = mods.contains(Modifiers::ALT);
321 let is_multiline = self.is_multiline;
322 let editor = &mut self.editor;
323 let mut driver = editor.driver(font_ctx, layout_ctx);
324 if let Some(command) = clipboard_command(&event) {
325 match command {
326 ClipboardCommand::Copy => {
327 if let Some(text) = driver.editor.selected_text() {
328 let _ = shell_provider.set_clipboard_text(text.to_owned());
329 }
330 }
331 ClipboardCommand::Cut => {
332 if let Some(text) = driver.editor.selected_text() {
333 let _ = shell_provider.set_clipboard_text(text.to_owned());
334 driver.delete_selection()
335 }
336 }
337 ClipboardCommand::Paste => {
338 let text = shell_provider.get_clipboard_text().unwrap_or_default();
339 driver.insert_or_replace_selection(&text)
340 }
341 }
342
343 return Some(GeneratedTextInputEvent::Input);
344 }
345 match event.key {
346 Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
347 if shift {
348 driver.collapse_selection()
349 } else {
350 driver.select_all()
351 }
352 return Some(GeneratedTextInputEvent::Select);
353 }
354 Key::ArrowLeft => {
355 if action_mod {
356 if shift {
357 driver.select_to_line_start()
358 } else {
359 driver.move_to_line_start()
360 }
361 } else if word_mod {
362 if shift {
363 driver.select_word_left()
364 } else {
365 driver.move_word_left()
366 }
367 } else if shift {
368 driver.select_left()
369 } else {
370 driver.move_left()
371 }
372 return Some(GeneratedTextInputEvent::Select);
373 }
374 Key::ArrowRight => {
375 if action_mod {
376 if shift {
377 driver.select_to_line_end()
378 } else {
379 driver.move_to_line_end()
380 }
381 } else if word_mod {
382 if shift {
383 driver.select_word_right()
384 } else {
385 driver.move_word_right()
386 }
387 } else if shift {
388 driver.select_right()
389 } else {
390 driver.move_right()
391 }
392 return Some(GeneratedTextInputEvent::Select);
393 }
394 Key::ArrowUp => {
395 if action_mod && shift {
396 driver.select_to_text_start()
397 } else if action_mod {
398 driver.move_to_text_start()
399 } else if shift {
400 driver.select_up()
401 } else {
402 driver.move_up()
403 }
404 return Some(GeneratedTextInputEvent::Select);
405 }
406 Key::ArrowDown => {
407 if action_mod && shift {
408 driver.select_to_text_end()
409 } else if action_mod {
410 driver.move_to_text_end()
411 } else if shift {
412 driver.select_down()
413 } else {
414 driver.move_down()
415 }
416 return Some(GeneratedTextInputEvent::Select);
417 }
418 Key::Home => {
419 if action_mod {
420 if shift {
421 driver.select_to_text_start()
422 } else {
423 driver.move_to_text_start()
424 }
425 } else if shift {
426 driver.select_to_line_start()
427 } else {
428 driver.move_to_line_start()
429 }
430 return Some(GeneratedTextInputEvent::Select);
431 }
432 Key::End => {
433 if action_mod {
434 if shift {
435 driver.select_to_text_end()
436 } else {
437 driver.move_to_text_end()
438 }
439 } else if shift {
440 driver.select_to_line_end()
441 } else {
442 driver.move_to_line_end()
443 }
444 return Some(GeneratedTextInputEvent::Select);
445 }
446 Key::Delete => {
447 #[cfg(target_os = "macos")]
448 if mods.contains(Modifiers::SUPER) {
449 if driver.editor.raw_selection().is_collapsed() {
450 driver.select_to_line_end();
451 }
452 driver.delete_selection();
453 } else if mods.contains(Modifiers::ALT) {
454 driver.delete_word();
455 } else {
456 driver.delete();
457 }
458 #[cfg(not(target_os = "macos"))]
459 if action_mod {
460 driver.delete_word();
461 } else {
462 driver.delete();
463 }
464 return Some(GeneratedTextInputEvent::Input);
465 }
466 Key::Backspace => {
467 #[cfg(target_os = "macos")]
468 if mods.contains(Modifiers::SUPER) {
469 if driver.editor.raw_selection().is_collapsed() {
470 driver.select_to_line_start();
471 }
472 driver.delete_selection();
473 } else if mods.contains(Modifiers::ALT) {
474 driver.backdelete_word();
475 } else {
476 driver.backdelete();
477 }
478 #[cfg(not(target_os = "macos"))]
479 if action_mod {
480 driver.backdelete_word();
481 } else {
482 driver.backdelete();
483 }
484 return Some(GeneratedTextInputEvent::Input);
485 }
486
487 Key::Character(c) if c == "\n" => {
488 if is_multiline {
489 driver.insert_or_replace_selection("\n");
490 return Some(GeneratedTextInputEvent::Input);
491 } else {
492 return Some(GeneratedTextInputEvent::Submit);
493 }
494 }
495 Key::Enter => {
496 if is_multiline {
497 driver.insert_or_replace_selection("\n");
498 return Some(GeneratedTextInputEvent::Input);
499 } else {
500 return Some(GeneratedTextInputEvent::Submit);
501 }
502 }
503 Key::Character(s)
504 if !mods.contains(Modifiers::CONTROL) && !mods.contains(Modifiers::SUPER) =>
505 {
506 driver.insert_or_replace_selection(&s);
507 return Some(GeneratedTextInputEvent::Input);
508 }
509 _ => {}
510 };
511
512 None
513 }
514
515 pub(crate) fn apply_apple_standard_keybinding(
516 &mut self,
517 font_ctx: &mut FontContext,
518 layout_ctx: &mut LayoutContext<TextBrush>,
519 shell_provider: &dyn ShellProvider,
520 command: &str,
521 ) -> Option<GeneratedTextInputEvent> {
522 let editor = &mut self.editor;
523 let mut driver = editor.driver(font_ctx, layout_ctx);
524 let is_multiline = self.is_multiline;
525
526 match command {
527 "insertBacktab:" => {}
531 "insertContainerBreak:" => {}
533 "insertDoubleQuoteIgnoringSubstitution:" => {
535 driver.insert_or_replace_selection("\"");
536 return Some(GeneratedTextInputEvent::Input);
537 }
538 "insertLineBreak:" => {
540 driver.insert_or_replace_selection("\n");
541 return Some(GeneratedTextInputEvent::Input);
542 }
543 "insertNewline:" => {
545 if is_multiline {
546 driver.insert_or_replace_selection("\n");
547 return Some(GeneratedTextInputEvent::Input);
548 } else {
549 return Some(GeneratedTextInputEvent::Submit);
550 }
551 }
552 "insertNewlineIgnoringFieldEditor:" => {
554 driver.insert_or_replace_selection("\n");
555 return Some(GeneratedTextInputEvent::Input);
556 }
557 "insertParagraphSeparator:" => {
559 driver.insert_or_replace_selection("\n");
560 return Some(GeneratedTextInputEvent::Input);
561 }
562 "insertSingleQuoteIgnoringSubstitution:" => {
563 driver.insert_or_replace_selection("'");
564 return Some(GeneratedTextInputEvent::Input);
565 }
566 "insertTab:" | "insertTabIgnoringFieldEditor:" => {
568 }
570 "insertText:" => {}
572
573 "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {}
579 "deleteForward:" => {}
580 "deleteToBeginningOfLine:" => {
582 if driver.editor.raw_selection().is_collapsed() {
583 driver.select_to_line_start();
584 }
585 driver.delete_selection();
586 return Some(GeneratedTextInputEvent::Input);
587 }
588 "deleteToEndOfLine:" => {
590 if driver.editor.raw_selection().is_collapsed() {
591 driver.select_to_line_end();
592 }
593 driver.delete_selection();
594 return Some(GeneratedTextInputEvent::Input);
595 }
596 "deleteToBeginningOfParagraph:" => {
597 if driver.editor.raw_selection().is_collapsed() {
598 driver.select_to_hard_line_start();
599 }
600 driver.delete_selection();
601 return Some(GeneratedTextInputEvent::Input);
602 }
603
604 "deleteToEndOfParagraph:" => {
606 if driver.editor.raw_selection().is_collapsed() {
607 driver.select_to_hard_line_end();
608 }
609 driver.delete_selection();
610 return Some(GeneratedTextInputEvent::Input);
611 }
612 "deleteWordBackward:" => {}
614 "deleteWordForward:" => {}
616 "yank:" => {
618 if let Some(text) = driver.editor.selected_text() {
619 let _ = shell_provider.set_clipboard_text(text.to_owned());
620 driver.delete_selection();
621 return Some(GeneratedTextInputEvent::Input);
622 }
623 }
624
625 "moveBackward:" => {
629 driver.move_left(); return Some(GeneratedTextInputEvent::Select);
631 }
632
633 "moveDown:" => {
635 driver.move_down();
636 return Some(GeneratedTextInputEvent::Select);
637 }
638 "moveForward:" => {
640 driver.move_right();
641 return Some(GeneratedTextInputEvent::Select);
642 } "moveLeft:" => {
646 driver.move_left();
647 return Some(GeneratedTextInputEvent::Select);
648 }
649 "moveRight:" => {
651 driver.move_right();
652 return Some(GeneratedTextInputEvent::Select);
653 }
654 "moveUp:" => {
656 driver.move_up();
657 return Some(GeneratedTextInputEvent::Select);
658 }
659
660 "moveBackwardAndModifySelection:" => {
664 driver.select_left(); return Some(GeneratedTextInputEvent::Select);
666 }
667 "moveDownAndModifySelection:" => {
669 driver.select_down();
670 return Some(GeneratedTextInputEvent::Select);
671 }
672 "moveForwardAndModifySelection:" => {
674 driver.select_right(); return Some(GeneratedTextInputEvent::Select);
676 }
677 "moveLeftAndModifySelection:" => {
679 driver.select_left();
680 return Some(GeneratedTextInputEvent::Select);
681 }
682 "moveRightAndModifySelection:" => {
684 driver.select_right();
685 return Some(GeneratedTextInputEvent::Select);
686 }
687 "moveUpAndModifySelection:" => {
689 driver.select_up();
690 return Some(GeneratedTextInputEvent::Select);
691 }
692
693 "selectAll:" => {
695 driver.select_all();
696 return Some(GeneratedTextInputEvent::Select);
697 }
698 "selectLine:" => {
699 driver.move_to_line_start();
700 driver.select_to_line_end();
701 return Some(GeneratedTextInputEvent::Select);
702 }
703 "selectParagraph:" => {
704 driver.move_to_hard_line_start();
705 driver.select_to_hard_line_end();
706 return Some(GeneratedTextInputEvent::Select);
707 }
708 "selectWord:" => {
709 }
711
712 "moveToBeginningOfDocument:" => {
714 driver.move_to_text_start();
715 return Some(GeneratedTextInputEvent::Select);
716 }
717 "moveToBeginningOfDocumentAndModifySelection:" => {
718 driver.select_to_text_start();
719 return Some(GeneratedTextInputEvent::Select);
720 }
721 "moveToEndOfDocument:" => {
722 driver.move_to_text_end();
723 return Some(GeneratedTextInputEvent::Select);
724 }
725 "moveToEndOfDocumentAndModifySelection:" => {
726 driver.move_to_text_end();
727 return Some(GeneratedTextInputEvent::Select);
728 }
729
730 "moveParagraphBackwardAndModifySelection:" => {}
732 "moveParagraphForwardAndModifySelection:" => {}
733 "moveToBeginningOfParagraph:" => {
734 driver.move_to_hard_line_start();
735 return Some(GeneratedTextInputEvent::Select);
736 }
737 "moveToBeginningOfParagraphAndModifySelection:" => {
738 driver.select_to_hard_line_start();
739 return Some(GeneratedTextInputEvent::Select);
740 }
741 "moveToEndOfParagraph:" => {
742 driver.move_to_hard_line_end();
743 return Some(GeneratedTextInputEvent::Select);
744 }
745 "moveToEndOfParagraphAndModifySelection:" => {
746 driver.select_to_hard_line_end();
747 return Some(GeneratedTextInputEvent::Select);
748 }
749
750 "moveToBeginningOfLine:" => {
752 driver.move_to_line_start();
753 return Some(GeneratedTextInputEvent::Select);
754 }
755 "moveToBeginningOfLineAndModifySelection:" => {
756 driver.select_to_line_start();
757 return Some(GeneratedTextInputEvent::Select);
758 }
759 "moveToEndOfLine:" => {
760 driver.move_to_line_end();
761 return Some(GeneratedTextInputEvent::Select);
762 }
763 "moveToEndOfLineAndModifySelection:" => {
764 driver.select_to_line_end();
765 return Some(GeneratedTextInputEvent::Select);
766 }
767 "moveToLeftEndOfLine:" => {
768 driver.move_to_text_start();
769 return Some(GeneratedTextInputEvent::Select);
770 }
771 "moveToLeftEndOfLineAndModifySelection:" => {
772 driver.select_to_line_start();
773 return Some(GeneratedTextInputEvent::Select);
774 }
775 "moveToRightEndOfLine:" => {
776 driver.move_to_line_end();
777 return Some(GeneratedTextInputEvent::Select);
778 }
779 "moveToRightEndOfLineAndModifySelection:" => {
780 driver.select_to_line_end();
781 return Some(GeneratedTextInputEvent::Select);
782 }
783
784 "moveWordBackward:" => {
786 driver.move_word_left();
787 return Some(GeneratedTextInputEvent::Select);
788 }
789 "moveWordBackwardAndModifySelection:" => {
790 driver.select_word_left();
791 return Some(GeneratedTextInputEvent::Select);
792 }
793 "moveWordForward:" => {
794 driver.move_word_right();
795 return Some(GeneratedTextInputEvent::Select);
796 }
797 "moveWordForwardAndModifySelection:" => {
798 driver.select_word_right();
799 return Some(GeneratedTextInputEvent::Select);
800 }
801 "moveWordLeft:" => {
802 driver.move_word_left();
803 return Some(GeneratedTextInputEvent::Select);
804 }
805 "moveWordLeftAndModifySelection:" => {
806 driver.select_word_left();
807 return Some(GeneratedTextInputEvent::Select);
808 }
809 "moveWordRight:" => {
810 driver.move_word_right();
811 return Some(GeneratedTextInputEvent::Select);
812 }
813 "moveWordRightAndModifySelection:" => {
814 driver.select_word_right();
815 return Some(GeneratedTextInputEvent::Select);
816 }
817
818 "scrollPageDown:" => {}
822 "scrollPageUp:" => {}
824 "scrollLineDown:" => {}
826 "scrollLineUp:" => {}
828 "scrollToBeginningOfDocument:" => {}
830 "scrollToEndOfDocument:" => {}
832 "pageDown:" => {}
834 "pageUp:" => {}
836 "pageDownAndModifySelection:" => {}
838 "pageUpAndModifySelection:" => {}
840 "centerSelectionInVisibleArea:" => {}
842
843 "transpose:" => {}
847 "transposeWords:" => {}
849
850 "indent:" => {}
853
854 "cancelOperation:" => {}
857
858 "quickLookPreviewItems:" => {}
861
862 "makeBaseWritingDirectionLeftToRight:" => {}
864 "makeBaseWritingDirectionNatural:" => {}
865 "makeBaseWritingDirectionRightToLeft:" => {}
866 "makeTextWritingDirectionLeftToRight:" => {}
867 "makeTextWritingDirectionNatural:" => {}
868 "makeTextWritingDirectionRightToLeft:" => {}
869
870 "capitalizeWord:" => {}
872 "changeCaseOfLetter:" => {}
873 "lowercaseWord:" => {}
874 "uppercaseWord:" => {}
875
876 "setMark:" => {}
878 "selectToMark:" => {}
879 "deleteToMark:" => {}
880 "swapWithMark:" => {}
881
882 "complete:" => {}
884
885 "showContextMenuForSelection:" => {}
887
888 _ => {}
890 };
891
892 None
893 }
894
895 pub(crate) fn apply_ime_event(
896 &mut self,
897 font_ctx: &mut FontContext,
898 layout_ctx: &mut LayoutContext<TextBrush>,
899 event: BlitzImeEvent,
900 ) -> Option<GeneratedTextInputEvent> {
901 let editor = &mut self.editor;
902 let mut driver = editor.driver(font_ctx, layout_ctx);
903
904 match event {
905 BlitzImeEvent::Enabled => {
906 None
908 }
909 BlitzImeEvent::Disabled => {
910 driver.clear_compose();
911 Some(GeneratedTextInputEvent::PreEditChange)
912 }
913 BlitzImeEvent::Commit(text) => {
914 driver.insert_or_replace_selection(&text);
915 Some(GeneratedTextInputEvent::Input)
916 }
917 BlitzImeEvent::Preedit(text, cursor) => {
918 if text.is_empty() {
919 driver.clear_compose();
920 } else {
921 driver.set_compose(&text, cursor);
922 }
923 Some(GeneratedTextInputEvent::PreEditChange)
924 }
925 BlitzImeEvent::DeleteSurrounding {
926 before_bytes,
927 after_bytes,
928 } => {
929 let _ = before_bytes;
930 let _ = after_bytes;
931 None
933 }
934 }
935 }
936}
937
938#[cfg(test)]
939mod content_widths_cache_tests {
940 use super::*;
941 use parley::{InlineBox, InlineBoxKind, TextStyle};
942
943 fn build_layout(text: &str, inline_box_width: Option<f32>) -> TextLayout {
946 let mut font_ctx = FontContext::default();
947 let mut layout_ctx = LayoutContext::new();
948 let style: TextStyle<'_, '_, TextBrush> = TextStyle::default();
949 let mut builder = layout_ctx.tree_builder(&mut font_ctx, 1.0, true, &style);
950 builder.push_text(text);
951 if let Some(width) = inline_box_width {
952 builder.push_inline_box(InlineBox {
953 id: 0,
954 kind: InlineBoxKind::InFlow,
955 index: text.len(),
956 width,
957 height: 10.0,
958 });
959 }
960
961 let mut text_layout = TextLayout::new();
962 text_layout.text = builder.build_into(&mut text_layout.layout);
963 text_layout
964 }
965
966 #[test]
967 fn first_call_matches_an_uncached_computation() {
968 let mut text_layout = build_layout("the quick brown fox", None);
969 let expected = text_layout.layout.calculate_content_widths();
970
971 let cached = text_layout.content_widths();
972
973 assert_eq!(cached.min, expected.min);
974 assert_eq!(cached.max, expected.max);
975 assert!(cached.min > 0.0);
976 assert!(cached.max > cached.min);
977 }
978
979 #[test]
980 fn text_only_layout_reuses_the_cached_widths() {
981 let mut text_layout = build_layout("the quick brown fox", None);
982 text_layout.content_widths();
983
984 let poison = ContentWidths {
987 min: -1.0,
988 max: -2.0,
989 };
990 text_layout.content_widths.as_mut().unwrap().widths = poison;
991
992 let second = text_layout.content_widths();
993 assert_eq!(second.min, poison.min);
994 assert_eq!(second.max, poison.max);
995 }
996
997 #[test]
998 fn a_changed_inline_box_width_forces_a_recompute() {
999 let mut text_layout = build_layout("the quick brown fox", Some(40.0));
1000 let first = text_layout.content_widths();
1001
1002 text_layout.content_widths.as_mut().unwrap().widths = ContentWidths {
1004 min: -1.0,
1005 max: -2.0,
1006 };
1007
1008 text_layout.layout.inline_boxes_mut()[0].width = 400.0;
1011
1012 let second = text_layout.content_widths();
1013 assert!(second.min > 0.0);
1014 assert!(second.max > first.max);
1015 assert_eq!(second.min, 400.0);
1016 }
1017
1018 #[test]
1019 fn an_unchanged_inline_box_width_still_hits_the_cache() {
1020 let mut text_layout = build_layout("the quick brown fox", Some(40.0));
1021 text_layout.content_widths();
1022
1023 let poison = ContentWidths {
1024 min: -1.0,
1025 max: -2.0,
1026 };
1027 text_layout.content_widths.as_mut().unwrap().widths = poison;
1028 text_layout.layout.inline_boxes_mut()[0].width = 40.0;
1030
1031 let second = text_layout.content_widths();
1032 assert_eq!(second.min, poison.min);
1033 assert_eq!(second.max, poison.max);
1034 }
1035
1036 #[test]
1037 fn rebuilding_the_layout_discards_the_cache() {
1038 let mut text_layout = build_layout("the quick brown fox", None);
1039 text_layout.content_widths();
1040 assert!(text_layout.content_widths.is_some());
1041
1042 text_layout.content_widths = None;
1044 let rebuilt = build_layout("a much much much longer run of text", None);
1045 text_layout.layout = rebuilt.layout;
1046 text_layout.text = rebuilt.text;
1047
1048 let widths = text_layout.content_widths();
1049 let expected = text_layout.layout.calculate_content_widths();
1050 assert_eq!(widths.max, expected.max);
1051 }
1052}
1053
1054#[cfg(test)]
1055mod shortcut_tests {
1056 use super::*;
1057 use blitz_traits::events::{BlitzKeyEvent, KeyState};
1058 use blitz_traits::shell::DummyShellProvider;
1059 use keyboard_types::Location;
1060
1061 fn control_event(key: Key, code: Code) -> BlitzKeyEvent {
1062 BlitzKeyEvent {
1063 key,
1064 code,
1065 modifiers: Modifiers::CONTROL,
1066 location: Location::Standard,
1067 is_auto_repeating: false,
1068 is_composing: false,
1069 state: KeyState::Pressed,
1070 text: None,
1071 }
1072 }
1073
1074 #[test]
1075 fn control_character_cut_uses_the_physical_key_code() {
1076 let event = control_event(Key::Character("\u{18}".into()), Code::KeyX);
1077 assert_eq!(clipboard_command(&event), Some(ClipboardCommand::Cut));
1078 }
1079
1080 #[test]
1081 fn backspace_does_not_depend_on_an_apple_standard_keybinding() {
1082 let mut data = TextInputData::new(false);
1083 let mut font_ctx = FontContext::default();
1084 let mut layout_ctx = LayoutContext::new();
1085 data.set_text(&mut font_ctx, &mut layout_ctx, "typo");
1086 data.editor
1087 .driver(&mut font_ctx, &mut layout_ctx)
1088 .move_to_text_end();
1089 let event = BlitzKeyEvent {
1090 key: Key::Backspace,
1091 code: Code::Backspace,
1092 modifiers: Modifiers::empty(),
1093 location: Location::Standard,
1094 is_auto_repeating: false,
1095 is_composing: false,
1096 state: KeyState::Pressed,
1097 text: None,
1098 };
1099
1100 assert!(matches!(
1101 data.apply_keypress_event(&mut font_ctx, &mut layout_ctx, &DummyShellProvider, event,),
1102 Some(GeneratedTextInputEvent::Input)
1103 ));
1104 assert_eq!(data.editor.raw_text(), "typ");
1105 }
1106}