requestty_ui/
text.rs

1use crate::{backend, layout::Layout, Widget};
2
3/// A string that can render over multiple lines.
4///
5/// If you need to render a single line of text or you don't want the text to wrap, use the [`Widget`]
6/// implementation on [`str`].
7#[derive(Debug, Clone)]
8pub struct Text<S> {
9    /// The text to render.
10    ///
11    /// If this is changed, the updated text is not guaranteed to be rendered. If the text is
12    /// changed, [`force_recompute`](Text::force_recompute) should be called.
13    pub text: S,
14    // FIXME: currently textwrap doesn't provide a way to find the locations at which the text
15    // should be split. Using that will be much more efficient than essentially duplicating the
16    // string.
17    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    /// Creates a new `Text`
32    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    /// The computed lines are cached between renders, and are only recomputed if the layout changes.
42    /// This will force a recomputation even if the layout is the same. This is useful if you need
43    /// to change the text.
44    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    /// Renders the Text moving to the next line after its done. This can trigger a recomputation.
64    /// In case the text cannot be fully rendered, [`layout.render_region`] is used to determine the
65    /// lines which are rendered.
66    ///
67    /// [`layout.render_region`]: crate::layout::Layout::render_region
68    fn render<B: backend::Backend>(
69        &mut self,
70        layout: &mut Layout,
71        backend: &mut B,
72    ) -> std::io::Result<()> {
73        // Update just in case the layout is out of date
74        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            // note: it may be possible to render things after the end of the last line, but for now
96            // we ignore that space and the text takes all the width.
97            layout.offset_y += nlines;
98        }
99        layout.line_offset = 0;
100
101        Ok(())
102    }
103
104    /// Calculates the height the text will take. This can trigger a recomputation.
105    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    /// Returns the location of the first character
112    fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
113        layout.offset_cursor((layout.line_offset, 0))
114    }
115
116    /// This widget does not handle any events
117    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
134// 200 spaces to remove allocation for indent
135static SPACES: &str = "                                                                                                                                                                                                        ";
136
137fn fill(text: &str, layout: Layout) -> String {
138    // This won't allocate until the **highly unlikely** case that there is a line
139    // offset of more than 200.
140    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}