radicle_term/
vstack.rs

1use crate::colors;
2use crate::{Color, Constraint, Element, Label, Line, Paint, Size};
3
4/// Options for [`VStack`].
5#[derive(Debug)]
6pub struct VStackOptions {
7    border: Option<Color>,
8    padding: usize,
9}
10
11impl Default for VStackOptions {
12    fn default() -> Self {
13        Self {
14            border: None,
15            padding: 1,
16        }
17    }
18}
19
20/// A vertical stack row.
21#[derive(Default, Debug)]
22enum Row<'a> {
23    Element(Box<dyn Element + 'a>),
24    #[default]
25    Dividier,
26}
27
28impl<'a> Row<'a> {
29    fn width(&self, c: Constraint) -> usize {
30        match self {
31            Self::Element(e) => e.columns(c),
32            Self::Dividier => c.min.cols,
33        }
34    }
35
36    fn height(&self, c: Constraint) -> usize {
37        match self {
38            Self::Element(e) => e.rows(c),
39            Self::Dividier => 1,
40        }
41    }
42}
43
44/// Vertical stack of [`Element`] objects that implements [`Element`].
45#[derive(Default, Debug)]
46pub struct VStack<'a> {
47    rows: Vec<Row<'a>>,
48    opts: VStackOptions,
49}
50
51impl<'a> VStack<'a> {
52    /// Add an element to the stack and return the stack.
53    pub fn child(mut self, child: impl Element + 'a) -> Self {
54        self.push(child);
55        self
56    }
57
58    /// Add a blank line to the stack.
59    pub fn blank(self) -> Self {
60        self.child(Label::blank())
61    }
62
63    /// Add a horizontal divider.
64    pub fn divider(mut self) -> Self {
65        self.rows.push(Row::Dividier);
66        self
67    }
68
69    /// Check if this stack is empty.
70    pub fn is_empty(&self) -> bool {
71        self.rows.is_empty()
72    }
73
74    /// Add multiple elements to the stack.
75    pub fn children<I>(self, children: I) -> Self
76    where
77        I: IntoIterator<Item = Box<dyn Element>>,
78    {
79        let mut vstack = self;
80
81        for child in children.into_iter() {
82            vstack = vstack.child(child);
83        }
84        vstack
85    }
86
87    /// Merge with another `VStack`.
88    pub fn merge(mut self, other: Self) -> Self {
89        for row in other.rows {
90            self.rows.push(row);
91        }
92        self
93    }
94
95    /// Set or unset the outer border.
96    pub fn border(mut self, color: Option<Color>) -> Self {
97        self.opts.border = color;
98        self
99    }
100
101    /// Set horizontal padding.
102    pub fn padding(mut self, cols: usize) -> Self {
103        self.opts.padding = cols;
104        self
105    }
106
107    /// Add an element to the stack.
108    pub fn push(&mut self, child: impl Element + 'a) {
109        self.rows.push(Row::Element(Box::new(child)));
110    }
111
112    /// Box this element.
113    pub fn boxed(self) -> Box<dyn Element + 'a> {
114        Box::new(self)
115    }
116
117    /// Inner size.
118    fn inner(&self, c: Constraint) -> Size {
119        let mut outer = self.outer(c);
120
121        if self.opts.border.is_some() {
122            outer.cols -= 2;
123            outer.rows -= 2;
124        }
125        outer
126    }
127
128    /// Outer size (includes borders).
129    fn outer(&self, c: Constraint) -> Size {
130        let padding = self.opts.padding * 2;
131        let mut cols = self.rows.iter().map(|r| r.width(c)).max().unwrap_or(0) + padding;
132        let mut rows = self.rows.iter().map(|r| r.height(c)).sum();
133
134        // Account for outer borders.
135        if self.opts.border.is_some() {
136            cols += 2;
137            rows += 2;
138        }
139        Size::new(cols, rows).constrain(c)
140    }
141}
142
143impl<'a> Element for VStack<'a> {
144    fn size(&self, parent: Constraint) -> Size {
145        self.outer(parent)
146    }
147
148    fn render(&self, parent: Constraint) -> Vec<Line> {
149        let mut lines = Vec::new();
150        let padding = self.opts.padding;
151        let inner = self.inner(parent);
152        let child = Constraint::tight(inner.cols - padding * 2);
153
154        if let Some(color) = self.opts.border {
155            lines.push(
156                Line::default()
157                    .item(Paint::new("╭").fg(color))
158                    .item(Paint::new("─".repeat(inner.cols)).fg(color))
159                    .item(Paint::new("╮").fg(color)),
160            );
161        }
162
163        for row in &self.rows {
164            match row {
165                Row::Element(elem) => {
166                    for mut line in elem.render(child) {
167                        line.pad(child.max.cols);
168
169                        if let Some(color) = self.opts.border {
170                            lines.push(
171                                Line::default()
172                                    .item(Paint::new(format!("│{}", " ".repeat(padding))).fg(color))
173                                    .extend(line)
174                                    .item(
175                                        Paint::new(format!("{}│", " ".repeat(padding))).fg(color),
176                                    ),
177                            );
178                        } else {
179                            lines.push(line);
180                        }
181                    }
182                }
183                Row::Dividier => {
184                    if let Some(color) = self.opts.border {
185                        lines.push(
186                            Line::default()
187                                .item(Paint::new("├").fg(color))
188                                .item(Paint::new("─".repeat(inner.cols)).fg(color))
189                                .item(Paint::new("┤").fg(color)),
190                        );
191                    } else {
192                        lines.push(Line::default());
193                    }
194                }
195            }
196        }
197
198        if let Some(color) = self.opts.border {
199            lines.push(
200                Line::default()
201                    .item(Paint::new("╰").fg(color))
202                    .item(Paint::new("─".repeat(inner.cols)).fg(color))
203                    .item(Paint::new("╯").fg(color)),
204            );
205        }
206        lines.into_iter().flat_map(|h| h.render(child)).collect()
207    }
208}
209
210/// Simple bordered vstack.
211pub fn bordered<'a>(child: impl Element + 'a) -> VStack<'a> {
212    VStack::default().border(Some(colors::FAINT)).child(child)
213}
214
215#[cfg(test)]
216mod test {
217    use super::*;
218    use pretty_assertions::assert_eq;
219
220    #[test]
221    fn test_vstack() {
222        let mut v = VStack::default().border(Some(Color::Unset)).padding(1);
223
224        v.push(Line::new("banana"));
225        v.push(Line::new("apple"));
226        v.push(Line::new("abricot"));
227
228        let constraint = Constraint::default();
229        let outer = v.outer(constraint);
230        assert_eq!(outer.cols, 11);
231        assert_eq!(outer.rows, 5);
232
233        let inner = v.inner(constraint);
234        assert_eq!(inner.cols, 9);
235        assert_eq!(inner.rows, 3);
236
237        assert_eq!(
238            v.display(constraint),
239            r#"
240╭─────────╮
241│ banana  │
242│ apple   │
243│ abricot │
244╰─────────╯
245"#
246            .trim_start()
247        );
248    }
249
250    #[test]
251    fn test_vstack_maximize() {
252        let mut v = VStack::default().border(Some(Color::Unset)).padding(1);
253
254        v.push(Line::new("banana"));
255        v.push(Line::new("apple"));
256        v.push(Line::new("abricot"));
257
258        let constraint = Constraint {
259            min: Size::new(14, 0),
260            max: Size::new(14, usize::MAX),
261        };
262        let outer = v.outer(constraint);
263        assert_eq!(outer.cols, 14);
264        assert_eq!(outer.rows, 5);
265
266        let inner = v.inner(constraint);
267        assert_eq!(inner.cols, 12);
268        assert_eq!(inner.rows, 3);
269
270        assert_eq!(
271            v.display(constraint),
272            r#"
273╭────────────╮
274│ banana     │
275│ apple      │
276│ abricot    │
277╰────────────╯
278"#
279            .trim_start()
280        );
281    }
282}