Merge pull request #790 from Demonthos/make-cursor-agnostic-over-storage

Make text editing utilities in native core agnostic over the text storage
This commit is contained in:
Jon Kelley 2023-01-21 00:40:16 -08:00 committed by GitHub
commit e686e42cfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 93 deletions

View file

@ -1,87 +1,160 @@
use std::cmp::Ordering; use std::{cmp::Ordering, ops::Range};
use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers}; use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers};
/// This contains the information about the text that is used by the cursor to handle navigation.
pub trait Text {
/// Returns the line at the given index.
fn line(&self, number: usize) -> Option<&Self>;
/// Returns the length of the text in characters.
fn length(&self) -> usize;
/// Returns the number of lines in the text.
fn line_count(&self) -> usize;
/// Returns the character at the given character index.
fn character(&self, idx: usize) -> Option<char>;
/// Returns the length of the text before the given line in characters.
fn len_before_line(&self, line: usize) -> usize;
}
impl Text for str {
fn line(&self, number: usize) -> Option<&str> {
self.lines().nth(number)
}
fn length(&self) -> usize {
self.chars().count()
}
fn line_count(&self) -> usize {
self.lines().count()
}
fn character(&self, idx: usize) -> Option<char> {
self.chars().nth(idx)
}
fn len_before_line(&self, line: usize) -> usize {
self.lines()
.take(line)
.map(|l| l.chars().count())
.sum::<usize>()
}
}
/// This contains the information about the text that is used by the cursor to handle editing text.
pub trait TextEditable<T: Text + ?Sized>: AsRef<T> {
/// Inserts a character at the given character index.
fn insert_character(&mut self, idx: usize, text: char);
/// Deletes the given character range.
fn delete_range(&mut self, range: Range<usize>);
}
impl TextEditable<str> for String {
fn insert_character(&mut self, idx: usize, text: char) {
self.insert(idx, text);
}
fn delete_range(&mut self, range: Range<usize>) {
self.replace_range(range, "");
}
}
/// A cursor position
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pos { pub struct Pos {
/// The virtual column of the cursor. This can be more than the line length. To get the realized column, use [`Pos::col()`].
pub col: usize, pub col: usize,
/// The row of the cursor.
pub row: usize, pub row: usize,
} }
impl Pos { impl Pos {
/// Creates a new cursor position.
pub fn new(col: usize, row: usize) -> Self { pub fn new(col: usize, row: usize) -> Self {
Self { row, col } Self { row, col }
} }
pub fn up(&mut self, rope: &str) { /// Moves the position up by one line.
self.move_row(-1, rope); pub fn up(&mut self, text: &(impl Text + ?Sized)) {
self.move_row(-1, text);
} }
pub fn down(&mut self, rope: &str) { /// Moves the position down by one line.
self.move_row(1, rope); pub fn down(&mut self, text: &(impl Text + ?Sized)) {
self.move_row(1, text);
} }
pub fn right(&mut self, rope: &str) { /// Moves the position right by one character.
self.move_col(1, rope); pub fn right(&mut self, text: &(impl Text + ?Sized)) {
self.move_col(1, text);
} }
pub fn left(&mut self, rope: &str) { /// Moves the position left by one character.
self.move_col(-1, rope); pub fn left(&mut self, text: &(impl Text + ?Sized)) {
self.move_col(-1, text);
} }
pub fn move_row(&mut self, change: i32, rope: &str) { /// Move the position's row by the given amount. (positive is down, negative is up)
pub fn move_row(&mut self, change: i32, text: &(impl Text + ?Sized)) {
let new = self.row as i32 + change; let new = self.row as i32 + change;
if new >= 0 && new < rope.lines().count() as i32 { if new >= 0 && new < text.line_count() as i32 {
self.row = new as usize; self.row = new as usize;
} }
} }
pub fn move_col(&mut self, change: i32, rope: &str) { /// Move the position's column by the given amount. (positive is right, negative is left)
self.realize_col(rope); pub fn move_col(&mut self, change: i32, text: &(impl Text + ?Sized)) {
let idx = self.idx(rope) as i32; self.realize_col(text);
if idx + change >= 0 && idx + change <= rope.len() as i32 { let idx = self.idx(text) as i32;
let len_line = self.len_line(rope) as i32; if idx + change >= 0 && idx + change <= text.length() as i32 {
let len_line = self.len_line(text) as i32;
let new_col = self.col as i32 + change; let new_col = self.col as i32 + change;
let diff = new_col - len_line; let diff = new_col - len_line;
if diff > 0 { if diff > 0 {
self.down(rope); self.down(text);
self.col = 0; self.col = 0;
self.move_col(diff - 1, rope); self.move_col(diff - 1, text);
} else if new_col < 0 { } else if new_col < 0 {
self.up(rope); self.up(text);
self.col = self.len_line(rope); self.col = self.len_line(text);
self.move_col(new_col + 1, rope); self.move_col(new_col + 1, text);
} else { } else {
self.col = new_col as usize; self.col = new_col as usize;
} }
} }
} }
pub fn col(&self, rope: &str) -> usize { /// Get the realized column of the position. This is the column, but capped at the line length.
self.col.min(self.len_line(rope)) pub fn col(&self, text: &(impl Text + ?Sized)) -> usize {
self.col.min(self.len_line(text))
} }
/// Get the row of the position.
pub fn row(&self) -> usize { pub fn row(&self) -> usize {
self.row self.row
} }
fn len_line(&self, rope: &str) -> usize { fn len_line(&self, text: &(impl Text + ?Sized)) -> usize {
let line = rope.lines().nth(self.row).unwrap_or_default(); if let Some(line) = text.line(self.row) {
let len = line.len(); let len = line.length();
if len > 0 && line.chars().nth(len - 1) == Some('\n') { if len > 0 && line.character(len - 1) == Some('\n') {
len - 1 len - 1
} else {
len
}
} else { } else {
len 0
} }
} }
pub fn idx(&self, rope: &str) -> usize { /// Get the character index of the position.
rope.lines().take(self.row).map(|l| l.len()).sum::<usize>() + self.col(rope) pub fn idx(&self, text: &(impl Text + ?Sized)) -> usize {
text.len_before_line(self.row) + self.col(text)
} }
// the column can be more than the line length, cap it /// If the column is more than the line length, cap it to the line length.
pub fn realize_col(&mut self, rope: &str) { pub fn realize_col(&mut self, text: &(impl Text + ?Sized)) {
self.col = self.col(rope); self.col = self.col(text);
} }
} }
@ -97,13 +170,17 @@ impl PartialOrd for Pos {
} }
} }
/// A cursor is a selection of text. It has a start and end position of the selection.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cursor { pub struct Cursor {
/// The start position of the selection. The start position is the origin of the selection, not necessarily the first position.
pub start: Pos, pub start: Pos,
/// The end position of the selection. If the end position is None, the cursor is a caret.
pub end: Option<Pos>, pub end: Option<Pos>,
} }
impl Cursor { impl Cursor {
/// Create a new cursor with the given start position.
pub fn from_start(pos: Pos) -> Self { pub fn from_start(pos: Pos) -> Self {
Self { Self {
start: pos, start: pos,
@ -111,6 +188,7 @@ impl Cursor {
} }
} }
/// Create a new cursor with the given start and end position.
pub fn new(start: Pos, end: Pos) -> Self { pub fn new(start: Pos, end: Pos) -> Self {
Self { Self {
start, start,
@ -118,7 +196,8 @@ impl Cursor {
} }
} }
fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) { /// Move the cursor position. If shift is true, the end position will be moved instead of the start position.
pub fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) {
if shift { if shift {
self.with_end(f); self.with_end(f);
} else { } else {
@ -127,38 +206,36 @@ impl Cursor {
} }
} }
fn delete_selection(&mut self, text: &mut String) -> [i32; 2] { /// Delete the currently selected text and update the cursor position.
pub fn delete_selection<T: Text + ?Sized>(&mut self, text: &mut impl TextEditable<T>) {
let first = self.first(); let first = self.first();
let last = self.last(); let last = self.last();
let dr = first.row as i32 - last.row as i32; text.delete_range(first.idx(text.as_ref())..last.idx(text.as_ref()));
let dc = if dr != 0 {
-(last.col as i32)
} else {
first.col as i32 - last.col as i32
};
text.replace_range(first.idx(text)..last.idx(text), "");
if let Some(end) = self.end.take() { if let Some(end) = self.end.take() {
if self.start > end { if self.start > end {
self.start = end; self.start = end;
} }
} }
[dc, dr]
} }
pub fn handle_input( /// Handle moving the cursor with the given key.
pub fn handle_input<T: Text + ?Sized>(
&mut self, &mut self,
data: &dioxus_html::KeyboardData, data: &dioxus_html::KeyboardData,
text: &mut String, text: &mut impl TextEditable<T>,
max_width: usize, max_text_length: usize,
) { ) {
use Code::*; use Code::*;
match data.code() { match data.code() {
ArrowUp => { ArrowUp => {
self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT)); self.move_cursor(
|c| c.up(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT),
);
} }
ArrowDown => { ArrowDown => {
self.move_cursor( self.move_cursor(
|c| c.down(text), |c| c.down(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );
} }
@ -167,22 +244,22 @@ impl Cursor {
self.move_cursor( self.move_cursor(
|c| { |c| {
let mut change = 1; let mut change = 1;
let idx = c.idx(text); let idx = c.idx(text.as_ref());
let length = text.len(); let length = text.as_ref().length();
while idx + change < length { while idx + change < length {
let chr = text.chars().nth(idx + change).unwrap(); let chr = text.as_ref().character(idx + change).unwrap();
if chr.is_whitespace() { if chr.is_whitespace() {
break; break;
} }
change += 1; change += 1;
} }
c.move_col(change as i32, text); c.move_col(change as i32, text.as_ref());
}, },
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );
} else { } else {
self.move_cursor( self.move_cursor(
|c| c.right(text), |c| c.right(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );
} }
@ -192,28 +269,28 @@ impl Cursor {
self.move_cursor( self.move_cursor(
|c| { |c| {
let mut change = -1; let mut change = -1;
let idx = c.idx(text) as i32; let idx = c.idx(text.as_ref()) as i32;
while idx + change > 0 { while idx + change > 0 {
let chr = text.chars().nth((idx + change) as usize).unwrap(); let chr = text.as_ref().character((idx + change) as usize).unwrap();
if chr == ' ' { if chr == ' ' {
break; break;
} }
change -= 1; change -= 1;
} }
c.move_col(change, text); c.move_col(change, text.as_ref());
}, },
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );
} else { } else {
self.move_cursor( self.move_cursor(
|c| c.left(text), |c| c.left(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );
} }
} }
End => { End => {
self.move_cursor( self.move_cursor(
|c| c.col = c.len_line(text), |c| c.col = c.len_line(text.as_ref()),
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );
} }
@ -221,64 +298,70 @@ impl Cursor {
self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT)); self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT));
} }
Backspace => { Backspace => {
self.start.realize_col(text); self.start.realize_col(text.as_ref());
let mut start_idx = self.start.idx(text); let mut start_idx = self.start.idx(text.as_ref());
if self.end.is_some() { if self.end.is_some() {
self.delete_selection(text); self.delete_selection(text);
} else if start_idx > 0 { } else if start_idx > 0 {
self.start.left(text); self.start.left(text.as_ref());
text.replace_range(start_idx - 1..start_idx, ""); text.delete_range(start_idx - 1..start_idx);
if data.modifiers().contains(Modifiers::CONTROL) { if data.modifiers().contains(Modifiers::CONTROL) {
start_idx = self.start.idx(text); start_idx = self.start.idx(text.as_ref());
while start_idx > 0 while start_idx > 0
&& text && text
.chars() .as_ref()
.nth(start_idx - 1) .character(start_idx - 1)
.filter(|c| *c != ' ') .filter(|c| *c != ' ')
.is_some() .is_some()
{ {
self.start.left(text); self.start.left(text.as_ref());
text.replace_range(start_idx - 1..start_idx, ""); text.delete_range(start_idx - 1..start_idx);
start_idx = self.start.idx(text); start_idx = self.start.idx(text.as_ref());
} }
} }
} }
} }
Enter => { Enter => {
if text.len() + 1 - self.selection_len(text) <= max_width { if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length
text.insert(self.start.idx(text), '\n'); {
text.insert_character(self.start.idx(text.as_ref()), '\n');
self.start.col = 0; self.start.col = 0;
self.start.down(text); self.start.down(text.as_ref());
} }
} }
Tab => { Tab => {
if text.len() + 1 - self.selection_len(text) <= max_width { if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length
self.start.realize_col(text); {
self.start.realize_col(text.as_ref());
self.delete_selection(text); self.delete_selection(text);
text.insert(self.start.idx(text), '\t'); text.insert_character(self.start.idx(text.as_ref()), '\t');
self.start.right(text); self.start.right(text.as_ref());
} }
} }
_ => { _ => {
self.start.realize_col(text); self.start.realize_col(text.as_ref());
if let Key::Character(character) = data.key() { if let Key::Character(character) = data.key() {
if text.len() + 1 - self.selection_len(text) <= max_width { if text.as_ref().length() + 1 - self.selection_len(text.as_ref())
<= max_text_length
{
self.delete_selection(text); self.delete_selection(text);
let character = character.chars().next().unwrap(); let character = character.chars().next().unwrap();
text.insert(self.start.idx(text), character); text.insert_character(self.start.idx(text.as_ref()), character);
self.start.right(text); self.start.right(text.as_ref());
} }
} }
} }
} }
} }
/// Modify the end selection position
pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) { pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) {
let mut new = self.end.take().unwrap_or_else(|| self.start.clone()); let mut new = self.end.take().unwrap_or_else(|| self.start.clone());
f(&mut new); f(&mut new);
self.end.replace(new); self.end.replace(new);
} }
/// Returns first position of the selection (this could be the start or the end depending on the position)
pub fn first(&self) -> &Pos { pub fn first(&self) -> &Pos {
if let Some(e) = &self.end { if let Some(e) = &self.end {
e.min(&self.start) e.min(&self.start)
@ -287,6 +370,7 @@ impl Cursor {
} }
} }
/// Returns last position of the selection (this could be the start or the end depending on the position)
pub fn last(&self) -> &Pos { pub fn last(&self) -> &Pos {
if let Some(e) = &self.end { if let Some(e) = &self.end {
e.max(&self.start) e.max(&self.start)
@ -295,7 +379,8 @@ impl Cursor {
} }
} }
pub fn selection_len(&self, text: &str) -> usize { /// Returns the length of the selection
pub fn selection_len(&self, text: &(impl Text + ?Sized)) -> usize {
self.last().idx(text) - self.first().idx(text) self.last().idx(text) - self.first().idx(text)
} }
} }

