fast_rich/
group.rs

1//! Render groups for combining multiple renderables.
2//!
3//! Groups renderables vertically with optional spacing and dividers.
4
5use crate::console::RenderContext;
6use crate::renderable::{BoxedRenderable, Renderable, Segment};
7use crate::rule::Rule;
8use crate::text::Span;
9
10/// Fit strategy for renderables in a group.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Fit {
13    /// Each renderable uses the full width
14    Fill,
15    /// Each renderable uses minimum width
16    Shrink,
17}
18
19/// A group of renderables rendered vertically.
20pub struct RenderGroup {
21    renderables: Vec<BoxedRenderable>,
22    spacing: usize,
23    fit: Fit,
24    divider: Option<String>,
25}
26
27impl RenderGroup {
28    /// Create a new empty render group.
29    pub fn new() -> Self {
30        RenderGroup {
31            renderables: Vec::new(),
32            spacing: 0,
33            fit: Fit::Fill,
34            divider: None,
35        }
36    }
37
38    /// Add a renderable to the group.
39    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) -> &mut Self {
40        self.renderables.push(Box::new(renderable));
41        self
42    }
43
44    /// Set the spacing between renderables (in lines).
45    pub fn spacing(mut self, spacing: usize) -> Self {
46        self.spacing = spacing;
47        self
48    }
49
50    /// Set the fit strategy.
51    pub fn fit(mut self, fit: Fit) -> Self {
52        self.fit = fit;
53        self
54    }
55
56    /// Add a divider between renderables.
57    pub fn divider(mut self, divider: impl Into<String>) -> Self {
58        self.divider = Some(divider.into());
59        self
60    }
61
62    /// Create a group from a vector of renderables.
63    pub fn from_renderables(renderables: Vec<BoxedRenderable>) -> Self {
64        RenderGroup {
65            renderables,
66            spacing: 0,
67            fit: Fit::Fill,
68            divider: None,
69        }
70    }
71}
72
73impl Default for RenderGroup {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl Renderable for RenderGroup {
80    fn render(&self, context: &RenderContext) -> Vec<Segment> {
81        let mut result = Vec::new();
82
83        for (i, renderable) in self.renderables.iter().enumerate() {
84            // Render the item
85            let item_context = match self.fit {
86                Fit::Fill => context.clone(),
87                Fit::Shrink => RenderContext {
88                    width: renderable.min_width().min(context.width),
89                    height: None,
90                },
91            };
92
93            let segments = renderable.render(&item_context);
94            result.extend(segments);
95
96            // Add spacing or divider between items (but not after last)
97            if i < self.renderables.len() - 1 {
98                // Add divider if set
99                if let Some(ref divider_text) = self.divider {
100                    let rule = Rule::new(divider_text);
101                    let divider_segments = rule.render(context);
102                    result.extend(divider_segments);
103                }
104
105                // Add spacing
106                for _ in 0..self.spacing {
107                    result.push(Segment::line(vec![Span::raw(" ".repeat(context.width))]));
108                }
109            }
110        }
111
112        result
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::text::Text;
120
121    #[test]
122    fn test_render_group_creation() {
123        let group = RenderGroup::new();
124        assert_eq!(group.renderables.len(), 0);
125    }
126
127    #[test]
128    fn test_add_renderables() {
129        let mut group = RenderGroup::new();
130        group.add(Text::plain("First"));
131        group.add(Text::plain("Second"));
132
133        assert_eq!(group.renderables.len(), 2);
134    }
135
136    #[test]
137    fn test_spacing() {
138        let group = RenderGroup::new().spacing(2);
139        assert_eq!(group.spacing, 2);
140    }
141
142    #[test]
143    fn test_fit() {
144        let group = RenderGroup::new().fit(Fit::Shrink);
145        assert_eq!(group.fit, Fit::Shrink);
146    }
147
148    #[test]
149    fn test_divider() {
150        let group = RenderGroup::new().divider("---");
151        assert!(group.divider.is_some());
152    }
153}