Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
term: Overhaul rendering
cloudhead committed 2 years ago
commit 7abfd4870cf22731fc3999b3c4d8091cb9ba1c53
parent b482ae85f0233e6a597bb98d17bf222612c32920
14 files changed +615 -174
modified radicle-cli/examples/rad-patch.md
@@ -99,10 +99,10 @@ And let's leave a quick comment for our team:

```
$ rad patch comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'I cannot wait to get back to the 90s!'
-
╭───────────────────────────────────────────────╮
-
│ z6MknSL…StBU8Vi (you) [   ...    ] 5c418a5    │
-
│ I cannot wait to get back to the 90s!         │
-
╰───────────────────────────────────────────────╯
+
╭────────────────────────────────────────────╮
+
│ z6MknSL…StBU8Vi (you) [   ...    ] 5c418a5 │
+
│ I cannot wait to get back to the 90s!      │
+
╰────────────────────────────────────────────╯
$ rad patch comment 73b73f376e93e09e0419664766ac9e433bf7d389 --message 'My favorite decade!' --reply-to 5c418a5 -q
729cdf63ce4793ab3cabffbe0dce24db16e45549
```
modified radicle-term/src/ansi.rs
@@ -12,5 +12,6 @@ mod windows;

pub use color::Color;
pub use paint::paint;
+
pub use paint::Filled;
pub use paint::Paint;
pub use style::Style;
modified radicle-term/src/ansi/paint.rs
@@ -3,6 +3,7 @@ use std::sync;
use std::sync::atomic::AtomicBool;
use std::{fmt, io};

+
use once_cell::sync::Lazy;
use unicode_width::UnicodeWidthStr;

use super::color::Color;
@@ -282,6 +283,12 @@ impl Paint<()> {
            || anstyle_query::clicolor_force()
    }

+
    /// Check 24-bit RGB color support.
+
    pub fn truecolor() -> bool {
+
        static TRUECOLOR: Lazy<bool> = Lazy::new(anstyle_query::term_supports_color);
+
        *TRUECOLOR
+
    }
+

    /// Enable paint styling.
    pub fn enable() {
        ENABLED.store(true, sync::atomic::Ordering::SeqCst);
@@ -299,6 +306,19 @@ impl Paint<()> {
    }
}

+
/// An object filled with a background color.
+
#[derive(Debug, Clone)]
+
pub struct Filled<T> {
+
    pub item: T,
+
    pub color: Color,
+
}
+

+
impl<T: fmt::Display> fmt::Display for Filled<T> {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "{}", Paint::wrapping(&self.item).bg(self.color))
+
    }
+
}
+

