Skip to main content

retroglyph_widgets/widget/
print_line.rs

1//! [`PrintLine`]: a single styled [`Line`].
2use retroglyph_core::text::Line;
3use retroglyph_core::{Backend, Rect, Terminal};
4use unicode_width::UnicodeWidthStr;
5
6use super::Widget;
7use crate::Align;
8use crate::text::truncate as truncate_to_cols;
9
10/// A [`Line`], drawn on the first row of the area it's rendered into and
11/// clipped to `area.width()` columns. Only the first row is used.
12///
13/// `align` defaults to [`Align::Left`] (drawn at the left edge); set it with
14/// [`PrintLine::align`] to right-align or center the whole line's spans as a
15/// unit within `area.width()` columns.
16#[derive(Clone, Copy, Debug)]
17pub struct PrintLine<'a> {
18    line: &'a Line,
19    align: Align,
20}
21
22impl<'a> PrintLine<'a> {
23    /// Print `line`, left-aligned and clipped to whatever width it's rendered
24    /// at.
25    #[must_use]
26    pub const fn new(line: &'a Line) -> Self {
27        Self {
28            line,
29            align: Align::Left,
30        }
31    }
32
33    /// Set how the line's spans are aligned, as a unit, within `area.width()`
34    /// columns.
35    #[must_use]
36    pub const fn align(mut self, align: Align) -> Self {
37        self.align = align;
38        self
39    }
40}
41
42impl<B: Backend> Widget<B> for PrintLine<'_> {
43    fn render(self, area: Rect, term: &mut Terminal<B>) {
44        let max_width = area.width();
45        let right = area.left() + max_width;
46        // Align the whole line as a unit: sum the spans' display widths
47        // (clamped to the area) and offset the start column accordingly.
48        let line_width = self
49            .line
50            .spans
51            .iter()
52            .fold(0u16, |acc, s| acc.saturating_add(s.content.width() as u16))
53            .min(max_width);
54        let mut x = area.left() + self.align.offset(max_width, line_width);
55        for span in &self.line.spans {
56            if x >= right {
57                break;
58            }
59            let remaining = (right - x) as usize;
60            let text = truncate_to_cols(&span.content, remaining);
61            term.reset_style()
62                .fg(span.style.foreground())
63                .bg(span.style.background());
64            term.print(x, area.top(), text);
65            x += text.width() as u16;
66        }
67        term.reset_style();
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use retroglyph_core::{Headless, text::Span};
74
75    use super::*;
76
77    #[test]
78    fn prints_every_span() {
79        let line = Line::from(vec![Span::raw("hi "), Span::raw("there")]);
80        let area = Rect::new(0, 0, 20, 1);
81        let mut term = Terminal::new(Headless::new(20, 1));
82        PrintLine::new(&line).render(area, &mut term);
83
84        let row: String = (0..20).map(|x| term.grid().get(x, 0).glyph()).collect();
85        assert!(row.starts_with("hi there"));
86    }
87
88    #[test]
89    fn right_align_places_the_whole_line_against_the_right_edge() {
90        let line = Line::from(vec![Span::raw("hi "), Span::raw("there")]);
91        let area = Rect::new(0, 0, 20, 1);
92        let mut term = Terminal::new(Headless::new(20, 1));
93        PrintLine::new(&line)
94            .align(Align::Right)
95            .render(area, &mut term);
96
97        // "hi there" is 8 cols; right-aligned in 20 it ends at column 19.
98        let row: String = (0..20).map(|x| term.grid().get(x, 0).glyph()).collect();
99        assert!(row.ends_with("hi there"), "row was {row:?}");
100    }
101
102    #[test]
103    fn clips_to_max_width() {
104        let line = Line::raw("a much longer message than fits");
105        let area = Rect::new(0, 0, 5, 1);
106        let mut term = Terminal::new(Headless::new(5, 1));
107        PrintLine::new(&line).render(area, &mut term);
108
109        // "a much longer..." clipped to 5 columns is "a muc".
110        assert_eq!(term.grid().get(4, 0).glyph(), 'c');
111    }
112}