Skip to main content

photon_ui/components/
text.rs

1use crate::{
2    Component,
3    RenderError,
4    Rendered,
5};
6
7/// Static text with optional horizontal and vertical padding.
8///
9/// Each line of the input text is prefixed with `pad_x` spaces, truncated to
10/// fit within the requested width, and padded with trailing spaces so that old
11/// terminal content on the same line is fully overwritten. `pad_y` empty lines
12/// are added before and after the content.
13pub struct Text {
14    text: String,
15    pad_x: u16,
16    pad_y: u16,
17}
18
19impl Text {
20    /// Create a new text component.
21    pub fn new(text: impl Into<String>, pad_x: u16, pad_y: u16) -> Self {
22        Self {
23            text: text.into(),
24            pad_x,
25            pad_y,
26        }
27    }
28}
29
30impl Component for Text {
31    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
32        let mut lines = Vec::new();
33        let empty_line = " ".repeat(width as usize);
34        for _ in 0..self.pad_y {
35            lines.push(empty_line.clone());
36        }
37        let pad = " ".repeat(self.pad_x as usize);
38        for line in self.text.lines() {
39            let padded = format!("{}{}", pad, line);
40            let truncated = crate::utils::truncate_to_width(&padded, width, "…");
41            let vw = crate::utils::visible_width(&truncated);
42            let mut final_line = truncated;
43            if vw < width as usize {
44                final_line.push_str(&" ".repeat(width as usize - vw));
45            }
46            lines.push(final_line);
47        }
48        for _ in 0..self.pad_y {
49            lines.push(empty_line.clone());
50        }
51        Ok(Rendered {
52            lines,
53            cursor: None,
54            images: Vec::new(),
55        })
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn text_truncates_to_width() {
65        let text = Text::new("hello world", 0, 0);
66        let rendered = text.render(5).unwrap();
67        assert_eq!(rendered.lines.len(), 1);
68        assert_eq!(rendered.lines[0].trim_end(), "hell…"); // ellipsis is 3 cols wide
69    }
70
71    #[test]
72    fn text_pads_to_width() {
73        let text = Text::new("hi", 0, 0);
74        let rendered = text.render(10).unwrap();
75        assert_eq!(rendered.lines.len(), 1);
76        assert_eq!(rendered.lines[0].len(), 10);
77        assert_eq!(rendered.lines[0], "hi        ");
78    }
79
80    #[test]
81    fn text_pad_y_adds_blank_lines() {
82        let text = Text::new("a", 0, 2);
83        let rendered = text.render(10).unwrap();
84        assert_eq!(rendered.lines.len(), 5); // 2 + 1 + 2
85        assert_eq!(rendered.lines[0], "          "); // padded to width
86        assert_eq!(rendered.lines[1], "          ");
87        assert!(rendered.lines[2].starts_with("a"));
88        assert_eq!(rendered.lines[3], "          ");
89        assert_eq!(rendered.lines[4], "          ");
90    }
91
92    #[test]
93    fn text_pad_x_prefixes_spaces() {
94        let text = Text::new("x", 3, 0);
95        let rendered = text.render(10).unwrap();
96        assert_eq!(rendered.lines[0], "   x      "); // 3 pad + 1 text + 6 spaces = 10
97    }
98
99    #[test]
100    fn text_long_line_with_pad_x_truncates() {
101        let text = Text::new("abcdefghij", 5, 0);
102        let rendered = text.render(10).unwrap();
103        // 5 spaces + 10 chars = 15, truncated to 10 with 3-col ellipsis
104        assert!(rendered.lines[0].starts_with("     ")); // 5 pad spaces preserved
105        assert!(rendered.lines[0].trim_end().ends_with("abcd…"));
106    }
107}