1use crate::{cell::Cell, Constraint, Element, Line, Paint, Size};
2
3pub const DEFAULT_WRAP: usize = 80;
5pub const SOFT_TAB: &str = " ";
7
8#[derive(Debug)]
12pub struct TextArea {
13 body: Paint<String>,
14 wrap: usize,
15}
16
17impl TextArea {
18 pub fn new(body: impl Into<Paint<String>>) -> Self {
20 Self {
21 body: body.into(),
22 wrap: DEFAULT_WRAP,
23 }
24 }
25
26 pub fn wrap(mut self, cols: usize) -> Self {
28 self.wrap = cols;
29 self
30 }
31
32 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 .map(|l| l.replace('\t', SOFT_TAB))
43 {
44 if line.starts_with("```") {
46 fenced = !fenced;
47 }
48 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 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
90pub 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}