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