use crate::{cell::Cell, Constraint, Element, Line, Paint, Size}; /// Default text wrap width. pub const DEFAULT_WRAP: usize = 80; /// Soft tab replacement for '\t'. pub const SOFT_TAB: &str = " "; /// Text area. /// /// A block of text that can contain multiple lines. #[derive(Debug)] pub struct TextArea { body: Paint, wrap: usize, } impl TextArea { /// Create a new text area. pub fn new(body: impl Into>) -> Self { Self { body: body.into(), wrap: DEFAULT_WRAP, } } /// Set wrap width. pub fn wrap(mut self, cols: usize) -> Self { self.wrap = cols; self } /// Get the lines of text in this text area. pub fn lines(&self) -> impl Iterator { let mut lines: Vec = Vec::new(); let mut fenced = false; for line in self .body .content() .lines() // Replace tabs as their visual width cannot be calculated. .map(|l| l.replace('\t', SOFT_TAB)) { // Fenced code block support. if line.starts_with("```") { fenced = !fenced; } // Code blocks are not wrapped, they are truncated. if fenced || line.starts_with('\t') || line.starts_with(' ') { lines.push(line.truncate(self.wrap, "…")); continue; } let mut current = String::new(); for word in line.split_whitespace() { if current.width() + word.width() > self.wrap { lines.push(current.trim_end().to_owned()); current = word.to_owned(); } else { current.push_str(word); } current.push(' '); } lines.push(current.trim_end().to_owned()); } lines.into_iter() } /// Box the text area. pub fn boxed(self) -> Box { Box::new(self) } } impl Element for TextArea { fn size(&self, _parent: Constraint) -> Size { let cols = self.lines().map(|l| l.width()).max().unwrap_or(0); let rows = self.lines().count(); Size::new(cols, rows) } fn render(&self, _parent: Constraint) -> Vec { self.lines() .map(|l| Line::new(Paint::new(l).with_style(self.body.style))) .collect() } } /// Create a new text area. pub fn textarea(content: impl Into>) -> TextArea { TextArea::new(content) } #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; #[test] fn test_wrapping() { let t = TextArea::new( "Radicle enables users to run their own nodes, \ ensuring censorship-resistant code collaboration \ and fostering a resilient network without reliance \ on third-parties.", ) .wrap(50); let wrapped = t.lines().collect::>(); assert_eq!( wrapped, vec![ "Radicle enables users to run their own nodes,".to_owned(), "ensuring censorship-resistant code collaboration".to_owned(), "and fostering a resilient network without reliance".to_owned(), "on third-parties.".to_owned(), ] ); } #[test] fn test_wrapping_paragraphs() { let t = TextArea::new( "Radicle enables users to run their own nodes, \ ensuring censorship-resistant code collaboration \ and fostering a resilient network without reliance \ on third-parties.\n\n\ All social artifacts are stored in git, and signed \ using public-key cryptography. Radicle verifies \ the authenticity and authorship of all data \ automatically.", ) .wrap(50); let wrapped = t.lines().collect::>(); assert_eq!( wrapped, vec![ "Radicle enables users to run their own nodes,".to_owned(), "ensuring censorship-resistant code collaboration".to_owned(), "and fostering a resilient network without reliance".to_owned(), "on third-parties.".to_owned(), "".to_owned(), "All social artifacts are stored in git, and signed".to_owned(), "using public-key cryptography. Radicle verifies".to_owned(), "the authenticity and authorship of all data".to_owned(), "automatically.".to_owned(), ] ); } #[test] fn test_wrapping_code_block() { let t = TextArea::new( "\ Here's an example: $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT $ rad sync Run the above and wait for your project to sync.\ ", ) .wrap(50); let wrapped = t.lines().collect::>(); assert_eq!( wrapped, vec![ "Here's an example:".to_owned(), "".to_owned(), " $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/…".to_owned(), " $ rad sync".to_owned(), "".to_owned(), "Run the above and wait for your project to sync.".to_owned() ] ); } #[test] fn test_wrapping_fenced_block() { let t = TextArea::new( "\ Here's an example: ``` $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT $ rad sync ``` Run the above and wait for your project to sync.\ ", ) .wrap(40); let wrapped = t.lines().collect::>(); assert_eq!( wrapped, vec![ "Here's an example:".to_owned(), "```".to_owned(), "$ git push rad://z3gqcJUoA1n9HaHKufZs5F…".to_owned(), "$ rad sync".to_owned(), "```".to_owned(), "Run the above and wait for your project".to_owned(), "to sync.".to_owned() ] ); } }