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, Renderable, RenderResult};
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 {
142            items: Vec::new(),
143        }
144    }
145
146    /// Add a renderable to the container.
147    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) -> &mut Self {
148        self.items.push(DynRenderable::new(renderable));
149        self
150    }
151}
152
153impl Renderable for Renderables {
154    fn render(&self, options: &ConsoleOptions) -> RenderResult {
155        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
156        for item in &self.items {
157            let result = item.render(options);
158            all_lines.extend(result.lines);
159        }
160        RenderResult {
161            lines: all_lines,
162            items: Vec::new(),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::console::ConsoleOptions;
171
172    #[test]
173    fn test_lines_empty() {
174        let lines = Lines::new();
175        let opts = ConsoleOptions::default();
176        let result = lines.render(&opts);
177        assert!(result.lines.is_empty());
178    }
179
180    #[test]
181    fn test_lines_with_content() {
182        let mut lines = Lines::new();
183        lines.add("Hello");
184        lines.add("World");
185        let opts = ConsoleOptions::default();
186        let result = lines.render(&opts);
187        assert_eq!(result.lines.len(), 2);
188    }
189
190    #[test]
191    fn test_lines_highlight() {
192        let mut lines = Lines::new().highlight(1);
193        lines.add("First");
194        lines.add("Highlighted");
195        lines.add("Third");
196        let opts = ConsoleOptions::default();
197        let result = lines.render(&opts);
198        assert_eq!(result.lines.len(), 3);
199    }
200
201    #[test]
202    fn test_renderables_empty() {
203        let items = Renderables::new();
204        let opts = ConsoleOptions::default();
205        let result = items.render(&opts);
206        assert!(result.lines.is_empty());
207    }
208
209    #[test]
210    fn test_renderables_with_content() {
211        let mut items = Renderables::new();
212        items.add("A");
213        items.add("B");
214        items.add("C");
215        let opts = ConsoleOptions::default();
216        let result = items.render(&opts);
217        assert_eq!(result.lines.len(), 3);
218    }
219}