aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/textinput.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/textinput.rs')
-rw-r--r--components/script/textinput.rs1073
1 files changed, 677 insertions, 396 deletions
diff --git a/components/script/textinput.rs b/components/script/textinput.rs
index 144be9dbd78..623085ccd19 100644
--- a/components/script/textinput.rs
+++ b/components/script/textinput.rs
@@ -1,34 +1,117 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Common handling of keyboard input and state management for text input controls
-use clipboard_provider::ClipboardProvider;
-use dom::bindings::str::DOMString;
-use dom::keyboardevent::KeyboardEvent;
-use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER};
-use msg::constellation_msg::{Key, KeyModifiers};
+use crate::clipboard_provider::ClipboardProvider;
+use crate::dom::bindings::str::DOMString;
+use crate::dom::compositionevent::CompositionEvent;
+use crate::dom::keyboardevent::KeyboardEvent;
+use keyboard_types::{Key, KeyState, Modifiers, ShortcutMatcher};
use std::borrow::ToOwned;
-use std::cmp::{max, min};
+use std::cmp::min;
use std::default::Default;
-use std::ops::Range;
+use std::ops::{Add, AddAssign, Range};
use std::usize;
use unicode_segmentation::UnicodeSegmentation;
-#[derive(Copy, Clone, PartialEq)]
+#[derive(Clone, Copy, PartialEq)]
pub enum Selection {
Selected,
- NotSelected
+ NotSelected,
}
-#[derive(JSTraceable, PartialEq, Copy, Clone, HeapSizeOf)]
+#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
pub enum SelectionDirection {
Forward,
Backward,
None,
}
+#[derive(Clone, Copy, Debug, Eq, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
+pub struct UTF8Bytes(pub usize);
+
+impl UTF8Bytes {
+ pub fn zero() -> UTF8Bytes {
+ UTF8Bytes(0)
+ }
+
+ pub fn one() -> UTF8Bytes {
+ UTF8Bytes(1)
+ }
+
+ pub fn unwrap_range(byte_range: Range<UTF8Bytes>) -> Range<usize> {
+ byte_range.start.0..byte_range.end.0
+ }
+
+ pub fn saturating_sub(self, other: UTF8Bytes) -> UTF8Bytes {
+ if self > other {
+ UTF8Bytes(self.0 - other.0)
+ } else {
+ UTF8Bytes::zero()
+ }
+ }
+}
+
+impl Add for UTF8Bytes {
+ type Output = UTF8Bytes;
+
+ fn add(self, other: UTF8Bytes) -> UTF8Bytes {
+ UTF8Bytes(self.0 + other.0)
+ }
+}
+
+impl AddAssign for UTF8Bytes {
+ fn add_assign(&mut self, other: UTF8Bytes) {
+ *self = UTF8Bytes(self.0 + other.0)
+ }
+}
+
+trait StrExt {
+ fn len_utf8(&self) -> UTF8Bytes;
+}
+impl StrExt for str {
+ fn len_utf8(&self) -> UTF8Bytes {
+ UTF8Bytes(self.len())
+ }
+}
+
+#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
+pub struct UTF16CodeUnits(pub usize);
+
+impl UTF16CodeUnits {
+ pub fn zero() -> UTF16CodeUnits {
+ UTF16CodeUnits(0)
+ }
+
+ pub fn one() -> UTF16CodeUnits {
+ UTF16CodeUnits(1)
+ }
+
+ pub fn saturating_sub(self, other: UTF16CodeUnits) -> UTF16CodeUnits {
+ if self > other {
+ UTF16CodeUnits(self.0 - other.0)
+ } else {
+ UTF16CodeUnits::zero()
+ }
+ }
+}
+
+impl Add for UTF16CodeUnits {
+ type Output = UTF16CodeUnits;
+
+ fn add(self, other: UTF16CodeUnits) -> UTF16CodeUnits {
+ UTF16CodeUnits(self.0 + other.0)
+ }
+}
+
+impl AddAssign for UTF16CodeUnits {
+ fn add_assign(&mut self, other: UTF16CodeUnits) {
+ *self = UTF16CodeUnits(self.0 + other.0)
+ }
+}
+
impl From<DOMString> for SelectionDirection {
fn from(direction: DOMString) -> SelectionDirection {
match direction.as_ref() {
@@ -49,33 +132,61 @@ impl From<SelectionDirection> for DOMString {
}
}
-#[derive(JSTraceable, Copy, Clone, HeapSizeOf, PartialEq)]
+#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq, PartialOrd)]
pub struct TextPoint {
/// 0-based line number
pub line: usize,
- /// 0-based column number in UTF-8 bytes
- pub index: usize,
+ /// 0-based column number in bytes
+ pub index: UTF8Bytes,
+}
+
+impl TextPoint {
+ /// Returns a TextPoint constrained to be a valid location within lines
+ fn constrain_to(&self, lines: &[DOMString]) -> TextPoint {
+ let line = min(self.line, lines.len() - 1);
+
+ TextPoint {
+ line,
+ index: min(self.index, lines[line].len_utf8()),
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq)]
+pub struct SelectionState {
+ start: TextPoint,
+ end: TextPoint,
+ direction: SelectionDirection,
}
/// Encapsulated state for handling keyboard input in a single or multiline text input control.
-#[derive(JSTraceable, HeapSizeOf)]
+#[derive(JSTraceable, MallocSizeOf)]
pub struct TextInput<T: ClipboardProvider> {
/// Current text input content, split across lines without trailing '\n'
lines: Vec<DOMString>,
+
/// Current cursor input point
- pub edit_point: TextPoint,
- /// Beginning of selection range with edit_point as end that can span multiple lines.
- pub selection_begin: Option<TextPoint>,
+ edit_point: TextPoint,
+
+ /// The current selection goes from the selection_origin until the edit_point. Note that the
+ /// selection_origin may be after the edit_point, in the case of a backward selection.
+ selection_origin: Option<TextPoint>,
+ selection_direction: SelectionDirection,
+
/// Is this a multiline input?
multiline: bool,
- #[ignore_heap_size_of = "Can't easily measure this generic type"]
+
+ #[ignore_malloc_size_of = "Can't easily measure this generic type"]
clipboard_provider: T,
+
/// The maximum number of UTF-16 code units this text input is allowed to hold.
///
- /// https://html.spec.whatwg.org/multipage/#attr-fe-maxlength
- pub max_length: Option<usize>,
- pub min_length: Option<usize>,
- pub selection_direction: SelectionDirection,
+ /// <https://html.spec.whatwg.org/multipage/#attr-fe-maxlength>
+ max_length: Option<UTF16CodeUnits>,
+ min_length: Option<UTF16CodeUnits>,
+
+ /// Was last change made by set_content?
+ was_last_change_by_set_content: bool,
}
/// Resulting action to be taken by the owner of a text input that is handling an event.
@@ -90,87 +201,117 @@ impl Default for TextPoint {
fn default() -> TextPoint {
TextPoint {
line: 0,
- index: 0,
+ index: UTF8Bytes::zero(),
}
}
}
/// Control whether this control should allow multiple lines.
-#[derive(PartialEq, Eq)]
+#[derive(Eq, PartialEq)]
pub enum Lines {
Single,
Multiple,
}
/// The direction in which to delete a character.
-#[derive(PartialEq, Eq, Copy, Clone)]
+#[derive(Clone, Copy, Eq, PartialEq)]
pub enum Direction {
Forward,
- Backward
+ Backward,
}
-
-/// Was the keyboard event accompanied by the standard control modifier,
-/// i.e. cmd on Mac OS or ctrl on other platforms.
+// Some shortcuts use Cmd on Mac and Control on other systems.
#[cfg(target_os = "macos")]
-fn is_control_key(mods: KeyModifiers) -> bool {
- mods.contains(SUPER) && !mods.contains(CONTROL | ALT)
-}
-
+pub const CMD_OR_CONTROL: Modifiers = Modifiers::META;
#[cfg(not(target_os = "macos"))]
-fn is_control_key(mods: KeyModifiers) -> bool {
- mods.contains(CONTROL) && !mods.contains(SUPER | ALT)
-}
+pub const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
/// The length in bytes of the first n characters in a UTF-8 string.
///
/// If the string has fewer than n characters, returns the length of the whole string.
-fn len_of_first_n_chars(text: &str, n: usize) -> usize {
+/// If n is 0, returns 0
+fn len_of_first_n_chars(text: &str, n: usize) -> UTF8Bytes {
match text.char_indices().take(n).last() {
- Some((index, ch)) => index + ch.len_utf8(),
- None => 0
+ Some((index, ch)) => UTF8Bytes(index + ch.len_utf8()),
+ None => UTF8Bytes::zero(),
}
}
-/// The length in bytes of the first n code units a string when encoded in UTF-16.
+/// The length in bytes of the first n code units in a string when encoded in UTF-16.
///
/// If the string is fewer than n code units, returns the length of the whole string.
-fn len_of_first_n_code_units(text: &str, n: usize) -> usize {
- let mut utf8_len = 0;
- let mut utf16_len = 0;
+fn len_of_first_n_code_units(text: &str, n: UTF16CodeUnits) -> UTF8Bytes {
+ let mut utf8_len = UTF8Bytes::zero();
+ let mut utf16_len = UTF16CodeUnits::zero();
for c in text.chars() {
- utf16_len += c.len_utf16();
+ utf16_len += UTF16CodeUnits(c.len_utf16());
if utf16_len > n {
break;
}
- utf8_len += c.len_utf8();
+ utf8_len += UTF8Bytes(c.len_utf8());
}
utf8_len
}
impl<T: ClipboardProvider> TextInput<T> {
/// Instantiate a new text input control
- pub fn new(lines: Lines, initial: DOMString,
- clipboard_provider: T, max_length: Option<usize>,
- min_length: Option<usize>,
- selection_direction: SelectionDirection) -> TextInput<T> {
+ pub fn new(
+ lines: Lines,
+ initial: DOMString,
+ clipboard_provider: T,
+ max_length: Option<UTF16CodeUnits>,
+ min_length: Option<UTF16CodeUnits>,
+ selection_direction: SelectionDirection,
+ ) -> TextInput<T> {
let mut i = TextInput {
- lines: vec!(),
+ lines: vec![],
edit_point: Default::default(),
- selection_begin: None,
+ selection_origin: None,
multiline: lines == Lines::Multiple,
clipboard_provider: clipboard_provider,
max_length: max_length,
min_length: min_length,
selection_direction: selection_direction,
+ was_last_change_by_set_content: true,
};
i.set_content(initial);
i
}
+ pub fn edit_point(&self) -> TextPoint {
+ self.edit_point
+ }
+
+ pub fn selection_origin(&self) -> Option<TextPoint> {
+ self.selection_origin
+ }
+
+ /// The selection origin, or the edit point if there is no selection. Note that the selection
+ /// origin may be after the edit point, in the case of a backward selection.
+ pub fn selection_origin_or_edit_point(&self) -> TextPoint {
+ self.selection_origin.unwrap_or(self.edit_point)
+ }
+
+ pub fn selection_direction(&self) -> SelectionDirection {
+ self.selection_direction
+ }
+
+ pub fn set_max_length(&mut self, length: Option<UTF16CodeUnits>) {
+ self.max_length = length;
+ }
+
+ pub fn set_min_length(&mut self, length: Option<UTF16CodeUnits>) {
+ self.min_length = length;
+ }
+
+ /// Was last edit made by set_content?
+ pub fn was_last_change_by_set_content(&self) -> bool {
+ self.was_last_change_by_set_content
+ }
+
/// Remove a character at the current editing point
pub fn delete_char(&mut self, dir: Direction) {
- if self.selection_begin.is_none() || self.selection_begin == Some(self.edit_point) {
+ if self.selection_origin.is_none() || self.selection_origin == Some(self.edit_point) {
self.adjust_horizontal_by_one(dir, Selection::Selected);
}
self.replace_selection(DOMString::new());
@@ -183,147 +324,198 @@ impl<T: ClipboardProvider> TextInput<T> {
/// Insert a string at the current editing point
pub fn insert_string<S: Into<String>>(&mut self, s: S) {
- if self.selection_begin.is_none() {
- self.selection_begin = Some(self.edit_point);
+ if self.selection_origin.is_none() {
+ self.selection_origin = Some(self.edit_point);
}
self.replace_selection(DOMString::from(s.into()));
}
- pub fn get_sorted_selection(&self) -> Option<(TextPoint, TextPoint)> {
- self.selection_begin.map(|begin| {
- let end = self.edit_point;
+ /// The start of the selection (or the edit point, if there is no selection). Always less than
+ /// or equal to selection_end(), regardless of the selection direction.
+ pub fn selection_start(&self) -> TextPoint {
+ match self.selection_direction {
+ SelectionDirection::None | SelectionDirection::Forward => {
+ self.selection_origin_or_edit_point()
+ },
+ SelectionDirection::Backward => self.edit_point,
+ }
+ }
- if begin.line < end.line || (begin.line == end.line && begin.index < end.index) {
- (begin, end)
- } else {
- (end, begin)
- }
- })
+ /// The byte offset of the selection_start()
+ pub fn selection_start_offset(&self) -> UTF8Bytes {
+ self.text_point_to_offset(&self.selection_start())
}
- // Check that the selection is valid.
- fn assert_ok_selection(&self) {
- if let Some(begin) = self.selection_begin {
- debug_assert!(begin.line < self.lines.len());
- debug_assert!(begin.index <= self.lines[begin.line].len());
+ /// The end of the selection (or the edit point, if there is no selection). Always greater
+ /// than or equal to selection_start(), regardless of the selection direction.
+ pub fn selection_end(&self) -> TextPoint {
+ match self.selection_direction {
+ SelectionDirection::None | SelectionDirection::Forward => self.edit_point,
+ SelectionDirection::Backward => self.selection_origin_or_edit_point(),
}
- debug_assert!(self.edit_point.line < self.lines.len());
- debug_assert!(self.edit_point.index <= self.lines[self.edit_point.line].len());
}
- /// Return the selection range as UTF-8 byte offsets from the start of the content.
+ /// The byte offset of the selection_end()
+ pub fn selection_end_offset(&self) -> UTF8Bytes {
+ self.text_point_to_offset(&self.selection_end())
+ }
+
+ /// Whether or not there is an active selection (the selection may be zero-length)
+ #[inline]
+ pub fn has_selection(&self) -> bool {
+ self.selection_origin.is_some()
+ }
+
+ /// Returns a tuple of (start, end) giving the bounds of the current selection. start is always
+ /// less than or equal to end.
+ pub fn sorted_selection_bounds(&self) -> (TextPoint, TextPoint) {
+ (self.selection_start(), self.selection_end())
+ }
+
+ /// Return the selection range as byte offsets from the start of the content.
///
- /// If there is no selection, returns an empty range at the insertion point.
- pub fn get_absolute_selection_range(&self) -> Range<usize> {
- match self.get_sorted_selection() {
- Some((begin, end)) => self.get_absolute_point_for_text_point(&begin) ..
- self.get_absolute_point_for_text_point(&end),
- None => {
- let insertion_point = self.get_absolute_insertion_point();
- insertion_point .. insertion_point
+ /// If there is no selection, returns an empty range at the edit point.
+ pub fn sorted_selection_offsets_range(&self) -> Range<UTF8Bytes> {
+ self.selection_start_offset()..self.selection_end_offset()
+ }
+
+ /// The state of the current selection. Can be used to compare whether selection state has changed.
+ pub fn selection_state(&self) -> SelectionState {
+ SelectionState {
+ start: self.selection_start(),
+ end: self.selection_end(),
+ direction: self.selection_direction,
+ }
+ }
+
+ // Check that the selection is valid.
+ fn assert_ok_selection(&self) {
+ debug!(
+ "edit_point: {:?}, selection_origin: {:?}, direction: {:?}",
+ self.edit_point, self.selection_origin, self.selection_direction
+ );
+ if let Some(begin) = self.selection_origin {
+ debug_assert!(begin.line < self.lines.len());
+ debug_assert!(begin.index <= self.lines[begin.line].len_utf8());
+
+ match self.selection_direction {
+ SelectionDirection::None | SelectionDirection::Forward => {
+ debug_assert!(begin <= self.edit_point)
+ },
+
+ SelectionDirection::Backward => debug_assert!(self.edit_point <= begin),
}
}
+
+ debug_assert!(self.edit_point.line < self.lines.len());
+ debug_assert!(self.edit_point.index <= self.lines[self.edit_point.line].len_utf8());
}
pub fn get_selection_text(&self) -> Option<String> {
let text = self.fold_selection_slices(String::new(), |s, slice| s.push_str(slice));
if text.is_empty() {
- return None
+ return None;
}
Some(text)
}
/// The length of the selected text in UTF-16 code units.
- fn selection_utf16_len(&self) -> usize {
- self.fold_selection_slices(0usize,
- |len, slice| *len += slice.chars().map(char::len_utf16).sum())
+ fn selection_utf16_len(&self) -> UTF16CodeUnits {
+ self.fold_selection_slices(UTF16CodeUnits::zero(), |len, slice| {
+ *len += UTF16CodeUnits(slice.chars().map(char::len_utf16).sum::<usize>())
+ })
}
/// Run the callback on a series of slices that, concatenated, make up the selected text.
///
/// The accumulator `acc` can be mutated by the callback, and will be returned at the end.
fn fold_selection_slices<B, F: FnMut(&mut B, &str)>(&self, mut acc: B, mut f: F) -> B {
- match self.get_sorted_selection() {
- Some((begin, end)) if begin.line == end.line => {
- f(&mut acc, &self.lines[begin.line][begin.index..end.index])
- }
- Some((begin, end)) => {
- f(&mut acc, &self.lines[begin.line][begin.index..]);
- for line in &self.lines[begin.line + 1 .. end.line] {
+ if self.has_selection() {
+ let (start, end) = self.sorted_selection_bounds();
+ let UTF8Bytes(start_offset) = start.index;
+ let UTF8Bytes(end_offset) = end.index;
+
+ if start.line == end.line {
+ f(&mut acc, &self.lines[start.line][start_offset..end_offset])
+ } else {
+ f(&mut acc, &self.lines[start.line][start_offset..]);
+ for line in &self.lines[start.line + 1..end.line] {
f(&mut acc, "\n");
f(&mut acc, line);
}
f(&mut acc, "\n");
- f(&mut acc, &self.lines[end.line][..end.index])
+ f(&mut acc, &self.lines[end.line][..end_offset])
}
- None => {}
}
+
acc
}
pub fn replace_selection(&mut self, insert: DOMString) {
- if let Some((begin, end)) = self.get_sorted_selection() {
- let allowed_to_insert_count = if let Some(max_length) = self.max_length {
- let len_after_selection_replaced = self.utf16_len() - self.selection_utf16_len();
- if len_after_selection_replaced >= max_length {
- // If, after deleting the selection, the len is still greater than the max
- // length, then don't delete/insert anything
- return
- }
+ if !self.has_selection() {
+ return;
+ }
- max_length - len_after_selection_replaced
- } else {
- usize::MAX
- };
+ let allowed_to_insert_count = if let Some(max_length) = self.max_length {
+ let len_after_selection_replaced =
+ self.utf16_len().saturating_sub(self.selection_utf16_len());
+ max_length.saturating_sub(len_after_selection_replaced)
+ } else {
+ UTF16CodeUnits(usize::MAX)
+ };
- let last_char_index = len_of_first_n_code_units(&*insert, allowed_to_insert_count);
- let chars_to_insert = &insert[..last_char_index];
+ let UTF8Bytes(last_char_index) =
+ len_of_first_n_code_units(&*insert, allowed_to_insert_count);
+ let to_insert = &insert[..last_char_index];
- self.clear_selection();
+ let (start, end) = self.sorted_selection_bounds();
+ let UTF8Bytes(start_offset) = start.index;
+ let UTF8Bytes(end_offset) = end.index;
- let new_lines = {
- let prefix = &self.lines[begin.line][..begin.index];
- let suffix = &self.lines[end.line][end.index..];
- let lines_prefix = &self.lines[..begin.line];
- let lines_suffix = &self.lines[end.line + 1..];
+ let new_lines = {
+ let prefix = &self.lines[start.line][..start_offset];
+ let suffix = &self.lines[end.line][end_offset..];
+ let lines_prefix = &self.lines[..start.line];
+ let lines_suffix = &self.lines[end.line + 1..];
- let mut insert_lines = if self.multiline {
- chars_to_insert.split('\n').map(|s| DOMString::from(s)).collect()
- } else {
- vec!(DOMString::from(chars_to_insert))
- };
+ let mut insert_lines = if self.multiline {
+ to_insert.split('\n').map(|s| DOMString::from(s)).collect()
+ } else {
+ vec![DOMString::from(to_insert)]
+ };
- // FIXME(ajeffrey): effecient append for DOMStrings
- let mut new_line = prefix.to_owned();
+ // FIXME(ajeffrey): effecient append for DOMStrings
+ let mut new_line = prefix.to_owned();
- new_line.push_str(&insert_lines[0]);
- insert_lines[0] = DOMString::from(new_line);
+ new_line.push_str(&insert_lines[0]);
+ insert_lines[0] = DOMString::from(new_line);
- let last_insert_lines_index = insert_lines.len() - 1;
- self.edit_point.index = insert_lines[last_insert_lines_index].len();
- self.edit_point.line = begin.line + last_insert_lines_index;
+ let last_insert_lines_index = insert_lines.len() - 1;
+ self.edit_point.index = insert_lines[last_insert_lines_index].len_utf8();
+ self.edit_point.line = start.line + last_insert_lines_index;
- // FIXME(ajeffrey): effecient append for DOMStrings
- insert_lines[last_insert_lines_index].push_str(suffix);
+ // FIXME(ajeffrey): effecient append for DOMStrings
+ insert_lines[last_insert_lines_index].push_str(suffix);
- let mut new_lines = vec!();
- new_lines.extend_from_slice(lines_prefix);
- new_lines.extend_from_slice(&insert_lines);
- new_lines.extend_from_slice(lines_suffix);
- new_lines
- };
+ let mut new_lines = vec![];
+ new_lines.extend_from_slice(lines_prefix);
+ new_lines.extend_from_slice(&insert_lines);
+ new_lines.extend_from_slice(lines_suffix);
+ new_lines
+ };
- self.lines = new_lines;
- }
+ self.lines = new_lines;
+ self.was_last_change_by_set_content = false;
+ self.clear_selection();
self.assert_ok_selection();
}
- /// Return the length in UTF-8 bytes of the current line under the editing point.
- pub fn current_line_length(&self) -> usize {
- self.lines[self.edit_point.line].len()
+ /// Return the length in bytes of the current line under the editing point.
+ pub fn current_line_length(&self) -> UTF8Bytes {
+ self.lines[self.edit_point.line].len_utf8()
}
- /// Adjust the editing point position by a given of lines. The resulting column is
+ /// Adjust the editing point position by a given number of lines. The resulting column is
/// as close to the original column position as possible.
pub fn adjust_vertical(&mut self, adjust: isize, select: Selection) {
if !self.multiline {
@@ -331,8 +523,8 @@ impl<T: ClipboardProvider> TextInput<T> {
}
if select == Selection::Selected {
- if self.selection_begin.is_none() {
- self.selection_begin = Some(self.edit_point);
+ if self.selection_origin.is_none() {
+ self.selection_origin = Some(self.edit_point);
}
} else {
self.clear_selection();
@@ -343,100 +535,167 @@ impl<T: ClipboardProvider> TextInput<T> {
let target_line: isize = self.edit_point.line as isize + adjust;
if target_line < 0 {
- self.edit_point.index = 0;
self.edit_point.line = 0;
+ self.edit_point.index = UTF8Bytes::zero();
+ if self.selection_origin.is_some() &&
+ (self.selection_direction == SelectionDirection::None ||
+ self.selection_direction == SelectionDirection::Forward)
+ {
+ self.selection_origin = Some(TextPoint {
+ line: 0,
+ index: UTF8Bytes::zero(),
+ });
+ }
return;
} else if target_line as usize >= self.lines.len() {
self.edit_point.line = self.lines.len() - 1;
self.edit_point.index = self.current_line_length();
+ if self.selection_origin.is_some() &&
+ (self.selection_direction == SelectionDirection::Backward)
+ {
+ self.selection_origin = Some(self.edit_point);
+ }
return;
}
-
- let col = self.lines[self.edit_point.line][..self.edit_point.index].chars().count();
-
+ let UTF8Bytes(edit_index) = self.edit_point.index;
+ let col = self.lines[self.edit_point.line][..edit_index]
+ .chars()
+ .count();
self.edit_point.line = target_line as usize;
+ // NOTE: this adjusts to the nearest complete Unicode codepoint, rather than grapheme cluster
self.edit_point.index = len_of_first_n_chars(&self.lines[self.edit_point.line], col);
+ if let Some(origin) = self.selection_origin {
+ if ((self.selection_direction == SelectionDirection::None ||
+ self.selection_direction == SelectionDirection::Forward) &&
+ self.edit_point <= origin) ||
+ (self.selection_direction == SelectionDirection::Backward &&
+ origin <= self.edit_point)
+ {
+ self.selection_origin = Some(self.edit_point);
+ }
+ }
self.assert_ok_selection();
}
/// Adjust the editing point position by a given number of bytes. If the adjustment
/// requested is larger than is available in the current line, the editing point is
/// adjusted vertically and the process repeats with the remaining adjustment requested.
- pub fn adjust_horizontal(&mut self, adjust: isize, select: Selection) {
- let direction = if adjust >= 0 { Direction::Forward } else { Direction::Backward };
+ pub fn adjust_horizontal(
+ &mut self,
+ adjust: UTF8Bytes,
+ direction: Direction,
+ select: Selection,
+ ) {
if self.adjust_selection_for_horizontal_change(direction, select) {
- return
+ return;
}
- self.perform_horizontal_adjustment(adjust, select);
+ self.perform_horizontal_adjustment(adjust, direction, select);
}
+ /// Adjust the editing point position by exactly one grapheme cluster. If the edit point
+ /// is at the beginning of the line and the direction is "Backward" or the edit point is at
+ /// the end of the line and the direction is "Forward", a vertical adjustment is made
pub fn adjust_horizontal_by_one(&mut self, direction: Direction, select: Selection) {
if self.adjust_selection_for_horizontal_change(direction, select) {
- return
+ return;
}
let adjust = {
let current_line = &self.lines[self.edit_point.line];
- match direction {
- Direction::Forward => {
- match current_line[self.edit_point.index..].graphemes(true).next() {
- Some(c) => c.len() as isize,
- None => 1, // Going to the next line is a "one byte" offset
- }
- }
- Direction::Backward => {
- match current_line[..self.edit_point.index].graphemes(true).next_back() {
- Some(c) => -(c.len() as isize),
- None => -1, // Going to the previous line is a "one byte" offset
- }
- }
+ let UTF8Bytes(current_offset) = self.edit_point.index;
+ let next_ch = match direction {
+ Direction::Forward => current_line[current_offset..].graphemes(true).next(),
+ Direction::Backward => current_line[..current_offset].graphemes(true).next_back(),
+ };
+ match next_ch {
+ Some(c) => UTF8Bytes(c.len() as usize),
+ None => UTF8Bytes::one(), // Going to the next line is a "one byte" offset
}
};
- self.perform_horizontal_adjustment(adjust, select);
+ self.perform_horizontal_adjustment(adjust, direction, select);
}
/// Return whether to cancel the caret move
- fn adjust_selection_for_horizontal_change(&mut self, adjust: Direction, select: Selection)
- -> bool {
+ fn adjust_selection_for_horizontal_change(
+ &mut self,
+ adjust: Direction,
+ select: Selection,
+ ) -> bool {
if select == Selection::Selected {
- if self.selection_begin.is_none() {
- self.selection_begin = Some(self.edit_point);
+ if self.selection_origin.is_none() {
+ self.selection_origin = Some(self.edit_point);
}
} else {
- if let Some((begin, end)) = self.get_sorted_selection() {
+ if self.has_selection() {
self.edit_point = match adjust {
- Direction::Backward => begin,
- Direction::Forward => end,
+ Direction::Backward => self.selection_start(),
+ Direction::Forward => self.selection_end(),
};
self.clear_selection();
- return true
+ return true;
}
}
false
}
- fn perform_horizontal_adjustment(&mut self, adjust: isize, select: Selection) {
- if adjust < 0 {
- let remaining = self.edit_point.index;
- if adjust.abs() as usize > remaining && self.edit_point.line > 0 {
- self.adjust_vertical(-1, select);
- self.edit_point.index = self.current_line_length();
- self.adjust_horizontal(adjust + remaining as isize + 1, select);
- } else {
- self.edit_point.index = max(0, self.edit_point.index as isize + adjust) as usize;
- }
+ /// Update the field selection_direction.
+ ///
+ /// When the edit_point (or focus) is before the selection_origin (or anchor)
+ /// you have a backward selection. Otherwise you have a forward selection.
+ fn update_selection_direction(&mut self) {
+ debug!(
+ "edit_point: {:?}, selection_origin: {:?}",
+ self.edit_point, self.selection_origin
+ );
+ self.selection_direction = if Some(self.edit_point) < self.selection_origin {
+ SelectionDirection::Backward
} else {
- let remaining = self.current_line_length() - self.edit_point.index;
- if adjust as usize > remaining && self.lines.len() > self.edit_point.line + 1 {
- self.adjust_vertical(1, select);
- self.edit_point.index = 0;
- // one shift is consumed by the change of line, hence the -1
- self.adjust_horizontal(adjust - remaining as isize - 1, select);
- } else {
- self.edit_point.index = min(self.current_line_length(),
- self.edit_point.index + adjust as usize);
- }
+ SelectionDirection::Forward
}
+ }
+
+ fn perform_horizontal_adjustment(
+ &mut self,
+ adjust: UTF8Bytes,
+ direction: Direction,
+ select: Selection,
+ ) {
+ match direction {
+ Direction::Backward => {
+ let remaining = self.edit_point.index;
+ if adjust > remaining && self.edit_point.line > 0 {
+ self.adjust_vertical(-1, select);
+ self.edit_point.index = self.current_line_length();
+ // one shift is consumed by the change of line, hence the -1
+ self.adjust_horizontal(
+ adjust.saturating_sub(remaining + UTF8Bytes::one()),
+ direction,
+ select,
+ );
+ } else {
+ self.edit_point.index = remaining.saturating_sub(adjust);
+ }
+ },
+ Direction::Forward => {
+ let remaining = self
+ .current_line_length()
+ .saturating_sub(self.edit_point.index);
+ if adjust > remaining && self.lines.len() > self.edit_point.line + 1 {
+ self.adjust_vertical(1, select);
+ self.edit_point.index = UTF8Bytes::zero();
+ // one shift is consumed by the change of line, hence the -1
+ self.adjust_horizontal(
+ adjust.saturating_sub(remaining + UTF8Bytes::one()),
+ direction,
+ select,
+ );
+ } else {
+ self.edit_point.index =
+ min(self.current_line_length(), self.edit_point.index + adjust);
+ }
+ },
+ };
+ self.update_selection_direction();
self.assert_ok_selection();
}
@@ -452,266 +711,273 @@ impl<T: ClipboardProvider> TextInput<T> {
/// Select all text in the input control.
pub fn select_all(&mut self) {
- self.selection_begin = Some(TextPoint {
+ self.selection_origin = Some(TextPoint {
line: 0,
- index: 0,
+ index: UTF8Bytes::zero(),
});
let last_line = self.lines.len() - 1;
self.edit_point.line = last_line;
- self.edit_point.index = self.lines[last_line].len();
+ self.edit_point.index = self.lines[last_line].len_utf8();
+ self.selection_direction = SelectionDirection::Forward;
self.assert_ok_selection();
}
/// Remove the current selection.
pub fn clear_selection(&mut self) {
- self.selection_begin = None;
+ self.selection_origin = None;
+ self.selection_direction = SelectionDirection::None;
+ }
+
+ /// Remove the current selection and set the edit point to the end of the content.
+ pub fn clear_selection_to_limit(&mut self, direction: Direction) {
+ self.clear_selection();
+ self.adjust_horizontal_to_limit(direction, Selection::NotSelected);
}
pub fn adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection) {
if self.adjust_selection_for_horizontal_change(direction, select) {
- return
+ return;
}
- let shift_increment: isize = {
- let input: &str;
+ let shift_increment: UTF8Bytes = {
+ let current_index = self.edit_point.index;
+ let current_line = self.edit_point.line;
+ let mut newline_adjustment = UTF8Bytes::zero();
+ let mut shift_temp = UTF8Bytes::zero();
match direction {
Direction::Backward => {
- let remaining = self.edit_point.index;
- let current_line = self.edit_point.line;
- let mut newline_adjustment = 0;
- if remaining == 0 && current_line > 0 {
- input = &self
- .lines[current_line-1];
- newline_adjustment = 1;
+ let input: &str;
+ if current_index == UTF8Bytes::zero() && current_line > 0 {
+ input = &self.lines[current_line - 1];
+ newline_adjustment = UTF8Bytes::one();
} else {
- input = &self
- .lines[current_line]
- [..remaining];
+ let UTF8Bytes(remaining) = current_index;
+ input = &self.lines[current_line][..remaining];
}
let mut iter = input.split_word_bounds().rev();
- let mut shift_temp: isize = 0;
loop {
match iter.next() {
None => break,
Some(x) => {
- shift_temp += - (x.len() as isize);
+ shift_temp += UTF8Bytes(x.len() as usize);
if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
break;
}
- }
+ },
}
}
- shift_temp - newline_adjustment
- }
+ },
Direction::Forward => {
- let remaining = self.current_line_length() - self.edit_point.index;
- let current_line = self.edit_point.line;
- let mut newline_adjustment = 0;
- if remaining == 0 && self.lines.len() > self.edit_point.line + 1 {
- input = &self
- .lines[current_line + 1];
- newline_adjustment = 1;
+ let input: &str;
+ let remaining = self.current_line_length().saturating_sub(current_index);
+ if remaining == UTF8Bytes::zero() && self.lines.len() > self.edit_point.line + 1
+ {
+ input = &self.lines[current_line + 1];
+ newline_adjustment = UTF8Bytes::one();
} else {
- input = &self
- .lines[current_line]
- [self.edit_point.index..];
+ let UTF8Bytes(current_offset) = current_index;
+ input = &self.lines[current_line][current_offset..];
}
let mut iter = input.split_word_bounds();
- let mut shift_temp: isize = 0;
loop {
match iter.next() {
None => break,
Some(x) => {
- shift_temp += x.len() as isize;
+ shift_temp += UTF8Bytes(x.len() as usize);
if x.chars().any(|x| x.is_alphabetic() || x.is_numeric()) {
break;
}
- }
+ },
}
}
- shift_temp + newline_adjustment
- }
- }
+ },
+ };
+
+ shift_temp + newline_adjustment
};
- self.adjust_horizontal(shift_increment, select);
+ self.adjust_horizontal(shift_increment, direction, select);
}
pub fn adjust_horizontal_to_line_end(&mut self, direction: Direction, select: Selection) {
if self.adjust_selection_for_horizontal_change(direction, select) {
- return
+ return;
}
- let shift: isize = {
+ let shift: usize = {
let current_line = &self.lines[self.edit_point.line];
+ let UTF8Bytes(current_offset) = self.edit_point.index;
match direction {
- Direction::Backward => {
- - (current_line[..self.edit_point.index].len() as isize)
- },
- Direction::Forward => {
- current_line[self.edit_point.index..].len() as isize
- }
+ Direction::Backward => current_line[..current_offset].len(),
+ Direction::Forward => current_line[current_offset..].len(),
}
};
- self.perform_horizontal_adjustment(shift, select);
+ self.perform_horizontal_adjustment(UTF8Bytes(shift), direction, select);
}
pub fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection) {
if self.adjust_selection_for_horizontal_change(direction, select) {
- return
+ return;
}
match direction {
Direction::Backward => {
self.edit_point.line = 0;
- self.edit_point.index = 0;
+ self.edit_point.index = UTF8Bytes::zero();
},
Direction::Forward => {
self.edit_point.line = &self.lines.len() - 1;
- self.edit_point.index = (&self.lines[&self.lines.len() - 1]).len();
- }
+ self.edit_point.index = (&self.lines[&self.lines.len() - 1]).len_utf8();
+ },
}
}
/// Process a given `KeyboardEvent` and return an action for the caller to execute.
pub fn handle_keydown(&mut self, event: &KeyboardEvent) -> KeyReaction {
- if let Some(key) = event.get_key() {
- self.handle_keydown_aux(event.printable(), key, event.get_key_modifiers())
- } else {
- KeyReaction::Nothing
- }
+ let key = event.key();
+ let mods = event.modifiers();
+ self.handle_keydown_aux(key, mods, cfg!(target_os = "macos"))
}
- pub fn handle_keydown_aux(&mut self,
- printable: Option<char>,
- key: Key,
- mods: KeyModifiers) -> KeyReaction {
- let maybe_select = if mods.contains(SHIFT) { Selection::Selected } else { Selection::NotSelected };
- match (printable, key) {
- (_, Key::B) if mods.contains(CONTROL | ALT) => {
+ // This function exists for easy unit testing.
+ // To test Mac OS shortcuts on other systems a flag is passed.
+ pub fn handle_keydown_aux(
+ &mut self,
+ key: Key,
+ mut mods: Modifiers,
+ macos: bool,
+ ) -> KeyReaction {
+ let maybe_select = if mods.contains(Modifiers::SHIFT) {
+ Selection::Selected
+ } else {
+ Selection::NotSelected
+ };
+ mods.remove(Modifiers::SHIFT);
+ ShortcutMatcher::new(KeyState::Down, key.clone(), mods)
+ .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'B', || {
self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- (_, Key::F) if mods.contains(CONTROL | ALT) => {
+ })
+ .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'F', || {
self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- (_, Key::A) if mods.contains(CONTROL | ALT) => {
+ })
+ .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'A', || {
self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- (_, Key::E) if mods.contains(CONTROL | ALT) => {
+ })
+ .shortcut(Modifiers::CONTROL | Modifiers::ALT, 'E', || {
self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- #[cfg(target_os = "macos")]
- (None, Key::A) if mods == CONTROL => {
+ })
+ .optional_shortcut(macos, Modifiers::CONTROL, 'A', || {
self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- #[cfg(target_os = "macos")]
- (None, Key::E) if mods == CONTROL => {
+ })
+ .optional_shortcut(macos, Modifiers::CONTROL, 'E', || {
self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- (Some('a'), _) if is_control_key(mods) => {
+ })
+ .shortcut(CMD_OR_CONTROL, 'A', || {
self.select_all();
KeyReaction::RedrawSelection
- },
- (Some('c'), _) if is_control_key(mods) => {
+ })
+ .shortcut(CMD_OR_CONTROL, 'X', || {
if let Some(text) = self.get_selection_text() {
self.clipboard_provider.set_clipboard_contents(text);
+ self.delete_char(Direction::Backward);
}
KeyReaction::DispatchInput
- },
- (Some('v'), _) if is_control_key(mods) => {
+ })
+ .shortcut(CMD_OR_CONTROL, 'C', || {
+ if let Some(text) = self.get_selection_text() {
+ self.clipboard_provider.set_clipboard_contents(text);
+ }
+ KeyReaction::DispatchInput
+ })
+ .shortcut(CMD_OR_CONTROL, 'V', || {
let contents = self.clipboard_provider.clipboard_contents();
self.insert_string(contents);
KeyReaction::DispatchInput
- },
- (Some(c), _) => {
- self.insert_char(c);
- KeyReaction::DispatchInput
- },
- #[cfg(target_os = "macos")]
- (None, Key::Home) => {
- KeyReaction::RedrawSelection
- },
- #[cfg(target_os = "macos")]
- (None, Key::End) => {
- KeyReaction::RedrawSelection
- },
- (None, Key::Delete) => {
+ })
+ .shortcut(Modifiers::empty(), Key::Delete, || {
self.delete_char(Direction::Forward);
KeyReaction::DispatchInput
- },
- (None, Key::Backspace) => {
+ })
+ .shortcut(Modifiers::empty(), Key::Backspace, || {
self.delete_char(Direction::Backward);
KeyReaction::DispatchInput
- },
- #[cfg(target_os = "macos")]
- (None, Key::Left) if mods.contains(SUPER) => {
+ })
+ .optional_shortcut(macos, Modifiers::META, Key::ArrowLeft, || {
self.adjust_horizontal_to_line_end(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- #[cfg(target_os = "macos")]
- (None, Key::Right) if mods.contains(SUPER) => {
+ })
+ .optional_shortcut(macos, Modifiers::META, Key::ArrowRight, || {
self.adjust_horizontal_to_line_end(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- #[cfg(target_os = "macos")]
- (None, Key::Up) if mods.contains(SUPER) => {
+ })
+ .optional_shortcut(macos, Modifiers::META, Key::ArrowUp, || {
self.adjust_horizontal_to_limit(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- #[cfg(target_os = "macos")]
- (None, Key::Down) if mods.contains(SUPER) => {
+ })
+ .optional_shortcut(macos, Modifiers::META, Key::ArrowDown, || {
self.adjust_horizontal_to_limit(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Left) if mods.contains(ALT) => {
+ })
+ .shortcut(Modifiers::ALT, Key::ArrowLeft, || {
self.adjust_horizontal_by_word(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Right) if mods.contains(ALT) => {
+ })
+ .shortcut(Modifiers::ALT, Key::ArrowRight, || {
self.adjust_horizontal_by_word(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Left) => {
+ })
+ .shortcut(Modifiers::empty(), Key::ArrowLeft, || {
self.adjust_horizontal_by_one(Direction::Backward, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Right) => {
+ })
+ .shortcut(Modifiers::empty(), Key::ArrowRight, || {
self.adjust_horizontal_by_one(Direction::Forward, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Up) => {
+ })
+ .shortcut(Modifiers::empty(), Key::ArrowUp, || {
self.adjust_vertical(-1, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Down) => {
+ })
+ .shortcut(Modifiers::empty(), Key::ArrowDown, || {
self.adjust_vertical(1, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::Enter) | (None, Key::KpEnter) => self.handle_return(),
- (None, Key::Home) => {
- self.edit_point.index = 0;
+ })
+ .shortcut(Modifiers::empty(), Key::Enter, || self.handle_return())
+ .optional_shortcut(macos, Modifiers::empty(), Key::Home, || {
+ self.edit_point.index = UTF8Bytes::zero();
KeyReaction::RedrawSelection
- },
- (None, Key::End) => {
+ })
+ .optional_shortcut(macos, Modifiers::empty(), Key::End, || {
self.edit_point.index = self.current_line_length();
self.assert_ok_selection();
KeyReaction::RedrawSelection
- },
- (None, Key::PageUp) => {
+ })
+ .shortcut(Modifiers::empty(), Key::PageUp, || {
self.adjust_vertical(-28, maybe_select);
KeyReaction::RedrawSelection
- },
- (None, Key::PageDown) => {
+ })
+ .shortcut(Modifiers::empty(), Key::PageDown, || {
self.adjust_vertical(28, maybe_select);
KeyReaction::RedrawSelection
- },
- _ => KeyReaction::Nothing,
- }
+ })
+ .otherwise(|| {
+ if let Key::Character(ref c) = key {
+ self.insert_string(c.as_str());
+ return KeyReaction::DispatchInput;
+ }
+ KeyReaction::Nothing
+ })
+ .unwrap()
+ }
+
+ pub fn handle_compositionend(&mut self, event: &CompositionEvent) -> KeyReaction {
+ self.insert_string(event.data());
+ KeyReaction::DispatchInput
}
/// Whether the content is empty.
@@ -720,20 +986,27 @@ impl<T: ClipboardProvider> TextInput<T> {
}
/// The length of the content in bytes.
- pub fn len(&self) -> usize {
- self.lines.iter().fold(0, |m, l| {
- m + l.len() + 1 // + 1 for the '\n'
- }) - 1
+ pub fn len_utf8(&self) -> UTF8Bytes {
+ self.lines
+ .iter()
+ .fold(UTF8Bytes::zero(), |m, l| {
+ m + l.len_utf8() + UTF8Bytes::one() // + 1 for the '\n'
+ })
+ .saturating_sub(UTF8Bytes::one())
}
- /// The length of the content in bytes.
- pub fn utf16_len(&self) -> usize {
- self.lines.iter().fold(0, |m, l| {
- m + l.chars().map(char::len_utf16).sum::<usize>() + 1 // + 1 for the '\n'
- }) - 1
+ /// The total number of code units required to encode the content in utf16.
+ pub fn utf16_len(&self) -> UTF16CodeUnits {
+ self.lines
+ .iter()
+ .fold(UTF16CodeUnits::zero(), |m, l| {
+ m + UTF16CodeUnits(l.chars().map(char::len_utf16).sum::<usize>() + 1)
+ // + 1 for the '\n'
+ })
+ .saturating_sub(UTF16CodeUnits::one())
}
- /// The length of the content in chars.
+ /// The length of the content in Unicode code points.
pub fn char_count(&self) -> usize {
self.lines.iter().fold(0, |m, l| {
m + l.chars().count() + 1 // + 1 for the '\n'
@@ -752,65 +1025,82 @@ impl<T: ClipboardProvider> TextInput<T> {
DOMString::from(content)
}
+ /// Get a reference to the contents of a single-line text input. Panics if self is a multiline input.
+ pub fn single_line_content(&self) -> &DOMString {
+ assert!(!self.multiline);
+ &self.lines[0]
+ }
+
/// Set the current contents of the text input. If this is control supports multiple lines,
/// any \n encountered will be stripped and force a new logical line.
pub fn set_content(&mut self, content: DOMString) {
self.lines = if self.multiline {
- content.split('\n').map(DOMString::from).collect()
+ // https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
+ content
+ .replace("\r\n", "\n")
+ .split(|c| c == '\n' || c == '\r')
+ .map(DOMString::from)
+ .collect()
} else {
- vec!(content)
+ vec![content]
};
- self.edit_point.line = min(self.edit_point.line, self.lines.len() - 1);
- self.edit_point.index = min(self.edit_point.index, self.current_line_length());
- self.selection_begin = None;
- self.assert_ok_selection();
- }
- /// Get the insertion point as a byte offset from the start of the content.
- pub fn get_absolute_insertion_point(&self) -> usize {
- self.get_absolute_point_for_text_point(&self.edit_point)
+ self.was_last_change_by_set_content = true;
+ self.edit_point = self.edit_point.constrain_to(&self.lines);
+
+ if let Some(origin) = self.selection_origin {
+ self.selection_origin = Some(origin.constrain_to(&self.lines));
+ }
+ self.assert_ok_selection();
}
/// Convert a TextPoint into a byte offset from the start of the content.
- pub fn get_absolute_point_for_text_point(&self, text_point: &TextPoint) -> usize {
- self.lines.iter().enumerate().fold(0, |acc, (i, val)| {
- if i < text_point.line {
- acc + val.len() + 1 // +1 for the \n
- } else {
- acc
- }
- }) + text_point.index
+ fn text_point_to_offset(&self, text_point: &TextPoint) -> UTF8Bytes {
+ self.lines
+ .iter()
+ .enumerate()
+ .fold(UTF8Bytes::zero(), |acc, (i, val)| {
+ if i < text_point.line {
+ acc + val.len_utf8() + UTF8Bytes::one() // +1 for the \n
+ } else {
+ acc
+ }
+ }) +
+ text_point.index
}
/// Convert a byte offset from the start of the content into a TextPoint.
- pub fn get_text_point_for_absolute_point(&self, abs_point: usize) -> TextPoint {
+ fn offset_to_text_point(&self, abs_point: UTF8Bytes) -> TextPoint {
let mut index = abs_point;
let mut line = 0;
-
let last_line_idx = self.lines.len() - 1;
- self.lines.iter().enumerate().fold(0, |acc, (i, val)| {
- if i != last_line_idx {
- let line_end = max(val.len(), 1);
- let new_acc = acc + line_end;
- if abs_point > new_acc && index > line_end {
- index -= line_end + 1;
- line += 1;
+ self.lines
+ .iter()
+ .enumerate()
+ .fold(UTF8Bytes::zero(), |acc, (i, val)| {
+ if i != last_line_idx {
+ let line_end = val.len_utf8();
+ let new_acc = acc + line_end + UTF8Bytes::one();
+ if abs_point >= new_acc && index > line_end {
+ index = index.saturating_sub(line_end + UTF8Bytes::one());
+ line += 1;
+ }
+ new_acc
+ } else {
+ acc
}
- new_acc
- } else {
- acc
- }
- });
+ });
TextPoint {
- line: line, index: index
+ line: line,
+ index: index,
}
}
- pub fn set_selection_range(&mut self, start: u32, end: u32) {
- let mut start = start as usize;
- let mut end = end as usize;
- let text_end = self.get_content().len();
+ pub fn set_selection_range(&mut self, start: u32, end: u32, direction: SelectionDirection) {
+ let mut start = UTF8Bytes(start as usize);
+ let mut end = UTF8Bytes(end as usize);
+ let text_end = self.get_content().len_utf8();
if end > text_end {
end = text_end;
@@ -819,36 +1109,27 @@ impl<T: ClipboardProvider> TextInput<T> {
start = end;
}
- match self.selection_direction {
- SelectionDirection::None |
- SelectionDirection::Forward => {
- self.selection_begin = Some(self.get_text_point_for_absolute_point(start));
- self.edit_point = self.get_text_point_for_absolute_point(end);
+ self.selection_direction = direction;
+
+ match direction {
+ SelectionDirection::None | SelectionDirection::Forward => {
+ self.selection_origin = Some(self.offset_to_text_point(start));
+ self.edit_point = self.offset_to_text_point(end);
},
SelectionDirection::Backward => {
- self.selection_begin = Some(self.get_text_point_for_absolute_point(end));
- self.edit_point = self.get_text_point_for_absolute_point(start);
- }
+ self.selection_origin = Some(self.offset_to_text_point(end));
+ self.edit_point = self.offset_to_text_point(start);
+ },
}
self.assert_ok_selection();
}
- pub fn get_selection_start(&self) -> u32 {
- let selection_start = match self.selection_begin {
- Some(selection_begin_point) => {
- self.get_absolute_point_for_text_point(&selection_begin_point)
- },
- None => self.get_absolute_insertion_point()
- };
-
- selection_start as u32
- }
-
+ /// Set the edit point index position based off of a given grapheme cluster offset
pub fn set_edit_point_index(&mut self, index: usize) {
- let byte_size = self.lines[self.edit_point.line]
+ let byte_offset = self.lines[self.edit_point.line]
.graphemes(true)
.take(index)
- .fold(0, |acc, x| acc + x.len());
- self.edit_point.index = byte_size;
+ .fold(UTF8Bytes::zero(), |acc, x| acc + x.len_utf8());
+ self.edit_point.index = byte_offset;
}
}