iced_pancurses/renderer/
text.rs

1use crate::primitive::Primitive;
2use crate::renderer::PancursesRenderer;
3
4use iced_native::widget::text;
5use iced_native::{Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment};
6
7impl text::Renderer for PancursesRenderer {
8    fn default_size(&self) -> u16 {
9        1
10    }
11
12    fn measure(&self, content: &str, _size: u16, _font: Font, bounds: Size) -> (f32, f32) {
13        let content: String = content.into();
14        let max_x = bounds.width as u32;
15        let max_y = bounds.height as u32;
16        let layout = TextLayout::compute_layout(&content, max_x, max_y);
17        (layout.0 as f32, layout.1 as f32)
18    }
19
20    fn draw(
21        &mut self,
22        bounds: Rectangle,
23        content: &str,
24        _size: u16,
25        _font: Font,
26        color: Option<Color>,
27        horizontal_alignment: HorizontalAlignment,
28        _vertical_alignment: VerticalAlignment,
29    ) -> Self::Output {
30        let wrapped_content = TextLayout::wrap(
31            content,
32            bounds.width as u32,
33            bounds.height as u32,
34            horizontal_alignment,
35        );
36        Primitive::Text(wrapped_content, bounds, color.unwrap_or(Color::WHITE))
37    }
38}
39
40pub struct TextLayout;
41
42impl TextLayout {
43    /// Computes a correct layout size. This is the minimum size that the text component has to
44    /// take in order to be displayed correctly.
45    /// Wraps the text if it is bigger than the bounds.
46    pub fn compute_layout(content: &str, max_x: u32, max_y: u32) -> (u32, u32) {
47        // max_len should be length of the longest line in the content
48        let mut max_len = 0;
49
50        // Computes the number of lines after they've been wrapped
51        let lines: u32 = content
52            .lines()
53            .map(|l| {
54                let chars = l.chars().count() as u32;
55                max_len = max_len.max(chars);
56                let offset = if chars % max_x == 0 { 0 } else { 1 };
57                (chars / max_x) + offset
58            })
59            .sum();
60        (max_len.min(max_x), lines.min(max_y))
61    }
62
63    /// Compute lines as they should be displayed on the screen, given :
64    /// * The bounds of the text box (max_x, max_y)
65    /// * The Horizontal Alignement of a text
66    pub fn wrap(content: &str, max_x: u32, max_y: u32, align: HorizontalAlignment) -> Vec<String> {
67        let (wrapped_x, _) = TextLayout::compute_layout(content, max_x, max_y);
68        content
69            .lines()
70            .flat_map(|l| {
71                let len = l.chars().count() as u32;
72                if len > wrapped_x {
73                    l.as_bytes()
74                        .chunks(wrapped_x as usize)
75                        .map(|bytes| String::from_utf8(bytes.to_vec()).unwrap())
76                        .collect()
77                } else {
78                    let diff = wrapped_x - len;
79                    match align {
80                        HorizontalAlignment::Left => {
81                            let padding: String = (0..diff).map(|_| ' ').collect();
82                            vec![format!("{}{}", l, padding)]
83                        }
84                        HorizontalAlignment::Center => {
85                            let pad = diff / 2;
86                            let padding: String = (0..pad).map(|_| ' ').collect();
87                            let offset = if diff % 2 == 0 { "" } else { " " };
88                            vec![format!("{}{}{}{}", offset, padding, l, padding)]
89                        }
90                        HorizontalAlignment::Right => {
91                            let padding: String = (0..diff).map(|_| ' ').collect();
92                            vec![format!("{}{}", padding, l)]
93                        }
94                    }
95                }
96            })
97            .collect()
98    }
99}
100
101#[cfg(test)]
102pub mod tests {
103
104    use super::TextLayout;
105    use iced_native::HorizontalAlignment;
106
107    #[test]
108    pub fn text_layout_compute_should_work() {
109        let content = "First line\ntest!";
110        // This particular text should look like this in a terminal:
111        //
112        // First line
113        // test!
114        //
115        // This means that the size it should take on a (10, 2) or bigger is always (10, 2)
116        assert_eq!(TextLayout::compute_layout(content, 10, 2), (10, 2));
117        assert_eq!(TextLayout::compute_layout(content, 15, 3), (10, 2));
118    }
119
120    #[test]
121    pub fn text_layout_compute_should_wrap() {
122        let content = "First line\ntest!";
123        // Lets test the behaviour on smaller layout, and make the text wrap.
124        //
125        // On a (5, 10) box, the text should wrap as follows:
126        //
127        // First
128        // line
129        // test!
130        assert_eq!(TextLayout::compute_layout(content, 5, 10), (5, 3));
131
132        // On a (4, 10) box, the text should wrap as follows:
133        //
134        // Firs
135        // t li
136        // ne
137        // test
138        // !
139        assert_eq!(TextLayout::compute_layout(content, 4, 10), (4, 5));
140    }
141
142    #[test]
143    pub fn text_layout_wrap_should_work() {
144        let content = "First line\ntest!";
145
146        // Lets try normal layoung with Left alignment
147        assert_eq!(
148            TextLayout::wrap(content, 10, 2, HorizontalAlignment::Left),
149            vec!["First line", "test!     "]
150        );
151
152        // ... Center ...
153        assert_eq!(
154            TextLayout::wrap(content, 10, 2, HorizontalAlignment::Center),
155            vec!["First line", "   test!  "]
156        );
157
158        // ... and Right
159        assert_eq!(
160            TextLayout::wrap(content, 10, 2, HorizontalAlignment::Right),
161            vec!["First line", "     test!"]
162        )
163    }
164}