Skip to main content

script/dom/html/form_controls/
text_input.rs

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