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