Skip to main content

retroglyph_widgets/widget/
text.rs

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