Skip to main content

retroglyph_widgets/widget/
paragraph.rs

1//! [`Paragraph`]: word-wrapped text, implementing both [`Widget`] and
2//! [`Measure`] so a caller can size a pane to fit before rendering.
3//!
4//! Requires the `egc` feature: wrapping is delegated entirely to
5//! [`retroglyph_core::layout::TextLayout`], which handles grapheme clusters
6//! and hard newlines correctly. This module adds no wrapping logic of its
7//! own: see `crates/widgets/src/text.rs` for why that duplication was
8//! removed.
9use retroglyph_core::layout::TextLayout;
10use retroglyph_core::text::{Line, Span};
11use retroglyph_core::{Rect, Style};
12
13use super::{Measure, Widget};
14use crate::Surface;
15
16/// Word-wrapped text in a single [`Style`].
17///
18/// `Paragraph::new(text)` wraps `text` to whatever width it is rendered at
19/// (via [`Widget::render`]), or reports the height it would need at a
20/// given width without rendering (via [`Measure::height_for`]) so a caller
21/// can size its pane to fit instead of guessing a fixed height. `style`
22/// defaults to [`Style::new()`]; set it with [`Paragraph::style`].
23///
24/// # Examples
25///
26/// ```
27/// use retroglyph_core::{Grid, Rect};
28/// use retroglyph_widgets::{Measure, Paragraph, Surface, Widget};
29///
30/// let p = Paragraph::new("the quick brown fox jumps");
31/// let height = p.height_for(10); // rows needed to wrap at 10 columns
32///
33/// let area = Rect::new(0, 0, 10, height);
34/// let mut grid = Grid::new(10, height);
35/// p.render(area, &mut Surface::new(&mut grid, area, 0));
36/// ```
37#[derive(Clone, Copy, Debug)]
38pub struct Paragraph<'a> {
39    text: &'a str,
40    style: Style,
41}
42
43impl<'a> Paragraph<'a> {
44    /// Text to be word-wrapped, in the default style.
45    #[must_use]
46    pub fn new(text: &'a str) -> Self {
47        Self {
48            text,
49            style: Style::new(),
50        }
51    }
52
53    /// Set the text's style.
54    #[must_use]
55    pub const fn style(mut self, style: Style) -> Self {
56        self.style = style;
57        self
58    }
59
60    fn line(&self) -> Line {
61        Line::from(Span::styled(self.text, self.style))
62    }
63}
64
65impl Measure for Paragraph<'_> {
66    fn height_for(&self, width: u16) -> u16 {
67        let line = self.line();
68        TextLayout::new(&line)
69            .rect(Rect::new(0, 0, width, u16::MAX))
70            .measure()
71            .height
72    }
73}
74
75impl Widget for Paragraph<'_> {
76    fn render(&self, area: Rect, surface: &mut Surface<'_>) {
77        let line = self.line();
78        let layer = surface.layer();
79        TextLayout::new(&line)
80            .rect(area)
81            .render_to_grid(surface.grid_mut(), layer);
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use retroglyph_core::{Grid, Pos};
88
89    use super::*;
90
91    #[test]
92    fn height_for_matches_wrapped_line_count() {
93        let p = Paragraph::new("the quick brown fox jumps");
94        assert_eq!(p.height_for(10), 3); // "the quick" / "brown fox" / "jumps"
95        assert_eq!(p.height_for(100), 1);
96    }
97
98    #[test]
99    fn height_for_respects_hard_newlines() {
100        // A naive whitespace-based wrap would flatten this to one paragraph;
101        // TextLayout treats "\n" as a hard break regardless of width.
102        let p = Paragraph::new("first\nsecond\nthird");
103        assert_eq!(p.height_for(100), 3);
104    }
105
106    #[test]
107    fn render_draws_one_line_per_wrapped_row() {
108        let area = Rect::new(0, 0, 10, 5);
109        let mut grid = Grid::new(10, 5);
110        Paragraph::new("the quick brown fox jumps")
111            .render(area, &mut Surface::new(&mut grid, area, 0));
112
113        let row0: String = (0..10).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
114        let row1: String = (0..10).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
115        let row2: String = (0..10).map(|x| grid[Pos::new(x, 2)].glyph()).collect();
116        assert!(row0.starts_with("the quick"));
117        assert!(row1.starts_with("brown fox"));
118        assert!(row2.starts_with("jumps"));
119    }
120
121    #[test]
122    fn render_stops_at_the_area_bottom() {
123        // Only 1 row of height: only the first wrapped line should draw.
124        let area = Rect::new(0, 0, 10, 1);
125        let mut grid = Grid::new(10, 2);
126        Paragraph::new("the quick brown fox jumps")
127            .render(area, &mut Surface::new(&mut grid, area, 0));
128
129        let row1: String = (0..10).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
130        assert_eq!(row1.trim(), "");
131    }
132}