Skip to main content

blitz_dom/node/
text.rs

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