use crate::segment::{Segment, Segments};
use crate::text::Text;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ColumnAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Columns {
items: Vec<Text>,
padding: (usize, usize, usize, usize),
width: Option<usize>,
expand: bool,
equal: bool,
column_first: bool,
right_to_left: bool,
align: ColumnAlign,
title: Option<Text>,
}
impl Default for Columns {
#[inline]
fn default() -> Self {
Self::new(Vec::<&str>::new())
}
}
impl Columns {
#[must_use]
#[inline]
pub fn new<I, T>(items: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Text>,
{
Self {
items: items.into_iter().map(Into::into).collect(),
padding: (0, 0, 1, 0), width: None,
expand: false,
equal: false,
column_first: false,
right_to_left: false,
align: ColumnAlign::Left,
title: None,
}
}
#[must_use]
#[inline]
pub fn add<T: Into<Text>>(mut self, item: T) -> Self {
self.items.push(item.into());
self
}
#[must_use]
#[inline]
pub const fn padding(mut self, left: usize, top: usize, right: usize, bottom: usize) -> Self {
self.padding = (left, top, right, bottom);
self
}
#[must_use]
#[inline]
pub const fn padding_horizontal(mut self, padding: usize) -> Self {
self.padding.0 = padding;
self.padding.2 = padding;
self
}
#[must_use]
#[inline]
pub const fn padding_vertical(mut self, padding: usize) -> Self {
self.padding.1 = padding;
self.padding.3 = padding;
self
}
#[must_use]
#[inline]
pub const fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
#[must_use]
#[inline]
pub const fn expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
#[must_use]
#[inline]
pub const fn equal(mut self, equal: bool) -> Self {
self.equal = equal;
self
}
#[must_use]
#[inline]
pub const fn column_first(mut self, column_first: bool) -> Self {
self.column_first = column_first;
self
}
#[must_use]
#[inline]
pub const fn right_to_left(mut self, rtl: bool) -> Self {
self.right_to_left = rtl;
self
}
#[must_use]
#[inline]
pub const fn align(mut self, align: ColumnAlign) -> Self {
self.align = align;
self
}
#[must_use]
#[inline]
pub fn title<T: Into<Text>>(mut self, title: T) -> Self {
self.title = Some(title.into());
self
}
fn text_width(text: &Text) -> usize {
text.plain().width()
}
fn item_widths(&self) -> Vec<usize> {
let pad_h = self.padding.0.saturating_add(self.padding.2);
self.items
.iter()
.map(|item| Self::text_width(item).saturating_add(pad_h))
.collect()
}
fn calculate_columns(&self, max_width: usize) -> usize {
if self.items.is_empty() {
return 0;
}
let widths = self.item_widths();
let max_item_width = widths.iter().copied().max().unwrap_or(1);
if self.equal {
max_width.checked_div(max_item_width).unwrap_or(1).max(1)
} else {
let total_width: usize = widths.iter().sum();
let avg_width = total_width.checked_div(widths.len()).unwrap_or(1).max(1);
max_width.checked_div(avg_width).unwrap_or(1).max(1)
}
}
#[must_use]
pub fn render(&self, max_width: usize) -> Segments {
let mut segments = Segments::new();
if self.items.is_empty() {
return segments;
}
let render_width = self.width.unwrap_or(max_width);
let num_columns = self.calculate_columns(render_width);
let widths = self.item_widths();
let pad_h = self.padding.0.saturating_add(self.padding.2);
let column_width = if self.equal {
let max_item_width = widths.iter().copied().max().unwrap_or(1);
if self.expand {
render_width
.checked_div(num_columns)
.unwrap_or(max_item_width)
} else {
max_item_width
}
} else {
widths.iter().copied().max().unwrap_or(1)
};
let mut rows: Vec<Vec<usize>> = Vec::new();
if self.column_first {
let num_items = self.items.len();
let num_rows = num_items
.saturating_add(num_columns)
.saturating_sub(1)
.checked_div(num_columns)
.unwrap_or(1);
for row_idx in 0..num_rows {
let mut row = Vec::new();
for col_idx in 0..num_columns {
let item_idx = col_idx.saturating_mul(num_rows).saturating_add(row_idx);
if item_idx < num_items {
row.push(item_idx);
}
}
if !row.is_empty() {
rows.push(row);
}
}
} else {
for chunk in self
.items
.iter()
.enumerate()
.collect::<Vec<_>>()
.chunks(num_columns)
{
rows.push(chunk.iter().map(|(idx, _)| *idx).collect());
}
}
for (row_idx, row) in rows.iter().enumerate() {
if row_idx == 0 {
for _ in 0..self.padding.1 {
segments.push(Segment::newline());
}
}
let row_items: Vec<usize> = if self.right_to_left {
row.iter().copied().rev().collect()
} else {
row.clone()
};
for (col_idx, &item_idx) in row_items.iter().enumerate() {
if let Some(item) = self.items.get(item_idx) {
if self.padding.0 > 0 {
segments.push(Segment::new(" ".repeat(self.padding.0)));
}
let content_width = Self::text_width(item);
let cell_width = column_width.saturating_sub(pad_h);
let (left_pad, right_pad) = match self.align {
ColumnAlign::Left => (0, cell_width.saturating_sub(content_width)),
ColumnAlign::Right => (cell_width.saturating_sub(content_width), 0),
ColumnAlign::Center => {
let total_pad = cell_width.saturating_sub(content_width);
let left = total_pad.checked_div(2).unwrap_or(0);
(left, total_pad.saturating_sub(left))
}
};
if left_pad > 0 {
segments.push(Segment::new(" ".repeat(left_pad)));
}
segments.extend(item.to_segments().into_iter());
if right_pad > 0 && col_idx < row_items.len().saturating_sub(1) {
segments.push(Segment::new(" ".repeat(right_pad)));
}
if self.padding.2 > 0 && col_idx < row_items.len().saturating_sub(1) {
segments.push(Segment::new(" ".repeat(self.padding.2)));
}
}
}
segments.push(Segment::newline());
for _ in 0..self.padding.3 {
segments.push(Segment::newline());
}
}
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_columns_new() {
let items = vec!["a", "b", "c"];
let columns = Columns::new(items);
assert_eq!(columns.items.len(), 3);
assert!(!columns.equal);
assert!(!columns.expand);
assert!(!columns.column_first);
assert!(!columns.right_to_left);
}
#[test]
fn test_columns_add() {
let columns = Columns::new(vec!["a", "b"]).add("c");
assert_eq!(columns.items.len(), 3);
}
#[test]
fn test_columns_add_multiple() {
let columns = Columns::new(vec!["a"]).add("b").add("c").add("d");
assert_eq!(columns.items.len(), 4);
}
#[test]
fn test_columns_equal() {
let columns = Columns::new(vec!["short", "verylongitem"]).equal(true);
assert!(columns.equal);
}
#[test]
fn test_columns_equal_false() {
let columns = Columns::new(vec!["short", "verylongitem"]).equal(false);
assert!(!columns.equal);
}
#[test]
fn test_columns_expand() {
let columns = Columns::new(vec!["a", "b"]).expand(true);
assert!(columns.expand);
}
#[test]
fn test_columns_render_basic() {
let items = vec!["Item 1", "Item 2", "Item 3"];
let columns = Columns::new(items);
let segments = columns.render(80);
let output = segments.to_ansi();
assert!(output.contains("Item 1"));
assert!(output.contains("Item 2"));
assert!(output.contains("Item 3"));
}
#[test]
fn test_columns_empty() {
let columns = Columns::new(Vec::<&str>::new());
let segments = columns.render(80);
assert!(segments.to_ansi().is_empty());
}
#[test]
fn test_columns_single_item() {
let columns = Columns::new(vec!["single"]);
let segments = columns.render(80);
assert!(segments.to_ansi().contains("single"));
}
#[test]
fn test_columns_alignment_left() {
let columns = Columns::new(vec!["a", "b"])
.align(ColumnAlign::Left)
.equal(true);
assert_eq!(columns.align, ColumnAlign::Left);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_alignment_center() {
let columns = Columns::new(vec!["a", "b"])
.align(ColumnAlign::Center)
.equal(true);
assert_eq!(columns.align, ColumnAlign::Center);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_alignment_right() {
let columns = Columns::new(vec!["a", "b"])
.align(ColumnAlign::Right)
.equal(true);
assert_eq!(columns.align, ColumnAlign::Right);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_rtl() {
let columns = Columns::new(vec!["first", "second"]).right_to_left(true);
assert!(columns.right_to_left);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_column_first() {
let columns = Columns::new(vec!["1", "2", "3", "4"]).column_first(true);
assert!(columns.column_first);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_padding() {
let columns = Columns::new(vec!["a"]).padding(1, 2, 3, 4);
assert_eq!(columns.padding, (1, 2, 3, 4));
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_width() {
let columns = Columns::new(vec!["a"]).width(40);
assert_eq!(columns.width, Some(40));
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_title() {
let columns = Columns::new(vec!["a", "b"]).title("My Columns");
assert!(columns.title.is_some());
}
#[test]
fn test_columns_render_equal_width() {
let columns = Columns::new(vec!["short", "verylongitem", "medium"]).equal(true);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_render_expand() {
let columns = Columns::new(vec!["a", "b", "c"]).expand(true).equal(true);
let segments = columns.render(100);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_render_narrow() {
let columns = Columns::new(vec!["item1", "item2", "item3"]);
let segments = columns.render(20);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_render_with_padding() {
let columns = Columns::new(vec!["a", "b", "c"]).padding(2, 1, 2, 1);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_render_column_first() {
let columns = Columns::new(vec!["1", "2", "3", "4", "5", "6"]).column_first(true);
let segments = columns.render(40);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_render_rtl() {
let columns = Columns::new(vec!["first", "second", "third"]).right_to_left(true);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_columns_many_items() {
let items: Vec<String> = (0..20).map(|i| format!("item{}", i)).collect();
let columns = Columns::new(items);
let segments = columns.render(80);
assert!(!segments.is_empty());
}
#[test]
fn test_column_align_default() {
let align = ColumnAlign::default();
assert_eq!(align, ColumnAlign::Left);
}
}