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