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::{
12    EditingAction, EditingDirection, EditingMotion, EmbedderMsg, ModifySelection,
13    ScriptToEmbedderChan,
14};
15use js::context::JSContext;
16use layout_api::SegmentGranularity;
17use script_bindings::match_domstring_ascii;
18use script_bindings::root::Dom;
19use script_bindings::trace::CustomTraceable;
20use script_traits::MouseButtons;
21use servo_base::generic_channel::GenericCallback;
22use servo_base::id::WebViewId;
23use servo_base::text::{AssumeUnder4GB, RangeAny, Utf8CodeUnits, Utf16CodeUnits, Utf32CodeUnits};
24use servo_base::{Rope, RopeIndex, RopeMovement, RopeSlice};
25
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::str::DOMString;
28use crate::dom::editing::MouseButtonSelectionAction;
29use crate::dom::event::Event;
30use crate::dom::inputevent::HitTestResult;
31use crate::dom::mouseevent::MouseEvent;
32use crate::dom::text_control::TextControlElement;
33use crate::dom::types::{HTMLInputElement, HTMLTextAreaElement};
34use crate::dom::{Element, NodeTraits};
35use crate::drag::drag_gesture::{DragGesture, DragHandler};
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, Debug, JSTraceable, MallocSizeOf, PartialEq)]
68pub enum SelectionDirection {
69    Forward,
70    Backward,
71    None,
72}
73
74impl From<DOMString> for SelectionDirection {
75    fn from(direction: DOMString) -> SelectionDirection {
76        match_domstring_ascii!(direction,
77            "forward" => SelectionDirection::Forward,
78            "backward" => SelectionDirection::Backward,
79            _ => SelectionDirection::None,
80        )
81    }
82}
83
84impl From<SelectionDirection> for DOMString {
85    fn from(direction: SelectionDirection) -> DOMString {
86        match direction {
87            SelectionDirection::Forward => DOMString::from_static("forward"),
88            SelectionDirection::Backward => DOMString::from_static("backward"),
89            SelectionDirection::None => DOMString::from_static("none"),
90        }
91    }
92}
93
94#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
95pub enum Lines {
96    Single,
97    Multiple,
98}
99
100impl Lines {
101    fn normalize(&self, contents: impl Into<String>) -> String {
102        let contents = contents.into().replace("\r\n", "\n");
103        match self {
104            Self::Multiple => {
105                // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
106                contents.replace("\r", "\n")
107            },
108            // https://infra.spec.whatwg.org/#strip-newlines
109            //
110            // Browsers generally seem to convert newlines to spaces, so we do the same.
111            Lines::Single => contents.replace(['\r', '\n'], " "),
112        }
113    }
114}
115
116#[derive(Clone, Copy, PartialEq)]
117pub(crate) struct SelectionState {
118    start: RopeIndex,
119    end: RopeIndex,
120    direction: SelectionDirection,
121}
122
123/// Encapsulated state for handling keyboard input in a single or multiline text input control.
124#[derive(JSTraceable, MallocSizeOf)]
125pub struct TextInput<T: ClipboardProvider> {
126    #[no_trace]
127    rope: Rope,
128
129    /// The type of [`TextInput`] this is. When in multi-line mode, the [`TextInput`] will
130    /// automatically split all inserted text into lines and incorporate them into
131    /// the [`Self::rope`]. When in single line mode, the inserted text will be stripped of
132    /// newlines.
133    mode: Lines,
134
135    /// Current cursor input point
136    #[no_trace]
137    edit_point: RopeIndex,
138
139    /// The current selection goes from the selection_origin until the edit_point. Note that the
140    /// selection_origin may be after the edit_point, in the case of a backward selection.
141    #[no_trace]
142    selection_origin: Option<RopeIndex>,
143    selection_direction: SelectionDirection,
144
145    #[ignore_malloc_size_of = "Can't easily measure this generic type"]
146    clipboard_provider: T,
147
148    /// The maximum number of UTF-16 code units this text input is allowed to hold.
149    ///
150    /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
151    max_length: Option<Utf16CodeUnits>,
152    min_length: Option<Utf16CodeUnits>,
153
154    /// Was last change made by set_content?
155    was_last_change_by_set_content: bool,
156
157    #[no_trace]
158    pub(crate) previous_selection_range: Range<RopeIndex>,
159    #[no_trace]
160    pub(crate) selection_for_layout: Option<RangeAny<Utf32CodeUnits>>,
161}
162
163/// <https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes>
164#[derive(Clone, Copy, PartialEq)]
165pub enum InputEventType {
166    InsertText,
167    InsertLineBreak,
168    InsertFromPaste,
169    InsertCompositionText,
170    DeleteByCut,
171    DeleteContentBackward,
172    DeleteContentForward,
173}
174
175impl InputEventType {
176    pub(crate) fn as_str(&self) -> &str {
177        match *self {
178            Self::InsertText => "insertText",
179            Self::InsertLineBreak => "insertLineBreak",
180            Self::InsertFromPaste => "insertFromPaste",
181            Self::InsertCompositionText => "insertCompositionText",
182            Self::DeleteByCut => "deleteByCut",
183            Self::DeleteContentBackward => "deleteContentBackward",
184            Self::DeleteContentForward => "deleteContentForward",
185        }
186    }
187
188    /// <https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes>
189    pub(crate) fn is_beforeinput_cancelable(self) -> bool {
190        self != Self::InsertCompositionText
191    }
192
193    pub(crate) fn is_composing(self) -> bool {
194        self == Self::InsertCompositionText
195    }
196}
197
198/// Resulting action to be taken by the owner of a text input that is handling an event.
199#[derive(PartialEq)]
200pub enum KeyReaction {
201    TriggerDefaultAction,
202    DispatchInput(Option<String>, InputEventType),
203    RedrawSelection,
204    Nothing,
205}
206
207impl<T: ClipboardProvider> TextInput<T> {
208    /// Instantiate a new text input control
209    pub fn new(lines: Lines, initial: DOMString, clipboard_provider: T) -> TextInput<T> {
210        Self {
211            rope: Rope::new(initial),
212            mode: lines,
213            edit_point: Default::default(),
214            selection_origin: None,
215            clipboard_provider,
216            max_length: Default::default(),
217            min_length: Default::default(),
218            selection_direction: SelectionDirection::None,
219            was_last_change_by_set_content: true,
220            previous_selection_range: Default::default(),
221            selection_for_layout: None,
222        }
223    }
224
225    pub fn edit_point(&self) -> RopeIndex {
226        self.edit_point
227    }
228
229    pub fn selection_origin(&self) -> Option<RopeIndex> {
230        self.selection_origin
231    }
232
233    /// The selection origin, or the edit point if there is no selection. Note that the selection
234    /// origin may be after the edit point, in the case of a backward selection.
235    pub fn selection_origin_or_edit_point(&self) -> RopeIndex {
236        self.selection_origin.unwrap_or(self.edit_point)
237    }
238
239    pub fn selection_direction(&self) -> SelectionDirection {
240        self.selection_direction
241    }
242
243    pub fn set_max_length(&mut self, length: Option<Utf16CodeUnits>) {
244        self.max_length = length;
245    }
246
247    pub fn set_min_length(&mut self, length: Option<Utf16CodeUnits>) {
248        self.min_length = length;
249    }
250
251    /// Was last edit made by set_content?
252    pub(crate) fn was_last_change_by_set_content(&self) -> bool {
253        self.was_last_change_by_set_content
254    }
255
256    pub(crate) fn clone_content_from(&mut self, other: &Self) {
257        self.set_content(other.get_content());
258        self.was_last_change_by_set_content = other.was_last_change_by_set_content;
259    }
260
261    /// If there is an uncollapsed selection, delete it, otherwise do nothing. Returns
262    /// true if any text was deleted.
263    pub(crate) fn delete_selection(&mut self) -> bool {
264        if self.selection_start() == self.selection_end() {
265            return false;
266        }
267        self.replace_selection(&DOMString::new());
268        true
269    }
270
271    /// If there is an uncollapsed selection, delete it. Otherwise delete the given [`unit`]
272    /// worth of text in [`direction`] Remove a character at the current editing point
273    ///
274    /// Returns true if any text was deleted.
275    pub fn delete_unit_or_selection(
276        &mut self,
277        unit: RopeMovement,
278        direction: EditingDirection,
279    ) -> bool {
280        if !self.has_uncollapsed_selection() {
281            let amount = match direction {
282                EditingDirection::Forward => 1,
283                EditingDirection::Backward => -1,
284            };
285            self.modify_selection(amount, unit);
286        }
287        self.delete_selection()
288    }
289
290    /// Insert a string at the current editing point or replace the selection if
291    /// one exists.
292    pub fn insert<S: Into<String>>(&mut self, string: S) {
293        if self.selection_origin.is_none() {
294            self.selection_origin = Some(self.edit_point);
295        }
296        self.replace_selection(&DOMString::from(string.into()));
297    }
298
299    /// The start of the selection (or the edit point, if there is no selection). Always less than
300    /// or equal to selection_end(), regardless of the selection direction.
301    pub fn selection_start(&self) -> RopeIndex {
302        match self.selection_direction {
303            SelectionDirection::None | SelectionDirection::Forward => {
304                self.selection_origin_or_edit_point()
305            },
306            SelectionDirection::Backward => self.edit_point,
307        }
308    }
309
310    pub(crate) fn selection_start_utf16(&self) -> Utf16CodeUnits {
311        self.rope.index_to_utf16_offset(self.selection_start())
312    }
313
314    /// The byte offset of the selection_start()
315    fn selection_start_offset(&self) -> Utf8CodeUnits {
316        self.rope.index_to_utf8_offset(self.selection_start())
317    }
318
319    /// The end of the selection (or the edit point, if there is no selection). Always greater
320    /// than or equal to selection_start(), regardless of the selection direction.
321    pub fn selection_end(&self) -> RopeIndex {
322        match self.selection_direction {
323            SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
324            SelectionDirection::Backward => self.selection_origin_or_edit_point(),
325        }
326    }
327
328    pub(crate) fn selection_end_utf16(&self) -> Utf16CodeUnits {
329        self.rope.index_to_utf16_offset(self.selection_end())
330    }
331
332    /// Whether or not there is an active uncollapsed selection. This means that the
333    /// selection origin is set and it differs from the edit point.
334    #[inline]
335    pub(crate) fn has_uncollapsed_selection(&self) -> bool {
336        self.selection_origin
337            .is_some_and(|selection_origin| selection_origin != self.edit_point)
338    }
339
340    /// Return the selection range as UTF-32 offsets from the start of the content.
341    ///
342    /// If there is no selection, returns an empty range at the edit point.
343    ///
344    /// If the start or/and end of the range is at the start/end of the text,
345    /// return a `RangeAny` unbounded on that side.
346    pub(crate) fn sorted_selection_character_offsets_range(&self) -> RangeAny<Utf32CodeUnits> {
347        let rope = &self.rope;
348        let start = self.selection_start();
349        let end = self.selection_end();
350        let start = (start != rope.first_index()).then(|| rope.index_to_character_offset(start));
351        let end = (end != rope.last_index()).then(|| rope.index_to_character_offset(end));
352        RangeAny::new(start, end)
353    }
354
355    /// The state of the current selection. Can be used to compare whether selection state has changed.
356    pub(crate) fn selection_state(&self) -> SelectionState {
357        SelectionState {
358            start: self.selection_start(),
359            end: self.selection_end(),
360            direction: self.selection_direction,
361        }
362    }
363
364    // Check that the selection is valid.
365    fn assert_ok_selection(&self) {
366        debug!(
367            "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
368            self.edit_point, self.selection_origin, self.selection_direction
369        );
370
371        debug_assert_eq!(self.edit_point, self.rope.normalize_index(self.edit_point));
372        if let Some(selection_origin) = self.selection_origin {
373            debug_assert_eq!(
374                selection_origin,
375                self.rope.normalize_index(selection_origin)
376            );
377            match self.selection_direction {
378                SelectionDirection::None | SelectionDirection::Forward => {
379                    debug_assert!(selection_origin <= self.edit_point)
380                },
381                SelectionDirection::Backward => debug_assert!(self.edit_point <= selection_origin),
382            }
383        }
384    }
385
386    fn selection_slice(&self) -> RopeSlice<'_> {
387        self.rope
388            .slice(Some(self.selection_start()), Some(self.selection_end()))
389    }
390
391    pub(crate) fn selection_content(&self) -> Option<String> {
392        let text: String = self.selection_slice().into();
393        if text.is_empty() {
394            return None;
395        }
396        Some(text)
397    }
398
399    /// The length of the selected text in UTF-16 code units.
400    fn selection_utf16_len(&self) -> Utf16CodeUnits {
401        self.selection_slice().len_utf16()
402    }
403
404    /// Replace the current selection with the given [`DOMString`]. If the [`Rope`] is in
405    /// single line mode this *will* strip newlines, as opposed to [`Self::set_content`],
406    /// which does not.
407    pub fn replace_selection(&mut self, insert: &DOMString) {
408        let string_to_insert = if let Some(max_length) = self.max_length {
409            let utf16_length_without_selection =
410                self.len_utf16().saturating_sub(self.selection_utf16_len());
411            let utf16_length_that_can_be_inserted =
412                max_length.saturating_sub(utf16_length_without_selection);
413            // TODO: ensure that DOMString’s are under 4 GiB?
414            let last_char_index = usize::from(
415                utf16_length_that_can_be_inserted
416                    .to_utf8_code_units_in(AssumeUnder4GB, &insert.str()),
417            );
418            &insert.str()[..last_char_index]
419        } else {
420            &insert.str()
421        };
422        let string_to_insert = self.mode.normalize(string_to_insert);
423
424        let start = self.selection_start();
425        let end = self.selection_end();
426        let end_index_of_insertion = self.rope.replace_range(start..end, string_to_insert);
427
428        self.was_last_change_by_set_content = false;
429        self.clear_selection();
430        self.edit_point = end_index_of_insertion;
431    }
432
433    pub fn modify_edit_point(&mut self, amount: isize, movement: RopeMovement) {
434        if amount == 0 {
435            return;
436        }
437
438        // When moving by lines or if we do not have a selection, we do actually move
439        // the edit point from its position.
440        if matches!(movement, RopeMovement::Line) || !self.has_uncollapsed_selection() {
441            self.clear_selection();
442            self.edit_point = self.rope.move_by(self.edit_point, movement, amount);
443            return;
444        }
445
446        // If there's a selection and we are moving by words or characters, we just collapse
447        // the selection in the direction of the motion.
448        let new_edit_point = if amount > 0 {
449            self.selection_end()
450        } else {
451            self.selection_start()
452        };
453        self.clear_selection();
454        self.edit_point = new_edit_point;
455    }
456
457    pub fn modify_selection(&mut self, amount: isize, movement: RopeMovement) {
458        let old_edit_point = self.edit_point;
459        self.edit_point = self.rope.move_by(old_edit_point, movement, amount);
460
461        if self.selection_origin.is_none() {
462            self.selection_origin = Some(old_edit_point);
463        }
464        self.update_selection_direction();
465    }
466
467    pub fn modify_selection_or_edit_point(
468        &mut self,
469        amount: isize,
470        movement: RopeMovement,
471        update_selection: ModifySelection,
472    ) {
473        match update_selection {
474            ModifySelection::Yes => self.modify_selection(amount, movement),
475            ModifySelection::No => self.modify_edit_point(amount, movement),
476        }
477        self.assert_ok_selection();
478    }
479
480    pub fn mode(&self) -> Lines {
481        self.mode
482    }
483
484    /// Update the field selection_direction.
485    ///
486    /// When the edit_point (or focus) is before the selection_origin (or anchor)
487    /// you have a backward selection. Otherwise you have a forward selection.
488    fn update_selection_direction(&mut self) {
489        debug!(
490            "edit_point: {:?}, selection_origin: {:?}",
491            self.edit_point, self.selection_origin
492        );
493        self.selection_direction = if Some(self.edit_point) < self.selection_origin {
494            SelectionDirection::Backward
495        } else {
496            SelectionDirection::Forward
497        }
498    }
499
500    /// Deal with a newline input.
501    pub fn handle_return(&mut self) -> KeyReaction {
502        match self.mode {
503            Lines::Multiple => {
504                self.insert('\n');
505                KeyReaction::DispatchInput(None, InputEventType::InsertLineBreak)
506            },
507            Lines::Single => KeyReaction::TriggerDefaultAction,
508        }
509    }
510
511    /// Select all text in the input control.
512    pub fn select_all(&mut self) {
513        self.selection_origin = Some(RopeIndex::default());
514        self.edit_point = self.rope.last_index();
515        self.selection_direction = SelectionDirection::Forward;
516        self.assert_ok_selection();
517    }
518
519    /// Remove the current selection.
520    pub fn clear_selection(&mut self) {
521        self.selection_origin = None;
522        self.selection_direction = SelectionDirection::None;
523    }
524
525    /// Remove the current selection and set the edit point to the end of the content.
526    pub(crate) fn clear_selection_to_end(&mut self) {
527        self.clear_selection();
528        self.edit_point = self.rope.last_index();
529    }
530
531    pub(crate) fn clear_selection_to_start(&mut self) {
532        self.clear_selection();
533        self.edit_point = Default::default();
534    }
535
536    /// Process a given `EditingAction` and return an action for the caller to execute.
537    ///
538    /// This is public so that it can be used in external unit tests.
539    pub fn perform_editing_action(&mut self, action: EditingAction) -> KeyReaction {
540        match action {
541            EditingAction::MoveCursor(direction, motion, selection) => {
542                let movement = match motion {
543                    EditingMotion::Character => RopeMovement::Character,
544                    EditingMotion::Grapheme => RopeMovement::Grapheme,
545                    EditingMotion::Word => RopeMovement::Word,
546                    EditingMotion::Line => RopeMovement::Line,
547                    EditingMotion::LineStartOrEnd => RopeMovement::LineStartOrEnd,
548                    EditingMotion::Page => RopeMovement::Line,
549                    EditingMotion::DocumentStartOrEnd => RopeMovement::RopeStartOrEnd,
550                };
551                let amount = match (direction, motion) {
552                    // TODO: This should really be based on the size of the text area.
553                    (EditingDirection::Forward, EditingMotion::Page) => 28,
554                    (EditingDirection::Backward, EditingMotion::Page) => -28,
555                    (EditingDirection::Forward, _) => 1,
556                    (EditingDirection::Backward, _) => -1,
557                };
558                self.modify_selection_or_edit_point(amount, movement, selection);
559                KeyReaction::RedrawSelection
560            },
561            EditingAction::InsertNewline | EditingAction::InsertParagraph => self.handle_return(),
562            EditingAction::InsertText(text) => {
563                self.insert(&text);
564                KeyReaction::DispatchInput(Some(text), InputEventType::InsertText)
565            },
566            EditingAction::InsertCompositionText(text) => {
567                self.insert_for_composition(&text);
568                KeyReaction::DispatchInput(Some(text), InputEventType::InsertCompositionText)
569            },
570            EditingAction::EndComposition(..) => {
571                self.clear_selection();
572                KeyReaction::RedrawSelection
573            },
574            EditingAction::Delete => {
575                if self.delete_unit_or_selection(RopeMovement::Grapheme, EditingDirection::Forward)
576                {
577                    KeyReaction::DispatchInput(None, InputEventType::DeleteContentForward)
578                } else {
579                    KeyReaction::Nothing
580                }
581            },
582            EditingAction::Backspace(motion) => {
583                let movement = match motion {
584                    EditingMotion::Character => RopeMovement::Character,
585                    EditingMotion::Grapheme => RopeMovement::Grapheme,
586                    EditingMotion::Word => RopeMovement::Word,
587                    EditingMotion::Line => RopeMovement::Line,
588                    EditingMotion::LineStartOrEnd => RopeMovement::LineStartOrEnd,
589                    EditingMotion::Page => return KeyReaction::Nothing,
590                    EditingMotion::DocumentStartOrEnd => RopeMovement::RopeStartOrEnd,
591                };
592                if self.delete_unit_or_selection(movement, EditingDirection::Backward) {
593                    KeyReaction::DispatchInput(None, InputEventType::DeleteContentBackward)
594                } else {
595                    KeyReaction::Nothing
596                }
597            },
598            EditingAction::SelectAll |
599            EditingAction::Clipboard(..) |
600            EditingAction::StartComposition => KeyReaction::Nothing,
601        }
602    }
603
604    fn insert_for_composition<S: Into<String>>(&mut self, insertion: S) {
605        let start = self.selection_start_offset();
606        let insertion = insertion.into();
607        self.insert(&insertion);
608        self.set_selection_range_utf8(
609            start,
610            start + Utf8CodeUnits(insertion.len() as u32),
611            SelectionDirection::Forward,
612        );
613    }
614
615    fn edit_point_for_hit_test_result(&self, hit_test_result: &HitTestResult) -> RopeIndex {
616        hit_test_result
617            .dom_position_for_selection
618            .as_ref()
619            .map(|boundary| {
620                self.rope.move_by(
621                    Default::default(),
622                    RopeMovement::Character,
623                    boundary.utf32_offset().0 as isize,
624                )
625            })
626            .unwrap_or_else(|| self.rope.last_index())
627    }
628
629    fn drag_moved(
630        &mut self,
631        cx: &mut JSContext,
632        element: &impl TextControlElement,
633        hit_test_result: &HitTestResult,
634    ) {
635        let point_in_viewport = hit_test_result.point_in_frame.map(Au::from_f32_px);
636        let element = element.as_element();
637        self.edit_point = element
638            .owner_window()
639            .text_index_query_on_node_for_event(cx, element.upcast(), point_in_viewport)
640            .map(|(_, character_offset)| {
641                self.rope.move_by(
642                    Default::default(),
643                    RopeMovement::Character,
644                    character_offset.0 as isize,
645                )
646            })
647            .unwrap_or_else(|| self.rope.last_index());
648
649        self.update_selection_direction();
650    }
651
652    /// Handle a "mousedown" event that happened on this [`TextInput`], belonging to the
653    /// given [`Node`].
654    ///
655    /// Returns `true` if the [`TextInput`] changed at all or `false` otherwise.
656    pub(crate) fn handle_mousedown_event(
657        &mut self,
658        element: &Element,
659        mouse_event: &MouseEvent,
660        hit_test_result: &HitTestResult,
661    ) -> bool {
662        assert_eq!(mouse_event.upcast::<Event>().type_(), atom!("mousedown"));
663
664        let selection_changed = match MouseButtonSelectionAction::from_mouse_event(mouse_event) {
665            Some(MouseButtonSelectionAction::SelectRange(granularity)) => {
666                let boundaries = match granularity {
667                    SegmentGranularity::Word => self.rope.relevant_word_boundaries(self.edit_point),
668                    SegmentGranularity::HardLineBreak => self.rope.line_boundaries(self.edit_point),
669                };
670                self.edit_point = boundaries.end;
671                self.selection_origin = Some(boundaries.start);
672                self.update_selection_direction();
673                true
674            },
675            Some(MouseButtonSelectionAction::MoveCursor) => {
676                self.clear_selection();
677                self.edit_point = self.edit_point_for_hit_test_result(hit_test_result);
678                self.selection_origin = Some(self.edit_point);
679                self.update_selection_direction();
680                true
681            },
682            _ => false,
683        };
684
685        if selection_changed && mouse_event.buttons().contains(MouseButtons::Primary) {
686            element
687                .owner_document()
688                .event_handler()
689                .install_drag_gesture(DragGesture::new(DragHandler::TextInputSelection(
690                    TextInputSelectionDragHandler(Dom::from_ref(element)),
691                )));
692        }
693
694        selection_changed
695    }
696
697    /// Whether the content is empty.
698    pub(crate) fn is_empty(&self) -> bool {
699        self.rope.is_empty()
700    }
701
702    /// The total number of code units required to encode the content in utf16.
703    pub(crate) fn len_utf16(&self) -> Utf16CodeUnits {
704        self.rope.len_utf16()
705    }
706
707    /// Get the current contents of the text input. Multiple lines are joined by \n.
708    pub fn get_content(&self) -> DOMString {
709        self.rope.contents().into()
710    }
711
712    /// Set the current contents of the text input. If this is control supports multiple lines,
713    /// any \n encountered will be stripped and force a new logical line.
714    ///
715    /// Note that when the [`Rope`] is in single line mode, this will **not** strip newlines.
716    /// Newline stripping only happens for incremental updates to the [`Rope`] as `<input>`
717    /// elements currently need to store unsanitized values while being created.
718    pub fn set_content(&mut self, content: DOMString) {
719        self.rope = Rope::new(content.str().replace("\r\n", "\n").replace("\r", "\n"));
720        self.was_last_change_by_set_content = true;
721
722        self.edit_point = self.rope.normalize_index(self.edit_point());
723        self.selection_origin = self
724            .selection_origin
725            .map(|selection_origin| self.rope.normalize_index(selection_origin));
726    }
727
728    pub fn set_selection_range_utf16(
729        &mut self,
730        start: Utf16CodeUnits,
731        end: Utf16CodeUnits,
732        direction: SelectionDirection,
733    ) {
734        self.set_selection_range_utf8(
735            self.rope.utf16_offset_to_utf8_offset(start),
736            self.rope.utf16_offset_to_utf8_offset(end),
737            direction,
738        );
739    }
740
741    pub fn set_selection_range_utf8(
742        &mut self,
743        mut start: Utf8CodeUnits,
744        mut end: Utf8CodeUnits,
745        direction: SelectionDirection,
746    ) {
747        let text_end = self.get_content().len_utf8();
748        if end > text_end {
749            end = text_end;
750        }
751        if start > end {
752            start = end;
753        }
754
755        self.selection_direction = direction;
756
757        match direction {
758            SelectionDirection::None | SelectionDirection::Forward => {
759                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(start));
760                self.edit_point = self.rope.utf8_offset_to_rope_index(end);
761            },
762            SelectionDirection::Backward => {
763                self.selection_origin = Some(self.rope.utf8_offset_to_rope_index(end));
764                self.edit_point = self.rope.utf8_offset_to_rope_index(start);
765            },
766        }
767
768        self.assert_ok_selection();
769    }
770}
771
772#[derive(JSTraceable, MallocSizeOf)]
773#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
774pub(crate) struct TextInputSelectionDragHandler(Dom<Element>);
775
776impl TextInputSelectionDragHandler {
777    pub(crate) fn still_connected(&self) -> bool {
778        self.0.is_connected()
779    }
780
781    /// Process a mouse move event on this [`TextInputSelectionDragHandler`].
782    ///
783    /// Returns `true` if the drag should continue and `false` otherwise.
784    pub(crate) fn moved(&self, cx: &mut JSContext, hit_test_result: &HitTestResult) -> bool {
785        if !self.0.is_connected() {
786            return false;
787        }
788
789        if let Some(input) = self.0.downcast::<HTMLInputElement>() {
790            input
791                .text_input_mut()
792                .drag_moved(cx, input, hit_test_result);
793            input.maybe_update_shared_selection();
794            true
795        } else if let Some(text_area) = self.0.downcast::<HTMLTextAreaElement>() {
796            text_area
797                .text_input_mut()
798                .drag_moved(cx, text_area, hit_test_result);
799            text_area.maybe_update_shared_selection();
800            true
801        } else {
802            false
803        }
804    }
805}