From 23ffeb8bea19ef187fffde4638c0173a323bff76 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Sat, 19 Dec 2020 19:40:15 +0100 Subject: [PATCH] fix multiline textinput drawing (closes #405) --- src/components/mod.rs | 13 ++-- src/components/reset.rs | 6 +- src/components/textinput.rs | 138 ++++++++++++++++++++++++------------ 3 files changed, 104 insertions(+), 53 deletions(-) diff --git a/src/components/mod.rs b/src/components/mod.rs index 6714f311..38efec75 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -47,7 +47,7 @@ use crossterm::event::Event; use tui::{ backend::Backend, layout::{Alignment, Rect}, - text::{Span, Spans, Text}, + text::{Span, Text}, widgets::{Block, BorderType, Borders, Paragraph, Wrap}, Frame, }; @@ -204,13 +204,16 @@ fn dialog_paragraph<'a>( .alignment(Alignment::Left) } -fn popup_paragraph<'a>( +fn popup_paragraph<'a, T>( title: &'a str, - content: Vec>, + content: T, theme: &Theme, focused: bool, -) -> Paragraph<'a> { - Paragraph::new(Spans::from(content)) +) -> Paragraph<'a> +where + T: Into>, +{ + Paragraph::new(content.into()) .block( Block::default() .title(Span::styled(title, theme.title(focused))) diff --git a/src/components/reset.rs b/src/components/reset.rs index da275c59..be8e76d5 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -11,7 +11,7 @@ use anyhow::Result; use crossterm::event::Event; use std::borrow::Cow; use tui::{ - backend::Backend, layout::Rect, text::Span, widgets::Clear, Frame, + backend::Backend, layout::Rect, text::Text, widgets::Clear, Frame, }; use ui::style::SharedTheme; @@ -33,10 +33,10 @@ impl DrawableComponent for ResetComponent { if self.visible { let (title, msg) = self.get_text(); - let txt = vec![Span::styled( + let txt = Text::styled( Cow::from(msg), self.theme.text_danger(), - )]; + ); let area = ui::centered_rect(30, 20, f.size()); f.render_widget(Clear, area); diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 50c62575..8e30582f 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -13,8 +13,12 @@ use crossterm::event::{Event, KeyCode, KeyModifiers}; use itertools::Itertools; use std::ops::Range; use tui::{ - backend::Backend, layout::Rect, style::Modifier, text::Span, - widgets::Clear, Frame, + backend::Backend, + layout::Rect, + style::Modifier, + text::{Spans, Text}, + widgets::Clear, + Frame, }; #[derive(PartialEq)] @@ -45,7 +49,7 @@ impl TextInputComponent { default_msg: &str, ) -> Self { Self { - msg: String::default(), + msg: String::new(), visible: false, theme, key_config, @@ -125,17 +129,24 @@ impl TextInputComponent { self.title = t; } - fn get_draw_text(&self) -> Vec { + fn get_draw_text(&self) -> Text { let style = self.theme.text(true, false); - let mut txt = Vec::new(); + 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 { - txt.push(Span::styled( - self.get_msg(0..self.cursor_position), - style, - )); + 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(Spans::default()); + // txt = text_append(txt, Text::styled("\n\r", style)); + } } let cursor_str = self @@ -147,27 +158,36 @@ impl TextInputComponent { }); if cursor_str == "\n" { - txt.push(Span::styled( - "\u{21b5}", - self.theme - .text(false, false) - .add_modifier(Modifier::UNDERLINED), - )); + txt = text_append( + txt, + Text::styled( + "\u{21b5}\n\r", + self.theme + .text(false, false) + .add_modifier(Modifier::UNDERLINED), + ), + ); + } else { + txt = text_append( + txt, + Text::styled( + cursor_str, + style.add_modifier(Modifier::UNDERLINED), + ), + ); } - txt.push(Span::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.push(Span::styled( - self.get_msg(pos..self.msg.len()), - style, - )); + txt = text_append( + txt, + Text::styled( + self.get_msg(pos..self.msg.len()), + style, + ), + ); } } @@ -182,6 +202,26 @@ impl TextInputComponent { } } +// 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.0.extend(first_line.0.clone()); + } + + if append.lines.len() > 1 { + for line in 1..append.lines.len() { + let spans = append.lines[line].clone(); + txt.lines.push(spans); + } + } + } else { + txt = append + } + txt +} + impl DrawableComponent for TextInputComponent { fn draw( &self, @@ -190,10 +230,10 @@ impl DrawableComponent for TextInputComponent { ) -> Result<()> { if self.visible { let txt = if self.msg.is_empty() { - vec![Span::styled( + Text::styled( self.default_msg.as_str(), self.theme.text(false, false), - )] + ) } else { self.get_draw_text() }; @@ -311,7 +351,7 @@ impl Component for TextInputComponent { #[cfg(test)] mod tests { use super::*; - use tui::style::Style; + use tui::{style::Style, text::Span}; #[test] fn test_smoke() { @@ -350,9 +390,9 @@ mod tests { let txt = comp.get_draw_text(); - assert_eq!(txt.len(), 1); - assert_eq!(get_text(&txt[0]), Some("a")); - assert_eq!(get_style(&txt[0]), Some(&underlined)); + assert_eq!(txt.lines[0].0.len(), 1); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!(get_style(&txt.lines[0].0[0]), Some(&underlined)); } #[test] @@ -375,11 +415,14 @@ mod tests { let txt = comp.get_draw_text(); - assert_eq!(txt.len(), 2); - assert_eq!(get_text(&txt[0]), Some("a")); - assert_eq!(get_style(&txt[0]), Some(¬_underlined)); - assert_eq!(get_text(&txt[1]), Some(" ")); - assert_eq!(get_style(&txt[1]), Some(&underlined)); + assert_eq!(txt.lines[0].0.len(), 2); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!( + get_style(&txt.lines[0].0[0]), + Some(¬_underlined) + ); + assert_eq!(get_text(&txt.lines[0].0[1]), Some(" ")); + assert_eq!(get_style(&txt.lines[0].0[1]), Some(&underlined)); } #[test] @@ -401,12 +444,14 @@ mod tests { let txt = comp.get_draw_text(); - assert_eq!(txt.len(), 4); - assert_eq!(get_text(&txt[0]), Some("a")); - assert_eq!(get_text(&txt[1]), Some("\u{21b5}")); - assert_eq!(get_style(&txt[1]), Some(&underlined)); - assert_eq!(get_text(&txt[2]), Some("\n")); - assert_eq!(get_text(&txt[3]), Some("b")); + assert_eq!(txt.lines.len(), 2); + assert_eq!(txt.lines[0].0.len(), 2); + assert_eq!(txt.lines[1].0.len(), 2); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!(get_text(&txt.lines[0].0[1]), Some("\u{21b5}")); + assert_eq!(get_style(&txt.lines[0].0[1]), Some(&underlined)); + assert_eq!(get_text(&txt.lines[1].0[0]), Some("")); + assert_eq!(get_text(&txt.lines[1].0[1]), Some("b")); } #[test] @@ -427,10 +472,13 @@ mod tests { let txt = comp.get_draw_text(); - assert_eq!(txt.len(), 2); - assert_eq!(get_text(&txt[0]), Some("a")); - assert_eq!(get_style(&txt[0]), Some(&underlined)); - assert_eq!(get_text(&txt[1]), Some("\nb")); + assert_eq!(txt.lines.len(), 2); + assert_eq!(txt.lines[0].0.len(), 2); + assert_eq!(txt.lines[1].0.len(), 1); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!(get_text(&txt.lines[0].0[1]), Some("")); + assert_eq!(get_style(&txt.lines[0].0[0]), Some(&underlined)); + assert_eq!(get_text(&txt.lines[1].0[0]), Some("b")); } fn get_text<'a>(t: &'a Span) -> Option<&'a str> {