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)]
14pub struct TextBrush {
16 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 pub block_offset: f32,
34 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
58pub enum GeneratedTextInputEvent {
60 Input,
61 Select,
62 PreEditChange,
63 Submit,
64}
65
66pub struct TextInputData {
67 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 pub is_multiline: bool,
76 pub scroll_offset: f32,
82}
83
84impl 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 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 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 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 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 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 let scale = layout.scale();
361
362 let Some(caret) = self.cursor_geometry(1.5) else {
364 return;
365 };
366
367 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 if caret_end > offset + viewport {
388 offset = caret_end - viewport;
389 }
390 if caret_start < offset {
391 offset = caret_start;
392 }
393
394 let max_offset = (content.max(caret_end) - viewport).max(0.0);
398 self.scroll_offset = offset.clamp(0.0, max_offset);
399 }
400
401 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 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 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 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 #[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 "insertBacktab:" => {}
778 "insertContainerBreak:" => {}
780 "insertDoubleQuoteIgnoringSubstitution:" => {
782 driver.insert_or_replace_selection("\"");
783 return Some(GeneratedTextInputEvent::Input);
784 }
785 "insertLineBreak:" => {
787 driver.insert_or_replace_selection("\n");
788 return Some(GeneratedTextInputEvent::Input);
789 }
790 "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 "insertNewlineIgnoringFieldEditor:" => {
801 driver.insert_or_replace_selection("\n");
802 return Some(GeneratedTextInputEvent::Input);
803 }
804 "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 "insertTab:" | "insertTabIgnoringFieldEditor:" => {
815 }
817 "insertText:" => {}
819
820 "deleteBackward:" | "deleteBackwardByDecomposingPreviousCharacter:" => {
825 driver.backdelete();
826 return Some(GeneratedTextInputEvent::Input);
827 }
828 "deleteForward:" => {
829 driver.delete();
830 return Some(GeneratedTextInputEvent::Input);
831 }
832 "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 "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 "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 "deleteWordBackward:" => {
866 driver.backdelete_word();
867 return Some(GeneratedTextInputEvent::Input);
868 }
869 "deleteWordForward:" => {
871 driver.delete_word();
872 return Some(GeneratedTextInputEvent::Input);
873 }
874 "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 "moveBackward:" => {
887 driver.move_left(); return Some(GeneratedTextInputEvent::Select);
889 }
890
891 "moveDown:" => {
893 driver.move_down();
894 return Some(GeneratedTextInputEvent::Select);
895 }
896 "moveForward:" => {
898 driver.move_right();
899 return Some(GeneratedTextInputEvent::Select);
900 } "moveLeft:" => {
904 driver.move_left();
905 return Some(GeneratedTextInputEvent::Select);
906 }
907 "moveRight:" => {
909 driver.move_right();
910 return Some(GeneratedTextInputEvent::Select);
911 }
912 "moveUp:" => {
914 driver.move_up();
915 return Some(GeneratedTextInputEvent::Select);
916 }
917
918 "moveBackwardAndModifySelection:" => {
922 driver.select_left(); return Some(GeneratedTextInputEvent::Select);
924 }
925 "moveDownAndModifySelection:" => {
927 driver.select_down();
928 return Some(GeneratedTextInputEvent::Select);
929 }
930 "moveForwardAndModifySelection:" => {
932 driver.select_right(); return Some(GeneratedTextInputEvent::Select);
934 }
935 "moveLeftAndModifySelection:" => {
937 driver.select_left();
938 return Some(GeneratedTextInputEvent::Select);
939 }
940 "moveRightAndModifySelection:" => {
942 driver.select_right();
943 return Some(GeneratedTextInputEvent::Select);
944 }
945 "moveUpAndModifySelection:" => {
947 driver.select_up();
948 return Some(GeneratedTextInputEvent::Select);
949 }
950
951 "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 }
969
970 "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 "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 "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 "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 "scrollPageDown:" => {}
1080 "scrollPageUp:" => {}
1082 "scrollLineDown:" => {}
1084 "scrollLineUp:" => {}
1086 "scrollToBeginningOfDocument:" => {}
1088 "scrollToEndOfDocument:" => {}
1090 "pageDown:" => {}
1092 "pageUp:" => {}
1094 "pageDownAndModifySelection:" => {}
1096 "pageUpAndModifySelection:" => {}
1098 "centerSelectionInVisibleArea:" => {}
1100
1101 "transpose:" => {}
1105 "transposeWords:" => {}
1107
1108 "indent:" => {}
1111
1112 "cancelOperation:" => {}
1115
1116 "quickLookPreviewItems:" => {}
1119
1120 "makeBaseWritingDirectionLeftToRight:" => {}
1122 "makeBaseWritingDirectionNatural:" => {}
1123 "makeBaseWritingDirectionRightToLeft:" => {}
1124 "makeTextWritingDirectionLeftToRight:" => {}
1125 "makeTextWritingDirectionNatural:" => {}
1126 "makeTextWritingDirectionRightToLeft:" => {}
1127
1128 "capitalizeWord:" => {}
1130 "changeCaseOfLetter:" => {}
1131 "lowercaseWord:" => {}
1132 "uppercaseWord:" => {}
1133
1134 "setMark:" => {}
1136 "selectToMark:" => {}
1137 "deleteToMark:" => {}
1138 "swapWithMark:" => {}
1139
1140 "complete:" => {}
1142
1143 "showContextMenuForSelection:" => {}
1145
1146 _ => {}
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 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 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}