/// Shorthand for [`Paint::new`].
pub fn paint<T>(item: T) -> Paint<T> {
    Paint::new(item)
modified radicle-term/src/ansi/style.rs
@@ -135,6 +135,19 @@ impl Style {
        self
    }

+
    /// Merge styles with other. This is an additive process, so colors will only
+
    /// be changed if they aren't set on the receiver object.
+
    pub fn merge(mut self, other: Style) -> Style {
+
        if self.foreground == Color::Unset {
+
            self.foreground = other.foreground;
+
        }
+
        if self.background == Color::Unset {
+
            self.background = other.background;
+
        }
+
        self.properties.set(other.properties);
+
        self
+
    }
+

    /// Sets `self` to be wrapping.
    ///
    /// A wrapping `Style` converts all color resets written out by the internal
modified radicle-term/src/cell.rs
@@ -1,12 +1,12 @@
-
use std::fmt::Display;
+
use std::fmt;

-
use super::{Element, Line, Paint};
+
use super::{Color, Filled, Line, Paint};

use unicode_segmentation::UnicodeSegmentation as _;
use unicode_width::UnicodeWidthStr;

/// Text that can be displayed on the terminal, measured, truncated and padded.
-
pub trait Cell: Display {
+
pub trait Cell: fmt::Display {
    /// Type after truncation.
    type Truncated: Cell;
    /// Type after padding.
@@ -14,6 +14,10 @@ pub trait Cell: Display {

    /// Cell display width in number of terminal columns.
    fn width(&self) -> usize;
+
    /// Background color of cell.
+
    fn background(&self) -> Color {
+
        Color::Unset
+
    }
    /// Truncate cell if longer than given width. Shows the delimiter if truncated.
    #[must_use]
    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated;
@@ -30,6 +34,10 @@ impl Cell for Paint<String> {
        Cell::width(self.content())
    }

+
    fn background(&self) -> Color {
+
        self.style.background
+
    }
+

    fn truncate(&self, width: usize, delim: &str) -> Self {
        Self {
            item: self.item.truncate(width, delim),
@@ -50,7 +58,7 @@ impl Cell for Line {
    type Padded = Line;

    fn width(&self) -> usize {
-
        <Self as Element>::size(self).cols
+
        Line::width(self)
    }

    fn pad(&self, width: usize) -> Self::Padded {
@@ -74,6 +82,10 @@ impl Cell for Paint<&str> {
        Cell::width(self.item)
    }

+
    fn background(&self) -> Color {
+
        self.style.background
+
    }
+

    fn truncate(&self, width: usize, delim: &str) -> Paint<String> {
        Paint {
            item: self.item.truncate(width, delim),
@@ -171,3 +183,24 @@ impl<T: Cell + ?Sized> Cell for &T {
        T::pad(self, width)
    }
}
+

+
impl<T: Cell + fmt::Display> Cell for Filled<T> {
+
    type Truncated = T::Truncated;
+
    type Padded = T::Padded;
+

+
    fn width(&self) -> usize {
+
        T::width(&self.item)
+
    }
+

+
    fn background(&self) -> Color {
+
        self.color
+
    }
+

+
    fn truncate(&self, width: usize, delim: &str) -> Self::Truncated {
+
        T::truncate(&self.item, width, delim)
+
    }
+

+
    fn pad(&self, width: usize) -> Self::Padded {
+
        T::pad(&self.item, width)
+
    }
+
}
modified radicle-term/src/colors.rs
@@ -1,7 +1,81 @@
use crate::ansi::Color;

/// The faintest color; useful for borders and such.
-
pub const FAINT: Color = Color::Fixed(236);
+
pub const FAINT: Color = fixed::FAINT;

-
/// Negative color, useful for errors.
-
pub const NEGATIVE: Color = Color::Red;
+
// RGB (24-bit) colors supported by modern terminals.
+
pub mod rgb {
+
    use super::*;
+

+
    pub const NEGATIVE: Color = Color::RGB(60, 10, 20);
+
    pub const POSITIVE: Color = Color::RGB(10, 60, 20);
+
    pub const NEGATIVE_DARK: Color = Color::RGB(30, 10, 20);
+
    pub const POSITIVE_DARK: Color = Color::RGB(10, 30, 20);
+
    pub const NEGATIVE_LIGHT: Color = Color::RGB(170, 80, 120);
+
    pub const POSITIVE_LIGHT: Color = Color::RGB(80, 170, 120);
+
    pub const DIM: Color = Color::RGB(100, 100, 100);
+
    pub const FAINT: Color = Color::RGB(20, 20, 20);
+

+
    // Default syntax theme.
+
    pub const PURPLE: Color = Color::RGB(0xd2, 0xa8, 0xff);
+
    pub const RED: Color = Color::RGB(0xff, 0x7b, 0x72);
+
    pub const GREEN: Color = Color::RGB(0x7e, 0xd7, 0x87);
+
    pub const TEAL: Color = Color::RGB(0xa5, 0xd6, 0xff);
+
    pub const ORANGE: Color = Color::RGB(0xff, 0xa6, 0x57);
+
    pub const BLUE: Color = Color::RGB(0x79, 0xc0, 0xff);
+
    pub const GREY: Color = Color::RGB(0x8b, 0x94, 0x9e);
+
    pub const GREY_LIGHT: Color = Color::RGB(0xc9, 0xd1, 0xd9);
+

+
    /// Get a color using the color name.
+
    pub fn theme(name: &'static str) -> Option<Color> {
+
        match name {
+
            "negative" => Some(NEGATIVE),
+
            "negative.dark" => Some(NEGATIVE_DARK),
+
            "negative.light" => Some(NEGATIVE_LIGHT),
+
            "positive" => Some(POSITIVE),
+
            "positive.dark" => Some(POSITIVE_DARK),
+
            "positive.light" => Some(POSITIVE_LIGHT),
+
            "dim" => Some(DIM),
+
            "faint" => Some(FAINT),
+
            "purple" => Some(PURPLE),
+
            "red" => Some(RED),
+
            "green" => Some(GREEN),
+
            "teal" => Some(TEAL),
+
            "orange" => Some(ORANGE),
+
            "blue" => Some(BLUE),
+
            "grey" => Some(GREY),
+
            "grey.light" => Some(GREY_LIGHT),
+

+
            _ => None,
+
        }
+
    }
+
}
+

+
/// "Fixed" ANSI colors, supported by most terminals.
+
pub mod fixed {
+
    use super::*;
+

+
    /// The faintest color; useful for borders and such.
+
    pub const FAINT: Color = Color::Fixed(236);
+
    /// Slightly brighter than faint.
+
    pub const DIM: Color = Color::Fixed(239);
+

+
    /// Get a color using the color name.
+
    pub fn theme(name: &'static str) -> Option<Color> {
+
        match name {
+
            "negative" => Some(Color::Red),
+
            "negative.dark" => None,
+
            "positive" => Some(Color::Green),
+
            "positive.dark" => None,
+
            "dim" => None,
+
            "faint" => None,
+
            "blue" => Some(Color::Blue),
+
            "green" => Some(Color::Green),
+
            "red" => Some(Color::Red),
+
            "teal" => Some(Color::Cyan),
+
            "purple" => Some(Color::Magenta),
+

+
            _ => None,
+
        }
+
    }
+
}
modified radicle-term/src/element.rs
@@ -1,41 +1,114 @@
use std::fmt;
+
use std::io::IsTerminal;
use std::ops::Deref;
-
use std::vec;
+
use std::{io, vec};

use crate::cell::Cell;
-
use crate::Label;
+
use crate::{viewport, Color, Filled, Label, Style};
+

+
/// Rendering constraint.
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub struct Constraint {
+
    /// Minimum space the element can take.
+
    pub min: Size,
+
    /// Maximum space the element can take.
+
    pub max: Size,
+
}
+

+
impl Default for Constraint {
+
    fn default() -> Self {
+
        Self::UNBOUNDED
+
    }
+
}
+

+
impl Constraint {
+
    /// Can satisfy any size of object.
+
    pub const UNBOUNDED: Self = Self {
+
        min: Size::MIN,
+
        max: Size::MAX,
+
    };
+

+
    /// Create a new constraint.
+
    pub fn new(min: Size, max: Size) -> Self {
+
        assert!(min.cols <= max.cols && min.rows <= max.rows);
+

+
        Self { min, max }
+
    }
+

+
    /// A constraint with only a maximum size.
+
    pub fn max(max: Size) -> Self {
+
        Self {
+
            min: Size::MIN,
+
            max,
+
        }
+
    }
+

+
    /// A constraint that can only be satisfied by a single size.
+
    pub fn tight(size: Size) -> Self {
+
        Self {
+
            min: size,
+
            max: size,
+
        }
+
    }
+

+
    /// Return a new constraint that forces objects to the maximum size.
+
    pub fn maximize(self) -> Self {
+
        Self {
+
            min: self.max,
+
            max: self.max,
+
        }
+
    }
+

+
    /// Create a constraint from the terminal environment.
+
    ///
+
    /// If standard out isn't a terminal, returns an unbounded constraint.
+
    pub fn from_env() -> Self {
+
        if io::stdout().is_terminal() {
+
            Self::max(viewport().unwrap_or(Size::MAX))
+
        } else {
+
            Self::UNBOUNDED
+
        }
+
    }
+
}

/// A text element that has a size and can be rendered to the terminal.
pub trait Element: fmt::Debug {
    /// Get the size of the element, in rows and columns.
-
    fn size(&self) -> Size;
+
    fn size(&self, parent: Constraint) -> Size;

    #[must_use]
    /// Render the element as lines of text that can be printed.
-
    fn render(&self) -> Vec<Line>;
+
    fn render(&self, parent: Constraint) -> Vec<Line>;

    /// Get the number of columns occupied by this element.
-
    fn columns(&self) -> usize {
-
        self.size().cols
+
    fn columns(&self, parent: Constraint) -> usize {
+
        self.size(parent).cols
    }

    /// Get the number of rows occupied by this element.
-
    fn rows(&self) -> usize {
-
        self.size().rows
+
    fn rows(&self, parent: Constraint) -> usize {
+
        self.size(parent).rows
    }

    /// Print this element to stdout.
    fn print(&self) {
-
        for line in self.render() {
-
            println!("{line}");
+
        for line in self.render(Constraint::from_env()) {
+
            println!("{}", line.to_string().trim_end());
+
        }
+
    }
+

+
    /// Write to a writer.
+
    fn write(&self, constraints: Constraint) {
+
        for line in self.render(constraints) {
+
            println!("{}", line.to_string().trim_end());
        }
    }

    #[must_use]
    /// Return a string representation of this element.
-
    fn display(&self) -> String {
+
    fn display(&self, constraints: Constraint) -> String {
        let mut out = String::new();
-
        for line in self.render() {
+
        for line in self.render(constraints) {
            out.extend(line.into_iter().map(|l| l.to_string()));
            out.push('\n');
        }
@@ -44,12 +117,12 @@ pub trait Element: fmt::Debug {
}

impl<'a> Element for Box<dyn Element + 'a> {
-
    fn size(&self) -> Size {
-
        self.deref().size()
+
    fn size(&self, parent: Constraint) -> Size {
+
        self.deref().size(parent)
    }

-
    fn render(&self) -> Vec<Line> {
-
        self.deref().render()
+
    fn render(&self, parent: Constraint) -> Vec<Line> {
+
        self.deref().render(parent)
    }

    fn print(&self) {
@@ -58,12 +131,12 @@ impl<'a> Element for Box<dyn Element + 'a> {
}

impl<T: Element> Element for &T {
-
    fn size(&self) -> Size {
-
        (*self).size()
+
    fn size(&self, parent: Constraint) -> Size {
+
        (*self).size(parent)
    }

-
    fn render(&self) -> Vec<Line> {
-
        (*self).render()
+
    fn render(&self, parent: Constraint) -> Vec<Line> {
+
        (*self).render(parent)
    }

    fn print(&self) {
@@ -71,13 +144,6 @@ impl<T: Element> Element for &T {
    }
}

-
/// Used to specify maximum width or height.
-
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
-
pub struct Max {
-
    pub width: Option<usize>,
-
    pub height: Option<usize>,
-
}
-

/// A line of text that has styling and can be displayed.
#[derive(Clone, Default, Debug)]
pub struct Line {
@@ -97,6 +163,21 @@ impl Line {
        Self { items: vec![] }
    }

+
    /// Return a styled line by styling all its labels.
+
    pub fn style(self, style: Style) -> Line {
+
        Self {
+
            items: self
+
                .items
+
                .into_iter()
+
                .map(|l| {
+
                    let style = l.paint().style().merge(style);
+
                    l.style(style)
+
                })
+
                .collect(),
+
        }
+
    }
+

+
    /// Return a line with a single space between the given labels.
    pub fn spaced(items: impl IntoIterator<Item = Label>) -> Self {
        let mut line = Self::default();
        for item in items.into_iter() {
@@ -126,18 +207,23 @@ impl Line {

    /// Pad this line to occupy the given width.
    pub fn pad(&mut self, width: usize) {
-
        let w = self.columns();
+
        let w = self.width();

        if width > w {
            let pad = width - w;
-
            self.items.push(Label::new(" ".repeat(pad).as_str()));
+
            let bg = if let Some(last) = self.items.last() {
+
                last.background()
+
            } else {
+
                Color::Unset
+
            };
+
            self.items.push(Label::new(" ".repeat(pad).as_str()).bg(bg));
        }
    }

    /// Truncate this line to the given width.
    pub fn truncate(&mut self, width: usize, delim: &str) {
-
        while self.columns() > width {
-
            let total = self.columns();
+
        while self.width() > width {
+
            let total = self.width();

            if total - self.items.last().map_or(0, Cell::width) > width {
                self.items.pop();
@@ -147,22 +233,34 @@ impl Line {
        }
    }

+
    /// Get the actual column width of this line.
+
    pub fn width(&self) -> usize {
+
        self.items.iter().map(Cell::width).sum()
+
    }
+

+
    /// Create a line that contains a single space.
    pub fn space(mut self) -> Self {
        self.items.push(Label::space());
        self
    }

+
    /// Box this line as an [`Element`].
    pub fn boxed(self) -> Box<dyn Element> {
        Box::new(self)
    }
+

+
    /// Return a filled line.
+
    pub fn filled(self, color: Color) -> Filled<Self> {
+
        Filled { item: self, color }
+
    }
}

impl IntoIterator for Line {
    type Item = Label;
-
    type IntoIter = vec::IntoIter<Label>;
+
    type IntoIter = Box<dyn Iterator<Item = Label>>;

    fn into_iter(self) -> Self::IntoIter {
-
        self.items.into_iter()
+
        Box::new(self.items.into_iter())
    }
}

@@ -172,12 +270,18 @@ impl<T: Into<Label>> From<T> for Line {
    }
}

+
impl From<Vec<Label>> for Line {
+
    fn from(items: Vec<Label>) -> Self {
+
        Self { items }
+
    }
+
}
+

impl Element for Line {
-
    fn size(&self) -> Size {
+
    fn size(&self, _parent: Constraint) -> Size {
        Size::new(self.items.iter().map(Cell::width).sum(), 1)
    }

-
    fn render(&self) -> Vec<Line> {
+
    fn render(&self, _parent: Constraint) -> Vec<Line> {
        vec![self.clone()]
    }
}
@@ -201,20 +305,28 @@ pub struct Size {
}

impl Size {
+
    /// Minimum size.
+
    pub const MIN: Self = Self {
+
        cols: usize::MIN,
+
        rows: usize::MIN,
+
    };
+
    /// Maximum size.
+
    pub const MAX: Self = Self {
+
        cols: usize::MAX,
+
        rows: usize::MAX,
+
    };
+

    /// Create a new [`Size`].
    pub fn new(cols: usize, rows: usize) -> Self {
        Self { cols, rows }
    }

-
    /// Limit size.
-
    pub fn limit(mut self, lim: Max) -> Self {
-
        if let Some(w) = lim.width {
-
            self.cols = self.cols.min(w);
-
        }
-
        if let Some(h) = lim.height {
-
            self.rows = self.rows.min(h);
+
    /// Constrain size.
+
    pub fn constrain(self, c: Constraint) -> Self {
+
        Self {
+
            cols: self.cols.clamp(c.min.cols, c.max.cols),
+
            rows: self.rows.clamp(c.min.rows, c.max.rows),
        }
-
        self
    }
}

modified radicle-term/src/hstack.rs
@@ -1,11 +1,9 @@
-
use crate::{Element, Line, Size};
+
use crate::{Constraint, Element, Line, Size};

/// Horizontal stack of [`Element`] objects that implements [`Element`].
#[derive(Default, Debug)]
pub struct HStack<'a> {
    elems: Vec<Box<dyn Element + 'a>>,
-
    width: usize,
-
    height: usize,
}

impl<'a> HStack<'a> {
@@ -16,18 +14,19 @@ impl<'a> HStack<'a> {
    }

    pub fn push(&mut self, child: impl Element + 'a) {
-
        self.width += child.columns() + 1;
-
        self.height = self.height.max(child.rows());
        self.elems.push(Box::new(child));
    }
}

impl<'a> Element for HStack<'a> {
-
    fn size(&self) -> Size {
-
        Size::new(self.width, self.height)
+
    fn size(&self, parent: Constraint) -> Size {
+
        let width = self.elems.iter().map(|c| c.columns(parent)).sum();
+
        let height = self.elems.iter().map(|c| c.rows(parent)).max().unwrap_or(0);
+

+
        Size::new(width, height)
    }

-
    fn render(&self) -> Vec<Line> {
+
    fn render(&self, parent: Constraint) -> Vec<Line> {
        fn rearrange(input: Vec<Vec<Line>>) -> Vec<Line> {
            let max_len = input.iter().map(|v| v.len()).max().unwrap_or(0);

@@ -37,11 +36,11 @@ impl<'a> Element for HStack<'a> {
                        input
                            .iter()
                            .filter_map(move |v| v.get(i))
-
                            .flat_map(|l| l.clone().into_iter()),
+
                            .flat_map(|l| l.clone()),
                    )
                })
                .collect()
        }
-
        rearrange(self.elems.iter().map(|e| e.render()).collect())
+
        rearrange(self.elems.iter().map(|e| e.render(parent)).collect())
    }
}
modified radicle-term/src/io.rs
@@ -10,7 +10,7 @@ use zeroize::Zeroizing;

use crate::command;
use crate::format;
-
use crate::{style, Paint};
+
use crate::{style, Paint, Size};

pub use inquire;
pub use inquire::Select;
@@ -92,6 +92,12 @@ pub fn columns() -> Option<usize> {
    termion::terminal_size().map(|(cols, _)| cols as usize).ok()
}

+
pub fn viewport() -> Option<Size> {
+
    termion::terminal_size()
+
        .map(|(cols, rows)| Size::new(cols as usize, rows as usize))
+
        .ok()
+
}
+

pub fn headline(headline: impl fmt::Display) {
    println!();
    println!("{}", style(headline).bold());
modified radicle-term/src/label.rs
@@ -1,6 +1,6 @@
use std::fmt;

-
use crate::{cell::Cell, Element, Line, Paint, Size};
+
use crate::{cell::Cell, Color, Constraint, Element, Filled, Line, Paint, Size, Style};

/// A styled string that does not contain any `'\n'` and implements [`Element`] and [`Cell`].
#[derive(Clone, Default, Debug)]
@@ -31,14 +31,44 @@ impl Label {
    pub fn boxed(self) -> Box<dyn Element> {
        Box::new(self)
    }
+

+
    /// Color the label's foreground.
+
    pub fn fg(self, color: Color) -> Self {
+
        Self(self.0.fg(color))
+
    }
+

+
    /// Color the label's background.
+
    pub fn bg(self, color: Color) -> Self {
+
        Self(self.0.bg(color))
+
    }
+

+
    /// Style a label.
+
    pub fn style(self, style: Style) -> Self {
+
        Self(self.0.with_style(style))
+
    }
+

+
    /// Get inner paint object.
+
    pub fn paint(&self) -> &Paint<String> {
+
        &self.0
+
    }
+

+
    /// Return a filled cell from this label.
+
    pub fn filled(self, color: Color) -> Filled<Self> {
+
        Filled { item: self, color }
+
    }
+

+
    /// Wrap into a line.
+
    pub fn to_line(self) -> Line {
+
        Line::from(self)
+
    }
}

impl Element for Label {
-
    fn size(&self) -> Size {
+
    fn size(&self, _parent: Constraint) -> Size {
        Size::new(self.0.width(), 1)
    }

-
    fn render(&self) -> Vec<Line> {
+
    fn render(&self, _parent: Constraint) -> Vec<Line> {
        vec![Line::new(self.clone())]
    }
}
@@ -53,6 +83,10 @@ impl Cell for Label {
    type Padded = Self;
    type Truncated = Self;

+
    fn background(&self) -> Color {
+
        self.paint().style.background
+
    }
+

    fn pad(&self, width: usize) -> Self::Padded {
        Self(self.0.pad(width))
    }
@@ -89,7 +123,7 @@ impl From<&str> for Label {

/// Create a new label from a [`Paint`] object.
pub fn label(s: impl Into<Paint<String>>) -> Label {
-
    Label(s.into())
+
    Label::from(s.into())
}

/// Cleanup the input string for display as a label.
modified radicle-term/src/lib.rs
@@ -14,9 +14,9 @@ pub mod textarea;
pub mod vstack;

pub use ansi::Color;
-
pub use ansi::{paint, Paint, Style};
+
pub use ansi::{paint, Filled, Paint, Style};
pub use editor::Editor;
-
pub use element::{Element, Line, Max, Size};
+
pub use element::{Constraint, Element, Line, Size};
pub use hstack::HStack;
pub use inquire::ui::Styled;
pub use io::*;
modified radicle-term/src/table.rs
@@ -18,9 +18,9 @@
//! ```
use std::fmt;

-
use crate as term;
use crate::cell::Cell;
-
use crate::{Color, Line, Max, Paint, Size};
+
use crate::{self as term, Style};
+
use crate::{Color, Constraint, Line, Paint, Size};

pub use crate::Element;

@@ -28,8 +28,6 @@ pub use crate::Element;
pub struct TableOptions {
    /// Whether the table should be allowed to overflow.
    pub overflow: bool,
-
    /// The maximum width and height.
-
    pub max: Max,
    /// Horizontal spacing between table cells.
    pub spacing: usize,
    /// Table border.
@@ -40,7 +38,6 @@ impl Default for TableOptions {
    fn default() -> Self {
        Self {
            overflow: false,
-
            max: Max::default(),
            spacing: 1,
            border: None,
        }
@@ -84,16 +81,14 @@ impl<const W: usize, T: Cell + fmt::Debug> Element for Table<W, T>
where
    T::Padded: Into<Line>,
{
-
    fn size(&self) -> Size {
-
        Table::size(self)
+
    fn size(&self, parent: Constraint) -> Size {
+
        Table::size(self, parent)
    }

-
    fn render(&self) -> Vec<Line> {
+
    fn render(&self, parent: Constraint) -> Vec<Line> {
        let mut lines = Vec::new();
-
        let limits = self.limits();
-
        let width = limits.width;
        let border = self.opts.border;
-
        let inner = self.inner().limit(limits);
+
        let inner = self.inner(parent);
        let cols = inner.cols;

        if let Some(color) = border {
@@ -115,20 +110,19 @@ where
                    }
                    for (i, cell) in cells.iter().enumerate() {
                        let pad = if i == cells.len() - 1 {
-
                            if border.is_some() {
-
                                self.widths[i]
-
                            } else {
-
                                0
-
                            }
+
                            0
                        } else {
                            self.widths[i] + self.opts.spacing
                        };
-
                        line = line.extend(cell.pad(pad).into());
+
                        line = line.extend(
+
                            cell.pad(pad)
+
                                .into()
+
                                .style(Style::default().bg(cell.background())),
+
                        );
                    }
+
                    Line::pad(&mut line, cols);
+
                    Line::truncate(&mut line, cols, "…");

-
                    if let Some(width) = width {
-
                        line = line.truncate(width, "…");
-
                    }
                    if let Some(color) = border {
                        line.push(Paint::new(" │").fg(color));
                    }
@@ -169,28 +163,14 @@ impl<const W: usize, T: Cell> Table<W, T> {
        }
    }

-
    pub fn size(&self) -> Size {
-
        self.outer()
+
    pub fn size(&self, parent: Constraint) -> Size {
+
        self.outer(parent)
    }

    pub fn divider(&mut self) {
        self.rows.push(Row::Divider);
    }

-
    pub fn limits(&self) -> Max {
-
        let width = self.opts.max.width.or_else(term::columns).map(|w| {
-
            if self.opts.border.is_some() {
-
                w - 2
-
            } else {
-
                w
-
            }
-
        });
-
        Max {
-
            width,
-
            height: None,
-
        }
-
    }
-

    pub fn push(&mut self, row: [T; W]) {
        for (i, cell) in row.iter().enumerate() {
            self.widths[i] = self.widths[i].max(cell.width());
@@ -198,27 +178,27 @@ impl<const W: usize, T: Cell> Table<W, T> {
        self.rows.push(Row::Data(row));
    }

-
    fn inner(&self) -> Size {
-
        let mut cols = self.widths.iter().sum::<usize>() + (W - 1) * self.opts.spacing;
-
        let rows = self.rows.len();
-
        let limits = self.limits();
+
    fn inner(&self, c: Constraint) -> Size {
+
        let mut outer = self.outer(c);

-
        // Account for inner spacing.
        if self.opts.border.is_some() {
-
            cols += 2;
+
            outer.cols -= 2;
+
            outer.rows -= 2;
        }
-
        Size::new(cols, rows).limit(limits)
+
        outer
    }

-
    fn outer(&self) -> Size {
-
        let mut inner = self.inner();
+
    fn outer(&self, c: Constraint) -> Size {
+
        let mut cols = self.widths.iter().sum::<usize>() + (W - 1) * self.opts.spacing;
+
        let mut rows = self.rows.len();
+
        let padding = 2;

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

@@ -251,10 +231,10 @@ mod test {

        #[rustfmt::skip]
        assert_eq!(
-
            t.display(),
+
            t.display(Constraint::UNBOUNDED),
            [
                "pineapple rosemary\n",
-
                "apples    pears\n"
+
                "apples    pears   \n"
            ].join("")
        );
    }
@@ -273,16 +253,16 @@ mod test {
        t.push(["Switzerland", "7M", "CH"]);
        t.push(["Germany", "80M", "DE"]);

-
        let inner = t.inner();
+
        let inner = t.inner(Constraint::UNBOUNDED);
        assert_eq!(inner.cols, 33);
        assert_eq!(inner.rows, 5);

-
        let outer = t.outer();
+
        let outer = t.outer(Constraint::UNBOUNDED);
        assert_eq!(outer.cols, 35);
        assert_eq!(outer.rows, 7);

        assert_eq!(
-
            t.display(),
+
            t.display(Constraint::UNBOUNDED),
            r#"
╭─────────────────────────────────╮
│ Country       Population   Code │
@@ -301,10 +281,6 @@ mod test {
        let mut t = Table::new(TableOptions {
            border: Some(Color::Unset),
            spacing: 3,
-
            max: Max {
-
                width: Some(19),
-
                height: None,
-
            },
            ..TableOptions::default()
        });

@@ -314,16 +290,20 @@ mod test {
        t.push(["CH", "Switzerland"]);
        t.push(["DE", "Germany"]);

-
        let inner = t.inner();
-
        assert_eq!(inner.cols, 17);
-
        assert_eq!(inner.rows, 5);
-

-
        let outer = t.outer();
+
        let constrain = Constraint::max(Size {
+
            cols: 19,
+
            rows: usize::MAX,
+
        });
+
        let outer = t.outer(constrain);
        assert_eq!(outer.cols, 19);
        assert_eq!(outer.rows, 7);

+
        let inner = t.inner(constrain);
+
        assert_eq!(inner.cols, 17);
+
        assert_eq!(inner.rows, 5);
+

        assert_eq!(
-
            t.display(),
+
            t.display(constrain),
            r#"
╭─────────────────╮
│ Code   Name     │
@@ -338,24 +318,69 @@ mod test {
    }

    #[test]
-
    fn test_table_truncate() {
+
    fn test_table_border_maximized() {
        let mut t = Table::new(TableOptions {
-
            max: Max {
-
                width: Some(16),
-
                height: None,
-
            },
+
            border: Some(Color::Unset),
+
            spacing: 3,
            ..TableOptions::default()
        });

+
        t.push(["Code", "Name"]);
+
        t.divider();
+
        t.push(["FR", "France"]);
+
        t.push(["CH", "Switzerland"]);
+
        t.push(["DE", "Germany"]);
+

+
        let constrain = Constraint::new(
+
            Size { cols: 26, rows: 0 },
+
            Size {
+
                cols: 26,
+
                rows: usize::MAX,
+
            },
+
        );
+
        let outer = t.outer(constrain);
+
        assert_eq!(outer.cols, 26);
+
        assert_eq!(outer.rows, 7);
+

+
        let inner = t.inner(constrain);
+
        assert_eq!(inner.cols, 24);
+
        assert_eq!(inner.rows, 5);
+

+
        assert_eq!(
+
            t.display(constrain),
+
            r#"
+
╭────────────────────────╮
+
│ Code   Name            │
+
├────────────────────────┤
+
│ FR     France          │
+
│ CH     Switzerland     │
+
│ DE     Germany         │
+
╰────────────────────────╯
+
"#
+
            .trim_start()
+
        );
+
    }
+

+
    #[test]
+
    fn test_table_truncate() {
+
        let mut t = Table::default();
+
        let constrain = Constraint::new(
+
            Size::MIN,
+
            Size {
+
                cols: 16,
+
                rows: usize::MAX,
+
            },
+
        );
+

        t.push(["pineapple", "rosemary"]);
        t.push(["apples", "pears"]);

        #[rustfmt::skip]
        assert_eq!(
-
            t.display(),
+
            t.display(constrain),
            [
                "pineapple rosem…\n",
-
                "apples    pears\n"
+
                "apples    pears \n"
            ].join("")
        );
    }
@@ -369,9 +394,9 @@ mod test {

        #[rustfmt::skip]
        assert_eq!(
-
            t.display(),
+
            t.display(Constraint::UNBOUNDED),
            [
-
                "🍍pineapple __rosemary __sage\n",
+
                "🍍pineapple __rosemary __sage   \n",
                "__pears     🍎apples   🍌bananas\n"
            ].join("")
        );
@@ -380,19 +405,18 @@ mod test {
    #[test]
    fn test_table_unicode_truncate() {
        let mut t = Table::new(TableOptions {
-
            max: Max {
-
                width: Some(16),
-
                height: None,
-
            },
            ..TableOptions::default()
        });
-

+
        let constrain = Constraint::max(Size {
+
            cols: 16,
+
            rows: usize::MAX,
+
        });
        t.push(["🍍pineapple", "__rosemary"]);
        t.push(["__pears", "🍎apples"]);

        #[rustfmt::skip]
        assert_eq!(
-
            t.display(),
+
            t.display(constrain),
            [
                "🍍pineapple __r…\n",
                "__pears     🍎a…\n"
modified radicle-term/src/textarea.rs
@@ -1,4 +1,4 @@
-
use crate::{cell::Cell, Element, Line, Paint, Size};
+
use crate::{cell::Cell, Constraint, Element, Line, Paint, Size};

/// Default text wrap width.
pub const DEFAULT_WRAP: usize = 80;
@@ -65,14 +65,14 @@ impl TextArea {
}

impl Element for TextArea {
-
    fn size(&self) -> Size {
+
    fn size(&self, _parent: Constraint) -> Size {
        let cols = self.lines().map(|l| l.width()).max().unwrap_or(0);
        let rows = self.lines().count();

        Size::new(cols, rows)
    }

-
    fn render(&self) -> Vec<Line> {
+
    fn render(&self, _parent: Constraint) -> Vec<Line> {
        self.lines()
            .map(|l| Line::new(Paint::new(l).with_style(self.body.style)))
            .collect()
modified radicle-term/src/vstack.rs
@@ -1,10 +1,20 @@
use crate::colors;
-
use crate::{Color, Element, Label, Line, Paint, Size};
+
use crate::{Color, Constraint, Element, Label, Line, Paint, Size};

/// Options for [`VStack`].
-
#[derive(Default, Debug)]
+
#[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.
@@ -14,13 +24,21 @@ enum Row<'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,
+
        }
+
    }
+
}
+

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

impl<'a> VStack<'a> {
@@ -38,7 +56,6 @@ impl<'a> VStack<'a> {
    /// Add a horizontal divider.
    pub fn divider(mut self) -> Self {
        self.rows.push(Row::Dividier);
-
        self.height += 1;
        self
    }

@@ -61,31 +78,67 @@ impl<'a> VStack<'a> {
        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.width = self.width.max(child.columns());
-
        self.height += child.rows();
        self.rows.push(Row::Element(Box::new(child)));
    }
-
}

-
impl<'a> Element for VStack<'a> {
-
    fn size(&self) -> Size {
+
    /// 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.len();
+

+
        // Account for outer borders.
        if self.opts.border.is_some() {
-
            Size::new(self.width + 4, self.height + 2)
-
        } else {
-
            Size::new(self.width, self.height)
+
            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) -> Vec<Line> {
+
    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(Size {
+
            cols: inner.cols - padding * 2,
+
            rows: usize::MAX,
+
        });

        if let Some(color) = self.opts.border {
            lines.push(
                Line::default()
                    .item(Paint::new("╭").fg(color))
-
                    .item(Paint::new("─".repeat(self.width + 2)).fg(color))
+
                    .item(Paint::new("─".repeat(inner.cols)).fg(color))
                    .item(Paint::new("╮").fg(color)),
            );
        }
@@ -93,14 +146,17 @@ impl<'a> Element for VStack<'a> {
        for row in &self.rows {
            match row {
                Row::Element(elem) => {
-
                    for mut line in elem.render() {
+
                    for mut line in elem.render(child) {
+
                        line.pad(child.max.cols);
+

                        if let Some(color) = self.opts.border {
-
                            line.pad(self.width);
                            lines.push(
                                Line::default()
-
                                    .item(Paint::new("│ ").fg(color))
+
                                    .item(Paint::new(format!("│{}", " ".repeat(padding))).fg(color))
                                    .extend(line)
-
                                    .item(Paint::new(" │").fg(color)),
+
                                    .item(
+
                                        Paint::new(format!("{}│", " ".repeat(padding))).fg(color),
+
                                    ),
                            );
                        } else {
                            lines.push(line);
@@ -112,7 +168,7 @@ impl<'a> Element for VStack<'a> {
                        lines.push(
                            Line::default()
                                .item(Paint::new("├").fg(color))
-
                                .item(Paint::new("─".repeat(self.width + 2)).fg(color))
+
                                .item(Paint::new("─".repeat(inner.cols)).fg(color))
                                .item(Paint::new("┤").fg(color)),
                        );
                    } else {
@@ -126,11 +182,11 @@ impl<'a> Element for VStack<'a> {
            lines.push(
                Line::default()
                    .item(Paint::new("╰").fg(color))
-
                    .item(Paint::new("─".repeat(self.width + 2)).fg(color))
+
                    .item(Paint::new("─".repeat(inner.cols)).fg(color))
                    .item(Paint::new("╯").fg(color)),
            );
        }
-
        lines.into_iter().flat_map(|h| h.render()).collect()
+
        lines.into_iter().flat_map(|h| h.render(child)).collect()
    }
}

@@ -138,3 +194,72 @@ impl<'a> Element for VStack<'a> {
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()
+
        );
+
    }
+
}