Skip to main content

apace_dom/node/
text.rs

1use apace_traits::{
2    events::{ApaceImeEvent, ApaceKeyEvent},
3    node_id::NodeId,
4    shell::ShellProvider,
5};
6use keyboard_types::{Key, Modifiers, NamedKey};
7use parley::{ContentWidths, FontContext, LayoutContext};
8
9use super::password::Password;
10use super::placeholder::Placeholder;
11use crate::util::ACTION_MOD;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq)]
14/// Parley Brush type for Apace which contains the Apace node id
15pub struct TextBrush {
16    /// The node id for the span
17    pub id: NodeId,
18}
19
20impl TextBrush {
21    pub(crate) fn from_id(id: NodeId) -> Self {
22        Self { id }
23    }
24}
25
26#[derive(Clone, Default)]
27pub struct TextLayout {
28    pub text: String,
29    pub content_widths: Option<ContentWidths>,
30    pub layout: parley::layout::Layout<TextBrush>,
31    /// Block-axis offset (in CSS px) of the line boxes from the top of the container's content
32    /// box, as applied by `align-content`.
33    pub block_offset: f32,
34    /// The out-of-flow positions for which some inline span with an inline box below it is a
35    /// containing block (as `SPAN_*_CB` flags). The inline root claims those boxes in its place.
36    /// Set by each `RunMode::PerformLayout` run of inline layout.
37    pub span_cb_flags: stylo_takia::StyleFlags,
38}
39
40impl TextLayout {
41    pub fn new() -> Self {
42        Default::default()
43    }
44
45    pub fn content_widths(&mut self) -> ContentWidths {
46        *self
47            .content_widths
48            .get_or_insert_with(|| self.layout.calculate_content_widths())
49    }
50}
51
52impl std::fmt::Debug for TextLayout {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "TextLayout")
55    }
56}
57
58// TODO: support keypress events
59pub enum GeneratedTextInputEvent {
60    Input,
61    Select,
62    PreEditChange,
63    Submit,
64}
65
66pub struct TextInputData {
67    /// A parley TextEditor instance
68    pub editor: Box<parley::PlainEditor<TextBrush>>,
69    password: Option<Password>,
70    placeholder: Option<Placeholder>,
71    placeholder_width: Option<f32>,
72    width: Option<f32>,
73    alignment: parley::layout::Alignment,
74    /// Whether the input is a singleline or multiline input
75    pub is_multiline: bool,
76    /// The scroll offset of the text content within the input, in CSS (unscaled) pixels.
77    ///
78    /// For single-line inputs this is a horizontal offset; for multi-line inputs it is a
79    /// vertical offset. It is kept up to date so that the caret remains visible within the
80    /// input's content box.
81    pub scroll_offset: f32,
82}
83
84// FIXME: Implement Clone for PlainEditor
85impl Clone for TextInputData {
86    fn clone(&self) -> Self {
87        let mut data = TextInputData::new(self.is_multiline);
88        data.set_password(self.is_password());
89        data.set_placeholder(self.placeholder.as_ref().map(|hint| hint.text.as_str()));
90        data
91    }
92}
93
94impl TextInputData {
95    /// Export only the focused editor's bounded committed excerpt. Passwords
96    /// export an empty context; native marked text is never overwritten by an
97    /// acknowledgement of the committed application value.
98    pub(crate) fn sync_ime_context(&self, provider: &dyn ShellProvider) {
99        if !provider.supports_ime_surrounding_text() || self.editor.is_composing() {
100            return;
101        }
102        let text = self.editor.raw_text();
103        let selection = self.editor.raw_selection();
104        let anchor = selection.anchor().index();
105        let cursor = selection.focus().index();
106        let low = anchor.min(cursor);
107        let high = anchor.max(cursor);
108        const BUDGET: usize = 3999;
109        if self.is_password() || high - low > BUDGET {
110            provider.set_ime_surrounding_text("", 0, 0);
111            return;
112        }
113        let mut start = low.saturating_sub((BUDGET - (high - low)) / 2);
114        while !text.is_char_boundary(start) {
115            start += 1;
116        }
117        let mut end = text.len().min(start + BUDGET);
118        while !text.is_char_boundary(end) {
119            end -= 1;
120        }
121        provider.set_ime_surrounding_text(&text[start..end], cursor - start, anchor - start);
122    }
123    pub fn new(is_multiline: bool) -> Self {
124        let editor = Box::new(parley::PlainEditor::new(16.0));
125        Self {
126            editor,
127            password: None,
128            placeholder: None,
129            placeholder_width: None,
130            width: None,
131            alignment: parley::layout::Alignment::Start,
132            is_multiline,
133            scroll_offset: 0.0,
134        }
135    }
136
137    pub fn set_text(
138        &mut self,
139        font_ctx: &mut FontContext,
140        layout_ctx: &mut LayoutContext<TextBrush>,
141        text: &str,
142    ) {
143        if self.editor.text() != text {
144            self.editor.set_text(text);
145            self.editor.driver(font_ctx, layout_ctx).refresh_layout();
146            self.refresh_display(font_ctx, layout_ctx);
147        }
148    }
149
150    pub fn is_password(&self) -> bool {
151        self.password.is_some()
152    }
153
154    pub(crate) fn set_placeholder(&mut self, text: Option<&str>) {
155        let text = text.filter(|text| text.len() <= 4096);
156        if self.placeholder.as_ref().map(|hint| hint.text.as_str()) == text {
157            return;
158        }
159        self.placeholder = text.map(Placeholder::new);
160    }
161    pub fn placeholder_shown(&self) -> bool {
162        self.placeholder.is_some()
163            && self.editor.raw_text().is_empty()
164            && !self.editor.is_composing()
165    }
166    /// Painting only: selection, IME, copying and geometry always use the real editor.
167    pub fn placeholder_layout(&self) -> Option<&parley::Layout<TextBrush>> {
168        if !self.placeholder_shown() {
169            return None;
170        }
171        self.placeholder.as_ref()?.editor.as_ref()?.try_layout()
172    }
173    pub(crate) fn set_placeholder_width(&mut self, width: Option<f32>) {
174        if self.placeholder_width == width {
175            return;
176        }
177        self.placeholder_width = width;
178        if let Some(hint) = &mut self.placeholder {
179            hint.dirty = true;
180        }
181    }
182
183    pub(crate) fn set_password(&mut self, value: bool) {
184        if value && self.password.is_none() {
185            let mut password = Password::new(self.editor.get_font_size());
186            password.editor.set_width(self.width);
187            password.editor.set_alignment(self.alignment);
188            self.password = Some(password);
189        } else if !value {
190            self.password = None;
191        }
192    }
193
194    pub(crate) fn set_width(&mut self, width: Option<f32>) {
195        self.set_placeholder_width(width);
196        self.width = width;
197        self.editor.set_width(width);
198        if let Some(password) = &mut self.password {
199            password.editor.set_width(width);
200        }
201    }
202
203    pub(crate) fn set_alignment(&mut self, alignment: parley::layout::Alignment) {
204        if self.alignment != alignment
205            && let Some(hint) = &mut self.placeholder
206        {
207            hint.dirty = true;
208        }
209        self.alignment = alignment;
210        self.editor.set_alignment(alignment);
211        if let Some(password) = &mut self.password {
212            password.editor.set_alignment(alignment);
213        }
214    }
215
216    /// The only layout used for visual geometry. Editable bytes/IME offsets
217    /// deliberately remain available through the raw editor above.
218    pub fn display_editor(&self) -> &parley::PlainEditor<TextBrush> {
219        self.password
220            .as_ref()
221            .map_or(&self.editor, |password| &password.editor)
222    }
223
224    pub fn cursor_geometry(&self, size: f32) -> Option<parley::BoundingBox> {
225        self.editor
226            .is_cursor_visible()
227            .then(|| {
228                let display = self.display_editor();
229                display
230                    .try_layout()
231                    .map(|layout| display.raw_selection().focus().geometry(layout, size))
232            })
233            .flatten()
234    }
235
236    pub fn ime_cursor_area(&self) -> parley::BoundingBox {
237        let display = self.display_editor();
238        if let (Some(password), Some(compose), Some(layout)) = (
239            &self.password,
240            self.editor.raw_compose(),
241            display.try_layout(),
242        ) {
243            use parley::editing::{Cursor, Selection};
244            use parley::layout::Affinity;
245            let selection = Selection::new(
246                Cursor::from_byte_index(
247                    layout,
248                    password.display_index(compose.start, false),
249                    Affinity::Downstream,
250                ),
251                Cursor::from_byte_index(
252                    layout,
253                    password.display_index(compose.end, true),
254                    Affinity::Upstream,
255                ),
256            );
257            let mut bounds = display.ime_cursor_area();
258            for (rect, _) in selection.geometry(layout) {
259                bounds.x0 = bounds.x0.min(rect.x0);
260                bounds.y0 = bounds.y0.min(rect.y0);
261                bounds.x1 = bounds.x1.max(rect.x1);
262                bounds.y1 = bounds.y1.max(rect.y1);
263            }
264            bounds
265        } else {
266            display.ime_cursor_area()
267        }
268    }
269
270    pub(crate) fn refresh_display(
271        &mut self,
272        fonts: &mut FontContext,
273        layouts: &mut LayoutContext<TextBrush>,
274    ) {
275        if let Some(password) = &mut self.password {
276            password.sync(&self.editor, fonts, layouts);
277        }
278        if self.editor.raw_text().is_empty() {
279            self.scroll_offset = 0.;
280        }
281        if let Some(hint) = &mut self.placeholder {
282            hint.sync(
283                &self.editor,
284                self.placeholder_width,
285                self.alignment,
286                self.is_multiline,
287                fonts,
288                layouts,
289            );
290        }
291    }
292
293    pub(crate) fn refresh_display_config(
294        &mut self,
295        fonts: &mut FontContext,
296        layouts: &mut LayoutContext<TextBrush>,
297    ) {
298        if let Some(password) = &mut self.password {
299            password.invalidate_config();
300        }
301        if let Some(hint) = &mut self.placeholder {
302            hint.dirty = true;
303        }
304        self.refresh_display(fonts, layouts);
305    }
306
307    /// Perform selection in display coordinates, then map the resulting UTF-8
308    /// bullet indices back to raw extended-grapheme boundaries.
309    pub(crate) fn point_selection(
310        &mut self,
311        fonts: &mut FontContext,
312        layouts: &mut LayoutContext<TextBrush>,
313        apply: impl FnOnce(&mut parley::PlainEditorDriver<TextBrush>),
314    ) {
315        self.refresh_display(fonts, layouts);
316        if let Some(password) = &mut self.password {
317            apply(&mut password.editor.driver(fonts, layouts));
318            let selection = password.editor.raw_selection();
319            let anchor = password.raw_index(selection.anchor().index());
320            let focus = password.raw_index(selection.focus().index());
321            self.editor
322                .driver(fonts, layouts)
323                .select_byte_range(anchor, focus);
324            password.sync_selection(&self.editor, fonts, layouts);
325        } else {
326            apply(&mut self.editor.driver(fonts, layouts));
327        }
328    }
329
330    fn password_delete(
331        &mut self,
332        fonts: &mut FontContext,
333        layouts: &mut LayoutContext<TextBrush>,
334        backwards: bool,
335        whole: bool,
336    ) {
337        self.refresh_display(fonts, layouts);
338        let range =
339            self.password
340                .as_ref()
341                .expect("password")
342                .deletion(&self.editor, backwards, whole);
343        let mut driver = self.editor.driver(fonts, layouts);
344        driver.select_byte_range(range.start, range.end);
345        driver.delete_selection();
346        self.refresh_display(fonts, layouts);
347    }
348
349    /// Recompute [`Self::scroll_offset`] so that the caret stays visible within the input's
350    /// content box.
351    ///
352    /// `content_box_width` and `content_box_height` are the dimensions of the input's content
353    /// box in CSS (unscaled) pixels.
354    pub fn clamp_scroll_offset(&mut self, content_box_width: f32, content_box_height: f32) {
355        let Some(layout) = self.display_editor().try_layout() else {
356            return;
357        };
358        // Parley lays out at the editor's scale, so its geometry is in scaled (device) pixels.
359        // We convert into CSS (unscaled) pixels to match `scroll_offset` and the content box.
360        let scale = layout.scale();
361
362        // The caret geometry relative to the start of the text content.
363        let Some(caret) = self.cursor_geometry(1.5) else {
364            return;
365        };
366
367        // Caret bounds and content/viewport extents along the scrolling axis (CSS pixels).
368        let (caret_start, caret_end, content, viewport) = if self.is_multiline {
369            (
370                caret.y0 as f32 / scale,
371                caret.y1 as f32 / scale,
372                layout.height() / scale,
373                content_box_height,
374            )
375        } else {
376            (
377                caret.x0 as f32 / scale,
378                caret.x1 as f32 / scale,
379                layout.full_width() / scale,
380                content_box_width,
381            )
382        };
383
384        let mut offset = self.scroll_offset;
385
386        // Scroll so that both edges of the caret are within the visible region.
387        if caret_end > offset + viewport {
388            offset = caret_end - viewport;
389        }
390        if caret_start < offset {
391            offset = caret_start;
392        }
393
394        // Never scroll past the content, and never scroll into negative space. The content
395        // extent includes the caret so that a caret at the very end remains fully visible
396        // (its rendered width extends slightly past the text).
397        let max_offset = (content.max(caret_end) - viewport).max(0.0);
398        self.scroll_offset = offset.clamp(0.0, max_offset);
399    }
400
401    /// The maximum valid value of [`Self::scroll_offset`] (in CSS pixels) given the input's
402    /// content box, i.e. the extent by which the text content overflows the content box along
403    /// the input's scroll axis.
404    ///
405    /// `content_box_width` and `content_box_height` are the dimensions of the input's content
406    /// box in CSS (unscaled) pixels.
407    pub fn max_scroll_offset(&self, content_box_width: f32, content_box_height: f32) -> f32 {
408        let Some(layout) = self.display_editor().try_layout() else {
409            return 0.0;
410        };
411        let scale = layout.scale();
412        let (content, viewport) = if self.is_multiline {
413            (layout.height() / scale, content_box_height)
414        } else {
415            (layout.full_width() / scale, content_box_width)
416        };
417        (content - viewport).max(0.0)
418    }
419
420    /// Scroll the input's text content by `delta` CSS pixels along its scroll axis (horizontal
421    /// for single-line inputs, vertical for multi-line inputs), clamping to the scrollable
422    /// range.
423    ///
424    /// Returns the portion of `delta` that could not be consumed (because the input was already
425    /// scrolled to its limit), so the caller can bubble it up to an ancestor scroller.
426    pub fn scroll_by(
427        &mut self,
428        delta: f32,
429        content_box_width: f32,
430        content_box_height: f32,
431    ) -> f32 {
432        let max_offset = self.max_scroll_offset(content_box_width, content_box_height);
433        if max_offset <= 0.0 {
434            return delta;
435        }
436
437        // Match the sign convention used for block scrolling: a positive delta decreases the
438        // scroll offset.
439        let new_offset = (self.scroll_offset - delta).clamp(0.0, max_offset);
440        let consumed = self.scroll_offset - new_offset;
441        self.scroll_offset = new_offset;
442        delta - consumed
443    }
444
445    pub(crate) fn apply_keypress_event(
446        &mut self,
447        font_ctx: &mut FontContext,
448        layout_ctx: &mut LayoutContext<TextBrush>,
449        shell_provider: &dyn ShellProvider,
450        mut event: ApaceKeyEvent,
451    ) -> Option<GeneratedTextInputEvent> {
452        if self.is_password() && event.state.is_pressed() {
453            let action = event.modifiers.contains(ACTION_MOD);
454            if action
455                && matches!(&event.key, Key::Character(c) if c.eq_ignore_ascii_case("c") || c.eq_ignore_ascii_case("x"))
456            {
457                return None;
458            }
459            if matches!(
460                event.key,
461                Key::Named(NamedKey::Delete | NamedKey::Backspace)
462            ) {
463                self.password_delete(
464                    font_ctx,
465                    layout_ctx,
466                    event.key == Key::Named(NamedKey::Backspace),
467                    action,
468                );
469                return Some(GeneratedTextInputEvent::Input);
470            }
471            let navigation = matches!(
472                event.key,
473                Key::Named(
474                    NamedKey::ArrowLeft
475                        | NamedKey::ArrowRight
476                        | NamedKey::ArrowUp
477                        | NamedKey::ArrowDown
478                        | NamedKey::Home
479                        | NamedKey::End
480                )
481            ) || action
482                && matches!(&event.key, Key::Character(c) if c.eq_ignore_ascii_case("a"));
483            if navigation {
484                if action && event.key == Key::Named(NamedKey::ArrowLeft) {
485                    event.key = Key::Named(NamedKey::Home);
486                } else if action && event.key == Key::Named(NamedKey::ArrowRight) {
487                    event.key = Key::Named(NamedKey::End);
488                }
489                self.refresh_display(font_ctx, layout_ctx);
490                let mut password = self.password.take().expect("password");
491                std::mem::swap(&mut self.editor, &mut password.editor);
492                let result =
493                    self.apply_plain_keypress_event(font_ctx, layout_ctx, shell_provider, event);
494                std::mem::swap(&mut self.editor, &mut password.editor);
495                let selection = password.editor.raw_selection();
496                let a = password.raw_index(selection.anchor().index());
497                let f = password.raw_index(selection.focus().index());
498                self.editor
499                    .driver(font_ctx, layout_ctx)
500                    .select_byte_range(a, f);
501                password.sync_selection(&self.editor, font_ctx, layout_ctx);
502                self.password = Some(password);
503                return result;
504            }
505        }
506        let result = self.apply_plain_keypress_event(font_ctx, layout_ctx, shell_provider, event);
507        self.refresh_display(font_ctx, layout_ctx);
508        result
509    }
510
511    fn apply_plain_keypress_event(
512        &mut self,
513        font_ctx: &mut FontContext,
514        layout_ctx: &mut LayoutContext<TextBrush>,
515        shell_provider: &dyn ShellProvider,
516        event: ApaceKeyEvent,
517    ) -> Option<GeneratedTextInputEvent> {
518        // Do nothing if it is a keyup event
519        if !event.state.is_pressed() {
520            return None;
521        }
522
523        let mods = event.modifiers;
524        let shift = mods.contains(Modifiers::SHIFT);
525        let action_mod = mods.contains(ACTION_MOD);
526
527        let is_multiline = self.is_multiline;
528        let editor = &mut self.editor;
529        let mut driver = editor.driver(font_ctx, layout_ctx);
530        match event.key {
531            Key::Character(c) if action_mod && matches!(c.as_str(), "c" | "x" | "v") => {
532                match c.to_lowercase().as_str() {
533                    "c" => {
534                        if let Some(text) = driver.editor.selected_text() {
535                            let _ = shell_provider.set_clipboard_text(text.to_owned());
536                        }
537                    }
538                    "x" => {
539                        if let Some(text) = driver.editor.selected_text() {
540                            let _ = shell_provider.set_clipboard_text(text.to_owned());
541                            driver.delete_selection()
542                        }
543                    }
544                    "v" => {
545                        let text = shell_provider.get_clipboard_text().unwrap_or_default();
546                        driver.insert_or_replace_selection(&text)
547                    }
548                    _ => unreachable!(),
549                }
550
551                return Some(GeneratedTextInputEvent::Input);
552            }
553            Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
554                if shift {
555                    driver.collapse_selection()
556                } else {
557                    driver.select_all()
558                }
559                return Some(GeneratedTextInputEvent::Select);
560            }
561            Key::Named(NamedKey::ArrowLeft) => {
562                if action_mod {
563                    if shift {
564                        driver.select_word_left()
565                    } else {
566                        driver.move_word_left()
567                    }
568                } else if shift {
569                    driver.select_left()
570                } else {
571                    driver.move_left()
572                }
573                return Some(GeneratedTextInputEvent::Select);
574            }
575            Key::Named(NamedKey::ArrowRight) => {
576                if action_mod {
577                    if shift {
578                        driver.select_word_right()
579                    } else {
580                        driver.move_word_right()
581                    }
582                } else if shift {
583                    driver.select_right()
584                } else {
585                    driver.move_right()
586                }
587                return Some(GeneratedTextInputEvent::Select);
588            }
589            Key::Named(NamedKey::ArrowUp) => {
590                if shift {
591                    driver.select_up()
592                } else {
593                    driver.move_up()
594                }
595                return Some(GeneratedTextInputEvent::Select);
596            }
597            Key::Named(NamedKey::ArrowDown) => {
598                if shift {
599                    driver.select_down()
600                } else {
601                    driver.move_down()
602                }
603                return Some(GeneratedTextInputEvent::Select);
604            }
605            Key::Named(NamedKey::Home) => {
606                if action_mod {
607                    if shift {
608                        driver.select_to_text_start()
609                    } else {
610                        driver.move_to_text_start()
611                    }
612                } else if shift {
613                    driver.select_to_line_start()
614                } else {
615                    driver.move_to_line_start()
616                }
617                return Some(GeneratedTextInputEvent::Select);
618            }
619            Key::Named(NamedKey::End) => {
620                if action_mod {
621                    if shift {
622                        driver.select_to_text_end()
623                    } else {
624                        driver.move_to_text_end()
625                    }
626                } else if shift {
627                    driver.select_to_line_end()
628                } else {
629                    driver.move_to_line_end()
630                }
631                return Some(GeneratedTextInputEvent::Select);
632            }
633            Key::Named(NamedKey::Delete) => {
634                if action_mod {
635                    driver.delete_word()
636                } else {
637                    driver.delete()
638                }
639                return Some(GeneratedTextInputEvent::Input);
640            }
641
642            // On macOS this is handled by the apple standard keybindings
643            #[cfg(not(target_os = "macos"))]
644            Key::Named(NamedKey::Backspace) => {
645                if action_mod {
646                    driver.backdelete_word()
647                } else {
648                    driver.backdelete()
649                }
650                return Some(GeneratedTextInputEvent::Input);
651            }
652
653            Key::Character(c) if c == "\n" => {
654                if is_multiline {
655                    driver.insert_or_replace_selection("\n");
656                    return Some(GeneratedTextInputEvent::Input);
657                } else {
658                    return Some(GeneratedTextInputEvent::Submit);
659                }
660            }
661            Key::Named(NamedKey::Enter) => {
662                if is_multiline {
663                    driver.insert_or_replace_selection("\n");
664                    return Some(GeneratedTextInputEvent::Input);
665                } else {
666                    return Some(GeneratedTextInputEvent::Submit);
667                }
668            }
669            Key::Character(s)
670                if !mods.contains(Modifiers::CONTROL) && !mods.contains(Modifiers::META) =>
671            {
672                driver.insert_or_replace_selection(&s);
673                return Some(GeneratedTextInputEvent::Input);
674            }
675            _ => {}
676        };
677
678        None
679    }
680
681    pub(crate) fn apply_apple_standard_keybinding(
682        &mut self,
683        font_ctx: &mut FontContext,
684        layout_ctx: &mut LayoutContext<TextBrush>,
685        shell_provider: &dyn ShellProvider,
686        command: &str,
687    ) -> Option<GeneratedTextInputEvent> {
688        if self.is_password() {
689            if matches!(command, "copy:" | "cut:") {
690                return None;
691            }
692            let deletion = match command {
693                "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
694                    Some((true, false))
695                }
696                "deleteForward:" => Some((false, false)),
697                "deleteWordBackward:"
698                | "deleteToBeginningOfLine:"
699                | "deleteToBeginningOfParagraph:" => Some((true, true)),
700                "deleteWordForward:" | "deleteToEndOfLine:" | "deleteToEndOfParagraph:" => {
701                    Some((false, true))
702                }
703                _ => None,
704            };
705            if let Some((backwards, whole)) = deletion {
706                self.password_delete(font_ctx, layout_ctx, backwards, whole);
707                return Some(GeneratedTextInputEvent::Input);
708            }
709            if command.starts_with("move") || command.starts_with("select") {
710                if command.contains("Word") {
711                    self.point_selection(font_ctx, layout_ctx, |driver| {
712                        if command.starts_with("select") {
713                            driver.select_all();
714                        } else if command.contains("Backward") || command.contains("Left") {
715                            if command.contains("ModifySelection") {
716                                driver.select_to_text_start();
717                            } else {
718                                driver.move_to_text_start();
719                            }
720                        } else if command.contains("ModifySelection") {
721                            driver.select_to_text_end();
722                        } else {
723                            driver.move_to_text_end();
724                        }
725                    });
726                    return Some(GeneratedTextInputEvent::Select);
727                }
728                self.refresh_display(font_ctx, layout_ctx);
729                let mut password = self.password.take().expect("password");
730                std::mem::swap(&mut self.editor, &mut password.editor);
731                let result = self.apply_plain_apple_standard_keybinding(
732                    font_ctx,
733                    layout_ctx,
734                    shell_provider,
735                    command,
736                );
737                std::mem::swap(&mut self.editor, &mut password.editor);
738                let selection = password.editor.raw_selection();
739                let a = password.raw_index(selection.anchor().index());
740                let f = password.raw_index(selection.focus().index());
741                self.editor
742                    .driver(font_ctx, layout_ctx)
743                    .select_byte_range(a, f);
744                password.sync_selection(&self.editor, font_ctx, layout_ctx);
745                self.password = Some(password);
746                return result;
747            }
748        }
749        let result = self.apply_plain_apple_standard_keybinding(
750            font_ctx,
751            layout_ctx,
752            shell_provider,
753            command,
754        );
755        self.refresh_display(font_ctx, layout_ctx);
756        result
757    }
758
759    fn apply_plain_apple_standard_keybinding(
760        &mut self,
761        font_ctx: &mut FontContext,
762        layout_ctx: &mut LayoutContext<TextBrush>,
763        shell_provider: &dyn ShellProvider,
764        command: &str,
765    ) -> Option<GeneratedTextInputEvent> {
766        let editor = &mut self.editor;
767        let mut driver = editor.driver(font_ctx, layout_ctx);
768        let is_multiline = self.is_multiline;
769
770        match command {
771            // Inserting Content
772
773            // Inserts a backtab character.
774            "insertBacktab:" => {}
775            // Inserts a container break, such as a new page break.
776            "insertContainerBreak:" => {}
777            // Inserts a double quotation mark without substituting a curly quotation mark.
778            "insertDoubleQuoteIgnoringSubstitution:" => {
779                driver.insert_or_replace_selection("\"");
780                return Some(GeneratedTextInputEvent::Input);
781            }
782            // Inserts a line break character.
783            "insertLineBreak:" => {
784                driver.insert_or_replace_selection("\n");
785                return Some(GeneratedTextInputEvent::Input);
786            }
787            // Inserts a newline character.
788            "insertNewline:" => {
789                if is_multiline {
790                    driver.insert_or_replace_selection("\n");
791                    return Some(GeneratedTextInputEvent::Input);
792                } else {
793                    return Some(GeneratedTextInputEvent::Submit);
794                }
795            }
796            // Inserts a newline character without invoking the field editor’s normal handling to end editing.
797            "insertNewlineIgnoringFieldEditor:" => {
798                driver.insert_or_replace_selection("\n");
799                return Some(GeneratedTextInputEvent::Input);
800            }
801            // Inserts a paragraph separator.
802            "insertParagraphSeparator:" => {
803                driver.insert_or_replace_selection("\n");
804                return Some(GeneratedTextInputEvent::Input);
805            }
806            "insertSingleQuoteIgnoringSubstitution:" => {
807                driver.insert_or_replace_selection("'");
808                return Some(GeneratedTextInputEvent::Input);
809            }
810            // Inserts a tab character.
811            "insertTab:" | "insertTabIgnoringFieldEditor:" => {
812                // Ignore for now seeing as parley has poor support for laying out tabs
813            }
814            // Inserts the text you specify.
815            "insertText:" => {}
816
817            // Deleting Content
818
819            // Deletes content moving backward from the current insertion point.
820            // TODO: handle deleteBackwardByDecomposingPreviousCharacter separately
821            "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
822                driver.backdelete();
823                return Some(GeneratedTextInputEvent::Input);
824            }
825            "deleteForward:" => {
826                driver.delete();
827                return Some(GeneratedTextInputEvent::Input);
828            }
829            // Deletes content from the insertion point to the beginning of the current line.
830            "deleteToBeginningOfLine:" => {
831                if driver.editor.raw_selection().is_collapsed() {
832                    driver.select_to_line_start();
833                }
834                driver.delete_selection();
835                return Some(GeneratedTextInputEvent::Input);
836            }
837            // Deletes content from the insertion point to the beginning of the current paragraph.
838            "deleteToEndOfLine:" => {
839                if driver.editor.raw_selection().is_collapsed() {
840                    driver.select_to_line_end();
841                }
842                driver.delete_selection();
843                return Some(GeneratedTextInputEvent::Input);
844            }
845            "deleteToBeginningOfParagraph:" => {
846                if driver.editor.raw_selection().is_collapsed() {
847                    driver.select_to_hard_line_start();
848                }
849                driver.delete_selection();
850                return Some(GeneratedTextInputEvent::Input);
851            }
852
853            // Deletes content from the insertion point to the end of the current line.
854            "deleteToEndOfParagraph:" => {
855                if driver.editor.raw_selection().is_collapsed() {
856                    driver.select_to_hard_line_end();
857                }
858                driver.delete_selection();
859                return Some(GeneratedTextInputEvent::Input);
860            }
861            // Deletes content from the insertion point to the end of the current paragraph.
862            "deleteWordBackward:" => {
863                driver.backdelete_word();
864                return Some(GeneratedTextInputEvent::Input);
865            }
866            // Deletes the word preceding the current insertion point.
867            "deleteWordForward:" => {
868                driver.delete_word();
869                return Some(GeneratedTextInputEvent::Input);
870            }
871            // Deletes the current selection, placing it in a temporary buffer, such as the Clipboard.
872            "yank:" => {
873                if let Some(text) = driver.editor.selected_text() {
874                    let _ = shell_provider.set_clipboard_text(text.to_owned());
875                    driver.delete_selection();
876                    return Some(GeneratedTextInputEvent::Input);
877                }
878            }
879
880            // Moving the Insertion Pointer
881
882            // Moves the insertion pointer backward in the current content.
883            "moveBackward:" => {
884                driver.move_left(); // TODO: Bidi-aware
885                return Some(GeneratedTextInputEvent::Select);
886            }
887
888            // Moves the insertion pointer down in the current content.
889            "moveDown:" => {
890                driver.move_down();
891                return Some(GeneratedTextInputEvent::Select);
892            }
893            // Moves the insertion pointer forward in the current content.
894            "moveForward:" => {
895                driver.move_right();
896                return Some(GeneratedTextInputEvent::Select);
897            } // TODO: Bidi-aware
898
899            // Moves the insertion pointer left in the current content.
900            "moveLeft:" => {
901                driver.move_left();
902                return Some(GeneratedTextInputEvent::Select);
903            }
904            // Moves the insertion pointer right in the current content.
905            "moveRight:" => {
906                driver.move_right();
907                return Some(GeneratedTextInputEvent::Select);
908            }
909            // Moves the insertion pointer up in the current content.
910            "moveUp:" => {
911                driver.move_up();
912                return Some(GeneratedTextInputEvent::Select);
913            }
914
915            // Modifying the Selection
916
917            // Extends the selection to include the content before the current selection.
918            "moveBackwardAndModifySelection:" => {
919                driver.select_left(); // TODO: Bidi-aware
920                return Some(GeneratedTextInputEvent::Select);
921            }
922            // Extends the selection to include the content below the current selection.
923            "moveDownAndModifySelection:" => {
924                driver.select_down();
925                return Some(GeneratedTextInputEvent::Select);
926            }
927            // Extends the selection to include the content after the current selection.
928            "moveForwardAndModifySelection:" => {
929                driver.select_right(); // TODO: Bidi-aware
930                return Some(GeneratedTextInputEvent::Select);
931            }
932            // Extends the selection to include the content to the left of the current selection.
933            "moveLeftAndModifySelection:" => {
934                driver.select_left();
935                return Some(GeneratedTextInputEvent::Select);
936            }
937            // Extends the selection to include the content to the right of the current selection.
938            "moveRightAndModifySelection:" => {
939                driver.select_right();
940                return Some(GeneratedTextInputEvent::Select);
941            }
942            // Extends the selection to include the content above the current selection.
943            "moveUpAndModifySelection:" => {
944                driver.select_up();
945                return Some(GeneratedTextInputEvent::Select);
946            }
947
948            // Changing the Selection
949            "selectAll:" => {
950                driver.select_all();
951                return Some(GeneratedTextInputEvent::Select);
952            }
953            "selectLine:" => {
954                driver.move_to_line_start();
955                driver.select_to_line_end();
956                return Some(GeneratedTextInputEvent::Select);
957            }
958            "selectParagraph:" => {
959                driver.move_to_hard_line_start();
960                driver.select_to_hard_line_end();
961                return Some(GeneratedTextInputEvent::Select);
962            }
963            "selectWord:" => {
964                // TODO
965            }
966
967            // Moving the Selection in Documents
968            "moveToBeginningOfDocument:" => {
969                driver.move_to_text_start();
970                return Some(GeneratedTextInputEvent::Select);
971            }
972            "moveToBeginningOfDocumentAndModifySelection:" => {
973                driver.select_to_text_start();
974                return Some(GeneratedTextInputEvent::Select);
975            }
976            "moveToEndOfDocument:" => {
977                driver.move_to_text_end();
978                return Some(GeneratedTextInputEvent::Select);
979            }
980            "moveToEndOfDocumentAndModifySelection:" => {
981                driver.move_to_text_end();
982                return Some(GeneratedTextInputEvent::Select);
983            }
984
985            // Moving the Selection in Paragraphs
986            "moveParagraphBackwardAndModifySelection:" => {}
987            "moveParagraphForwardAndModifySelection:" => {}
988            "moveToBeginningOfParagraph:" => {
989                driver.move_to_hard_line_start();
990                return Some(GeneratedTextInputEvent::Select);
991            }
992            "moveToBeginningOfParagraphAndModifySelection:" => {
993                driver.select_to_hard_line_start();
994                return Some(GeneratedTextInputEvent::Select);
995            }
996            "moveToEndOfParagraph:" => {
997                driver.move_to_hard_line_end();
998                return Some(GeneratedTextInputEvent::Select);
999            }
1000            "moveToEndOfParagraphAndModifySelection:" => {
1001                driver.select_to_hard_line_end();
1002                return Some(GeneratedTextInputEvent::Select);
1003            }
1004
1005            // Moving the Selection in Lines of Text
1006            "moveToBeginningOfLine:" => {
1007                driver.move_to_line_start();
1008                return Some(GeneratedTextInputEvent::Select);
1009            }
1010            "moveToBeginningOfLineAndModifySelection:" => {
1011                driver.select_to_line_start();
1012                return Some(GeneratedTextInputEvent::Select);
1013            }
1014            "moveToEndOfLine:" => {
1015                driver.move_to_line_end();
1016                return Some(GeneratedTextInputEvent::Select);
1017            }
1018            "moveToEndOfLineAndModifySelection:" => {
1019                driver.select_to_line_end();
1020                return Some(GeneratedTextInputEvent::Select);
1021            }
1022            "moveToLeftEndOfLine:" => {
1023                driver.move_to_text_start();
1024                return Some(GeneratedTextInputEvent::Select);
1025            }
1026            "moveToLeftEndOfLineAndModifySelection:" => {
1027                driver.select_to_line_start();
1028                return Some(GeneratedTextInputEvent::Select);
1029            }
1030            "moveToRightEndOfLine:" => {
1031                driver.move_to_line_end();
1032                return Some(GeneratedTextInputEvent::Select);
1033            }
1034            "moveToRightEndOfLineAndModifySelection:" => {
1035                driver.select_to_line_end();
1036                return Some(GeneratedTextInputEvent::Select);
1037            }
1038
1039            // Moving the Selection by Word Boundaries
1040            "moveWordBackward:" => {
1041                driver.move_word_left();
1042                return Some(GeneratedTextInputEvent::Select);
1043            }
1044            "moveWordBackwardAndModifySelection:" => {
1045                driver.select_word_left();
1046                return Some(GeneratedTextInputEvent::Select);
1047            }
1048            "moveWordForward:" => {
1049                driver.move_word_right();
1050                return Some(GeneratedTextInputEvent::Select);
1051            }
1052            "moveWordForwardAndModifySelection:" => {
1053                driver.select_word_right();
1054                return Some(GeneratedTextInputEvent::Select);
1055            }
1056            "moveWordLeft:" => {
1057                driver.move_word_left();
1058                return Some(GeneratedTextInputEvent::Select);
1059            }
1060            "moveWordLeftAndModifySelection:" => {
1061                driver.select_word_left();
1062                return Some(GeneratedTextInputEvent::Select);
1063            }
1064            "moveWordRight:" => {
1065                driver.move_word_right();
1066                return Some(GeneratedTextInputEvent::Select);
1067            }
1068            "moveWordRightAndModifySelection:" => {
1069                driver.select_word_right();
1070                return Some(GeneratedTextInputEvent::Select);
1071            }
1072
1073            // Scrolling Content
1074
1075            // Scrolls the content down by a page.
1076            "scrollPageDown:" => {}
1077            // Scrolls the content up by a page.
1078            "scrollPageUp:" => {}
1079            // Scrolls the content down by a line.
1080            "scrollLineDown:" => {}
1081            // Scrolls the content up by a line.
1082            "scrollLineUp:" => {}
1083            // Scrolls the content to the beginning of the document.
1084            "scrollToBeginningOfDocument:" => {}
1085            // Scrolls the content to the end of the document.
1086            "scrollToEndOfDocument:" => {}
1087            // Moves the visible content region down by a page.
1088            "pageDown:" => {}
1089            // Moves the visible content region up by a page.
1090            "pageUp:" => {}
1091            // Moves the visible content region down by a page, and extends the current selection.
1092            "pageDownAndModifySelection:" => {}
1093            // Moves the visible content region up by a page, and extends the current selection.
1094            "pageUpAndModifySelection:" => {}
1095            // Moves the visible content region so the current selection is visually centered.
1096            "centerSelectionInVisibleArea:" => {}
1097
1098            // Transposing Elements
1099
1100            // Transposes the content around the current selection.
1101            "transpose:" => {}
1102            // Transposes the words around the current selection.
1103            "transposeWords:" => {}
1104
1105            // Indenting Content
1106            // Indents the content at the current selection.
1107            "indent:" => {}
1108
1109            // Canceling Operations
1110            // Cancels the current operation.
1111            "cancelOperation:" => {}
1112
1113            // Supporting QuickLook
1114            // Invokes QuickLook to preview the current selection.
1115            "quickLookPreviewItems:" => {}
1116
1117            // Supporting Writing Directions
1118            "makeBaseWritingDirectionLeftToRight:" => {}
1119            "makeBaseWritingDirectionNatural:" => {}
1120            "makeBaseWritingDirectionRightToLeft:" => {}
1121            "makeTextWritingDirectionLeftToRight:" => {}
1122            "makeTextWritingDirectionNatural:" => {}
1123            "makeTextWritingDirectionRightToLeft:" => {}
1124
1125            // Changing Capitalization
1126            "capitalizeWord:" => {}
1127            "changeCaseOfLetter:" => {}
1128            "lowercaseWord:" => {}
1129            "uppercaseWord:" => {}
1130
1131            // Supporting Marked Selections
1132            "setMark:" => {}
1133            "selectToMark:" => {}
1134            "deleteToMark:" => {}
1135            "swapWithMark:" => {}
1136
1137            // Supporting Autocomplete
1138            "complete:" => {}
1139
1140            // Instance Methods
1141            "showContextMenuForSelection:" => {}
1142
1143            // Unknown command
1144            _ => {}
1145        };
1146
1147        None
1148    }
1149
1150    pub(crate) fn apply_ime_event(
1151        &mut self,
1152        font_ctx: &mut FontContext,
1153        layout_ctx: &mut LayoutContext<TextBrush>,
1154        event: ApaceImeEvent,
1155    ) -> Option<GeneratedTextInputEvent> {
1156        let result = self.apply_plain_ime_event(font_ctx, layout_ctx, event);
1157        self.refresh_display(font_ctx, layout_ctx);
1158        result
1159    }
1160
1161    fn apply_plain_ime_event(
1162        &mut self,
1163        font_ctx: &mut FontContext,
1164        layout_ctx: &mut LayoutContext<TextBrush>,
1165        event: ApaceImeEvent,
1166    ) -> Option<GeneratedTextInputEvent> {
1167        let editor = &mut self.editor;
1168        let mut driver = editor.driver(font_ctx, layout_ctx);
1169
1170        match event {
1171            ApaceImeEvent::Enabled => {
1172                // Do nothing
1173                None
1174            }
1175            ApaceImeEvent::Disabled => {
1176                driver.clear_compose();
1177                Some(GeneratedTextInputEvent::PreEditChange)
1178            }
1179            ApaceImeEvent::Commit(text) => {
1180                driver.clear_compose();
1181                driver.insert_or_replace_selection(&text);
1182                Some(GeneratedTextInputEvent::Input)
1183            }
1184            ApaceImeEvent::Preedit(text, cursor) => {
1185                if text.is_empty() {
1186                    driver.clear_compose();
1187                } else {
1188                    driver.set_compose(&text, cursor);
1189                }
1190                Some(GeneratedTextInputEvent::PreEditChange)
1191            }
1192            ApaceImeEvent::DeleteSurrounding {
1193                before_bytes,
1194                after_bytes,
1195            } => {
1196                let _ = before_bytes;
1197                let _ = after_bytes;
1198                // TODO
1199                None
1200            }
1201        }
1202    }
1203}
1204
1205#[cfg(test)]
1206#[path = "ime_context_tests.rs"]
1207mod ime_context_tests;