script/
textinput.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::borrow::ToOwned;
8use std::cmp::min;
9use std::default::Default;
10use std::ops::{Add, AddAssign, Range};
11
12use bitflags::bitflags;
13use keyboard_types::{Key, KeyState, Modifiers, NamedKey, ShortcutMatcher};
14use script_bindings::match_domstring_ascii;
15use unicode_segmentation::UnicodeSegmentation;
16
17use crate::clipboard_provider::ClipboardProvider;
18use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
19use crate::dom::bindings::inheritance::Castable;
20use crate::dom::bindings::refcounted::Trusted;
21use crate::dom::bindings::reflector::DomGlobal;
22use crate::dom::bindings::str::DOMString;
23use crate::dom::compositionevent::CompositionEvent;
24use crate::dom::event::Event;
25use crate::dom::eventtarget::EventTarget;
26use crate::dom::inputevent::InputEvent;
27use crate::dom::keyboardevent::KeyboardEvent;
28use crate::dom::types::ClipboardEvent;
29use crate::drag_data_store::Kind;
30use crate::script_runtime::CanGc;
31
32#[derive(Clone, Copy, PartialEq)]
33pub enum Selection {
34    Selected,
35    NotSelected,
36}
37
38#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
39pub enum SelectionDirection {
40    Forward,
41    Backward,
42    None,
43}
44
45#[derive(Clone, Copy, Debug, Eq, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
46pub struct UTF8Bytes(pub usize);
47
48impl UTF8Bytes {
49    pub fn zero() -> UTF8Bytes {
50        UTF8Bytes(0)
51    }
52
53    pub fn one() -> UTF8Bytes {
54        UTF8Bytes(1)
55    }
56
57    pub(crate) fn unwrap_range(byte_range: Range<UTF8Bytes>) -> Range<usize> {
58        byte_range.start.0..byte_range.end.0
59    }
60
61    pub(crate) fn saturating_sub(self, other: UTF8Bytes) -> UTF8Bytes {
62        if self > other {
63            UTF8Bytes(self.0 - other.0)
64        } else {
65            UTF8Bytes::zero()
66        }
67    }
68}
69
70impl Add for UTF8Bytes {
71    type Output = UTF8Bytes;
72
73    fn add(self, other: UTF8Bytes) -> UTF8Bytes {
74        UTF8Bytes(self.0 + other.0)
75    }
76}
77
78impl AddAssign for UTF8Bytes {
79    fn add_assign(&mut self, other: UTF8Bytes) {
80        *self = UTF8Bytes(self.0 + other.0)
81    }
82}
83
84trait StrExt {
85    fn len_utf8(&self) -> UTF8Bytes;
86}
87impl StrExt for DOMString {
88    fn len_utf8(&self) -> UTF8Bytes {
89        UTF8Bytes(self.len())
90    }
91}
92
93impl StrExt for str {
94    fn len_utf8(&self) -> UTF8Bytes {
95        UTF8Bytes(self.len())
96    }
97}
98
99#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
100pub struct UTF16CodeUnits(pub usize);
101
102impl UTF16CodeUnits {
103    pub fn zero() -> UTF16CodeUnits {
104        UTF16CodeUnits(0)
105    }
106
107    pub fn one() -> UTF16CodeUnits {
108        UTF16CodeUnits(1)
109    }
110
111    pub(crate) fn saturating_sub(self, other: UTF16CodeUnits) -> UTF16CodeUnits {
112        if self > other {
113            UTF16CodeUnits(self.0 - other.0)
114        } else {
115            UTF16CodeUnits::zero()
116        }
117    }
118}
119
120impl Add for UTF16CodeUnits {
121    type Output = UTF16CodeUnits;
122
123    fn add(self, other: UTF16CodeUnits) -> UTF16CodeUnits {
124        UTF16CodeUnits(self.0 + other.0)
125    }
126}
127
128impl AddAssign for UTF16CodeUnits {
129    fn add_assign(&mut self, other: UTF16CodeUnits) {
130        *self = UTF16CodeUnits(self.0 + other.0)
131    }
132}
133
134impl From<DOMString> for SelectionDirection {
135    fn from(direction: DOMString) -> SelectionDirection {
136        match_domstring_ascii!(direction,
137            "forward" => SelectionDirection::Forward,
138            "backward" => SelectionDirection::Backward,
139            _ => SelectionDirection::None,
140        )
141    }
142}
143
144impl From<SelectionDirection> for DOMString {
145    fn from(direction: SelectionDirection) -> DOMString {
146        match direction {
147            SelectionDirection::Forward => DOMString::from("forward"),
148            SelectionDirection::Backward => DOMString::from("backward"),
149            SelectionDirection::None => DOMString::from("none"),
150        }
151    }
152}
153
154#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
155pub struct TextPoint {
156    /// 0-based line number
157    pub line: usize,
158    /// 0-based column number in bytes
159    pub index: UTF8Bytes,
160}
161
162impl TextPoint {
163    /// Returns a TextPoint constrained to be a valid location within lines
164    fn constrain_to(&self, lines: &[DOMString]) -> TextPoint {
165        let line = min(self.line, lines.len() - 1);
166
167        TextPoint {
168            line,
169            index: min(self.index, lines[line].len_utf8()),
170        }
171    }
172}
173
174#[derive(Clone, Copy, PartialEq)]
175pub(crate) struct SelectionState {
176    start: TextPoint,
177    end: TextPoint,
178    direction: SelectionDirection,
179}
180
181/// Encapsulated state for handling keyboard input in a single or multiline text input control.
182#[derive(JSTraceable, MallocSizeOf)]
183pub struct TextInput<T: ClipboardProvider> {
184    /// Current text input content, split across lines without trailing '\n'
185    lines: Vec<DOMString>,
186
187    /// Current cursor input point
188    edit_point: TextPoint,
189
190    /// The current selection goes from the selection_origin until the edit_point. Note that the
191    /// selection_origin may be after the edit_point, in the case of a backward selection.
192    selection_origin: Option<TextPoint>,
193    selection_direction: SelectionDirection,
194
195    /// Is this a multiline input?
196    multiline: bool,
197
198    #[ignore_malloc_size_of = "Can't easily measure this generic type"]
199    clipboard_provider: T,
200
201    /// The maximum number of UTF-16 code units this text input is allowed to hold.
202    ///
203    /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
204    max_length: Option<UTF16CodeUnits>,
205    min_length: Option<UTF16CodeUnits>,
206
207    /// Was last change made by set_content?
208    was_last_change_by_set_content: bool,
209}
210
211#[derive(Clone, Copy, PartialEq)]
212pub enum IsComposing {
213    Composing,
214    NotComposing,
215}
216
217impl From<IsComposing> for bool {
218    fn from(is_composing: IsComposing) -> Self {
219        match is_composing {
220            IsComposing::Composing => true,
221            IsComposing::NotComposing => false,
222        }
223    }
224}
225
226/// <https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes>
227#[derive(Clone, Copy, PartialEq)]
228pub enum InputType {
229    InsertText,
230    InsertLineBreak,
231    InsertFromPaste,
232    InsertCompositionText,
233    DeleteByCut,
234    DeleteContentBackward,
235    DeleteContentForward,
236    Nothing,
237}
238
239impl InputType {
240    fn as_str(&self) -> &str {
241        match *self {
242            InputType::InsertText => "insertText",
243            InputType::InsertLineBreak => "insertLineBreak",
244            InputType::InsertFromPaste => "insertFromPaste",
245            InputType::InsertCompositionText => "insertCompositionText",
246            InputType::DeleteByCut => "deleteByCut",
247            InputType::DeleteContentBackward => "deleteContentBackward",
248            InputType::DeleteContentForward => "deleteContentForward",
249            InputType::Nothing => "",
250        }
251    }
252}
253
254/// Resulting action to be taken by the owner of a text input that is handling an event.
255pub enum KeyReaction {
256    TriggerDefaultAction,
257    DispatchInput(Option<String>, IsComposing, InputType),
258    RedrawSelection,
259    Nothing,
260}
261
262bitflags! {
263    /// Resulting action to be taken by the owner of a text input that is handling a clipboard
264    /// event.
265    #[derive(Clone, Copy)]
266    pub struct ClipboardEventFlags: u8 {
267        const QueueInputEvent = 1 << 0;
268        const FireClipboardChangedEvent = 1 << 1;
269    }
270}
271
272pub struct ClipboardEventReaction {
273    pub flags: ClipboardEventFlags,
274    pub text: Option<String>,
275    pub input_type: InputType,
276}
277
278impl ClipboardEventReaction {
279    fn new(flags: ClipboardEventFlags) -> Self {
280        Self {
281            flags,
282            text: None,
283            input_type: InputType::Nothing,
284        }
285    }
286
287    fn with_text(mut self, text: String) -> Self {
288        self.text = Some(text);
289        self
290    }
291
292    fn with_input_type(mut self, input_type: InputType) -> Self {
293        self.input_type = input_type;
294        self
295    }
296
297    fn empty() -> Self {
298        Self::new(ClipboardEventFlags::empty())
299    }
300}
301
302impl Default for TextPoint {
303    fn default() -> TextPoint {
304        TextPoint {
305            line: 0,
306            index: UTF8Bytes::zero(),
307        }
308    }
309}
310
311/// Control whether this control should allow multiple lines.
312#[derive(Eq, PartialEq)]
313pub enum Lines {
314    Single,
315    Multiple,
316}
317
318/// The direction in which to delete a character.
319#[derive(Clone, Copy, Eq, PartialEq)]
320pub enum Direction {
321    Forward,
322    Backward,
323}
324
325// Some shortcuts use Cmd on Mac and Control on other systems.
326#[cfg(target_os = "macos")]
327pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::META;
328#[cfg(not(target_os = "macos"))]
329pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
330
331/// The length in bytes of the first n characters in a UTF-8 string.
332///
333/// If the string has fewer than n characters, returns the length of the whole string.
334/// If n is 0, returns 0
335fn len_of_first_n_chars(text: &DOMString, n: usize) -> UTF8Bytes {
336    match text.str().char_indices().take(n).last() {
337        Some((index, ch)) => UTF8Bytes(index + ch.len_utf8()),
338        None => UTF8Bytes::zero(),
339    }
340}
341
342/// The length in bytes of the first n code units in a string when encoded in UTF-16.
343///
344/// If the string is fewer than n code units, returns the length of the whole string.
345fn len_of_first_n_code_units(text: &DOMString, n: UTF16CodeUnits) -> UTF8Bytes {
346    let mut utf8_len = UTF8Bytes::zero();
347    let mut utf16_len = UTF16CodeUnits::zero();
348    for c in text.str().chars() {
349        utf16_len += UTF16CodeUnits(c.len_utf16());
350        if utf16_len > n {
351            break;
352        }
353        utf8_len += UTF8Bytes(c.len_utf8());
354    }
355    utf8_len
356}
357
358impl<T: ClipboardProvider> TextInput<T> {
359    /// Instantiate a new text input control
360    pub fn new(
361        lines: Lines,
362        initial: DOMString,
363        clipboard_provider: T,
364        max_length: Option<UTF16CodeUnits>,
365        min_length: Option<UTF16CodeUnits>,
366        selection_direction: SelectionDirection,
367    ) -> TextInput<T> {
368        let mut i = TextInput {
369            lines: vec![],
370            edit_point: Default::default(),
371            selection_origin: None,
372            multiline: lines == Lines::Multiple,
373            clipboard_provider,
374            max_length,
375            min_length,
376            selection_direction,
377            was_last_change_by_set_content: true,
378        };
379        i.set_content(initial);
380        i
381    }
382
383    pub fn edit_point(&self) -> TextPoint {
384        self.edit_point
385    }
386
387    pub fn selection_origin(&self) -> Option<TextPoint> {
388        self.selection_origin
389    }
390
391    /// The selection origin, or the edit point if there is no selection. Note that the selection
392    /// origin may be after the edit point, in the case of a backward selection.
393    pub fn selection_origin_or_edit_point(&self) -> TextPoint {
394        self.selection_origin.unwrap_or(self.edit_point)
395    }
396
397    pub fn selection_direction(&self) -> SelectionDirection {
398        self.selection_direction
399    }
400
401    pub(crate) fn set_max_length(&mut self, length: Option<UTF16CodeUnits>) {
402        self.max_length = length;
403    }
404
405    pub(crate) fn set_min_length(&mut self, length: Option<UTF16CodeUnits>) {
406        self.min_length = length;
407    }
408
409    /// Was last edit made by set_content?
410    pub(crate) fn was_last_change_by_set_content(&self) -> bool {
411        self.was_last_change_by_set_content
412    }
413
414    /// Remove a character at the current editing point
415    ///
416    /// Returns true if any character was deleted
417    pub fn delete_char(&mut self, dir: Direction) -> bool {
418        if self.selection_origin.is_none() || self.selection_origin == Some(self.edit_point) {
419            self.adjust_horizontal_by_one(dir, Selection::Selected);
420        }
421        if self.selection_start() == self.selection_end() {
422            false
423        } else {
424            self.replace_selection(DOMString::new());
425            true
426        }
427    }
428
429    /// Insert a character at the current editing point
430    pub fn insert_char(&mut self, ch: char) {
431        self.insert_string(ch.to_string());
432    }
433
434    /// Insert a string at the current editing point or replace the selection if
435    /// one exists.
436    pub fn insert_string<S: Into<String>>(&mut self, s: S) {
437        if self.selection_origin.is_none() {
438            self.selection_origin = Some(self.edit_point);
439        }
440        self.replace_selection(DOMString::from(s.into()));
441    }
442
443    /// The start of the selection (or the edit point, if there is no selection). Always less than
444    /// or equal to selection_end(), regardless of the selection direction.
445    pub fn selection_start(&self) -> TextPoint {
446        match self.selection_direction {
447            SelectionDirection::None | SelectionDirection::Forward => {
448                self.selection_origin_or_edit_point()
449            },
450            SelectionDirection::Backward => self.edit_point,
451        }
452    }
453
454    /// The byte offset of the selection_start()
455    pub fn selection_start_offset(&self) -> UTF8Bytes {
456        self.text_point_to_offset(&self.selection_start())
457    }
458
459    /// The end of the selection (or the edit point, if there is no selection). Always greater
460    /// than or equal to selection_start(), regardless of the selection direction.
461    pub fn selection_end(&self) -> TextPoint {
462        match self.selection_direction {
463            SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
464            SelectionDirection::Backward => self.selection_origin_or_edit_point(),
465        }
466    }
467
468    /// The byte offset of the selection_end()
469    pub fn selection_end_offset(&self) -> UTF8Bytes {
470        self.text_point_to_offset(&self.selection_end())
471    }
472
473    /// Whether or not there is an active selection (the selection may be zero-length)
474    #[inline]
475    pub(crate) fn has_selection(&self) -> bool {
476        self.selection_origin.is_some()
477    }
478
479    /// Returns a tuple of (start, end) giving the bounds of the current selection. start is always
480    /// less than or equal to end.
481    pub fn sorted_selection_bounds(&self) -> (TextPoint, TextPoint) {
482        (self.selection_start(), self.selection_end())
483    }
484
485    /// Return the selection range as byte offsets from the start of the content.
486    ///
487    /// If there is no selection, returns an empty range at the edit point.
488    pub(crate) fn sorted_selection_offsets_range(&self) -> Range<UTF8Bytes> {
489        self.selection_start_offset()..self.selection_end_offset()
490    }
491
492    /// The state of the current selection. Can be used to compare whether selection state has changed.
493    pub(crate) fn selection_state(&self) -> SelectionState {
494        SelectionState {
495            start: self.selection_start(),
496            end: self.selection_end(),
497            direction: self.selection_direction,
498        }
499    }
500
501    // Check that the selection is valid.
502    fn assert_ok_selection(&self) {
503        debug!(
504            "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
505            self.edit_point, self.selection_origin, self.selection_direction
506        );
507        if let Some(begin) = self.selection_origin {
508            debug_assert!(begin.line < self.lines.len());
509            debug_assert!(begin.index <= self.lines[begin.line].len_utf8());
510
511            match self.selection_direction {
512                SelectionDirection::None | SelectionDirection::Forward => {
513                    debug_assert!(begin <= self.edit_point)
514                },
515
516                SelectionDirection::Backward => debug_assert!(self.edit_point <= begin),
517            }
518        }
519
520        debug_assert!(self.edit_point.line < self.lines.len());
521        debug_assert!(self.edit_point.index <= self.lines[self.edit_point.line].len_utf8());
522    }
523
524    pub(crate) fn get_selection_text(&self) -> Option<String> {
525        let text = self.fold_selection_slices(String::new(), |s, slice| s.push_str(slice));
526        if text.is_empty() {
527            return None;
528        }
529        Some(text)
530    }
531
532    /// The length of the selected text in UTF-16 code units.
533    fn selection_utf16_len(&self) -> UTF16CodeUnits {
534        self.fold_selection_slices(UTF16CodeUnits::zero(), |len, slice| {
535            *len += UTF16CodeUnits(slice.chars().map(char::len_utf16).sum::<usize>())
536        })
537    }
538
539    /// Run the callback on a series of slices that, concatenated, make up the selected text.
540    ///
541    /// The accumulator `acc` can be mutated by the callback, and will be returned at the end.
542    fn fold_selection_slices<B, F: FnMut(&mut B, &str)>(&self, mut acc: B, mut f: F) -> B {
543        if self.has_selection() {
544            let (start, end) = self.sorted_selection_bounds();
545            let UTF8Bytes(start_offset) = start.index;
546            let UTF8Bytes(end_offset) = end.index;
547
548            if start.line == end.line {
549                f(
550                    &mut acc,
551                    &self.lines[start.line].str()[start_offset..end_offset],
552                )
553            } else {
554                f(&mut acc, &self.lines[start.line].str()[start_offset..]);
555                for line in &self.lines[start.line + 1..end.line] {
556                    f(&mut acc, "\n");
557                    f(&mut acc, &line.str());
558                }
559                f(&mut acc, "\n");
560                f(&mut acc, &self.lines[end.line].str()[..end_offset])
561            }
562        }
563
564        acc
565    }
566
567    pub fn replace_selection(&mut self, insert: DOMString) {
568        if !self.has_selection() {
569            return;
570        }
571
572        let allowed_to_insert_count = if let Some(max_length) = self.max_length {
573            let len_after_selection_replaced =
574                self.utf16_len().saturating_sub(self.selection_utf16_len());
575            max_length.saturating_sub(len_after_selection_replaced)
576        } else {
577            UTF16CodeUnits(usize::MAX)
578        };
579
580        let UTF8Bytes(last_char_index) =
581            len_of_first_n_code_units(&insert, allowed_to_insert_count);
582        let to_insert = &insert.str()[..last_char_index];
583
584        let (start, end) = self.sorted_selection_bounds();
585        let UTF8Bytes(start_offset) = start.index;
586        let UTF8Bytes(end_offset) = end.index;
587
588        let new_lines = {
589            let prefix = &self.lines[start.line].str()[..start_offset];
590            let suffix = &self.lines[end.line].str()[end_offset..];
591            let lines_prefix = &self.lines[..start.line];
592            let lines_suffix = &self.lines[end.line + 1..];
593
594            let mut insert_lines = if self.multiline {
595                to_insert.split('\n').map(DOMString::from).collect()
596            } else {
597                vec![DOMString::from(to_insert)]
598            };
599
600            // FIXME(ajeffrey): efficient append for DOMStrings
601            let mut new_line = prefix.to_owned();
602
603            new_line.push_str(&insert_lines[0].str());
604            insert_lines[0] = DOMString::from(new_line);
605
606            let last_insert_lines_index = insert_lines.len() - 1;
607            self.edit_point.index = insert_lines[last_insert_lines_index].len_utf8();
608            self.edit_point.line = start.line + last_insert_lines_index;
609
610            // FIXME(ajeffrey): efficient append for DOMStrings
611            insert_lines[last_insert_lines_index].push_str(suffix);
612
613            let mut new_lines = vec![];
614            new_lines.extend_from_slice(lines_prefix);
615            new_lines.extend_from_slice(&insert_lines);
616            new_lines.extend_from_slice(lines_suffix);
617            new_lines
618        };
619
620        self.lines = new_lines;
621        self.was_last_change_by_set_content = false;
622        self.clear_selection();
623        self.assert_ok_selection();
624    }
625
626    /// Return the length in bytes of the current line under the editing point.
627    pub fn current_line_length(&self) -> UTF8Bytes {
628        self.lines[self.edit_point.line].len_utf8()
629    }
630
631    /// Adjust the editing point position by a given number of lines. The resulting column is
632    /// as close to the original column position as possible.
633    pub fn adjust_vertical(&mut self, adjust: isize, select: Selection) {
634        if !self.multiline {
635            return;
636        }
637
638        if select == Selection::Selected {
639            if self.selection_origin.is_none() {
640                self.selection_origin = Some(self.edit_point);
641            }
642        } else {
643            self.clear_selection();
644        }
645
646        assert!(self.edit_point.line < self.lines.len());
647
648        let target_line: isize = self.edit_point.line as isize + adjust;
649
650        if target_line < 0 {
651            self.edit_point.line = 0;
652            self.edit_point.index = UTF8Bytes::zero();
653            if self.selection_origin.is_some() &&
654                (self.selection_direction == SelectionDirection::None ||
655                    self.selection_direction == SelectionDirection::Forward)
656            {
657                self.selection_origin = Some(TextPoint {
658                    line: 0,
659                    index: UTF8Bytes::zero(),
660                });
661            }
662            return;
663        } else if target_line as usize >= self.lines.len() {
664            self.edit_point.line = self.lines.len() - 1;
665            self.edit_point.index = self.current_line_length();
666            if self.selection_origin.is_some() &&
667                (self.selection_direction == SelectionDirection::Backward)
668            {
669                self.selection_origin = Some(self.edit_point);
670            }
671            return;
672        }
673
674        let UTF8Bytes(edit_index) = self.edit_point.index;
675        let col = self.lines[self.edit_point.line].str()[..edit_index]
676            .chars()
677            .count();
678        self.edit_point.line = target_line as usize;
679        // NOTE: this adjusts to the nearest complete Unicode codepoint, rather than grapheme cluster
680        self.edit_point.index = len_of_first_n_chars(&self.lines[self.edit_point.line], col);
681        if let Some(origin) = self.selection_origin {
682            if ((self.selection_direction == SelectionDirection::None ||
683                self.selection_direction == SelectionDirection::Forward) &&
684                self.edit_point <= origin) ||
685                (self.selection_direction == SelectionDirection::Backward &&
686                    origin <= self.edit_point)
687            {
688                self.selection_origin = Some(self.edit_point);
689            }
690        }
691        self.assert_ok_selection();
692    }
693
694    /// Adjust the editing point position by a given number of bytes. If the adjustment
695    /// requested is larger than is available in the current line, the editing point is
696    /// adjusted vertically and the process repeats with the remaining adjustment requested.
697    pub fn adjust_horizontal(
698        &mut self,
699        adjust: UTF8Bytes,
700        direction: Direction,
701        select: Selection,
702    ) {
703        if self.adjust_selection_for_horizontal_change(direction, select) {
704            return;
705        }
706        self.perform_horizontal_adjustment(adjust, direction, select);
707    }
708
709    /// Adjust the editing point position by exactly one grapheme cluster. If the edit point
710    /// is at the beginning of the line and the direction is "Backward" or the edit point is at
711    /// the end of the line and the direction is "Forward", a vertical adjustment is made
712    pub fn adjust_horizontal_by_one(&mut self, direction: Direction, select: Selection) {
713        if self.adjust_selection_for_horizontal_change(direction, select) {
714            return;
715        }
716        let adjust = {
717            let current_line = self.lines[self.edit_point.line].str();
718            let UTF8Bytes(current_offset) = self.edit_point.index;
719            let next_ch = match direction {
720                Direction::Forward => current_line[current_offset..].graphemes(true).next(),
721                Direction::Backward => current_line[..current_offset].graphemes(true).next_back(),
722            };
723            match next_ch {
724                Some(c) => UTF8Bytes(c.len()),
725                None => UTF8Bytes::one(), // Going to the next line is a "one byte" offset
726            }
727        };
728        self.perform_horizontal_adjustment(adjust, direction, select);
729    }
730
731    /// Return whether to cancel the caret move
732    fn adjust_selection_for_horizontal_change(
733        &mut self,
734        adjust: Direction,
735        select: Selection,
736    ) -> bool {
737        if select == Selection::Selected {
738            if self.selection_origin.is_none() {
739                self.selection_origin = Some(self.edit_point);
740            }
741        } else if self.has_selection() {
742            self.edit_point = match adjust {
743                Direction::Backward => self.selection_start(),
744                Direction::Forward => self.selection_end(),
745            };
746            self.clear_selection();
747            return true;
748        }
749        false
750    }
751
752    /// Update the field selection_direction.
753    ///
754    /// When the edit_point (or focus) is before the selection_origin (or anchor)
755    /// you have a backward selection. Otherwise you have a forward selection.
756    fn update_selection_direction(&mut self) {
757        debug!(
758            "edit_point: {:?}, selection_origin: {:?}",
759            self.edit_point, self.selection_origin
760        );
761        self.selection_direction = if Some(self.edit_point) < self.selection_origin {
762            SelectionDirection::Backward
763        } else {
764            SelectionDirection::Forward
765        }
766    }
767
768    fn perform_horizontal_adjustment(
769        &mut self,
770        adjust: UTF8Bytes,
771        direction: Direction,
772        select: Selection,
773    ) {
774        match direction {
775            Direction::Backward => {
776                let remaining = self.edit_point.index;
777                if adjust > remaining && self.edit_point.line > 0 {
778                    // Preserve the current selection origin because `adjust_vertical`
779                    // modifies `selection_origin`. Since we are moving backward instead of
780                    // highlighting vertically, we need to restore it after adjusting the line.
781                    let selection_origin_temp = self.selection_origin;
782                    self.adjust_vertical(-1, select);
783                    self.edit_point.index = self.current_line_length();
784                    // Restore the original selection origin to maintain expected behavior.
785                    self.selection_origin = selection_origin_temp;
786                    // one shift is consumed by the change of line, hence the -1
787                    self.adjust_horizontal(
788                        adjust.saturating_sub(remaining + UTF8Bytes::one()),
789                        direction,
790                        select,
791                    );
792                } else {
793                    self.edit_point.index = remaining.saturating_sub(adjust);
794                }
795            },
796            Direction::Forward => {
797                let remaining = self
798                    .current_line_length()
799                    .saturating_sub(self.edit_point.index);
800                if adjust > remaining && self.lines.len() > self.edit_point.line + 1 {
801                    self.adjust_vertical(1, select);
802                    self.edit_point.index = UTF8Bytes::zero();
803                    // one shift is consumed by the change of line, hence the -1
804                    self.adjust_horizontal(
805                        adjust.saturating_sub(remaining + UTF8Bytes::one()),
806                        direction,
807                        select,
808                    );
809                } else {
810                    self.edit_point.index =
811                        min(self.current_line_length(), self.edit_point.index + adjust);
812                }
813            },
814        };
815        self.update_selection_direction();
816        self.assert_ok_selection();
817    }
818
819    /// Deal with a newline input.
820    pub fn handle_return(&mut self) -> KeyReaction {
821        if !self.multiline {
822            KeyReaction::TriggerDefaultAction
823        } else {
824            self.insert_char('\n');
825            KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::InsertLineBreak)
826        }
827    }
828
829    /// Select all text in the input control.
830    pub fn select_all(&mut self) {
831        self.selection_origin = Some(TextPoint {
832            line: 0,
833            index: UTF8Bytes::zero(),
834        });
835        let last_line = self.lines.len() - 1;
836        self.edit_point.line = last_line;
837        self.edit_point.index = self.lines[last_line].len_utf8();
838        self.selection_direction = SelectionDirection::Forward;
839        self.assert_ok_selection();
840    }
841
842    /// Remove the current selection.
843    pub fn clear_selection(&mut self) {
844        self.selection_origin = None;
845        self.selection_direction = SelectionDirection::None;
846    }
847
848    /// Remove the current selection and set the edit point to the end of the content.
849    pub(crate) fn clear_selection_to_limit(&mut self, direction: Direction) {
850        self.clear_selection();
851        self.adjust_horizontal_to_limit(direction, Selection::NotSelected);
852    }
853
854    pub fn adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection) {
855        if self.adjust_selection_for_horizontal_change(direction, select) {
856            return;
857        }
858        let shift_increment: UTF8Bytes = {
859            let current_index = self.edit_point.index;
860            let current_line_index = self.edit_point.line;
861            let current_line = self.lines[current_line_index].str();
862            let mut newline_adjustment = UTF8Bytes::zero();
863            let mut shift_temp = UTF8Bytes::zero();
864            match direction {
865                Direction::Backward => {
866                    let previous_line = current_line_index
867                        .checked_sub(1)
868                        .and_then(|index| self.lines.get(index))
869                        .map(|s| s.str());
870
871                    let input: &str;
872                    if current_index == UTF8Bytes::zero() && current_line_index > 0 {
873                        input = previous_line.as_ref().unwrap();
874                        newline_adjustment = UTF8Bytes::one();
875                    } else {
876                        let UTF8Bytes(remaining) = current_index;
877                        input = &current_line[..remaining];
878                    }
879
880                    let mut iter = input.split_word_bounds().rev();
881                    loop {
882                        match iter.next() {
883                            None => break,
884                            Some(x) => {
885                                shift_temp += UTF8Bytes(x.len());
886                                if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
887                                    break;
888                                }
889                            },
890                        }
891                    }
892                },
893                Direction::Forward => {
894                    let input: &str;
895                    let next_line = self.lines.get(current_line_index + 1).map(|s| s.str());
896                    let remaining = self.current_line_length().saturating_sub(current_index);
897                    if remaining == UTF8Bytes::zero() && self.lines.len() > self.edit_point.line + 1
898                    {
899                        input = next_line.as_ref().unwrap();
900                        newline_adjustment = UTF8Bytes::one();
901                    } else {
902                        let UTF8Bytes(current_offset) = current_index;
903                        input = &current_line[current_offset..];
904                    }
905
906                    let mut iter = input.split_word_bounds();
907                    loop {
908                        match iter.next() {
909                            None => break,
910                            Some(x) => {
911                                shift_temp += UTF8Bytes(x.len());
912                                if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
913                                    break;
914                                }
915                            },
916                        }
917                    }
918                },
919            };
920
921            shift_temp + newline_adjustment
922        };
923
924        self.adjust_horizontal(shift_increment, direction, select);
925    }
926
927    pub fn adjust_horizontal_to_line_end(&mut self, direction: Direction, select: Selection) {
928        if self.adjust_selection_for_horizontal_change(direction, select) {
929            return;
930        }
931        let shift: usize = {
932            let current_line = &self.lines[self.edit_point.line];
933            let UTF8Bytes(current_offset) = self.edit_point.index;
934            match direction {
935                Direction::Backward => current_line.str()[..current_offset].len(),
936                Direction::Forward => current_line.str()[current_offset..].len(),
937            }
938        };
939        self.perform_horizontal_adjustment(UTF8Bytes(shift), direction, select);
940    }
941
942    pub(crate) fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection) {
943        if self.adjust_selection_for_horizontal_change(direction, select) {
944            return;
945        }
946        match direction {
947            Direction::Backward => {
948                self.edit_point.line = 0;
949                self.edit_point.index = UTF8Bytes::zero();
950            },
951            Direction::Forward => {
952                self.edit_point.line = &self.lines.len() - 1;
953                self.edit_point.index = (self.lines[&self.lines.len() - 1]).len_utf8();
954            },
955        }
956    }
957
958    /// Process a given `KeyboardEvent` and return an action for the caller to execute.
959    pub(crate) fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
960        let key = event.key();
961        let mods = event.modifiers();
962        self.handle_keydown_aux(key, mods, cfg!(target_os = "macos"))
963    }
964
965    // This function exists for easy unit testing.
966    // To test Mac OS shortcuts on other systems a flag is passed.
967    pub fn handle_keydown_aux(
968        &mut self,
969        key: Key,
970        mut mods: Modifiers,
971        macos: bool,
972    ) -> KeyReaction {
973        let maybe_select = if mods.contains(Modifiers::SHIFT) {
974            Selection::Selected
975        } else {
976            Selection::NotSelected
977        };
978        mods.remove(Modifiers::SHIFT);
979        ShortcutMatcher::new(KeyState::Down, key.clone(), mods)
980            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
981                self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
982                KeyReaction::RedrawSelection
983            })
984            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
985                self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
986                KeyReaction::RedrawSelection
987            })
988            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'A', || {
989                self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
990                KeyReaction::RedrawSelection
991            })
992            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'E', || {
993                self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
994                KeyReaction::RedrawSelection
995            })
996            .optional_shortcut(macos, Modifiers::CONTROL, 'A', || {
997                self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
998                KeyReaction::RedrawSelection
999            })
1000            .optional_shortcut(macos, Modifiers::CONTROL, 'E', || {
1001                self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
1002                KeyReaction::RedrawSelection
1003            })
1004            .shortcut(CMD_OR_CONTROL, 'A', || {
1005                self.select_all();
1006                KeyReaction::RedrawSelection
1007            })
1008            .shortcut(CMD_OR_CONTROL, 'X', || {
1009                if let Some(text) = self.get_selection_text() {
1010                    self.clipboard_provider.set_text(text);
1011                    self.delete_char(Direction::Backward);
1012                }
1013                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::DeleteByCut)
1014            })
1015            .shortcut(CMD_OR_CONTROL, 'C', || {
1016                // TODO(stevennovaryo): we should not provide text to clipboard for type=password
1017                if let Some(text) = self.get_selection_text() {
1018                    self.clipboard_provider.set_text(text);
1019                }
1020                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::Nothing)
1021            })
1022            .shortcut(CMD_OR_CONTROL, 'V', || {
1023                if let Ok(text_content) = self.clipboard_provider.get_text() {
1024                    self.insert_string(&text_content);
1025                    KeyReaction::DispatchInput(
1026                        Some(text_content),
1027                        IsComposing::NotComposing,
1028                        InputType::InsertFromPaste,
1029                    )
1030                } else {
1031                    KeyReaction::DispatchInput(
1032                        Some("".to_string()),
1033                        IsComposing::NotComposing,
1034                        InputType::InsertFromPaste,
1035                    )
1036                }
1037            })
1038            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
1039                if self.delete_char(Direction::Forward) {
1040                    KeyReaction::DispatchInput(
1041                        None,
1042                        IsComposing::NotComposing,
1043                        InputType::DeleteContentForward,
1044                    )
1045                } else {
1046                    KeyReaction::Nothing
1047                }
1048            })
1049            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
1050                if self.delete_char(Direction::Backward) {
1051                    KeyReaction::DispatchInput(
1052                        None,
1053                        IsComposing::NotComposing,
1054                        InputType::DeleteContentBackward,
1055                    )
1056                } else {
1057                    KeyReaction::Nothing
1058                }
1059            })
1060            .optional_shortcut(
1061                macos,
1062                Modifiers::META,
1063                Key::Named(NamedKey::ArrowLeft),
1064                || {
1065                    self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
1066                    KeyReaction::RedrawSelection
1067                },
1068            )
1069            .optional_shortcut(
1070                macos,
1071                Modifiers::META,
1072                Key::Named(NamedKey::ArrowRight),
1073                || {
1074                    self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
1075                    KeyReaction::RedrawSelection
1076                },
1077            )
1078            .optional_shortcut(
1079                macos,
1080                Modifiers::META,
1081                Key::Named(NamedKey::ArrowUp),
1082                || {
1083                    self.adjust_horizontal_to_limit(Direction::Backward, maybe_select);
1084                    KeyReaction::RedrawSelection
1085                },
1086            )
1087            .optional_shortcut(
1088                macos,
1089                Modifiers::META,
1090                Key::Named(NamedKey::ArrowDown),
1091                || {
1092                    self.adjust_horizontal_to_limit(Direction::Forward, maybe_select);
1093                    KeyReaction::RedrawSelection
1094                },
1095            )
1096            .shortcut(Modifiers::ALT, Key::Named(NamedKey::ArrowLeft), || {
1097                self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
1098                KeyReaction::RedrawSelection
1099            })
1100            .shortcut(Modifiers::ALT, Key::Named(NamedKey::ArrowRight), || {
1101                self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
1102                KeyReaction::RedrawSelection
1103            })
1104            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
1105                self.adjust_horizontal_by_one(Direction::Backward, maybe_select);
1106                KeyReaction::RedrawSelection
1107            })
1108            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
1109                self.adjust_horizontal_by_one(Direction::Forward, maybe_select);
1110                KeyReaction::RedrawSelection
1111            })
1112            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
1113                self.adjust_vertical(-1, maybe_select);
1114                KeyReaction::RedrawSelection
1115            })
1116            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
1117                self.adjust_vertical(1, maybe_select);
1118                KeyReaction::RedrawSelection
1119            })
1120            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
1121                self.handle_return()
1122            })
1123            .optional_shortcut(
1124                macos,
1125                Modifiers::empty(),
1126                Key::Named(NamedKey::Home),
1127                || {
1128                    self.edit_point.index = UTF8Bytes::zero();
1129                    KeyReaction::RedrawSelection
1130                },
1131            )
1132            .optional_shortcut(macos, Modifiers::empty(), Key::Named(NamedKey::End), || {
1133                self.edit_point.index = self.current_line_length();
1134                self.assert_ok_selection();
1135                KeyReaction::RedrawSelection
1136            })
1137            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
1138                self.adjust_vertical(-28, maybe_select);
1139                KeyReaction::RedrawSelection
1140            })
1141            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
1142                self.adjust_vertical(28, maybe_select);
1143                KeyReaction::RedrawSelection
1144            })
1145            .otherwise(|| {
1146                if let Key::Character(ref c) = key {
1147                    self.insert_string(c.as_str());
1148                    return KeyReaction::DispatchInput(
1149                        Some(c.to_string()),
1150                        IsComposing::NotComposing,
1151                        InputType::InsertText,
1152                    );
1153                }
1154                if matches!(key, Key::Named(NamedKey::Process)) {
1155                    return KeyReaction::DispatchInput(
1156                        None,
1157                        IsComposing::Composing,
1158                        InputType::Nothing,
1159                    );
1160                }
1161                KeyReaction::Nothing
1162            })
1163            .unwrap()
1164    }
1165
1166    pub(crate) fn handle_compositionend(&mut self, event: &CompositionEvent) -> KeyReaction {
1167        let ch = event.data().str();
1168        self.insert_string(ch.as_ref());
1169        KeyReaction::DispatchInput(
1170            Some(ch.to_string()),
1171            IsComposing::NotComposing,
1172            InputType::InsertCompositionText,
1173        )
1174    }
1175
1176    pub(crate) fn handle_compositionupdate(&mut self, event: &CompositionEvent) -> KeyReaction {
1177        let ch = event.data().str();
1178        let start = self.selection_start_offset().0;
1179        self.insert_string(ch.as_ref());
1180        self.set_selection_range(
1181            start as u32,
1182            (start + event.data().len_utf8().0) as u32,
1183            SelectionDirection::Forward,
1184        );
1185        KeyReaction::DispatchInput(
1186            Some(ch.to_string()),
1187            IsComposing::Composing,
1188            InputType::InsertCompositionText,
1189        )
1190    }
1191
1192    /// Whether the content is empty.
1193    pub(crate) fn is_empty(&self) -> bool {
1194        self.lines.len() <= 1 && self.lines.first().is_none_or(|line| line.is_empty())
1195    }
1196
1197    /// The length of the content in bytes.
1198    pub(crate) fn len_utf8(&self) -> UTF8Bytes {
1199        self.lines
1200            .iter()
1201            .fold(UTF8Bytes::zero(), |m, l| {
1202                m + l.len_utf8() + UTF8Bytes::one() // + 1 for the '\n'
1203            })
1204            .saturating_sub(UTF8Bytes::one())
1205    }
1206
1207    /// The total number of code units required to encode the content in utf16.
1208    pub(crate) fn utf16_len(&self) -> UTF16CodeUnits {
1209        self.lines
1210            .iter()
1211            .fold(UTF16CodeUnits::zero(), |m, l| {
1212                m + UTF16CodeUnits(l.str().chars().map(char::len_utf16).sum::<usize>() + 1)
1213                // + 1 for the '\n'
1214            })
1215            .saturating_sub(UTF16CodeUnits::one())
1216    }
1217
1218    /// The length of the content in Unicode code points.
1219    pub(crate) fn char_count(&self) -> usize {
1220        self.lines.iter().fold(0, |m, l| {
1221            m + l.str().chars().count() + 1 // + 1 for the '\n'
1222        }) - 1
1223    }
1224
1225    /// Get the current contents of the text input. Multiple lines are joined by \n.
1226    pub fn get_content(&self) -> DOMString {
1227        let mut content = "".to_owned();
1228        for (i, line) in self.lines.iter().enumerate() {
1229            content.push_str(&line.str());
1230            if i < self.lines.len() - 1 {
1231                content.push('\n');
1232            }
1233        }
1234        DOMString::from(content)
1235    }
1236
1237    /// Get a reference to the contents of a single-line text input. Panics if self is a multiline input.
1238    pub(crate) fn single_line_content(&self) -> &DOMString {
1239        assert!(!self.multiline);
1240        &self.lines[0]
1241    }
1242
1243    /// Set the current contents of the text input. If this is control supports multiple lines,
1244    /// any \n encountered will be stripped and force a new logical line.
1245    pub fn set_content(&mut self, content: DOMString) {
1246        self.lines = if self.multiline {
1247            // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
1248            content
1249                .str()
1250                .replace("\r\n", "\n")
1251                .split(['\n', '\r'])
1252                .map(DOMString::from)
1253                .collect()
1254        } else {
1255            vec![content]
1256        };
1257
1258        self.was_last_change_by_set_content = true;
1259        self.edit_point = self.edit_point.constrain_to(&self.lines);
1260
1261        if let Some(origin) = self.selection_origin {
1262            self.selection_origin = Some(origin.constrain_to(&self.lines));
1263        }
1264        self.assert_ok_selection();
1265    }
1266
1267    /// Convert a TextPoint into a byte offset from the start of the content.
1268    fn text_point_to_offset(&self, text_point: &TextPoint) -> UTF8Bytes {
1269        self.lines
1270            .iter()
1271            .enumerate()
1272            .fold(UTF8Bytes::zero(), |acc, (i, val)| {
1273                if i < text_point.line {
1274                    acc + val.len_utf8() + UTF8Bytes::one() // +1 for the \n
1275                } else {
1276                    acc
1277                }
1278            }) +
1279            text_point.index
1280    }
1281
1282    /// Convert a byte offset from the start of the content into a TextPoint.
1283    fn offset_to_text_point(&self, abs_point: UTF8Bytes) -> TextPoint {
1284        let mut index = abs_point;
1285        let mut line = 0;
1286        let last_line_idx = self.lines.len() - 1;
1287        self.lines
1288            .iter()
1289            .enumerate()
1290            .fold(UTF8Bytes::zero(), |acc, (i, val)| {
1291                if i != last_line_idx {
1292                    let line_end = val.len_utf8();
1293                    let new_acc = acc + line_end + UTF8Bytes::one();
1294                    if abs_point >= new_acc && index > line_end {
1295                        index = index.saturating_sub(line_end + UTF8Bytes::one());
1296                        line += 1;
1297                    }
1298                    new_acc
1299                } else {
1300                    acc
1301                }
1302            });
1303
1304        TextPoint { line, index }
1305    }
1306
1307    pub fn set_selection_range(&mut self, start: u32, end: u32, direction: SelectionDirection) {
1308        let mut start = UTF8Bytes(start as usize);
1309        let mut end = UTF8Bytes(end as usize);
1310        let text_end = self.get_content().len_utf8();
1311
1312        if end > text_end {
1313            end = text_end;
1314        }
1315        if start > end {
1316            start = end;
1317        }
1318
1319        self.selection_direction = direction;
1320
1321        match direction {
1322            SelectionDirection::None | SelectionDirection::Forward => {
1323                self.selection_origin = Some(self.offset_to_text_point(start));
1324                self.edit_point = self.offset_to_text_point(end);
1325            },
1326            SelectionDirection::Backward => {
1327                self.selection_origin = Some(self.offset_to_text_point(end));
1328                self.edit_point = self.offset_to_text_point(start);
1329            },
1330        }
1331        self.assert_ok_selection();
1332    }
1333
1334    /// Set the edit point index position based off of a given grapheme cluster offset
1335    pub fn set_edit_point_index(&mut self, index: usize) {
1336        let byte_offset = self.lines[self.edit_point.line]
1337            .str()
1338            .graphemes(true)
1339            .take(index)
1340            .fold(UTF8Bytes::zero(), |acc, x| acc + x.len_utf8());
1341        self.edit_point.index = byte_offset;
1342    }
1343
1344    /// This implements step 3 onward from:
1345    ///
1346    ///  - <https://www.w3.org/TR/clipboard-apis/#copy-action>
1347    ///  - <https://www.w3.org/TR/clipboard-apis/#cut-action>
1348    ///  - <https://www.w3.org/TR/clipboard-apis/#paste-action>
1349    ///
1350    /// Earlier steps should have already been run by the callers.
1351    pub(crate) fn handle_clipboard_event(
1352        &mut self,
1353        clipboard_event: &ClipboardEvent,
1354    ) -> ClipboardEventReaction {
1355        let event = clipboard_event.upcast::<Event>();
1356        if !event.IsTrusted() {
1357            return ClipboardEventReaction::empty();
1358        }
1359
1360        // This step is common to all event types in the specification.
1361        // Step 3: If the event was not canceled, then
1362        if event.DefaultPrevented() {
1363            // Step 4: Else, if the event was canceled
1364            // Step 4.1: Return false.
1365            return ClipboardEventReaction::empty();
1366        }
1367
1368        let event_type = event.Type();
1369        match_domstring_ascii!(event_type,
1370            "copy" => {
1371                // These steps are from <https://www.w3.org/TR/clipboard-apis/#copy-action>:
1372                let selection = self.get_selection_text();
1373
1374                // Step 3.1 Copy the selected contents, if any, to the clipboard
1375                if let Some(text) = selection {
1376                    self.clipboard_provider.set_text(text);
1377                }
1378
1379                // Step 3.2 Fire a clipboard event named clipboardchange
1380                ClipboardEventReaction::new(ClipboardEventFlags::FireClipboardChangedEvent)
1381            },
1382            "cut" => {
1383                // These steps are from <https://www.w3.org/TR/clipboard-apis/#cut-action>:
1384                let selection = self.get_selection_text();
1385
1386                // Step 3.1 If there is a selection in an editable context where cutting is enabled, then
1387                let Some(text) = selection else {
1388                    // Step 3.2 Else, if there is no selection or the context is not editable, then
1389                    return ClipboardEventReaction::empty();
1390                };
1391
1392                // Step 3.1.1 Copy the selected contents, if any, to the clipboard
1393                self.clipboard_provider.set_text(text);
1394
1395                // Step 3.1.2 Remove the contents of the selection from the document and collapse the selection.
1396                self.delete_char(Direction::Backward);
1397
1398                // Step 3.1.3 Fire a clipboard event named clipboardchange
1399                // Step 3.1.4 Queue tasks to fire any events that should fire due to the modification.
1400                ClipboardEventReaction::new(
1401                    ClipboardEventFlags::FireClipboardChangedEvent |
1402                        ClipboardEventFlags::QueueInputEvent,
1403                )
1404                .with_input_type(InputType::DeleteByCut)
1405            },
1406            "paste" => {
1407                // These steps are from <https://www.w3.org/TR/clipboard-apis/#paste-action>:
1408                let Some(data_transfer) = clipboard_event.get_clipboard_data() else {
1409                    return ClipboardEventReaction::empty();
1410                };
1411                let Some(drag_data_store) = data_transfer.data_store() else {
1412                    return ClipboardEventReaction::empty();
1413                };
1414
1415                // Step 3.1: If there is a selection or cursor in an editable context where pasting is
1416                // enabled, then:
1417                // TODO: Our TextInput always has a selection or an input point. It's likely that this
1418                // shouldn't be the case when the entry loses the cursor.
1419
1420                // Step 3.1.1: Insert the most suitable content found on the clipboard, if any, into the
1421                // context.
1422                // TODO: Only text content is currently supported, but other data types should be supported
1423                // in the future.
1424                let Some(text_content) =
1425                    drag_data_store
1426                        .iter_item_list()
1427                        .find_map(|item| match item {
1428                            Kind::Text { data, .. } => Some(data.to_string()),
1429                            _ => None,
1430                        })
1431                else {
1432                    return ClipboardEventReaction::empty();
1433                };
1434                if text_content.is_empty() {
1435                    return ClipboardEventReaction::empty();
1436                }
1437
1438                self.insert_string(&text_content);
1439
1440                // Step 3.1.2: Queue tasks to fire any events that should fire due to the
1441                // modification, see ยง 5.3 Integration with other scripts and events for details.
1442                ClipboardEventReaction::new(ClipboardEventFlags::QueueInputEvent)
1443                    .with_text(text_content)
1444                    .with_input_type(InputType::InsertFromPaste)
1445            },
1446        _ => ClipboardEventReaction::empty(),)
1447    }
1448
1449    /// <https://w3c.github.io/uievents/#event-type-input>
1450    pub(crate) fn queue_input_event(
1451        &self,
1452        target: &EventTarget,
1453        data: Option<String>,
1454        is_composing: IsComposing,
1455        input_type: InputType,
1456    ) {
1457        let global = target.global();
1458        let target = Trusted::new(target);
1459        global.task_manager().user_interaction_task_source().queue(
1460            task!(fire_input_event: move || {
1461                let target = target.root();
1462                let global = target.global();
1463                let window = global.as_window();
1464                let event = InputEvent::new(
1465                    window,
1466                    None,
1467                    DOMString::from("input"),
1468                    true,
1469                    false,
1470                    Some(window),
1471                    0,
1472                    data.map(DOMString::from),
1473                    is_composing.into(),
1474                    input_type.as_str().into(),
1475                    CanGc::note(),
1476                );
1477                let event = event.upcast::<Event>();
1478                event.set_composed(true);
1479                event.fire(&target, CanGc::note());
1480            }),
1481        );
1482    }
1483}