radicle_term/
textarea.rs

1use crate::{cell::Cell, Constraint, Element, Line, Paint, Size};
2
3/// Default text wrap width.
4pub const DEFAULT_WRAP: usize = 80;
5/// Soft tab replacement for '\t'.
6pub const SOFT_TAB: &str = "  ";
7
8/// Text area.
9///
10/// A block of text that can contain multiple lines.
11#[derive(Debug)]
12pub struct TextArea {
13    body: Paint<String>,
14    wrap: usize,
15}
16
17impl TextArea {
18    /// Create a new text area.
19    pub fn new(body: impl Into<Paint<String>>) -> Self {
20        Self {
21            body: body.into(),
22            wrap: DEFAULT_WRAP,
23        }
24    }
25
26    /// Set wrap width.
27    pub fn wrap(mut self, cols: usize) -> Self {
28        self.wrap = cols;
29        self
30    }
31
32    /// Get the lines of text in this text area.
33    pub fn lines(&self) -> impl Iterator<Item = String> {
34        let mut lines: Vec<String> = Vec::new();
35        let mut fenced = false;
36
37        for line in self
38            .body
39            .content()
40            .lines()
41            // Replace tabs as their visual width cannot be calculated.
42            .map(|l| l.replace('\t', SOFT_TAB))
43        {
44            // Fenced code block support.
45            if line.starts_with("```") {
46                fenced = !fenced;
47            }
48            // Code blocks are not wrapped, they are truncated.
49            if fenced || line.starts_with('\t') || line.starts_with(' ') {
50                lines.push(line.truncate(self.wrap, "…"));
51                continue;
52            }
53            let mut current = String::new();
54
55            for word in line.split_whitespace() {
56                if current.width() + word.width() > self.wrap {
57                    lines.push(current.trim_end().to_owned());
58                    current = word.to_owned();
59                } else {
60                    current.push_str(word);
61                }
62                current.push(' ');
63            }
64            lines.push(current.trim_end().to_owned());
65        }
66        lines.into_iter()
67    }
68
69    /// Box the text area.
70    pub fn boxed(self) -> Box<dyn Element> {
71        Box::new(self)
72    }
73}
74
75impl Element for TextArea {
76    fn size(&self, _parent: Constraint) -> Size {
77        let cols = self.lines().map(|l| l.width()).max().unwrap_or(0);
78        let rows = self.lines().count();
79
80        Size::new(cols, rows)
81    }
82
83    fn render(&self, _parent: Constraint) -> Vec<Line> {
84        self.lines()
85            .map(|l| Line::new(Paint::new(l).with_style(self.body.style)))
86            .collect()
87    }
88}
89
90/// Create a new text area.
91pub fn textarea(content: impl Into<Paint<String>>) -> TextArea {
92    TextArea::new(content)
93}
94
95#[cfg(test)]
96mod test {
97    use super::*;
98    use pretty_assertions::assert_eq;
99
100    #[test]
101    fn test_wrapping() {
102        let t = TextArea::new(
103            "Radicle enables users to run their own nodes, \
104            ensuring censorship-resistant code collaboration \
105            and fostering a resilient network without reliance \
106            on third-parties.",
107        )
108        .wrap(50);
109        let wrapped = t.lines().collect::<Vec<_>>();
110
111        assert_eq!(
112            wrapped,
113            vec![
114                "Radicle enables users to run their own nodes,".to_owned(),
115                "ensuring censorship-resistant code collaboration".to_owned(),
116                "and fostering a resilient network without reliance".to_owned(),
117                "on third-parties.".to_owned(),
118            ]
119        );
120    }
121
122    #[test]
123    fn test_wrapping_paragraphs() {
124        let t = TextArea::new(
125            "Radicle enables users to run their own nodes, \
126            ensuring censorship-resistant code collaboration \
127            and fostering a resilient network without reliance \
128            on third-parties.\n\n\
129            All social artifacts are stored in git, and signed \
130            using public-key cryptography. Radicle verifies \
131            the authenticity and authorship of all data \
132            automatically.",
133        )
134        .wrap(50);
135        let wrapped = t.lines().collect::<Vec<_>>();
136
137        assert_eq!(
138            wrapped,
139            vec![
140                "Radicle enables users to run their own nodes,".to_owned(),
141                "ensuring censorship-resistant code collaboration".to_owned(),
142                "and fostering a resilient network without reliance".to_owned(),
143                "on third-parties.".to_owned(),
144                "".to_owned(),
145                "All social artifacts are stored in git, and signed".to_owned(),
146                "using public-key cryptography. Radicle verifies".to_owned(),
147                "the authenticity and authorship of all data".to_owned(),
148                "automatically.".to_owned(),
149            ]
150        );
151    }
152
153    #[test]
154    fn test_wrapping_code_block() {
155        let t = TextArea::new(
156            "\
157Here's an example:
158
159  $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
160  $ rad sync
161
162Run the above and wait for your project to sync.\
163        ",
164        )
165        .wrap(50);
166        let wrapped = t.lines().collect::<Vec<_>>();
167
168        assert_eq!(
169            wrapped,
170            vec![
171                "Here's an example:".to_owned(),
172                "".to_owned(),
173                "  $ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/…".to_owned(),
174                "  $ rad sync".to_owned(),
175                "".to_owned(),
176                "Run the above and wait for your project to sync.".to_owned()
177            ]
178        );
179    }
180
181    #[test]
182    fn test_wrapping_fenced_block() {
183        let t = TextArea::new(
184            "\
185Here's an example:
186```
187$ git push rad://z3gqcJUoA1n9HaHKufZs5FCSGazv5/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
188$ rad sync
189```
190Run the above and wait for your project to sync.\
191        ",
192        )
193        .wrap(40);
194        let wrapped = t.lines().collect::<Vec<_>>();
195
196        assert_eq!(
197            wrapped,
198            vec![
199                "Here's an example:".to_owned(),
200                "```".to_owned(),
201                "$ git push rad://z3gqcJUoA1n9HaHKufZs5F…".to_owned(),
202                "$ rad sync".to_owned(),
203                "```".to_owned(),
204                "Run the above and wait for your project".to_owned(),
205                "to sync.".to_owned()
206            ]
207        );
208    }
209}