summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorpm100 <paulmoore100@hotmail.com>2024-02-18 01:24:18 -0800
committerGitHub <noreply@github.com>2024-02-18 10:24:18 +0100
commitb9a2e131f26d9f8c2ab24700ed7189cc0fc65aba (patch)
tree0581140e8dc663da46cdaa8832fb7d1d97628a2d /src
parent34398628544cf40bc66e1e54401501a3f574022b (diff)
tui textarea (#2051)
Diffstat (limited to 'src')
-rw-r--r--src/components/commit.rs8
-rw-r--r--src/components/textinput.rs1198
-rw-r--r--src/keys/key_list.rs4
-rw-r--r--src/strings.rs14
4 files changed, 669 insertions, 555 deletions
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));
+ }
}
#[test]
fn text_cursor_initial_position() {
- let mut comp = TextInputComponent::new(
- &Environment::test_env(),
- "",
- "",
- false,
- );
- let theme = SharedTheme::default();
- let underlined = theme
- .text(true, false)
- .add_modifier(Modifier::UNDERLINED);
-
- comp.set_text(String::from("a"));
-
- let txt = comp.get_draw_text();
-
- assert_eq!(txt.lines[0].spans.len(), 1);
- assert_eq!(get_text(&txt.lines[0].spans[0]), Some("a"));
- assert_eq!(
- get_style(&txt.lines[0].spans[0]),
- Some(&underlined)
- );
- }
-
- #[test]
- fn test_cursor_second_position() {
- let mut comp = TextInputComponent::new(
- &Environment::test_env(),
- "",
- "",
- false,
- );
- let theme = SharedTheme::default();
- let underlined_whitespace = theme
- .text(false, false)
- .add_modifier(Modifier::UNDERLINED);
-
- let not_underlined = Style::default();
-
+ let env = Environment::test_env();
+ let mut comp = TextInputComponent::new(&env, "", "", false);
+ comp.show_inner_textarea();
comp.set_text(String::from("a"));
- comp.incr_cursor();
-
- let txt = comp.get_draw_text();
-
- assert_eq!(txt.lines[0].spans.len(), 2);
- assert_eq!(get_text(&txt.lines[0].spans[0]), Some("a"));
- assert_eq!(
- get_style(&txt.lines[0].spans[0]),
- Some(&not_underlined)
- );
- assert_eq!(
- get_text(&txt.lines[0].spans[1]),
- Some(symbol::WHITESPACE)
- );
- assert_eq!(
- get_style(&txt.lines[0].spans[1]),
- Some(&underlined_whitespace)
- );
- }
-
- #[test]
- fn test_visualize_newline() {
- let mut comp = TextInputComponent::new(
- &Environment::test_env(),
- "",
- "",
- false,
- );
-
- let theme = SharedTheme::default();
- let underlined = theme
- .text(false, false)
- .add_modifier(Modifier::UNDERLINED);
-
- comp.set_text(String::from("a\nb"));
- comp.incr_cursor();
-
- let txt = comp.get_draw_text();
-
- assert_eq!(txt.lines.len(), 2);
- assert_eq!(txt.lines[0].spans.len(), 2);
- assert_eq!(txt.lines[1].spans.len(), 2);
- assert_eq!(get_text(&txt.lines[0].spans[0]), Some("a"));
- assert_eq!(
- get_text(&txt.lines[0].spans[1])