fast_rich/
renderable.rs

1//! The Renderable trait defines objects that can be rendered to the console.
2//!
3//! This is the core protocol for rich terminal output, similar to Rich's Console Protocol.
4
5use crate::console::RenderContext;
6use crate::text::Span;
7
8/// A segment of renderable output.
9#[derive(Debug, Clone)]
10pub struct Segment {
11    /// The styled spans that make up this segment
12    pub spans: Vec<Span>,
13    /// Whether this segment ends with a newline
14    pub newline: bool,
15}
16
17impl Segment {
18    /// Create a new segment without a newline.
19    pub fn new(spans: Vec<Span>) -> Self {
20        Segment {
21            spans,
22            newline: false,
23        }
24    }
25
26    /// Create a new segment with a newline at the end.
27    pub fn line(spans: Vec<Span>) -> Self {
28        Segment {
29            spans,
30            newline: true,
31        }
32    }
33
34    /// Create an empty line segment.
35    pub fn empty_line() -> Self {
36        Segment {
37            spans: Vec::new(),
38            newline: true,
39        }
40    }
41
42    /// Create a segment from a single span.
43    pub fn from_span(span: Span) -> Self {
44        Segment {
45            spans: vec![span],
46            newline: false,
47        }
48    }
49
50    /// Get the total display width of this segment.
51    pub fn width(&self) -> usize {
52        self.spans.iter().map(|s| s.width()).sum()
53    }
54
55    /// Get the plain text content.
56    pub fn plain_text(&self) -> String {
57        self.spans.iter().map(|s| s.text.as_ref()).collect()
58    }
59}
60
61/// Trait for objects that can be rendered to the console.
62///
63/// This is the core abstraction for renderable content, similar to Rich's
64/// `__rich_console__` protocol.
65pub trait Renderable {
66    /// Render this object to a sequence of segments.
67    ///
68    /// The `context` provides information about the rendering environment,
69    /// such as available width.
70    fn render(&self, context: &RenderContext) -> Vec<Segment>;
71
72    /// Get the minimum width required to render this object.
73    fn min_width(&self) -> usize {
74        1
75    }
76
77    /// Get the maximum/natural width of this object.
78    fn max_width(&self) -> usize {
79        usize::MAX
80    }
81}
82
83/// Implement Renderable for String.
84impl Renderable for String {
85    fn render(&self, _context: &RenderContext) -> Vec<Segment> {
86        vec![Segment::new(vec![Span::raw(self.clone())])]
87    }
88
89    fn max_width(&self) -> usize {
90        unicode_width::UnicodeWidthStr::width(self.as_str())
91    }
92}
93
94/// Implement Renderable for &str.
95impl Renderable for &str {
96    fn render(&self, _context: &RenderContext) -> Vec<Segment> {
97        vec![Segment::new(vec![Span::raw(self.to_string())])]
98    }
99
100    fn max_width(&self) -> usize {
101        unicode_width::UnicodeWidthStr::width(*self)
102    }
103}
104
105/// Implement Renderable for Text.
106impl Renderable for crate::text::Text {
107    fn render(&self, context: &RenderContext) -> Vec<Segment> {
108        let lines = self.wrap(context.width);
109        lines
110            .into_iter()
111            .map(|line| {
112                let aligned = self.align_line(line, context.width);
113                Segment::line(aligned)
114            })
115            .collect()
116    }
117
118    fn min_width(&self) -> usize {
119        // Minimum width is the longest word
120        self.spans
121            .iter()
122            .flat_map(|s| s.text.split_whitespace())
123            .map(unicode_width::UnicodeWidthStr::width)
124            .max()
125            .unwrap_or(1)
126    }
127
128    fn max_width(&self) -> usize {
129        self.width()
130    }
131}
132
133/// A boxed renderable for dynamic dispatch.
134pub type BoxedRenderable = Box<dyn Renderable + Send + Sync>;
135
136impl Renderable for BoxedRenderable {
137    fn render(&self, context: &RenderContext) -> Vec<Segment> {
138        (**self).render(context)
139    }
140
141    fn min_width(&self) -> usize {
142        (**self).min_width()
143    }
144
145    fn max_width(&self) -> usize {
146        (**self).max_width()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_segment_width() {
156        let segment = Segment::new(vec![Span::raw("hello")]);
157        assert_eq!(segment.width(), 5);
158    }
159
160    #[test]
161    fn test_segment_plain_text() {
162        let segment = Segment::new(vec![Span::raw("hello"), Span::raw(" world")]);
163        assert_eq!(segment.plain_text(), "hello world");
164    }
165
166    #[test]
167    fn test_string_renderable() {
168        let s = "Hello, World!".to_string();
169        let context = RenderContext {
170            width: 80,
171            height: None,
172        };
173        let segments = s.render(&context);
174        assert_eq!(segments.len(), 1);
175        assert_eq!(segments[0].plain_text(), "Hello, World!");
176    }
177}