#![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;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Direction {
LeftToRight,
TopToBottom,
}
#[derive(PartialEq, Eq, Debug)]
pub enum Filling {
Spaces(usize),
Text(String),
}
impl Filling {
fn width(&self) -> usize {
match self {
Filling::Spaces(w) => *w,
Filling::Text(t) => ansi_width(t),
}
}
}
#[derive(Debug)]
pub struct GridOptions {
pub direction: Direction,
pub filling: Filling,
pub width: usize,
}
#[derive(PartialEq, Eq, Debug)]
struct Dimensions {
num_lines: 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> {
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 width = options.width;
let mut grid = Self {
options,
cells,
widths,
widest_cell_width,
dimensions: Dimensions {
num_lines: 0,
widths: Vec::new(),
},
};
grid.dimensions = grid.width_dimensions(width).unwrap_or(Dimensions {
num_lines: grid.cells.len(),
widths: vec![widest_cell_width],
});
grid
}
pub fn width(&self) -> usize {
self.dimensions.total_width(self.options.filling.width())
}
pub fn row_count(&self) -> usize {
self.dimensions.num_lines
}
pub fn column_widths(&self) -> &[usize] {
&self.dimensions.widths
}
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_lines,
widths: column_widths,
}
}
fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize {
let mut widths = self.widths.clone();
widths.sort_unstable_by(|a, b| b.cmp(a));
let mut col_total_width_so_far = 0;
for (i, &width) in widths.iter().enumerate() {
let adjusted_width = if i == 0 {
width
} else {
width + self.options.filling.width()
};
if col_total_width_so_far + adjusted_width <= maximum_width {
col_total_width_so_far += adjusted_width;
} else {
return div_ceil(self.cells.len(), i);
}
}
1
}
fn width_dimensions(&self, maximum_width: usize) -> Option<Dimensions> {
if self.widest_cell_width > maximum_width {
return None;
}
if self.cells.is_empty() {
return Some(Dimensions {
num_lines: 0,
widths: Vec::new(),
});
}
if self.cells.len() == 1 {
let cell_widths = self.widths[0];
return Some(Dimensions {
num_lines: 1,
widths: vec![cell_widths],
});
}
let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width);
if theoretical_max_num_lines == 1 {
return Some(Dimensions {
num_lines: 1,
widths: self.widths.clone(),
});
}
let mut smallest_dimensions_yet = None;
for num_lines in (1..=theoretical_max_num_lines).rev() {
let num_columns = div_ceil(self.cells.len(), num_lines);
let total_separator_width = (num_columns - 1) * self.options.filling.width();
if maximum_width < total_separator_width {
continue;
}
let adjusted_width = maximum_width - total_separator_width;
let potential_dimensions = self.compute_dimensions(num_lines, num_columns);
if potential_dimensions.widths.iter().sum::<usize>() <= adjusted_width {
smallest_dimensions_yet = Some(potential_dimensions);
} else {
break;
}
}
smallest_dimensions_yet
}
}
impl<T: AsRef<str>> fmt::Display for Grid<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let separator = match &self.options.filling {
Filling::Spaces(n) => " ".repeat(*n),
Filling::Text(s) => s.clone(),
};
let padding = " ".repeat(self.widest_cell_width);
for y in 0..self.dimensions.num_lines {
for x in 0..self.dimensions.widths.len() {
let num = match self.options.direction {
Direction::LeftToRight => y * self.dimensions.widths.len() + x,
Direction::TopToBottom => y + self.dimensions.num_lines * x,
};
if num >= self.cells.len() {
continue;
}
let contents = &self.cells[num];
let width = self.widths[num];
let last_in_row = x == self.dimensions.widths.len() - 1;
let col_width = self.dimensions.widths[x];
let padding_size = col_width - width;
f.write_str(contents.as_ref())?;
if !last_in_row {
if padding_size > 0 {
f.write_str(&padding[0..padding_size])?;
}
f.write_str(&separator)?;
}
}
f.write_str("\n")?;
}
Ok(())
}
}
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
}
}