use crate::box_drawing::Line;
use crate::console::RenderContext;
use crate::panel::BorderStyle;
use crate::renderable::{Renderable, Segment};
use crate::style::Style;
use crate::text::{Span, Text};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColumnAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColumnWidth {
#[default]
Auto,
Fixed(usize),
Min(usize),
Max(usize),
}
#[derive(Debug, Clone)]
pub struct Column {
pub header: String,
pub align: ColumnAlign,
pub width: ColumnWidth,
pub header_style: Style,
pub style: Style,
pub wrap: bool,
#[allow(dead_code)]
min_width: usize,
#[allow(dead_code)]
max_width: usize,
}
impl Column {
pub fn new(header: &str) -> Self {
let header_width = UnicodeWidthStr::width(header);
Column {
header: header.to_string(),
align: ColumnAlign::Left,
width: ColumnWidth::Auto,
header_style: Style::new().bold(),
style: Style::new(),
wrap: true,
min_width: header_width,
max_width: header_width,
}
}
pub fn align(mut self, align: ColumnAlign) -> Self {
self.align = align;
self
}
pub fn width(mut self, width: ColumnWidth) -> Self {
self.width = width;
self
}
pub fn header_style(mut self, style: Style) -> Self {
self.header_style = style;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn center(self) -> Self {
self.align(ColumnAlign::Center)
}
pub fn right(self) -> Self {
self.align(ColumnAlign::Right)
}
}
#[derive(Debug, Clone)]
pub struct Row {
cells: Vec<Text>,
style: Option<Style>,
}
impl Row {
pub fn new<I, T>(cells: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Text>,
{
Row {
cells: cells.into_iter().map(Into::into).collect(),
style: None,
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
}
#[derive(Debug, Clone)]
pub struct Table {
columns: Vec<Column>,
rows: Vec<Row>,
border_style: BorderStyle,
style: Style,
show_header: bool,
show_border: bool,
show_row_lines: bool,
padding: usize,
title: Option<String>,
expand: bool,
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
impl Table {
pub fn new() -> Self {
Table {
columns: Vec::new(),
rows: Vec::new(),
border_style: BorderStyle::Rounded,
style: Style::new(),
show_header: true,
show_border: true,
show_row_lines: false,
padding: 1,
title: None,
expand: false,
}
}
pub fn add_column<C: Into<Column>>(&mut self, column: C) -> &mut Self {
self.columns.push(column.into());
self
}
pub fn column(mut self, header: &str) -> Self {
self.columns.push(Column::new(header));
self
}
pub fn columns<I, S>(mut self, headers: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for header in headers {
self.columns.push(Column::new(header.as_ref()));
}
self
}
pub fn add_row<I, T>(&mut self, cells: I) -> &mut Self
where
I: IntoIterator<Item = T>,
T: Into<Text>,
{
self.rows.push(Row::new(cells));
self
}
pub fn add_row_strs(&mut self, cells: &[&str]) -> &mut Self {
let text_cells: Vec<Text> = cells.iter().map(|s| Text::plain(s.to_string())).collect();
self.rows.push(Row {
cells: text_cells,
style: None,
});
self
}
pub fn add_row_obj(&mut self, row: Row) -> &mut Self {
self.rows.push(row);
self
}
pub fn border_style(mut self, style: BorderStyle) -> Self {
self.border_style = style;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn set_title(mut self, title: &str) -> Self {
self.title = Some(title.to_string());
self
}
pub fn show_header(mut self, show: bool) -> Self {
self.show_header = show;
self
}
pub fn show_border(mut self, show: bool) -> Self {
self.show_border = show;
self
}
pub fn show_row_lines(mut self, show: bool) -> Self {
self.show_row_lines = show;
self
}
pub fn padding(mut self, padding: usize) -> Self {
self.padding = padding;
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_string());
self
}
pub fn expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
fn calculate_widths(&self, available_width: usize) -> Vec<usize> {
let num_cols = self.columns.len();
if num_cols == 0 {
return vec![];
}
let mut max_widths: Vec<usize> = self
.columns
.iter()
.map(|c| UnicodeWidthStr::width(c.header.as_str()))
.collect();
for row in &self.rows {
for (i, cell) in row.cells.iter().enumerate() {
if i < max_widths.len() {
max_widths[i] = max_widths[i].max(cell.width());
}
}
}
let overhead = if self.show_border {
1 + num_cols + 1 + (self.padding * 2 * num_cols)
} else {
(num_cols - 1) + (self.padding * 2 * num_cols)
};
let content_width = available_width.saturating_sub(overhead);
let total_content: usize = max_widths.iter().sum();
if total_content == 0 {
return vec![content_width / num_cols.max(1); num_cols];
}
if total_content <= content_width {
if self.expand {
let extra = content_width - total_content;
let per_col = extra / num_cols;
max_widths.iter().map(|w| w + per_col).collect()
} else {
max_widths
}
} else {
max_widths
.iter()
.map(|w| {
let ratio = *w as f64 / total_content as f64;
((content_width as f64 * ratio) as usize).max(1)
})
.collect()
}
}
fn render_horizontal_line(&self, widths: &[usize], line: &Line) -> Segment {
let mut spans = vec![Span::styled(line.left.to_string(), self.style)];
for (i, &width) in widths.iter().enumerate() {
let cell_width = width + self.padding * 2;
spans.push(Span::styled(
line.mid.to_string().repeat(cell_width),
self.style,
));
if i < widths.len() - 1 {
spans.push(Span::styled(line.cross.to_string(), self.style));
}
}
spans.push(Span::styled(line.right.to_string(), self.style));
Segment::line(spans)
}
fn render_row(
&self,
cells: &[Text],
widths: &[usize],
line: &Line,
cell_styles: &[Style],
) -> Vec<Segment> {
let mut spans = Vec::new();
if self.show_border {
spans.push(Span::styled(line.left.to_string(), self.style));
}
for (i, width) in widths.iter().enumerate() {
let cell = cells.get(i);
let content = cell.map(|c| c.plain_text()).unwrap_or_default();
let _content_width = UnicodeWidthStr::width(content.as_str());
let cell_style = cell_styles.get(i).copied().unwrap_or_default();
let align = self.columns.get(i).map(|c| c.align).unwrap_or_default();
let padded = pad_string(&content, *width, align);
spans.push(Span::raw(" ".repeat(self.padding)));
spans.push(Span::styled(padded, cell_style));
spans.push(Span::raw(" ".repeat(self.padding)));
if i < widths.len() - 1 {
spans.push(Span::styled(line.cross.to_string(), self.style));
} else if self.show_border {
spans.push(Span::styled(line.right.to_string(), self.style));
}
}
vec![Segment::line(spans)]
}
}
fn pad_string(s: &str, width: usize, align: ColumnAlign) -> String {
let content_width = UnicodeWidthStr::width(s);
if content_width >= width {
return truncate_string(s, width);
}
let padding = width - content_width;
match align {
ColumnAlign::Left => format!("{}{}", s, " ".repeat(padding)),
ColumnAlign::Right => format!("{}{}", " ".repeat(padding), s),
ColumnAlign::Center => {
let left = padding / 2;
let right = padding - left;
format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
}
}
}
fn truncate_string(s: &str, width: usize) -> String {
use unicode_segmentation::UnicodeSegmentation;
let mut result = String::new();
let mut current_width = 0;
for grapheme in s.graphemes(true) {
let grapheme_width = UnicodeWidthStr::width(grapheme);
if current_width + grapheme_width > width {
if width > 1 && current_width < width {
result.push('…');
}
break;
}
result.push_str(grapheme);
current_width += grapheme_width;
}
while current_width < width {
result.push(' ');
current_width += 1;
}
result
}
impl From<&str> for Column {
fn from(s: &str) -> Self {
Column::new(s)
}
}
impl From<String> for Column {
fn from(s: String) -> Self {
Column::new(&s)
}
}
impl Renderable for Table {
fn render(&self, context: &RenderContext) -> Vec<Segment> {
if self.columns.is_empty() {
return vec![];
}
let box_chars = self.border_style.to_box();
let widths = self.calculate_widths(context.width);
let mut segments = Vec::new();
let content_width: usize = widths.iter().map(|w| w + self.padding * 2).sum();
let border_overhead = if self.show_border {
widths.len() + 1
} else {
widths.len() - 1
};
let table_width = content_width + border_overhead;
if let Some(title) = &self.title {
let title_width = UnicodeWidthStr::width(title.as_str());
if title_width <= table_width {
let padding = table_width - title_width;
let left_pad = padding / 2;
let right_pad = padding - left_pad;
let mut spans = Vec::new();
if left_pad > 0 {
spans.push(Span::raw(" ".repeat(left_pad)));
}
spans.push(Span::styled(title.clone(), Style::new().bold()));
if right_pad > 0 {
spans.push(Span::raw(" ".repeat(right_pad)));
}
segments.push(Segment::line(spans));
} else {
segments.push(Segment::line(vec![Span::styled(
title.clone(),
Style::new().bold(),
)]));
}
}
if self.show_border {
segments.push(self.render_horizontal_line(&widths, &box_chars.top));
}
if self.show_header {
let header_cells: Vec<Text> = self
.columns
.iter()
.map(|c| Text::styled(c.header.clone(), c.header_style))
.collect();
let header_styles: Vec<Style> = self.columns.iter().map(|c| c.header_style).collect();
segments.extend(self.render_row(
&header_cells,
&widths,
&box_chars.header,
&header_styles,
));
if self.show_border || self.show_row_lines {
segments.push(self.render_horizontal_line(&widths, &box_chars.head));
}
}
for (row_idx, row) in self.rows.iter().enumerate() {
let cell_styles: Vec<Style> = self.columns.iter().map(|c| c.style).collect();
segments.extend(self.render_row(&row.cells, &widths, &box_chars.cell, &cell_styles));
if self.show_row_lines && row_idx < self.rows.len() - 1 {
segments.push(self.render_horizontal_line(&widths, &box_chars.mid));
}
}
if self.show_border {
segments.push(self.render_horizontal_line(&widths, &box_chars.bottom));
}
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_basic() {
let mut table = Table::new();
table.add_column("Name");
table.add_column("Age");
table.add_row_strs(&["Alice", "30"]);
table.add_row_strs(&["Bob", "25"]);
let context = RenderContext {
width: 40,
height: None,
};
let segments = table.render(&context);
assert!(!segments.is_empty());
let text: String = segments.iter().map(|s| s.plain_text()).collect();
assert!(text.contains("Name"));
assert!(text.contains("Alice"));
assert!(text.contains("Bob"));
}
#[test]
fn test_table_builder() {
let table = Table::new()
.columns(["A", "B", "C"])
.border_style(BorderStyle::Square);
assert_eq!(table.columns.len(), 3);
}
#[test]
fn test_pad_string() {
assert_eq!(pad_string("hi", 5, ColumnAlign::Left), "hi ");
assert_eq!(pad_string("hi", 5, ColumnAlign::Right), " hi");
assert_eq!(pad_string("hi", 5, ColumnAlign::Center), " hi ");
}
}