Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-term src vstack.rs
use crate::colors;
use crate::{Color, Constraint, Element, Label, Line, Paint, Size};

/// Options for [`VStack`].
#[derive(Debug)]
pub struct VStackOptions {
    border: Option<Color>,
    padding: usize,
}

impl Default for VStackOptions {
    fn default() -> Self {
        Self {
            border: None,
            padding: 1,
        }
    }
}

/// A vertical stack row.
#[derive(Default, Debug)]
enum Row<'a> {
    Element(Box<dyn Element + 'a>),
    #[default]
    Dividier,
}

impl Row<'_> {
    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,
        }
    }
}

/// Vertical stack of [`Element`] objects that implements [`Element`].
#[derive(Default, Debug)]
pub struct VStack<'a> {
    rows: Vec<Row<'a>>,
    opts: VStackOptions,
}

impl<'a> VStack<'a> {
    /// Add an element to the stack and return the stack.
    pub fn child(mut self, child: impl Element + 'a) -> Self {
        self.push(child);
        self
    }

    /// Add a blank line to the stack.
    pub fn blank(self) -> Self {
        self.child(Label::blank())
    }

    /// Add a horizontal divider.
    pub fn divider(mut self) -> Self {
        self.rows.push(Row::Dividier);
        self
    }

    /// Check if this stack is empty.
    pub fn is_empty(&self) -> bool {
        self.rows.is_empty()
    }

    /// Add multiple elements to the stack.
    pub fn children<I>(mut self, children: I) -> Self
    where
        I: IntoIterator<Item = Box<dyn Element>>,
    {
        self.rows
            .extend(children.into_iter().map(|e| Row::Element(Box::new(e))));
        self
    }

    /// Merge with another `VStack`.
    pub fn merge(mut self, other: Self) -> Self {
        for row in other.rows {
            self.rows.push(row);
        }
        self
    }

    /// Set or unset the outer border.
    pub fn border(mut self, color: Option<Color>) -> Self {
        self.opts.border = color;
        self
    }

    /// Set horizontal padding.
    pub fn padding(mut self, cols: usize) -> Self {
        self.opts.padding = cols;
        self
    }

    /// Add an element to the stack.
    pub fn push(&mut self, child: impl Element + 'a) {
        self.rows.push(Row::Element(Box::new(child)));
    }

    /// Box this element.
    pub fn boxed(self) -> Box<dyn Element + 'a> {
        Box::new(self)
    }

    /// Inner size.
    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
    }

    /// Outer size (includes borders).
    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();

        // Account for outer borders.
        if self.opts.border.is_some() {
            cols += 2;
            rows += 2;
        }
        Size::new(cols, rows).constrain(c)
    }
}

impl Element for VStack<'_> {
    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()
    }
}

/// Simple bordered vstack.
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()
        );
    }
}