1use crate::{backend, layout::Layout, Widget};
2
3#[derive(Debug, Clone)]
8pub struct Text<S> {
9 pub text: S,
14 wrapped: String,
18 line_offset: u16,
19 width: u16,
20}
21
22impl<S: PartialEq> PartialEq for Text<S> {
23 fn eq(&self, other: &Self) -> bool {
24 self.text == other.text
25 }
26}
27
28impl<S: Eq> Eq for Text<S> {}
29
30impl<S: AsRef<str>> Text<S> {
31 pub fn new(text: S) -> Self {
33 Self {
34 text,
35 wrapped: String::new(),
36 width: 0,
37 line_offset: 0,
38 }
39 }
40
41 pub fn force_recompute(&mut self) {
45 self.line_offset = u16::MAX;
46 self.width = u16::MAX;
47 }
48
49 fn max_height(&mut self, layout: Layout) -> u16 {
50 let width = layout.available_width();
51
52 if self.width != width || self.line_offset != layout.line_offset {
53 self.wrapped = fill(self.text.as_ref(), layout);
54 self.width = width;
55 self.line_offset = layout.line_offset;
56 }
57
58 self.wrapped.lines().count() as u16
59 }
60}
61
62impl<S: AsRef<str>> Widget for Text<S> {
63 fn render<B: backend::Backend>(
69 &mut self,
70 layout: &mut Layout,
71 backend: &mut B,
72 ) -> std::io::Result<()> {
73 let height = self.max_height(*layout);
75
76 if height == 1 {
77 backend.write_all(self.wrapped.as_bytes())?;
78 layout.offset_y += 1;
79 backend.move_cursor_to(layout.offset_x, layout.offset_y)?;
80 } else {
81 let start = layout.get_start(height) as usize;
82 let nlines = height.min(layout.max_height);
83
84 for (i, line) in self
85 .wrapped
86 .lines()
87 .skip(start)
88 .take(nlines as usize)
89 .enumerate()
90 {
91 backend.write_all(line.as_bytes())?;
92 backend.move_cursor_to(layout.offset_x, layout.offset_y + i as u16 + 1)?;
93 }
94
95 layout.offset_y += nlines;
98 }
99 layout.line_offset = 0;
100
101 Ok(())
102 }
103
104 fn height(&mut self, layout: &mut Layout) -> u16 {
106 let height = self.max_height(*layout).min(layout.max_height);
107 layout.offset_y += height;
108 height
109 }
110
111 fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
113 layout.offset_cursor((layout.line_offset, 0))
114 }
115
116 fn handle_key(&mut self, _: crate::events::KeyEvent) -> bool {
118 false
119 }
120}
121
122impl<S: AsRef<str>> AsRef<str> for Text<S> {
123 fn as_ref(&self) -> &str {
124 self.text.as_ref()
125 }
126}
127
128impl<S: AsRef<str>> From<S> for Text<S> {
129 fn from(text: S) -> Self {
130 Self::new(text)
131 }
132}
133
134static SPACES: &str = " ";
136
137fn fill(text: &str, layout: Layout) -> String {
138 let s: String;
141
142 let indent_len = layout.line_offset as usize;
143
144 let indent = if SPACES.len() > indent_len {
145 &SPACES[..indent_len]
146 } else {
147 s = " ".repeat(indent_len);
148 &s[..]
149 };
150
151 let mut text = textwrap::fill(
152 text,
153 textwrap::Options::new(layout.available_width() as usize).initial_indent(indent),
154 );
155
156 drop(text.drain(..indent_len));
157
158 text
159}
160
161#[cfg(test)]
162mod tests {
163 use crate::{backend::TestBackend, test_consts::*};
164
165 use super::*;
166
167 #[test]
168 fn test_fill() {
169 fn test(text: &str, indent: usize, max_width: usize, nlines: usize) {
170 let layout = Layout::new(indent as u16, (max_width as u16, 100).into());
171 let filled = fill(text, layout);
172
173 assert_eq!(nlines, filled.lines().count());
174 let mut lines = filled.lines();
175
176 assert!(lines.next().unwrap().chars().count() <= max_width - indent);
177
178 for line in lines {
179 assert!(line.chars().count() <= max_width);
180 }
181 }
182
183 test("Hello World", 0, 80, 1);
184
185 test("Hello World", 0, 6, 2);
186
187 test(LOREM, 40, 80, 7);
188 test(UNICODE, 40, 80, 7);
189 }
190
191 #[test]
192 fn test_text_height() {
193 let mut layout = Layout::new(40, (80, 100).into());
194 let mut text = Text::new(LOREM);
195
196 assert_eq!(text.max_height(layout), 7);
197 assert_eq!(text.height(&mut layout.with_max_height(5)), 5);
198 layout.line_offset = 0;
199 layout.width = 110;
200 assert_eq!(text.height(&mut layout.clone()), text.max_height(layout));
201 assert_eq!(text.height(&mut layout.clone()), 5);
202
203 let mut layout = Layout::new(40, (80, 100).into());
204 let mut text = Text::new(UNICODE);
205
206 assert_eq!(text.max_height(layout), 7);
207 assert_eq!(text.height(&mut layout.with_max_height(5)), 5);
208 layout.line_offset = 0;
209 layout.width = 110;
210 assert_eq!(text.height(&mut layout.clone()), text.max_height(layout));
211 assert_eq!(text.height(&mut layout.clone()), 5);
212 }
213
214 #[test]
215 fn test_render_single_line() {
216 let size = (100, 20).into();
217 let mut layout = Layout::new(0, size);
218 let mut backend = TestBackend::new(size);
219
220 let mut text = Text::new("Hello, World!");
221 text.render(&mut layout, &mut backend).unwrap();
222
223 crate::assert_backend_snapshot!(backend);
224 assert_eq!(layout, layout.with_offset(0, 1));
225 }
226
227 #[test]
228 fn test_render_multiline() {
229 let size = (100, 20).into();
230 let mut layout = Layout::new(0, size);
231
232 let mut backend = TestBackend::new(size);
233 let mut text = Text::new(LOREM);
234 text.render(&mut layout, &mut backend).unwrap();
235
236 crate::assert_backend_snapshot!(backend);
237 assert_eq!(layout, Layout::new(0, size).with_offset(0, 5));
238
239 layout = Layout::new(0, size).with_offset(10, 10);
240 backend.reset_with_layout(layout);
241
242 let mut text = Text::new(UNICODE);
243 text.render(&mut layout, &mut backend).unwrap();
244
245 crate::assert_backend_snapshot!(backend);
246 assert_eq!(layout, Layout::new(0, size).with_offset(10, 16));
247 }
248}