collapse UnicodeTextStorage api to only expose methods operating on the direction/boundary enums
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
//! - text sanitation & validation (see no-op implementation of [`EditableTextState::validate_incoming_text`])
|
||||
//! - nav & select via PageUp/PageDown
|
||||
//! - masking text (e.g. for passwords)
|
||||
//! - disabling `insert_tab` if favor of tab being used to change focus between elements (i.e. escaping the field)
|
||||
//!
|
||||
|
||||
pub mod actions;
|
||||
mod caret;
|
||||
|
||||
@@ -904,10 +904,13 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(slice.to_string()));
|
||||
self.replace_text(self.selected_range.clone(), "");
|
||||
} else {
|
||||
use NavigationDirection::*;
|
||||
use TextBoundary::*;
|
||||
|
||||
// No selection: cut the entire current line (including newline)
|
||||
let caret = self.caret_pos();
|
||||
let line_start = self.storage.find_line_start(caret);
|
||||
let line_end = self.storage.find_line_end(caret);
|
||||
let line_start = self.storage.offset_from_caret(caret, Back, Line);
|
||||
let line_end = self.storage.offset_from_caret(caret, Forward, Line);
|
||||
let storage_len_utf8 = self.storage.content_utf8().len();
|
||||
|
||||
// Include the newline character if there is one after the line
|
||||
@@ -976,7 +979,7 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'app, Self>,
|
||||
) {
|
||||
let character_pos = self.index_for_pixel_point(text_position, window.line_height());
|
||||
let caret_pos = self.index_for_pixel_point(text_position, window.line_height());
|
||||
|
||||
window.focus(&self.focus_handle, cx);
|
||||
self.is_selecting = true;
|
||||
@@ -999,13 +1002,15 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
|
||||
match self.click_count {
|
||||
2 => {
|
||||
let (word_start, word_end) = self.storage.word_range_at(character_pos);
|
||||
self.selected_range = word_start..word_end;
|
||||
self.selected_range = self.storage.word_range_at(caret_pos);
|
||||
cx.notify();
|
||||
}
|
||||
3 => {
|
||||
let line_start = self.storage.find_line_start(character_pos);
|
||||
let line_end = self.storage.find_line_end(character_pos);
|
||||
use NavigationDirection::*;
|
||||
use TextBoundary::*;
|
||||
|
||||
let line_start = self.storage.offset_from_caret(caret_pos, Back, Line);
|
||||
let line_end = self.storage.offset_from_caret(caret_pos, Forward, Line);
|
||||
let line_end_with_newline = if line_end < self.storage.content_utf8().len() {
|
||||
line_end + 1
|
||||
} else {
|
||||
@@ -1016,9 +1021,9 @@ impl<'app> EditableTextActionHandler<Context<'app, Self>> for EditableTextState
|
||||
}
|
||||
_ => {
|
||||
if event.modifiers.shift {
|
||||
self.select_to(character_pos, cx);
|
||||
self.select_to(caret_pos, cx);
|
||||
} else {
|
||||
self.move_to(character_pos, cx);
|
||||
self.move_to(caret_pos, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1643,14 +1648,18 @@ mod tests {
|
||||
let view = create_test_input(cx, "first\nsecond\nthird", 0..0);
|
||||
view.update(cx, |view, _window, cx| {
|
||||
view.input.update(cx, |input, _cx| {
|
||||
assert_eq!(input.storage().find_line_start(0), 0);
|
||||
assert_eq!(input.storage().find_line_start(3), 0);
|
||||
assert_eq!(input.storage().find_line_start(6), 6);
|
||||
assert_eq!(input.storage().find_line_start(13), 13);
|
||||
use NavigationDirection::*;
|
||||
use TextBoundary::*;
|
||||
let storage = input.storage();
|
||||
|
||||
assert_eq!(input.storage().find_line_end(0), 5);
|
||||
assert_eq!(input.storage().find_line_end(6), 12);
|
||||
assert_eq!(input.storage().find_line_end(13), 18);
|
||||
assert_eq!(storage.offset_from_caret(0, Back, Line), 0);
|
||||
assert_eq!(storage.offset_from_caret(3, Back, Line), 0);
|
||||
assert_eq!(storage.offset_from_caret(6, Back, Line), 6);
|
||||
assert_eq!(storage.offset_from_caret(13, Back, Line), 13);
|
||||
|
||||
assert_eq!(storage.offset_from_caret(0, Forward, Line), 5);
|
||||
assert_eq!(storage.offset_from_caret(6, Forward, Line), 12);
|
||||
assert_eq!(storage.offset_from_caret(13, Forward, Line), 18);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1720,7 +1729,9 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello", 0..0);
|
||||
view.update(cx, |view, _window, cx| {
|
||||
view.input.update(cx, |input, _cx| {
|
||||
assert_eq!(input.storage().previous_boundary(0), 0);
|
||||
use NavigationDirection::*;
|
||||
use TextBoundary::*;
|
||||
assert_eq!(input.storage().offset_from_caret(0, Back, Graphmeme), 0);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1731,8 +1742,11 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello", 0..0);
|
||||
view.update(cx, |view, _window, cx| {
|
||||
view.input.update(cx, |input, _cx| {
|
||||
assert_eq!(input.storage().next_boundary(5), 5);
|
||||
assert_eq!(input.storage().next_boundary(100), 5);
|
||||
use NavigationDirection::*;
|
||||
use TextBoundary::*;
|
||||
let storage = input.storage();
|
||||
assert_eq!(storage.offset_from_caret(5, Forward, Graphmeme), 5);
|
||||
assert_eq!(storage.offset_from_caret(100, Forward, Graphmeme), 5);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
@@ -1743,13 +1757,13 @@ mod tests {
|
||||
let view = create_test_input(cx, "hello world", 0..0);
|
||||
view.update(cx, |view, _window, cx| {
|
||||
view.input.update(cx, |input, _cx| {
|
||||
let (start, end) = input.storage().word_range_at(5);
|
||||
assert_eq!(start, 0);
|
||||
assert_eq!(end, 5);
|
||||
let range = input.storage().word_range_at(5);
|
||||
assert_eq!(range.start, 0);
|
||||
assert_eq!(range.end, 5);
|
||||
|
||||
let (start, end) = input.storage().word_range_at(8);
|
||||
assert_eq!(start, 6);
|
||||
assert_eq!(end, 11);
|
||||
let range = input.storage().word_range_at(8);
|
||||
assert_eq!(range.start, 6);
|
||||
assert_eq!(range.end, 11);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -4,13 +4,13 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// Describes a boundary within a chunk of text.
|
||||
pub enum TextBoundary {
|
||||
/// The next utf-8 character in a direction from the caret
|
||||
/// The utf-8 character
|
||||
Graphmeme,
|
||||
/// The next word in a direction from the caret
|
||||
/// The current word (using whitespace as delimiters)
|
||||
Word,
|
||||
/// The start/end of the current line
|
||||
/// The current line
|
||||
Line,
|
||||
/// The rest of the text to the start/end of a document
|
||||
/// The entire document
|
||||
Document,
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ pub trait UnicodeTextStorage {
|
||||
/// Replace contents within the provided range with the given str slice.
|
||||
fn replace_range(&mut self, range: Range<usize>, text: &str);
|
||||
|
||||
// TODO: Refine the api for these methods
|
||||
|
||||
/// Returns the utf16 position equivalent of the provided utf8 character position.
|
||||
fn utf_offset_8to16(&self, pos_uft8: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_uft8 == 0 {
|
||||
@@ -86,6 +85,7 @@ pub trait UnicodeTextStorage {
|
||||
count_utf16
|
||||
}
|
||||
|
||||
/// Returns the utf8 position equivalent of the provided utf16 character position.
|
||||
fn utf_offset_16to8(&self, pos_utf16: usize) -> usize {
|
||||
// Fast path: if offset is 0, return 0
|
||||
if pos_utf16 == 0 {
|
||||
@@ -102,104 +102,20 @@ pub trait UnicodeTextStorage {
|
||||
self.content_utf8().len()
|
||||
}
|
||||
|
||||
/// Converts a utf8 character range into a utf16 character range.
|
||||
fn utf_range_8to16(&self, range_utf8: &Range<usize>) -> Range<usize> {
|
||||
self.utf_offset_8to16(range_utf8.start)..self.utf_offset_8to16(range_utf8.end)
|
||||
}
|
||||
|
||||
/// Converts a utf16 character range into a utf8 character range.
|
||||
fn utf_range_16to8(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
self.utf_offset_16to8(range_utf16.start)..self.utf_offset_16to8(range_utf16.end)
|
||||
}
|
||||
|
||||
fn previous_boundary(&self, offset: usize) -> usize {
|
||||
if offset == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let text_before = &self.content_utf8()[..offset.min(self.content_utf8().len())];
|
||||
text_before
|
||||
.grapheme_indices(true)
|
||||
.map(|(i, _)| i)
|
||||
.next_back()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn next_boundary(&self, offset: usize) -> usize {
|
||||
let len_utf8 = self.content_utf8().len();
|
||||
if offset >= len_utf8 {
|
||||
return len_utf8;
|
||||
}
|
||||
|
||||
let text_after = &self.content_utf8()[offset..];
|
||||
text_after
|
||||
.grapheme_indices(true)
|
||||
.nth(1)
|
||||
.map(|(i, _)| offset + i)
|
||||
.unwrap_or(len_utf8)
|
||||
}
|
||||
|
||||
fn previous_word_boundary(&self, offset: usize) -> usize {
|
||||
if offset == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let text_before = &self.content_utf8()[..offset.min(self.content_utf8().len())];
|
||||
|
||||
let mut last_word_start = 0;
|
||||
for (idx, _) in text_before.unicode_word_indices() {
|
||||
if idx < offset {
|
||||
last_word_start = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if last_word_start == 0 && offset > 0 {
|
||||
let trimmed = text_before.trim_end();
|
||||
if trimmed.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
for (idx, _) in trimmed.unicode_word_indices() {
|
||||
last_word_start = idx;
|
||||
}
|
||||
}
|
||||
|
||||
last_word_start
|
||||
}
|
||||
|
||||
fn next_word_boundary(&self, offset: usize) -> usize {
|
||||
let len_utf8 = self.content_utf8().len();
|
||||
if offset >= len_utf8 {
|
||||
return len_utf8;
|
||||
}
|
||||
|
||||
let text_after = &self.content_utf8()[offset..];
|
||||
|
||||
for (idx, word) in text_after.unicode_word_indices() {
|
||||
let word_end = offset + idx + word.len();
|
||||
if word_end > offset {
|
||||
return word_end;
|
||||
}
|
||||
}
|
||||
|
||||
len_utf8
|
||||
}
|
||||
|
||||
/// Returns the utf-8 character position of first character after the first new-line preceeding the character at the provided utf-8 character position.
|
||||
fn find_line_start(&self, position: usize) -> usize {
|
||||
let content = self.content_utf8();
|
||||
content[..position.min(content.len())]
|
||||
.rfind('\n')
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the utf-8 character position of the character immediately before the first new-line character after the character at the provided utf-8 character position.
|
||||
fn find_line_end(&self, position: usize) -> usize {
|
||||
let content = self.content_utf8();
|
||||
content[position.min(content.len())..]
|
||||
.find('\n')
|
||||
.map(|pos| position + pos)
|
||||
.unwrap_or(content.len())
|
||||
}
|
||||
|
||||
/// Builds a utf8 character range based on a caret position within the storage,
|
||||
/// the direction to traverse, and the boundary to stop at.
|
||||
/// The start of the range will be the earlier position (destination if Back, caret if Forward),
|
||||
/// and the end will be the later position (caret if Back, destination if Forward).
|
||||
fn range_from_caret(
|
||||
&self,
|
||||
caret: usize,
|
||||
@@ -213,37 +129,109 @@ pub trait UnicodeTextStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the next location from the caret based on the direction to traverse and the boundary to stop at.
|
||||
fn offset_from_caret(
|
||||
&self,
|
||||
caret: usize,
|
||||
direction: NavigationDirection,
|
||||
magnitude: TextBoundary,
|
||||
boundary: TextBoundary,
|
||||
) -> usize {
|
||||
use NavigationDirection::*;
|
||||
use TextBoundary::*;
|
||||
match (direction, magnitude) {
|
||||
(Back, Graphmeme) => self.previous_boundary(caret),
|
||||
(Forward, Graphmeme) => self.next_boundary(caret),
|
||||
(Back, Word) => self.previous_word_boundary(caret),
|
||||
(Forward, Word) => self.next_word_boundary(caret),
|
||||
(Back, Line) => self.find_line_start(caret),
|
||||
(Forward, Line) => self.find_line_end(caret),
|
||||
match (direction, boundary) {
|
||||
(Back, Graphmeme) => {
|
||||
if caret == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let str = self.content_utf8();
|
||||
let iter = str[..caret.min(str.len())].grapheme_indices(true);
|
||||
iter.map(|(i, _)| i).next_back().unwrap_or(0)
|
||||
}
|
||||
(Forward, Graphmeme) => {
|
||||
let str = self.content_utf8();
|
||||
let len_utf8 = str.len();
|
||||
if caret >= len_utf8 {
|
||||
return len_utf8;
|
||||
}
|
||||
|
||||
let mut iter = str[caret..].grapheme_indices(true);
|
||||
iter.nth(1).map(|(i, _)| caret + i).unwrap_or(len_utf8)
|
||||
}
|
||||
(Back, Word) => {
|
||||
if caret == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let str = self.content_utf8();
|
||||
let str = &str[..caret.min(str.len())];
|
||||
|
||||
let mut last_word_start = 0;
|
||||
for (idx, _) in str.unicode_word_indices() {
|
||||
if idx < caret {
|
||||
last_word_start = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if last_word_start == 0 && caret > 0 {
|
||||
let trimmed = str.trim_end();
|
||||
if trimmed.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
for (idx, _) in trimmed.unicode_word_indices() {
|
||||
last_word_start = idx;
|
||||
}
|
||||
}
|
||||
|
||||
last_word_start
|
||||
}
|
||||
(Forward, Word) => {
|
||||
let str = self.content_utf8();
|
||||
let len_utf8 = str.len();
|
||||
if caret >= len_utf8 {
|
||||
return len_utf8;
|
||||
}
|
||||
|
||||
let str = &str[caret..];
|
||||
for (idx, word) in str.unicode_word_indices() {
|
||||
let word_end = caret + idx + word.len();
|
||||
if word_end > caret {
|
||||
return word_end;
|
||||
}
|
||||
}
|
||||
len_utf8
|
||||
}
|
||||
// Returns the utf-8 character position of first character after the first new-line
|
||||
// preceding the character at the provided utf-8 character position.
|
||||
(Back, Line) => {
|
||||
let str = self.content_utf8();
|
||||
let iter = str[..caret.min(str.len())].rfind('\n');
|
||||
iter.map(|pos| pos + 1).unwrap_or(0)
|
||||
}
|
||||
// Returns the utf-8 character position of the character immediately before the first
|
||||
// new-line character after the character at the provided utf-8 character position.
|
||||
(Forward, Line) => {
|
||||
let str = self.content_utf8();
|
||||
let iter = str[caret.min(str.len())..].find('\n');
|
||||
iter.map(|pos| caret + pos).unwrap_or(str.len())
|
||||
}
|
||||
(Back, Document) => 0,
|
||||
(Forward, Document) => self.content_utf8().len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn word_range_at(&self, offset: usize) -> (usize, usize) {
|
||||
let offset = offset.min(self.content_utf8().len());
|
||||
/// Returns the start and end of the word the position resides within.
|
||||
fn word_range_at(&self, position: usize) -> Range<usize> {
|
||||
let offset = position.min(self.content_utf8().len());
|
||||
|
||||
for (idx, word) in self.content_utf8().unicode_word_indices() {
|
||||
let word_end = idx + word.len();
|
||||
if offset >= idx && offset <= word_end {
|
||||
return (idx, word_end);
|
||||
return idx..word_end;
|
||||
}
|
||||
}
|
||||
|
||||
(offset, offset)
|
||||
offset..offset
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user