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                        if shell_provider.request_clipboard_text().is_ok() {
546                            return None;
547                        }
548                        let text = shell_provider.get_clipboard_text().unwrap_or_default();
549                        driver.insert_or_replace_selection(&text);
550                    }
551                    _ => unreachable!(),
552                }
553
554                return Some(GeneratedTextInputEvent::Input);
555            }
556            Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
557                if shift {
558                    driver.collapse_selection()
559                } else {
560                    driver.select_all()
561                }
562                return Some(GeneratedTextInputEvent::Select);
563            }
564            Key::Named(NamedKey::ArrowLeft) => {
565                if action_mod {
566                    if shift {
567                        driver.select_word_left()
568                    } else {
569                        driver.move_word_left()
570                    }
571                } else if shift {
572                    driver.select_left()
573                } else {
574                    driver.move_left()
575                }
576                return Some(GeneratedTextInputEvent::Select);
577            }
578            Key::Named(NamedKey::ArrowRight) => {
579                if action_mod {
580                    if shift {
581                        driver.select_word_right()
582                    } else {
583                        driver.move_word_right()
584                    }
585                } else if shift {
586                    driver.select_right()
587                } else {
588                    driver.move_right()
589                }
590                return Some(GeneratedTextInputEvent::Select);
591            }
592            Key::Named(NamedKey::ArrowUp) => {
593                if shift {
594                    driver.select_up()
595                } else {
596                    driver.move_up()
597                }
598                return Some(GeneratedTextInputEvent::Select);
599            }
600            Key::Named(NamedKey::ArrowDown) => {
601                if shift {
602                    driver.select_down()
603                } else {
604                    driver.move_down()
605                }
606                return Some(GeneratedTextInputEvent::Select);
607            }
608            Key::Named(NamedKey::Home) => {
609                if action_mod {
610                    if shift {
611                        driver.select_to_text_start()
612                    } else {
613                        driver.move_to_text_start()
614                    }
615                } else if shift {
616                    driver.select_to_line_start()
617                } else {
618                    driver.move_to_line_start()
619                }
620                return Some(GeneratedTextInputEvent::Select);
621            }
622            Key::Named(NamedKey::End) => {
623                if action_mod {
624                    if shift {
625                        driver.select_to_text_end()
626                    } else {
627                        driver.move_to_text_end()
628                    }
629                } else if shift {
630                    driver.select_to_line_end()
631                } else {
632                    driver.move_to_line_end()
633                }
634                return Some(GeneratedTextInputEvent::Select);
635            }
636            Key::Named(NamedKey::Delete) => {
637                if action_mod {
638                    driver.delete_word()
639                } else {
640                    driver.delete()
641                }
642                return Some(GeneratedTextInputEvent::Input);
643            }
644
645            // On macOS this is handled by the apple standard keybindings
646            #[cfg(not(target_os = "macos"))]
647            Key::Named(NamedKey::Backspace) => {
648                if action_mod {
649                    driver.backdelete_word()
650                } else {
651                    driver.backdelete()
652                }
653                return Some(GeneratedTextInputEvent::Input);
654            }
655
656            Key::Character(c) if c == "\n" => {
657                if is_multiline {
658                    driver.insert_or_replace_selection("\n");
659                    return Some(GeneratedTextInputEvent::Input);
660                } else {
661                    return Some(GeneratedTextInputEvent::Submit);
662                }
663            }
664            Key::Named(NamedKey::Enter) => {
665                if is_multiline {
666                    driver.insert_or_replace_selection("\n");
667                    return Some(GeneratedTextInputEvent::Input);
668                } else {
669                    return Some(GeneratedTextInputEvent::Submit);
670                }
671            }
672            Key::Character(s)
673                if !mods.contains(Modifiers::CONTROL) && !mods.contains(Modifiers::META) =>
674            {
675                driver.insert_or_replace_selection(&s);
676                return Some(GeneratedTextInputEvent::Input);
677            }
678            _ => {}
679        };
680
681        None
682    }
683
684    pub(crate) fn apply_apple_standard_keybinding(
685        &mut self,
686        font_ctx: &mut FontContext,
687        layout_ctx: &mut LayoutContext<TextBrush>,
688        shell_provider: &dyn ShellProvider,
689        command: &str,
690    ) -> Option<GeneratedTextInputEvent> {
691        if self.is_password() {
692            if matches!(command, "copy:" | "cut:") {
693                return None;
694            }
695            let deletion = match command {
696                "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
697                    Some((true, false))
698                }
699                "deleteForward:" => Some((false, false)),
700                "deleteWordBackward:"
701                | "deleteToBeginningOfLine:"
702                | "deleteToBeginningOfParagraph:" => Some((true, true)),
703                "deleteWordForward:" | "deleteToEndOfLine:" | "deleteToEndOfParagraph:" => {
704                    Some((false, true))
705                }
706                _ => None,
707            };
708            if let Some((backwards, whole)) = deletion {
709                self.password_delete(font_ctx, layout_ctx, backwards, whole);
710                return Some(GeneratedTextInputEvent::Input);
711            }
712            if command.starts_with("move") || command.starts_with("select") {
713                if command.contains("Word") {
714                    self.point_selection(font_ctx, layout_ctx, |driver| {
715                        if command.starts_with("select") {
716                            driver.select_all();
717                        } else if command.contains("Backward") || command.contains("Left") {
718                            if command.contains("ModifySelection") {
719                                driver.select_to_text_start();
720                            } else {
721                                driver.move_to_text_start();
722                            }
723                        } else if command.contains("ModifySelection") {
724                            driver.select_to_text_end();
725                        } else {
726                            driver.move_to_text_end();
727                        }
728                    });
729                    return Some(GeneratedTextInputEvent::Select);
730                }
731                self.refresh_display(font_ctx, layout_ctx);
732                let mut password = self.password.take().expect("password");
733                std::mem::swap(&mut self.editor, &mut password.editor);
734                let result = self.apply_plain_apple_standard_keybinding(
735                    font_ctx,
736                    layout_ctx,
737                    shell_provider,
738                    command,
739                );
740                std::mem::swap(&mut self.editor, &mut password.editor);
741                let selection = password.editor.raw_selection();
742                let a = password.raw_index(selection.anchor().index());
743                let f = password.raw_index(selection.focus().index());
744                self.editor
745                    .driver(font_ctx, layout_ctx)
746                    .select_byte_range(a, f);
747                password.sync_selection(&self.editor, font_ctx, layout_ctx);
748                self.password = Some(password);
749                return result;
750            }
751        }
752        let result = self.apply_plain_apple_standard_keybinding(
753            font_ctx,
754            layout_ctx,
755            shell_provider,
756            command,
757        );
758        self.refresh_display(font_ctx, layout_ctx);
759        result
760    }
761
762    fn apply_plain_apple_standard_keybinding(
763        &mut self,
764        font_ctx: &mut FontContext,
765        layout_ctx: &mut LayoutContext<TextBrush>,
766        shell_provider: &dyn ShellProvider,
767        command: &str,
768    ) -> Option<GeneratedTextInputEvent> {
769        let editor = &mut self.editor;
770        let mut driver = editor.driver(font_ctx, layout_ctx);
771        let is_multiline = self.is_multiline;
772
773        match command {
774            // Inserting Content
775
776            // Inserts a backtab character.
777            "insertBacktab:" => {}
778            // Inserts a container break, such as a new page break.
779            "insertContainerBreak:" => {}
780            // Inserts a double quotation mark without substituting a curly quotation mark.
781            "insertDoubleQuoteIgnoringSubstitution:" => {
782                driver.insert_or_replace_selection("\"");
783                return Some(GeneratedTextInputEvent::Input);
784            }
785            // Inserts a line break character.
786            "insertLineBreak:" => {
787                driver.insert_or_replace_selection("\n");
788                return Some(GeneratedTextInputEvent::Input);
789            }
790            // Inserts a newline character.
791            "insertNewline:" => {
792                if is_multiline {
793                    driver.insert_or_replace_selection("\n");
794                    return Some(GeneratedTextInputEvent::Input);
795                } else {
796                    return Some(GeneratedTextInputEvent::Submit);
797                }
798            }
799            // Inserts a newline character without invoking the field editor’s normal handling to end editing.
800            "insertNewlineIgnoringFieldEditor:" => {
801                driver.insert_or_replace_selection("\n");
802                return Some(GeneratedTextInputEvent::Input);
803            }
804            // Inserts a paragraph separator.
805            "insertParagraphSeparator:" => {
806                driver.insert_or_replace_selection("\n");
807                return Some(GeneratedTextInputEvent::Input);
808            }
809            "insertSingleQuoteIgnoringSubstitution:" => {
810                driver.insert_or_replace_selection("'");
811                return Some(GeneratedTextInputEvent::Input);
812            }
813            // Inserts a tab character.
814            "insertTab:" | "insertTabIgnoringFieldEditor:" => {
815                // Ignore for now seeing as parley has poor support for laying out tabs
816            }
817            // Inserts the text you specify.
818            "insertText:" => {}
819
820            // Deleting Content
821
822            // Deletes content moving backward from the current insertion point.
823            // TODO: handle deleteBackwardByDecomposingPreviousCharacter separately
824            "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
825                driver.backdelete();
826                return Some(GeneratedTextInputEvent::Input);
827            }
828            "deleteForward:" => {
829                driver.delete();
830                return Some(GeneratedTextInputEvent::Input);
831            }
832            // Deletes content from the insertion point to the beginning of the current line.
833            "deleteToBeginningOfLine:" => {
834                if driver.editor.raw_selection().is_collapsed() {
835                    driver.select_to_line_start();
836                }
837                driver.delete_selection();
838                return Some(GeneratedTextInputEvent::Input);
839            }
840            // Deletes content from the insertion point to the beginning of the current paragraph.
841            "deleteToEndOfLine:" => {
842                if driver.editor.raw_selection().is_collapsed() {
843                    driver.select_to_line_end();
844                }
845                driver.delete_selection();
846                return Some(GeneratedTextInputEvent::Input);
847            }
848            "deleteToBeginningOfParagraph:" => {
849                if driver.editor.raw_selection().is_collapsed() {
850                    driver.select_to_hard_line_start();
851                }
852                driver.delete_selection();
853                return Some(GeneratedTextInputEvent::Input);
854            }
855
856            // Deletes content from the insertion point to the end of the current line.
857            "deleteToEndOfParagraph:" => {
858                if driver.editor.raw_selection().is_collapsed() {
859                    driver.select_to_hard_line_end();
860                }
861                driver.delete_selection();
862                return Some(GeneratedTextInputEvent::Input);
863            }
864            // Deletes content from the insertion point to the end of the current paragraph.
865            "deleteWordBackward:" => {
866                driver.backdelete_word();
867                return Some(GeneratedTextInputEvent::Input);
868            }
869            // Deletes the word preceding the current insertion point.
870            "deleteWordForward:" => {
871                driver.delete_word();
872                return Some(GeneratedTextInputEvent::Input);
873            }
874            // Deletes the current selection, placing it in a temporary buffer, such as the Clipboard.
875            "yank:" => {
876                if let Some(text) = driver.editor.selected_text() {
877                    let _ = shell_provider.set_clipboard_text(text.to_owned());
878                    driver.delete_selection();
879                    return Some(GeneratedTextInputEvent::Input);
880                }
881            }
882
883            // Moving the Insertion Pointer
884
885            // Moves the insertion pointer backward in the current content.
886            "moveBackward:" => {
887                driver.move_left(); // TODO: Bidi-aware
888                return Some(GeneratedTextInputEvent::Select);
889            }
890
891            // Moves the insertion pointer down in the current content.
892            "moveDown:" => {
893                driver.move_down();
894                return Some(GeneratedTextInputEvent::Select);
895            }
896            // Moves the insertion pointer forward in the current content.
897            "moveForward:" => {
898                driver.move_right();
899                return Some(GeneratedTextInputEvent::Select);
900            } // TODO: Bidi-aware
901
902            // Moves the insertion pointer left in the current content.
903            "moveLeft:" => {
904                driver.move_left();
905                return Some(GeneratedTextInputEvent::Select);
906            }
907            // Moves the insertion pointer right in the current content.
908            "moveRight:" => {
909                driver.move_right();
910                return Some(GeneratedTextInputEvent::Select);
911            }
912            // Moves the insertion pointer up in the current content.
913            "moveUp:" => {
914                driver.move_up();
915                return Some(GeneratedTextInputEvent::Select);
916            }
917
918            // Modifying the Selection
919
920            // Extends the selection to include the content before the current selection.
921            "moveBackwardAndModifySelection:" => {
922                driver.select_left(); // TODO: Bidi-aware
923                return Some(GeneratedTextInputEvent::Select);
924            }
925            // Extends the selection to include the content below the current selection.
926            "moveDownAndModifySelection:" => {
927                driver.select_down();
928                return Some(GeneratedTextInputEvent::Select);
929            }
930            // Extends the selection to include the content after the current selection.
931            "moveForwardAndModifySelection:" => {
932                driver.select_right(); // TODO: Bidi-aware
933                return Some(GeneratedTextInputEvent::Select);
934            }
935            // Extends the selection to include the content to the left of the current selection.
936            "moveLeftAndModifySelection:" => {
937                driver.select_left();
938                return Some(GeneratedTextInputEvent::Select);
939            }
940            // Extends the selection to include the content to the right of the current selection.
941            "moveRightAndModifySelection:" => {
942                driver.select_right();
943                return Some(GeneratedTextInputEvent::Select);
944            }
945            // Extends the selection to include the content above the current selection.
946            "moveUpAndModifySelection:" => {
947                driver.select_up();
948                return Some(GeneratedTextInputEvent::Select);
949            }
950
951            // Changing the Selection
952            "selectAll:" => {
953                driver.select_all();
954                return Some(GeneratedTextInputEvent::Select);
955            }
956            "selectLine:" => {
957                driver.move_to_line_start();
958                driver.select_to_line_end();
959                return Some(GeneratedTextInputEvent::Select);
960            }
961            "selectParagraph:" => {
962                driver.move_to_hard_line_start();
963                driver.select_to_hard_line_end();
964                return Some(GeneratedTextInputEvent::Select);
965            }
966            "selectWord:" => {
967                // TODO
968            }
969
970            // Moving the Selection in Documents
971            "moveToBeginningOfDocument:" => {
972                driver.move_to_text_start();
973                return Some(GeneratedTextInputEvent::Select);
974            }
975            "moveToBeginningOfDocumentAndModifySelection:" => {
976                driver.select_to_text_start();
977                return Some(GeneratedTextInputEvent::Select);
978            }
979            "moveToEndOfDocument:" => {
980                driver.move_to_text_end();
981                return Some(GeneratedTextInputEvent::Select);
982            }
983            "moveToEndOfDocumentAndModifySelection:" => {
984                driver.move_to_text_end();
985                return Some(GeneratedTextInputEvent::Select);
986            }
987
988            // Moving the Selection in Paragraphs
989            "moveParagraphBackwardAndModifySelection:" => {}
990            "moveParagraphForwardAndModifySelection:" => {}
991            "moveToBeginningOfParagraph:" => {
992                driver.move_to_hard_line_start();
993                return Some(GeneratedTextInputEvent::Select);
994            }
995            "moveToBeginningOfParagraphAndModifySelection:" => {
996                driver.select_to_hard_line_start();
997                return Some(GeneratedTextInputEvent::Select);
998            }
999            "moveToEndOfParagraph:" => {
1000                driver.move_to_hard_line_end();
1001                return Some(GeneratedTextInputEvent::Select);
1002            }
1003            "moveToEndOfParagraphAndModifySelection:" => {
1004                driver.select_to_hard_line_end();
1005                return Some(GeneratedTextInputEvent::Select);
1006            }
1007
1008            // Moving the Selection in Lines of Text
1009            "moveToBeginningOfLine:" => {
1010                driver.move_to_line_start();
1011                return Some(GeneratedTextInputEvent::Select);
1012            }
1013            "moveToBeginningOfLineAndModifySelection:" => {
1014                driver.select_to_line_start();
1015                return Some(GeneratedTextInputEvent::Select);
1016            }
1017            "moveToEndOfLine:" => {
1018                driver.move_to_line_end();
1019                return Some(GeneratedTextInputEvent::Select);
1020            }
1021            "moveToEndOfLineAndModifySelection:" => {
1022                driver.select_to_line_end();
1023                return Some(GeneratedTextInputEvent::Select);
1024            }
1025            "moveToLeftEndOfLine:" => {
1026                driver.move_to_text_start();
1027                return Some(GeneratedTextInputEvent::Select);
1028            }
1029            "moveToLeftEndOfLineAndModifySelection:" => {
1030                driver.select_to_line_start();
1031                return Some(GeneratedTextInputEvent::Select);
1032            }
1033            "moveToRightEndOfLine:" => {
1034                driver.move_to_line_end();
1035                return Some(GeneratedTextInputEvent::Select);
1036            }
1037            "moveToRightEndOfLineAndModifySelection:" => {
1038                driver.select_to_line_end();
1039                return Some(GeneratedTextInputEvent::Select);
1040            }
1041
1042            // Moving the Selection by Word Boundaries
1043            "moveWordBackward:" => {
1044                driver.move_word_left();
1045                return Some(GeneratedTextInputEvent::Select);
1046            }
1047            "moveWordBackwardAndModifySelection:" => {
1048                driver.select_word_left();
1049                return Some(GeneratedTextInputEvent::Select);
1050            }
1051            "moveWordForward:" => {
1052                driver.move_word_right();
1053                return Some(GeneratedTextInputEvent::Select);
1054            }
1055            "moveWordForwardAndModifySelection:" => {
1056                driver.select_word_right();
1057                return Some(GeneratedTextInputEvent::Select);
1058            }
1059            "moveWordLeft:" => {
1060                driver.move_word_left();
1061                return Some(GeneratedTextInputEvent::Select);
1062            }
1063            "moveWordLeftAndModifySelection:" => {
1064                driver.select_word_left();
1065                return Some(GeneratedTextInputEvent::Select);
1066            }
1067            "moveWordRight:" => {
1068                driver.move_word_right();
1069                return Some(GeneratedTextInputEvent::Select);
1070            }
1071            "moveWordRightAndModifySelection:" => {
1072                driver.select_word_right();
1073                return Some(GeneratedTextInputEvent::Select);
1074            }
1075
1076            // Scrolling Content
1077
1078            // Scrolls the content down by a page.
1079            "scrollPageDown:" => {}
1080            // Scrolls the content up by a page.
1081            "scrollPageUp:" => {}
1082            // Scrolls the content down by a line.
1083            "scrollLineDown:" => {}
1084            // Scrolls the content up by a line.
1085            "scrollLineUp:" => {}
1086            // Scrolls the content to the beginning of the document.
1087            "scrollToBeginningOfDocument:" => {}
1088            // Scrolls the content to the end of the document.
1089            "scrollToEndOfDocument:" => {}
1090            // Moves the visible content region down by a page.
1091            "pageDown:" => {}
1092            // Moves the visible content region up by a page.
1093            "pageUp:" => {}
1094            // Moves the visible content region down by a page, and extends the current selection.
1095            "pageDownAndModifySelection:" => {}
1096            // Moves the visible content region up by a page, and extends the current selection.
1097            "pageUpAndModifySelection:" => {}
1098            // Moves the visible content region so the current selection is visually centered.
1099            "centerSelectionInVisibleArea:" => {}
1100
1101            // Transposing Elements
1102
1103            // Transposes the content around the current selection.
1104            "transpose:" => {}
1105            // Transposes the words around the current selection.
1106            "transposeWords:" => {}
1107
1108            // Indenting Content
1109            // Indents the content at the current selection.
1110            "indent:" => {}
1111
1112            // Canceling Operations
1113            // Cancels the current operation.
1114            "cancelOperation:" => {}
1115
1116            // Supporting QuickLook
1117            // Invokes QuickLook to preview the current selection.
1118            "quickLookPreviewItems:" => {}
1119
1120            // Supporting Writing Directions
1121            "makeBaseWritingDirectionLeftToRight:" => {}
1122            "makeBaseWritingDirectionNatural:" => {}
1123            "makeBaseWritingDirectionRightToLeft:" => {}
1124            "makeTextWritingDirectionLeftToRight:" => {}
1125            "makeTextWritingDirectionNatural:" => {}
1126            "makeTextWritingDirectionRightToLeft:" => {}
1127
1128            // Changing Capitalization
1129            "capitalizeWord:" => {}
1130            "changeCaseOfLetter:" => {}
1131            "lowercaseWord:" => {}
1132            "uppercaseWord:" => {}
1133
1134            // Supporting Marked Selections
1135            "setMark:" => {}
1136            "selectToMark:" => {}
1137            "deleteToMark:" => {}
1138            "swapWithMark:" => {}
1139
1140            // Supporting Autocomplete
1141            "complete:" => {}
1142
1143            // Instance Methods
1144            "showContextMenuForSelection:" => {}
1145
1146            // Unknown command
1147            _ => {}
1148        };
1149
1150        None
1151    }
1152
1153    pub(crate) fn apply_ime_event(
1154        &mut self,
1155        font_ctx: &mut FontContext,
1156        layout_ctx: &mut LayoutContext<TextBrush>,
1157        event: ApaceImeEvent,
1158    ) -> Option<GeneratedTextInputEvent> {
1159        let result = self.apply_plain_ime_event(font_ctx, layout_ctx, event);
1160        self.refresh_display(font_ctx, layout_ctx);
1161        result
1162    }
1163
1164    fn apply_plain_ime_event(
1165        &mut self,
1166        font_ctx: &mut FontContext,
1167        layout_ctx: &mut LayoutContext<TextBrush>,
1168        event: ApaceImeEvent,
1169    ) -> Option<GeneratedTextInputEvent> {
1170        let editor = &mut self.editor;
1171        let mut driver = editor.driver(font_ctx, layout_ctx);
1172
1173        match event {
1174            ApaceImeEvent::Enabled => {
1175                // Do nothing
1176                None
1177            }
1178            ApaceImeEvent::Disabled => {
1179                driver.clear_compose();
1180                Some(GeneratedTextInputEvent::PreEditChange)
1181            }
1182            ApaceImeEvent::Commit(text) => {
1183                driver.clear_compose();
1184                driver.insert_or_replace_selection(&text);
1185                Some(GeneratedTextInputEvent::Input)
1186            }
1187            ApaceImeEvent::Preedit(text, cursor) => {
1188                if text.is_empty() {
1189                    driver.clear_compose();
1190                } else {
1191                    driver.set_compose(&text, cursor);
1192                }
1193                Some(GeneratedTextInputEvent::PreEditChange)
1194            }
1195            ApaceImeEvent::DeleteSurrounding {
1196                before_bytes,
1197                after_bytes,
1198            } => {
1199                let _ = before_bytes;
1200                let _ = after_bytes;
1201                // TODO
1202                None
1203            }
1204        }
1205    }
1206}
1207
1208#[cfg(test)]
1209#[path = "ime_context_tests.rs"]
1210mod ime_context_tests;
1211
1212#[cfg(test)]
1213mod tests {
1214    use super::*;
1215
1216    #[test]
1217    fn ime_commit_replaces_preedit() {
1218        for committed in ["完成", ""] {
1219            for cursor in [Some((3, 3)), None] {
1220                let mut input = TextInputData::new(false);
1221                let mut font_ctx = FontContext::new();
1222                let mut layout_ctx = LayoutContext::new();
1223                input.apply_ime_event(
1224                    &mut font_ctx,
1225                    &mut layout_ctx,
1226                    ApaceImeEvent::Preedit("仮".into(), cursor),
1227                );
1228                input.apply_ime_event(
1229                    &mut font_ctx,
1230                    &mut layout_ctx,
1231                    ApaceImeEvent::Commit(committed.into()),
1232                );
1233                assert_eq!(input.editor.text(), committed);
1234                input.apply_ime_event(&mut font_ctx, &mut layout_ctx, ApaceImeEvent::Disabled);
1235                assert_eq!(input.editor.text(), committed);
1236            }
1237        }
1238    }
1239
1240    #[test]
1241    fn ime_commit_without_preedit() {
1242        let mut input = TextInputData::new(false);
1243        input.apply_ime_event(
1244            &mut FontContext::new(),
1245            &mut LayoutContext::new(),
1246            ApaceImeEvent::Commit("text".into()),
1247        );
1248        assert_eq!(input.editor.text(), "text");
1249    }
1250}