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 Row<'_> {
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>(mut self, children: I) -> Self
76    where
77        I: IntoIterator<Item = Box<dyn Element>>,
78    {
79        self.rows
80            .extend(children.into_iter().map(|e| Row::Element(Box::new(e))));
81        self
82    }
83
84    /// Merge with another `VStack`.
85    pub fn merge(mut self, other: Self) -> Self {
86        for row in other.rows {
87            self.rows.push(row);
88        }
89        self
90    }
91
92    /// Set or unset the outer border.
93    pub fn border(mut self, color: Option<Color>) -> Self {
94        self.opts.border = color;
95        self
96    }
97
98    /// Set horizontal padding.
99    pub fn padding(mut self, cols: usize) -> Self {
100        self.opts.padding = cols;
101        self
102    }
103
104    /// Add an element to the stack.
105    pub fn push(&mut self, child: impl Element + 'a) {
106        self.rows.push(Row::Element(Box::new(child)));
107    }
108
109    /// Box this element.
110    pub fn boxed(self) -> Box<dyn Element + 'a> {
111        Box::new(self)
112    }
113
114    /// Inner size.
115    fn inner(&self, c: Constraint) -> Size {
116        let mut outer = self.outer(c);
117
118        if self.opts.border.is_some() {
119            outer.cols -= 2;
120            outer.rows -= 2;
121        }
122        outer
123    }
124
125    /// Outer size (includes borders).
126    fn outer(&self, c: Constraint) -> Size {
127        let padding = self.opts.padding * 2;
128        let mut cols = self.rows.iter().map(|r| r.width(c)).max().unwrap_or(0) + padding;
129        let mut rows = self.rows.iter().map(|r| r.height(c)).sum();
130
131        // Account for outer borders.
132        if self.opts.border.is_some() {
133            cols += 2;
134            rows += 2;
135        }
136        Size::new(cols, rows).constrain(c)
137    }
138}
139
140impl Element for VStack<'_> {
141    fn size(&self, parent: Constraint) -> Size {
142        self.outer(parent)
143    }
144
145    fn render(&self, parent: Constraint) -> Vec<Line> {
146        let mut lines = Vec::new();
147        let padding = self.opts.padding;
148        let inner = self.inner(parent);
149        let child = Constraint::tight(inner.cols - padding * 2);
150
151        if let Some(color) = self.opts.border {
152            lines.push(
153                Line::default()
154                    .item(Paint::new("╭").fg(color))
155                    .item(Paint::new("─".repeat(inner.cols)).fg(color))
156                    .item(Paint::new("╮").fg(color)),
157            );
158        }
159
160        for row in &self.rows {
161            match row {
162                Row::Element(elem) => {
163                    for mut line in elem.render(child) {
164                        line.pad(child.max.cols);
165
166                        if let Some(color) = self.opts.border {
167                            lines.push(
168                                Line::default()
169                                    .item(Paint::new(format!("│{}", " ".repeat(padding))).fg(color))
170                                    .extend(line)
171                                    .item(
172                                        Paint::new(format!("{}│", " ".repeat(padding))).fg(color),
173                                    ),
174                            );
175                        } else {
176                            lines.push(line);
177                        }
178                    }
179                }
180                Row::Dividier => {
181                    if let Some(color) = self.opts.border {
182                        lines.push(
183                            Line::default()
184                                .item(Paint::new("├").fg(color))
185                                .item(Paint::new("─".repeat(inner.cols)).fg(color))
186                                .item(Paint::new("┤").fg(color)),
187                        );
188                    } else {
189                        lines.push(Line::default());
190                    }
191                }
192            }
193        }
194
195        if let Some(color) = self.opts.border {
196            lines.push(
197                Line::default()
198                    .item(Paint::new("╰").fg(color))
199                    .item(Paint::new("─".repeat(inner.cols)).fg(color))
200                    .item(Paint::new("╯").fg(color)),
201            );
202        }
203        lines.into_iter().flat_map(|h| h.render(child)).collect()
204    }
205}
206
207/// Simple bordered vstack.
208pub fn bordered<'a>(child: impl Element + 'a) -> VStack<'a> {
209    VStack::default().border(Some(colors::FAINT)).child(child)
210}
211
212#[cfg(test)]
213mod test {
214    use super::*;
215    use pretty_assertions::assert_eq;
216
217    #[test]
218    fn test_vstack() {
219        let mut v = VStack::default().border(Some(Color::Unset)).padding(1);
220
221        v.push(Line::new("banana"));
222        v.push(Line::new("apple"));
223        v.push(Line::new("abricot"));
224
225        let constraint = Constraint::default();
226        let outer = v.outer(constraint);
227        assert_eq!(outer.cols, 11);
228        assert_eq!(outer.rows, 5);
229
230        let inner = v.inner(constraint);
231        assert_eq!(inner.cols, 9);
232        assert_eq!(inner.rows, 3);
233
234        assert_eq!(
235            v.display(constraint),
236            r#"
237╭─────────╮
238│ banana  │
239│ apple   │
240│ abricot │
241╰─────────╯
242"#
243            .trim_start()
244        );
245    }
246
247    #[test]
248    fn test_vstack_maximize() {
249        let mut v = VStack::default().border(Some(Color::Unset)).padding(1);
250
251        v.push(Line::new("banana"));
252        v.push(Line::new("apple"));
253        v.push(Line::new("abricot"));
254
255        let constraint = Constraint {
256            min: Size::new(14, 0),
257            max: Size::new(14, usize::MAX),
258        };
259        let outer = v.outer(constraint);
260        assert_eq!(outer.cols, 14);
261        assert_eq!(outer.rows, 5);
262
263        let inner = v.inner(constraint);
264        assert_eq!(inner.cols, 12);
265        assert_eq!(inner.rows, 3);
266
267        assert_eq!(
268            v.display(constraint),
269            r#"
270╭────────────╮
271│ banana     │
272│ apple      │
273│ abricot    │
274╰────────────╯
275"#
276            .trim_start()
277        );
278    }
279}