1use blitz_traits::{
2 events::{BlitzImeEvent, BlitzKeyEvent},
3 shell::ShellProvider,
4};
5use keyboard_types::{Key, Modifiers};
6use parley::{ContentWidths, FontContext, LayoutContext};
7
8use crate::util::ACTION_MOD;
9
10#[derive(Debug, Clone, Copy, Default, PartialEq)]
11pub struct TextBrush {
13 pub id: usize,
15}
16
17impl TextBrush {
18 pub(crate) fn from_id(id: usize) -> Self {
19 Self { id }
20 }
21}
22
23#[derive(Clone, Default)]
24pub struct TextLayout {
25 pub text: String,
26 pub content_widths: Option<ContentWidths>,
27 pub layout: parley::layout::Layout<TextBrush>,
28}
29
30impl TextLayout {
31 pub fn new() -> Self {
32 Default::default()
33 }
34
35 pub fn content_widths(&mut self) -> ContentWidths {
36 *self
37 .content_widths
38 .get_or_insert_with(|| self.layout.calculate_content_widths())
39 }
40}
41
42impl std::fmt::Debug for TextLayout {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "TextLayout")
45 }
46}
47
48pub enum GeneratedTextInputEvent {
50 Input,
51 Select,
52 PreEditChange,
53 Submit,
54}
55
56pub struct TextInputData {
57 pub editor: Box<parley::PlainEditor<TextBrush>>,
59 pub is_multiline: bool,
61 pub scroll_offset: f32,
67}
68
69impl Clone for TextInputData {
71 fn clone(&self) -> Self {
72 TextInputData::new(self.is_multiline)
73 }
74}
75
76impl TextInputData {
77 pub fn new(is_multiline: bool) -> Self {
78 let editor = Box::new(parley::PlainEditor::new(16.0));
79 Self {
80 editor,
81 is_multiline,
82 scroll_offset: 0.0,
83 }
84 }
85
86 pub fn set_text(
87 &mut self,
88 font_ctx: &mut FontContext,
89 layout_ctx: &mut LayoutContext<TextBrush>,
90 text: &str,
91 ) {
92 if self.editor.text() != text {
93 self.editor.set_text(text);
94 self.editor.driver(font_ctx, layout_ctx).refresh_layout();
95 }
96 }
97
98 pub fn clamp_scroll_offset(&mut self, content_box_width: f32, content_box_height: f32) {
104 let Some(layout) = self.editor.try_layout() else {
105 return;
106 };
107 let scale = layout.scale();
110
111 let Some(caret) = self.editor.cursor_geometry(1.5) else {
113 return;
114 };
115
116 let (caret_start, caret_end, content, viewport) = if self.is_multiline {
118 (
119 caret.y0 as f32 / scale,
120 caret.y1 as f32 / scale,
121 layout.height() / scale,
122 content_box_height,
123 )
124 } else {
125 (
126 caret.x0 as f32 / scale,
127 caret.x1 as f32 / scale,
128 layout.full_width() / scale,
129 content_box_width,
130 )
131 };
132
133 let mut offset = self.scroll_offset;
134
135 if caret_end > offset + viewport {
137 offset = caret_end - viewport;
138 }
139 if caret_start < offset {
140 offset = caret_start;
141 }
142
143 let max_offset = (content.max(caret_end) - viewport).max(0.0);
147 self.scroll_offset = offset.clamp(0.0, max_offset);
148 }
149
150 pub fn max_scroll_offset(&self, content_box_width: f32, content_box_height: f32) -> f32 {
157 let Some(layout) = self.editor.try_layout() else {
158 return 0.0;
159 };
160 let scale = layout.scale();
161 let (content, viewport) = if self.is_multiline {
162 (layout.height() / scale, content_box_height)
163 } else {
164 (layout.full_width() / scale, content_box_width)
165 };
166 (content - viewport).max(0.0)
167 }
168
169 pub fn scroll_by(
176 &mut self,
177 delta: f32,
178 content_box_width: f32,
179 content_box_height: f32,
180 ) -> f32 {
181 let max_offset = self.max_scroll_offset(content_box_width, content_box_height);
182 if max_offset <= 0.0 {
183 return delta;
184 }
185
186 let new_offset = (self.scroll_offset - delta).clamp(0.0, max_offset);
189 let consumed = self.scroll_offset - new_offset;
190 self.scroll_offset = new_offset;
191 delta - consumed
192 }
193
194 pub(crate) fn apply_keypress_event(
195 &mut self,
196 font_ctx: &mut FontContext,
197 layout_ctx: &mut LayoutContext<TextBrush>,
198 shell_provider: &dyn ShellProvider,
199 event: BlitzKeyEvent,
200 ) -> Option<GeneratedTextInputEvent> {
201 if !event.state.is_pressed() {
203 return None;
204 }
205
206 let mods = event.modifiers;
207 let shift = mods.contains(Modifiers::SHIFT);
208 let action_mod = mods.contains(ACTION_MOD);
209
210 let is_multiline = self.is_multiline;
211 let editor = &mut self.editor;
212 let mut driver = editor.driver(font_ctx, layout_ctx);
213 match event.key {
214 Key::Character(c) if action_mod && matches!(c.as_str(), "c" | "x" | "v") => {
215 match c.to_lowercase().as_str() {
216 "c" => {
217 if let Some(text) = driver.editor.selected_text() {
218 let _ = shell_provider.set_clipboard_text(text.to_owned());
219 }
220 }
221 "x" => {
222 if let Some(text) = driver.editor.selected_text() {
223 let _ = shell_provider.set_clipboard_text(text.to_owned());
224 driver.delete_selection()
225 }
226 }
227 "v" => {
228 let text = shell_provider.get_clipboard_text().unwrap_or_default();
229 driver.insert_or_replace_selection(&text)
230 }
231 _ => unreachable!(),
232 }
233
234 return Some(GeneratedTextInputEvent::Input);
235 }
236 Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
237 if shift {
238 driver.collapse_selection()
239 } else {
240 driver.select_all()
241 }
242 return Some(GeneratedTextInputEvent::Select);
243 }
244 Key::ArrowLeft => {
245 if action_mod {
246 if shift {
247 driver.select_word_left()
248 } else {
249 driver.move_word_left()
250 }
251 } else if shift {
252 driver.select_left()
253 } else {
254 driver.move_left()
255 }
256 return Some(GeneratedTextInputEvent::Select);
257 }
258 Key::ArrowRight => {
259 if action_mod {
260 if shift {
261 driver.select_word_right()
262 } else {
263 driver.move_word_right()
264 }
265 } else if shift {
266 driver.select_right()
267 } else {
268 driver.move_right()
269 }
270 return Some(GeneratedTextInputEvent::Select);
271 }
272 Key::ArrowUp => {
273 if shift {
274 driver.select_up()
275 } else {
276 driver.move_up()
277 }
278 return Some(GeneratedTextInputEvent::Select);
279 }
280 Key::ArrowDown => {
281 if shift {
282 driver.select_down()
283 } else {
284 driver.move_down()
285 }
286 return Some(GeneratedTextInputEvent::Select);
287 }
288 Key::Home => {
289 if action_mod {
290 if shift {
291 driver.select_to_text_start()
292 } else {
293 driver.move_to_text_start()
294 }
295 } else if shift {
296 driver.select_to_line_start()
297 } else {
298 driver.move_to_line_start()
299 }
300 return Some(GeneratedTextInputEvent::Select);
301 }
302 Key::End => {
303 if action_mod {
304 if shift {
305 driver.select_to_text_end()
306 } else {
307 driver.move_to_text_end()
308 }
309 } else if shift {
310 driver.select_to_line_end()
311 } else {
312 driver.move_to_line_end()
313 }
314 return Some(GeneratedTextInputEvent::Select);
315 }
316 Key::Delete => {
317 if action_mod {
318 driver.delete_word()
319 } else {
320 driver.delete()
321 }
322 return Some(GeneratedTextInputEvent::Input);
323 }
324
325 #[cfg(not(target_os = "macos"))]
327 Key::Backspace => {
328 if action_mod {
329 driver.backdelete_word()
330 } else {
331 driver.backdelete()
332 }
333 return Some(GeneratedTextInputEvent::Input);
334 }
335
336 Key::Character(c) if c == "\n" => {
337 if is_multiline {
338 driver.insert_or_replace_selection("\n");
339 return Some(GeneratedTextInputEvent::Input);
340 } else {
341 return Some(GeneratedTextInputEvent::Submit);
342 }
343 }
344 Key::Enter => {
345 if is_multiline {
346 driver.insert_or_replace_selection("\n");
347 return Some(GeneratedTextInputEvent::Input);
348 } else {
349 return Some(GeneratedTextInputEvent::Submit);
350 }
351 }
352 Key::Character(s)
353 if !mods.contains(Modifiers::CONTROL) && !mods.contains(Modifiers::SUPER) =>
354 {
355 driver.insert_or_replace_selection(&s);
356 return Some(GeneratedTextInputEvent::Input);
357 }
358 _ => {}
359 };
360
361 None
362 }
363
364 pub(crate) fn apply_apple_standard_keybinding(
365 &mut self,
366 font_ctx: &mut FontContext,
367 layout_ctx: &mut LayoutContext<TextBrush>,
368 shell_provider: &dyn ShellProvider,
369 command: &str,
370 ) -> Option<GeneratedTextInputEvent> {
371 let editor = &mut self.editor;
372 let mut driver = editor.driver(font_ctx, layout_ctx);
373 let is_multiline = self.is_multiline;
374
375 match command {
376 "insertBacktab:" => {}
380 "insertContainerBreak:" => {}
382 "insertDoubleQuoteIgnoringSubstitution:" => {
384 driver.insert_or_replace_selection("\"");
385 return Some(GeneratedTextInputEvent::Input);
386 }
387 "insertLineBreak:" => {
389 driver.insert_or_replace_selection("\n");
390 return Some(GeneratedTextInputEvent::Input);
391 }
392 "insertNewline:" => {
394 if is_multiline {
395 driver.insert_or_replace_selection("\n");
396 return Some(GeneratedTextInputEvent::Input);
397 } else {
398 return Some(GeneratedTextInputEvent::Submit);
399 }
400 }
401 "insertNewlineIgnoringFieldEditor:" => {
403 driver.insert_or_replace_selection("\n");
404 return Some(GeneratedTextInputEvent::Input);
405 }
406 "insertParagraphSeparator:" => {
408 driver.insert_or_replace_selection("\n");
409 return Some(GeneratedTextInputEvent::Input);
410 }
411 "insertSingleQuoteIgnoringSubstitution:" => {
412 driver.insert_or_replace_selection("'");
413 return Some(GeneratedTextInputEvent::Input);
414 }
415 "insertTab:" | "insertTabIgnoringFieldEditor:" => {
417 }
419 "insertText:" => {}
421
422 "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
427 driver.backdelete();
428 return Some(GeneratedTextInputEvent::Input);
429 }
430 "deleteForward:" => {
431 driver.delete();
432 return Some(GeneratedTextInputEvent::Input);
433 }
434 "deleteToBeginningOfLine:" => {
436 if driver.editor.raw_selection().is_collapsed() {
437 driver.select_to_line_start();
438 }
439 driver.delete_selection();
440 return Some(GeneratedTextInputEvent::Input);
441 }
442 "deleteToEndOfLine:" => {
444 if driver.editor.raw_selection().is_collapsed() {
445 driver.select_to_line_end();
446 }
447 driver.delete_selection();
448 return Some(GeneratedTextInputEvent::Input);
449 }
450 "deleteToBeginningOfParagraph:" => {
451 if driver.editor.raw_selection().is_collapsed() {
452 driver.select_to_hard_line_start();
453 }
454 driver.delete_selection();
455 return Some(GeneratedTextInputEvent::Input);
456 }
457
458 "deleteToEndOfParagraph:" => {
460 if driver.editor.raw_selection().is_collapsed() {
461 driver.select_to_hard_line_end();
462 }
463 driver.delete_selection();
464 return Some(GeneratedTextInputEvent::Input);
465 }
466 "deleteWordBackward:" => {
468 driver.backdelete_word();
469 return Some(GeneratedTextInputEvent::Input);
470 }
471 "deleteWordForward:" => {
473 driver.delete_word();
474 return Some(GeneratedTextInputEvent::Input);
475 }
476 "yank:" => {
478 if let Some(text) = driver.editor.selected_text() {
479 let _ = shell_provider.set_clipboard_text(text.to_owned());
480 driver.delete_selection();
481 return Some(GeneratedTextInputEvent::Input);
482 }
483 }
484
485 "moveBackward:" => {
489 driver.move_left(); return Some(GeneratedTextInputEvent::Select);
491 }
492
493 "moveDown:" => {
495 driver.move_down();
496 return Some(GeneratedTextInputEvent::Select);
497 }
498 "moveForward:" => {
500 driver.move_right();
501 return Some(GeneratedTextInputEvent::Select);
502 } "moveLeft:" => {
506 driver.move_left();
507 return Some(GeneratedTextInputEvent::Select);
508 }
509 "moveRight:" => {
511 driver.move_right();
512 return Some(GeneratedTextInputEvent::Select);
513 }
514 "moveUp:" => {
516 driver.move_up();
517 return Some(GeneratedTextInputEvent::Select);
518 }
519
520 "moveBackwardAndModifySelection:" => {
524 driver.select_left(); return Some(GeneratedTextInputEvent::Select);
526 }
527 "moveDownAndModifySelection:" => {
529 driver.select_down();
530 return Some(GeneratedTextInputEvent::Select);
531 }
532 "moveForwardAndModifySelection:" => {
534 driver.select_right(); return Some(GeneratedTextInputEvent::Select);
536 }
537 "moveLeftAndModifySelection:" => {
539 driver.select_left();
540 return Some(GeneratedTextInputEvent::Select);
541 }
542 "moveRightAndModifySelection:" => {
544 driver.select_right();
545 return Some(GeneratedTextInputEvent::Select);
546 }
547 "moveUpAndModifySelection:" => {
549 driver.select_up();
550 return Some(GeneratedTextInputEvent::Select);
551 }
552
553 "selectAll:" => {
555 driver.select_all();
556 return Some(GeneratedTextInputEvent::Select);
557 }
558 "selectLine:" => {
559 driver.move_to_line_start();
560 driver.select_to_line_end();
561 return Some(GeneratedTextInputEvent::Select);
562 }
563 "selectParagraph:" => {
564 driver.move_to_hard_line_start();
565 driver.select_to_hard_line_end();
566 return Some(GeneratedTextInputEvent::Select);
567 }
568 "selectWord:" => {
569 }
571
572 "moveToBeginningOfDocument:" => {
574 driver.move_to_text_start();
575 return Some(GeneratedTextInputEvent::Select);
576 }
577 "moveToBeginningOfDocumentAndModifySelection:" => {
578 driver.select_to_text_start();
579 return Some(GeneratedTextInputEvent::Select);
580 }
581 "moveToEndOfDocument:" => {
582 driver.move_to_text_end();
583 return Some(GeneratedTextInputEvent::Select);
584 }
585 "moveToEndOfDocumentAndModifySelection:" => {
586 driver.move_to_text_end();
587 return Some(GeneratedTextInputEvent::Select);
588 }
589
590 "moveParagraphBackwardAndModifySelection:" => {}
592 "moveParagraphForwardAndModifySelection:" => {}
593 "moveToBeginningOfParagraph:" => {
594 driver.move_to_hard_line_start();
595 return Some(GeneratedTextInputEvent::Select);
596 }
597 "moveToBeginningOfParagraphAndModifySelection:" => {
598 driver.select_to_hard_line_start();
599 return Some(GeneratedTextInputEvent::Select);
600 }
601 "moveToEndOfParagraph:" => {
602 driver.move_to_hard_line_end();
603 return Some(GeneratedTextInputEvent::Select);
604 }
605 "moveToEndOfParagraphAndModifySelection:" => {
606 driver.select_to_hard_line_end();
607 return Some(GeneratedTextInputEvent::Select);
608 }
609
610 "moveToBeginningOfLine:" => {
612 driver.move_to_line_start();
613 return Some(GeneratedTextInputEvent::Select);
614 }
615 "moveToBeginningOfLineAndModifySelection:" => {
616 driver.select_to_line_start();
617 return Some(GeneratedTextInputEvent::Select);
618 }
619 "moveToEndOfLine:" => {
620 driver.move_to_line_end();
621 return Some(GeneratedTextInputEvent::Select);
622 }
623 "moveToEndOfLineAndModifySelection:" => {
624 driver.select_to_line_end();
625 return Some(GeneratedTextInputEvent::Select);
626 }
627 "moveToLeftEndOfLine:" => {
628 driver.move_to_text_start();
629 return Some(GeneratedTextInputEvent::Select);
630 }
631 "moveToLeftEndOfLineAndModifySelection:" => {
632 driver.select_to_line_start();
633 return Some(GeneratedTextInputEvent::Select);
634 }
635 "moveToRightEndOfLine:" => {
636 driver.move_to_line_end();
637 return Some(GeneratedTextInputEvent::Select);
638 }
639 "moveToRightEndOfLineAndModifySelection:" => {
640 driver.select_to_line_end();
641 return Some(GeneratedTextInputEvent::Select);
642 }
643
644 "moveWordBackward:" => {
646 driver.move_word_left();
647 return Some(GeneratedTextInputEvent::Select);
648 }
649 "moveWordBackwardAndModifySelection:" => {
650 driver.select_word_left();
651 return Some(GeneratedTextInputEvent::Select);
652 }
653 "moveWordForward:" => {
654 driver.move_word_right();
655 return Some(GeneratedTextInputEvent::Select);
656 }
657 "moveWordForwardAndModifySelection:" => {
658 driver.select_word_right();
659 return Some(GeneratedTextInputEvent::Select);
660 }
661 "moveWordLeft:" => {
662 driver.move_word_left();
663 return Some(GeneratedTextInputEvent::Select);
664 }
665 "moveWordLeftAndModifySelection:" => {
666 driver.select_word_left();
667 return Some(GeneratedTextInputEvent::Select);
668 }
669 "moveWordRight:" => {
670 driver.move_word_right();
671 return Some(GeneratedTextInputEvent::Select);
672 }
673 "moveWordRightAndModifySelection:" => {
674 driver.select_word_right();
675 return Some(GeneratedTextInputEvent::Select);
676 }
677
678 "scrollPageDown:" => {}
682 "scrollPageUp:" => {}
684 "scrollLineDown:" => {}
686 "scrollLineUp:" => {}
688 "scrollToBeginningOfDocument:" => {}
690 "scrollToEndOfDocument:" => {}
692 "pageDown:" => {}
694 "pageUp:" => {}
696 "pageDownAndModifySelection:" => {}
698 "pageUpAndModifySelection:" => {}
700 "centerSelectionInVisibleArea:" => {}
702
703 "transpose:" => {}
707 "transposeWords:" => {}
709
710 "indent:" => {}
713
714 "cancelOperation:" => {}
717
718 "quickLookPreviewItems:" => {}
721
722 "makeBaseWritingDirectionLeftToRight:" => {}
724 "makeBaseWritingDirectionNatural:" => {}
725 "makeBaseWritingDirectionRightToLeft:" => {}
726 "makeTextWritingDirectionLeftToRight:" => {}
727 "makeTextWritingDirectionNatural:" => {}
728 "makeTextWritingDirectionRightToLeft:" => {}
729
730 "capitalizeWord:" => {}
732 "changeCaseOfLetter:" => {}
733 "lowercaseWord:" => {}
734 "uppercaseWord:" => {}
735
736 "setMark:" => {}
738 "selectToMark:" => {}
739 "deleteToMark:" => {}
740 "swapWithMark:" => {}
741
742 "complete:" => {}
744
745 "showContextMenuForSelection:" => {}
747
748 _ => {}
750 };
751
752 None
753 }
754
755 pub(crate) fn apply_ime_event(
756 &mut self,
757 font_ctx: &mut FontContext,
758 layout_ctx: &mut LayoutContext<TextBrush>,
759 event: BlitzImeEvent,
760 ) -> Option<GeneratedTextInputEvent> {
761 let editor = &mut self.editor;
762 let mut driver = editor.driver(font_ctx, layout_ctx);
763
764 match event {
765 BlitzImeEvent::Enabled => {
766 None
768 }
769 BlitzImeEvent::Disabled => {
770 driver.clear_compose();
771 Some(GeneratedTextInputEvent::PreEditChange)
772 }
773 BlitzImeEvent::Commit(text) => {
774 driver.insert_or_replace_selection(&text);
775 Some(GeneratedTextInputEvent::Input)
776 }
777 BlitzImeEvent::Preedit(text, cursor) => {
778 if text.is_empty() {
779 driver.clear_compose();
780 } else {
781 driver.set_compose(&text, cursor);
782 }
783 Some(GeneratedTextInputEvent::PreEditChange)
784 }
785 BlitzImeEvent::DeleteSurrounding {
786 before_bytes,
787 after_bytes,
788 } => {
789 let _ = before_bytes;
790 let _ = after_bytes;
791 None
793 }
794 }
795 }
796}