diff options
author | pm100 <paulmoore100@hotmail.com> | 2024-02-18 01:24:18 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-18 10:24:18 +0100 |
commit | b9a2e131f26d9f8c2ab24700ed7189cc0fc65aba (patch) | |
tree | 0581140e8dc663da46cdaa8832fb7d1d97628a2d | |
parent | 34398628544cf40bc66e1e54401501a3f574022b (diff) |
tui textarea (#2051)
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/components/commit.rs | 8 | ||||
-rw-r--r-- | src/components/textinput.rs | 1198 | ||||
-rw-r--r-- | src/keys/key_list.rs | 4 | ||||
-rw-r--r-- | src/strings.rs | 14 |
6 files changed, 682 insertions, 555 deletions
@@ -740,6 +740,7 @@ dependencies = [ "struct-patch", "syntect", "tempfile", + "tui-textarea", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -1704,6 +1705,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] +name = "tui-textarea" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width", +] + +[[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -54,6 +54,7 @@ syntect = { version = "5.2", default-features = false, features = [ "default-themes", "html", ] } +tui-textarea = "0.4.0" unicode-segmentation = "1.11" unicode-truncate = "0.2" unicode-width = "0.1" diff --git a/src/components/commit.rs b/src/components/commit.rs index 60365eee..83e18ea9 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -553,6 +553,12 @@ impl Component for CommitComponent { self.options.borrow().has_commit_msg_history(), true, )); + + out.push(CommandInfo::new( + strings::commands::newline(&self.key_config), + true, + true, + )); } visibility_blocking(self) @@ -565,7 +571,7 @@ impl Component for CommitComponent { } if let Event::Key(e) = ev { - if key_match(e, self.key_config.keys.enter) + if key_match(e, self.key_config.keys.commit) && self.can_commit() { try_or_popup!( diff --git a/src/components/textinput.rs b/src/components/textinput.rs index e60737a6..7d8dfc2b 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -1,52 +1,54 @@ use crate::app::Environment; use crate::keys::key_match; -use crate::strings::symbol; use crate::ui::Size; use crate::{ components::{ - popup_paragraph, visibility_blocking, CommandBlocking, - CommandInfo, Component, DrawableComponent, EventState, + visibility_blocking, CommandBlocking, CommandInfo, Component, + DrawableComponent, EventState, }, keys::SharedKeyConfig, strings, ui::{self, style::SharedTheme}, }; use anyhow::Result; -use crossterm::event::{Event, KeyCode, KeyModifiers}; -use itertools::Itertools; +use crossterm::event::Event; +use ratatui::widgets::{Block, Borders}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, - style::Modifier, - text::{Line, Text}, widgets::{Clear, Paragraph}, Frame, }; -use std::{cell::Cell, collections::HashMap, ops::Range}; -use unicode_segmentation::UnicodeSegmentation; - +use std::cell::Cell; +use std::cell::OnceCell; +use std::convert::From; +use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea}; #[derive(PartialEq, Eq)] pub enum InputType { Singleline, Multiline, Password, } - -/// primarily a subcomponet for user input of text (used in `CommitComponent`) +#[derive(PartialEq, Eq)] +enum SelectionState { + Selecting, + NotSelecting, + SelectionEndPending, +} +type TextAreaComponent = TextArea<'static>; pub struct TextInputComponent { title: String, default_msg: String, - msg: String, - visible: bool, selected: Option<bool>, + msg: OnceCell<String>, show_char_count: bool, theme: SharedTheme, key_config: SharedKeyConfig, - cursor_position: usize, input_type: InputType, current_area: Cell<Rect>, embed: bool, - char_count: usize, + textarea: Option<TextAreaComponent>, + select_state: SelectionState, } impl TextInputComponent { @@ -58,19 +60,18 @@ impl TextInputComponent { show_char_count: bool, ) -> Self { Self { - msg: String::new(), - visible: false, + msg: OnceCell::default(), theme: env.theme.clone(), key_config: env.key_config.clone(), show_char_count, title: title.to_string(), default_msg: default_msg.to_string(), selected: None, - cursor_position: 0, input_type: InputType::Multiline, current_area: Cell::new(Rect::default()), embed: false, - char_count: 0, + textarea: None, + select_state: SelectionState::NotSelecting, } } @@ -84,14 +85,29 @@ impl TextInputComponent { /// Clear the `msg`. pub fn clear(&mut self) { - self.msg.clear(); - self.update_count(); - self.cursor_position = 0; + self.msg.take(); + if self.is_visible() { + self.show_inner_textarea(); + } } /// Get the `msg`. pub fn get_text(&self) -> &str { - self.msg.as_str() + // the fancy footwork with the OnceCell is to allow + // the reading of msg as a &str. + // tui_textarea returns its lines to the caller as &[String] + // gitui wants &str of \n delimited text + // it would be simple if this was a mut method. You could + // just load up msg from the lines area and return an &str pointing at it + // but its not a mut method. So we need to store the text in a OnceCell + // The methods that change msg call take() on the cell. That makes + // get_or_init run again + + self.msg.get_or_init(|| { + self.textarea + .as_ref() + .map_or_else(String::new, |ta| ta.lines().join("\n")) + }) } /// screen area (last time we got drawn) @@ -109,120 +125,51 @@ impl TextInputComponent { self.selected = Some(enable); } - /// Move the cursor right one char. - fn incr_cursor(&mut self) { - if let Some(pos) = self.next_char_position() { - self.cursor_position = pos; - } - } - - /// Move the cursor left one char. - fn decr_cursor(&mut self) { - let mut index = self.cursor_position.saturating_sub(1); - while index > 0 && !self.msg.is_char_boundary(index) { - index -= 1; - } - self.cursor_position = index; - } - - /// Get the position of the next char, or, if the cursor points - /// to the last char, the `msg.len()`. - /// Returns None when the cursor is already at `msg.len()`. - fn next_char_position(&self) -> Option<usize> { - if self.cursor_position >= self.msg.len() { - return None; - } - let mut index = self.cursor_position.saturating_add(1); - while index < self.msg.len() - && !self.msg.is_char_boundary(index) - { - index += 1; - } - Some(index) - } - - /// Helper for `next/previous_word_position`. - fn at_alphanumeric(&self, i: usize) -> bool { - self.msg[i..] - .chars() - .next() - .map_or(false, char::is_alphanumeric) - } - - /// Get the position of the first character of the next word, or, if there - /// isn't a next word, the `msg.len()`. - /// Returns None when the cursor is already at `msg.len()`. - /// - /// A Word is continuous sequence of alphanumeric characters. - fn next_word_position(&self) -> Option<usize> { - if self.cursor_position >= self.msg.len() { - return None; - } - - let mut was_in_word = - self.at_alphanumeric(self.cursor_position); - - let mut index = self.cursor_position.saturating_add(1); - while index < self.msg.len() { - if !self.msg.is_char_boundary(index) { - index += 1; - continue; - } - - let is_in_word = self.at_alphanumeric(index); - if !was_in_word && is_in_word { - break; - } - was_in_word = is_in_word; - index += 1; - } - Some(index) - } - - /// Get the position of the first character of the previous word, or, if there - /// isn't a previous word, returns `0`. - /// Returns None when the cursor is already at `0`. - /// - /// A Word is continuous sequence of alphanumeric characters. - fn previous_word_position(&self) -> Option<usize> { - if self.cursor_position == 0 { - return None; - } - - let mut was_in_word = false; - - let mut last_pos = self.cursor_position; - let mut index = self.cursor_position; - while index > 0 { - index -= 1; - if !self.msg.is_char_boundary(index) { - continue; + fn show_inner_textarea(&mut self) { + // create the textarea and then load it with the text + // from self.msg + let lines: Vec<String> = self + .msg + .get() + .unwrap_or(&String::new()) + .split('\n') + .map(ToString::to_string) + .collect(); + self.textarea = Some({ + let style = + self.theme.text(self.selected.unwrap_or(true), false); + let mut text_area = TextArea::new(lines); + if self.input_type == InputType::Password { + text_area.set_mask_char('*'); } - - let is_in_word = self.at_alphanumeric(index); - if was_in_word && !is_in_word { - return Some(last_pos); - } - - last_pos = index; - was_in_word = is_in_word; - } - Some(0) - } - - fn backspace(&mut self) { - if self.cursor_position > 0 { - self.decr_cursor(); - self.msg.remove(self.cursor_position); - self.update_count(); - } + text_area + .set_cursor_line_style(self.theme.text(true, false)); + text_area.set_placeholder_text(self.default_msg.clone()); + text_area.set_placeholder_style(style); + text_area.set_style(style); + if !self.embed { + text_area.set_block( + Block::default() + .borders(Borders::ALL) + .border_style( + ratatui::style::Style::default() + .add_modifier( + ratatui::style::Modifier::BOLD, + ), + ) + .title(self.title.clone()), + ); + }; + text_area + }); } /// Set the `msg`. pub fn set_text(&mut self, msg: String) { - self.msg = msg; - self.cursor_position = 0; - self.update_count(); + self.msg = msg.into(); + if self.is_visible() { + self.show_inner_textarea(); + } } /// Set the `title`. @@ -233,96 +180,16 @@ impl TextInputComponent { /// pub fn set_default_msg(&mut self, v: String) { self.default_msg = v; - } - - fn get_draw_text(&self) -> Text { - let style = - self.theme.text(self.selected.unwrap_or(true), false); - - let mut txt = Text::default(); - // The portion of the text before the cursor is added - // if the cursor is not at the first character. - if self.cursor_position > 0 { - let text_before_cursor = - self.get_msg(0..self.cursor_position); - let ends_in_nl = text_before_cursor.ends_with('\n'); - txt = text_append( - txt, - Text::styled(text_before_cursor, style), - ); - if ends_in_nl { - txt.lines.push(Line::default()); - } - } - - let cursor_str = self - .next_char_position() - // if the cursor is at the end of the msg - // a whitespace is used to underline - .map_or(" ".to_owned(), |pos| { - self.get_msg(self.cursor_position..pos) - }); - - let cursor_highlighting = { - let mut h = HashMap::with_capacity(2); - h.insert("\n", "\u{21b5}\r\n\n"); - h.insert(" ", symbol::WHITESPACE); - h - }; - - if let Some(substitute) = - cursor_highlighting.get(cursor_str.as_str()) - { - txt = text_append( - txt, - Text::styled( - substitute.to_owned(), - self.theme - .text(false, false) - .add_modifier(Modifier::UNDERLINED), - ), - ); - } else { - txt = text_append( - txt, - Text::styled( - cursor_str, - style.add_modifier(Modifier::UNDERLINED), - ), - ); - } - - // The final portion of the text is added if there are - // still remaining characters. - if let Some(pos) = self.next_char_position() { - if pos < self.msg.len() { - txt = text_append( - txt, - Text::styled( - self.get_msg(pos..self.msg.len()), - style, - ), - ); - } - } - - txt - } - - fn get_msg(&self, range: Range<usize>) -> String { - match self.input_type { - InputType::Password => range.map(|_| "*").join(""), - _ => self.msg[range].to_owned(), + if self.is_visible() { + self.show_inner_textarea(); } } fn draw_char_count<B: Backend>(&self, f: &mut Frame<B>, r: Rect) { - if self.char_count > 0 { - let w = Paragraph::new(format!( - "[{} chars]", - self.char_count - )) - .alignment(Alignment::Right); + let count = self.get_text().len(); + if count > 0 { + let w = Paragraph::new(format!("[{count} chars]")) + .alignment(Alignment::Right); let mut rect = { let mut rect = r; @@ -340,29 +207,40 @@ impl TextInputComponent { } } - fn update_count(&mut self) { - self.char_count = self.msg.graphemes(true).count(); - } -} - -// merges last line of `txt` with first of `append` so we do not generate unneeded newlines -fn text_append<'a>(txt: Text<'a>, append: Text<'a>) -> Text<'a> { - let mut txt = txt; - if let Some(last_line) = txt.lines.last_mut() { - if let Some(first_line) = append.lines.first() { - last_line.spans.extend(first_line.spans.clone()); + fn should_select(&mut self, input: &Input) { + if input.key == Key::Null { + return; } + // Should we start selecting text, stop the current selection, or do nothing? + // the end is handled after the ending keystroke - if append.lines.len() > 1 { - for line in 1..append.lines.len() { - let spans = append.lines[line].clone(); - txt.lines.push(spans); + match (&self.select_state, input.shift) { + (SelectionState::Selecting, true) + | (SelectionState::NotSelecting, false) => { + // continue selecting or not selecting + } + (SelectionState::Selecting, false) => { + // end select + self.select_state = + SelectionState::SelectionEndPending; + } + (SelectionState::NotSelecting, true) => { + // start select + // this should always work since we are only called + // if we have a textarea to get input + if let Some(ta) = &mut self.textarea { + ta.start_selection(); + self.select_state = SelectionState::Selecting; + } + } + (SelectionState::SelectionEndPending, _) => { + // this really should not happen because the end pending state + // should have been picked up in the same pass as it was set + // so lets clear it + self.select_state = SelectionState::NotSelecting; } } - } else { - txt = append; } - txt } impl DrawableComponent for TextInputComponent { @@ -371,19 +249,9 @@ impl DrawableComponent for TextInputComponent { f: &mut Frame<B>, rect: Rect, ) -> Result<()> { - if self.visible { - let txt = if self.msg.is_empty() { - Text::styled( - self.default_msg.as_str(), - self.theme.text( - self.selected.unwrap_or_default(), - false, - ), - ) - } else { - self.get_draw_text() - }; - + // this should always be true since draw should only be being called + // is control is visible + if let Some(ta) = &self.textarea { let area = if self.embed { rect } else { @@ -402,16 +270,8 @@ impl DrawableComponent for TextInputComponent { }; f.render_widget(Clear, area); - f.render_widget( - popup_paragraph( - self.title.as_str(), - txt, - &self.theme, - true, - !self.embed, - ), - area, - ); + + f.render_widget(ta.widget(), area); if self.show_char_count { self.draw_char_count(f, area); @@ -419,7 +279,6 @@ impl DrawableComponent for TextInputComponent { self.current_area.set(area); } - Ok(()) } } @@ -434,115 +293,438 @@ impl Component for TextInputComponent { CommandInfo::new( strings::commands::close_popup(&self.key_config), true, - self.visible, + self.is_visible(), ) .order(1), ); visibility_blocking(self) } + // the fixes this clippy wants make the code much harder to follow + #[allow(clippy::unnested_or_patterns)] + // it just has to be this big + #[allow(clippy::too_many_lines)] + fn event(&mut self, ev: &Event) -> Result<EventState> { - if self.visible { + let input = Input::from(ev.clone()); + self.should_select(&input); + if let Some(ta) = &mut self.textarea { if let Event::Key(e) = ev { if key_match(e, self.key_config.keys.exit_popup) { self.hide(); return Ok(EventState::Consumed); } - let is_ctrl = - e.modifiers.contains(KeyModifiers::CONTROL); - - match e.code { - KeyCode::Char(c) if !is_ctrl => { - self.msg.insert(self.cursor_position, c); - self.update_count(); - self.incr_cursor(); - return Ok(EventState::Consumed); - } - KeyCode::Delete if is_ctrl => { - if let Some(pos) = self.next_word_position() { - self.msg.replace_range( - self.cursor_position..pos, - "", - ); - self.update_count(); - } - return Ok(EventState::Consumed); + // for a multi line box we want to allow the user to enter new lines + // so test for what might be a different enter to mean 'ok do it' + if self.input_type == InputType::Multiline { + if key_match(e, self.key_config.keys.commit) { + // means pass it back up to the caller to handle + return Ok(EventState::NotConsumed); } - KeyCode::Backspace | KeyCode::Char('w') - if is_ctrl => + } else if key_match(e, self.key_config.keys.enter) { + // ditto - we dont want it + return Ok(EventState::NotConsumed); + } + + // here all 'known' special keys for any textinput call are filtered out + + if key_match(e, self.key_config.keys.toggle_verify) + || key_match(e, self.key_config.keys.commit_amend) + || key_match( + e, + self.key_config.keys.open_commit_editor, + ) || key_match( + e, + self.key_config.keys.commit_history_next, + ) { + return Ok(EventState::NotConsumed); + } + + /* + here we do key handling rather than passing it to textareas input function + - so that we know which keys were handled and which were not + - to get fine control over what each key press does + - allow for key mapping based off key config.... + but in fact the original textinput ignored all key bindings, up,down,right,.... + so they are also ignored here + + */ + + // was the text buffer changed? + + let modified = + if key_match(e, self.key_config.keys.newline) + && self.input_type == InputType::Multiline { - if let Some(pos) = - self.previous_word_position() - { - self.msg.replace_range( - pos..self.cursor_position, - "", - ); - self.cursor_position = pos; - self.update_count(); - } - return Ok(EventState::Consumed); - } - KeyCode::Left if is_ctrl => { - if let Some(pos) = - self.previous_word_position() - { - self.cursor_position = pos; + ta.insert_newline(); + true + } else { + match input { + Input { + key: Key::Char(c), + ctrl: false, + alt: false, + .. + } => { + ta.insert_char(c); + true + } + + Input { + key: Key::Tab, + ctrl: false, + alt: false, + .. + } => { + ta.insert_tab(); + true + } + Input { + key: Key::Char('h'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Backspace, + ctrl: false, + alt: false, + .. + } => ta.delete_char(), + Input { + key: Key::Char('d'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Delete, + ctrl: false, + alt: false, + .. + } => ta.delete_next_char(), + Input { + key: Key::Char('k'), + ctrl: true, + alt: false, + .. + } => ta.delete_line_by_end(), + Input { + key: Key::Char('j'), + ctrl: true, + alt: false, + .. + } => ta.delete_line_by_head(), + Input { + key: Key::Char('w'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Char('h'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Backspace, + ctrl: false, + alt: true, + .. + } => ta.delete_word(), + Input { + key: Key::Delete, + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Char('d'), + ctrl: false, + alt: true, + .. + } => ta.delete_next_word(), + Input { + key: Key::Char('n'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Down, + ctrl: false, + alt: false, + .. + } => { + ta.move_cursor(CursorMove::Down); + false + } + Input { + key: Key::Char('p'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Up, + ctrl: false, + alt: false, + .. + } => { + ta.move_cursor(CursorMove::Up); + false + } + Input { + key: Key::Char('f'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Right, + ctrl: false, + alt: false, + .. + } => { + ta.move_cursor(CursorMove::Forward); + false + } + Input { + key: Key::Char('b'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::Left, + ctrl: false, + alt: false, + .. + } => { + ta.move_cursor(CursorMove::Back); + false + } + // normally picked up earlier as 'amend' + Input { + key: Key::Char('a'), + ctrl: true, + alt: false, + .. + } + | Input { key: Key::Home, .. } + | Input { + key: Key::Left | Key::Char('b'), + ctrl: true, + alt: true, + .. + } => { + ta.move_cursor(CursorMove::Head); + false + } + // normally picked up earlier as 'invoke editor' + Input { + key: Key::Char('e'), + ctrl: true, + alt: false, + .. + } + | Input { key: Key::End, .. } + | Input { + key: Key::Right | Key::Char('f'), + ctrl: true, + alt: true, + .. + } => { + ta.move_cursor(CursorMove::End); + false + } + Input { + key: Key::Char('<'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Up | Key::Char('p'), + ctrl: true, + alt: true, + .. + } => { + ta.move_cursor(CursorMove::Top); + false + } + Input { + key: Key::Char('>'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Down | Key::Char('n'), + ctrl: true, + alt: true, + .. + } => { + ta.move_cursor(CursorMove::Bottom); + false + } + Input { + key: Key::Char('f'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Right, + ctrl: true, + alt: false, + .. + } => { + ta.move_cursor( + CursorMove::WordForward, + ); + false + } + + Input { + key: Key::Char('b'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Left, + ctrl: true, + alt: false, + .. + } => { + ta.move_cursor(CursorMove::WordBack); + false + } + + Input { + key: Key::Char(']'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Char('n'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Down, + ctrl: true, + alt: false, + .. + } => { + ta.move_cursor( + CursorMove::ParagraphForward, + ); + false + } + Input { + key: Key::Char('['), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Char('p'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::Up, + ctrl: true, + alt: false, + .. + } => { + ta.move_cursor( + CursorMove::ParagraphBack, + ); + false + } + Input { + key: Key::Char('u'), + ctrl: true, + alt: false, + .. + } => ta.undo(), + Input { + key: Key::Char('r'), + ctrl: true, + alt: false, + .. + } => ta.redo(), + Input { + key: Key::Char('y'), + ctrl: true, + alt: false, + .. + } => ta.paste(), + Input { + key: Key::Char('v'), + ctrl: true, + alt: false, + .. + } + | Input { + key: Key::PageDown, .. + } => { + ta.scroll(Scrolling::PageDown); + false + } + + Input { + key: Key::Char('v'), + ctrl: false, + alt: true, + .. + } + | Input { + key: Key::PageUp, .. + } => { + ta.scroll(Scrolling::PageUp); + false + } + _ => return Ok(EventState::NotConsumed), } - return Ok(EventState::Consumed); - } - KeyCode::Right if is_ctrl => { - if let Some(pos) = self.next_word_position() { - self.cursor_position = pos; - } - return Ok(EventState::Consumed); - } - KeyCode::Delete => { - if self.cursor_position < self.msg.len() { - self.msg.remove(self.cursor_position); - self.update_count(); - } - return Ok(EventState::Consumed); - } - KeyCode::Backspace => { - self.backspace(); - return Ok(EventState::Consumed); - } - KeyCode::Left => { - self.decr_cursor(); - return Ok(EventState::Consumed); - } - KeyCode::Right => { - self.incr_cursor(); - return Ok(EventState::Consumed); - } - KeyCode::Home => { - self.cursor_position = 0; - return Ok(EventState::Consumed); - } - KeyCode::End => { - self.cursor_position = self.msg.len(); - return Ok(EventState::Consumed); - } - _ => (), - }; + }; + if modified { + self.msg.take(); + } + } + if self.select_state + == SelectionState::SelectionEndPending + { + ta.cancel_selection(); + self.select_state = SelectionState::NotSelecting; } + return Ok(EventState::Consumed); } Ok(EventState::NotConsumed) } - + /* + visible maps to textarea Option + None = > not visible + Some => visible + */ fn is_visible(&self) -> bool { - self.visible + self.textarea.is_some() } fn hide(&mut self) { - self.visible = false; + self.textarea = None; } fn show(&mut self) -> Result<()> { - self.visible = true; - + self.show_inner_textarea(); Ok(()) } } @@ -550,246 +732,158 @@ impl Component for TextInputComponent { #[cfg(test)] mod tests { use super::*; - use ratatui::{style::Style, text::Span}; #[test] fn test_smoke() { - let mut comp = TextInputComponent::new( - &Environment::test_env(), - "", - "", - false, - ); - + let env = Environment::test_env(); + let mut comp = TextInputComponent::new(&env, "", "", false); + comp.show_inner_textarea(); comp.set_text(String::from("a\nb")); + assert!(comp.is_visible()); + if let Some(ta) = &mut comp.textarea { + assert_eq!(ta.cursor(), (0, 0)); - assert_eq!(comp.cursor_position, 0); - - comp.incr_cursor(); - assert_eq!(comp.cursor_position, 1); + ta.move_cursor(CursorMove::Forward); + assert_eq!(ta.cursor(), (0, 1)); - comp.decr_cursor(); - assert_eq!(comp.cursor_position, 0); + ta.move_cursor(CursorMove::Back); + assert_eq!(ta.cursor(), (0, 0)); + } |