Skip to main content

retroglyph_widgets/widget/
print_line.rs

1//! [`PrintLine`]: a single styled [`Line`].
2use retroglyph_core::Rect;
3use retroglyph_core::text::Line;
4use unicode_width::UnicodeWidthStr;
5
6use super::Widget;
7use crate::Align;
8use crate::Surface;
9use crate::text::truncate as truncate_to_cols;
10
11/// A [`Line`], drawn on the first row of the area it's rendered into and
12/// clipped to `area.width()` columns. Only the first row is used.
13///
14/// `align` defaults to [`Align::Left`] (drawn at the left edge); set it with
15/// [`PrintLine::align`] to right-align or center the whole line's spans as a
16/// unit within `area.width()` columns.
17///
18/// # Examples
19///
20/// ```
21/// use retroglyph_core::backend::Headless;
22/// use retroglyph_core::text::Line;
23/// use retroglyph_core::{Rect, Terminal};
24/// use retroglyph_widgets::{PrintLine, Widget};
25///
26/// let mut term = Terminal::new(Headless::new(20, 1));
27/// let line = Line::raw("hello");
28/// term.draw(|surface| {
29///     PrintLine::new(&line).render(Rect::new(0, 0, 20, 1), surface);
30/// })
31/// .unwrap();
32/// ```
33#[derive(Clone, Copy, Debug)]
34pub struct PrintLine<'a> {
35    line: &'a Line,
36    align: Align,
37}
38
39impl<'a> PrintLine<'a> {
40    /// Print `line`, left-aligned and clipped to whatever width it's rendered
41    /// at.
42    #[must_use]
43    pub const fn new(line: &'a Line) -> Self {
44        Self {
45            line,
46            align: Align::Left,
47        }
48    }
49
50    /// Set how the line's spans are aligned, as a unit, within `area.width()`
51    /// columns.
52    #[must_use]
53    pub const fn align(mut self, align: Align) -> Self {
54        self.align = align;
55        self
56    }
57}
58
59impl Widget for PrintLine<'_> {
60    fn render(&self, area: Rect, surface: &mut Surface<'_>) {
61        let max_width = area.width();
62        let right = area.left() + max_width;
63        // Align the whole line as a unit: sum the spans' display widths
64        // (clamped to the area) and offset the start column accordingly.
65        // A single span wider than `u16::MAX` columns would already be unaddressable in this
66        // crate's `u16` coordinate space; the running total still saturates rather than
67        // overflowing even if one span's cast wraps.
68        #[allow(clippy::cast_possible_truncation)]
69        let line_width = self
70            .line
71            .spans
72            .iter()
73            .fold(0u16, |acc, s| acc.saturating_add(s.content.width() as u16))
74            .min(max_width);
75        let mut x = area.left() + self.align.offset(max_width, line_width);
76        for span in &self.line.spans {
77            if x >= right {
78                break;
79            }
80            let remaining = (right - x) as usize;
81            let text = truncate_to_cols(&span.content, remaining);
82            surface.print((x, area.top()), text, span.style);
83            // `text` is bounded to `remaining` columns above, itself derived from the `u16`
84            // `right`/`x`, so narrowing its display width back is always exact.
85            #[allow(clippy::cast_possible_truncation)]
86            let text_w = text.width() as u16;
87            x += text_w;
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use retroglyph_core::text::Span;
95    use retroglyph_core::{Grid, Pos};
96
97    use super::*;
98
99    #[test]
100    fn prints_every_span() {
101        let line = Line::from(vec![Span::raw("hi "), Span::raw("there")]);
102        let area = Rect::new(0, 0, 20, 1);
103        let mut grid = Grid::new(20, 1);
104        PrintLine::new(&line).render(area, &mut Surface::new(&mut grid, area, 0));
105
106        let row: String = (0..20).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
107        assert!(row.starts_with("hi there"));
108    }
109
110    #[test]
111    fn right_align_places_the_whole_line_against_the_right_edge() {
112        let line = Line::from(vec![Span::raw("hi "), Span::raw("there")]);
113        let area = Rect::new(0, 0, 20, 1);
114        let mut grid = Grid::new(20, 1);
115        PrintLine::new(&line)
116            .align(Align::Right)
117            .render(area, &mut Surface::new(&mut grid, area, 0));
118
119        // "hi there" is 8 cols; right-aligned in 20 it ends at column 19.
120        let row: String = (0..20).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
121        assert!(row.ends_with("hi there"), "row was {row:?}");
122    }
123
124    #[test]
125    fn clips_to_max_width() {
126        let line = Line::raw("a much longer message than fits");
127        let area = Rect::new(0, 0, 5, 1);
128        let mut grid = Grid::new(5, 1);
129        PrintLine::new(&line).render(area, &mut Surface::new(&mut grid, area, 0));
130
131        // "a much longer..." clipped to 5 columns is "a muc".
132        assert_eq!(grid[Pos::new(4, 0)].glyph(), 'c');
133    }
134}