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