use std::fmt;
use std::io::IsTerminal;
use std::ops::Deref;
use std::{io, vec};
use crate::cell::Cell;
use crate::{Color, Filled, Label, Style, viewport};
/// 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 column size.
/// The rows are unconstrained.
pub fn tight(cols: usize) -> Self {
Self {
min: Size::new(cols, 1),
max: Size::new(cols, usize::MAX),
}
}
/// Create a constraint from the terminal environment.
/// Returns [`None`] if the output device is not a terminal.
pub fn from_env() -> Option<Self> {
if io::stdout().is_terminal() {
Some(Self::max(viewport().unwrap_or(Size::MAX)))
} else {
None
}
}
}
/// A text element that has a size and can be rendered to the terminal.
pub trait Element: fmt::Debug + Send + Sync {
/// Get the size of the element, in rows and columns.
fn size(&self, parent: Constraint) -> Size;
#[must_use]
/// Render the element as lines of text that can be printed.
fn render(&self, parent: Constraint) -> Vec<Line>;
/// Get the number of columns occupied by this element.
fn columns(&self, parent: Constraint) -> usize {
self.size(parent).cols
}
/// Get the number of rows occupied by this element.
fn rows(&self, parent: Constraint) -> usize {
self.size(parent).rows
}
/// Print this element to stdout.
fn print(&self) {
use std::io::Write;
let mut stdout = io::stdout().lock();
for line in self.render(Constraint::from_env().unwrap_or_default()) {
let _ = writeln!(stdout, "{}", line.to_string().trim_end())
.or_else(crate::io::swallow_broken_pipe_stdout);
}
}
/// Write using the given constraints to `stdout`.
fn write(&self, constraints: Constraint) -> io::Result<()>
where
Self: Sized,
{
self::write_to(self, &mut io::stdout(), constraints)
}
#[must_use]
/// Return a string representation of this element.
fn display(&self, constraints: Constraint) -> String {
let mut out = String::new();
for line in self.render(constraints) {
out.extend(line.into_iter().map(|l| l.to_string()));
out.push('\n');
}
out
}
}
impl Element for Box<dyn Element + '_> {
fn size(&self, parent: Constraint) -> Size {
self.deref().size(parent)
}
fn render(&self, parent: Constraint) -> Vec<Line> {
self.deref().render(parent)
}
fn print(&self) {
self.deref().print()
}
}
impl<T: Element> Element for &T {
fn size(&self, parent: Constraint) -> Size {
(*self).size(parent)
}
fn render(&self, parent: Constraint) -> Vec<Line> {
(*self).render(parent)
}
fn print(&self) {
(*self).print()
}
}
/// Write using the given constraints, to a writer.
pub fn write_to(
elem: &impl Element,
writer: &mut impl io::Write,
constraints: Constraint,
) -> io::Result<()> {
for line in elem.render(constraints) {
writeln!(writer, "{}", line.to_string().trim_end())?;
}
Ok(())
}
/// A line of text that has styling and can be displayed.
#[derive(Clone, Default, Debug)]
pub struct Line {
items: Vec<Label>,
}
impl Line {
/// Create a new line.
pub fn new(item: impl Into<Label>) -> Self {
Self {
items: vec![item.into()],
}
}
/// Create a blank line.
pub fn blank() -> Self {
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.
// TODO: Make this impl trivial once [`Iterator::intersperse`] is stable.
pub fn spaced(items: impl IntoIterator<Item = Label>) -> Self {
let iter = items.into_iter();
let mut line = Self {
items: Vec::with_capacity({
let (min, max) = iter.size_hint();
let likely = max.unwrap_or(min);
// Technically (likely + (likely - 1)), but we push the last space before
// we pop it again, so we need that additional space anyways.
likely * 2
}),
};
// Don't create spaces around empty labels.
for item in iter.filter(|i| !i.is_blank()) {
line.push(item);
line.push(Label::space());
}
line.items.pop();
line
}
/// Add an item to this line.
pub fn item(mut self, item: impl Into<Label>) -> Self {
self.push(item);
self
}
/// Add multiple items to this line.
pub fn extend(mut self, items: impl IntoIterator<Item = Label>) -> Self {
self.items.extend(items);
self
}
/// Add an item to this line.
pub fn push(&mut self, item: impl Into<Label>) {
self.items.push(item.into());
}
/// Pad this line to occupy the given width.
pub fn pad(&mut self, width: usize) {
let w = self.width();
if width > w {
let pad = width - w;
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.width() > width {
let total = self.width();
if total - self.items.last().map_or(0, Cell::width) > width {
self.items.pop();
} else if let Some(item) = self.items.last_mut() {
*item = item.truncate(width - (total - Cell::width(item)), delim);
}
}
}
/// 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 = Box<dyn Iterator<Item = Label>>;
fn into_iter(self) -> Self::IntoIter {
Box::new(self.items.into_iter())
}
}
impl<T: Into<Label>> From<T> for Line {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl From<Vec<Label>> for Line {
fn from(items: Vec<Label>) -> Self {
Self { items }
}
}
impl Element for Line {
fn size(&self, _parent: Constraint) -> Size {
Size::new(self.items.iter().map(Cell::width).sum(), 1)
}
fn render(&self, _parent: Constraint) -> Vec<Line> {
vec![self.clone()]
}
}
impl Element for Vec<Line> {
fn size(&self, parent: Constraint) -> Size {
let width = self
.iter()
.map(|e| e.columns(parent))
.max()
.unwrap_or_default();
let height = self.len();
Size::new(width, height)
}
fn render(&self, parent: Constraint) -> Vec<Line> {
self.iter()
.cloned()
.flat_map(|l| l.render(parent))
.collect()
}
}
impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for item in &self.items {
write!(f, "{item}")?;
}
Ok(())
}
}
/// Size of a text element, in columns and rows.
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
pub struct Size {
/// Columns occupied.
pub cols: usize,
/// Rows occupied.
pub rows: usize,
}
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 }
}
/// 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),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_truncate() {
let line = Line::default().item("banana").item("peach").item("apple");
let mut actual = line.clone();
actual = actual.truncate(9, "…");
assert_eq!(actual.to_string(), "bananape…");
let mut actual = line.clone();
actual = actual.truncate(7, "…");
assert_eq!(actual.to_string(), "banana…");
let mut actual = line.clone();
actual = actual.truncate(1, "…");
assert_eq!(actual.to_string(), "…");
let mut actual = line;
actual = actual.truncate(0, "…");
assert_eq!(actual.to_string(), "");
}
#[test]
fn test_width() {
// Nb. This might not display correctly in some editors or terminals.
let line = Line::new("Radicle Heartwood Protocol & Stack ❤️🪵");
assert_eq!(line.width(), 39, "{line}");
let line = Line::new("❤\u{fe0f}");
assert_eq!(line.width(), 2, "{line}");
let line = Line::new("❤️");
assert_eq!(line.width(), 2, "{line}");
}
#[test]
fn test_spaced() {
let line = Line::spaced(["banana", "peach", "apple"].into_iter().map(Label::new));
let iterated: Vec<_> = line.into_iter().collect();
assert_eq!(
iterated
.into_iter()
.map(|l| l.to_string())
.collect::<Vec<_>>(),
["banana", " ", "peach", " ", "apple"]
);
}
}