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