script/
textinput.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::borrow::ToOwned;
8use std::cmp::min;
9use std::default::Default;
10use std::ops::{Add, AddAssign, Range};
11
12use bitflags::bitflags;
13use keyboard_types::{Key, KeyState, Modifiers, NamedKey, ShortcutMatcher};
14use unicode_segmentation::UnicodeSegmentation;
15
16use crate::clipboard_provider::ClipboardProvider;
17use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::str::DOMString;
20use crate::dom::compositionevent::CompositionEvent;
21use crate::dom::event::Event;
22use crate::dom::keyboardevent::KeyboardEvent;
23use crate::dom::types::ClipboardEvent;
24use crate::drag_data_store::Kind;
25
26#[derive(Clone, Copy, PartialEq)]
27pub enum Selection {
28    Selected,
29    NotSelected,
30}
31
32#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
33pub enum SelectionDirection {
34    Forward,
35    Backward,
36    None,
37}
38
39#[derive(Clone, Copy, Debug, Eq, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
40pub struct UTF8Bytes(pub usize);
41
42impl UTF8Bytes {
43    pub fn zero() -> UTF8Bytes {
44        UTF8Bytes(0)
45    }
46
47    pub fn one() -> UTF8Bytes {
48        UTF8Bytes(1)
49    }
50
51    pub(crate) fn unwrap_range(byte_range: Range<UTF8Bytes>) -> Range<usize> {
52        byte_range.start.0..byte_range.end.0
53    }
54
55    pub(crate) fn saturating_sub(self, other: UTF8Bytes) -> UTF8Bytes {
56        if self > other {
57            UTF8Bytes(self.0 - other.0)
58        } else {
59            UTF8Bytes::zero()
60        }
61    }
62}
63
64impl Add for UTF8Bytes {
65    type Output = UTF8Bytes;
66
67    fn add(self, other: UTF8Bytes) -> UTF8Bytes {
68        UTF8Bytes(self.0 + other.0)
69    }
70}
71
72impl AddAssign for UTF8Bytes {
73    fn add_assign(&mut self, other: UTF8Bytes) {
74        *self = UTF8Bytes(self.0 + other.0)
75    }
76}
77
78trait StrExt {
79    fn len_utf8(&self) -> UTF8Bytes;
80}
81impl StrExt for DOMString {
82    fn len_utf8(&self) -> UTF8Bytes {
83        UTF8Bytes(self.len())
84    }
85}
86
87impl StrExt for str {
88    fn len_utf8(&self) -> UTF8Bytes {
89        UTF8Bytes(self.len())
90    }
91}
92
93#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
94pub struct UTF16CodeUnits(pub usize);
95
96impl UTF16CodeUnits {
97    pub fn zero() -> UTF16CodeUnits {
98        UTF16CodeUnits(0)
99    }
100
101    pub fn one() -> UTF16CodeUnits {
102        UTF16CodeUnits(1)
103    }
104
105    pub(crate) fn saturating_sub(self, other: UTF16CodeUnits) -> UTF16CodeUnits {
106        if self > other {
107            UTF16CodeUnits(self.0 - other.0)
108        } else {
109            UTF16CodeUnits::zero()
110        }
111    }
112}
113
114impl Add for UTF16CodeUnits {
115    type Output = UTF16CodeUnits;
116
117    fn add(self, other: UTF16CodeUnits) -> UTF16CodeUnits {
118        UTF16CodeUnits(self.0 + other.0)
119    }
120}
121
122impl AddAssign for UTF16CodeUnits {
123    fn add_assign(&mut self, other: UTF16CodeUnits) {
124        *self = UTF16CodeUnits(self.0 + other.0)
125    }
126}
127
128impl From<DOMString> for SelectionDirection {
129    fn from(direction: DOMString) -> SelectionDirection {
130        match &*direction.str() {
131            "forward" => SelectionDirection::Forward,
132            "backward" => SelectionDirection::Backward,
133            _ => SelectionDirection::None,
134        }
135    }
136}
137
138impl From<SelectionDirection> for DOMString {
139    fn from(direction: SelectionDirection) -> DOMString {
140        match direction {
141            SelectionDirection::Forward => DOMString::from("forward"),
142            SelectionDirection::Backward => DOMString::from("backward"),
143            SelectionDirection::None => DOMString::from("none"),
144        }
145    }
146}
147
148#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
149pub struct TextPoint {
150    /// 0-based line number
151    pub line: usize,
152    /// 0-based column number in bytes
153    pub index: UTF8Bytes,
154}
155
156impl TextPoint {
157    /// Returns a TextPoint constrained to be a valid location within lines
158    fn constrain_to(&self, lines: &[DOMString]) -> TextPoint {
159        let line = min(self.line, lines.len() - 1);
160
161        TextPoint {
162            line,
163            index: min(self.index, lines[line].len_utf8()),
164        }
165    }
166}
167
168#[derive(Clone, Copy, PartialEq)]
169pub(crate) struct SelectionState {
170    start: TextPoint,
171    end: TextPoint,
172    direction: SelectionDirection,
173}
174
175/// Encapsulated state for handling keyboard input in a single or multiline text input control.
176#[derive(JSTraceable, MallocSizeOf)]
177pub struct TextInput<T: ClipboardProvider> {
178    /// Current text input content, split across lines without trailing '\n'
179    lines: Vec<DOMString>,
180
181    /// Current cursor input point
182    edit_point: TextPoint,
183
184    /// The current selection goes from the selection_origin until the edit_point. Note that the
185    /// selection_origin may be after the edit_point, in the case of a backward selection.
186    selection_origin: Option<TextPoint>,
187    selection_direction: SelectionDirection,
188
189    /// Is this a multiline input?
190    multiline: bool,
191
192    #[ignore_malloc_size_of = "Can't easily measure this generic type"]
193    clipboard_provider: T,
194
195    /// The maximum number of UTF-16 code units this text input is allowed to hold.
196    ///
197    /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
198    max_length: Option<UTF16CodeUnits>,
199    min_length: Option<UTF16CodeUnits>,
200
201    /// Was last change made by set_content?
202    was_last_change_by_set_content: bool,
203}
204
205/// Resulting action to be taken by the owner of a text input that is handling an event.
206pub enum KeyReaction {
207    TriggerDefaultAction,
208    DispatchInput,
209    RedrawSelection,
210    Nothing,
211}
212
213bitflags! {
214    /// Resulting action to be taken by the owner of a text input that is handling a clipboard
215    /// event.
216    pub struct ClipboardEventReaction: u8 {
217        const QueueInputEvent = 1 << 0;
218        const FireClipboardChangedEvent = 1 << 1;
219    }
220}
221
222impl Default for TextPoint {
223    fn default() -> TextPoint {
224        TextPoint {
225            line: 0,
226            index: UTF8Bytes::zero(),
227        }
228    }
229}
230
231/// Control whether this control should allow multiple lines.
232#[derive(Eq, PartialEq)]
233pub enum Lines {
234    Single,
235    Multiple,
236}
237
238/// The direction in which to delete a character.
239#[derive(Clone, Copy, Eq, PartialEq)]
240pub enum Direction {
241    Forward,
242    Backward,
243}
244
245// Some shortcuts use Cmd on Mac and Control on other systems.
246#[cfg(target_os = "macos")]
247pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::META;
248#[cfg(not(target_os = "macos"))]
249pub(crate) const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
250
251/// The length in bytes of the first n characters in a UTF-8 string.
252///
253/// If the string has fewer than n characters, returns the length of the whole string.
254/// If n is 0, returns 0
255fn len_of_first_n_chars(text: &DOMString, n: usize) -> UTF8Bytes {
256    match text.str().char_indices().take(n).last() {
257        Some((index, ch)) => UTF8Bytes(index + ch.len_utf8()),
258        None => UTF8Bytes::zero(),
259    }
260}
261
262/// The length in bytes of the first n code units in a string when encoded in UTF-16.
263///
264/// If the string is fewer than n code units, returns the length of the whole string.
265fn len_of_first_n_code_units(text: &DOMString, n: UTF16CodeUnits) -> UTF8Bytes {
266    let mut utf8_len = UTF8Bytes::zero();
267    let mut utf16_len = UTF16CodeUnits::zero();
268    for c in text.str().chars() {
269        utf16_len += UTF16CodeUnits(c.len_utf16());
270        if utf16_len > n {
271            break;
272        }
273        utf8_len += UTF8Bytes(c.len_utf8());
274    }
275    utf8_len
276}
277
278impl<T: ClipboardProvider> TextInput<T> {
279    /// Instantiate a new text input control
280    pub fn new(
281        lines: Lines,
282        initial: DOMString,
283        clipboard_provider: T,
284        max_length: Option<UTF16CodeUnits>,
285        min_length: Option<UTF16CodeUnits>,
286        selection_direction: SelectionDirection,
287    ) -> TextInput<T> {
288        let mut i = TextInput {
289            lines: vec![],
290            edit_point: Default::default(),
291            selection_origin: None,
292            multiline: lines == Lines::Multiple,
293            clipboard_provider,
294            max_length,
295            min_length,
296            selection_direction,
297            was_last_change_by_set_content: true,
298        };
299        i.set_content(initial);
300        i
301    }
302
303    pub fn edit_point(&self) -> TextPoint {
304        self.edit_point
305    }
306
307    pub fn selection_origin(&self) -> Option<TextPoint> {
308        self.selection_origin
309    }
310
311    /// The selection origin, or the edit point if there is no selection. Note that the selection
312    /// origin may be after the edit point, in the case of a backward selection.
313    pub fn selection_origin_or_edit_point(&self) -> TextPoint {
314        self.selection_origin.unwrap_or(self.edit_point)
315    }
316
317    pub fn selection_direction(&self) -> SelectionDirection {
318        self.selection_direction
319    }
320
321    pub(crate) fn set_max_length(&mut self, length: Option<UTF16CodeUnits>) {
322        self.max_length = length;
323    }
324
325    pub(crate) fn set_min_length(&mut self, length: Option<UTF16CodeUnits>) {
326        self.min_length = length;
327    }
328
329    /// Was last edit made by set_content?
330    pub(crate) fn was_last_change_by_set_content(&self) -> bool {
331        self.was_last_change_by_set_content
332    }
333
334    /// Remove a character at the current editing point
335    ///
336    /// Returns true if any character was deleted
337    pub fn delete_char(&mut self, dir: Direction) -> bool {
338        if self.selection_origin.is_none() || self.selection_origin == Some(self.edit_point) {
339            self.adjust_horizontal_by_one(dir, Selection::Selected);
340        }
341        if self.selection_start() == self.selection_end() {
342            false
343        } else {
344            self.replace_selection(DOMString::new());
345            true
346        }
347    }
348
349    /// Insert a character at the current editing point
350    pub fn insert_char(&mut self, ch: char) {
351        self.insert_string(ch.to_string());
352    }
353
354    /// Insert a string at the current editing point or replace the selection if
355    /// one exists.
356    pub fn insert_string<S: Into<String>>(&mut self, s: S) {
357        if self.selection_origin.is_none() {
358            self.selection_origin = Some(self.edit_point);
359        }
360        self.replace_selection(DOMString::from(s.into()));
361    }
362
363    /// The start of the selection (or the edit point, if there is no selection). Always less than
364    /// or equal to selection_end(), regardless of the selection direction.
365    pub fn selection_start(&self) -> TextPoint {
366        match self.selection_direction {
367            SelectionDirection::None | SelectionDirection::Forward => {
368                self.selection_origin_or_edit_point()
369            },
370            SelectionDirection::Backward => self.edit_point,
371        }
372    }
373
374    /// The byte offset of the selection_start()
375    pub fn selection_start_offset(&self) -> UTF8Bytes {
376        self.text_point_to_offset(&self.selection_start())
377    }
378
379    /// The end of the selection (or the edit point, if there is no selection). Always greater
380    /// than or equal to selection_start(), regardless of the selection direction.
381    pub fn selection_end(&self) -> TextPoint {
382        match self.selection_direction {
383            SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
384            SelectionDirection::Backward => self.selection_origin_or_edit_point(),
385        }
386    }
387
388    /// The byte offset of the selection_end()
389    pub fn selection_end_offset(&self) -> UTF8Bytes {
390        self.text_point_to_offset(&self.selection_end())
391    }
392
393    /// Whether or not there is an active selection (the selection may be zero-length)
394    #[inline]
395    pub(crate) fn has_selection(&self) -> bool {
396        self.selection_origin.is_some()
397    }
398
399    /// Returns a tuple of (start, end) giving the bounds of the current selection. start is always
400    /// less than or equal to end.
401    pub fn sorted_selection_bounds(&self) -> (TextPoint, TextPoint) {
402        (self.selection_start(), self.selection_end())
403    }
404
405    /// Return the selection range as byte offsets from the start of the content.
406    ///
407    /// If there is no selection, returns an empty range at the edit point.
408    pub(crate) fn sorted_selection_offsets_range(&self) -> Range<UTF8Bytes> {
409        self.selection_start_offset()..self.selection_end_offset()
410    }
411
412    /// The state of the current selection. Can be used to compare whether selection state has changed.
413    pub(crate) fn selection_state(&self) -> SelectionState {
414        SelectionState {
415            start: self.selection_start(),
416            end: self.selection_end(),
417            direction: self.selection_direction,
418        }
419    }
420
421    // Check that the selection is valid.
422    fn assert_ok_selection(&self) {
423        debug!(
424            "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
425            self.edit_point, self.selection_origin, self.selection_direction
426        );
427        if let Some(begin) = self.selection_origin {
428            debug_assert!(begin.line < self.lines.len());
429            debug_assert!(begin.index <= self.lines[begin.line].len_utf8());
430
431            match self.selection_direction {
432                SelectionDirection::None | SelectionDirection::Forward => {
433                    debug_assert!(begin <= self.edit_point)
434                },
435
436                SelectionDirection::Backward => debug_assert!(self.edit_point <= begin),
437            }
438        }
439
440        debug_assert!(self.edit_point.line < self.lines.len());
441        debug_assert!(self.edit_point.index <= self.lines[self.edit_point.line].len_utf8());
442    }
443
444    pub(crate) fn get_selection_text(&self) -> Option<String> {
445        let text = self.fold_selection_slices(String::new(), |s, slice| s.push_str(slice));
446        if text.is_empty() {
447            return None;
448        }
449        Some(text)
450    }
451
452    /// The length of the selected text in UTF-16 code units.
453    fn selection_utf16_len(&self) -> UTF16CodeUnits {
454        self.fold_selection_slices(UTF16CodeUnits::zero(), |len, slice| {
455            *len += UTF16CodeUnits(slice.chars().map(char::len_utf16).sum::<usize>())
456        })
457    }
458
459    /// Run the callback on a series of slices that, concatenated, make up the selected text.
460    ///
461    /// The accumulator `acc` can be mutated by the callback, and will be returned at the end.
462    fn fold_selection_slices<B, F: FnMut(&mut B, &str)>(&self, mut acc: B, mut f: F) -> B {
463        if self.has_selection() {
464            let (start, end) = self.sorted_selection_bounds();
465            let UTF8Bytes(start_offset) = start.index;
466            let UTF8Bytes(end_offset) = end.index;
467
468            if start.line == end.line {
469                f(
470                    &mut acc,
471                    &self.lines[start.line].str()[start_offset..end_offset],
472                )
473            } else {
474                f(&mut acc, &self.lines[start.line].str()[start_offset..]);
475                for line in &self.lines[start.line + 1..end.line] {
476                    f(&mut acc, "\n");
477                    f(&mut acc, &line.str());
478                }
479                f(&mut acc, "\n");
480                f(&mut acc, &self.lines[end.line].str()[..end_offset])
481            }
482        }
483
484        acc
485    }
486
487    pub fn replace_selection(&mut self, insert: DOMString) {
488        if !self.has_selection() {
489            return;
490        }
491
492        let allowed_to_insert_count = if let Some(max_length) = self.max_length {
493            let len_after_selection_replaced =
494                self.utf16_len().saturating_sub(self.selection_utf16_len());
495            max_length.saturating_sub(len_after_selection_replaced)
496        } else {
497            UTF16CodeUnits(usize::MAX)
498        };
499
500        let UTF8Bytes(last_char_index) =
501            len_of_first_n_code_units(&insert, allowed_to_insert_count);
502        let to_insert = &insert.str()[..last_char_index];
503
504        let (start, end) = self.sorted_selection_bounds();
505        let UTF8Bytes(start_offset) = start.index;
506        let UTF8Bytes(end_offset) = end.index;
507
508        let new_lines = {
509            let prefix = &self.lines[start.line].str()[..start_offset];
510            let suffix = &self.lines[end.line].str()[end_offset..];
511            let lines_prefix = &self.lines[..start.line];
512            let lines_suffix = &self.lines[end.line + 1..];
513
514            let mut insert_lines = if self.multiline {
515                to_insert.split('\n').map(DOMString::from).collect()
516            } else {
517                vec![DOMString::from(to_insert)]
518            };
519
520            // FIXME(ajeffrey): efficient append for DOMStrings
521            let mut new_line = prefix.to_owned();
522
523            new_line.push_str(&insert_lines[0].str());
524            insert_lines[0] = DOMString::from(new_line);
525
526            let last_insert_lines_index = insert_lines.len() - 1;
527            self.edit_point.index = insert_lines[last_insert_lines_index].len_utf8();
528            self.edit_point.line = start.line + last_insert_lines_index;
529
530            // FIXME(ajeffrey): efficient append for DOMStrings
531            insert_lines[last_insert_lines_index].push_str(suffix);
532
533            let mut new_lines = vec![];
534            new_lines.extend_from_slice(lines_prefix);
535            new_lines.extend_from_slice(&insert_lines);
536            new_lines.extend_from_slice(lines_suffix);
537            new_lines
538        };
539
540        self.lines = new_lines;
541        self.was_last_change_by_set_content = false;
542        self.clear_selection();
543        self.assert_ok_selection();
544    }
545
546    /// Return the length in bytes of the current line under the editing point.
547    pub fn current_line_length(&self) -> UTF8Bytes {
548        self.lines[self.edit_point.line].len_utf8()
549    }
550
551    /// Adjust the editing point position by a given number of lines. The resulting column is
552    /// as close to the original column position as possible.
553    pub fn adjust_vertical(&mut self, adjust: isize, select: Selection) {
554        if !self.multiline {
555            return;
556        }
557
558        if select == Selection::Selected {
559            if self.selection_origin.is_none() {
560                self.selection_origin = Some(self.edit_point);
561            }
562        } else {
563            self.clear_selection();
564        }
565
566        assert!(self.edit_point.line < self.lines.len());
567
568        let target_line: isize = self.edit_point.line as isize + adjust;
569
570        if target_line < 0 {
571            self.edit_point.line = 0;
572            self.edit_point.index = UTF8Bytes::zero();
573            if self.selection_origin.is_some() &&
574                (self.selection_direction == SelectionDirection::None ||
575                    self.selection_direction == SelectionDirection::Forward)
576            {
577                self.selection_origin = Some(TextPoint {
578                    line: 0,
579                    index: UTF8Bytes::zero(),
580                });
581            }
582            return;
583        } else if target_line as usize >= self.lines.len() {
584            self.edit_point.line = self.lines.len() - 1;
585            self.edit_point.index = self.current_line_length();
586            if self.selection_origin.is_some() &&
587                (self.selection_direction == SelectionDirection::Backward)
588            {
589                self.selection_origin = Some(self.edit_point);
590            }
591            return;
592        }
593
594        let UTF8Bytes(edit_index) = self.edit_point.index;
595        let col = self.lines[self.edit_point.line].str()[..edit_index]
596            .chars()
597            .count();
598        self.edit_point.line = target_line as usize;
599        // NOTE: this adjusts to the nearest complete Unicode codepoint, rather than grapheme cluster
600        self.edit_point.index = len_of_first_n_chars(&self.lines[self.edit_point.line], col);
601        if let Some(origin) = self.selection_origin {
602            if ((self.selection_direction == SelectionDirection::None ||
603                self.selection_direction == SelectionDirection::Forward) &&
604                self.edit_point <= origin) ||
605                (self.selection_direction == SelectionDirection::Backward &&
606                    origin <= self.edit_point)
607            {
608                self.selection_origin = Some(self.edit_point);
609            }
610        }
611        self.assert_ok_selection();
612    }
613
614    /// Adjust the editing point position by a given number of bytes. If the adjustment
615    /// requested is larger than is available in the current line, the editing point is
616    /// adjusted vertically and the process repeats with the remaining adjustment requested.
617    pub fn adjust_horizontal(
618        &mut self,
619        adjust: UTF8Bytes,
620        direction: Direction,
621        select: Selection,
622    ) {
623        if self.adjust_selection_for_horizontal_change(direction, select) {
624            return;
625        }
626        self.perform_horizontal_adjustment(adjust, direction, select);
627    }
628
629    /// Adjust the editing point position by exactly one grapheme cluster. If the edit point
630    /// is at the beginning of the line and the direction is "Backward" or the edit point is at
631    /// the end of the line and the direction is "Forward", a vertical adjustment is made
632    pub fn adjust_horizontal_by_one(&mut self, direction: Direction, select: Selection) {
633        if self.adjust_selection_for_horizontal_change(direction, select) {
634            return;
635        }
636        let adjust = {
637            let current_line = self.lines[self.edit_point.line].str();
638            let UTF8Bytes(current_offset) = self.edit_point.index;
639            let next_ch = match direction {
640                Direction::Forward => current_line[current_offset..].graphemes(true).next(),
641                Direction::Backward => current_line[..current_offset].graphemes(true).next_back(),
642            };
643            match next_ch {
644                Some(c) => UTF8Bytes(c.len()),
645                None => UTF8Bytes::one(), // Going to the next line is a "one byte" offset
646            }
647        };
648        self.perform_horizontal_adjustment(adjust, direction, select);
649    }
650
651    /// Return whether to cancel the caret move
652    fn adjust_selection_for_horizontal_change(
653        &mut self,
654        adjust: Direction,
655        select: Selection,
656    ) -> bool {
657        if select == Selection::Selected {
658            if self.selection_origin.is_none() {
659                self.selection_origin = Some(self.edit_point);
660            }
661        } else if self.has_selection() {
662            self.edit_point = match adjust {
663                Direction::Backward => self.selection_start(),
664                Direction::Forward => self.selection_end(),
665            };
666            self.clear_selection();
667            return true;
668        }
669        false
670    }
671
672    /// Update the field selection_direction.
673    ///
674    /// When the edit_point (or focus) is before the selection_origin (or anchor)
675    /// you have a backward selection. Otherwise you have a forward selection.
676    fn update_selection_direction(&mut self) {
677        debug!(
678            "edit_point: {:?}, selection_origin: {:?}",
679            self.edit_point, self.selection_origin
680        );
681        self.selection_direction = if Some(self.edit_point) < self.selection_origin {
682            SelectionDirection::Backward
683        } else {
684            SelectionDirection::Forward
685        }
686    }
687
688    fn perform_horizontal_adjustment(
689        &mut self,
690        adjust: UTF8Bytes,
691        direction: Direction,
692        select: Selection,
693    ) {
694        match direction {
695            Direction::Backward => {
696                let remaining = self.edit_point.index;
697                if adjust > remaining && self.edit_point.line > 0 {
698                    // Preserve the current selection origin because `adjust_vertical`
699                    // modifies `selection_origin`. Since we are moving backward instead of
700                    // highlighting vertically, we need to restore it after adjusting the line.
701                    let selection_origin_temp = self.selection_origin;
702                    self.adjust_vertical(-1, select);
703                    self.edit_point.index = self.current_line_length();
704                    // Restore the original selection origin to maintain expected behavior.
705                    self.selection_origin = selection_origin_temp;
706                    // one shift is consumed by the change of line, hence the -1
707                    self.adjust_horizontal(
708                        adjust.saturating_sub(remaining + UTF8Bytes::one()),
709                        direction,
710                        select,
711                    );
712                } else {
713                    self.edit_point.index = remaining.saturating_sub(adjust);
714                }
715            },
716            Direction::Forward => {
717                let remaining = self
718                    .current_line_length()
719                    .saturating_sub(self.edit_point.index);
720                if adjust > remaining && self.lines.len() > self.edit_point.line + 1 {
721                    self.adjust_vertical(1, select);
722                    self.edit_point.index = UTF8Bytes::zero();
723                    // one shift is consumed by the change of line, hence the -1
724                    self.adjust_horizontal(
725                        adjust.saturating_sub(remaining + UTF8Bytes::one()),
726                        direction,
727                        select,
728                    );
729                } else {
730                    self.edit_point.index =
731                        min(self.current_line_length(), self.edit_point.index + adjust);
732                }
733            },
734        };
735        self.update_selection_direction();
736        self.assert_ok_selection();
737    }
738
739    /// Deal with a newline input.
740    pub fn handle_return(&mut self) -> KeyReaction {
741        if !self.multiline {
742            KeyReaction::TriggerDefaultAction
743        } else {
744            self.insert_char('\n');
745            KeyReaction::DispatchInput
746        }
747    }
748
749    /// Select all text in the input control.
750    pub fn select_all(&mut self) {
751        self.selection_origin = Some(TextPoint {
752            line: 0,
753            index: UTF8Bytes::zero(),
754        });
755        let last_line = self.lines.len() - 1;
756        self.edit_point.line = last_line;
757        self.edit_point.index = self.lines[last_line].len_utf8();
758        self.selection_direction = SelectionDirection::Forward;
759        self.assert_ok_selection();
760    }
761
762    /// Remove the current selection.
763    pub fn clear_selection(&mut self) {
764        self.selection_origin = None;
765        self.selection_direction = SelectionDirection::None;
766    }
767
768    /// Remove the current selection and set the edit point to the end of the content.
769    pub(crate) fn clear_selection_to_limit(&mut self, direction: Direction) {
770        self.clear_selection();
771        self.adjust_horizontal_to_limit(direction, Selection::NotSelected);
772    }
773
774    pub fn adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection) {
775        if self.adjust_selection_for_horizontal_change(direction, select) {
776            return;
777        }
778        let shift_increment: UTF8Bytes = {
779            let current_index = self.edit_point.index;
780            let current_line_index = self.edit_point.line;
781            let current_line = self.lines[current_line_index].str();
782            let mut newline_adjustment = UTF8Bytes::zero();
783            let mut shift_temp = UTF8Bytes::zero();
784            match direction {
785                Direction::Backward => {
786                    let previous_line = current_line_index
787                        .checked_sub(1)
788                        .and_then(|index| self.lines.get(index))
789                        .map(|s| s.str());
790
791                    let input: &str;
792                    if current_index == UTF8Bytes::zero() && current_line_index > 0 {
793                        input = previous_line.as_ref().unwrap();
794                        newline_adjustment = UTF8Bytes::one();
795                    } else {
796                        let UTF8Bytes(remaining) = current_index;
797                        input = &current_line[..remaining];
798                    }
799
800                    let mut iter = input.split_word_bounds().rev();
801                    loop {
802                        match iter.next() {
803                            None => break,
804                            Some(x) => {
805                                shift_temp += UTF8Bytes(x.len());
806                                if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
807                                    break;
808                                }
809                            },
810                        }
811                    }
812                },
813                Direction::Forward => {
814                    let input: &str;
815                    let next_line = self.lines.get(current_line_index + 1).map(|s| s.str());
816                    let remaining = self.current_line_length().saturating_sub(current_index);
817                    if remaining == UTF8Bytes::zero() && self.lines.len() > self.edit_point.line + 1
818                    {
819                        input = next_line.as_ref().unwrap();
820                        newline_adjustment = UTF8Bytes::one();
821                    } else {
822                        let UTF8Bytes(current_offset) = current_index;
823                        input = &current_line[current_offset..];
824                    }
825
826                    let mut iter = input.split_word_bounds();
827                    loop {
828                        match iter.next() {
829                            None => break,
830                            Some(x) => {
831                                shift_temp += UTF8Bytes(x.len());
832                                if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
833                                    break;
834                                }
835                            },
836                        }
837                    }
838                },
839            };
840
841            shift_temp + newline_adjustment
842        };
843
844        self.adjust_horizontal(shift_increment, direction, select);
845    }
846
847    pub fn adjust_horizontal_to_line_end(&mut self, direction: Direction, select: Selection) {
848        if self.adjust_selection_for_horizontal_change(direction, select) {
849            return;
850        }
851        let shift: usize = {
852            let current_line = &self.lines[self.edit_point.line];
853            let UTF8Bytes(current_offset) = self.edit_point.index;
854            match direction {
855                Direction::Backward => current_line.str()[..current_offset].len(),
856                Direction::Forward => current_line.str()[current_offset..].len(),
857            }
858        };
859        self.perform_horizontal_adjustment(UTF8Bytes(shift), direction, select);
860    }
861
862    pub(crate) fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection) {
863        if self.adjust_selection_for_horizontal_change(direction, select) {
864            return;
865        }
866        match direction {
867            Direction::Backward => {
868                self.edit_point.line = 0;
869                self.edit_point.index = UTF8Bytes::zero();
870            },
871            Direction::Forward => {
872                self.edit_point.line = &self.lines.len() - 1;
873                self.edit_point.index = (self.lines[&self.lines.len() - 1]).len_utf8();
874            },
875        }
876    }
877
878    /// Process a given `KeyboardEvent` and return an action for the caller to execute.
879    pub(crate) fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
880        let key = event.key();
881        let mods = event.modifiers();
882        self.handle_keydown_aux(key, mods, cfg!(target_os = "macos"))
883    }
884
885    // This function exists for easy unit testing.
886    // To test Mac OS shortcuts on other systems a flag is passed.
887    pub fn handle_keydown_aux(
888        &mut self,
889        key: Key,
890        mut mods: Modifiers,
891        macos: bool,
892    ) -> KeyReaction {
893        let maybe_select = if mods.contains(Modifiers::SHIFT) {
894            Selection::Selected
895        } else {
896            Selection::NotSelected
897        };
898        mods.remove(Modifiers::SHIFT);
899        ShortcutMatcher::new(KeyState::Down, key.clone(), mods)
900            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
901                self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
902                KeyReaction::RedrawSelection
903            })
904            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
905                self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
906                KeyReaction::RedrawSelection
907            })
908            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'A', || {
909                self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
910                KeyReaction::RedrawSelection
911            })
912            .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'E', || {
913                self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
914                KeyReaction::RedrawSelection
915            })
916            .optional_shortcut(macos, Modifiers::CONTROL, 'A', || {
917                self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
918                KeyReaction::RedrawSelection
919            })
920            .optional_shortcut(macos, Modifiers::CONTROL, 'E', || {
921                self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
922                KeyReaction::RedrawSelection
923            })
924            .shortcut(CMD_OR_CONTROL, 'A', || {
925                self.select_all();
926                KeyReaction::RedrawSelection
927            })
928            .shortcut(CMD_OR_CONTROL, 'X', || {
929                if let Some(text) = self.get_selection_text() {
930                    self.clipboard_provider.set_text(text);
931                    self.delete_char(Direction::Backward);
932                }
933                KeyReaction::DispatchInput
934            })
935            .shortcut(CMD_OR_CONTROL, 'C', || {
936                // TODO(stevennovaryo): we should not provide text to clipboard for type=password
937                if let Some(text) = self.get_selection_text() {
938                    self.clipboard_provider.set_text(text);
939                }
940                KeyReaction::DispatchInput
941            })
942            .shortcut(CMD_OR_CONTROL, 'V', || {
943                if let Ok(text_content) = self.clipboard_provider.get_text() {
944                    self.insert_string(text_content);
945                }
946                KeyReaction::DispatchInput
947            })
948            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Delete), || {
949                if self.delete_char(Direction::Forward) {
950                    KeyReaction::DispatchInput
951                } else {
952                    KeyReaction::Nothing
953                }
954            })
955            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Backspace), || {
956                if self.delete_char(Direction::Backward) {
957                    KeyReaction::DispatchInput
958                } else {
959                    KeyReaction::Nothing
960                }
961            })
962            .optional_shortcut(
963                macos,
964                Modifiers::META,
965                Key::Named(NamedKey::ArrowLeft),
966                || {
967                    self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
968                    KeyReaction::RedrawSelection
969                },
970            )
971            .optional_shortcut(
972                macos,
973                Modifiers::META,
974                Key::Named(NamedKey::ArrowRight),
975                || {
976                    self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
977                    KeyReaction::RedrawSelection
978                },
979            )
980            .optional_shortcut(
981                macos,
982                Modifiers::META,
983                Key::Named(NamedKey::ArrowUp),
984                || {
985                    self.adjust_horizontal_to_limit(Direction::Backward, maybe_select);
986                    KeyReaction::RedrawSelection
987                },
988            )
989            .optional_shortcut(
990                macos,
991                Modifiers::META,
992                Key::Named(NamedKey::ArrowDown),
993                || {
994                    self.adjust_horizontal_to_limit(Direction::Forward, maybe_select);
995                    KeyReaction::RedrawSelection
996                },
997            )
998            .shortcut(Modifiers::ALT, Key::Named(NamedKey::ArrowLeft), || {
999                self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
1000                KeyReaction::RedrawSelection
1001            })
1002            .shortcut(Modifiers::ALT, Key::Named(NamedKey::ArrowRight), || {
1003                self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
1004                KeyReaction::RedrawSelection
1005            })
1006            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || {
1007                self.adjust_horizontal_by_one(Direction::Backward, maybe_select);
1008                KeyReaction::RedrawSelection
1009            })
1010            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || {
1011                self.adjust_horizontal_by_one(Direction::Forward, maybe_select);
1012                KeyReaction::RedrawSelection
1013            })
1014            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || {
1015                self.adjust_vertical(-1, maybe_select);
1016                KeyReaction::RedrawSelection
1017            })
1018            .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || {
1019                self.adjust_vertical(1, maybe_select);
1020                KeyReaction::RedrawSelection
1021            })
1022            .shortcut(Modifiers::empty(), Key::Named(NamedKey::Enter), || {
1023                self.handle_return()
1024            })
1025            .optional_shortcut(
1026                macos,
1027                Modifiers::empty(),
1028                Key::Named(NamedKey::Home),
1029                || {
1030                    self.edit_point.index = UTF8Bytes::zero();
1031                    KeyReaction::RedrawSelection
1032                },
1033            )
1034            .optional_shortcut(macos, Modifiers::empty(), Key::Named(NamedKey::End), || {
1035                self.edit_point.index = self.current_line_length();
1036                self.assert_ok_selection();
1037                KeyReaction::RedrawSelection
1038            })
1039            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || {
1040                self.adjust_vertical(-28, maybe_select);
1041                KeyReaction::RedrawSelection
1042            })
1043            .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || {
1044                self.adjust_vertical(28, maybe_select);
1045                KeyReaction::RedrawSelection
1046            })
1047            .otherwise(|| {
1048                if let Key::Character(ref c) = key {
1049                    self.insert_string(c.as_str());
1050                    return KeyReaction::DispatchInput;
1051                }
1052                if matches!(key, Key::Named(NamedKey::Process)) {
1053                    return KeyReaction::DispatchInput;
1054                }
1055                KeyReaction::Nothing
1056            })
1057            .unwrap()
1058    }
1059
1060    pub(crate) fn handle_compositionend(&mut self, event: &CompositionEvent) -> KeyReaction {
1061        self.insert_string(event.data().str());
1062        KeyReaction::DispatchInput
1063    }
1064
1065    pub(crate) fn handle_compositionupdate(&mut self, event: &CompositionEvent) -> KeyReaction {
1066        let start = self.selection_start_offset().0;
1067        self.insert_string(event.data().str());
1068        self.set_selection_range(
1069            start as u32,
1070            (start + event.data().len_utf8().0) as u32,
1071            SelectionDirection::Forward,
1072        );
1073        KeyReaction::DispatchInput
1074    }
1075
1076    /// Whether the content is empty.
1077    pub(crate) fn is_empty(&self) -> bool {
1078        self.lines.len() <= 1 && self.lines.first().is_none_or(|line| line.is_empty())
1079    }
1080
1081    /// The length of the content in bytes.
1082    pub(crate) fn len_utf8(&self) -> UTF8Bytes {
1083        self.lines
1084            .iter()
1085            .fold(UTF8Bytes::zero(), |m, l| {
1086                m + l.len_utf8() + UTF8Bytes::one() // + 1 for the '\n'
1087            })
1088            .saturating_sub(UTF8Bytes::one())
1089    }
1090
1091    /// The total number of code units required to encode the content in utf16.
1092    pub(crate) fn utf16_len(&self) -> UTF16CodeUnits {
1093        self.lines
1094            .iter()
1095            .fold(UTF16CodeUnits::zero(), |m, l| {
1096                m + UTF16CodeUnits(l.str().chars().map(char::len_utf16).sum::<usize>() + 1)
1097                // + 1 for the '\n'
1098            })
1099            .saturating_sub(UTF16CodeUnits::one())
1100    }
1101
1102    /// The length of the content in Unicode code points.
1103    pub(crate) fn char_count(&self) -> usize {
1104        self.lines.iter().fold(0, |m, l| {
1105            m + l.str().chars().count() + 1 // + 1 for the '\n'
1106        }) - 1
1107    }
1108
1109    /// Get the current contents of the text input. Multiple lines are joined by \n.
1110    pub fn get_content(&self) -> DOMString {
1111        let mut content = "".to_owned();
1112        for (i, line) in self.lines.iter().enumerate() {
1113            content.push_str(&line.str());
1114            if i < self.lines.len() - 1 {
1115                content.push('\n');
1116            }
1117        }
1118        DOMString::from(content)
1119    }
1120
1121    /// Get a reference to the contents of a single-line text input. Panics if self is a multiline input.
1122    pub(crate) fn single_line_content(&self) -> &DOMString {
1123        assert!(!self.multiline);
1124        &self.lines[0]
1125    }
1126
1127    /// Set the current contents of the text input. If this is control supports multiple lines,
1128    /// any \n encountered will be stripped and force a new logical line.
1129    pub fn set_content(&mut self, content: DOMString) {
1130        self.lines = if self.multiline {
1131            // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
1132            content
1133                .str()
1134                .replace("\r\n", "\n")
1135                .split(['\n', '\r'])
1136                .map(DOMString::from)
1137                .collect()
1138        } else {
1139            vec![content]
1140        };
1141
1142        self.was_last_change_by_set_content = true;
1143        self.edit_point = self.edit_point.constrain_to(&self.lines);
1144
1145        if let Some(origin) = self.selection_origin {
1146            self.selection_origin = Some(origin.constrain_to(&self.lines));
1147        }
1148        self.assert_ok_selection();
1149    }
1150
1151    /// Convert a TextPoint into a byte offset from the start of the content.
1152    fn text_point_to_offset(&self, text_point: &TextPoint) -> UTF8Bytes {
1153        self.lines
1154            .iter()
1155            .enumerate()
1156            .fold(UTF8Bytes::zero(), |acc, (i, val)| {
1157                if i < text_point.line {
1158                    acc + val.len_utf8() + UTF8Bytes::one() // +1 for the \n
1159                } else {
1160                    acc
1161                }
1162            }) +
1163            text_point.index
1164    }
1165
1166    /// Convert a byte offset from the start of the content into a TextPoint.
1167    fn offset_to_text_point(&self, abs_point: UTF8Bytes) -> TextPoint {
1168        let mut index = abs_point;
1169        let mut line = 0;
1170        let last_line_idx = self.lines.len() - 1;
1171        self.lines
1172            .iter()
1173            .enumerate()
1174            .fold(UTF8Bytes::zero(), |acc, (i, val)| {
1175                if i != last_line_idx {
1176                    let line_end = val.len_utf8();
1177                    let new_acc = acc + line_end + UTF8Bytes::one();
1178                    if abs_point >= new_acc && index > line_end {
1179                        index = index.saturating_sub(line_end + UTF8Bytes::one());
1180                        line += 1;
1181                    }
1182                    new_acc
1183                } else {
1184                    acc
1185                }
1186            });
1187
1188        TextPoint { line, index }
1189    }
1190
1191    pub fn set_selection_range(&mut self, start: u32, end: u32, direction: SelectionDirection) {
1192        let mut start = UTF8Bytes(start as usize);
1193        let mut end = UTF8Bytes(end as usize);
1194        let text_end = self.get_content().len_utf8();
1195
1196        if end > text_end {
1197            end = text_end;
1198        }
1199        if start > end {
1200            start = end;
1201        }
1202
1203        self.selection_direction = direction;
1204
1205        match direction {
1206            SelectionDirection::None | SelectionDirection::Forward => {
1207                self.selection_origin = Some(self.offset_to_text_point(start));
1208                self.edit_point = self.offset_to_text_point(end);
1209            },
1210            SelectionDirection::Backward => {
1211                self.selection_origin = Some(self.offset_to_text_point(end));
1212                self.edit_point = self.offset_to_text_point(start);
1213            },
1214        }
1215        self.assert_ok_selection();
1216    }
1217
1218    /// Set the edit point index position based off of a given grapheme cluster offset
1219    pub fn set_edit_point_index(&mut self, index: usize) {
1220        let byte_offset = self.lines[self.edit_point.line]
1221            .str()
1222            .graphemes(true)
1223            .take(index)
1224            .fold(UTF8Bytes::zero(), |acc, x| acc + x.len_utf8());
1225        self.edit_point.index = byte_offset;
1226    }
1227
1228    /// This implements step 3 onward from:
1229    ///
1230    ///  - <https://www.w3.org/TR/clipboard-apis/#copy-action>
1231    ///  - <https://www.w3.org/TR/clipboard-apis/#cut-action>
1232    ///  - <https://www.w3.org/TR/clipboard-apis/#paste-action>
1233    ///
1234    /// Earlier steps should have already been run by the callers.
1235    pub(crate) fn handle_clipboard_event(
1236        &mut self,
1237        clipboard_event: &ClipboardEvent,
1238    ) -> ClipboardEventReaction {
1239        let event = clipboard_event.upcast::<Event>();
1240        if !event.IsTrusted() {
1241            return ClipboardEventReaction::empty();
1242        }
1243
1244        // This step is common to all event types in the specification.
1245        // Step 3: If the event was not canceled, then
1246        if event.DefaultPrevented() {
1247            // Step 4: Else, if the event was canceled
1248            // Step 4.1: Return false.
1249            return ClipboardEventReaction::empty();
1250        }
1251
1252        match &*event.Type().str() {
1253            "copy" => {
1254                // These steps are from <https://www.w3.org/TR/clipboard-apis/#copy-action>:
1255                let selection = self.get_selection_text();
1256
1257                // Step 3.1 Copy the selected contents, if any, to the clipboard
1258                if let Some(text) = selection {
1259                    self.clipboard_provider.set_text(text);
1260                }
1261
1262                // Step 3.2 Fire a clipboard event named clipboardchange
1263                ClipboardEventReaction::FireClipboardChangedEvent
1264            },
1265            "cut" => {
1266                // These steps are from <https://www.w3.org/TR/clipboard-apis/#cut-action>:
1267                let selection = self.get_selection_text();
1268
1269                // Step 3.1 If there is a selection in an editable context where cutting is enabled, then
1270                let Some(text) = selection else {
1271                    // Step 3.2 Else, if there is no selection or the context is not editable, then
1272                    return ClipboardEventReaction::empty();
1273                };
1274
1275                // Step 3.1.1 Copy the selected contents, if any, to the clipboard
1276                self.clipboard_provider.set_text(text);
1277
1278                // Step 3.1.2 Remove the contents of the selection from the document and collapse the selection.
1279                self.delete_char(Direction::Backward);
1280
1281                // Step 3.1.3 Fire a clipboard event named clipboardchange
1282                // Step 3.1.4 Queue tasks to fire any events that should fire due to the modification.
1283                ClipboardEventReaction::FireClipboardChangedEvent |
1284                    ClipboardEventReaction::QueueInputEvent
1285            },
1286            "paste" => {
1287                // These steps are from <https://www.w3.org/TR/clipboard-apis/#paste-action>:
1288                let Some(data_transfer) = clipboard_event.get_clipboard_data() else {
1289                    return ClipboardEventReaction::empty();
1290                };
1291                let Some(drag_data_store) = data_transfer.data_store() else {
1292                    return ClipboardEventReaction::empty();
1293                };
1294
1295                // Step 3.1: If there is a selection or cursor in an editable context where pasting is
1296                // enabled, then:
1297                // TODO: Our TextInput always has a selection or an input point. It's likely that this
1298                // shouldn't be the case when the entry loses the cursor.
1299
1300                // Step 3.1.1: Insert the most suitable content found on the clipboard, if any, into the
1301                // context.
1302                // TODO: Only text content is currently supported, but other data types should be supported
1303                // in the future.
1304                let Some(text_content) =
1305                    drag_data_store
1306                        .iter_item_list()
1307                        .find_map(|item| match item {
1308                            Kind::Text { data, .. } => Some(data.to_string()),
1309                            _ => None,
1310                        })
1311                else {
1312                    return ClipboardEventReaction::empty();
1313                };
1314                if text_content.is_empty() {
1315                    return ClipboardEventReaction::empty();
1316                }
1317
1318                self.insert_string(text_content);
1319
1320                // Step 3.1.2: Queue tasks to fire any events that should fire due to the
1321                // modification, see ยง 5.3 Integration with other scripts and events for details.
1322                ClipboardEventReaction::QueueInputEvent
1323            },
1324            _ => ClipboardEventReaction::empty(),
1325        }
1326    }
1327}