diff --git a/packages/native-core/src/utils/cursor.rs b/packages/native-core/src/utils/cursor.rs index c042d4cbf..eaded383e 100644 --- a/packages/native-core/src/utils/cursor.rs +++ b/packages/native-core/src/utils/cursor.rs @@ -1,87 +1,160 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, ops::Range}; 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; + /// 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 { + self.chars().nth(idx) + } + + fn len_before_line(&self, line: usize) -> usize { + self.lines() + .take(line) + .map(|l| l.chars().count()) + .sum::() + } +} + +/// This contains the information about the text that is used by the cursor to handle editing text. +pub trait TextEditable: AsRef { + /// 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); +} + +impl TextEditable for String { + fn insert_character(&mut self, idx: usize, text: char) { + self.insert(idx, text); + } + + fn delete_range(&mut self, range: Range) { + self.replace_range(range, ""); + } +} + +/// A cursor position #[derive(Debug, Clone, PartialEq, Eq)] 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, + /// The row of the cursor. pub row: usize, } impl Pos { + /// Creates a new cursor position. pub fn new(col: usize, row: usize) -> Self { Self { row, col } } - pub fn up(&mut self, rope: &str) { - self.move_row(-1, rope); + /// Moves the position up by one line. + pub fn up(&mut self, text: &(impl Text + ?Sized)) { + self.move_row(-1, text); } - pub fn down(&mut self, rope: &str) { - self.move_row(1, rope); + /// Moves the position down by one line. + pub fn down(&mut self, text: &(impl Text + ?Sized)) { + self.move_row(1, text); } - pub fn right(&mut self, rope: &str) { - self.move_col(1, rope); + /// Moves the position right by one character. + pub fn right(&mut self, text: &(impl Text + ?Sized)) { + self.move_col(1, text); } - pub fn left(&mut self, rope: &str) { - self.move_col(-1, rope); + /// Moves the position left by one character. + 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; - if new >= 0 && new < rope.lines().count() as i32 { + if new >= 0 && new < text.line_count() as i32 { self.row = new as usize; } } - pub fn move_col(&mut self, change: i32, rope: &str) { - self.realize_col(rope); - let idx = self.idx(rope) as i32; - if idx + change >= 0 && idx + change <= rope.len() as i32 { - let len_line = self.len_line(rope) as i32; + /// Move the position's column by the given amount. (positive is right, negative is left) + pub fn move_col(&mut self, change: i32, text: &(impl Text + ?Sized)) { + self.realize_col(text); + let idx = self.idx(text) 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 diff = new_col - len_line; if diff > 0 { - self.down(rope); + self.down(text); self.col = 0; - self.move_col(diff - 1, rope); + self.move_col(diff - 1, text); } else if new_col < 0 { - self.up(rope); - self.col = self.len_line(rope); - self.move_col(new_col + 1, rope); + self.up(text); + self.col = self.len_line(text); + self.move_col(new_col + 1, text); } else { self.col = new_col as usize; } } } - pub fn col(&self, rope: &str) -> usize { - self.col.min(self.len_line(rope)) + /// Get the realized column of the position. This is the column, but capped at the line length. + 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 { self.row } - fn len_line(&self, rope: &str) -> usize { - let line = rope.lines().nth(self.row).unwrap_or_default(); - let len = line.len(); - if len > 0 && line.chars().nth(len - 1) == Some('\n') { - len - 1 + fn len_line(&self, text: &(impl Text + ?Sized)) -> usize { + if let Some(line) = text.line(self.row) { + let len = line.length(); + if len > 0 && line.character(len - 1) == Some('\n') { + len - 1 + } else { + len + } } else { - len + 0 } } - pub fn idx(&self, rope: &str) -> usize { - rope.lines().take(self.row).map(|l| l.len()).sum::() + self.col(rope) + /// Get the character index of the position. + 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 - pub fn realize_col(&mut self, rope: &str) { - self.col = self.col(rope); + /// If the column is more than the line length, cap it to the line length. + pub fn realize_col(&mut self, text: &(impl Text + ?Sized)) { + 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)] 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, + /// The end position of the selection. If the end position is None, the cursor is a caret. pub end: Option, } impl Cursor { + /// Create a new cursor with the given start position. pub fn from_start(pos: Pos) -> Self { Self { 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 { Self { 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 { self.with_end(f); } 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(&mut self, text: &mut impl TextEditable) { let first = self.first(); let last = self.last(); - let dr = first.row as i32 - last.row as i32; - 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), ""); + text.delete_range(first.idx(text.as_ref())..last.idx(text.as_ref())); if let Some(end) = self.end.take() { if self.start > end { self.start = end; } } - [dc, dr] } - pub fn handle_input( + /// Handle moving the cursor with the given key. + pub fn handle_input( &mut self, data: &dioxus_html::KeyboardData, - text: &mut String, - max_width: usize, + text: &mut impl TextEditable, + max_text_length: usize, ) { use Code::*; match data.code() { 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 => { self.move_cursor( - |c| c.down(text), + |c| c.down(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } @@ -167,22 +244,22 @@ impl Cursor { self.move_cursor( |c| { let mut change = 1; - let idx = c.idx(text); - let length = text.len(); + let idx = c.idx(text.as_ref()); + let length = text.as_ref().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() { break; } change += 1; } - c.move_col(change as i32, text); + c.move_col(change as i32, text.as_ref()); }, data.modifiers().contains(Modifiers::SHIFT), ); } else { self.move_cursor( - |c| c.right(text), + |c| c.right(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } @@ -192,28 +269,28 @@ impl Cursor { self.move_cursor( |c| { let mut change = -1; - let idx = c.idx(text) as i32; + let idx = c.idx(text.as_ref()) as i32; 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 == ' ' { break; } change -= 1; } - c.move_col(change, text); + c.move_col(change, text.as_ref()); }, data.modifiers().contains(Modifiers::SHIFT), ); } else { self.move_cursor( - |c| c.left(text), + |c| c.left(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } } End => { self.move_cursor( - |c| c.col = c.len_line(text), + |c| c.col = c.len_line(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } @@ -221,64 +298,70 @@ impl Cursor { self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT)); } Backspace => { - self.start.realize_col(text); - let mut start_idx = self.start.idx(text); + self.start.realize_col(text.as_ref()); + let mut start_idx = self.start.idx(text.as_ref()); if self.end.is_some() { self.delete_selection(text); } else if start_idx > 0 { - self.start.left(text); - text.replace_range(start_idx - 1..start_idx, ""); + self.start.left(text.as_ref()); + text.delete_range(start_idx - 1..start_idx); if data.modifiers().contains(Modifiers::CONTROL) { - start_idx = self.start.idx(text); + start_idx = self.start.idx(text.as_ref()); while start_idx > 0 && text - .chars() - .nth(start_idx - 1) + .as_ref() + .character(start_idx - 1) .filter(|c| *c != ' ') .is_some() { - self.start.left(text); - text.replace_range(start_idx - 1..start_idx, ""); - start_idx = self.start.idx(text); + self.start.left(text.as_ref()); + text.delete_range(start_idx - 1..start_idx); + start_idx = self.start.idx(text.as_ref()); } } } } Enter => { - if text.len() + 1 - self.selection_len(text) <= max_width { - text.insert(self.start.idx(text), '\n'); + if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length + { + text.insert_character(self.start.idx(text.as_ref()), '\n'); self.start.col = 0; - self.start.down(text); + self.start.down(text.as_ref()); } } Tab => { - if text.len() + 1 - self.selection_len(text) <= max_width { - self.start.realize_col(text); + if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length + { + self.start.realize_col(text.as_ref()); self.delete_selection(text); - text.insert(self.start.idx(text), '\t'); - self.start.right(text); + text.insert_character(self.start.idx(text.as_ref()), '\t'); + 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 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); let character = character.chars().next().unwrap(); - text.insert(self.start.idx(text), character); - self.start.right(text); + text.insert_character(self.start.idx(text.as_ref()), character); + self.start.right(text.as_ref()); } } } } } + /// Modify the end selection position pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) { let mut new = self.end.take().unwrap_or_else(|| self.start.clone()); f(&mut 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 { if let Some(e) = &self.end { 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 { if let Some(e) = &self.end { 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) } } diff --git a/packages/tui/src/widgets/number.rs b/packages/tui/src/widgets/number.rs index 2df1f512b..f34618f60 100644 --- a/packages/tui/src/widgets/number.rs +++ b/packages/tui/src/widgets/number.rs @@ -40,8 +40,8 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().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_highlighted, text_after_second_cursor) = 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{ 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()); 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.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); diff --git a/packages/tui/src/widgets/password.rs b/packages/tui/src/widgets/password.rs index f82d0a346..6574ab4f9 100644 --- a/packages/tui/src/widgets/password.rs +++ b/packages/tui/src/widgets/password.rs @@ -40,8 +40,8 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().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_highlighted, text_after_second_cursor) = 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; } 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 { input_handler.call(FormData { value: text.clone(), @@ -147,7 +147,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { // textboxs are only one line tall new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); diff --git a/packages/tui/src/widgets/textbox.rs b/packages/tui/src/widgets/textbox.rs index 4a58725bc..5e3cc693a 100644 --- a/packages/tui/src/widgets/textbox.rs +++ b/packages/tui/src/widgets/textbox.rs @@ -40,8 +40,8 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().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_highlighted, text_after_second_cursor) = 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; } 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{ input_handler.call(FormData{ value: text.clone(), @@ -138,7 +138,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { // textboxs are only one line tall new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap());