use crate::element::{Component, Element};
use crate::style::{Color, Style};
use super::BorderStyle;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ColumnWidth {
Fixed(u16),
Percent(f32),
#[default]
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CellAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Default)]
pub struct TableCell {
pub content: String,
pub color: Option<Color>,
pub bg_color: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub align: Option<CellAlign>,
}
impl TableCell {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
..Default::default()
}
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn bg_color(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
#[must_use]
pub fn dim(mut self) -> Self {
self.dim = true;
self
}
#[must_use]
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
#[must_use]
pub fn align(mut self, align: CellAlign) -> Self {
self.align = Some(align);
self
}
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
}
impl<S: Into<String>> From<S> for TableCell {
fn from(s: S) -> Self {
TableCell::new(s)
}
}
#[derive(Debug, Clone, Default)]
pub struct Row {
pub cells: Vec<TableCell>,
pub bg_color: Option<Color>,
pub style: Option<Style>,
}
impl Row {
pub fn new<I, C>(cells: I) -> Self
where
I: IntoIterator<Item = C>,
C: Into<TableCell>,
{
Self {
cells: cells.into_iter().map(Into::into).collect(),
..Default::default()
}
}
#[must_use]
pub fn bg_color(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn len(&self) -> usize {
self.cells.len()
}
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
}
impl<I, C> From<I> for Row
where
I: IntoIterator<Item = C>,
C: Into<TableCell>,
{
fn from(cells: I) -> Self {
Row::new(cells)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RowStyle {
#[default]
None,
Striped,
}
#[derive(Debug, Clone)]
pub struct TableProps {
pub header: Option<Row>,
pub rows: Vec<Row>,
pub widths: Vec<ColumnWidth>,
pub column_aligns: Vec<CellAlign>,
pub border_style: BorderStyle,
pub border_color: Option<Color>,
pub column_spacing: u16,
pub header_color: Option<Color>,
pub header_bg_color: Option<Color>,
pub header_bold: bool,
pub row_style: RowStyle,
pub stripe_color: Option<Color>,
pub selected: Option<usize>,
pub selected_color: Option<Color>,
pub selected_bg_color: Option<Color>,
pub row_dividers: bool,
pub width: Option<u16>,
pub bg_color: Option<Color>,
}
impl Default for TableProps {
fn default() -> Self {
Self {
header: None,
rows: Vec::new(),
widths: Vec::new(),
column_aligns: Vec::new(),
border_style: BorderStyle::None,
border_color: None,
column_spacing: 2,
header_color: None,
header_bg_color: None,
header_bold: true,
row_style: RowStyle::None,
stripe_color: Some(Color::DarkGray),
selected: None,
selected_color: None,
selected_bg_color: None,
row_dividers: false,
width: None,
bg_color: None,
}
}
}
impl TableProps {
pub fn new<I, R>(rows: I) -> Self
where
I: IntoIterator<Item = R>,
R: Into<Row>,
{
Self {
rows: rows.into_iter().map(Into::into).collect(),
..Default::default()
}
}
#[must_use]
pub fn header<R: Into<Row>>(mut self, header: R) -> Self {
self.header = Some(header.into());
self
}
#[must_use]
pub fn widths<I: IntoIterator<Item = ColumnWidth>>(mut self, widths: I) -> Self {
self.widths = widths.into_iter().collect();
self
}
#[must_use]
pub fn fixed_widths<I: IntoIterator<Item = u16>>(mut self, widths: I) -> Self {
self.widths = widths.into_iter().map(ColumnWidth::Fixed).collect();
self
}
#[must_use]
pub fn column_aligns<I: IntoIterator<Item = CellAlign>>(mut self, aligns: I) -> Self {
self.column_aligns = aligns.into_iter().collect();
self
}
#[must_use]
pub fn border(mut self, style: BorderStyle) -> Self {
self.border_style = style;
self
}
#[must_use]
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
#[must_use]
pub fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
#[must_use]
pub fn header_color(mut self, color: Color) -> Self {
self.header_color = Some(color);
self
}
#[must_use]
pub fn header_bg_color(mut self, color: Color) -> Self {
self.header_bg_color = Some(color);
self
}
#[must_use]
pub fn header_bold(mut self, bold: bool) -> Self {
self.header_bold = bold;
self
}
#[must_use]
pub fn striped(mut self) -> Self {
self.row_style = RowStyle::Striped;
self
}
#[must_use]
pub fn stripe_color(mut self, color: Color) -> Self {
self.stripe_color = Some(color);
self
}
#[must_use]
pub fn selected(mut self, index: usize) -> Self {
self.selected = Some(index);
self
}
#[must_use]
pub fn selected_style(mut self, color: Option<Color>, bg_color: Option<Color>) -> Self {
self.selected_color = color;
self.selected_bg_color = bg_color;
self
}
#[must_use]
pub fn row_dividers(mut self) -> Self {
self.row_dividers = true;
self
}
#[must_use]
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub fn bg_color(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
fn num_columns(&self) -> usize {
if !self.widths.is_empty() {
self.widths.len()
} else if let Some(ref header) = self.header {
header.len()
} else if let Some(first_row) = self.rows.first() {
first_row.len()
} else {
0
}
}
fn get_align(&self, col: usize) -> CellAlign {
self.column_aligns.get(col).copied().unwrap_or_default()
}
fn get_width(&self, col: usize) -> ColumnWidth {
self.widths.get(col).copied().unwrap_or_default()
}
}
pub struct Table;
impl Component for Table {
type Props = TableProps;
fn render(props: &Self::Props) -> Element {
let num_cols = props.num_columns();
if num_cols == 0 {
return Element::text("");
}
let mut lines: Vec<String> = Vec::new();
if let Some(ref header) = props.header {
lines.push(render_row_string(header, props));
if props.row_dividers || props.border_style != BorderStyle::None {
lines.push(render_divider_string(props));
}
}
for (i, row) in props.rows.iter().enumerate() {
lines.push(render_row_string(row, props));
if props.row_dividers && i < props.rows.len() - 1 {
lines.push(render_divider_string(props));
}
}
let content = lines.join("\n");
let mut style = Style::new();
if let Some(bg) = props.bg_color {
style = style.bg(bg);
}
Element::styled_text(&content, style)
}
}
fn render_row_string(row: &Row, props: &TableProps) -> String {
let num_cols = props.num_columns();
let spacing = " ".repeat(props.column_spacing as usize);
let mut parts: Vec<String> = Vec::new();
for col in 0..num_cols {
let cell = row.cells.get(col);
let cell_text = render_cell_content(cell, col, props);
parts.push(cell_text);
}
parts.join(&spacing)
}
fn render_cell_content(cell: Option<&TableCell>, col: usize, props: &TableProps) -> String {
let content = cell.map(|c| c.content.as_str()).unwrap_or("");
let align = cell
.and_then(|c| c.align)
.unwrap_or_else(|| props.get_align(col));
let width = match props.get_width(col) {
ColumnWidth::Fixed(w) => w as usize,
ColumnWidth::Percent(p) => {
if let Some(table_width) = props.width {
(table_width as f32 * p).floor() as usize
} else {
content.chars().count()
}
}
ColumnWidth::Auto => content.chars().count(),
};
let content_len = content.chars().count();
if content_len >= width {
content.chars().take(width).collect::<String>()
} else {
let padding = width - content_len;
match align {
CellAlign::Left => format!("{}{}", content, " ".repeat(padding)),
CellAlign::Right => format!("{}{}", " ".repeat(padding), content),
CellAlign::Center => {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
format!(
"{}{}{}",
" ".repeat(left_pad),
content,
" ".repeat(right_pad)
)
}
}
}
}
fn render_divider_string(props: &TableProps) -> String {
let num_cols = props.num_columns();
let total_width: usize = (0..num_cols)
.map(|col| match props.get_width(col) {
ColumnWidth::Fixed(w) => w as usize,
ColumnWidth::Percent(p) => {
if let Some(table_width) = props.width {
(table_width as f32 * p).floor() as usize
} else {
10 }
}
ColumnWidth::Auto => 10, })
.sum::<usize>()
+ (num_cols.saturating_sub(1)) * props.column_spacing as usize;
"─".repeat(total_width)
}
#[derive(Debug, Clone, Default)]
pub struct TableState {
pub selected: usize,
pub row_count: usize,
}
impl TableState {
pub fn new(row_count: usize) -> Self {
Self {
selected: 0,
row_count,
}
}
pub fn up(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
pub fn down(&mut self) {
if self.selected < self.row_count.saturating_sub(1) {
self.selected += 1;
}
}
pub fn first(&mut self) {
self.selected = 0;
}
pub fn last(&mut self) {
self.selected = self.row_count.saturating_sub(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_new() {
let cell = TableCell::new("Hello");
assert_eq!(cell.content, "Hello");
assert!(cell.color.is_none());
assert!(!cell.bold);
}
#[test]
fn test_cell_builder() {
let cell = TableCell::new("Test")
.color(Color::Red)
.bold()
.italic()
.align(CellAlign::Right);
assert_eq!(cell.content, "Test");
assert_eq!(cell.color, Some(Color::Red));
assert!(cell.bold);
assert!(cell.italic);
assert_eq!(cell.align, Some(CellAlign::Right));
}
#[test]
fn test_cell_from_string() {
let cell: TableCell = "Hello".into();
assert_eq!(cell.content, "Hello");
}
#[test]
fn test_row_new() {
let row = Row::new(vec!["A", "B", "C"]);
assert_eq!(row.len(), 3);
assert_eq!(row.cells[0].content, "A");
}
#[test]
fn test_row_from_iter() {
let row: Row = vec!["X", "Y"].into();
assert_eq!(row.len(), 2);
}
#[test]
fn test_row_bg_color() {
let row = Row::new(vec!["A"]).bg_color(Color::Blue);
assert_eq!(row.bg_color, Some(Color::Blue));
}
#[test]
fn test_table_props_new() {
let props = TableProps::new(vec![vec!["A", "B"], vec!["C", "D"]]);
assert_eq!(props.rows.len(), 2);
assert!(props.header.is_none());
}
#[test]
fn test_table_props_header() {
let props = TableProps::new(vec![vec!["A"]]).header(vec!["Header"]);
assert!(props.header.is_some());
assert_eq!(props.header.unwrap().cells[0].content, "Header");
}
#[test]
fn test_table_props_widths() {
let props = TableProps::new(vec![vec!["A", "B"]])
.widths([ColumnWidth::Fixed(10), ColumnWidth::Percent(0.5)]);
assert_eq!(props.widths.len(), 2);
assert_eq!(props.widths[0], ColumnWidth::Fixed(10));
}
#[test]
fn test_table_props_fixed_widths() {
let props = TableProps::new(vec![vec!["A", "B"]]).fixed_widths([10, 20]);
assert_eq!(props.widths[0], ColumnWidth::Fixed(10));
assert_eq!(props.widths[1], ColumnWidth::Fixed(20));
}
#[test]
fn test_table_props_striped() {
let props = TableProps::new(vec![vec!["A"]]).striped();
assert_eq!(props.row_style, RowStyle::Striped);
}
#[test]
fn test_table_props_border() {
let props = TableProps::new(vec![vec!["A"]])
.border(BorderStyle::Round)
.border_color(Color::Cyan);
assert_eq!(props.border_style, BorderStyle::Round);
assert_eq!(props.border_color, Some(Color::Cyan));
}
#[test]
fn test_table_props_selected() {
let props = TableProps::new(vec![vec!["A"], vec!["B"]])
.selected(1)
.selected_style(Some(Color::Yellow), Some(Color::Blue));
assert_eq!(props.selected, Some(1));
assert_eq!(props.selected_color, Some(Color::Yellow));
assert_eq!(props.selected_bg_color, Some(Color::Blue));
}
#[test]
fn test_table_state_navigation() {
let mut state = TableState::new(5);
assert_eq!(state.selected, 0);
state.down();
assert_eq!(state.selected, 1);
state.down();
state.down();
assert_eq!(state.selected, 3);
state.up();
assert_eq!(state.selected, 2);
state.last();
assert_eq!(state.selected, 4);
state.first();
assert_eq!(state.selected, 0);
state.up();
assert_eq!(state.selected, 0); }
#[test]
fn test_table_num_columns() {
let props = TableProps::new(vec![vec!["A", "B"]]).widths([
ColumnWidth::Fixed(10),
ColumnWidth::Fixed(10),
ColumnWidth::Fixed(10),
]);
assert_eq!(props.num_columns(), 3);
let props = TableProps::new(Vec::<Vec<&str>>::new()).header(vec!["A", "B"]);
assert_eq!(props.num_columns(), 2);
let props = TableProps::new(vec![vec!["A", "B", "C", "D"]]);
assert_eq!(props.num_columns(), 4);
}
#[test]
fn test_table_render_empty() {
let props = TableProps::new(Vec::<Vec<&str>>::new());
let elem = Table::render(&props);
assert!(elem.is_text());
}
#[test]
fn test_table_render_basic() {
let props = TableProps::new(vec![vec!["A", "B"]]).header(vec!["Col1", "Col2"]);
let elem = Table::render(&props);
assert!(elem.is_text());
}
#[test]
fn test_column_width_default() {
let width = ColumnWidth::default();
assert_eq!(width, ColumnWidth::Auto);
}
#[test]
fn test_cell_align_default() {
let align = CellAlign::default();
assert_eq!(align, CellAlign::Left);
}
}