#![warn(future_incompatible)]
#![warn(missing_copy_implementations)]
#![warn(missing_docs)]
#![warn(nonstandard_style)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused)]
#![deny(unsafe_code)]
#![doc = include_str!("../README.md")]
use ansi_width::ansi_width;
use std::fmt;
pub const SPACES_IN_TAB: usize = 8;
pub const DEFAULT_SEPARATOR_SIZE: usize = 2;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Direction {
LeftToRight,
TopToBottom,
}
#[derive(PartialEq, Eq, Debug)]
pub enum Filling {
Spaces(usize),
Text(String),
Tabs {
spaces: usize,
tab_size: usize,
},
}
impl Filling {
fn width(&self) -> usize {
match self {
Self::Spaces(w) => *w,
Self::Text(t) => ansi_width(t),
Self::Tabs { spaces, .. } => *spaces,
}
}
}
#[derive(Debug)]
pub struct GridOptions {
pub direction: Direction,
pub filling: Filling,
pub width: usize,
}
#[derive(PartialEq, Eq, Debug)]
struct Dimensions {
num_rows: usize,
widths: Vec<usize>,
}
impl Dimensions {
fn total_width(&self, separator_width: usize) -> usize {
if self.widths.is_empty() {
0
} else {
let values = self.widths.iter().sum::<usize>();
let separators = separator_width * (self.widths.len() - 1);
values + separators
}
}
}
#[derive(Debug)]
pub struct Grid<T: AsRef<str>> {
options: GridOptions,
cells: Vec<T>,
widths: Vec<usize>,
widest_cell_width: usize,
dimensions: Dimensions,
}
impl<T: AsRef<str>> Grid<T> {
#[must_use]
pub fn new(cells: Vec<T>, options: GridOptions) -> Self {
let widths: Vec<usize> = cells.iter().map(|c| ansi_width(c.as_ref())).collect();
let widest_cell_width = widths.iter().copied().max().unwrap_or(0);
let mut grid = Self {
options,
cells,
widths,
widest_cell_width,
dimensions: Dimensions {
num_rows: 0,
widths: Vec::new(),
},
};
if !grid.cells.is_empty() {
grid.dimensions = grid.width_dimensions();
}
grid
}
#[must_use]
pub fn width(&self) -> usize {
self.dimensions.total_width(self.options.filling.width())
}
#[must_use]
pub const fn row_count(&self) -> usize {
self.dimensions.num_rows
}
#[must_use]
pub fn column_widths(&self) -> &[usize] {
&self.dimensions.widths
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.dimensions.widths.iter().all(|&x| x > 0)
}
fn compute_dimensions(&self, num_lines: usize, num_columns: usize) -> Dimensions {
let mut column_widths = vec![0; num_columns];
for (index, cell_width) in self.widths.iter().copied().enumerate() {
let index = match self.options.direction {
Direction::LeftToRight => index % num_columns,
Direction::TopToBottom => index / num_lines,
};
if cell_width > column_widths[index] {
column_widths[index] = cell_width;
}
}
Dimensions {
num_rows: num_lines,
widths: column_widths,
}
}
fn width_dimensions(&self) -> Dimensions {
if self.cells.len() == 1 {
let cell_widths = self.widths[0];
return Dimensions {
num_rows: 1,
widths: vec![cell_widths],
};
}
let widest_column = self.widest_cell_width + self.options.filling.width();
if widest_column > self.options.width {
return Dimensions {
num_rows: self.cells.len(),
widths: vec![self.widest_cell_width],
};
}
let min_columns = self
.cells
.len()
.min((self.options.width + self.options.filling.width()) / widest_column);
let max_rows = div_ceil(self.cells.len(), min_columns);
let mut potential_dimension = self.compute_dimensions(max_rows, min_columns);
if max_rows == 1 {
return potential_dimension;
}
for num_columns in min_columns + 1..=self.cells.len() {
let Some(adjusted_width) = self
.options
.width
.checked_sub((num_columns - 1) * self.options.filling.width())
else {
break;
};
let num_rows = div_ceil(self.cells.len(), num_columns);
let new_dimension = self.compute_dimensions(num_rows, num_columns);
if new_dimension.widths.iter().sum::<usize>() <= adjusted_width {
potential_dimension = new_dimension;
}
}
potential_dimension
}
}
impl<T: AsRef<str>> fmt::Display for Grid<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
if self.cells.is_empty() {
return Ok(());
}
let (tab_size, separator) = match &self.options.filling {
Filling::Spaces(n) => (0, " ".repeat(*n)),
Filling::Text(s) => (0, s.clone()),
Filling::Tabs { spaces, tab_size } => (*tab_size, " ".repeat(*spaces)),
};
let padding = " ".repeat(self.widest_cell_width + self.options.filling.width());
for y in 0..self.dimensions.num_rows {
let mut cursor: usize = 0;
for x in 0..self.dimensions.widths.len() {
let (current, offset) = match self.options.direction {
Direction::LeftToRight => (y * self.dimensions.widths.len() + x, 1),
Direction::TopToBottom => {
(y + self.dimensions.num_rows * x, self.dimensions.num_rows)
}
};
if current >= self.cells.len() {
break;
}
let last_in_row = x == self.dimensions.widths.len() - 1;
let contents = &self.cells[current];
let width = self.widths[current];
let col_width = self.dimensions.widths[x];
let padding_size = col_width - width;
f.write_str(contents.as_ref())?;
if last_in_row || current + offset >= self.cells.len() {
break;
}
if tab_size == 0 {
f.write_str(&padding[..padding_size])?;
f.write_str(&separator)?;
} else {
cursor += width;
let total_spaces = padding_size + self.options.filling.width();
let closest_tab = tab_size - (cursor % tab_size);
if closest_tab > total_spaces {
f.write_str(&padding[..total_spaces])?;
} else {
let rest_spaces = total_spaces - closest_tab;
let tabs = 1 + (rest_spaces / tab_size);
let spaces = rest_spaces % tab_size;
f.write_str(&"\t".repeat(tabs))?;
f.write_str(&padding[..spaces])?;
}
cursor += total_spaces;
}
}
f.write_str("\n")?;
}
Ok(())
}
}
#[must_use]
pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
let d = lhs / rhs;
let r = lhs % rhs;
if r > 0 && rhs > 0 {
d + 1
} else {
d
}
}