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