Skip to main content

retroglyph_widgets/widget/
text.rs

1//! [`Text`]: a single line of plain text in one [`Style`].
2use retroglyph_core::{Rect, Style};
3use unicode_width::UnicodeWidthStr;
4
5use super::Widget;
6use crate::Align;
7use crate::Surface;
8use crate::text::truncate as truncate_to_cols;
9
10/// A single line of text in one [`Style`], clipped (not wrapped) to
11/// `area.width()` columns. Only the first row of `area` is used.
12///
13/// The plain-content cousin of [`PrintLine`](super::PrintLine) (which
14/// prints a multi-span [`Line`](retroglyph_core::text::Line), for mixed
15/// styling within one line) and [`Paragraph`](super::Paragraph) (which
16/// word-wraps across multiple lines, and needs the `egc` feature): reach
17/// for `Text` for a single already-one-line label or readout in a single
18/// style, with no wrapping and no per-span styling. `style` defaults to
19/// [`Style::new()`] and `align` to [`Align::Left`]; set them with
20/// [`Text::style`]/[`Text::align`].
21///
22/// # Examples
23///
24/// ```
25/// use retroglyph_core::{Grid, Rect};
26/// use retroglyph_widgets::{Align, Surface, Text, Widget};
27///
28/// let area = Rect::new(0, 0, 10, 1);
29/// let mut grid = Grid::new(10, 1);
30/// Text::new("OK")
31///     .align(Align::Right)
32///     .render(area, &mut Surface::new(&mut grid, area, 0));
33/// ```
34#[derive(Clone, Copy, Debug)]
35pub struct Text<'a> {
36    content: &'a str,
37    style: Style,
38    align: Align,
39}
40
41impl<'a> Text<'a> {
42    /// A line of `content` in the default style, left-aligned.
43    #[must_use]
44    pub fn new(content: &'a str) -> Self {
45        Self {
46            content,
47            style: Style::new(),
48            align: Align::Left,
49        }
50    }
51
52    /// Set the text's style.
53    #[must_use]
54    pub const fn style(mut self, style: Style) -> Self {
55        self.style = style;
56        self
57    }
58
59    /// Set how the line is aligned within `area.width()` columns.
60    #[must_use]
61    pub const fn align(mut self, align: Align) -> Self {
62        self.align = align;
63        self
64    }
65}
66
67impl Widget for Text<'_> {
68    fn render(&self, area: Rect, surface: &mut Surface<'_>) {
69        if area.width() == 0 {
70            return;
71        }
72        let text = truncate_to_cols(self.content, area.width_usize());
73        // `text` is bounded to `area.width_usize()` columns above, itself widened from `area`'s
74        // own `u16` width, so narrowing the display width back is always exact.
75        #[allow(clippy::cast_possible_truncation)]
76        let text_width = text.width() as u16;
77        let x = area.left() + self.align.offset(area.width(), text_width);
78        surface.print((x, area.top()), text, self.style);
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use retroglyph_core::{Color, Grid, Pos};
85
86    use super::*;
87
88    #[test]
89    fn prints_the_content_in_the_given_style() {
90        let area = Rect::new(0, 0, 10, 1);
91        let mut grid = Grid::new(10, 1);
92        Text::new("hi")
93            .style(Style::new().fg(Color::WHITE))
94            .render(area, &mut Surface::new(&mut grid, area, 0));
95
96        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
97        assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
98        assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::WHITE);
99    }
100
101    #[test]
102    fn clips_to_area_width() {
103        let area = Rect::new(0, 0, 5, 1);
104        let mut grid = Grid::new(5, 1);
105        Text::new("a much longer message than fits")
106            .render(area, &mut Surface::new(&mut grid, area, 0));
107
108        assert_eq!(grid[Pos::new(4, 0)].glyph(), 'c'); // "a muc"
109    }
110
111    #[test]
112    fn right_align_places_text_against_the_right_edge() {
113        let area = Rect::new(0, 0, 10, 1);
114        let mut grid = Grid::new(10, 1);
115        Text::new("hi")
116            .align(Align::Right)
117            .render(area, &mut Surface::new(&mut grid, area, 0));
118
119        // "hi" (2 cols) in 10 cols, right-aligned: starts at column 8.
120        assert_eq!(grid[Pos::new(8, 0)].glyph(), 'h');
121        assert_eq!(grid[Pos::new(9, 0)].glyph(), 'i');
122        assert_eq!(grid[Pos::new(7, 0)].glyph(), ' ');
123    }
124
125    #[test]
126    fn center_align_centers_text() {
127        let area = Rect::new(0, 0, 10, 1);
128        let mut grid = Grid::new(10, 1);
129        Text::new("hi")
130            .align(Align::Center)
131            .render(area, &mut Surface::new(&mut grid, area, 0));
132
133        // 8 cols slack, 4 on the left: "hi" starts at column 4.
134        assert_eq!(grid[Pos::new(4, 0)].glyph(), 'h');
135        assert_eq!(grid[Pos::new(5, 0)].glyph(), 'i');
136    }
137
138    #[test]
139    fn zero_width_is_a_no_op() {
140        let area = Rect::new(0, 0, 0, 1);
141        let mut grid = Grid::new(1, 1);
142        Text::new("hi").render(area, &mut Surface::new(&mut grid, area, 0));
143        assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
144    }
145}