Skip to main content

script/dom/html/form_controls/
text_input.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Common handling of keyboard input and state management for text input controls
6
7use std::default::Default;
8use std::ops::Range;
9
10use app_units::Au;
11use bitflags::bitflags;
12use embedder_traits::{EmbedderMsg, MouseButton, ScriptToEmbedderChan};
13use keyboard_types::{Key, KeyState, Modifiers, NamedKey, ShortcutMatcher};
14use script_bindings::codegen::GenericBindings::UIEventBinding::UIEventMethods;
15use script_bindings::match_domstring_ascii;
16use script_bindings::root::Dom;
17use script_bindings::trace::CustomTraceable;
18use script_traits::MouseButtons;
19use servo_base::generic_channel::GenericCallback;
20use servo_base::id::WebViewId;
21use servo_base::text::{Utf8CodeUnits, Utf16CodeUnits};
22use servo_base::{Rope, RopeIndex, RopeMovement, RopeSlice};
23
24use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
25use crate::dom::bindings::inheritance::Castable;
26use crate::dom::bindings::refcounted::Trusted;
27use crate::dom::bindings::reflector::DomGlobal;
28use crate::dom::bindings::str::DOMString;
29use crate::dom::compositionevent::CompositionEvent;
30use crate::dom::event::Event;
31use crate::dom::eventtarget::EventTarget;
32use crate::dom::inputevent::{HitTestResult, InputEvent};
33use crate::dom::keyboardevent::KeyboardEvent;
34use crate::dom::mouseevent::MouseEvent;
35use crate::dom::text_control::TextControlElement;
36use crate::dom::types::{ClipboardEvent, HTMLInputElement, HTMLTextAreaElement, UIEvent};
37use crate::dom::{Element, NodeTraits};
38use crate::drag::drag_data_store::Kind;
39use crate::drag::drag_gesture::{DragGesture, DragHandler};
40
41/// A trait which abstracts access to the embedder's clipboard in order to allow unit
42/// testing clipboard-dependent parts of `script`.
43pub trait ClipboardProvider {
44    /// Get the text content of the clipboard.
45    fn get_text(&mut self) -> Result<String, String>;
46    /// Set the text content of the clipboard.
47    fn set_text(&mut self, _: String);
48}
49
50#[derive(MallocSizeOf)]
51pub(crate) struct EmbedderClipboardProvider {
52    pub embedder_sender: ScriptToEmbedderChan,
53    pub webview_id: WebViewId,
54}
55
56impl ClipboardProvider for EmbedderClipboardProvider {
57    fn get_text(&mut self) -> Result<String, String> {
58        let (callback, rx) = GenericCallback::new_blocking().unwrap();
59        self.embedder_sender
60            .send(EmbedderMsg::GetClipboardText(self.webview_id, callback))
61            .unwrap();
62        rx.recv().unwrap()
63    }
64    fn set_text(&mut self, s: String) {
65        self.embedder_sender
66            .send(EmbedderMsg::SetClipboardText(self.webview_id, s))
67            .unwrap();
68    }
69}
70
71#[derive(Clone, Copy, PartialEq)]
72pub enum Selection {
73    Selected,
74    NotSelected,
75}
76
77#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
78pub enum SelectionDirection {
79    Forward,
80    Backward,
81    None,
82}
83
84impl From<DOMString> for SelectionDirection {
85    fn from(direction: DOMString) -> SelectionDirection {
86        match_domstring_ascii!(direction,
87            "forward" => SelectionDirection::Forward,
88            "backward" => SelectionDirection::Backward,
89            _ => SelectionDirection::None,
90        )
91    }
92}
93
94impl From<SelectionDirection> for DOMString {
95    fn from(direction: SelectionDirection) -> DOMString {
96        match direction {
97            SelectionDirection::Forward => DOMString::from("forward"),
98            SelectionDirection::Backward => DOMString::from("backward"),
99            SelectionDirection::None => DOMString::from("none"),
100        }
101    }
102}
103
104#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
105pub enum Lines {
106    Single,
107    Multiple,
108}
109
110impl Lines {
111    fn normalize(&self, contents: impl Into<String>) -> String {
112        let contents = contents.into().replace("\r\n", "\n");
113        match self {
114            Self::Multiple => {
115                // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
116                contents.replace("\r", "\n")
117            },
118            // https://infra.spec.whatwg.org/#strip-newlines
119            //
120            // Browsers generally seem to convert newlines to spaces, so we do the same.
121            Lines::Single => contents.replace(['\r', '\n'], " "),
122        }
123    }
124}
125
126#[derive(Clone, Copy, PartialEq)]
127pub(crate) struct SelectionState {
128    start: RopeIndex,
129    end: RopeIndex,
130    direction: SelectionDirection,
131}
132
133/// Encapsulated state for handling keyboard input in a single or multiline text input control.
134#[derive(JSTraceable, MallocSizeOf)]
135pub struct TextInput<T: ClipboardProvider> {
136    #[no_trace]
137    rope: Rope,
138
139    /// The type of [`TextInput`] this is. When in multi-line mode, the [`TextInput`] will
140    /// automatically split all inserted text into lines and incorporate them into
141    /// the [`Self::rope`]. When in single line mode, the inserted text will be stripped of
142    /// newlines.
143    mode: Lines,
144
145    /// Current cursor input point
146    #[no_trace]
147    edit_point: RopeIndex,
148
149    /// The current selection goes from the selection_origin until the edit_point. Note that the
150    /// selection_origin may be after the edit_point, in the case of a backward selection.
151    #[no_trace]
152    selection_origin: Option<RopeIndex>,
153    selection_direction: SelectionDirection,
154
155    #[ignore_malloc_size_of = "Can't easily measure this generic type"]
156    clipboard_provider: T,
157
158    /// The maximum number of UTF-16 code units this text input is allowed to hold.
159    ///
160    /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
161    max_length: Option<Utf16CodeUnits>,
162    min_length: Option<Utf16CodeUnits>,
163
164    /// Was last change made by set_content?
165    was_last_change_by_set_content: bool,
166}
167
168#[derive(Clone, Copy, PartialEq)]
169pub enum IsComposing {
170    Composing,
171    NotComposing,
172}
173
174impl From<IsComposing> for bool {
175    fn from(is_composing: IsComposing) -> Self {
176        match is_composing {
177            IsComposing::Composing => true,
178            IsComposing::NotComposing => false,
179        }
180    }
181}
182
183/// <https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes>
184#[derive(Clone, Copy, PartialEq)]
185pub enum InputType {
186    InsertText,
187    InsertLineBreak,
188    InsertFromPaste,
189    InsertCompositionText,
190    DeleteByCut,
191    DeleteContentBackward,
192    DeleteContentForward,
193    Nothing,
194}
195
196impl InputType {
197    fn as_str(&self) -> &str {
198        match *self {
199            InputType::InsertText => "insertText",
200            InputType::InsertLineBreak => "insertLineBreak",
201            InputType::InsertFromPaste => "insertFromPaste",
202            InputType::InsertCompositionText => "insertCompositionText",
203            InputType::DeleteByCut => "deleteByCut",
204            InputType::DeleteContentBackward => "deleteContentBackward",
205            InputType::DeleteContentForward => "deleteContentForward",
206            InputType::Nothing => "",
207        }
208    }
209}
210
211/// Resulting action to be taken by the owner of a text input that is handling an event.
212pub enum KeyReaction {
213    TriggerDefaultAction,
214    DispatchInput(Option<String>, IsComposing, InputType),
215    RedrawSelection,
216    Nothing,
217}
218
219bitflags! {
220    /// Resulting action to be taken by the owner of a text input that is handling a clipboard
221    /// event.
222    #[derive(Clone, Copy)]
223    pub struct ClipboardEventFlags: u8 {
224        const QueueInputEvent = 1 << 0;
225        const FireClipboardChangedEvent = 1 << 1;
226    }
227}
228
229pub struct ClipboardEventReaction {
230    pub flags: ClipboardEventFlags,
231    pub text: Option<String>,
232    pub input_type: InputType,
233}
234
235impl ClipboardEventReaction {
236    fn new(flags: ClipboardEventFlags) -> Self {
237        Self {
238            flags,
239            text: None,
240            input_type: InputType::Nothing,
241        }
242    }
243
244    fn with_text(mut self, text: String) -> Self {
245        self.text = Some(text);
246        self
247    }
248
249    fn with_input_type(mut self, input_type: InputType) -> Self {
250        self.input_type = input_type;
251        self
252    }
253
254    fn empty() -> Self {
255        Self::new(ClipboardEventFlags::empty())
256    }
257}
258
259/// The direction in which to delete a character.
260#[derive(Clone, Copy, Eq, PartialEq)]
261pub enum Direction {
262    Forward,
263    Backward,
264}
265
266// Some shortcuts use Cmd on Mac and Control on other systems.
267#[cfg(target_os = "macos")]
268pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::META;
269#[cfg(not(target_os = "macos"))]
270pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
271
272/// The length in bytes of the first n code units in a string when encoded in UTF-16.
273///
274/// If the string is fewer than n code units, returns the length of the whole string.
275fn len_of_first_n_code_units(text: &DOMString, n: Utf16CodeUnits) -> Utf8CodeUnits {
276    let mut utf8_len = Utf8CodeUnits::zero();
277    let mut utf16_len = Utf16CodeUnits::zero();
278    for c in text.str().chars() {
279        utf16_len += Utf16CodeUnits(c.len_utf16());
280        if utf16_len > n {
281            break;
282        }
283        utf8_len += Utf8CodeUnits(c.len_utf8());
284    }
285    utf8_len
286}
287
288impl<T: ClipboardProvider> TextInput<T> {
289    /// Instantiate a new text input control
290    pub fn new(lines: Lines, initial: DOMString, clipboard_provider: T) -> TextInput<T> {
291        Self {
292            rope: Rope::new(initial),
293            mode: lines,
294            edit_point: Default::default(),
295            selection_origin: None,
296            clipboard_provider,
297            max_length: Default::default(),
298            min_length: Default::default(),
299            selection_direction: SelectionDirection::None,
300            was_last_change_by_set_content: true,
301        }
302    }
303
304    pub fn edit_point(&self) -> RopeIndex {
305        self.edit_point
306    }
307
308    pub fn selection_origin(&self) -> Option<RopeIndex> {
309        self.selection_origin
310    }
311
312    /// The selection origin, or the edit point if there is no selection. Note that the selection
313    /// origin may be after the edit point, in the case of a backward selection.
314    pub fn selection_origin_or_edit_point(&self) -> RopeIndex {
315        self.selection_origin.unwrap_or(self.edit_point)
316    }
317
318    pub fn selection_direction(&self) -> SelectionDirection {
319        self.selection_direction
320    }
321
322    pub fn set_max_length(&mut self, length: Option<Utf16CodeUnits>) {
323        self.max_length = length;
324    }
325
326    pub fn set_min_length(&mut self, length: Option<Utf16CodeUnits>) {
327        self.min_length = length;
328    }
329
330    /// Was last edit made by set_content?
331    pub(crate) fn was_last_change_by_set_content(&self) -> bool {
332        self.was_last_change_by_set_content
333    }
334
335    /// If there is an uncollapsed selection, delete it, otherwise do nothing. Returns
336    /// true if any text was deleted.
337    fn delete_selection(&mut self) -> bool {
338        if self.selection_start() == self.selection_end() {
339            return false;
340        }
341        self.replace_selection(&DOMString::new());
342        true
343    }
344
345    /// If there is an uncollapsed selection, delete it. Otherwise delete the given [`unit`]
346    /// worth of text in [`direction`] Remove a character at the current editing point
347    ///
348    /// Returns true if any text was deleted.
349    pub fn delete_unit_or_selection(&mut self, unit: RopeMovement, direction: Direction) -> bool {
350        if !self.has_uncollapsed_selection() {
351            let amount = match direction {
352                Direction::Forward => 1,
353                Direction::Backward => -1,
354            };
355            self.modify_selection(amount, unit);
356        }
357        self.delete_selection()
358    }
359
360    /// Insert a string at the current editing point or replace the selection if
361    /// one exists.
362    pub fn insert<S: Into<String>>(&mut self, string: S) {
363        if self.selection_origin.is_none() {
364            self.selection_origin = Some(self.edit_point);
365        }
366        self.replace_selection(&DOMString::from(string.into()));
367    }
368
369    /// The start of the selection (or the edit point, if there is no selection). Always less than
370    /// or equal to selection_end(), regardless of the selection direction.
371    pub fn selection_start(&self) -> RopeIndex {
372        match self.selection_direction {
373            SelectionDirection::None | SelectionDirection::Forward => {
374                self.selection_origin_or_edit_point()
375            },
376            SelectionDirection::Backward => self.edit_point,
377        }
378    }
379
380    pub(crate) fn selection_start_utf16(&self) -> Utf16CodeUnits {
381        self.rope.index_to_utf16_offset(self.selection_start())
382    }
383
384    /// The byte offset of the selection_start()
385    fn selection_start_offset(&self) -> Utf8CodeUnits {
386        self.rope.index_to_utf8_offset(self.selection_start())
387    }
388
389    /// The end of the selection (or the edit point, if there is no selection). Always greater
390    /// than or equal to selection_start(), regardless of the selection direction.
391    pub fn selection_end(&self) -> RopeIndex {
392        match self.selection_direction {
393            SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
394            SelectionDirection::Backward => self.selection_origin_or_edit_point(),
395        }
396    }
397
398    pub(crate) fn selection_end_utf16(&self) -> Utf16CodeUnits {
399        self.rope.index_to_utf16_offset(self.selection_end())
400    }
401
402    /// The byte offset of the selection_end()
403    pub fn selection_end_offset(&self) -> Utf8CodeUnits {
404        self.rope.index_to_utf8_offset(self.selection_end())
405    }
406
407    /// Whether or not there is an active uncollapsed selection. This means that the
408    /// selection origin is set and it differs from the edit point.
409    #[inline]
410    pub(crate) fn has_uncollapsed_selection(&self) -> bool {
411        self.selection_origin
412            .is_some_and(|selection_origin| selection_origin != self.edit_point)
413    }
414
415    /// Return the selection range as byte offsets from the start of the content.
416    ///
417    /// If there is no selection, returns an empty range at the edit point.
418    pub(crate) fn sorted_selection_offsets_range(&self) -> Range<Utf8CodeUnits> {
419        self.selection_start_offset()..self.selection_end_offset()
420    }
421
422    /// Return the selection range as character offsets from the start of the content.
423    ///
424    /// If there is no selection, returns an empty range at the edit point.
425    pub(crate) fn sorted_selection_character_offsets_range(&self) -> Range<usize> {
426        self.rope.index_to_character_offset(self.selection_start())..
427            self.rope.index_to_character_offset(self.selection_end())
428    }
429
430    /// The state of the current selection. Can be used to compare whether selection state has changed.
431    pub(crate) fn selection_state(&self) -> SelectionState {
432        SelectionState {
433            start: self.selection_start(),
434            end: self.selection_end(),
435            direction: self.selection_direction,
436        }
437    }
438
439    // Check that the selection is valid.
440    fn assert_ok_selection(&self) {
441        debug!(
442            "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
443            self.edit_point, self.selection_origin, self.selection_direction
444        );
445
446        debug_assert_eq!(self.edit_point, self.rope.normalize_index(self.edit_point));
447        if let Some(selection_origin) = self.selection_origin {
448            debug_assert_eq!(
449                selection_origin,
450                self.rope.normalize_index(selection_origin)
451            );
452            match self.selection_direction {
453                SelectionDirection::None | SelectionDirection::Forward => {
454                    debug_assert!(selection_origin <= self.edit_point)
455                },
456                SelectionDirection::Backward => debug_assert!(self.edit_point <= selection_origin),
457            }
458        }
459    }
460
461    fn selection_slice(&self) -> RopeSlice<'_> {
462        self.rope
463            .slice(Some(self.selection_start()), Some(self.selection_end()))
464    }
465
466    pub(crate) fn get_selection_text(&self) -> Option<String> {
467        let text: String = self.selection_slice().into();
468        if text.is_empty() {
469            return None;
470        }
471        Some(text)
472    }
473
474    /// The length of the selected text in UTF-16 code units.
475    fn selection_utf16_len(&self) -> Utf16CodeUnits {
476        Utf16CodeUnits(
477            self.selection_slice()
478                .chars()
479                .map(char::len_utf16)
480                .sum::<usize>(),
481        )
482    }
483
484    /// Replace the current selection with the given [`DOMString`]. If the [`Rope`] is in
485    /// single line mode this *will* strip newlines, as opposed to [`Self::set_content`],
486    /// which does not.
487    pub fn replace_selection(&mut self, insert: &DOMString) {
488        let string_to_insert = if let Some(max_length) = self.max_length {
489            let utf16_length_without_selection =
490                self.len_utf16().saturating_sub(self.selection_utf16_len());
491            let utf16_length_that_can_be_inserted =
492                max_length.saturating_sub(utf16_length_without_selection);
493            let Utf8CodeUnits(last_char_index) =
494                len_of_first_n_code_units(insert, utf16_length_that_can_be_inserted);
495            &insert.str()[..last_char_index]
496        } else {
497            &insert.str()
498        };
499        let string_to_insert = self.mode.normalize(string_to_insert);
500
501        let start = self.selection_start();
502        let end = self.selection_end();
503        let end_index_of_insertion = self.rope.replace_range(start..end, string_to_insert);
504
505        self.was_last_change_by_set_content = false;
506        self.clear_selection();
507        self.edit_point = end_index_of_insertion;
508    }
509
510    pub fn modify_edit_point(&mut self, amount: isize, movement: RopeMovement) {
511        if amount == 0 {
512            return;
513        }
514
515        // When moving by lines or if we do not have a selection, we do actually move
516        // the edit point from its position.
517        if matches!(movement, RopeMovement::Line) || !self.has_uncollapsed_selection() {
518            self.clear_selection();
519            self.edit_point = self.rope.move_by(self.edit_point, movement, amount);
520            return;
521        }
522
523        // If there's a selection and we are moving by words or characters, we just collapse
524        // the selection in the direction of the motion.
525        let new_edit_point = if amount > 0 {
526            self.selection_end()
527        } else {
528            self.selection_start()
529        };
530        self.clear_selection();
531        self.edit_point = new_edit_point;
532    }
533
534    pub fn modify_selection(&mut self, amount: isize, movement: RopeMovement) {
535        let old_edit_point = self.edit_point;
536        self.edit_point = self.rope.move_by(old_edit_point, movement, amount);
537
538        if self.selection_origin.is_none() {
539            self.selection_origin = Some(old_edit_point);
540        }
541        self.update_selection_direction();
542    }
543
544    pub fn modify_selection_or_edit_point(
545        &mut self,
546        amount: isize,
547        movement: RopeMovement,
548        select: Selection,
549    ) {
550        match select {
551            Selection::Selected => self.modify_selection(amount, movement),
552            Selection::NotSelected => self.modify_edit_point(amount, movement),
553        }
554        self.assert_ok_selection();
555    }
556
557    /// Update the field selection_direction.
558    ///
559    /// When the edit_point (or focus) is before the selection_origin (or anchor)
560    /// you have a backward selection. Otherwise you have a forward selection.
561    fn update_selection_direction(&mut self) {
562        debug!(
563            "edit_point: {:?}, selection_origin: {:?}",
564            self.edit_point, self.selection_origin
565        );
566        self.selection_direction = if Some(self.edit_point) < self.selection_origin {
567            SelectionDirection::Backward
568        } else {
569            SelectionDirection::Forward
570        }
571    }
572
573    /// Deal with a newline input.
574    pub fn handle_return(&mut self) -> KeyReaction {
575        match self.mode {
576            Lines::Multiple => {
577                self.insert('\n');
578                KeyReaction::DispatchInput(
579                    None,
580                    IsComposing::NotComposing,
581                    InputType::InsertLineBreak,
582                )
583            },
584            Lines::Single => KeyReaction::TriggerDefaultAction,
585        }
586    }
587
588    /// Select all text in the input control.
589    pub fn select_all(&mut self) {
590        self.selection_origin = Some(RopeIndex::default());
591        self.edit_point = self.rope.last_index();
592        self.selection_direction = SelectionDirection::Forward;
593        self.assert_ok_selection();
594    }
595
596    /// Remove the current selection.
597    pub fn clear_selection(&mut self) {
598        self.selection_origin = None;
599        self.selection_direction = SelectionDirection::None;
600    }
601
602    /// Remove the current selection and set the edit point to the end of the content.
603    pub(crate) fn clear_selection_to_end(&mut self) {
604        self.clear_selection();
605        self.edit_point = self.rope.last_index();
606    }
607
608    pub(crate) fn clear_selection_to_start(&mut self) {
609        self.clear_selection();
610        self.edit_point = Default::default();
611    }
612
613    /// Process a given `KeyboardEvent` and return an action for the caller to execute.
614    pub(crate) fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
615        let key = event.key();
616        let mods = event.modifiers();
617        self.handle_keydown_aux(key, mods, cfg!(target_os = "macos"))
618    }
619
620    // This function exists for easy unit testing.
621    // To test Mac OS shortcuts on other systems a flag is passed.
622    pub fn handle_keydown_aux(
623        &mut self,
624        key: Key,
625        mut mods: Modifiers,
626        macos: bool,
627    ) -> KeyReaction {
628        let maybe_select = if mods.contains(Modifiers::SHIFT) {
629            Selection::Selected
630        } else {
631            Selection::NotSelected
632        };
633
634        let alt_or_control = if macos {
635            Modifiers::ALT
636        } else {
637            Modifiers::CONTROL
638        };
639
640        mods.remove(Modifiers::SHIFT);
641        ShortcutMatcher::new(KeyState::Down, key.clone(), mods)
642            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
643                self.modify_selection_or_edit_point(-1, RopeMovement::Word, maybe_select);
644                KeyReaction::RedrawSelection
645            })
646            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
647                self.modify_selection_or_edit_point(1, RopeMovement::Word, maybe_select);
648                KeyReaction::RedrawSelection
649            })
650            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'A', || {
651                self.modify_selection_or_edit_point(-1, RopeMovement::LineStartOrEnd, maybe_select);
652                KeyReaction::RedrawSelection
653            })
654            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'E', || {
655                self.modify_selection_or_edit_point(1, RopeMovement::LineStartOrEnd, maybe_select);
656                KeyReaction::RedrawSelection
657            })
658            .optional_shortcut(macos, Modifiers::CONTROL, 'A', || {
659                self.modify_selection_or_edit_point(-1, RopeMovement::LineStartOrEnd, maybe_select);
660                KeyReaction::RedrawSelection
661            })
662            .optional_shortcut(macos, Modifiers::CONTROL, 'E', || {
663                self.modify_selection_or_edit_point(1, RopeMovement::LineStartOrEnd, maybe_select);
664                KeyReaction::RedrawSelection
665            })
666            .shortcut(CMD_OR_CONTROL, 'A', || {
667                self.select_all();
668                KeyReaction::RedrawSelection
669            })
670            .shortcut(CMD_OR_CONTROL, 'X', || {
671                if let Some(text) = self.get_selection_text() {
672                    self.clipboard_provider.set_text(text);
673                    self.delete_selection();
674                }
675                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::DeleteByCut)
676            })
677            .shortcut(CMD_OR_CONTROL, 'C', || {
678                // TODO(stevennovaryo): we should not provide text to clipboard for type=password
679                if let Some(text) = self.get_selection_text() {
680                    self.clipboard_provider.set_text(text);
681                }
682                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::Nothing)
683            })
684            .shortcut(CMD_OR_CONTROL, 'V', || {
685                if let Ok(text_content) = self.clipboard_provider.get_text() {
686                    self.insert(&text_content);
687                    KeyReaction::DispatchInput(
688                        Some(text_content),
689                        IsComposing::NotComposing,
690                        InputType::InsertFromPaste,
691                    )
692                } else {
693                    KeyReaction::DispatchInput(
694                        Some("".to_string()),
695                        IsComposing::NotComposing,
696                        InputType::InsertFromPaste,
697                    )
698                }
699            })
700            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
701                if self.delete_unit_or_selection(RopeMovement::Grapheme, Direction::Forward) {
702                    KeyReaction::DispatchInput(
703                        None,
704                        IsComposing::NotComposing,
705                        InputType::DeleteContentForward,
706                    )
707                } else {
708                    KeyReaction::Nothing
709                }
710            })
711            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
712                if self.delete_unit_or_selection(RopeMovement::Grapheme, Direction::Backward) {
713                    KeyReaction::DispatchInput(
714                        None,
715                        IsComposing::NotComposing,
716                        InputType::DeleteContentBackward,
717                    )
718                } else {
719                    KeyReaction::Nothing
720                }
721            })
722            .shortcut(alt_or_control, Key::Named(NamedKey::Backspace), || {
723                if self.delete_unit_or_selection(RopeMovement::Word, Direction::Backward) {
724                    KeyReaction::DispatchInput(
725                        None,
726                        IsComposing::NotComposing,
727                        InputType::DeleteContentBackward,
728                    )
729                } else {
730                    KeyReaction::Nothing
731                }
732            })
733            .optional_shortcut(
734                macos,
735                Modifiers::META,
736                Key::Named(NamedKey::ArrowLeft),
737                || {
738                    self.modify_selection_or_edit_point(
739                        -1,
740                        RopeMovement::LineStartOrEnd,
741                        maybe_select,
742                    );
743                    KeyReaction::RedrawSelection
744                },
745            )
746            .optional_shortcut(
747                macos,
748                Modifiers::META,
749                Key::Named(NamedKey::ArrowRight),
750                || {
751                    self.modify_selection_or_edit_point(
752                        1,
753                        RopeMovement::LineStartOrEnd,
754                        maybe_select,
755                    );
756                    KeyReaction::RedrawSelection
757                },
758            )
759            .optional_shortcut(
760                macos,
761                Modifiers::META,
762                Key::Named(NamedKey::ArrowUp),
763                || {
764                    self.modify_selection_or_edit_point(
765                        -1,
766                        RopeMovement::RopeStartOrEnd,
767                        maybe_select,
768                    );
769                    KeyReaction::RedrawSelection
770                },
771            )
772            .optional_shortcut(
773                macos,
774                Modifiers::META,
775                Key::Named(NamedKey::ArrowDown),
776                || {
777                    self.modify_selection_or_edit_point(
778                        1,
779                        RopeMovement::RopeStartOrEnd,
780                        maybe_select,
781                    );
782                    KeyReaction::RedrawSelection
783                },
784            )
785            .shortcut(alt_or_control, Key::Named(NamedKey::ArrowLeft), || {
786                self.modify_selection_or_edit_point(-1, RopeMovement::Word, maybe_select);
787                KeyReaction::RedrawSelection
788            })
789            .shortcut(alt_or_control, Key::Named(NamedKey::ArrowRight), || {
790                self.modify_selection_or_edit_point(1, RopeMovement::Word, maybe_select);
791                KeyReaction::RedrawSelection
792            })
793            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
794                self.modify_selection_or_edit_point(-1, RopeMovement::Grapheme, maybe_select);
795                KeyReaction::RedrawSelection
796            })
797            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
798                self.modify_selection_or_edit_point(1, RopeMovement::Grapheme, maybe_select);
799                KeyReaction::RedrawSelection
800            })
801            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
802                self.modify_selection_or_edit_point(-1, RopeMovement::Line, maybe_select);
803                KeyReaction::RedrawSelection
804            })
805            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
806                self.modify_selection_or_edit_point(1, RopeMovement::Line, maybe_select);
807                KeyReaction::RedrawSelection
808            })
809            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
810                self.handle_return()
811            })
812            .optional_shortcut(
813                macos,
814                Modifiers::empty(),
815                Key::Named(NamedKey::Home),
816                || {
817                    self.modify_selection_or_edit_point(
818                        -1,
819                        RopeMovement::RopeStartOrEnd,
820                        maybe_select,
821                    );
822                    KeyReaction::RedrawSelection
823                },
824            )
825            .optional_shortcut(macos, Modifiers::empty(), Key::Named(NamedKey::End), || {
826                self.modify_selection_or_edit_point(1, RopeMovement::RopeStartOrEnd, maybe_select);
827                KeyReaction::RedrawSelection
828            })
829            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
830                self.modify_selection_or_edit_point(-28, RopeMovement::Line, maybe_select);
831                KeyReaction::RedrawSelection
832            })
833            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
834                self.modify_selection_or_edit_point(28, RopeMovement::Line, maybe_select);
835                KeyReaction::RedrawSelection
836            })
837            .otherwise(|| {
838                if let Key::Character(ref character) = key {
839                    self.insert(character);
840                    return KeyReaction::DispatchInput(
841                        Some(character.to_string()),
842                        IsComposing::NotComposing,
843                        InputType::InsertText,
844                    );
845                }
846                if matches!(key, Key::Named(NamedKey::Process)) {
847                    return KeyReaction::DispatchInput(
848                        None,
849                        IsComposing::Composing,
850                        InputType::Nothing,
851                    );
852                }
853                KeyReaction::Nothing
854            })
855            .unwrap()
856    }
857
858    pub(crate) fn handle_compositionend(&mut self, event: &CompositionEvent) -> KeyReaction {
859        let insertion = event.data().str();
860        if insertion.is_empty() {
861            self.clear_selection();
862            return KeyReaction::RedrawSelection;
863        }
864
865        self.insert(insertion.to_string());
866        KeyReaction::DispatchInput(
867            Some(insertion.to_string()),
868            IsComposing::NotComposing,
869            InputType::InsertCompositionText,
870        )
871    }
872
873    pub(crate) fn handle_compositionupdate(&mut self, event: &CompositionEvent) -> KeyReaction {
874        let insertion = event.data().str();
875        if insertion.is_empty() {
876            return KeyReaction::Nothing;
877        }
878
879        let start = self.selection_start_offset();
880        let insertion = insertion.to_string();
881        self.insert(insertion.clone());
882        self.set_selection_range_utf8(
883            start,
884            start + event.data().len_utf8(),
885            SelectionDirection::Forward,
886        );
887        KeyReaction::DispatchInput(
888            Some(insertion),
889            IsComposing::Composing,
890            InputType::InsertCompositionText,
891        )
892    }
893
894    fn edit_point_for_hit_test_result(&self, hit_test_result: &HitTestResult) -> RopeIndex {
895        hit_test_result
896            .dom_position_for_selection
897            .as_ref()
898            .map(|(_, character_offset)| {
899                self.rope.move_by(
900                    Default::default(),
901                    RopeMovement::Character,
902                    character_offset.0 as isize,
903                )
904            })
905            .unwrap_or_else(|| self.rope.last_index())
906    }
907
908    fn drag_moved(&mut self, element: &impl TextControlElement, hit_test_result: &HitTestResult) {
909        let point_in_viewport = hit_test_result.point_in_frame.map(Au::from_f32_px);
910        self.edit_point = element
911            .owner_window()
912            .text_index_query_on_node_for_event(element.upcast(), point_in_viewport)
913            .map(|(_, character_offset)| {
914                self.rope.move_by(
915                    Default::default(),
916                    RopeMovement::Character,
917                    character_offset.0 as isize,
918                )
919            })
920            .unwrap_or_else(|| self.rope.last_index());
921
922        self.update_selection_direction();
923    }
924
925    /// Handle a "mousedown" event that happened on this [`TextInput`], belonging to the
926    /// given [`Node`].
927    ///
928    /// Returns `true` if the [`TextInput`] changed at all or `false` otherwise.
929    pub(crate) fn handle_mousedown_event(
930        &mut self,
931        element: &Element,
932        mouse_event: &MouseEvent,
933        hit_test_result: &HitTestResult,
934    ) -> bool {
935        assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
936
937        let button = mouse_event.button();
938        let selection_changed = match mouse_event.upcast::<UIEvent>().Detail() {
939            3 if button == MouseButton::Primary => {
940                let word_boundaries = self.rope.line_boundaries(self.edit_point);
941                self.edit_point = word_boundaries.end;
942                self.selection_origin = Some(word_boundaries.start);
943                self.update_selection_direction();
944                true
945            },
946            2 if button == MouseButton::Primary => {
947                let word_boundaries = self.rope.relevant_word_boundaries(self.edit_point);
948                self.edit_point = word_boundaries.end;
949                self.selection_origin = Some(word_boundaries.start);
950                self.update_selection_direction();
951                true
952            },
953            1 if matches!(button, MouseButton::Primary | MouseButton::Auxiliary) => {
954                self.clear_selection();
955                self.edit_point = self.edit_point_for_hit_test_result(hit_test_result);
956                self.selection_origin = Some(self.edit_point);
957                self.update_selection_direction();
958                true
959            },
960            _ => {
961                // We currently don't do anything for higher click counts, but some platforms do.
962                // We should re-examine this when implementing support for platform-specific editing
963                // behaviors.
964                false
965            },
966        };
967
968        if selection_changed && mouse_event.buttons().contains(MouseButtons::Primary) {
969            element
970                .owner_document()
971                .event_handler()
972                .install_drag_gesture(DragGesture::new(DragHandler::TextInputSelection(
973                    TextInputSelectionDragHandler(Dom::from_ref(element)),
974                )));
975        }
976
977        selection_changed
978    }
979
980    /// Whether the content is empty.
981    pub(crate) fn is_empty(&self) -> bool {
982        self.rope.is_empty()
983    }
984
985    /// The total number of code units required to encode the content in utf16.
986    pub(crate) fn len_utf16(&self) -> Utf16CodeUnits {
987        self.rope.len_utf16()
988    }
989
990    /// Get the current contents of the text input. Multiple lines are joined by \n.
991    pub fn get_content(&self) -> DOMString {
992        self.rope.contents().into()
993    }
994
995    /// Set the current contents of the text input. If this is control supports multiple lines,
996    /// any \n encountered will be stripped and force a new logical line.
997    ///
998    /// Note that when the [`Rope`] is in single line mode, this will **not** strip newlines.
999    /// Newline stripping only happens for incremental updates to the [`Rope`] as `<input>`
1000    /// elements currently need to store unsanitized values while being created.
1001    pub fn set_content(&mut self, content: DOMString) {
1002        self.rope = Rope::new(content.str().replace("\r\n", "\n").replace("\r", "\n"));
1003        self.was_last_change_by_set_content = true;
1004
1005        self.edit_point = self.rope.normalize_index(self.edit_point());
1006        self.selection_origin = self
1007            .selection_origin
1008            .map(|selection_origin| self.rope.normalize_index(selection_origin));
1009    }
1010
1011    pub fn set_selection_range_utf16(
1012        &mut self,
1013        start: Utf16CodeUnits,
1014        end: Utf16CodeUnits,
1015        direction: SelectionDirection,
1016    ) {
1017        self.set_selection_range_utf8(
1018            self.rope.utf16_offset_to_utf8_offset(start),
1019            self.rope.utf16_offset_to_utf8_offset(end),
1020            direction,
1021        );
1022    }
1023
1024    pub fn set_selection_range_utf8(
1025        &mut self,
1026        mut start: Utf8CodeUnits,
1027        mut end: Utf8CodeUnits,
1028        direction: SelectionDirection,
1029    ) {
1030        let text_end = self.get_content().len_utf8();
1031        if end > text_end {
1032            end = text_end;
1033        }
1034        if start > end {
1035            start = end;
1036        }
1037
1038        self.selection_direction = direction;
1039
1040        match direction {
1041            SelectionDirection::None | SelectionDirection::Forward => {
1042                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(start));
1043                self.edit_point = self.rope.utf8_offset_to_rope_index(end);
1044            },
1045            SelectionDirection::Backward => {
1046                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(end));
1047                self.edit_point = self.rope.utf8_offset_to_rope_index(start);
1048            },
1049        }
1050
1051        self.assert_ok_selection();
1052    }
1053
1054    /// This implements step 3 onward from:
1055    ///
1056    ///  - <https://www.w3.org/TR/clipboard-apis/#copy-action>
1057    ///  - <https://www.w3.org/TR/clipboard-apis/#cut-action>
1058    ///  - <https://www.w3.org/TR/clipboard-apis/#paste-action>
1059    ///
1060    /// Earlier steps should have already been run by the callers.
1061    pub(crate) fn handle_clipboard_event(
1062        &mut self,
1063        clipboard_event: &ClipboardEvent,
1064    ) -> ClipboardEventReaction {
1065        let event = clipboard_event.upcast::<Event>();
1066        if !event.IsTrusted() {
1067            return ClipboardEventReaction::empty();
1068        }
1069
1070        // This step is common to all event types in the specification.
1071        // Step 3: If the event was not canceled, then
1072        if event.DefaultPrevented() {
1073            // Step 4: Else, if the event was canceled
1074            // Step 4.1: Return false.
1075            return ClipboardEventReaction::empty();
1076        }
1077
1078        let event_type = event.Type();
1079        match_domstring_ascii!(event_type,
1080            "copy" => {
1081                // These steps are from <https://www.w3.org/TR/clipboard-apis/#copy-action>:
1082                let selection = self.get_selection_text();
1083
1084                // Step 3.1 Copy the selected contents, if any, to the clipboard
1085                if let Some(text) = selection {
1086                    self.clipboard_provider.set_text(text);
1087                }
1088
1089                // Step 3.2 Fire a clipboard event named clipboardchange
1090                ClipboardEventReaction::new(ClipboardEventFlags::FireClipboardChangedEvent)
1091            },
1092            "cut" => {
1093                // These steps are from <https://www.w3.org/TR/clipboard-apis/#cut-action>:
1094                let selection = self.get_selection_text();
1095
1096                // Step 3.1 If there is a selection in an editable context where cutting is enabled, then
1097                let Some(text) = selection else {
1098                    // Step 3.2 Else, if there is no selection or the context is not editable, then
1099                    return ClipboardEventReaction::empty();
1100                };
1101
1102                // Step 3.1.1 Copy the selected contents, if any, to the clipboard
1103                self.clipboard_provider.set_text(text);
1104
1105                // Step 3.1.2 Remove the contents of the selection from the document and collapse the selection.
1106                self.delete_selection();
1107
1108                // Step 3.1.3 Fire a clipboard event named clipboardchange
1109                // Step 3.1.4 Queue tasks to fire any events that should fire due to the modification.
1110                ClipboardEventReaction::new(
1111                    ClipboardEventFlags::FireClipboardChangedEvent |
1112                        ClipboardEventFlags::QueueInputEvent,
1113                )
1114                .with_input_type(InputType::DeleteByCut)
1115            },
1116            "paste" => {
1117                // These steps are from <https://www.w3.org/TR/clipboard-apis/#paste-action>:
1118                let Some(data_transfer) = clipboard_event.get_clipboard_data() else {
1119                    return ClipboardEventReaction::empty();
1120                };
1121                let Some(drag_data_store) = data_transfer.data_store() else {
1122                    return ClipboardEventReaction::empty();
1123                };
1124
1125                // Step 3.1: If there is a selection or cursor in an editable context where pasting is
1126                // enabled, then:
1127                // TODO: Our TextInput always has a selection or an input point. It's likely that this
1128                // shouldn't be the case when the entry loses the cursor.
1129
1130                // Step 3.1.1: Insert the most suitable content found on the clipboard, if any, into the
1131                // context.
1132                // TODO: Only text content is currently supported, but other data types should be supported
1133                // in the future.
1134                let Some(text_content) =
1135                    drag_data_store
1136                        .iter_item_list()
1137                        .find_map(|item| match item {
1138                            Kind::Text { data, .. } => Some(data.to_string()),
1139                            _ => None,
1140                        })
1141                else {
1142                    return ClipboardEventReaction::empty();
1143                };
1144                if text_content.is_empty() {
1145                    return ClipboardEventReaction::empty();
1146                }
1147
1148                self.insert(&text_content);
1149
1150                // Step 3.1.2: Queue tasks to fire any events that should fire due to the
1151                // modification, see ยง 5.3 Integration with other scripts and events for details.
1152                ClipboardEventReaction::new(ClipboardEventFlags::QueueInputEvent)
1153                    .with_text(text_content)
1154                    .with_input_type(InputType::InsertFromPaste)
1155            },
1156        _ => ClipboardEventReaction::empty(),)
1157    }
1158
1159    /// <https://w3c.github.io/uievents/#event-type-input>
1160    pub(crate) fn queue_input_event(
1161        &self,
1162        target: &EventTarget,
1163        data: Option<String>,
1164        is_composing: IsComposing,
1165        input_type: InputType,
1166    ) {
1167        let global = target.global();
1168        let target = Trusted::new(target);
1169        global.task_manager().user_interaction_task_source().queue(
1170            task!(fire_input_event: move |cx| {
1171                let target = target.root();
1172                let global = target.global();
1173                let window = global.as_window();
1174                let event = InputEvent::new(
1175                    cx,
1176                    window,
1177                    None,
1178                    atom!("input"),
1179                    true,
1180                    false,
1181                    Some(window),
1182                    0,
1183                    data.map(DOMString::from),
1184                    is_composing.into(),
1185                    input_type.as_str().into(),
1186                );
1187                let event = event.upcast::<Event>();
1188                event.set_composed(true);
1189                event.fire(cx, &target);
1190            }),
1191        );
1192    }
1193}
1194
1195#[derive(JSTraceable, MallocSizeOf)]
1196#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
1197pub(crate) struct TextInputSelectionDragHandler(Dom<Element>);
1198
1199impl TextInputSelectionDragHandler {
1200    pub(crate) fn still_connected(&self) -> bool {
1201        self.0.is_connected()
1202    }
1203
1204    /// Process a mouse move event on this [`TextInputSelectionDragHandler`].
1205    ///
1206    /// Returns `true` if the drag should continue and `false` otherwise.
1207    pub(crate) fn moved(&self, hit_test_result: &HitTestResult) -> bool {
1208        if !self.0.is_connected() {
1209            return false;
1210        }
1211
1212        if let Some(input) = self.0.downcast::<HTMLInputElement>() {
1213            input.textinput_mut().drag_moved(input, hit_test_result);
1214            input.maybe_update_shared_selection();
1215            true
1216        } else if let Some(text_area) = self.0.downcast::<HTMLTextAreaElement>() {
1217            text_area
1218                .textinput_mut()
1219                .drag_moved(text_area, hit_test_result);
1220            text_area.maybe_update_shared_selection();
1221            true
1222        } else {
1223            false
1224        }
1225    }
1226}