View file

@ -40,8 +40,8 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
let dragging = use_state(cx, || false); let dragging = use_state(cx, || false);
let text = text_ref.read().clone(); let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&text); let start_highlight = cursor.read().first().idx(&*text);
let end_highlight = cursor.read().last().idx(&text); let end_highlight = cursor.read().last().idx(&*text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) = let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight); text_after_first_cursor.split_at(end_highlight - start_highlight);
@ -113,7 +113,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
}; };
if is_text{ if is_text{
let mut text = text_ref.write(); let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut text, max_len); cursor.write().handle_input(&k, &mut *text, max_len);
update(text.clone()); update(text.clone());
let node = tui_query.get(get_root_id(cx).unwrap()); let node = tui_query.get(get_root_id(cx).unwrap());
@ -165,7 +165,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
} }
new.row = 0; new.row = 0;
new.realize_col(&text_ref.read()); new.realize_col(text_ref.read().as_str());
cursor.set(Cursor::from_start(new)); cursor.set(Cursor::from_start(new));
dragging.set(true); dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap()); let node = tui_query_clone.get(get_root_id(cx).unwrap());

View file

@ -40,8 +40,8 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
let dragging = use_state(cx, || false); let dragging = use_state(cx, || false);
let text = text_ref.read().clone(); let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&text); let start_highlight = cursor.read().first().idx(&*text);
let end_highlight = cursor.read().last().idx(&text); let end_highlight = cursor.read().last().idx(&*text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) = let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight); text_after_first_cursor.split_at(end_highlight - start_highlight);
@ -88,7 +88,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
return; return;
} }
let mut text = text_ref.write(); let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut text, max_len); cursor.write().handle_input(&k, &mut *text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput { if let Some(input_handler) = &cx.props.raw_oninput {
input_handler.call(FormData { input_handler.call(FormData {
value: text.clone(), value: text.clone(),
@ -147,7 +147,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
// textboxs are only one line tall // textboxs are only one line tall
new.row = 0; new.row = 0;
new.realize_col(&text_ref.read()); new.realize_col(text_ref.read().as_str());
cursor.set(Cursor::from_start(new)); cursor.set(Cursor::from_start(new));
dragging.set(true); dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap()); let node = tui_query_clone.get(get_root_id(cx).unwrap());

View file

@ -40,8 +40,8 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
let dragging = use_state(cx, || false); let dragging = use_state(cx, || false);
let text = text_ref.read().clone(); let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&text); let start_highlight = cursor.read().first().idx(&*text);
let end_highlight = cursor.read().last().idx(&text); let end_highlight = cursor.read().last().idx(&*text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) = let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight); text_after_first_cursor.split_at(end_highlight - start_highlight);
@ -90,7 +90,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
return; return;
} }
let mut text = text_ref.write(); let mut text = text_ref.write();
cursor.write().handle_input(&k, &mut text, max_len); cursor.write().handle_input(&k, &mut *text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput{ if let Some(input_handler) = &cx.props.raw_oninput{
input_handler.call(FormData{ input_handler.call(FormData{
value: text.clone(), value: text.clone(),
@ -138,7 +138,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
// textboxs are only one line tall // textboxs are only one line tall
new.row = 0; new.row = 0;
new.realize_col(&text_ref.read()); new.realize_col(text_ref.read().as_str());
cursor.set(Cursor::from_start(new)); cursor.set(Cursor::from_start(new));
dragging.set(true); dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap()); let node = tui_query_clone.get(get_root_id(cx).unwrap());