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    /// If there is an uncollapsed selection, delete it, otherwise do nothing. Returns
305    /// true if any text was deleted.
306    fn delete_selection(&mut self) -> bool {
307        if self.selection_start() == self.selection_end() {
308            return false;
309        }
310        self.replace_selection(&DOMString::new());
311        true
312    }
313
314    /// If there is an uncollapsed selection, delete it. Otherwise delete the given [`unit`]
315    /// worth of text in [`direction`] Remove a character at the current editing point
316    ///
317    /// Returns true if any text was deleted.
318    pub fn delete_unit_or_selection(&mut self, unit: RopeMovement, direction: Direction) -> bool {
319        if !self.has_uncollapsed_selection() {
320            let amount = match direction {
321                Direction::Forward => 1,
322                Direction::Backward => -1,
323            };
324            self.modify_selection(amount, unit);
325        }
326        self.delete_selection()
327    }
328
329    /// Insert a string at the current editing point or replace the selection if
330    /// one exists.
331    pub fn insert<S: Into<String>>(&mut self, string: S) {
332        if self.selection_origin.is_none() {
333            self.selection_origin = Some(self.edit_point);
334        }
335        self.replace_selection(&DOMString::from(string.into()));
336    }
337
338    /// The start of the selection (or the edit point, if there is no selection). Always less than
339    /// or equal to selection_end(), regardless of the selection direction.
340    pub fn selection_start(&self) -> RopeIndex {
341        match self.selection_direction {
342            SelectionDirection::None | SelectionDirection::Forward => {
343                self.selection_origin_or_edit_point()
344            },
345            SelectionDirection::Backward => self.edit_point,
346        }
347    }
348
349    pub(crate) fn selection_start_utf16(&self) -> Utf16CodeUnitLength {
350        self.rope.index_to_utf16_offset(self.selection_start())
351    }
352
353    /// The byte offset of the selection_start()
354    fn selection_start_offset(&self) -> Utf8CodeUnitLength {
355        self.rope.index_to_utf8_offset(self.selection_start())
356    }
357
358    /// The end of the selection (or the edit point, if there is no selection). Always greater
359    /// than or equal to selection_start(), regardless of the selection direction.
360    pub fn selection_end(&self) -> RopeIndex {
361        match self.selection_direction {
362            SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
363            SelectionDirection::Backward => self.selection_origin_or_edit_point(),
364        }
365    }
366
367    pub(crate) fn selection_end_utf16(&self) -> Utf16CodeUnitLength {
368        self.rope.index_to_utf16_offset(self.selection_end())
369    }
370
371    /// The byte offset of the selection_end()
372    pub fn selection_end_offset(&self) -> Utf8CodeUnitLength {
373        self.rope.index_to_utf8_offset(self.selection_end())
374    }
375
376    /// Whether or not there is an active uncollapsed selection. This means that the
377    /// selection origin is set and it differs from the edit point.
378    #[inline]
379    pub(crate) fn has_uncollapsed_selection(&self) -> bool {
380        self.selection_origin
381            .is_some_and(|selection_origin| selection_origin != self.edit_point)
382    }
383
384    /// Return the selection range as byte offsets from the start of the content.
385    ///
386    /// If there is no selection, returns an empty range at the edit point.
387    pub(crate) fn sorted_selection_offsets_range(&self) -> Range<Utf8CodeUnitLength> {
388        self.selection_start_offset()..self.selection_end_offset()
389    }
390
391    /// Return the selection range as character offsets from the start of the content.
392    ///
393    /// If there is no selection, returns an empty range at the edit point.
394    pub(crate) fn sorted_selection_character_offsets_range(&self) -> Range<usize> {
395        self.rope.index_to_character_offset(self.selection_start())..
396            self.rope.index_to_character_offset(self.selection_end())
397    }
398
399    /// The state of the current selection. Can be used to compare whether selection state has changed.
400    pub(crate) fn selection_state(&self) -> SelectionState {
401        SelectionState {
402            start: self.selection_start(),
403            end: self.selection_end(),
404            direction: self.selection_direction,
405        }
406    }
407
408    // Check that the selection is valid.
409    fn assert_ok_selection(&self) {
410        debug!(
411            "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
412            self.edit_point, self.selection_origin, self.selection_direction
413        );
414
415        debug_assert_eq!(self.edit_point, self.rope.normalize_index(self.edit_point));
416        if let Some(selection_origin) = self.selection_origin {
417            debug_assert_eq!(
418                selection_origin,
419                self.rope.normalize_index(selection_origin)
420            );
421            match self.selection_direction {
422                SelectionDirection::None | SelectionDirection::Forward => {
423                    debug_assert!(selection_origin <= self.edit_point)
424                },
425                SelectionDirection::Backward => debug_assert!(self.edit_point <= selection_origin),
426            }
427        }
428    }
429
430    fn selection_slice(&self) -> RopeSlice<'_> {
431        self.rope
432            .slice(Some(self.selection_start()), Some(self.selection_end()))
433    }
434
435    pub(crate) fn get_selection_text(&self) -> Option<String> {
436        let text: String = self.selection_slice().into();
437        if text.is_empty() {
438            return None;
439        }
440        Some(text)
441    }
442
443    /// The length of the selected text in UTF-16 code units.
444    fn selection_utf16_len(&self) -> Utf16CodeUnitLength {
445        Utf16CodeUnitLength(
446            self.selection_slice()
447                .chars()
448                .map(char::len_utf16)
449                .sum::<usize>(),
450        )
451    }
452
453    /// Replace the current selection with the given [`DOMString`]. If the [`Rope`] is in
454    /// single line mode this *will* strip newlines, as opposed to [`Self::set_content`],
455    /// which does not.
456    pub fn replace_selection(&mut self, insert: &DOMString) {
457        let string_to_insert = if let Some(max_length) = self.max_length {
458            let utf16_length_without_selection =
459                self.len_utf16().saturating_sub(self.selection_utf16_len());
460            let utf16_length_that_can_be_inserted =
461                max_length.saturating_sub(utf16_length_without_selection);
462            let Utf8CodeUnitLength(last_char_index) =
463                len_of_first_n_code_units(insert, utf16_length_that_can_be_inserted);
464            &insert.str()[..last_char_index]
465        } else {
466            &insert.str()
467        };
468        let string_to_insert = self.mode.normalize(string_to_insert);
469
470        let start = self.selection_start();
471        let end = self.selection_end();
472        let end_index_of_insertion = self.rope.replace_range(start..end, string_to_insert);
473
474        self.was_last_change_by_set_content = false;
475        self.clear_selection();
476        self.edit_point = end_index_of_insertion;
477    }
478
479    pub fn modify_edit_point(&mut self, amount: isize, movement: RopeMovement) {
480        if amount == 0 {
481            return;
482        }
483
484        // When moving by lines or if we do not have a selection, we do actually move
485        // the edit point from its position.
486        if matches!(movement, RopeMovement::Line) || !self.has_uncollapsed_selection() {
487            self.clear_selection();
488            self.edit_point = self.rope.move_by(self.edit_point, movement, amount);
489            return;
490        }
491
492        // If there's a selection and we are moving by words or characters, we just collapse
493        // the selection in the direction of the motion.
494        let new_edit_point = if amount > 0 {
495            self.selection_end()
496        } else {
497            self.selection_start()
498        };
499        self.clear_selection();
500        self.edit_point = new_edit_point;
501    }
502
503    pub fn modify_selection(&mut self, amount: isize, movement: RopeMovement) {
504        let old_edit_point = self.edit_point;
505        self.edit_point = self.rope.move_by(old_edit_point, movement, amount);
506
507        if self.selection_origin.is_none() {
508            self.selection_origin = Some(old_edit_point);
509        }
510        self.update_selection_direction();
511    }
512
513    pub fn modify_selection_or_edit_point(
514        &mut self,
515        amount: isize,
516        movement: RopeMovement,
517        select: Selection,
518    ) {
519        match select {
520            Selection::Selected => self.modify_selection(amount, movement),
521            Selection::NotSelected => self.modify_edit_point(amount, movement),
522        }
523        self.assert_ok_selection();
524    }
525
526    /// Update the field selection_direction.
527    ///
528    /// When the edit_point (or focus) is before the selection_origin (or anchor)
529    /// you have a backward selection. Otherwise you have a forward selection.
530    fn update_selection_direction(&mut self) {
531        debug!(
532            "edit_point: {:?}, selection_origin: {:?}",
533            self.edit_point, self.selection_origin
534        );
535        self.selection_direction = if Some(self.edit_point) < self.selection_origin {
536            SelectionDirection::Backward
537        } else {
538            SelectionDirection::Forward
539        }
540    }
541
542    /// Deal with a newline input.
543    pub fn handle_return(&mut self) -> KeyReaction {
544        match self.mode {
545            Lines::Multiple => {
546                self.insert('\n');
547                KeyReaction::DispatchInput(
548                    None,
549                    IsComposing::NotComposing,
550                    InputType::InsertLineBreak,
551                )
552            },
553            Lines::Single => KeyReaction::TriggerDefaultAction,
554        }
555    }
556
557    /// Select all text in the input control.
558    pub fn select_all(&mut self) {
559        self.selection_origin = Some(RopeIndex::default());
560        self.edit_point = self.rope.last_index();
561        self.selection_direction = SelectionDirection::Forward;
562        self.assert_ok_selection();
563    }
564
565    /// Remove the current selection.
566    pub fn clear_selection(&mut self) {
567        self.selection_origin = None;
568        self.selection_direction = SelectionDirection::None;
569    }
570
571    /// Remove the current selection and set the edit point to the end of the content.
572    pub(crate) fn clear_selection_to_end(&mut self) {
573        self.clear_selection();
574        self.edit_point = self.rope.last_index();
575    }
576
577    pub(crate) fn clear_selection_to_start(&mut self) {
578        self.clear_selection();
579        self.edit_point = Default::default();
580    }
581
582    /// Process a given `KeyboardEvent` and return an action for the caller to execute.
583    pub(crate) fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
584        let key = event.key();
585        let mods = event.modifiers();
586        self.handle_keydown_aux(key, mods, cfg!(target_os = "macos"))
587    }
588
589    // This function exists for easy unit testing.
590    // To test Mac OS shortcuts on other systems a flag is passed.
591    pub fn handle_keydown_aux(
592        &mut self,
593        key: Key,
594        mut mods: Modifiers,
595        macos: bool,
596    ) -> KeyReaction {
597        let maybe_select = if mods.contains(Modifiers::SHIFT) {
598            Selection::Selected
599        } else {
600            Selection::NotSelected
601        };
602
603        let alt_or_control = if macos {
604            Modifiers::ALT
605        } else {
606            Modifiers::CONTROL
607        };
608
609        mods.remove(Modifiers::SHIFT);
610        ShortcutMatcher::new(KeyState::Down, key.clone(), mods)
611            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
612                self.modify_selection_or_edit_point(-1, RopeMovement::Word, maybe_select);
613                KeyReaction::RedrawSelection
614            })
615            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
616                self.modify_selection_or_edit_point(1, RopeMovement::Word, maybe_select);
617                KeyReaction::RedrawSelection
618            })
619            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'A', || {
620                self.modify_selection_or_edit_point(-1, RopeMovement::LineStartOrEnd, maybe_select);
621                KeyReaction::RedrawSelection
622            })
623            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'E', || {
624                self.modify_selection_or_edit_point(1, RopeMovement::LineStartOrEnd, maybe_select);
625                KeyReaction::RedrawSelection
626            })
627            .optional_shortcut(macos, Modifiers::CONTROL, 'A', || {
628                self.modify_selection_or_edit_point(-1, RopeMovement::LineStartOrEnd, maybe_select);
629                KeyReaction::RedrawSelection
630            })
631            .optional_shortcut(macos, Modifiers::CONTROL, 'E', || {
632                self.modify_selection_or_edit_point(1, RopeMovement::LineStartOrEnd, maybe_select);
633                KeyReaction::RedrawSelection
634            })
635            .shortcut(CMD_OR_CONTROL, 'A', || {
636                self.select_all();
637                KeyReaction::RedrawSelection
638            })
639            .shortcut(CMD_OR_CONTROL, 'X', || {
640                if let Some(text) = self.get_selection_text() {
641                    self.clipboard_provider.set_text(text);
642                    self.delete_selection();
643                }
644                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::DeleteByCut)
645            })
646            .shortcut(CMD_OR_CONTROL, 'C', || {
647                // TODO(stevennovaryo): we should not provide text to clipboard for type=password
648                if let Some(text) = self.get_selection_text() {
649                    self.clipboard_provider.set_text(text);
650                }
651                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputType::Nothing)
652            })
653            .shortcut(CMD_OR_CONTROL, 'V', || {
654                if let Ok(text_content) = self.clipboard_provider.get_text() {
655                    self.insert(&text_content);
656                    KeyReaction::DispatchInput(
657                        Some(text_content),
658                        IsComposing::NotComposing,
659                        InputType::InsertFromPaste,
660                    )
661                } else {
662                    KeyReaction::DispatchInput(
663                        Some("".to_string()),
664                        IsComposing::NotComposing,
665                        InputType::InsertFromPaste,
666                    )
667                }
668            })
669            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
670                if self.delete_unit_or_selection(RopeMovement::Grapheme, Direction::Forward) {
671                    KeyReaction::DispatchInput(
672                        None,
673                        IsComposing::NotComposing,
674                        InputType::DeleteContentForward,
675                    )
676                } else {
677                    KeyReaction::Nothing
678                }
679            })
680            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
681                if self.delete_unit_or_selection(RopeMovement::Grapheme, Direction::Backward) {
682                    KeyReaction::DispatchInput(
683                        None,
684                        IsComposing::NotComposing,
685                        InputType::DeleteContentBackward,
686                    )
687                } else {
688                    KeyReaction::Nothing
689                }
690            })
691            .shortcut(alt_or_control, Key::Named(NamedKey::Backspace), || {
692                if self.delete_unit_or_selection(RopeMovement::Word, Direction::Backward) {
693                    KeyReaction::DispatchInput(
694                        None,
695                        IsComposing::NotComposing,
696                        InputType::DeleteContentBackward,
697                    )
698                } else {
699                    KeyReaction::Nothing
700                }
701            })
702            .optional_shortcut(
703                macos,
704                Modifiers::META,
705                Key::Named(NamedKey::ArrowLeft),
706                || {
707                    self.modify_selection_or_edit_point(
708                        -1,
709                        RopeMovement::LineStartOrEnd,
710                        maybe_select,
711                    );
712                    KeyReaction::RedrawSelection
713                },
714            )
715            .optional_shortcut(
716                macos,
717                Modifiers::META,
718                Key::Named(NamedKey::ArrowRight),
719                || {
720                    self.modify_selection_or_edit_point(
721                        1,
722                        RopeMovement::LineStartOrEnd,
723                        maybe_select,
724                    );
725                    KeyReaction::RedrawSelection
726                },
727            )
728            .optional_shortcut(
729                macos,
730                Modifiers::META,
731                Key::Named(NamedKey::ArrowUp),
732                || {
733                    self.modify_selection_or_edit_point(
734                        -1,
735                        RopeMovement::RopeStartOrEnd,
736                        maybe_select,
737                    );
738                    KeyReaction::RedrawSelection
739                },
740            )
741            .optional_shortcut(
742                macos,
743                Modifiers::META,
744                Key::Named(NamedKey::ArrowDown),
745                || {
746                    self.modify_selection_or_edit_point(
747                        1,
748                        RopeMovement::RopeStartOrEnd,
749                        maybe_select,
750                    );
751                    KeyReaction::RedrawSelection
752                },
753            )
754            .shortcut(alt_or_control, Key::Named(NamedKey::ArrowLeft), || {
755                self.modify_selection_or_edit_point(-1, RopeMovement::Word, maybe_select);
756                KeyReaction::RedrawSelection
757            })
758            .shortcut(alt_or_control, Key::Named(NamedKey::ArrowRight), || {
759                self.modify_selection_or_edit_point(1, RopeMovement::Word, maybe_select);
760                KeyReaction::RedrawSelection
761            })
762            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
763                self.modify_selection_or_edit_point(-1, RopeMovement::Grapheme, maybe_select);
764                KeyReaction::RedrawSelection
765            })
766            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
767                self.modify_selection_or_edit_point(1, RopeMovement::Grapheme, maybe_select);
768                KeyReaction::RedrawSelection
769            })
770            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
771                self.modify_selection_or_edit_point(-1, RopeMovement::Line, maybe_select);
772                KeyReaction::RedrawSelection
773            })
774            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
775                self.modify_selection_or_edit_point(1, RopeMovement::Line, maybe_select);
776                KeyReaction::RedrawSelection
777            })
778            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
779                self.handle_return()
780            })
781            .optional_shortcut(
782                macos,
783                Modifiers::empty(),
784                Key::Named(NamedKey::Home),
785                || {
786                    self.modify_selection_or_edit_point(
787                        -1,
788                        RopeMovement::RopeStartOrEnd,
789                        maybe_select,
790                    );
791                    KeyReaction::RedrawSelection
792                },
793            )
794            .optional_shortcut(macos, Modifiers::empty(), Key::Named(NamedKey::End), || {
795                self.modify_selection_or_edit_point(1, RopeMovement::RopeStartOrEnd, maybe_select);
796                KeyReaction::RedrawSelection
797            })
798            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
799                self.modify_selection_or_edit_point(-28, RopeMovement::Line, maybe_select);
800                KeyReaction::RedrawSelection
801            })
802            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
803                self.modify_selection_or_edit_point(28, RopeMovement::Line, maybe_select);
804                KeyReaction::RedrawSelection
805            })
806            .otherwise(|| {
807                if let Key::Character(ref character) = key {
808                    self.insert(character);
809                    return KeyReaction::DispatchInput(
810                        Some(character.to_string()),
811                        IsComposing::NotComposing,
812                        InputType::InsertText,
813                    );
814                }
815                if matches!(key, Key::Named(NamedKey::Process)) {
816                    return KeyReaction::DispatchInput(
817                        None,
818                        IsComposing::Composing,
819                        InputType::Nothing,
820                    );
821                }
822                KeyReaction::Nothing
823            })
824            .unwrap()
825    }
826
827    pub(crate) fn handle_compositionend(&mut self, event: &CompositionEvent) -> KeyReaction {
828        let insertion = event.data().str();
829        if insertion.is_empty() {
830            self.clear_selection();
831            return KeyReaction::RedrawSelection;
832        }
833
834        self.insert(insertion.to_string());
835        KeyReaction::DispatchInput(
836            Some(insertion.to_string()),
837            IsComposing::NotComposing,
838            InputType::InsertCompositionText,
839        )
840    }
841
842    pub(crate) fn handle_compositionupdate(&mut self, event: &CompositionEvent) -> KeyReaction {
843        let insertion = event.data().str();
844        if insertion.is_empty() {
845            return KeyReaction::Nothing;
846        }
847
848        let start = self.selection_start_offset();
849        let insertion = insertion.to_string();
850        self.insert(insertion.clone());
851        self.set_selection_range_utf8(
852            start,
853            start + event.data().len_utf8(),
854            SelectionDirection::Forward,
855        );
856        KeyReaction::DispatchInput(
857            Some(insertion),
858            IsComposing::Composing,
859            InputType::InsertCompositionText,
860        )
861    }
862
863    fn edit_point_for_mouse_event(&self, node: &Node, event: &MouseEvent) -> RopeIndex {
864        node.owner_window()
865            .text_index_query_on_node_for_event(node, event)
866            .map(|grapheme_index| {
867                self.rope.move_by(
868                    Default::default(),
869                    RopeMovement::Character,
870                    grapheme_index as isize,
871                )
872            })
873            .unwrap_or_else(|| self.rope.last_index())
874    }
875
876    /// Handle a mouse even that has happened in this [`TextInput`]. Returns `true` if the selection
877    /// in the input may have changed and `false` otherwise.
878    pub(crate) fn handle_mouse_event(&mut self, node: &Node, mouse_event: &MouseEvent) -> bool {
879        // Cancel any ongoing drags if we see a mouseup of any kind or notice
880        // that a button other than the primary button is pressed.
881        let event_type = mouse_event.upcast::<Event>().type_();
882        if event_type == atom!("mouseup") || mouse_event.Buttons() & 1 != 1 {
883            self.currently_dragging = false;
884        }
885
886        if event_type == atom!("mousedown") {
887            return self.handle_mousedown(node, mouse_event);
888        }
889
890        if event_type == atom!("mousemove") && self.currently_dragging {
891            self.edit_point = self.edit_point_for_mouse_event(node, mouse_event);
892            self.update_selection_direction();
893            return true;
894        }
895
896        false
897    }
898
899    /// Handle a "mousedown" event that happened on this [`TextInput`], belonging to the
900    /// given [`Node`].
901    ///
902    /// Returns `true` if the [`TextInput`] changed at all or `false` otherwise.
903    fn handle_mousedown(&mut self, node: &Node, mouse_event: &MouseEvent) -> bool {
904        assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
905
906        // Only update the cursor in text fields when the primary buton is pressed.
907        //
908        // From <https://w3c.github.io/uievents/#dom-mouseevent-button>:
909        // > 0 MUST indicate the primary button of the device (in general, the left button
910        // > or the only button on single-button devices, used to activate a user interface
911        // > control or select text) or the un-initialized value.
912        if mouse_event.Button() != 0 {
913            return false;
914        }
915
916        self.currently_dragging = true;
917        match mouse_event.upcast::<UIEvent>().Detail() {
918            3 => {
919                let word_boundaries = self.rope.line_boundaries(self.edit_point);
920                self.edit_point = word_boundaries.end;
921                self.selection_origin = Some(word_boundaries.start);
922                self.update_selection_direction();
923                true
924            },
925            2 => {
926                let word_boundaries = self.rope.relevant_word_boundaries(self.edit_point);
927                self.edit_point = word_boundaries.end;
928                self.selection_origin = Some(word_boundaries.start);
929                self.update_selection_direction();
930                true
931            },
932            1 => {
933                self.clear_selection();
934                self.edit_point = self.edit_point_for_mouse_event(node, mouse_event);
935                self.selection_origin = Some(self.edit_point);
936                self.update_selection_direction();
937                true
938            },
939            _ => {
940                // We currently don't do anything for higher click counts, but some platforms do.
941                // We should re-examine this when implementing support for platform-specific editing
942                // behaviors.
943                false
944            },
945        }
946    }
947
948    /// Whether the content is empty.
949    pub(crate) fn is_empty(&self) -> bool {
950        self.rope.is_empty()
951    }
952
953    /// The total number of code units required to encode the content in utf16.
954    pub(crate) fn len_utf16(&self) -> Utf16CodeUnitLength {
955        self.rope.len_utf16()
956    }
957
958    /// Get the current contents of the text input. Multiple lines are joined by \n.
959    pub fn get_content(&self) -> DOMString {
960        self.rope.contents().into()
961    }
962
963    /// Set the current contents of the text input. If this is control supports multiple lines,
964    /// any \n encountered will be stripped and force a new logical line.
965    ///
966    /// Note that when the [`Rope`] is in single line mode, this will **not** strip newlines.
967    /// Newline stripping only happens for incremental updates to the [`Rope`] as `<input>`
968    /// elements currently need to store unsanitized values while being created.
969    pub fn set_content(&mut self, content: DOMString) {
970        self.rope = Rope::new(
971            content
972                .str()
973                .to_string()
974                .replace("\r\n", "\n")
975                .replace("\r", "\n"),
976        );
977        self.was_last_change_by_set_content = true;
978
979        self.edit_point = self.rope.normalize_index(self.edit_point());
980        self.selection_origin = self
981            .selection_origin
982            .map(|selection_origin| self.rope.normalize_index(selection_origin));
983    }
984
985    pub fn set_selection_range_utf16(
986        &mut self,
987        start: Utf16CodeUnitLength,
988        end: Utf16CodeUnitLength,
989        direction: SelectionDirection,
990    ) {
991        self.set_selection_range_utf8(
992            self.rope.utf16_offset_to_utf8_offset(start),
993            self.rope.utf16_offset_to_utf8_offset(end),
994            direction,
995        );
996    }
997
998    pub fn set_selection_range_utf8(
999        &mut self,
1000        mut start: Utf8CodeUnitLength,
1001        mut end: Utf8CodeUnitLength,
1002        direction: SelectionDirection,
1003    ) {
1004        let text_end = self.get_content().len_utf8();
1005        if end > text_end {
1006            end = text_end;
1007        }
1008        if start > end {
1009            start = end;
1010        }
1011
1012        self.selection_direction = direction;
1013
1014        match direction {
1015            SelectionDirection::None | SelectionDirection::Forward => {
1016                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(start));
1017                self.edit_point = self.rope.utf8_offset_to_rope_index(end);
1018            },
1019            SelectionDirection::Backward => {
1020                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(end));
1021                self.edit_point = self.rope.utf8_offset_to_rope_index(start);
1022            },
1023        }
1024
1025        self.assert_ok_selection();
1026    }
1027
1028    /// This implements step 3 onward from:
1029    ///
1030    ///  - <https://www.w3.org/TR/clipboard-apis/#copy-action>
1031    ///  - <https://www.w3.org/TR/clipboard-apis/#cut-action>
1032    ///  - <https://www.w3.org/TR/clipboard-apis/#paste-action>
1033    ///
1034    /// Earlier steps should have already been run by the callers.
1035    pub(crate) fn handle_clipboard_event(
1036        &mut self,
1037        clipboard_event: &ClipboardEvent,
1038    ) -> ClipboardEventReaction {
1039        let event = clipboard_event.upcast::<Event>();
1040        if !event.IsTrusted() {
1041            return ClipboardEventReaction::empty();
1042        }
1043
1044        // This step is common to all event types in the specification.
1045        // Step 3: If the event was not canceled, then
1046        if event.DefaultPrevented() {
1047            // Step 4: Else, if the event was canceled
1048            // Step 4.1: Return false.
1049            return ClipboardEventReaction::empty();
1050        }
1051
1052        let event_type = event.Type();
1053        match_domstring_ascii!(event_type,
1054            "copy" => {
1055                // These steps are from <https://www.w3.org/TR/clipboard-apis/#copy-action>:
1056                let selection = self.get_selection_text();
1057
1058                // Step 3.1 Copy the selected contents, if any, to the clipboard
1059                if let Some(text) = selection {
1060                    self.clipboard_provider.set_text(text);
1061                }
1062
1063                // Step 3.2 Fire a clipboard event named clipboardchange
1064                ClipboardEventReaction::new(ClipboardEventFlags::FireClipboardChangedEvent)
1065            },
1066            "cut" => {
1067                // These steps are from <https://www.w3.org/TR/clipboard-apis/#cut-action>:
1068                let selection = self.get_selection_text();
1069
1070                // Step 3.1 If there is a selection in an editable context where cutting is enabled, then
1071                let Some(text) = selection else {
1072                    // Step 3.2 Else, if there is no selection or the context is not editable, then
1073                    return ClipboardEventReaction::empty();
1074                };
1075
1076                // Step 3.1.1 Copy the selected contents, if any, to the clipboard
1077                self.clipboard_provider.set_text(text);
1078
1079                // Step 3.1.2 Remove the contents of the selection from the document and collapse the selection.
1080                self.delete_selection();
1081
1082                // Step 3.1.3 Fire a clipboard event named clipboardchange
1083                // Step 3.1.4 Queue tasks to fire any events that should fire due to the modification.
1084                ClipboardEventReaction::new(
1085                    ClipboardEventFlags::FireClipboardChangedEvent |
1086                        ClipboardEventFlags::QueueInputEvent,
1087                )
1088                .with_input_type(InputType::DeleteByCut)
1089            },
1090            "paste" => {
1091                // These steps are from <https://www.w3.org/TR/clipboard-apis/#paste-action>:
1092                let Some(data_transfer) = clipboard_event.get_clipboard_data() else {
1093                    return ClipboardEventReaction::empty();
1094                };
1095                let Some(drag_data_store) = data_transfer.data_store() else {
1096                    return ClipboardEventReaction::empty();
1097                };
1098
1099                // Step 3.1: If there is a selection or cursor in an editable context where pasting is
1100                // enabled, then:
1101                // TODO: Our TextInput always has a selection or an input point. It's likely that this
1102                // shouldn't be the case when the entry loses the cursor.
1103
1104                // Step 3.1.1: Insert the most suitable content found on the clipboard, if any, into the
1105                // context.
1106                // TODO: Only text content is currently supported, but other data types should be supported
1107                // in the future.
1108                let Some(text_content) =
1109                    drag_data_store
1110                        .iter_item_list()
1111                        .find_map(|item| match item {
1112                            Kind::Text { data, .. } => Some(data.to_string()),
1113                            _ => None,
1114                        })
1115                else {
1116                    return ClipboardEventReaction::empty();
1117                };
1118                if text_content.is_empty() {
1119                    return ClipboardEventReaction::empty();
1120                }
1121
1122                self.insert(&text_content);
1123
1124                // Step 3.1.2: Queue tasks to fire any events that should fire due to the
1125                // modification, see ยง 5.3 Integration with other scripts and events for details.
1126                ClipboardEventReaction::new(ClipboardEventFlags::QueueInputEvent)
1127                    .with_text(text_content)
1128                    .with_input_type(InputType::InsertFromPaste)
1129            },
1130        _ => ClipboardEventReaction::empty(),)
1131    }
1132
1133    /// <https://w3c.github.io/uievents/#event-type-input>
1134    pub(crate) fn queue_input_event(
1135        &self,
1136        target: &EventTarget,
1137        data: Option<String>,
1138        is_composing: IsComposing,
1139        input_type: InputType,
1140    ) {
1141        let global = target.global();
1142        let target = Trusted::new(target);
1143        global.task_manager().user_interaction_task_source().queue(
1144            task!(fire_input_event: move || {
1145                let target = target.root();
1146                let global = target.global();
1147                let window = global.as_window();
1148                let event = InputEvent::new(
1149                    window,
1150                    None,
1151                    atom!("input"),
1152                    true,
1153                    false,
1154                    Some(window),
1155                    0,
1156                    data.map(DOMString::from),
1157                    is_composing.into(),
1158                    input_type.as_str().into(),
1159                    CanGc::note(),
1160                );
1161                let event = event.upcast::<Event>();
1162                event.set_composed(true);
1163                event.fire(&target, CanGc::note());
1164            }),
1165        );
1166    }
1167}