Skip to main content

retroglyph_ui/widget/
print_line.rs

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