1use std::cell::RefCell;
6use std::rc::Rc;
7
8use embedder_traits::{
9 ClipboardAction, EditingAction, EditingDirection, EditingMotion, EmbedderMsg, InputEventResult,
10 ModifySelection, MouseButton,
11};
12use js::context::{JSContext, NoGC};
13use keyboard_types::{
14 CompositionEvent, CompositionState, Key, KeyState, KeyboardEvent as KeyboardTypesEvent,
15 Modifiers, NamedKey, ShortcutMatcher,
16};
17use layout_api::QueryMsg;
18use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
19use script_bindings::codegen::GenericBindings::EventBinding::EventMethods;
20use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
21use script_bindings::codegen::GenericBindings::UIEventBinding::UIEventMethods as _;
22use script_bindings::dom::UnrootedDom;
23use script_bindings::inheritance::Castable;
24use script_bindings::root::DomRoot;
25use script_bindings::str::DOMString;
26use servo_base::generic_channel::GenericCallback;
27
28use crate::dom::clipboardevent::ClipboardEventType;
29use crate::dom::event::{EventBubbles, EventCancelable};
30use crate::dom::execcommand::execcommands::DocumentExecCommandSupport;
31use crate::dom::inputevent::HitTestResult;
32use crate::dom::selection_range::RootedSelectionBoundary;
33use crate::dom::text_control::TextControlElement;
34use crate::dom::text_input::{InputEventType, IsComposing};
35use crate::dom::types::{
36 ClipboardEvent, DataTransfer, Event, EventTarget, HTMLInputElement, HTMLTextAreaElement,
37 MouseEvent, UIEvent,
38};
39use crate::dom::{Document, Node, NodeTraits};
40use crate::drag::document_selection_drag::{
41 DocumentSelectionDragHandler, adjust_anchor_for_user_select,
42};
43use crate::drag::drag_data_store::{DragDataStore, Kind, Mode};
44use crate::drag::drag_gesture::{DragGesture, DragHandler};
45
46#[cfg(target_os = "macos")]
47pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::META;
48#[cfg(not(target_os = "macos"))]
49pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
50
51#[cfg(target_os = "macos")]
52pub(crate) const ALT_OR_CONTROL: Modifiers = Modifiers::ALT;
53#[cfg(not(target_os = "macos"))]
54pub(crate) const ALT_OR_CONTROL: Modifiers = Modifiers::CONTROL;
55
56#[derive(Clone, Copy, Debug, PartialEq)]
58pub(crate) enum SelectionGranularity {
59 Position,
62 Word,
64 LineIgnoringSoftWrap,
67}
68
69impl SelectionGranularity {
70 pub(crate) fn from_mouse_event(mouse_event: &MouseEvent) -> Option<Self> {
71 match mouse_event.button() {
72 MouseButton::Primary => {}, MouseButton::Auxiliary => return Some(Self::Position),
74 _ => return None,
75 }
76
77 let click_count = mouse_event.upcast::<UIEvent>().Detail();
86 match click_count {
87 1 => Some(Self::Position),
88 2 => Some(Self::Word),
89 3 => Some(Self::LineIgnoringSoftWrap),
90 _ => None,
91 }
92 }
93}
94
95impl Document {
96 pub(crate) fn editing_context(&self, no_gc: &NoGC, node: &Node) -> EditingContext {
97 if let Ok(editing_context) = EditingContext::try_from(node) {
98 return editing_context;
99 }
100
101 let mut current_node = UnrootedDom::from_ref(node, no_gc);
102 while let Some(parent) = current_node.parent_in_flat_tree(no_gc).into_parent() {
103 if let Ok(editing_context) = EditingContext::try_from(&**parent) {
104 return editing_context;
105 }
106 current_node = parent;
107 }
108
109 EditingContext::Document(DomRoot::from_ref(self))
110 }
111
112 pub(crate) fn handle_clipboard_action(
114 &self,
115 cx: &mut JSContext,
116 editing_context: &EditingContext,
117 action: ClipboardAction,
118 ) -> InputEventResult {
119 let clipboard_event_type = match action {
120 ClipboardAction::Copy => ClipboardEventType::Copy,
121 ClipboardAction::Cut => ClipboardEventType::Cut,
122 ClipboardAction::Paste => ClipboardEventType::Paste,
123 };
124
125 let script_triggered = false;
127
128 let script_may_access_clipboard = false;
132
133 if script_triggered && !script_may_access_clipboard {
135 return InputEventResult::empty();
136 }
137
138 let event_target = editing_context.event_target();
140 let clipboard_event = self.fire_clipboard_event(cx, &event_target, clipboard_event_type);
141
142 let event = clipboard_event.upcast::<Event>();
143 if !event.DefaultPrevented() {
144 match clipboard_event.clipboard_event_type() {
146 ClipboardEventType::Copy if editing_context.copying_enabled() => {
147 if let Some(selection) = editing_context.selection_content(cx) {
151 self.send_to_embedder(EmbedderMsg::SetClipboardText(
152 self.webview_id(),
153 selection,
154 ));
155 }
156 self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
158
159 event.mark_as_handled();
161 },
162 ClipboardEventType::Cut => {
163 if let Some(selection) = editing_context.selection_content(cx) &&
164 editing_context.cutting_enabled()
165 {
166 let canceled = editing_context.fire_cut_beforeinput_event(cx);
167 if canceled {
168 return InputEventResult::empty();
169 }
170
171 self.send_to_embedder(EmbedderMsg::SetClipboardText(
177 self.webview_id(),
178 selection,
179 ));
180
181 editing_context.remove_the_contents_of_the_selection(cx);
184
185 self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
187
188 editing_context.fire_cut_events();
192
193 event.mark_as_handled();
195 }
196 },
197 ClipboardEventType::Paste => {
198 if editing_context.has_selection_or_cursor() &&
199 editing_context.pasting_enabled() &&
200 let Some(text_content) = clipboard_event.text_content()
201 {
202 let canceled =
203 editing_context.fire_paste_beforeinput_event(cx, &text_content);
204 if canceled {
205 return InputEventResult::empty();
206 }
207
208 editing_context.insert_content(cx, &text_content);
213
214 editing_context.fire_paste_events(&text_content);
218
219 event.mark_as_handled();
221 }
222 },
223 _ => (),
224 }
225 } else {
226 match clipboard_event.clipboard_event_type() {
228 ClipboardEventType::Copy => {
229 if let Some(clipboard_data) = clipboard_event.clipboard_data() {
232 let drag_data_store =
233 clipboard_data.data_store().expect("This shouldn't fail");
234 self.write_content_to_the_clipboard(&drag_data_store);
235 }
236 },
237 ClipboardEventType::Cut => {
238 if let Some(clipboard_data) = clipboard_event.clipboard_data() {
241 let drag_data_store =
242 clipboard_data.data_store().expect("This shouldn't fail");
243 self.write_content_to_the_clipboard(&drag_data_store);
244 }
245
246 self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
248 },
249 ClipboardEventType::Paste => (),
251 _ => (),
252 }
253 }
254
255 event.flags().into()
258 }
259
260 fn fire_clipboard_event(
262 &self,
263 cx: &mut JSContext,
264 target: &EventTarget,
265 clipboard_event_type: ClipboardEventType,
266 ) -> DomRoot<ClipboardEvent> {
267 let clipboard_event = ClipboardEvent::new(
268 cx,
269 self.window(),
270 None,
271 clipboard_event_type,
272 EventBubbles::Bubbles,
273 EventCancelable::Cancelable,
274 None,
275 );
276
277 let mut drag_data_store = DragDataStore::new();
280
281 let trusted = true;
285
286 match clipboard_event.clipboard_event_type() {
290 ClipboardEventType::Copy | ClipboardEventType::Cut => {
291 drag_data_store.set_mode(Mode::ReadWrite);
293 },
294 ClipboardEventType::Paste => {
295 let (callback, receiver) =
296 GenericCallback::new_blocking().expect("Could not create callback");
297 self.send_to_embedder(EmbedderMsg::GetClipboardText(self.webview_id(), callback));
298 let text_contents = receiver
299 .recv()
300 .map(Result::unwrap_or_default)
301 .unwrap_or_default();
302
303 drag_data_store.set_mode(Mode::ReadOnly);
305 if trusted {
307 let data = DOMString::from(text_contents);
311 let type_ = DOMString::from_static("text/plain");
312 let _ = drag_data_store.add(Kind::Text { data, type_ });
313
314 }
320 },
321 ClipboardEventType::Change | ClipboardEventType::Other(..) => (),
322 }
323
324 let clipboard_event_data = DataTransfer::new(
326 cx,
327 self.window(),
328 Rc::new(RefCell::new(Some(drag_data_store))),
329 );
330
331 clipboard_event.set_clipboard_data(Some(&clipboard_event_data));
333
334 let event = clipboard_event.upcast::<Event>();
336 event.set_trusted(trusted);
337
338 event.set_composed(true);
340
341 event.dispatch(cx, target, false);
343
344 DomRoot::from(clipboard_event)
345 }
346
347 fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) {
349 if drag_data_store.list_len() > 0 {
351 self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id()));
353 for item in drag_data_store.iter_item_list() {
355 match item {
356 Kind::Text { data, .. } => {
357 self.send_to_embedder(EmbedderMsg::SetClipboardText(
361 self.webview_id(),
362 data.to_string(),
363 ));
364 },
365 Kind::File { .. } => {
366 },
370 }
371 }
372 } else {
373 if drag_data_store.clear_was_called {
375 self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id()));
377 }
380 }
381 }
382
383 pub(crate) fn perform_editing_action(
389 &self,
390 cx: &mut JSContext,
391 editing_action: &EditingAction,
392 ) -> bool {
393 if !servo_config::pref!(dom_exec_command_enabled) {
394 return false;
395 }
396
397 let (command, argument) = match editing_action {
400 EditingAction::MoveCursor(..) |
402 EditingAction::StartComposition |
403 EditingAction::EndComposition(..) => return false,
404 EditingAction::SelectAll | EditingAction::Clipboard(_) => {
405 unreachable!("Should have been handled before this point.")
406 },
407 EditingAction::InsertNewline => {
408 (DOMString::from_static("insertlinebreak"), DOMString::new())
414 },
415 EditingAction::InsertParagraph => {
416 (DOMString::from_static("insertparagraph"), DOMString::new())
421 },
422 EditingAction::Backspace(..) => (DOMString::from_static("delete"), DOMString::new()),
429 EditingAction::Delete => (DOMString::from_static("forwarddelete"), DOMString::new()),
434 EditingAction::InsertText(text) | EditingAction::InsertCompositionText(text) => (
442 DOMString::from_static("inserttext"),
443 DOMString::from(text.as_str()),
444 ),
445 };
446
447 self.exec_command_for_command_id(cx, command, argument)
448 }
449
450 fn handle_mousedown_event(
451 &self,
452 cx: &mut JSContext,
453 mouse_event: &MouseEvent,
454 hit_test_result: &HitTestResult,
455 ) {
456 assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
457
458 let Some(selection) = self.GetSelection(cx) else {
459 return;
460 };
461
462 if mouse_event.button() == MouseButton::Auxiliary {
463 let _ = selection.Collapse(cx, None, 0);
464 mouse_event.upcast::<Event>().mark_as_handled();
465 return;
466 }
467
468 if mouse_event.button() != MouseButton::Primary {
469 return;
470 }
471
472 let boundary = hit_test_result
479 .dom_position_for_selection
480 .clone()
481 .unwrap_or_else(|| RootedSelectionBoundary::start_of(&hit_test_result.node));
482 let Some((boundary, user_select_contain_node)) =
483 adjust_anchor_for_user_select(cx, boundary)
484 else {
485 return;
486 };
487 selection.collapse_to_dom_position(cx, &boundary);
488 self.event_handler().install_drag_gesture(DragGesture::new(
489 DragHandler::DocumentSelection(DocumentSelectionDragHandler::new(
490 user_select_contain_node.as_deref(),
491 )),
492 ));
493 mouse_event.upcast::<Event>().mark_as_handled();
494 }
495}
496
497pub(crate) enum TextControlElementEditingContext {
498 TextArea(DomRoot<HTMLTextAreaElement>),
499 Input(DomRoot<HTMLInputElement>),
500}
501
502impl TextControlElementEditingContext {
503 fn text_control_element(&self) -> &dyn TextControlElement {
504 match self {
505 TextControlElementEditingContext::TextArea(text_area) => &**text_area,
506 TextControlElementEditingContext::Input(input) => &**input,
507 }
508 }
509
510 fn handle_mousedown_event(&self, mouse_event: &MouseEvent, hit_test_result: &HitTestResult) {
511 let text_control = self.text_control_element();
512 if text_control.text_input_mut().handle_mousedown_event(
513 text_control.as_element(),
514 mouse_event,
515 hit_test_result,
516 ) {
517 text_control.maybe_update_shared_selection();
518 mouse_event.upcast::<Event>().mark_as_handled();
519 }
520 }
521}
522
523pub(crate) enum EditingContext {
524 TextControl(TextControlElementEditingContext),
525 Document(DomRoot<Document>),
526}
527
528impl TryFrom<&Node> for EditingContext {
529 type Error = ();
530
531 fn try_from(node: &Node) -> Result<Self, Self::Error> {
532 if let Some(text_area) = node.downcast::<HTMLTextAreaElement>() {
533 return Ok(EditingContext::TextControl(
534 TextControlElementEditingContext::TextArea(DomRoot::from_ref(text_area)),
535 ));
536 }
537 if let Some(input) = node.downcast::<HTMLInputElement>() &&
538 input.is_textual_or_password()
539 {
540 return Ok(EditingContext::TextControl(
541 TextControlElementEditingContext::Input(DomRoot::from_ref(input)),
542 ));
543 }
544 Err(())
545 }
546}
547
548impl EditingContext {
549 pub(crate) fn event_target(&self) -> DomRoot<EventTarget> {
550 match self {
551 EditingContext::TextControl(element) => match element {
552 TextControlElementEditingContext::TextArea(text_area) => {
553 DomRoot::from_ref(text_area.upcast())
554 },
555 TextControlElementEditingContext::Input(input) => DomRoot::from_ref(input.upcast()),
556 },
557 EditingContext::Document(document) => {
558 document.event_handler().target_for_events_following_focus()
559 },
560 }
561 }
562
563 pub(crate) fn document(&self) -> DomRoot<Document> {
564 match self {
565 EditingContext::TextControl(element) => match element {
566 TextControlElementEditingContext::TextArea(element) => element.owner_document(),
567 TextControlElementEditingContext::Input(element) => element.owner_document(),
568 },
569 EditingContext::Document(document) => document.clone(),
570 }
571 }
572
573 pub(crate) fn selection_content(&self, cx: &mut JSContext) -> Option<String> {
574 match self {
575 EditingContext::TextControl(element) => element
576 .text_control_element()
577 .text_input()
578 .selection_content(),
579 EditingContext::Document(document) => document
580 .selection()
581 .map(|selection| selection.Stringifier(cx).to_string())
582 .filter(|selection| !selection.is_empty()),
583 }
584 }
585
586 pub(crate) fn has_uncollapsed_selection(&self) -> bool {
587 match self {
588 EditingContext::TextControl(element) => {
589 element.text_control_element().has_uncollapsed_selection()
590 },
591 EditingContext::Document(document) => document
592 .selection()
593 .is_some_and(|selection| !selection.collapsed()),
594 }
595 }
596
597 pub(crate) fn has_selectable_text(&self) -> bool {
598 match self {
599 EditingContext::TextControl(element) => {
600 element.text_control_element().has_selectable_text()
601 },
602 EditingContext::Document(..) => true,
603 }
604 }
605
606 pub(crate) fn has_selection_or_cursor(&self) -> bool {
607 match self {
608 EditingContext::TextControl(..) => true,
609 EditingContext::Document(document) => document
610 .selection()
611 .is_some_and(|selection| selection.RangeCount() > 0),
612 }
613 }
614
615 pub(crate) fn copying_enabled(&self) -> bool {
616 match self {
617 EditingContext::TextControl(element) => {
618 !element.text_control_element().is_password_field()
619 },
620 EditingContext::Document(..) => true,
621 }
622 }
623
624 pub(crate) fn cutting_enabled(&self) -> bool {
625 self.copying_enabled() && self.pasting_enabled()
628 }
629
630 pub(crate) fn pasting_enabled(&self) -> bool {
631 match self {
632 EditingContext::TextControl(element) => {
633 !element.text_control_element().read_only_or_disabled()
634 },
635 EditingContext::Document(..) => {
636 false
638 },
639 }
640 }
641
642 pub(crate) fn remove_the_contents_of_the_selection(&self, cx: &mut JSContext) {
643 match self {
644 EditingContext::TextControl(element) => {
645 element
646 .text_control_element()
647 .remove_the_contents_of_the_selection(cx);
648 },
649 EditingContext::Document(..) => {
650 },
652 }
653 }
654
655 pub(crate) fn fire_cut_beforeinput_event(&self, cx: &mut JSContext) -> bool {
657 match self {
658 EditingContext::TextControl(element) => {
659 element.text_control_element().fire_beforeinput_event(
660 cx,
661 None,
662 IsComposing::NotComposing,
663 InputEventType::DeleteByCut,
664 )
665 },
666 EditingContext::Document(..) => false,
667 }
668 }
669
670 pub(crate) fn fire_paste_beforeinput_event(&self, cx: &mut JSContext, text: &str) -> bool {
672 match self {
673 EditingContext::TextControl(element) => {
674 element.text_control_element().fire_beforeinput_event(
675 cx,
676 Some(text),
677 IsComposing::NotComposing,
678 InputEventType::InsertFromPaste,
679 )
680 },
681 EditingContext::Document(..) => false,
682 }
683 }
684
685 pub(crate) fn fire_cut_events(&self) {
686 match self {
687 EditingContext::TextControl(element) => {
688 element.text_control_element().queue_input_event(
689 None,
690 IsComposing::NotComposing,
691 InputEventType::DeleteByCut,
692 );
693 },
694 EditingContext::Document(..) => {},
695 }
696 }
697
698 pub(crate) fn insert_content(&self, cx: &mut JSContext, text_content: &str) {
699 match self {
700 EditingContext::TextControl(element) => {
701 element
702 .text_control_element()
703 .insert_content(cx, text_content);
704 },
705 EditingContext::Document(..) => {
706 },
708 }
709 }
710
711 pub(crate) fn fire_paste_events(&self, text_content: &str) {
712 match self {
713 EditingContext::TextControl(element) => {
714 element.text_control_element().queue_input_event(
715 Some(text_content.to_owned()),
716 IsComposing::NotComposing,
717 InputEventType::InsertFromPaste,
718 );
719 },
720 EditingContext::Document(..) => {},
721 }
722 }
723
724 pub(crate) fn select_all(&self, cx: &mut JSContext) {
725 match self {
726 EditingContext::TextControl(element) => element.text_control_element().select_all(),
727 EditingContext::Document(document) => {
728 let Some(selection) = document.GetSelection(cx) else {
729 return;
730 };
731 if let Some(node) = document
732 .GetBody()
733 .map(DomRoot::upcast::<Node>)
734 .or_else(|| document.GetDocumentElement().map(DomRoot::upcast::<Node>))
735 {
736 let _ = selection.SelectAllChildren(cx, &node);
737 }
738 },
739 }
740 }
741
742 pub(crate) fn perform_editing_action(&self, cx: &mut JSContext, action: EditingAction) -> bool {
743 if let Some(node) = self.event_target().downcast::<Node>() {
744 node.owner_window().layout_reflow(QueryMsg::StyleQuery);
749 if !node.is_being_rendered_or_delegates_rendering(None) {
750 return false;
751 }
752 }
753
754 if let EditingAction::Clipboard(clipboard_action) = action {
758 return self
759 .document()
760 .handle_clipboard_action(cx, self, clipboard_action)
761 .contains(InputEventResult::Consumed);
762 }
763
764 if let EditingAction::SelectAll = action {
765 self.select_all(cx);
766 return true;
767 }
768
769 match self {
770 EditingContext::TextControl(element) => element
771 .text_control_element()
772 .perform_editing_action(cx, action),
773 EditingContext::Document(document) => document.perform_editing_action(cx, &action),
774 }
775 }
776
777 pub(crate) fn handle_mousedown_event(
778 &self,
779 cx: &mut JSContext,
780 mouse_event: &MouseEvent,
781 hit_test_result: &HitTestResult,
782 ) {
783 match self {
784 EditingContext::TextControl(element) => {
785 element.handle_mousedown_event(mouse_event, hit_test_result)
786 },
787 EditingContext::Document(document) => {
788 document.handle_mousedown_event(cx, mouse_event, hit_test_result)
789 },
790 }
791 }
792}
793
794pub(crate) fn editing_action_from_keyboard_event(
795 event: &KeyboardTypesEvent,
796) -> Option<EditingAction> {
797 let mut modifiers = event.modifiers;
798 if let Some(action) = ShortcutMatcher::new(event.state, event.key.clone(), modifiers)
799 .shortcut(CMD_OR_CONTROL, 'A', || Some(EditingAction::SelectAll))
800 .shortcut(CMD_OR_CONTROL, 'C', || {
801 Some(EditingAction::Clipboard(ClipboardAction::Copy))
802 })
803 .shortcut(CMD_OR_CONTROL, 'X', || {
804 Some(EditingAction::Clipboard(ClipboardAction::Cut))
805 })
806 .shortcut(CMD_OR_CONTROL, 'V', || {
807 Some(EditingAction::Clipboard(ClipboardAction::Paste))
808 })
809 .shortcut(Modifiers::SHIFT, Key::Named(NamedKey::Enter), || {
813 Some(EditingAction::InsertNewline)
814 })
815 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
816 Some(EditingAction::InsertParagraph)
817 })
818 .otherwise(|| None)
819 .flatten()
820 {
821 return Some(action);
822 };
823
824 let update_selection = if modifiers.contains(Modifiers::SHIFT) {
825 ModifySelection::Yes
826 } else {
827 ModifySelection::No
828 };
829 modifiers.remove(Modifiers::SHIFT);
830
831 ShortcutMatcher::new(KeyState::Down, event.key.clone(), modifiers)
832 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
833 Some(EditingAction::MoveCursor(
834 EditingDirection::Backward,
835 EditingMotion::Grapheme,
836 update_selection,
837 ))
838 })
839 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
840 Some(EditingAction::MoveCursor(
841 EditingDirection::Forward,
842 EditingMotion::Grapheme,
843 update_selection,
844 ))
845 })
846 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
847 Some(EditingAction::MoveCursor(
848 EditingDirection::Backward,
849 EditingMotion::Line,
850 update_selection,
851 ))
852 })
853 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
854 Some(EditingAction::MoveCursor(
855 EditingDirection::Forward,
856 EditingMotion::Line,
857 update_selection,
858 ))
859 })
860 .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
861 Some(EditingAction::MoveCursor(
862 EditingDirection::Backward,
863 EditingMotion::Word,
864 update_selection,
865 ))
866 })
867 .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::ArrowLeft), || {
868 Some(EditingAction::MoveCursor(
869 EditingDirection::Backward,
870 EditingMotion::Word,
871 update_selection,
872 ))
873 })
874 .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
875 Some(EditingAction::MoveCursor(
876 EditingDirection::Forward,
877 EditingMotion::Word,
878 update_selection,
879 ))
880 })
881 .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::ArrowRight), || {
882 Some(EditingAction::MoveCursor(
883 EditingDirection::Forward,
884 EditingMotion::Word,
885 update_selection,
886 ))
887 })
888 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Home), || {
889 Some(EditingAction::MoveCursor(
890 EditingDirection::Backward,
891 EditingMotion::LineStartOrEnd,
892 update_selection,
893 ))
894 })
895 .optional_shortcut(cfg!(target_os = "macos"), Modifiers::CONTROL, 'A', || {
896 Some(EditingAction::MoveCursor(
897 EditingDirection::Backward,
898 EditingMotion::LineStartOrEnd,
899 update_selection,
900 ))
901 })
902 .shortcut(Modifiers::empty(), Key::Named(NamedKey::End), || {
903 Some(EditingAction::MoveCursor(
904 EditingDirection::Forward,
905 EditingMotion::LineStartOrEnd,
906 update_selection,
907 ))
908 })
909 .optional_shortcut(cfg!(target_os = "macos"), Modifiers::CONTROL, 'E', || {
910 Some(EditingAction::MoveCursor(
911 EditingDirection::Backward,
912 EditingMotion::LineStartOrEnd,
913 update_selection,
914 ))
915 })
916 .optional_shortcut(
917 cfg!(target_os = "macos"),
918 Modifiers::META,
919 Key::Named(NamedKey::ArrowLeft),
920 || {
921 Some(EditingAction::MoveCursor(
922 EditingDirection::Backward,
923 EditingMotion::LineStartOrEnd,
924 update_selection,
925 ))
926 },
927 )
928 .optional_shortcut(
929 cfg!(target_os = "macos"),
930 Modifiers::META,
931 Key::Named(NamedKey::ArrowRight),
932 || {
933 Some(EditingAction::MoveCursor(
934 EditingDirection::Forward,
935 EditingMotion::LineStartOrEnd,
936 update_selection,
937 ))
938 },
939 )
940 .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
941 Some(EditingAction::MoveCursor(
942 EditingDirection::Backward,
943 EditingMotion::Page,
944 update_selection,
945 ))
946 })
947 .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
948 Some(EditingAction::MoveCursor(
949 EditingDirection::Forward,
950 EditingMotion::Page,
951 update_selection,
952 ))
953 })
954 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
955 Some(EditingAction::Delete)
956 })
957 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
958 Some(EditingAction::Backspace(EditingMotion::Grapheme))
959 })
960 .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::Backspace), || {
961 Some(EditingAction::Backspace(EditingMotion::Word))
962 })
963 .otherwise(|| match &event.key {
964 Key::Character(character) if modifiers.is_empty() => {
965 Some(EditingAction::InsertText(character.to_string()))
966 },
967 _ => None,
968 })
969 .flatten()
970}
971
972pub(crate) fn editing_action_from_composition_event(event: &CompositionEvent) -> EditingAction {
973 match event.state {
974 CompositionState::Start => EditingAction::StartComposition,
975 CompositionState::Update => EditingAction::InsertCompositionText(event.data.to_string()),
976 CompositionState::End => EditingAction::EndComposition(event.data.to_string()),
977 }
978}