term: Overhaul rendering

The `Element::render` function now takes a constraint object. This
allows the parent element to influence how the child is rendered. This
is useful eg. to have elements take up more space than they need to fill
some space.

We also make various changes needed for pretty diffs, such as merging
styles, a new `Filled` type, and some color theming.
This commit is contained in:
cloudhead 2023-09-25 17:43:23 +02:00
parent b482ae85f0
commit 7abfd4870c
No known key found for this signature in database
14 changed files with 615 additions and 174 deletions

View File

@ -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
```

View File

@ -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;

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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,
}
}
}

View File

@ -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);
/// 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),
}
if let Some(h) = lim.height {
self.rows = self.rows.min(h);
}
self
}
}

View File

@ -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())
}
}

View File

@ -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());

View File

@ -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.

View File

@ -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::*;

View File

@ -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"

View File

@ -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()

View File

@ -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)));
}
/// 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() {
cols += 2;
rows += 2;
}
Size::new(cols, rows).constrain(c)
}
}
impl<'a> Element for VStack<'a> {
fn size(&self) -> Size {
if self.opts.border.is_some() {
Size::new(self.width + 4, self.height + 2)
} else {
Size::new(self.width, self.height)
}
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()
);
}
}