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 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#[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>(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 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 pub fn border(mut self, color: Option<Color>) -> Self {
94 self.opts.border = color;
95 self
96 }
97
98 pub fn padding(mut self, cols: usize) -> Self {
100 self.opts.padding = cols;
101 self
102 }
103
104 pub fn push(&mut self, child: impl Element + 'a) {
106 self.rows.push(Row::Element(Box::new(child)));
107 }
108
109 pub fn boxed(self) -> Box<dyn Element + 'a> {
111 Box::new(self)
112 }
113
114 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 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 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
207pub 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}