use std::iter; use crossterm::style::Stylize; use unicode_segmentation::UnicodeSegmentation; use crate::{Frame, Pos, Size, Style, Styled, Widget, WidthDb}; /// Like [`WidthDb::wrap`] but includes a final break index if the text ends /// with a newline. fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec { let mut breaks = widthdb.wrap(text, width); if text.ends_with('\n') { breaks.push(text.len()) } breaks } /////////// // State // /////////// #[derive(Debug, Clone)] pub struct EditorState { text: String, /// Index of the cursor in the text. /// /// Must point to a valid grapheme boundary. cursor_idx: usize, /// Column of the cursor on the screen just after it was last moved /// horizontally. cursor_col: usize, /// Position of the cursor when the editor was last rendered. last_cursor_pos: Pos, } impl EditorState { pub fn new() -> Self { Self::with_initial_text(String::new()) } pub fn with_initial_text(text: String) -> Self { Self { cursor_idx: text.len(), cursor_col: 0, last_cursor_pos: Pos::ZERO, text, } } /////////////////////////////// // Grapheme helper functions // /////////////////////////////// fn grapheme_boundaries(&self) -> Vec { self.text .grapheme_indices(true) .map(|(i, _)| i) .chain(iter::once(self.text.len())) .collect() } /// Ensure the cursor index lies on a 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) { for i in self.grapheme_boundaries() { #[allow(clippy::comparison_chain)] if i == self.cursor_idx { // We're at a valid grapheme boundary already return; } else if i > self.cursor_idx { // There was no valid grapheme boundary at our cursor index, so // we'll take the next one we can get. self.cursor_idx = i; return; } } // The cursor was out of bounds, so move it to the last valid index. self.cursor_idx = self.text.len(); } /////////////////////////////// // 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 { 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.cursor_idx >= *start { result = (i, *start, *end); } else { break; } } result } fn cursor_col(&self, widthdb: &mut WidthDb, line_start: usize) -> usize { widthdb.width(&self.text[line_start..self.cursor_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, widthdb: &mut WidthDb, line: usize, col: usize) { let (start, end) = self.line(line); let line = &self.text[start..end]; let mut width = 0; for (gi, g) in line.grapheme_indices(true) { self.cursor_idx = start + gi; if col > width { width += widthdb.grapheme_width(g, width) as usize; } else { return; } } if !line.ends_with('\n') { self.cursor_idx = end; } } fn record_cursor_col(&mut self, widthdb: &mut WidthDb) { let boundaries = self.line_boundaries(); let (_, start, _) = self.cursor_line(&boundaries); self.cursor_col = self.cursor_col(widthdb, start); } ///////////// // Editing // ///////////// pub fn text(&self) -> &str { &self.text } pub fn set_text(&mut self, widthdb: &mut WidthDb, text: String) { self.text = text; self.move_cursor_to_grapheme_boundary(); self.record_cursor_col(widthdb); } pub fn clear(&mut self) { self.text = String::new(); self.cursor_idx = 0; self.cursor_col = 0; } /// Insert a character at the current cursor position and move the cursor /// accordingly. pub fn insert_char(&mut self, widthdb: &mut WidthDb, ch: char) { self.text.insert(self.cursor_idx, ch); self.cursor_idx += ch.len_utf8(); self.record_cursor_col(widthdb); } /// Insert a string at the current cursor position and move the cursor /// accordingly. pub fn insert_str(&mut self, widthdb: &mut WidthDb, str: &str) { self.text.insert_str(self.cursor_idx, str); self.cursor_idx += str.len(); self.record_cursor_col(widthdb); } /// Delete the grapheme before the cursor position. pub fn backspace(&mut self, widthdb: &mut WidthDb) { let boundaries = self.grapheme_boundaries(); for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { if *end == self.cursor_idx { self.text.replace_range(start..end, ""); self.cursor_idx = *start; self.record_cursor_col(widthdb); break; } } } /// Delete the grapheme after the cursor position. pub fn delete(&mut self) { let boundaries = self.grapheme_boundaries(); for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { if *start == self.cursor_idx { self.text.replace_range(start..end, ""); break; } } } ///////////////////// // Cursor movement // ///////////////////// pub fn move_cursor_left(&mut self, widthdb: &mut WidthDb) { let boundaries = self.grapheme_boundaries(); for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { if *end == self.cursor_idx { self.cursor_idx = *start; self.record_cursor_col(widthdb); break; } } } pub fn move_cursor_right(&mut self, widthdb: &mut WidthDb) { let boundaries = self.grapheme_boundaries(); for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { if *start == self.cursor_idx { self.cursor_idx = *end; self.record_cursor_col(widthdb); break; } } } pub fn move_cursor_left_a_word(&mut self, widthdb: &mut WidthDb) { let boundaries = self.grapheme_boundaries(); let mut encountered_word = false; for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)).rev() { if *end == self.cursor_idx { let g = &self.text[*start..*end]; let whitespace = g.chars().all(|c| c.is_whitespace()); if encountered_word && whitespace { break; } else if !whitespace { encountered_word = true; } self.cursor_idx = *start; } } self.record_cursor_col(widthdb); } pub fn move_cursor_right_a_word(&mut self, widthdb: &mut WidthDb) { let boundaries = self.grapheme_boundaries(); let mut encountered_word = false; for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) { if *start == self.cursor_idx { let g = &self.text[*start..*end]; let whitespace = g.chars().all(|c| c.is_whitespace()); if encountered_word && whitespace { break; } else if !whitespace { encountered_word = true; } self.cursor_idx = *end; } } self.record_cursor_col(widthdb); } pub fn move_cursor_to_start_of_line(&mut self, widthdb: &mut WidthDb) { let boundaries = self.line_boundaries(); let (line, _, _) = self.cursor_line(&boundaries); self.move_cursor_to_line_col(widthdb, line, 0); self.record_cursor_col(widthdb); } pub fn move_cursor_to_end_of_line(&mut self, widthdb: &mut WidthDb) { let boundaries = self.line_boundaries(); let (line, _, _) = self.cursor_line(&boundaries); self.move_cursor_to_line_col(widthdb, line, usize::MAX); self.record_cursor_col(widthdb); } pub fn move_cursor_up(&mut self, widthdb: &mut WidthDb) { let boundaries = self.line_boundaries(); let (line, _, _) = self.cursor_line(&boundaries); if line > 0 { self.move_cursor_to_line_col(widthdb, line - 1, self.cursor_col); } } pub fn move_cursor_down(&mut self, widthdb: &mut WidthDb) { 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(widthdb, line + 1, self.cursor_col); } } pub fn last_cursor_pos(&self) -> Pos { self.last_cursor_pos } pub fn widget(&mut self) -> Editor<'_> { Editor { highlighted: Styled::new_plain(&self.text), hidden: None, focus: true, state: self, } } } impl Default for EditorState { fn default() -> Self { Self::new() } } //////////// // Widget // //////////// #[derive(Debug)] pub struct Editor<'a> { state: &'a mut EditorState, highlighted: Styled, pub hidden: Option, pub focus: bool, } impl Editor<'_> { pub fn state(&mut self) -> &mut EditorState { self.state } pub fn text(&self) -> &Styled { &self.highlighted } pub fn highlight(&mut self, highlight: F) where F: FnOnce(&str) -> Styled, { self.highlighted = highlight(&self.state.text); assert_eq!(self.state.text, self.highlighted.text()); } pub fn with_highlight(mut self, highlight: F) -> Self where F: FnOnce(&str) -> Styled, { self.highlight(highlight); self } pub fn with_visible(mut self) -> Self { self.hidden = None; self } pub fn with_hidden>(mut self, placeholder: S) -> Self { self.hidden = Some(placeholder.into()); self } pub fn with_hidden_default_placeholder(self) -> Self { self.with_hidden(("", Style::new().grey().italic())) } pub fn with_focus(mut self, active: bool) -> Self { self.focus = active; self } fn wrapped_cursor(cursor_idx: usize, break_indices: &[usize]) -> (usize, usize) { let mut row = 0; let mut line_idx = cursor_idx; for break_idx in break_indices { if cursor_idx < *break_idx { break; } else { row += 1; line_idx = cursor_idx - break_idx; } } (row, line_idx) } fn indices(&self, widthdb: &mut WidthDb, max_width: Option) -> Vec { let max_width = max_width // One extra column for cursor .map(|w| w.saturating_sub(1) as usize) .unwrap_or(usize::MAX); let text = self.hidden.as_ref().unwrap_or(&self.highlighted); wrap(widthdb, text.text(), max_width) } fn rows(&self, indices: &[usize]) -> Vec { let text = match self.hidden.as_ref() { Some(hidden) if !self.highlighted.text().is_empty() => hidden, _ => &self.highlighted, }; text.clone().split_at_indices(indices) } fn cursor(&self, widthdb: &mut WidthDb, width: u16, indices: &[usize], rows: &[Styled]) -> Pos { if self.hidden.is_some() { return Pos::new(0, 0); } let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.state.cursor_idx, indices); let cursor_col = widthdb.width(rows[cursor_row].text().split_at(cursor_line_idx).0); // Ensure the cursor is always visible let cursor_col = cursor_col.min(width.saturating_sub(1).into()); let cursor_row: i32 = cursor_row.try_into().unwrap_or(i32::MAX); let cursor_col: i32 = cursor_col.try_into().unwrap_or(i32::MAX); Pos::new(cursor_col, cursor_row) } } impl Widget for Editor<'_> { fn size( &self, widthdb: &mut WidthDb, max_width: Option, _max_height: Option, ) -> Result { let indices = self.indices(widthdb, max_width); let rows = self.rows(&indices); let width = rows .iter() .map(|row| widthdb.width(row.text())) .max() .unwrap_or(0) // One extra column for cursor .saturating_add(1); let height = rows.len(); let width: u16 = width.try_into().unwrap_or(u16::MAX); let height: u16 = height.try_into().unwrap_or(u16::MAX); Ok(Size::new(width, height)) } fn draw(mut self, frame: &mut Frame) -> Result<(), E> { let size = frame.size(); let indices = self.indices(frame.widthdb(), Some(size.width)); let rows = self.rows(&indices); let cursor = self.cursor(frame.widthdb(), size.width, &indices, &rows); for (i, row) in rows.into_iter().enumerate() { frame.write(Pos::new(0, i as i32), row); } if self.focus { frame.set_cursor(Some(cursor)); } self.state.last_cursor_pos = cursor; Ok(()) } }