1use crate::colors;
2use crate::{Color, Constraint, Element, Label, Line, Paint, Size};
3
4#[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#[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#[derive(Default, Debug)]
46pub struct VStack<'a> {
47 rows: Vec<Row<'a>>,
48 opts: VStackOptions,
49}
50
51impl<'a> VStack<'a> {
52 pub fn child(mut self, child: impl Element + 'a) -> Self {
54 self.push(child);
55 self
56 }
57
58 pub fn blank(self) -> Self {
60 self.child(Label::blank())
61 }
62
63 pub fn divider(mut self) -> Self {
65 self.rows.push(Row::Dividier);
66 self
67 }
68
69 pub fn is_empty(&self) -> bool {
71 self.rows.is_empty()
72 }
73
74 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 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 pub fn border(mut self, color: Option<Color>) -> Self {
97 self.opts.border = color;
98 self
99 }
100
101 pub fn padding(mut self, cols: usize) -> Self {
103 self.opts.padding = cols;
104 self
105 }
106
107 pub fn push(&mut self, child: impl Element + 'a) {
109 self.rows.push(Row::Element(Box::new(child)));
110 }
111
112 pub fn boxed(self) -> Box<dyn Element + 'a> {
114 Box::new(self)
115 }
116
117 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 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 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
210pub 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}