Skip to main content

blitz_dom/node/
text.rs

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)]
11/// Parley Brush type for Blitz which contains the Blitz node id
12pub struct TextBrush {
13    /// The node id for the span
14    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
48// TODO: support keypress events
49pub enum GeneratedTextInputEvent {
50    Input,
51    Select,
52    PreEditChange,
53    Submit,
54}
55
56pub struct TextInputData {
57    /// A parley TextEditor instance
58    pub editor: Box<parley::PlainEditor<TextBrush>>,
59    /// Whether the input is a singleline or multiline input
60    pub is_multiline: bool,
61    /// The scroll offset of the text content within the input, in CSS (unscaled) pixels.
62    ///
63    /// For single-line inputs this is a horizontal offset; for multi-line inputs it is a
64    /// vertical offset. It is kept up to date so that the caret remains visible within the
65    /// input's content box.
66    pub scroll_offset: f32,
67}
68
69// FIXME: Implement Clone for PlainEditor
70impl 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    /// Recompute [`Self::scroll_offset`] so that the caret stays visible within the input's
99    /// content box.
100    ///
101    /// `content_box_width` and `content_box_height` are the dimensions of the input's content
102    /// box in CSS (unscaled) pixels.
103    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        // Parley lays out at the editor's scale, so its geometry is in scaled (device) pixels.
108        // We convert into CSS (unscaled) pixels to match `scroll_offset` and the content box.
109        let scale = layout.scale();
110
111        // The caret geometry relative to the start of the text content.
112        let Some(caret) = self.editor.cursor_geometry(1.5) else {
113            return;
114        };
115
116        // Caret bounds and content/viewport extents along the scrolling axis (CSS pixels).
117        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        // Scroll so that both edges of the caret are within the visible region.
136        if caret_end > offset + viewport {
137            offset = caret_end - viewport;
138        }
139        if caret_start < offset {
140            offset = caret_start;
141        }
142
143        // Never scroll past the content, and never scroll into negative space. The content
144        // extent includes the caret so that a caret at the very end remains fully visible
145        // (its rendered width extends slightly past the text).
146        let max_offset = (content.max(caret_end) - viewport).max(0.0);
147        self.scroll_offset = offset.clamp(0.0, max_offset);
148    }
149
150    /// The maximum valid value of [`Self::scroll_offset`] (in CSS pixels) given the input's
151    /// content box, i.e. the extent by which the text content overflows the content box along
152    /// the input's scroll axis.
153    ///
154    /// `content_box_width` and `content_box_height` are the dimensions of the input's content
155    /// box in CSS (unscaled) pixels.
156    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    /// Scroll the input's text content by `delta` CSS pixels along its scroll axis (horizontal
170    /// for single-line inputs, vertical for multi-line inputs), clamping to the scrollable
171    /// range.
172    ///
173    /// Returns the portion of `delta` that could not be consumed (because the input was already
174    /// scrolled to its limit), so the caller can bubble it up to an ancestor scroller.
175    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        // Match the sign convention used for block scrolling: a positive delta decreases the
187        // scroll offset.
188        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        // Do nothing if it is a keyup event
202        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            // On macOS this is handled by the apple standard keybindings
326            #[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            // Inserting Content
377
378            // Inserts a backtab character.
379            "insertBacktab:" => {}
380            // Inserts a container break, such as a new page break.
381            "insertContainerBreak:" => {}
382            // Inserts a double quotation mark without substituting a curly quotation mark.
383            "insertDoubleQuoteIgnoringSubstitution:" => {
384                driver.insert_or_replace_selection("\"");
385                return Some(GeneratedTextInputEvent::Input);
386            }
387            // Inserts a line break character.
388            "insertLineBreak:" => {
389                driver.insert_or_replace_selection("\n");
390                return Some(GeneratedTextInputEvent::Input);
391            }
392            // Inserts a newline character.
393            "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            // Inserts a newline character without invoking the field editor’s normal handling to end editing.
402            "insertNewlineIgnoringFieldEditor:" => {
403                driver.insert_or_replace_selection("\n");
404                return Some(GeneratedTextInputEvent::Input);
405            }
406            // Inserts a paragraph separator.
407            "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            // Inserts a tab character.
416            "insertTab:" | "insertTabIgnoringFieldEditor:" => {
417                // Ignore for now seeing as parley has poor support for laying out tabs
418            }
419            // Inserts the text you specify.
420            "insertText:" => {}
421
422            // Deleting Content
423
424            // Deletes content moving backward from the current insertion point.
425            // TODO: handle deleteBackwardByDecomposingPreviousCharacter separately
426            "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
427                driver.backdelete();
428                return Some(GeneratedTextInputEvent::Input);
429            }
430            "deleteForward:" => {
431                driver.delete();
432                return Some(GeneratedTextInputEvent::Input);
433            }
434            // Deletes content from the insertion point to the beginning of the current line.
435            "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            // Deletes content from the insertion point to the beginning of the current paragraph.
443            "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            // Deletes content from the insertion point to the end of the current line.
459            "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            // Deletes content from the insertion point to the end of the current paragraph.
467            "deleteWordBackward:" => {
468                driver.backdelete_word();
469                return Some(GeneratedTextInputEvent::Input);
470            }
471            // Deletes the word preceding the current insertion point.
472            "deleteWordForward:" => {
473                driver.delete_word();
474                return Some(GeneratedTextInputEvent::Input);
475            }
476            // Deletes the current selection, placing it in a temporary buffer, such as the Clipboard.
477            "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            // Moving the Insertion Pointer
486
487            // Moves the insertion pointer backward in the current content.
488            "moveBackward:" => {
489                driver.move_left(); // TODO: Bidi-aware
490                return Some(GeneratedTextInputEvent::Select);
491            }
492
493            // Moves the insertion pointer down in the current content.
494            "moveDown:" => {
495                driver.move_down();
496                return Some(GeneratedTextInputEvent::Select);
497            }
498            // Moves the insertion pointer forward in the current content.
499            "moveForward:" => {
500                driver.move_right();
501                return Some(GeneratedTextInputEvent::Select);
502            } // TODO: Bidi-aware
503
504            // Moves the insertion pointer left in the current content.
505            "moveLeft:" => {
506                driver.move_left();
507                return Some(GeneratedTextInputEvent::Select);
508            }
509            // Moves the insertion pointer right in the current content.
510            "moveRight:" => {
511                driver.move_right();
512                return Some(GeneratedTextInputEvent::Select);
513            }
514            // Moves the insertion pointer up in the current content.
515            "moveUp:" => {
516                driver.move_up();
517                return Some(GeneratedTextInputEvent::Select);
518            }
519
520            // Modifying the Selection
521
522            // Extends the selection to include the content before the current selection.
523            "moveBackwardAndModifySelection:" => {
524                driver.select_left(); // TODO: Bidi-aware
525                return Some(GeneratedTextInputEvent::Select);
526            }
527            // Extends the selection to include the content below the current selection.
528            "moveDownAndModifySelection:" => {
529                driver.select_down();
530                return Some(GeneratedTextInputEvent::Select);
531            }
532            // Extends the selection to include the content after the current selection.
533            "moveForwardAndModifySelection:" => {
534                driver.select_right(); // TODO: Bidi-aware
535                return Some(GeneratedTextInputEvent::Select);
536            }
537            // Extends the selection to include the content to the left of the current selection.
538            "moveLeftAndModifySelection:" => {
539                driver.select_left();
540                return Some(GeneratedTextInputEvent::Select);
541            }
542            // Extends the selection to include the content to the right of the current selection.
543            "moveRightAndModifySelection:" => {
544                driver.select_right();
545                return Some(GeneratedTextInputEvent::Select);
546            }
547            // Extends the selection to include the content above the current selection.
548            "moveUpAndModifySelection:" => {
549                driver.select_up();
550                return Some(GeneratedTextInputEvent::Select);
551            }
552
553            // Changing the Selection
554            "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                // TODO
570            }
571
572            // Moving the Selection in Documents
573            "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            // Moving the Selection in Paragraphs
591            "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            // Moving the Selection in Lines of Text
611            "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            // Moving the Selection by Word Boundaries
645            "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            // Scrolling Content
679
680            // Scrolls the content down by a page.
681            "scrollPageDown:" => {}
682            // Scrolls the content up by a page.
683            "scrollPageUp:" => {}
684            // Scrolls the content down by a line.
685            "scrollLineDown:" => {}
686            // Scrolls the content up by a line.
687            "scrollLineUp:" => {}
688            // Scrolls the content to the beginning of the document.
689            "scrollToBeginningOfDocument:" => {}
690            // Scrolls the content to the end of the document.
691            "scrollToEndOfDocument:" => {}
692            // Moves the visible content region down by a page.
693            "pageDown:" => {}
694            // Moves the visible content region up by a page.
695            "pageUp:" => {}
696            // Moves the visible content region down by a page, and extends the current selection.
697            "pageDownAndModifySelection:" => {}
698            // Moves the visible content region up by a page, and extends the current selection.
699            "pageUpAndModifySelection:" => {}
700            // Moves the visible content region so the current selection is visually centered.
701            "centerSelectionInVisibleArea:" => {}
702
703            // Transposing Elements
704
705            // Transposes the content around the current selection.
706            "transpose:" => {}
707            // Transposes the words around the current selection.
708            "transposeWords:" => {}
709
710            // Indenting Content
711            // Indents the content at the current selection.
712            "indent:" => {}
713
714            // Canceling Operations
715            // Cancels the current operation.
716            "cancelOperation:" => {}
717
718            // Supporting QuickLook
719            // Invokes QuickLook to preview the current selection.
720            "quickLookPreviewItems:" => {}
721
722            // Supporting Writing Directions
723            "makeBaseWritingDirectionLeftToRight:" => {}
724            "makeBaseWritingDirectionNatural:" => {}
725            "makeBaseWritingDirectionRightToLeft:" => {}
726            "makeTextWritingDirectionLeftToRight:" => {}
727            "makeTextWritingDirectionNatural:" => {}
728            "makeTextWritingDirectionRightToLeft:" => {}
729
730            // Changing Capitalization
731            "capitalizeWord:" => {}
732            "changeCaseOfLetter:" => {}
733            "lowercaseWord:" => {}
734            "uppercaseWord:" => {}
735
736            // Supporting Marked Selections
737            "setMark:" => {}
738            "selectToMark:" => {}
739            "deleteToMark:" => {}
740            "swapWithMark:" => {}
741
742            // Supporting Autocomplete
743            "complete:" => {}
744
745            // Instance Methods
746            "showContextMenuForSelection:" => {}
747
748            // Unknown command
749            _ => {}
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                // Do nothing
767                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                // TODO
792                None
793            }
794        }
795    }
796}