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, SegmentGranularity};
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, SelectionRange};
33use crate::dom::text_control::TextControlElement;
34use crate::dom::text_input::InputEventType;
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 DocumentDragSegment, 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 MouseButtonSelectionAction {
59 MoveCursor,
62 SelectRange(SegmentGranularity),
64}
65
66impl MouseButtonSelectionAction {
67 pub(crate) fn from_mouse_event(mouse_event: &MouseEvent) -> Option<Self> {
68 match mouse_event.button() {
69 MouseButton::Primary => {}, MouseButton::Auxiliary => return Some(Self::MoveCursor),
71 _ => return None,
72 }
73
74 let click_count = mouse_event.upcast::<UIEvent>().Detail();
83 match click_count {
84 1 => Some(Self::MoveCursor),
85 2 => Some(Self::SelectRange(SegmentGranularity::Word)),
86 3 => Some(Self::SelectRange(SegmentGranularity::HardLineBreak)),
87 _ => None,
88 }
89 }
90}
91
92impl Document {
93 pub(crate) fn editing_context(&self, no_gc: &NoGC, node: &Node) -> EditingContext {
94 if let Ok(editing_context) = EditingContext::try_from(node) {
95 return editing_context;
96 }
97
98 let mut current_node = UnrootedDom::from_ref(node, no_gc);
99 while let Some(parent) = current_node.parent_in_flat_tree(no_gc).into_parent() {
100 if let Ok(editing_context) = EditingContext::try_from(&**parent) {
101 return editing_context;
102 }
103 current_node = parent;
104 }
105
106 EditingContext::Document(DomRoot::from_ref(self))
107 }
108
109 pub(crate) fn handle_clipboard_action(
111 &self,
112 cx: &mut JSContext,
113 editing_context: &EditingContext,
114 action: ClipboardAction,
115 ) -> InputEventResult {
116 let clipboard_event_type = match action {
117 ClipboardAction::Copy => ClipboardEventType::Copy,
118 ClipboardAction::Cut => ClipboardEventType::Cut,
119 ClipboardAction::Paste => ClipboardEventType::Paste,
120 };
121
122 let script_triggered = false;
124
125 let script_may_access_clipboard = false;
129
130 if script_triggered && !script_may_access_clipboard {
132 return InputEventResult::empty();
133 }
134
135 let event_target = editing_context.event_target();
137 let clipboard_event = self.fire_clipboard_event(cx, &event_target, clipboard_event_type);
138
139 let event = clipboard_event.upcast::<Event>();
140 if !event.DefaultPrevented() {
141 match clipboard_event.clipboard_event_type() {
143 ClipboardEventType::Copy if editing_context.copying_enabled() => {
144 if let Some(selection) = editing_context.selection_content(cx) {
148 self.send_to_embedder(EmbedderMsg::SetClipboardText(
149 self.webview_id(),
150 selection,
151 ));
152 }
153 self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
155
156 event.mark_as_handled();
158 },
159 ClipboardEventType::Cut => {
160 if let Some(selection) = editing_context.selection_content(cx) &&
161 editing_context.cutting_enabled()
162 {
163 let canceled = editing_context.fire_cut_beforeinput_event(cx);
164 if canceled {
165 return InputEventResult::empty();
166 }
167
168 self.send_to_embedder(EmbedderMsg::SetClipboardText(
174 self.webview_id(),
175 selection,
176 ));
177
178 editing_context.remove_the_contents_of_the_selection(cx);
181
182 self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
184
185 editing_context.fire_cut_events();
189
190 event.mark_as_handled();
192 }
193 },
194 ClipboardEventType::Paste => {
195 if editing_context.has_selection_or_cursor() &&
196 editing_context.pasting_enabled() &&
197 let Some(text_content) = clipboard_event.text_content()
198 {
199 let canceled =
200 editing_context.fire_paste_beforeinput_event(cx, &text_content);
201 if canceled {
202 return InputEventResult::empty();
203 }
204
205 editing_context.insert_content(cx, &text_content);
210
211 editing_context.fire_paste_events(&text_content);
215
216 event.mark_as_handled();
218 }
219 },
220 _ => (),
221 }
222 } else {
223 match clipboard_event.clipboard_event_type() {
225 ClipboardEventType::Copy => {
226 if let Some(clipboard_data) = clipboard_event.clipboard_data() {
229 let drag_data_store =
230 clipboard_data.data_store().expect("This shouldn't fail");
231 self.write_content_to_the_clipboard(&drag_data_store);
232 }
233 },
234 ClipboardEventType::Cut => {
235 if let Some(clipboard_data) = clipboard_event.clipboard_data() {
238 let drag_data_store =
239 clipboard_data.data_store().expect("This shouldn't fail");
240 self.write_content_to_the_clipboard(&drag_data_store);
241 }
242
243 self.fire_clipboard_event(cx, &event_target, ClipboardEventType::Change);
245 },
246 ClipboardEventType::Paste => (),
248 _ => (),
249 }
250 }
251
252 event.flags().into()
255 }
256
257 fn fire_clipboard_event(
259 &self,
260 cx: &mut JSContext,
261 target: &EventTarget,
262 clipboard_event_type: ClipboardEventType,
263 ) -> DomRoot<ClipboardEvent> {
264 let clipboard_event = ClipboardEvent::new(
265 cx,
266 self.window(),
267 None,
268 clipboard_event_type,
269 EventBubbles::Bubbles,
270 EventCancelable::Cancelable,
271 None,
272 );
273
274 let mut drag_data_store = DragDataStore::new();
277
278 let trusted = true;
282
283 match clipboard_event.clipboard_event_type() {
287 ClipboardEventType::Copy | ClipboardEventType::Cut => {
288 drag_data_store.set_mode(Mode::ReadWrite);
290 },
291 ClipboardEventType::Paste => {
292 let (callback, receiver) =
293 GenericCallback::new_blocking().expect("Could not create callback");
294 self.send_to_embedder(EmbedderMsg::GetClipboardText(self.webview_id(), callback));
295 let text_contents = receiver
296 .recv()
297 .map(Result::unwrap_or_default)
298 .unwrap_or_default();
299
300 drag_data_store.set_mode(Mode::ReadOnly);
302 if trusted {
304 let data = DOMString::from(text_contents);
308 let type_ = DOMString::from_static("text/plain");
309 let _ = drag_data_store.add(Kind::Text { data, type_ });
310
311 }
317 },
318 ClipboardEventType::Change | ClipboardEventType::Other(..) => (),
319 }
320
321 let clipboard_event_data = DataTransfer::new(
323 cx,
324 self.window(),
325 Rc::new(RefCell::new(Some(drag_data_store))),
326 );
327
328 clipboard_event.set_clipboard_data(Some(&clipboard_event_data));
330
331 let event = clipboard_event.upcast::<Event>();
333 event.set_trusted(trusted);
334
335 event.set_composed(true);
337
338 event.dispatch(cx, target, false);
340
341 DomRoot::from(clipboard_event)
342 }
343
344 fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) {
346 if drag_data_store.list_len() > 0 {
348 self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id()));
350 for item in drag_data_store.iter_item_list() {
352 match item {
353 Kind::Text { data, .. } => {
354 self.send_to_embedder(EmbedderMsg::SetClipboardText(
358 self.webview_id(),
359 data.to_string(),
360 ));
361 },
362 Kind::File { .. } => {
363 },
367 }
368 }
369 } else {
370 if drag_data_store.clear_was_called {
372 self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id()));
374 }
377 }
378 }
379
380 pub(crate) fn perform_editing_action(
386 &self,
387 cx: &mut JSContext,
388 editing_action: &EditingAction,
389 ) -> bool {
390 if !servo_config::pref!(dom_exec_command_enabled) {
391 return false;
392 }
393
394 let (command, argument) = match editing_action {
397 EditingAction::MoveCursor(..) |
399 EditingAction::StartComposition |
400 EditingAction::EndComposition(..) => return false,
401 EditingAction::SelectAll | EditingAction::Clipboard(_) => {
402 unreachable!("Should have been handled before this point.")
403 },
404 EditingAction::InsertNewline => {
405 (DOMString::from_static("insertlinebreak"), DOMString::new())
411 },
412 EditingAction::InsertParagraph => {
413 (DOMString::from_static("insertparagraph"), DOMString::new())
418 },
419 EditingAction::Backspace(..) => (DOMString::from_static("delete"), DOMString::new()),
426 EditingAction::Delete => (DOMString::from_static("forwarddelete"), DOMString::new()),
431 EditingAction::InsertText(text) | EditingAction::InsertCompositionText(text) => (
439 DOMString::from_static("inserttext"),
440 DOMString::from(text.as_str()),
441 ),
442 };
443
444 self.exec_command_for_command_id(cx, command, argument)
445 }
446
447 fn handle_mousedown_event(
448 &self,
449 cx: &mut JSContext,
450 mouse_event: &MouseEvent,
451 hit_test_result: &HitTestResult,
452 ) {
453 assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
454
455 let Some(selection) = self.GetSelection(cx) else {
456 return;
457 };
458
459 if mouse_event.button() == MouseButton::Auxiliary {
460 let _ = selection.Collapse(cx, None, 0);
461 mouse_event.upcast::<Event>().mark_as_handled();
462 return;
463 }
464
465 if mouse_event.button() != MouseButton::Primary {
466 return;
467 }
468
469 let Some(action) = MouseButtonSelectionAction::from_mouse_event(mouse_event) else {
470 return;
471 };
472
473 let boundary = hit_test_result
477 .dom_position_for_selection
478 .clone()
479 .unwrap_or_else(|| RootedSelectionBoundary::start_of(&hit_test_result.node));
480 let Some((boundary, user_select_contain_node)) =
481 adjust_anchor_for_user_select(cx, boundary)
482 else {
483 return;
484 };
485
486 let segment_data = match action {
487 MouseButtonSelectionAction::MoveCursor => {
488 selection.collapse_to_dom_position(cx, &boundary);
489 None
490 },
491 MouseButtonSelectionAction::SelectRange(granularity) => self
492 .window()
493 .query_text_segment(&boundary.container, boundary.utf32_offset(), granularity)
494 .map(|(start, end)| {
495 selection.set_anchor_and_focus(cx, &start, &end);
496 (granularity, start, end)
497 }),
498 };
499
500 self.event_handler().install_drag_gesture(DragGesture::new(
501 DragHandler::DocumentSelection(DocumentSelectionDragHandler::new(
502 self,
503 user_select_contain_node.as_deref(),
504 segment_data.map(|(granularity, start, end)| DocumentDragSegment {
505 granularity,
506 range: SelectionRange::new_from_rooted(start, end),
507 }),
508 )),
509 ));
510
511 mouse_event.upcast::<Event>().mark_as_handled();
512 }
513}
514
515pub(crate) enum TextControlElementEditingContext {
516 TextArea(DomRoot<HTMLTextAreaElement>),
517 Input(DomRoot<HTMLInputElement>),
518}
519
520impl TextControlElementEditingContext {
521 fn text_control_element(&self) -> &dyn TextControlElement {
522 match self {
523 TextControlElementEditingContext::TextArea(text_area) => &**text_area,
524 TextControlElementEditingContext::Input(input) => &**input,
525 }
526 }
527
528 fn handle_mousedown_event(&self, mouse_event: &MouseEvent, hit_test_result: &HitTestResult) {
529 let text_control = self.text_control_element();
530 if text_control.text_input_mut().handle_mousedown_event(
531 text_control.as_element(),
532 mouse_event,
533 hit_test_result,
534 ) {
535 text_control.maybe_update_shared_selection();
536 mouse_event.upcast::<Event>().mark_as_handled();
537 }
538 }
539}
540
541pub(crate) enum EditingContext {
542 TextControl(TextControlElementEditingContext),
543 Document(DomRoot<Document>),
544}
545
546impl TryFrom<&Node> for EditingContext {
547 type Error = ();
548
549 fn try_from(node: &Node) -> Result<Self, Self::Error> {
550 if let Some(text_area) = node.downcast::<HTMLTextAreaElement>() {
551 return Ok(EditingContext::TextControl(
552 TextControlElementEditingContext::TextArea(DomRoot::from_ref(text_area)),
553 ));
554 }
555 if let Some(input) = node.downcast::<HTMLInputElement>() &&
556 input.is_textual_or_password()
557 {
558 return Ok(EditingContext::TextControl(
559 TextControlElementEditingContext::Input(DomRoot::from_ref(input)),
560 ));
561 }
562 Err(())
563 }
564}
565
566impl EditingContext {
567 pub(crate) fn event_target(&self) -> DomRoot<EventTarget> {
568 match self {
569 EditingContext::TextControl(element) => match element {
570 TextControlElementEditingContext::TextArea(text_area) => {
571 DomRoot::from_ref(text_area.upcast())
572 },
573 TextControlElementEditingContext::Input(input) => DomRoot::from_ref(input.upcast()),
574 },
575 EditingContext::Document(document) => {
576 document.event_handler().target_for_events_following_focus()
577 },
578 }
579 }
580
581 pub(crate) fn document(&self) -> DomRoot<Document> {
582 match self {
583 EditingContext::TextControl(element) => match element {
584 TextControlElementEditingContext::TextArea(element) => element.owner_document(),
585 TextControlElementEditingContext::Input(element) => element.owner_document(),
586 },
587 EditingContext::Document(document) => document.clone(),
588 }
589 }
590
591 pub(crate) fn selection_content(&self, cx: &mut JSContext) -> Option<String> {
592 match self {
593 EditingContext::TextControl(element) => element
594 .text_control_element()
595 .text_input()
596 .selection_content(),
597 EditingContext::Document(document) => document
598 .selection()
599 .map(|selection| selection.Stringifier(cx).to_string())
600 .filter(|selection| !selection.is_empty()),
601 }
602 }
603
604 pub(crate) fn has_uncollapsed_selection(&self) -> bool {
605 match self {
606 EditingContext::TextControl(element) => {
607 element.text_control_element().has_uncollapsed_selection()
608 },
609 EditingContext::Document(document) => document
610 .selection()
611 .is_some_and(|selection| !selection.collapsed()),
612 }
613 }
614
615 pub(crate) fn has_selectable_text(&self) -> bool {
616 match self {
617 EditingContext::TextControl(element) => {
618 element.text_control_element().has_selectable_text()
619 },
620 EditingContext::Document(..) => true,
621 }
622 }
623
624 pub(crate) fn has_selection_or_cursor(&self) -> bool {
625 match self {
626 EditingContext::TextControl(..) => true,
627 EditingContext::Document(document) => document
628 .selection()
629 .is_some_and(|selection| selection.RangeCount() > 0),
630 }
631 }
632
633 pub(crate) fn copying_enabled(&self) -> bool {
634 match self {
635 EditingContext::TextControl(element) => {
636 !element.text_control_element().is_password_field()
637 },
638 EditingContext::Document(..) => true,
639 }
640 }
641
642 pub(crate) fn cutting_enabled(&self) -> bool {
643 self.copying_enabled() && self.pasting_enabled()
646 }
647
648 pub(crate) fn pasting_enabled(&self) -> bool {
649 match self {
650 EditingContext::TextControl(element) => {
651 !element.text_control_element().read_only_or_disabled()
652 },
653 EditingContext::Document(..) => {
654 false
656 },
657 }
658 }
659
660 pub(crate) fn remove_the_contents_of_the_selection(&self, cx: &mut JSContext) {
661 match self {
662 EditingContext::TextControl(element) => {
663 element
664 .text_control_element()
665 .remove_the_contents_of_the_selection(cx);
666 },
667 EditingContext::Document(..) => {
668 },
670 }
671 }
672
673 pub(crate) fn fire_cut_beforeinput_event(&self, cx: &mut JSContext) -> bool {
675 match self {
676 EditingContext::TextControl(element) => element
677 .text_control_element()
678 .fire_beforeinput_event(cx, None, InputEventType::DeleteByCut),
679 EditingContext::Document(..) => false,
680 }
681 }
682
683 pub(crate) fn fire_paste_beforeinput_event(&self, cx: &mut JSContext, text: &str) -> bool {
685 match self {
686 EditingContext::TextControl(element) => element
687 .text_control_element()
688 .fire_beforeinput_event(cx, Some(text), InputEventType::InsertFromPaste),
689 EditingContext::Document(..) => false,
690 }
691 }
692
693 pub(crate) fn fire_cut_events(&self) {
694 match self {
695 EditingContext::TextControl(element) => {
696 element
697 .text_control_element()
698 .queue_input_event(None, InputEventType::DeleteByCut);
699 },
700 EditingContext::Document(..) => {},
701 }
702 }
703
704 pub(crate) fn insert_content(&self, cx: &mut JSContext, text_content: &str) {
705 match self {
706 EditingContext::TextControl(element) => {
707 element
708 .text_control_element()
709 .insert_content(cx, text_content);
710 },
711 EditingContext::Document(..) => {
712 },
714 }
715 }
716
717 pub(crate) fn fire_paste_events(&self, text_content: &str) {
718 match self {
719 EditingContext::TextControl(element) => {
720 element.text_control_element().queue_input_event(
721 Some(text_content.to_owned()),
722 InputEventType::InsertFromPaste,
723 );
724 },
725 EditingContext::Document(..) => {},
726 }
727 }
728
729 pub(crate) fn select_all(&self, cx: &mut JSContext) {
730 match self {
731 EditingContext::TextControl(element) => element.text_control_element().select_all(),
732 EditingContext::Document(document) => {
733 let Some(selection) = document.GetSelection(cx) else {
734 return;
735 };
736 if let Some(node) = document
737 .GetBody()
738 .map(DomRoot::upcast::<Node>)
739 .or_else(|| document.GetDocumentElement().map(DomRoot::upcast::<Node>))
740 {
741 let _ = selection.SelectAllChildren(cx, &node);
742 }
743 },
744 }
745 }
746
747 pub(crate) fn perform_editing_action(&self, cx: &mut JSContext, action: EditingAction) -> bool {
748 if let Some(node) = self.event_target().downcast::<Node>() {
749 node.owner_window().layout_reflow(cx, QueryMsg::StyleQuery);
754 if !node.is_being_rendered_or_delegates_rendering(None) {
755 return false;
756 }
757 }
758
759 if let EditingAction::Clipboard(clipboard_action) = action {
763 return self
764 .document()
765 .handle_clipboard_action(cx, self, clipboard_action)
766 .contains(InputEventResult::Consumed);
767 }
768
769 if let EditingAction::SelectAll = action {
770 self.select_all(cx);
771 return true;
772 }
773
774 match self {
775 EditingContext::TextControl(element) => element
776 .text_control_element()
777 .perform_editing_action(cx, action),
778 EditingContext::Document(document) => document.perform_editing_action(cx, &action),
779 }
780 }
781
782 pub(crate) fn handle_mousedown_event(
783 &self,
784 cx: &mut JSContext,
785 mouse_event: &MouseEvent,
786 hit_test_result: &HitTestResult,
787 ) {
788 match self {
789 EditingContext::TextControl(element) => {
790 element.handle_mousedown_event(mouse_event, hit_test_result)
791 },
792 EditingContext::Document(document) => {
793 document.handle_mousedown_event(cx, mouse_event, hit_test_result)
794 },
795 }
796 }
797}
798
799pub(crate) fn editing_action_from_keyboard_event(
800 event: &KeyboardTypesEvent,
801) -> Option<EditingAction> {
802 let mut modifiers = event.modifiers;
803 if let Some(action) = ShortcutMatcher::new(event.state, event.key.clone(), modifiers)
804 .shortcut(CMD_OR_CONTROL, 'A', || Some(EditingAction::SelectAll))
805 .shortcut(CMD_OR_CONTROL, 'C', || {
806 Some(EditingAction::Clipboard(ClipboardAction::Copy))
807 })
808 .shortcut(CMD_OR_CONTROL, 'X', || {
809 Some(EditingAction::Clipboard(ClipboardAction::Cut))
810 })
811 .shortcut(CMD_OR_CONTROL, 'V', || {
812 Some(EditingAction::Clipboard(ClipboardAction::Paste))
813 })
814 .shortcut(Modifiers::SHIFT, Key::Named(NamedKey::Enter), || {
818 Some(EditingAction::InsertNewline)
819 })
820 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
821 Some(EditingAction::InsertParagraph)
822 })
823 .otherwise(|| None)
824 .flatten()
825 {
826 return Some(action);
827 };
828
829 let update_selection = if modifiers.contains(Modifiers::SHIFT) {
830 ModifySelection::Yes
831 } else {
832 ModifySelection::No
833 };
834 modifiers.remove(Modifiers::SHIFT);
835
836 ShortcutMatcher::new(KeyState::Down, event.key.clone(), modifiers)
837 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
838 Some(EditingAction::MoveCursor(
839 EditingDirection::Backward,
840 EditingMotion::Grapheme,
841 update_selection,
842 ))
843 })
844 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
845 Some(EditingAction::MoveCursor(
846 EditingDirection::Forward,
847 EditingMotion::Grapheme,
848 update_selection,
849 ))
850 })
851 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
852 Some(EditingAction::MoveCursor(
853 EditingDirection::Backward,
854 EditingMotion::Line,
855 update_selection,
856 ))
857 })
858 .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
859 Some(EditingAction::MoveCursor(
860 EditingDirection::Forward,
861 EditingMotion::Line,
862 update_selection,
863 ))
864 })
865 .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
866 Some(EditingAction::MoveCursor(
867 EditingDirection::Backward,
868 EditingMotion::Word,
869 update_selection,
870 ))
871 })
872 .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::ArrowLeft), || {
873 Some(EditingAction::MoveCursor(
874 EditingDirection::Backward,
875 EditingMotion::Word,
876 update_selection,
877 ))
878 })
879 .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
880 Some(EditingAction::MoveCursor(
881 EditingDirection::Forward,
882 EditingMotion::Word,
883 update_selection,
884 ))
885 })
886 .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::ArrowRight), || {
887 Some(EditingAction::MoveCursor(
888 EditingDirection::Forward,
889 EditingMotion::Word,
890 update_selection,
891 ))
892 })
893 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Home), || {
894 Some(EditingAction::MoveCursor(
895 EditingDirection::Backward,
896 EditingMotion::LineStartOrEnd,
897 update_selection,
898 ))
899 })
900 .optional_shortcut(cfg!(target_os = "macos"), Modifiers::CONTROL, 'A', || {
901 Some(EditingAction::MoveCursor(
902 EditingDirection::Backward,
903 EditingMotion::LineStartOrEnd,
904 update_selection,
905 ))
906 })
907 .shortcut(Modifiers::empty(), Key::Named(NamedKey::End), || {
908 Some(EditingAction::MoveCursor(
909 EditingDirection::Forward,
910 EditingMotion::LineStartOrEnd,
911 update_selection,
912 ))
913 })
914 .optional_shortcut(cfg!(target_os = "macos"), Modifiers::CONTROL, 'E', || {
915 Some(EditingAction::MoveCursor(
916 EditingDirection::Backward,
917 EditingMotion::LineStartOrEnd,
918 update_selection,
919 ))
920 })
921 .optional_shortcut(
922 cfg!(target_os = "macos"),
923 Modifiers::META,
924 Key::Named(NamedKey::ArrowLeft),
925 || {
926 Some(EditingAction::MoveCursor(
927 EditingDirection::Backward,
928 EditingMotion::LineStartOrEnd,
929 update_selection,
930 ))
931 },
932 )
933 .optional_shortcut(
934 cfg!(target_os = "macos"),
935 Modifiers::META,
936 Key::Named(NamedKey::ArrowRight),
937 || {
938 Some(EditingAction::MoveCursor(
939 EditingDirection::Forward,
940 EditingMotion::LineStartOrEnd,
941 update_selection,
942 ))
943 },
944 )
945 .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
946 Some(EditingAction::MoveCursor(
947 EditingDirection::Backward,
948 EditingMotion::Page,
949 update_selection,
950 ))
951 })
952 .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
953 Some(EditingAction::MoveCursor(
954 EditingDirection::Forward,
955 EditingMotion::Page,
956 update_selection,
957 ))
958 })
959 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
960 Some(EditingAction::Delete)
961 })
962 .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
963 Some(EditingAction::Backspace(EditingMotion::Grapheme))
964 })
965 .shortcut(ALT_OR_CONTROL, Key::Named(NamedKey::Backspace), || {
966 Some(EditingAction::Backspace(EditingMotion::Word))
967 })
968 .optional_shortcut(
969 cfg!(target_os = "macos"),
970 Modifiers::META,
971 Key::Named(NamedKey::Backspace),
972 || Some(EditingAction::Backspace(EditingMotion::Line)),
973 )
974 .otherwise(|| match &event.key {
975 Key::Character(character) if modifiers.is_empty() => {
976 Some(EditingAction::InsertText(character.to_string()))
977 },
978 _ => None,
979 })
980 .flatten()
981}
982
983pub(crate) fn editing_action_from_composition_event(event: &CompositionEvent) -> EditingAction {
984 match event.state {
985 CompositionState::Start => EditingAction::StartComposition,
986 CompositionState::Update => EditingAction::InsertCompositionText(event.data.to_string()),
987 CompositionState::End => EditingAction::EndComposition(event.data.to_string()),
988 }
989}