Skip to main content

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