Skip to main content

script/dom/html/form_controls/
text_input.rs

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