use crate::colors;
use crate::{Color, Constraint, Element, Label, Line, Paint, Size};
#[derive(Debug)]
pub struct VStackOptions {
border: Option<Color>,
padding: usize,
}
impl Default for VStackOptions {
fn default() -> Self {
Self {
border: None,
padding: 1,
}
}
}
#[derive(Default, Debug)]
enum Row<'a> {
Element(Box<dyn Element + 'a>),
#[default]
Dividier,
}
impl<'a> Row<'a> {
fn width(&self, c: Constraint) -> usize {
match self {
Self::Element(e) => e.columns(c),
Self::Dividier => c.min.cols,
}
}
fn height(&self, c: Constraint) -> usize {
match self {
Self::Element(e) => e.rows(c),
Self::Dividier => 1,
}
}
}
#[derive(Default, Debug)]
pub struct VStack<'a> {
rows: Vec<Row<'a>>,
opts: VStackOptions,
}
impl<'a> VStack<'a> {
pub fn child(mut self, child: impl Element + 'a) -> Self {
self.push(child);
self
}
pub fn blank(self) -> Self {
self.child(Label::blank())
}
pub fn divider(mut self) -> Self {
self.rows.push(Row::Dividier);
self
}
pub fn children<I>(self, children: I) -> Self
where
I: IntoIterator<Item = Box<dyn Element>>,
{
let mut vstack = self;
for child in children.into_iter() {
vstack = vstack.child(child);
}
vstack
}
pub fn border(mut self, color: Option<Color>) -> Self {
self.opts.border = color;
self
}
pub fn padding(mut self, cols: usize) -> Self {
self.opts.padding = cols;
self
}
pub fn push(&mut self, child: impl Element + 'a) {
self.rows.push(Row::Element(Box::new(child)));
}
pub fn boxed(self) -> Box<dyn Element + 'a> {
Box::new(self)
}
fn inner(&self, c: Constraint) -> Size {
let mut outer = self.outer(c);
if self.opts.border.is_some() {
outer.cols -= 2;
outer.rows -= 2;
}
outer
}
fn outer(&self, c: Constraint) -> Size {
let padding = self.opts.padding * 2;
let mut cols = self.rows.iter().map(|r| r.width(c)).max().unwrap_or(0) + padding;
let mut rows = self.rows.iter().map(|r| r.height(c)).sum();
if self.opts.border.is_some() {
cols += 2;
rows += 2;
}
Size::new(cols, rows).constrain(c)
}
}
impl<'a> Element for VStack<'a> {
fn size(&self, parent: Constraint) -> Size {
self.outer(parent)
}
fn render(&self, parent: Constraint) -> Vec<Line> {
let mut lines = Vec::new();
let padding = self.opts.padding;
let inner = self.inner(parent);
let child = Constraint::tight(inner.cols - padding * 2);
if let Some(color) = self.opts.border {
lines.push(
Line::default()
.item(Paint::new("╭").fg(color))
.item(Paint::new("─".repeat(inner.cols)).fg(color))
.item(Paint::new("╮").fg(color)),
);
}
for row in &self.rows {
match row {
Row::Element(elem) => {
for mut line in elem.render(child) {
line.pad(child.max.cols);
if let Some(color) = self.opts.border {
lines.push(
Line::default()
.item(Paint::new(format!("│{}", " ".repeat(padding))).fg(color))
.extend(line)
.item(
Paint::new(format!("{}│", " ".repeat(padding))).fg(color),
),
);
} else {
lines.push(line);
}
}
}
Row::Dividier => {
if let Some(color) = self.opts.border {
lines.push(
Line::default()
.item(Paint::new("├").fg(color))
.item(Paint::new("─".repeat(inner.cols)).fg(color))
.item(Paint::new("┤").fg(color)),
);
} else {
lines.push(Line::default());
}
}
}
}
if let Some(color) = self.opts.border {
lines.push(
Line::default()
.item(Paint::new("╰").fg(color))
.item(Paint::new("─".repeat(inner.cols)).fg(color))
.item(Paint::new("╯").fg(color)),
);
}
lines.into_iter().flat_map(|h| h.render(child)).collect()
}
}
pub fn bordered<'a>(child: impl Element + 'a) -> VStack<'a> {
VStack::default().border(Some(colors::FAINT)).child(child)
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_vstack() {
let mut v = VStack::default().border(Some(Color::Unset)).padding(1);
v.push(Line::new("banana"));
v.push(Line::new("apple"));
v.push(Line::new("abricot"));
let constraint = Constraint::default();
let outer = v.outer(constraint);
assert_eq!(outer.cols, 11);
assert_eq!(outer.rows, 5);
let inner = v.inner(constraint);
assert_eq!(inner.cols, 9);
assert_eq!(inner.rows, 3);
assert_eq!(
v.display(constraint),
r#"
╭─────────╮
│ banana │
│ apple │
│ abricot │
╰─────────╯
"#
.trim_start()
);
}
#[test]
fn test_vstack_maximize() {
let mut v = VStack::default().border(Some(Color::Unset)).padding(1);
v.push(Line::new("banana"));
v.push(Line::new("apple"));
v.push(Line::new("abricot"));
let constraint = Constraint {
min: Size::new(14, 0),
max: Size::new(14, usize::MAX),
};
let outer = v.outer(constraint);
assert_eq!(outer.cols, 14);
assert_eq!(outer.rows, 5);
let inner = v.inner(constraint);
assert_eq!(inner.cols, 12);
assert_eq!(inner.rows, 3);
assert_eq!(
v.display(constraint),
r#"
╭────────────╮
│ banana │
│ apple │
│ abricot │
╰────────────╯
"#
.trim_start()
);
}
}