use std::fmt;
use std::io::IsTerminal;
use std::ops::Deref;
use std::{io, vec};
use crate::cell::Cell;
use crate::{viewport, Color, Filled, Label, Style};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Constraint {
pub min: Size,
pub max: Size,
}
impl Default for Constraint {
fn default() -> Self {
Self::UNBOUNDED
}
}
impl Constraint {
pub const UNBOUNDED: Self = Self {
min: Size::MIN,
max: Size::MAX,
};
pub fn new(min: Size, max: Size) -> Self {
assert!(min.cols <= max.cols && min.rows <= max.rows);
Self { min, max }
}
pub fn max(max: Size) -> Self {
Self {
min: Size::MIN,
max,
}
}
pub fn tight(cols: usize) -> Self {
Self {
min: Size::new(cols, 1),
max: Size::new(cols, usize::MAX),
}
}
pub fn from_env() -> Option<Self> {
if io::stdout().is_terminal() {
Some(Self::max(viewport().unwrap_or(Size::MAX)))
} else {
None
}
}
}
pub trait Element: fmt::Debug + Send + Sync {
fn size(&self, parent: Constraint) -> Size;
#[must_use]
fn render(&self, parent: Constraint) -> Vec<Line>;
fn columns(&self, parent: Constraint) -> usize {
self.size(parent).cols
}
fn rows(&self, parent: Constraint) -> usize {
self.size(parent).rows
}
fn print(&self) {
for line in self.render(Constraint::from_env().unwrap_or_default()) {
println!("{}", line.to_string().trim_end());
}
}
fn write(&self, constraints: Constraint) -> io::Result<()>
where
Self: Sized,
{
self::write_to(self, &mut io::stdout(), constraints)
}
#[must_use]
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<'a> Element for Box<dyn Element + 'a> {
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()
}
}
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(())
}
#[derive(Clone, Default, Debug)]
pub struct Line {
items: Vec<Label>,
}
impl Line {
pub fn new(item: impl Into<Label>) -> Self {
Self {
items: vec![item.into()],
}
}
pub fn blank() -> Self {
Self { items: vec![] }
}
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(),
}
}
pub fn spaced(items: impl IntoIterator<Item = Label>) -> Self {
let mut line = Self::default();
for item in items.into_iter() {
if item.is_blank() {
continue;
}
line.push(item);
line.push(Label::space());
}
line.items.pop();
line
}
pub fn item(mut self, item: impl Into<Label>) -> Self {
self.push(item);
self
}
pub fn extend(mut self, items: impl IntoIterator<Item = Label>) -> Self {
self.items.extend(items);
self
}
pub fn push(&mut self, item: impl Into<Label>) {
self.items.push(item.into());
}
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));
}
}
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);
}
}
}
pub fn width(&self) -> usize {
self.items.iter().map(Cell::width).sum()
}
pub fn space(mut self) -> Self {
self.items.push(Label::space());
self
}
pub fn boxed(self) -> Box<dyn Element> {
Box::new(self)
}
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(())
}
}
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
pub struct Size {
pub cols: usize,
pub rows: usize,
}
impl Size {
pub const MIN: Self = Self {
cols: usize::MIN,
rows: usize::MIN,
};
pub const MAX: Self = Self {
cols: usize::MAX,
rows: usize::MAX,
};
pub fn new(cols: usize, rows: usize) -> Self {
Self { cols, rows }
}
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() {
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}");
}
}