Skip to main content

script/dom/html/form_controls/
text_input.rs

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