1use std::ops::Range;
2
3use textwrap::{
4 core::{break_words, Word},
5 word_splitters::split_words,
6 WordSeparator, WordSplitter, WrapAlgorithm,
7};
8
9use crate::{backend, layout::Layout, Widget};
10
11#[derive(Debug, Clone)]
16pub struct Text<S> {
17 pub text: S,
22 lines: Vec<Range<usize>>,
24 line_offset: u16,
25 width: u16,
26}
27
28impl<S: PartialEq> PartialEq for Text<S> {
29 fn eq(&self, other: &Self) -> bool {
30 self.text == other.text
31 }
32}
33
34impl<S: Eq> Eq for Text<S> {}
35
36impl<S: AsRef<str>> Text<S> {
37 pub fn new(text: S) -> Self {
39 Self {
40 text,
41 lines: Vec::new(),
42 width: 0,
43 line_offset: 0,
44 }
45 }
46
47 pub fn force_recompute(&mut self) {
51 self.line_offset = u16::MAX;
52 self.width = u16::MAX;
53 }
54
55 fn max_height(&mut self, layout: Layout) -> u16 {
56 let width = layout.available_width();
57
58 if self.width != width || self.line_offset != layout.line_offset {
59 wrap(self.text.as_ref(), layout, &mut self.lines);
60 self.width = width;
61 self.line_offset = layout.line_offset;
62 }
63
64 self.lines.len() as u16
65 }
66}
67
68impl<S: AsRef<str>> Widget for Text<S> {
69 fn render<B: backend::Backend>(
75 &mut self,
76 layout: &mut Layout,
77 backend: &mut B,
78 ) -> std::io::Result<()> {
79 let height = self.max_height(*layout);
81 let text = self.text.as_ref();
82
83 if height == 1 {
84 backend.write_all(text[self.lines[0].clone()].as_bytes())?;
85 layout.offset_y += 1;
86 backend.move_cursor_to(layout.offset_x, layout.offset_y)?;
87 } else {
88 let start = layout.get_start(height) as usize;
89 let nlines = height.min(layout.max_height);
90
91 for (i, line) in self
92 .lines
93 .iter()
94 .skip(start)
95 .take(nlines as usize)
96 .enumerate()
97 {
98 backend.write_all(text[line.clone()].as_bytes())?;
99 backend.move_cursor_to(layout.offset_x, layout.offset_y + i as u16 + 1)?;
100 }
101
102 layout.offset_y += nlines;
105 }
106 layout.line_offset = 0;
107
108 Ok(())
109 }
110
111 fn height(&mut self, layout: &mut Layout) -> u16 {
113 let height = self.max_height(*layout).min(layout.max_height);
114 layout.offset_y += height;
115 height
116 }
117
118 fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
120 layout.offset_cursor((layout.line_offset, 0))
121 }
122
123 fn handle_key(&mut self, _: crate::events::KeyEvent) -> bool {
125 false
126 }
127}
128
129impl<S: AsRef<str>> AsRef<str> for Text<S> {
130 fn as_ref(&self) -> &str {
131 self.text.as_ref()
132 }
133}
134
135impl<S: AsRef<str>> From<S> for Text<S> {
136 fn from(text: S) -> Self {
137 Self::new(text)
138 }
139}
140
141fn wrap(text: &str, layout: Layout, lines: &mut Vec<Range<usize>>) {
146 lines.clear();
147
148 let width = layout.available_width() as usize;
149 let line_widths = [width.saturating_sub(layout.line_offset as usize), width];
150
151 let mut start = 0;
152 for line in text.split_inclusive('\n') {
153 let next_start = start + line.len();
154 let line = line.strip_suffix('\n').unwrap_or(line);
155 let line = line.strip_suffix('\r').unwrap_or(line);
156 let first_line = lines.is_empty();
157
158 if line.len() < width && !(first_line && layout.line_offset > 0) {
159 lines.push(start..start + line.trim_end_matches(' ').len());
161 } else {
162 wrap_line(line, start, &line_widths, first_line, lines);
163 }
164
165 start = next_start;
166 }
167}
168
169fn wrap_line(
170 line: &str,
171 start: usize,
172 line_widths: &[usize; 2],
173 first_line: bool,
174 lines: &mut Vec<Range<usize>>,
175) {
176 let words = WordSeparator::new().find_words(line);
177 let words = split_words(words, &WordSplitter::HyphenSplitter);
178 let mut words = break_words(words, line_widths[1]);
179
180 if first_line && line_widths[0] != line_widths[1] {
181 words.insert(0, Word::from(""));
184 }
185
186 let mut idx = start;
187 for words in WrapAlgorithm::new().wrap(&words, line_widths) {
188 let last_word = match words.last() {
189 Some(word) => word,
190 None => {
191 lines.push(idx..idx);
192 continue;
193 }
194 };
195
196 debug_assert!(last_word.penalty.is_empty());
199
200 let len = words
201 .iter()
202 .map(|word| word.len() + word.whitespace.len())
203 .sum::<usize>();
204
205 lines.push(idx..idx + len - last_word.whitespace.len());
206 idx += len;
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use crate::{backend::TestBackend, test_consts::*};
213
214 use super::*;
215
216 #[test]
217 fn test_wrap() {
218 fn test(text: &str, indent: usize, max_width: usize, nlines: usize) {
219 let layout = Layout::new(indent as u16, (max_width as u16, 100).into());
220 let mut wrapped = Vec::new();
221 wrap(text, layout, &mut wrapped);
222
223 let indent_str = " ".repeat(indent);
225 let filled = textwrap::fill(
226 text,
227 textwrap::Options::new(max_width).initial_indent(&indent_str),
228 );
229 let expected: Vec<_> = filled[indent..].lines().collect();
230 let actual: Vec<_> = wrapped.iter().map(|r| &text[r.clone()]).collect();
231 assert_eq!(expected, actual);
232
233 assert_eq!(nlines, wrapped.len());
234 let mut lines = actual.into_iter();
235
236 assert!(lines.next().unwrap().chars().count() <= max_width - indent);
237
238 for line in lines {
239 assert!(line.chars().count() <= max_width);
240 }
241 }
242
243 test("Hello World", 0, 80, 1);
244
245 test("Hello World", 0, 6, 2);
246
247 test(LOREM, 40, 80, 7);
248 test(UNICODE, 40, 80, 7);
249
250 test("Hello\n\nWorld \n", 0, 80, 3);
251 test("Hello\r\nWorld", 3, 80, 2);
252 test(
253 "a-very-long-hyphenated-word and a supercalifragilisticexpialidocious one",
254 5,
255 12,
256 8,
257 );
258 }
259
260 #[test]
261 fn test_text_height() {
262 let mut layout = Layout::new(40, (80, 100).into());
263 let mut text = Text::new(LOREM);
264
265 assert_eq!(text.max_height(layout), 7);
266 assert_eq!(text.height(&mut layout.with_max_height(5)), 5);
267 layout.line_offset = 0;
268 layout.width = 110;
269 assert_eq!(text.height(&mut layout.clone()), text.max_height(layout));
270 assert_eq!(text.height(&mut layout.clone()), 5);
271
272 let mut layout = Layout::new(40, (80, 100).into());
273 let mut text = Text::new(UNICODE);
274
275 assert_eq!(text.max_height(layout), 7);
276 assert_eq!(text.height(&mut layout.with_max_height(5)), 5);
277 layout.line_offset = 0;
278 layout.width = 110;
279 assert_eq!(text.height(&mut layout.clone()), text.max_height(layout));
280 assert_eq!(text.height(&mut layout.clone()), 5);
281 }
282
283 #[test]
284 fn test_render_single_line() {
285 let size = (100, 20).into();
286 let mut layout = Layout::new(0, size);
287 let mut backend = TestBackend::new(size);
288
289 let mut text = Text::new("Hello, World!");
290 text.render(&mut layout, &mut backend).unwrap();
291
292 crate::assert_backend_snapshot!(backend);
293 assert_eq!(layout, layout.with_offset(0, 1));
294 }
295
296 #[test]
297 fn test_render_multiline() {
298 let size = (100, 20).into();
299 let mut layout = Layout::new(0, size);
300
301 let mut backend = TestBackend::new(size);
302 let mut text = Text::new(LOREM);
303 text.render(&mut layout, &mut backend).unwrap();
304
305 crate::assert_backend_snapshot!(backend);
306 assert_eq!(layout, Layout::new(0, size).with_offset(0, 5));
307
308 layout = Layout::new(0, size).with_offset(10, 10);
309 backend.reset_with_layout(layout);
310
311 let mut text = Text::new(UNICODE);
312 text.render(&mut layout, &mut backend).unwrap();
313
314 crate::assert_backend_snapshot!(backend);
315 assert_eq!(layout, Layout::new(0, size).with_offset(10, 16));
316 }
317}