Move cursor in message editor vertically
This commit is contained in:
parent
8b66de44e0
commit
c4d3f5ba4d
5 changed files with 170 additions and 38 deletions
|
|
@ -191,12 +191,14 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Enter,
|
code: KeyCode::Enter,
|
||||||
..
|
..
|
||||||
} => self.editor.insert_char('\n'),
|
} => self.editor.insert_char(terminal.frame(), '\n'),
|
||||||
|
|
||||||
key!(Char ch) => self.editor.insert_char(ch),
|
key!(Char ch) => self.editor.insert_char(terminal.frame(), ch),
|
||||||
key!(Left) => self.editor.move_cursor_left(),
|
key!(Left) => self.editor.move_cursor_left(terminal.frame()),
|
||||||
key!(Right) => self.editor.move_cursor_right(),
|
key!(Right) => self.editor.move_cursor_right(terminal.frame()),
|
||||||
key!(Backspace) => self.editor.backspace(),
|
key!(Up) => self.editor.move_cursor_up(terminal.frame()),
|
||||||
|
key!(Down) => self.editor.move_cursor_down(terminal.frame()),
|
||||||
|
key!(Backspace) => self.editor.backspace(terminal.frame()),
|
||||||
key!(Delete) => self.editor.delete(),
|
key!(Delete) => self.editor.delete(),
|
||||||
key!(Ctrl + 'e') => self.editor.edit_externally(terminal, crossterm_lock),
|
key!(Ctrl + 'e') => self.editor.edit_externally(terminal, crossterm_lock),
|
||||||
key!(Ctrl + 'l') => self.editor.clear(),
|
key!(Ctrl + 'l') => self.editor.clear(),
|
||||||
|
|
|
||||||
|
|
@ -385,10 +385,10 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
self.state = State::Normal;
|
self.state = State::Normal;
|
||||||
}
|
}
|
||||||
key!(Char ch) => ed.insert_char(ch),
|
key!(Char ch) => ed.insert_char(terminal.frame(), ch),
|
||||||
key!(Backspace) => ed.backspace(),
|
key!(Backspace) => ed.backspace(terminal.frame()),
|
||||||
key!(Left) => ed.move_cursor_left(),
|
key!(Left) => ed.move_cursor_left(terminal.frame()),
|
||||||
key!(Right) => ed.move_cursor_right(),
|
key!(Right) => ed.move_cursor_right(terminal.frame()),
|
||||||
key!(Delete) => ed.delete(),
|
key!(Delete) => ed.delete(),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -311,10 +311,12 @@ impl Rooms {
|
||||||
self.state = State::ShowRoom(name);
|
self.state = State::ShowRoom(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
key!(Char ch) if ch.is_ascii_alphanumeric() || ch == '_' => ed.insert_char(ch),
|
key!(Char ch) if ch.is_ascii_alphanumeric() || ch == '_' => {
|
||||||
key!(Left) => ed.move_cursor_left(),
|
ed.insert_char(terminal.frame(), ch)
|
||||||
key!(Right) => ed.move_cursor_right(),
|
}
|
||||||
key!(Backspace) => ed.backspace(),
|
key!(Left) => ed.move_cursor_left(terminal.frame()),
|
||||||
|
key!(Right) => ed.move_cursor_right(terminal.frame()),
|
||||||
|
key!(Backspace) => ed.backspace(terminal.frame()),
|
||||||
key!(Delete) => ed.delete(),
|
key!(Delete) => ed.delete(),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// Since the widget module is effectively a library and will probably be moved
|
// Since the widget module is effectively a library and will probably be moved
|
||||||
// to toss later, warnings about unused functions are mostly inaccurate.
|
// to toss later, warnings about unused functions are mostly inaccurate.
|
||||||
|
// TODO Restrict this a bit more?
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub mod background;
|
pub mod background;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ struct InnerEditorState {
|
||||||
/// Must point to a valid grapheme boundary.
|
/// Must point to a valid grapheme boundary.
|
||||||
idx: usize,
|
idx: usize,
|
||||||
|
|
||||||
|
/// Column of the cursor on the screen just after it was last moved
|
||||||
|
/// horizontally.
|
||||||
|
col: usize,
|
||||||
|
|
||||||
/// Width of the text when the editor was last rendered.
|
/// Width of the text when the editor was last rendered.
|
||||||
///
|
///
|
||||||
/// Does not include additional column for cursor.
|
/// Does not include additional column for cursor.
|
||||||
|
|
@ -44,11 +48,16 @@ impl InnerEditorState {
|
||||||
fn new(text: String) -> Self {
|
fn new(text: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
idx: text.len(),
|
idx: text.len(),
|
||||||
|
col: 0,
|
||||||
last_width: 0,
|
last_width: 0,
|
||||||
text,
|
text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// Grapheme helper functions //
|
||||||
|
///////////////////////////////
|
||||||
|
|
||||||
fn grapheme_boundaries(&self) -> Vec<usize> {
|
fn grapheme_boundaries(&self) -> Vec<usize> {
|
||||||
self.text
|
self.text
|
||||||
.grapheme_indices(true)
|
.grapheme_indices(true)
|
||||||
|
|
@ -57,9 +66,10 @@ impl InnerEditorState {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the cursor index lies on a grapheme boundary.
|
/// Ensure the cursor index lies on a grapheme boundary. If it doesn't, it
|
||||||
|
/// is moved to the next grapheme boundary.
|
||||||
///
|
///
|
||||||
/// If it doesn't, it is moved to the next grapheme boundary.
|
/// Can handle arbitrary cursor index.
|
||||||
fn move_cursor_to_grapheme_boundary(&mut self) {
|
fn move_cursor_to_grapheme_boundary(&mut self) {
|
||||||
for i in self.grapheme_boundaries() {
|
for i in self.grapheme_boundaries() {
|
||||||
#[allow(clippy::comparison_chain)]
|
#[allow(clippy::comparison_chain)]
|
||||||
|
|
@ -74,32 +84,114 @@ impl InnerEditorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This loop should always return since the index behind the last
|
// The cursor was out of bounds, so move it to the last valid index.
|
||||||
// grapheme is included in the grapheme boundary iterator.
|
self.idx = self.text.len();
|
||||||
panic!("cursor index out of bounds");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_text(&mut self, text: String) {
|
///////////////////////////////
|
||||||
|
// Line/col helper functions //
|
||||||
|
///////////////////////////////
|
||||||
|
|
||||||
|
/// Like [`Self::grapheme_boundaries`] but for lines.
|
||||||
|
///
|
||||||
|
/// Note that the last line can have a length of 0 if the text ends with a
|
||||||
|
/// newline.
|
||||||
|
fn line_boundaries(&self) -> Vec<usize> {
|
||||||
|
let newlines = self
|
||||||
|
.text
|
||||||
|
.char_indices()
|
||||||
|
.filter(|(_, c)| *c == '\n')
|
||||||
|
.map(|(i, _)| i + 1); // utf-8 encodes '\n' as a single byte
|
||||||
|
iter::once(0)
|
||||||
|
.chain(newlines)
|
||||||
|
.chain(iter::once(self.text.len()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the cursor's current line.
|
||||||
|
///
|
||||||
|
/// Returns `(line_nr, start_idx, end_idx)`.
|
||||||
|
fn cursor_line(&self, boundaries: &[usize]) -> (usize, usize, usize) {
|
||||||
|
let mut result = (0, 0, 0);
|
||||||
|
for (i, (start, end)) in boundaries.iter().zip(boundaries.iter().skip(1)).enumerate() {
|
||||||
|
if self.idx >= *start {
|
||||||
|
result = (i, *start, *end);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_col(&self, frame: &mut Frame, line_start: usize) -> usize {
|
||||||
|
frame.width(&self.text[line_start..self.idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line(&self, line: usize) -> (usize, usize) {
|
||||||
|
let boundaries = self.line_boundaries();
|
||||||
|
boundaries
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(boundaries.iter().copied().skip(1))
|
||||||
|
.nth(line)
|
||||||
|
.expect("line exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_to_line_col(&mut self, frame: &mut Frame, line: usize, col: usize) {
|
||||||
|
let (start, end) = self.line(line);
|
||||||
|
let line = &self.text[start..end];
|
||||||
|
|
||||||
|
self.idx = start;
|
||||||
|
let mut width = 0;
|
||||||
|
for (gi, g) in line.grapheme_indices(true) {
|
||||||
|
self.idx = start + gi;
|
||||||
|
if col > width {
|
||||||
|
width += frame.grapheme_width(g, width) as usize;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_cursor_col(&mut self, frame: &mut Frame) {
|
||||||
|
let boundaries = self.line_boundaries();
|
||||||
|
let (_, start, _) = self.cursor_line(&boundaries);
|
||||||
|
self.col = self.cursor_col(frame, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////
|
||||||
|
// Editing //
|
||||||
|
/////////////
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.text = String::new();
|
||||||
|
self.idx = 0;
|
||||||
|
self.col = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_text(&mut self, frame: &mut Frame, text: String) {
|
||||||
self.text = text;
|
self.text = text;
|
||||||
self.idx = self.idx.min(self.text.len());
|
|
||||||
self.move_cursor_to_grapheme_boundary();
|
self.move_cursor_to_grapheme_boundary();
|
||||||
|
self.record_cursor_col(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a character at the current cursor position and move the cursor
|
/// Insert a character at the current cursor position and move the cursor
|
||||||
/// accordingly.
|
/// accordingly.
|
||||||
fn insert_char(&mut self, ch: char) {
|
fn insert_char(&mut self, frame: &mut Frame, ch: char) {
|
||||||
self.text.insert(self.idx, ch);
|
self.text.insert(self.idx, ch);
|
||||||
self.idx += 1;
|
self.idx += 1;
|
||||||
self.move_cursor_to_grapheme_boundary();
|
self.move_cursor_to_grapheme_boundary();
|
||||||
|
self.record_cursor_col(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the grapheme before the cursor position.
|
/// Delete the grapheme before the cursor position.
|
||||||
fn backspace(&mut self) {
|
fn backspace(&mut self, frame: &mut Frame) {
|
||||||
let boundaries = self.grapheme_boundaries();
|
let boundaries = self.grapheme_boundaries();
|
||||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||||
if *end == self.idx {
|
if *end == self.idx {
|
||||||
self.text.replace_range(start..end, "");
|
self.text.replace_range(start..end, "");
|
||||||
self.idx = *start;
|
self.idx = *start;
|
||||||
|
self.record_cursor_col(frame);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,25 +208,52 @@ impl InnerEditorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_cursor_left(&mut self) {
|
/////////////////////
|
||||||
|
// Cursor movement //
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
fn move_cursor_left(&mut self, frame: &mut Frame) {
|
||||||
let boundaries = self.grapheme_boundaries();
|
let boundaries = self.grapheme_boundaries();
|
||||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||||
if *end == self.idx {
|
if *end == self.idx {
|
||||||
self.idx = *start;
|
self.idx = *start;
|
||||||
|
self.record_cursor_col(frame);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_cursor_right(&mut self) {
|
fn move_cursor_right(&mut self, frame: &mut Frame) {
|
||||||
let boundaries = self.grapheme_boundaries();
|
let boundaries = self.grapheme_boundaries();
|
||||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||||
if *start == self.idx {
|
if *start == self.idx {
|
||||||
self.idx = *end;
|
self.idx = *end;
|
||||||
|
self.record_cursor_col(frame);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_cursor_up(&mut self, frame: &mut Frame) {
|
||||||
|
let boundaries = self.line_boundaries();
|
||||||
|
let (line, _, _) = self.cursor_line(&boundaries);
|
||||||
|
if line > 0 {
|
||||||
|
self.move_cursor_to_line_col(frame, line - 1, self.col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_down(&mut self, frame: &mut Frame) {
|
||||||
|
let boundaries = self.line_boundaries();
|
||||||
|
|
||||||
|
// There's always at least one line, and always at least two line
|
||||||
|
// boundaries at 0 and self.text.len().
|
||||||
|
let amount_of_lines = boundaries.len() - 1;
|
||||||
|
|
||||||
|
let (line, _, _) = self.cursor_line(&boundaries);
|
||||||
|
if line + 1 < amount_of_lines {
|
||||||
|
self.move_cursor_to_line_col(frame, line + 1, self.col);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorState(Arc<Mutex<InnerEditorState>>);
|
pub struct EditorState(Arc<Mutex<InnerEditorState>>);
|
||||||
|
|
@ -163,21 +282,21 @@ impl EditorState {
|
||||||
self.0.lock().text.clone()
|
self.0.lock().text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_text(&self, text: String) {
|
|
||||||
self.0.lock().set_text(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
self.set_text(String::new());
|
self.0.lock().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_char(&self, ch: char) {
|
pub fn set_text(&self, frame: &mut Frame, text: String) {
|
||||||
self.0.lock().insert_char(ch);
|
self.0.lock().set_text(frame, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_char(&self, frame: &mut Frame, ch: char) {
|
||||||
|
self.0.lock().insert_char(frame, ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the grapheme before the cursor position.
|
/// Delete the grapheme before the cursor position.
|
||||||
pub fn backspace(&self) {
|
pub fn backspace(&self, frame: &mut Frame) {
|
||||||
self.0.lock().backspace();
|
self.0.lock().backspace(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the grapheme after the cursor position.
|
/// Delete the grapheme after the cursor position.
|
||||||
|
|
@ -185,18 +304,26 @@ impl EditorState {
|
||||||
self.0.lock().delete();
|
self.0.lock().delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor_left(&self) {
|
pub fn move_cursor_left(&self, frame: &mut Frame) {
|
||||||
self.0.lock().move_cursor_left();
|
self.0.lock().move_cursor_left(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor_right(&self) {
|
pub fn move_cursor_right(&self, frame: &mut Frame) {
|
||||||
self.0.lock().move_cursor_right();
|
self.0.lock().move_cursor_right(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_cursor_up(&self, frame: &mut Frame) {
|
||||||
|
self.0.lock().move_cursor_up(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_cursor_down(&self, frame: &mut Frame) {
|
||||||
|
self.0.lock().move_cursor_down(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edit_externally(&self, terminal: &mut Terminal, crossterm_lock: &Arc<FairMutex<()>>) {
|
pub fn edit_externally(&self, terminal: &mut Terminal, crossterm_lock: &Arc<FairMutex<()>>) {
|
||||||
let mut guard = self.0.lock();
|
let mut guard = self.0.lock();
|
||||||
if let Some(text) = util::prompt(terminal, crossterm_lock, &guard.text) {
|
if let Some(text) = util::prompt(terminal, crossterm_lock, &guard.text) {
|
||||||
guard.set_text(text);
|
guard.set_text(terminal.frame(), text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue