Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
term: Add text wrapping to `TextArea`
Alexis Sellier committed 3 years ago
commit f9c722c16046a372553ab39d96c41077a8320e56
parent 103ee4e23dea3b48ce9c5c305ad58f0ae4a7b633
1 file changed +163 -8
modified radicle-term/src/textarea.rs
@@ -1,22 +1,61 @@
-
use unicode_width::UnicodeWidthStr;
+
use crate::{cell::Cell, Element, Line, Paint, Size};

-
use crate::{Element, Line, Paint, Size};
+
/// Default text wrap width.
+
pub const DEFAULT_WRAP: usize = 80;

/// Text area.
///
/// A block of text that can contain multiple lines.
#[derive(Debug)]
-
pub struct TextArea(Paint<String>);
+
pub struct TextArea {
+
    body: Paint<String>,
+
    wrap: usize,
+
}

impl TextArea {
    /// Create a new text area.
-
    pub fn new(content: impl Into<Paint<String>>) -> Self {
-
        Self(content.into())
+
    pub fn new(body: impl Into<Paint<String>>) -> 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<Item = &str> {
-
        self.0.content().lines()
+
    pub fn lines(&self) -> impl Iterator<Item = String> {
+
        let mut lines: Vec<String> = Vec::new();
+
        let mut fenced = false;
+

+
        for line in self.body.content().lines() {
+
            // 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.
@@ -35,7 +74,7 @@ impl Element for TextArea {

    fn render(&self) -> Vec<Line> {
        self.lines()
-
            .map(|l| Line::new(Paint::new(l).with_style(self.0.style)))
+
            .map(|l| Line::new(Paint::new(l).with_style(self.body.style)))
            .collect()
    }
}
@@ -44,3 +83,119 @@ impl Element for TextArea {
pub fn textarea(content: impl Into<Paint<String>>) -> 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::<Vec<_>>();
+

+
        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::<Vec<_>>();
+

+
        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::<Vec<_>>();
+

+
        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::<Vec<_>>();
+

+
        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()
+
            ]
+
        );
+
    }
+
}