Skip to main content

rusty_rich/
containers.rs

1//! Container renderables — collections of lines and renderables.
2//!
3//! Provides [`Lines`] for rendering a collection of lines with optional
4//! highlight support, and [`Renderables`] for rendering a sequence of
5//! independent renderables one after another.
6
7use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
8use crate::segment::Segment;
9use crate::style::Style;
10
11// ---------------------------------------------------------------------------
12// Lines
13// ---------------------------------------------------------------------------
14
15/// A collection of lines (each line is a [`Renderable`]).
16///
17/// Each item in [`Lines`] is rendered as a single line of output. The
18/// `highlight` option applies a style to a specific 0-indexed line.
19///
20/// # Example
21///
22/// ```rust
23/// use rusty_rich::Lines;
24///
25/// let mut lines = Lines::new();
26/// lines.add("First line");
27/// lines.add("Second line");
28/// ```
29#[derive(Debug, Clone)]
30pub struct Lines {
31    lines: Vec<DynRenderable>,
32    highlight: Option<usize>,
33    style: Style,
34}
35
36impl Default for Lines {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Lines {
43    /// Create a new empty [`Lines`] container.
44    pub fn new() -> Self {
45        Self {
46            lines: Vec::new(),
47            highlight: None,
48            style: Style::new(),
49        }
50    }
51
52    /// Add a renderable line to the container.
53    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) -> &mut Self {
54        self.lines.push(DynRenderable::new(renderable));
55        self
56    }
57
58    /// Builder: highlight the line at the given 0-based index.
59    pub fn highlight(mut self, index: usize) -> Self {
60        self.highlight = Some(index);
61        self
62    }
63
64    /// Builder: set the default style for all lines.
65    pub fn style(mut self, style: Style) -> Self {
66        self.style = style;
67        self
68    }
69}
70
71impl Renderable for Lines {
72    fn render(&self, options: &ConsoleOptions) -> RenderResult {
73        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
74
75        for (i, item) in self.lines.iter().enumerate() {
76            let mut result = item.render(options);
77
78            // Apply highlight style to the highlighted line
79            if Some(i) == self.highlight {
80                for line in &mut result.lines {
81                    for seg in line.iter_mut() {
82                        if let Some(ref existing) = seg.style {
83                            seg.style = Some(existing.clone().bold(true));
84                        } else {
85                            seg.style = Some(self.style.clone().bold(true));
86                        }
87                    }
88                }
89            } else if !self.style.is_plain() {
90                for line in &mut result.lines {
91                    for seg in line.iter_mut() {
92                        if seg.style.is_none() {
93                            seg.style = Some(self.style.clone());
94                        }
95                    }
96                }
97            }
98
99            all_lines.extend(result.lines);
100        }
101
102        RenderResult {
103            lines: all_lines,
104            items: Vec::new(),
105        }
106    }
107}
108
109// ---------------------------------------------------------------------------
110// Renderables
111// ---------------------------------------------------------------------------
112
113/// A flexible container for multiple renderables.
114///
115/// Renders each contained renderable in sequence, concatenating their
116/// output lines.
117///
118/// # Example
119///
120/// ```rust
121/// use rusty_rich::Renderables;
122///
123/// let mut items = Renderables::new();
124/// items.add("First item");
125/// items.add("Second item");
126/// ```
127#[derive(Debug, Clone)]
128pub struct Renderables {
129    items: Vec<DynRenderable>,
130}
131
132impl Default for Renderables {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl Renderables {
139    /// Create a new empty [`Renderables`] container.
140    pub fn new() -> Self {
141        Self { items: Vec::new() }
142    }
143
144    /// Add a renderable to the container.
145    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) -> &mut Self {
146        self.items.push(DynRenderable::new(renderable));
147        self
148    }
149}
150
151impl Renderable for Renderables {
152    fn render(&self, options: &ConsoleOptions) -> RenderResult {
153        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
154        for item in &self.items {
155            let result = item.render(options);
156            all_lines.extend(result.lines);
157        }
158        RenderResult {
159            lines: all_lines,
160            items: Vec::new(),
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::console::ConsoleOptions;
169
170    #[test]
171    fn test_lines_empty() {
172        let lines = Lines::new();
173        let opts = ConsoleOptions::default();
174        let result = lines.render(&opts);
175        assert!(result.lines.is_empty());
176    }
177
178    #[test]
179    fn test_lines_with_content() {
180        let mut lines = Lines::new();
181        lines.add("Hello");
182        lines.add("World");
183        let opts = ConsoleOptions::default();
184        let result = lines.render(&opts);
185        assert_eq!(result.lines.len(), 2);
186    }
187
188    #[test]
189    fn test_lines_highlight() {
190        let mut lines = Lines::new().highlight(1);
191        lines.add("First");
192        lines.add("Highlighted");
193        lines.add("Third");
194        let opts = ConsoleOptions::default();
195        let result = lines.render(&opts);
196        assert_eq!(result.lines.len(), 3);
197    }
198
199    #[test]
200    fn test_renderables_empty() {
201        let items = Renderables::new();
202        let opts = ConsoleOptions::default();
203        let result = items.render(&opts);
204        assert!(result.lines.is_empty());
205    }
206
207    #[test]
208    fn test_renderables_with_content() {
209        let mut items = Renderables::new();
210        items.add("A");
211        items.add("B");
212        items.add("C");
213        let opts = ConsoleOptions::default();
214        let result = items.render(&opts);
215        assert_eq!(result.lines.len(), 3);
216    }
217}