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 embedder_traits::{EmbedderMsg, MouseButton, ScriptToEmbedderChan};
12use keyboard_types::{Key, KeyState, Modifiers, NamedKey, ShortcutMatcher};
13use script_bindings::codegen::GenericBindings::UIEventBinding::UIEventMethods;
14use script_bindings::match_domstring_ascii;
15use script_bindings::root::Dom;
16use script_bindings::trace::CustomTraceable;
17use script_traits::MouseButtons;
18use servo_base::generic_channel::GenericCallback;
19use servo_base::id::WebViewId;
20use servo_base::text::{AssumeUnder4GB, RangeAny, Utf8CodeUnits, Utf16CodeUnits, Utf32CodeUnits};
21use servo_base::{Rope, RopeIndex, RopeMovement, RopeSlice};
22
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::str::DOMString;
25use crate::dom::compositionevent::CompositionEvent;
26use crate::dom::event::Event;
27use crate::dom::inputevent::HitTestResult;
28use crate::dom::keyboardevent::KeyboardEvent;
29use crate::dom::mouseevent::MouseEvent;
30use crate::dom::text_control::TextControlElement;
31use crate::dom::types::{HTMLInputElement, HTMLTextAreaElement, UIEvent};
32use crate::dom::{Element, NodeTraits};
33use crate::drag::drag_gesture::{DragGesture, DragHandler};
34
35/// A trait which abstracts access to the embedder's clipboard in order to allow unit
36/// testing clipboard-dependent parts of `script`.
37pub trait ClipboardProvider {
38    /// Get the text content of the clipboard.
39    fn get_text(&mut self) -> Result<String, String>;
40    /// Set the text content of the clipboard.
41    fn set_text(&mut self, _: String);
42}
43
44#[derive(MallocSizeOf)]
45pub(crate) struct EmbedderClipboardProvider {
46    pub embedder_sender: ScriptToEmbedderChan,
47    pub webview_id: WebViewId,
48}
49
50impl ClipboardProvider for EmbedderClipboardProvider {
51    fn get_text(&mut self) -> Result<String, String> {
52        let (callback, rx) = GenericCallback::new_blocking().unwrap();
53        self.embedder_sender
54            .send(EmbedderMsg::GetClipboardText(self.webview_id, callback))
55            .unwrap();
56        rx.recv().unwrap()
57    }
58    fn set_text(&mut self, s: String) {
59        self.embedder_sender
60            .send(EmbedderMsg::SetClipboardText(self.webview_id, s))
61            .unwrap();
62    }
63}
64
65#[derive(Clone, Copy, PartialEq)]
66pub enum Selection {
67    Selected,
68    NotSelected,
69}
70
71#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
72pub enum SelectionDirection {
73    Forward,
74    Backward,
75    None,
76}
77
78impl From<DOMString> for SelectionDirection {
79    fn from(direction: DOMString) -> SelectionDirection {
80        match_domstring_ascii!(direction,
81            "forward" => SelectionDirection::Forward,
82            "backward" => SelectionDirection::Backward,
83            _ => SelectionDirection::None,
84        )
85    }
86}
87
88impl From<SelectionDirection> for DOMString {
89    fn from(direction: SelectionDirection) -> DOMString {
90        match direction {
91            SelectionDirection::Forward => DOMString::from_static("forward"),
92            SelectionDirection::Backward => DOMString::from_static("backward"),
93            SelectionDirection::None => DOMString::from_static("none"),
94        }
95    }
96}
97
98#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
99pub enum Lines {
100    Single,
101    Multiple,
102}
103
104impl Lines {
105    fn normalize(&self, contents: impl Into<String>) -> String {
106        let contents = contents.into().replace("\r\n", "\n");
107        match self {
108            Self::Multiple => {
109                // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
110                contents.replace("\r", "\n")
111            },
112            // https://infra.spec.whatwg.org/#strip-newlines
113            //
114            // Browsers generally seem to convert newlines to spaces, so we do the same.
115            Lines::Single => contents.replace(['\r', '\n'], " "),
116        }
117    }
118}
119
120#[derive(Clone, Copy, PartialEq)]
121pub(crate) struct SelectionState {
122    start: RopeIndex,
123    end: RopeIndex,
124    direction: SelectionDirection,
125}
126
127/// Encapsulated state for handling keyboard input in a single or multiline text input control.
128#[derive(JSTraceable, MallocSizeOf)]
129pub struct TextInput<T: ClipboardProvider> {
130    #[no_trace]
131    rope: Rope,
132
133    /// The type of [`TextInput`] this is. When in multi-line mode, the [`TextInput`] will
134    /// automatically split all inserted text into lines and incorporate them into
135    /// the [`Self::rope`]. When in single line mode, the inserted text will be stripped of
136    /// newlines.
137    mode: Lines,
138
139    /// Current cursor input point
140    #[no_trace]
141    edit_point: RopeIndex,
142
143    /// The current selection goes from the selection_origin until the edit_point. Note that the
144    /// selection_origin may be after the edit_point, in the case of a backward selection.
145    #[no_trace]
146    selection_origin: Option<RopeIndex>,
147    selection_direction: SelectionDirection,
148
149    #[ignore_malloc_size_of = "Can't easily measure this generic type"]
150    clipboard_provider: T,
151
152    /// The maximum number of UTF-16 code units this text input is allowed to hold.
153    ///
154    /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
155    max_length: Option<Utf16CodeUnits>,
156    min_length: Option<Utf16CodeUnits>,
157
158    /// Was last change made by set_content?
159    was_last_change_by_set_content: bool,
160
161    #[no_trace]
162    pub(crate) previous_selection_range: Range<RopeIndex>,
163    #[no_trace]
164    pub(crate) selection_for_layout: Option<RangeAny<Utf32CodeUnits>>,
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 InputEventType {
185    InsertText,
186    InsertLineBreak,
187    InsertFromPaste,
188    InsertCompositionText,
189    DeleteByCut,
190    DeleteContentBackward,
191    DeleteContentForward,
192    Nothing,
193}
194
195impl InputEventType {
196    pub(crate) fn as_str(&self) -> &str {
197        match *self {
198            Self::InsertText => "insertText",
199            Self::InsertLineBreak => "insertLineBreak",
200            Self::InsertFromPaste => "insertFromPaste",
201            Self::InsertCompositionText => "insertCompositionText",
202            Self::DeleteByCut => "deleteByCut",
203            Self::DeleteContentBackward => "deleteContentBackward",
204            Self::DeleteContentForward => "deleteContentForward",
205            Self::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, InputEventType),
214    RedrawSelection,
215    Nothing,
216}
217
218/// The direction in which to delete a character.
219#[derive(Clone, Copy, Eq, PartialEq)]
220pub enum Direction {
221    Forward,
222    Backward,
223}
224
225// Some shortcuts use Cmd on Mac and Control on other systems.
226#[cfg(target_os = "macos")]
227pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::META;
228#[cfg(not(target_os = "macos"))]
229pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
230
231impl<T: ClipboardProvider> TextInput<T> {
232    /// Instantiate a new text input control
233    pub fn new(lines: Lines, initial: DOMString, clipboard_provider: T) -> TextInput<T> {
234        Self {
235            rope: Rope::new(initial),
236            mode: lines,
237            edit_point: Default::default(),
238            selection_origin: None,
239            clipboard_provider,
240            max_length: Default::default(),
241            min_length: Default::default(),
242            selection_direction: SelectionDirection::None,
243            was_last_change_by_set_content: true,
244            previous_selection_range: Default::default(),
245            selection_for_layout: None,
246        }
247    }
248
249    pub fn edit_point(&self) -> RopeIndex {
250        self.edit_point
251    }
252
253    pub fn selection_origin(&self) -> Option<RopeIndex> {
254        self.selection_origin
255    }
256
257    /// The selection origin, or the edit point if there is no selection. Note that the selection
258    /// origin may be after the edit point, in the case of a backward selection.
259    pub fn selection_origin_or_edit_point(&self) -> RopeIndex {
260        self.selection_origin.unwrap_or(self.edit_point)
261    }
262
263    pub fn selection_direction(&self) -> SelectionDirection {
264        self.selection_direction
265    }
266
267    pub fn set_max_length(&mut self, length: Option<Utf16CodeUnits>) {
268        self.max_length = length;
269    }
270
271    pub fn set_min_length(&mut self, length: Option<Utf16CodeUnits>) {
272        self.min_length = length;
273    }
274
275    /// Was last edit made by set_content?
276    pub(crate) fn was_last_change_by_set_content(&self) -> bool {
277        self.was_last_change_by_set_content
278    }
279
280    /// If there is an uncollapsed selection, delete it, otherwise do nothing. Returns
281    /// true if any text was deleted.
282    pub(crate) fn delete_selection(&mut self) -> bool {
283        if self.selection_start() == self.selection_end() {
284            return false;
285        }
286        self.replace_selection(&DOMString::new());
287        true
288    }
289
290    /// If there is an uncollapsed selection, delete it. Otherwise delete the given [`unit`]
291    /// worth of text in [`direction`] Remove a character at the current editing point
292    ///
293    /// Returns true if any text was deleted.
294    pub fn delete_unit_or_selection(&mut self, unit: RopeMovement, direction: Direction) -> bool {
295        if !self.has_uncollapsed_selection() {
296            let amount = match direction {
297                Direction::Forward => 1,
298                Direction::Backward => -1,
299            };
300            self.modify_selection(amount, unit);
301        }
302        self.delete_selection()
303    }
304
305    /// Insert a string at the current editing point or replace the selection if
306    /// one exists.
307    pub fn insert<S: Into<String>>(&mut self, string: S) {
308        if self.selection_origin.is_none() {
309            self.selection_origin = Some(self.edit_point);
310        }
311        self.replace_selection(&DOMString::from(string.into()));
312    }
313
314    /// The start of the selection (or the edit point, if there is no selection). Always less than
315    /// or equal to selection_end(), regardless of the selection direction.
316    pub fn selection_start(&self) -> RopeIndex {
317        match self.selection_direction {
318            SelectionDirection::None | SelectionDirection::Forward => {
319                self.selection_origin_or_edit_point()
320            },
321            SelectionDirection::Backward => self.edit_point,
322        }
323    }
324
325    pub(crate) fn selection_start_utf16(&self) -> Utf16CodeUnits {
326        self.rope.index_to_utf16_offset(self.selection_start())
327    }
328
329    /// The byte offset of the selection_start()
330    fn selection_start_offset(&self) -> Utf8CodeUnits {
331        self.rope.index_to_utf8_offset(self.selection_start())
332    }
333
334    /// The end of the selection (or the edit point, if there is no selection). Always greater
335    /// than or equal to selection_start(), regardless of the selection direction.
336    pub fn selection_end(&self) -> RopeIndex {
337        match self.selection_direction {
338            SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
339            SelectionDirection::Backward => self.selection_origin_or_edit_point(),
340        }
341    }
342
343    pub(crate) fn selection_end_utf16(&self) -> Utf16CodeUnits {
344        self.rope.index_to_utf16_offset(self.selection_end())
345    }
346
347    /// Whether or not there is an active uncollapsed selection. This means that the
348    /// selection origin is set and it differs from the edit point.
349    #[inline]
350    pub(crate) fn has_uncollapsed_selection(&self) -> bool {
351        self.selection_origin
352            .is_some_and(|selection_origin| selection_origin != self.edit_point)
353    }
354
355    /// Return the selection range as UTF-32 offsets from the start of the content.
356    ///
357    /// If there is no selection, returns an empty range at the edit point.
358    ///
359    /// If the start or/and end of the range is at the start/end of the text,
360    /// return a `RangeAny` unbounded on that side.
361    pub(crate) fn sorted_selection_character_offsets_range(&self) -> RangeAny<Utf32CodeUnits> {
362        let rope = &self.rope;
363        let start = self.selection_start();
364        let end = self.selection_end();
365        let start = (start != rope.first_index()).then(|| rope.index_to_character_offset(start));
366        // TODO: `TextInputWidgetShadowTree::update` has a hack with a "\u{200B}" to force
367        // the text to be non-empty, so `rope.last_index()` is untrustworthy.
368        // For now, use a bounded end unconditionally instead.
369        // let end = (end != rope.last_index()).then(|| rope.index_to_character_offset(end));
370        let end = Some(rope.index_to_character_offset(end));
371        RangeAny::new(start, end)
372    }
373
374    /// The state of the current selection. Can be used to compare whether selection state has changed.
375    pub(crate) fn selection_state(&self) -> SelectionState {
376        SelectionState {
377            start: self.selection_start(),
378            end: self.selection_end(),
379            direction: self.selection_direction,
380        }
381    }
382
383    // Check that the selection is valid.
384    fn assert_ok_selection(&self) {
385        debug!(
386            "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
387            self.edit_point, self.selection_origin, self.selection_direction
388        );
389
390        debug_assert_eq!(self.edit_point, self.rope.normalize_index(self.edit_point));
391        if let Some(selection_origin) = self.selection_origin {
392            debug_assert_eq!(
393                selection_origin,
394                self.rope.normalize_index(selection_origin)
395            );
396            match self.selection_direction {
397                SelectionDirection::None | SelectionDirection::Forward => {
398                    debug_assert!(selection_origin <= self.edit_point)
399                },
400                SelectionDirection::Backward => debug_assert!(self.edit_point <= selection_origin),
401            }
402        }
403    }
404
405    fn selection_slice(&self) -> RopeSlice<'_> {
406        self.rope
407            .slice(Some(self.selection_start()), Some(self.selection_end()))
408    }
409
410    pub(crate) fn selection_content(&self) -> Option<String> {
411        let text: String = self.selection_slice().into();
412        if text.is_empty() {
413            return None;
414        }
415        Some(text)
416    }
417
418    /// The length of the selected text in UTF-16 code units.
419    fn selection_utf16_len(&self) -> Utf16CodeUnits {
420        self.selection_slice().len_utf16()
421    }
422
423    /// Replace the current selection with the given [`DOMString`]. If the [`Rope`] is in
424    /// single line mode this *will* strip newlines, as opposed to [`Self::set_content`],
425    /// which does not.
426    pub fn replace_selection(&mut self, insert: &DOMString) {
427        let string_to_insert = if let Some(max_length) = self.max_length {
428            let utf16_length_without_selection =
429                self.len_utf16().saturating_sub(self.selection_utf16_len());
430            let utf16_length_that_can_be_inserted =
431                max_length.saturating_sub(utf16_length_without_selection);
432            // TODO: ensure that DOMString’s are under 4 GiB?
433            let last_char_index = usize::from(
434                utf16_length_that_can_be_inserted
435                    .to_utf8_code_units_in(AssumeUnder4GB, &insert.str()),
436            );
437            &insert.str()[..last_char_index]
438        } else {
439            &insert.str()
440        };
441        let string_to_insert = self.mode.normalize(string_to_insert);
442
443        let start = self.selection_start();
444        let end = self.selection_end();
445        let end_index_of_insertion = self.rope.replace_range(start..end, string_to_insert);
446
447        self.was_last_change_by_set_content = false;
448        self.clear_selection();
449        self.edit_point = end_index_of_insertion;
450    }
451
452    pub fn modify_edit_point(&mut self, amount: isize, movement: RopeMovement) {
453        if amount == 0 {
454            return;
455        }
456
457        // When moving by lines or if we do not have a selection, we do actually move
458        // the edit point from its position.
459        if matches!(movement, RopeMovement::Line) || !self.has_uncollapsed_selection() {
460            self.clear_selection();
461            self.edit_point = self.rope.move_by(self.edit_point, movement, amount);
462            return;
463        }
464
465        // If there's a selection and we are moving by words or characters, we just collapse
466        // the selection in the direction of the motion.
467        let new_edit_point = if amount > 0 {
468            self.selection_end()
469        } else {
470            self.selection_start()
471        };
472        self.clear_selection();
473        self.edit_point = new_edit_point;
474    }
475
476    pub fn modify_selection(&mut self, amount: isize, movement: RopeMovement) {
477        let old_edit_point = self.edit_point;
478        self.edit_point = self.rope.move_by(old_edit_point, movement, amount);
479
480        if self.selection_origin.is_none() {
481            self.selection_origin = Some(old_edit_point);
482        }
483        self.update_selection_direction();
484    }
485
486    pub fn modify_selection_or_edit_point(
487        &mut self,
488        amount: isize,
489        movement: RopeMovement,
490        select: Selection,
491    ) {
492        match select {
493            Selection::Selected => self.modify_selection(amount, movement),
494            Selection::NotSelected => self.modify_edit_point(amount, movement),
495        }
496        self.assert_ok_selection();
497    }
498
499    /// Update the field selection_direction.
500    ///
501    /// When the edit_point (or focus) is before the selection_origin (or anchor)
502    /// you have a backward selection. Otherwise you have a forward selection.
503    fn update_selection_direction(&mut self) {
504        debug!(
505            "edit_point: {:?}, selection_origin: {:?}",
506            self.edit_point, self.selection_origin
507        );
508        self.selection_direction = if Some(self.edit_point) < self.selection_origin {
509            SelectionDirection::Backward
510        } else {
511            SelectionDirection::Forward
512        }
513    }
514
515    /// Deal with a newline input.
516    pub fn handle_return(&mut self) -> KeyReaction {
517        match self.mode {
518            Lines::Multiple => {
519                self.insert('\n');
520                KeyReaction::DispatchInput(
521                    None,
522                    IsComposing::NotComposing,
523                    InputEventType::InsertLineBreak,
524                )
525            },
526            Lines::Single => KeyReaction::TriggerDefaultAction,
527        }
528    }
529
530    /// Select all text in the input control.
531    pub fn select_all(&mut self) {
532        self.selection_origin = Some(RopeIndex::default());
533        self.edit_point = self.rope.last_index();
534        self.selection_direction = SelectionDirection::Forward;
535        self.assert_ok_selection();
536    }
537
538    /// Remove the current selection.
539    pub fn clear_selection(&mut self) {
540        self.selection_origin = None;
541        self.selection_direction = SelectionDirection::None;
542    }
543
544    /// Remove the current selection and set the edit point to the end of the content.
545    pub(crate) fn clear_selection_to_end(&mut self) {
546        self.clear_selection();
547        self.edit_point = self.rope.last_index();
548    }
549
550    pub(crate) fn clear_selection_to_start(&mut self) {
551        self.clear_selection();
552        self.edit_point = Default::default();
553    }
554
555    /// Process a given `KeyboardEvent` and return an action for the caller to execute.
556    pub(crate) fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
557        let key = event.key();
558        let mods = event.modifiers();
559        self.handle_keydown_aux(key, mods, cfg!(target_os = "macos"))
560    }
561
562    // This function exists for easy unit testing.
563    // To test Mac OS shortcuts on other systems a flag is passed.
564    pub fn handle_keydown_aux(
565        &mut self,
566        key: Key,
567        mut mods: Modifiers,
568        macos: bool,
569    ) -> KeyReaction {
570        let maybe_select = if mods.contains(Modifiers::SHIFT) {
571            Selection::Selected
572        } else {
573            Selection::NotSelected
574        };
575
576        let alt_or_control = if macos {
577            Modifiers::ALT
578        } else {
579            Modifiers::CONTROL
580        };
581
582        mods.remove(Modifiers::SHIFT);
583        ShortcutMatcher::new(KeyState::Down, key.clone(), mods)
584            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
585                self.modify_selection_or_edit_point(-1, RopeMovement::Word, maybe_select);
586                KeyReaction::RedrawSelection
587            })
588            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
589                self.modify_selection_or_edit_point(1, RopeMovement::Word, maybe_select);
590                KeyReaction::RedrawSelection
591            })
592            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'A', || {
593                self.modify_selection_or_edit_point(-1, RopeMovement::LineStartOrEnd, maybe_select);
594                KeyReaction::RedrawSelection
595            })
596            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'E', || {
597                self.modify_selection_or_edit_point(1, RopeMovement::LineStartOrEnd, maybe_select);
598                KeyReaction::RedrawSelection
599            })
600            .optional_shortcut(macos, Modifiers::CONTROL, 'A', || {
601                self.modify_selection_or_edit_point(-1, RopeMovement::LineStartOrEnd, maybe_select);
602                KeyReaction::RedrawSelection
603            })
604            .optional_shortcut(macos, Modifiers::CONTROL, 'E', || {
605                self.modify_selection_or_edit_point(1, RopeMovement::LineStartOrEnd, maybe_select);
606                KeyReaction::RedrawSelection
607            })
608            .shortcut(CMD_OR_CONTROL, 'A', || {
609                self.select_all();
610                KeyReaction::RedrawSelection
611            })
612            .shortcut(CMD_OR_CONTROL, 'X', || {
613                if let Some(text) = self.selection_content() {
614                    self.clipboard_provider.set_text(text);
615                    self.delete_selection();
616                }
617                KeyReaction::DispatchInput(
618                    None,
619                    IsComposing::NotComposing,
620                    InputEventType::DeleteByCut,
621                )
622            })
623            .shortcut(CMD_OR_CONTROL, 'C', || {
624                // TODO(stevennovaryo): we should not provide text to clipboard for type=password
625                if let Some(text) = self.selection_content() {
626                    self.clipboard_provider.set_text(text);
627                }
628                KeyReaction::DispatchInput(None, IsComposing::NotComposing, InputEventType::Nothing)
629            })
630            .shortcut(CMD_OR_CONTROL, 'V', || {
631                if let Ok(text_content) = self.clipboard_provider.get_text() {
632                    self.insert(&text_content);
633                    KeyReaction::DispatchInput(
634                        Some(text_content),
635                        IsComposing::NotComposing,
636                        InputEventType::InsertFromPaste,
637                    )
638                } else {
639                    KeyReaction::DispatchInput(
640                        Some(String::new()),
641                        IsComposing::NotComposing,
642                        InputEventType::InsertFromPaste,
643                    )
644                }
645            })
646            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
647                if self.delete_unit_or_selection(RopeMovement::Grapheme, Direction::Forward) {
648                    KeyReaction::DispatchInput(
649                        None,
650                        IsComposing::NotComposing,
651                        InputEventType::DeleteContentForward,
652                    )
653                } else {
654                    KeyReaction::Nothing
655                }
656            })
657            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
658                if self.delete_unit_or_selection(RopeMovement::Grapheme, Direction::Backward) {
659                    KeyReaction::DispatchInput(
660                        None,
661                        IsComposing::NotComposing,
662                        InputEventType::DeleteContentBackward,
663                    )
664                } else {
665                    KeyReaction::Nothing
666                }
667            })
668            .shortcut(alt_or_control, Key::Named(NamedKey::Backspace), || {
669                if self.delete_unit_or_selection(RopeMovement::Word, Direction::Backward) {
670                    KeyReaction::DispatchInput(
671                        None,
672                        IsComposing::NotComposing,
673                        InputEventType::DeleteContentBackward,
674                    )
675                } else {
676                    KeyReaction::Nothing
677                }
678            })
679            .optional_shortcut(
680                macos,
681                Modifiers::META,
682                Key::Named(NamedKey::ArrowLeft),
683                || {
684                    self.modify_selection_or_edit_point(
685                        -1,
686                        RopeMovement::LineStartOrEnd,
687                        maybe_select,
688                    );
689                    KeyReaction::RedrawSelection
690                },
691            )
692            .optional_shortcut(
693                macos,
694                Modifiers::META,
695                Key::Named(NamedKey::ArrowRight),
696                || {
697                    self.modify_selection_or_edit_point(
698                        1,
699                        RopeMovement::LineStartOrEnd,
700                        maybe_select,
701                    );
702                    KeyReaction::RedrawSelection
703                },
704            )
705            .optional_shortcut(
706                macos,
707                Modifiers::META,
708                Key::Named(NamedKey::ArrowUp),
709                || {
710                    self.modify_selection_or_edit_point(
711                        -1,
712                        RopeMovement::RopeStartOrEnd,
713                        maybe_select,
714                    );
715                    KeyReaction::RedrawSelection
716                },
717            )
718            .optional_shortcut(
719                macos,
720                Modifiers::META,
721                Key::Named(NamedKey::ArrowDown),
722                || {
723                    self.modify_selection_or_edit_point(
724                        1,
725                        RopeMovement::RopeStartOrEnd,
726                        maybe_select,
727                    );
728                    KeyReaction::RedrawSelection
729                },
730            )
731            .shortcut(alt_or_control, Key::Named(NamedKey::ArrowLeft), || {
732                self.modify_selection_or_edit_point(-1, RopeMovement::Word, maybe_select);
733                KeyReaction::RedrawSelection
734            })
735            .shortcut(alt_or_control, Key::Named(NamedKey::ArrowRight), || {
736                self.modify_selection_or_edit_point(1, RopeMovement::Word, maybe_select);
737                KeyReaction::RedrawSelection
738            })
739            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
740                self.modify_selection_or_edit_point(-1, RopeMovement::Grapheme, maybe_select);
741                KeyReaction::RedrawSelection
742            })
743            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
744                self.modify_selection_or_edit_point(1, RopeMovement::Grapheme, maybe_select);
745                KeyReaction::RedrawSelection
746            })
747            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
748                self.modify_selection_or_edit_point(-1, RopeMovement::Line, maybe_select);
749                KeyReaction::RedrawSelection
750            })
751            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
752                self.modify_selection_or_edit_point(1, RopeMovement::Line, maybe_select);
753                KeyReaction::RedrawSelection
754            })
755            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
756                self.handle_return()
757            })
758            .optional_shortcut(
759                macos,
760                Modifiers::empty(),
761                Key::Named(NamedKey::Home),
762                || {
763                    self.modify_selection_or_edit_point(
764                        -1,
765                        RopeMovement::RopeStartOrEnd,
766                        maybe_select,
767                    );
768                    KeyReaction::RedrawSelection
769                },
770            )
771            .optional_shortcut(macos, Modifiers::empty(), Key::Named(NamedKey::End), || {
772                self.modify_selection_or_edit_point(1, RopeMovement::RopeStartOrEnd, maybe_select);
773                KeyReaction::RedrawSelection
774            })
775            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
776                self.modify_selection_or_edit_point(-28, RopeMovement::Line, maybe_select);
777                KeyReaction::RedrawSelection
778            })
779            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
780                self.modify_selection_or_edit_point(28, RopeMovement::Line, maybe_select);
781                KeyReaction::RedrawSelection
782            })
783            .otherwise(|| {
784                if let Key::Character(ref character) = key {
785                    self.insert(character);
786                    return KeyReaction::DispatchInput(
787                        Some(character.to_string()),
788                        IsComposing::NotComposing,
789                        InputEventType::InsertText,
790                    );
791                }
792                if matches!(key, Key::Named(NamedKey::Process)) {
793                    return KeyReaction::DispatchInput(
794                        None,
795                        IsComposing::Composing,
796                        InputEventType::Nothing,
797                    );
798                }
799                KeyReaction::Nothing
800            })
801            .unwrap()
802    }
803
804    pub(crate) fn handle_compositionend(&mut self, event: &CompositionEvent) -> KeyReaction {
805        let insertion = event.data().str();
806        if insertion.is_empty() {
807            self.clear_selection();
808            return KeyReaction::RedrawSelection;
809        }
810
811        self.insert(insertion.to_string());
812        KeyReaction::DispatchInput(
813            Some(insertion.to_string()),
814            IsComposing::NotComposing,
815            InputEventType::InsertCompositionText,
816        )
817    }
818
819    pub(crate) fn handle_compositionupdate(&mut self, event: &CompositionEvent) -> KeyReaction {
820        let insertion = event.data().str();
821        if insertion.is_empty() {
822            return KeyReaction::Nothing;
823        }
824
825        let start = self.selection_start_offset();
826        let insertion = insertion.to_string();
827        self.insert(insertion.clone());
828        self.set_selection_range_utf8(
829            start,
830            start + event.data().len_utf8(),
831            SelectionDirection::Forward,
832        );
833        KeyReaction::DispatchInput(
834            Some(insertion),
835            IsComposing::Composing,
836            InputEventType::InsertCompositionText,
837        )
838    }
839
840    fn edit_point_for_hit_test_result(&self, hit_test_result: &HitTestResult) -> RopeIndex {
841        hit_test_result
842            .dom_position_for_selection
843            .as_ref()
844            .map(|(_, character_offset)| {
845                self.rope.move_by(
846                    Default::default(),
847                    RopeMovement::Character,
848                    character_offset.0 as isize,
849                )
850            })
851            .unwrap_or_else(|| self.rope.last_index())
852    }
853
854    fn drag_moved(&mut self, element: &impl TextControlElement, hit_test_result: &HitTestResult) {
855        let point_in_viewport = hit_test_result.point_in_frame.map(Au::from_f32_px);
856        let element = element.as_element();
857        self.edit_point = element
858            .owner_window()
859            .text_index_query_on_node_for_event(element.upcast(), point_in_viewport)
860            .map(|(_, character_offset)| {
861                self.rope.move_by(
862                    Default::default(),
863                    RopeMovement::Character,
864                    character_offset.0 as isize,
865                )
866            })
867            .unwrap_or_else(|| self.rope.last_index());
868
869        self.update_selection_direction();
870    }
871
872    /// Handle a "mousedown" event that happened on this [`TextInput`], belonging to the
873    /// given [`Node`].
874    ///
875    /// Returns `true` if the [`TextInput`] changed at all or `false` otherwise.
876    pub(crate) fn handle_mousedown_event(
877        &mut self,
878        element: &Element,
879        mouse_event: &MouseEvent,
880        hit_test_result: &HitTestResult,
881    ) -> bool {
882        assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
883
884        let button = mouse_event.button();
885        let selection_changed = match mouse_event.upcast::<UIEvent>().Detail() {
886            3 if button == MouseButton::Primary => {
887                let word_boundaries = self.rope.line_boundaries(self.edit_point);
888                self.edit_point = word_boundaries.end;
889                self.selection_origin = Some(word_boundaries.start);
890                self.update_selection_direction();
891                true
892            },
893            2 if button == MouseButton::Primary => {
894                let word_boundaries = self.rope.relevant_word_boundaries(self.edit_point);
895                self.edit_point = word_boundaries.end;
896                self.selection_origin = Some(word_boundaries.start);
897                self.update_selection_direction();
898                true
899            },
900            1 if matches!(button, MouseButton::Primary | MouseButton::Auxiliary) => {
901                self.clear_selection();
902                self.edit_point = self.edit_point_for_hit_test_result(hit_test_result);
903                self.selection_origin = Some(self.edit_point);
904                self.update_selection_direction();
905                true
906            },
907            _ => {
908                // We currently don't do anything for higher click counts, but some platforms do.
909                // We should re-examine this when implementing support for platform-specific editing
910                // behaviors.
911                false
912            },
913        };
914
915        if selection_changed && mouse_event.buttons().contains(MouseButtons::Primary) {
916            element
917                .owner_document()
918                .event_handler()
919                .install_drag_gesture(DragGesture::new(DragHandler::TextInputSelection(
920                    TextInputSelectionDragHandler(Dom::from_ref(element)),
921                )));
922        }
923
924        selection_changed
925    }
926
927    /// Whether the content is empty.
928    pub(crate) fn is_empty(&self) -> bool {
929        self.rope.is_empty()
930    }
931
932    /// The total number of code units required to encode the content in utf16.
933    pub(crate) fn len_utf16(&self) -> Utf16CodeUnits {
934        self.rope.len_utf16()
935    }
936
937    /// Get the current contents of the text input. Multiple lines are joined by \n.
938    pub fn get_content(&self) -> DOMString {
939        self.rope.contents().into()
940    }
941
942    /// Set the current contents of the text input. If this is control supports multiple lines,
943    /// any \n encountered will be stripped and force a new logical line.
944    ///
945    /// Note that when the [`Rope`] is in single line mode, this will **not** strip newlines.
946    /// Newline stripping only happens for incremental updates to the [`Rope`] as `<input>`
947    /// elements currently need to store unsanitized values while being created.
948    pub fn set_content(&mut self, content: DOMString) {
949        self.rope = Rope::new(content.str().replace("\r\n", "\n").replace("\r", "\n"));
950        self.was_last_change_by_set_content = true;
951
952        self.edit_point = self.rope.normalize_index(self.edit_point());
953        self.selection_origin = self
954            .selection_origin
955            .map(|selection_origin| self.rope.normalize_index(selection_origin));
956    }
957
958    pub fn set_selection_range_utf16(
959        &mut self,
960        start: Utf16CodeUnits,
961        end: Utf16CodeUnits,
962        direction: SelectionDirection,
963    ) {
964        self.set_selection_range_utf8(
965            self.rope.utf16_offset_to_utf8_offset(start),
966            self.rope.utf16_offset_to_utf8_offset(end),
967            direction,
968        );
969    }
970
971    pub fn set_selection_range_utf8(
972        &mut self,
973        mut start: Utf8CodeUnits,
974        mut end: Utf8CodeUnits,
975        direction: SelectionDirection,
976    ) {
977        let text_end = self.get_content().len_utf8();
978        if end > text_end {
979            end = text_end;
980        }
981        if start > end {
982            start = end;
983        }
984
985        self.selection_direction = direction;
986
987        match direction {
988            SelectionDirection::None | SelectionDirection::Forward => {
989                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(start));
990                self.edit_point = self.rope.utf8_offset_to_rope_index(end);
991            },
992            SelectionDirection::Backward => {
993                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(end));
994                self.edit_point = self.rope.utf8_offset_to_rope_index(start);
995            },
996        }
997
998        self.assert_ok_selection();
999    }
1000}
1001
1002#[derive(JSTraceable, MallocSizeOf)]
1003#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
1004pub(crate) struct TextInputSelectionDragHandler(Dom<Element>);
1005
1006impl TextInputSelectionDragHandler {
1007    pub(crate) fn still_connected(&self) -> bool {
1008        self.0.is_connected()
1009    }
1010
1011    /// Process a mouse move event on this [`TextInputSelectionDragHandler`].
1012    ///
1013    /// Returns `true` if the drag should continue and `false` otherwise.
1014    pub(crate) fn moved(&self, hit_test_result: &HitTestResult) -> bool {
1015        if !self.0.is_connected() {
1016            return false;
1017        }
1018
1019        if let Some(input) = self.0.downcast::<HTMLInputElement>() {
1020            input.text_input_mut().drag_moved(input, hit_test_result);
1021            input.maybe_update_shared_selection();
1022            true
1023        } else if let Some(text_area) = self.0.downcast::<HTMLTextAreaElement>() {
1024            text_area
1025                .text_input_mut()
1026                .drag_moved(text_area, hit_test_result);
1027            text_area.maybe_update_shared_selection();
1028            true
1029        } else {
1030            false
1031        }
1032    }
1033}