use super::content_stream::ContentStreamBuilder;
use crate::error::Result;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CellAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CellVAlign {
#[default]
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ColumnWidth {
#[default]
Auto,
Fixed(f32),
Percent(f32),
Weight(f32),
}
#[derive(Debug, Clone, Copy)]
pub struct TableBorderStyle {
pub width: f32,
pub color: (f32, f32, f32),
}
impl Default for TableBorderStyle {
fn default() -> Self {
Self {
width: 0.5,
color: (0.0, 0.0, 0.0), }
}
}
impl TableBorderStyle {
pub fn new(width: f32) -> Self {
Self {
width,
..Default::default()
}
}
pub fn with_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.color = (r, g, b);
self
}
pub fn thin() -> Self {
Self::new(0.25)
}
pub fn medium() -> Self {
Self::new(0.5)
}
pub fn thick() -> Self {
Self::new(1.0)
}
pub fn none() -> Self {
Self::new(0.0)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Borders {
pub top: Option<TableBorderStyle>,
pub right: Option<TableBorderStyle>,
pub bottom: Option<TableBorderStyle>,
pub left: Option<TableBorderStyle>,
}
impl Borders {
pub fn none() -> Self {
Self::default()
}
pub fn all(style: TableBorderStyle) -> Self {
Self {
top: Some(style),
right: Some(style),
bottom: Some(style),
left: Some(style),
}
}
pub fn horizontal(style: TableBorderStyle) -> Self {
Self {
top: Some(style),
bottom: Some(style),
..Default::default()
}
}
pub fn vertical(style: TableBorderStyle) -> Self {
Self {
left: Some(style),
right: Some(style),
..Default::default()
}
}
pub fn with_top(mut self, style: TableBorderStyle) -> Self {
self.top = Some(style);
self
}
pub fn with_bottom(mut self, style: TableBorderStyle) -> Self {
self.bottom = Some(style);
self
}
pub fn with_left(mut self, style: TableBorderStyle) -> Self {
self.left = Some(style);
self
}
pub fn with_right(mut self, style: TableBorderStyle) -> Self {
self.right = Some(style);
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct CellPadding {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Default for CellPadding {
fn default() -> Self {
Self {
top: 4.0,
right: 4.0,
bottom: 4.0,
left: 4.0,
}
}
}
impl CellPadding {
pub fn uniform(padding: f32) -> Self {
Self {
top: padding,
right: padding,
bottom: padding,
left: padding,
}
}
pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
pub fn none() -> Self {
Self::uniform(0.0)
}
pub fn horizontal(&self) -> f32 {
self.left + self.right
}
pub fn vertical(&self) -> f32 {
self.top + self.bottom
}
}
#[derive(Debug, Clone)]
pub struct TableCell {
pub content: String,
pub colspan: usize,
pub rowspan: usize,
pub align: CellAlign,
pub valign: CellVAlign,
pub padding: Option<CellPadding>,
pub borders: Option<Borders>,
pub background: Option<(f32, f32, f32)>,
pub font_name: Option<String>,
pub font_size: Option<f32>,
pub bold: bool,
pub italic: bool,
}
impl TableCell {
pub fn text(content: impl Into<String>) -> Self {
Self {
content: content.into(),
colspan: 1,
rowspan: 1,
align: CellAlign::default(),
valign: CellVAlign::default(),
padding: None,
borders: None,
background: None,
font_name: None,
font_size: None,
bold: false,
italic: false,
}
}
pub fn empty() -> Self {
Self::text("")
}
pub fn colspan(mut self, span: usize) -> Self {
self.colspan = span.max(1);
self
}
pub fn rowspan(mut self, span: usize) -> Self {
self.rowspan = span.max(1);
self
}
pub fn align(mut self, align: CellAlign) -> Self {
self.align = align;
self
}
pub fn valign(mut self, valign: CellVAlign) -> Self {
self.valign = valign;
self
}
pub fn padding(mut self, padding: CellPadding) -> Self {
self.padding = Some(padding);
self
}
pub fn borders(mut self, borders: Borders) -> Self {
self.borders = Some(borders);
self
}
pub fn background(mut self, r: f32, g: f32, b: f32) -> Self {
self.background = Some((r, g, b));
self
}
pub fn font(mut self, name: impl Into<String>, size: f32) -> Self {
self.font_name = Some(name.into());
self.font_size = Some(size);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn header(content: impl Into<String>) -> Self {
Self::text(content).align(CellAlign::Center).bold()
}
pub fn number(content: impl Into<String>) -> Self {
Self::text(content).align(CellAlign::Right)
}
}
#[derive(Debug, Clone)]
pub struct TableRow {
pub cells: Vec<TableCell>,
pub min_height: Option<f32>,
pub background: Option<(f32, f32, f32)>,
pub is_header: bool,
}
impl TableRow {
pub fn new(cells: Vec<TableCell>) -> Self {
Self {
cells,
min_height: None,
background: None,
is_header: false,
}
}
pub fn header(cells: Vec<TableCell>) -> Self {
Self {
cells,
min_height: None,
background: None,
is_header: true,
}
}
pub fn min_height(mut self, height: f32) -> Self {
self.min_height = Some(height);
self
}
pub fn background(mut self, r: f32, g: f32, b: f32) -> Self {
self.background = Some((r, g, b));
self
}
pub fn as_header(mut self) -> Self {
self.is_header = true;
self
}
}
#[derive(Debug, Clone)]
pub struct TableStyle {
pub cell_padding: CellPadding,
pub cell_borders: Borders,
pub outer_border: Option<TableBorderStyle>,
pub font_name: String,
pub font_size: f32,
pub header_background: Option<(f32, f32, f32)>,
pub stripe_color: Option<(f32, f32, f32)>,
pub row_spacing: f32,
}
impl Default for TableStyle {
fn default() -> Self {
Self {
cell_padding: CellPadding::default(),
cell_borders: Borders::all(TableBorderStyle::thin()),
outer_border: None,
font_name: "Helvetica".to_string(),
font_size: 10.0,
header_background: Some((0.9, 0.9, 0.9)), stripe_color: None,
row_spacing: 0.0,
}
}
}
impl TableStyle {
pub fn new() -> Self {
Self::default()
}
pub fn cell_padding(mut self, padding: CellPadding) -> Self {
self.cell_padding = padding;
self
}
pub fn cell_borders(mut self, borders: Borders) -> Self {
self.cell_borders = borders;
self
}
pub fn outer_border(mut self, border: TableBorderStyle) -> Self {
self.outer_border = Some(border);
self
}
pub fn font(mut self, name: impl Into<String>, size: f32) -> Self {
self.font_name = name.into();
self.font_size = size;
self
}
pub fn header_background(mut self, r: f32, g: f32, b: f32) -> Self {
self.header_background = Some((r, g, b));
self
}
pub fn striped(mut self, r: f32, g: f32, b: f32) -> Self {
self.stripe_color = Some((r, g, b));
self
}
pub fn minimal() -> Self {
Self {
cell_borders: Borders::none(),
outer_border: None,
header_background: None,
..Default::default()
}
}
pub fn bordered() -> Self {
Self {
cell_borders: Borders::all(TableBorderStyle::medium()),
outer_border: Some(TableBorderStyle::thick()),
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub struct Table {
pub rows: Vec<TableRow>,
pub column_widths: Vec<ColumnWidth>,
pub style: TableStyle,
pub width: Option<f32>,
pub column_aligns: Vec<CellAlign>,
}
impl Table {
pub fn new(rows: Vec<Vec<TableCell>>) -> Self {
let rows: Vec<TableRow> = rows.into_iter().map(TableRow::new).collect();
Self::from_rows(rows)
}
pub fn from_rows(rows: Vec<TableRow>) -> Self {
let num_cols = rows
.iter()
.map(|r| r.cells.iter().map(|c| c.colspan).sum::<usize>())
.max()
.unwrap_or(0);
Self {
rows,
column_widths: vec![ColumnWidth::Auto; num_cols],
style: TableStyle::default(),
width: None,
column_aligns: vec![CellAlign::Left; num_cols],
}
}
pub fn empty() -> Self {
Self {
rows: Vec::new(),
column_widths: Vec::new(),
style: TableStyle::default(),
width: None,
column_aligns: Vec::new(),
}
}
pub fn add_row(&mut self, row: TableRow) {
self.rows.push(row);
}
pub fn with_header_row(mut self) -> Self {
if let Some(row) = self.rows.first_mut() {
row.is_header = true;
}
self
}
pub fn with_style(mut self, style: TableStyle) -> Self {
self.style = style;
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
self.column_widths = widths;
self
}
pub fn with_column_aligns(mut self, aligns: Vec<CellAlign>) -> Self {
self.column_aligns = aligns;
self
}
pub fn num_columns(&self) -> usize {
self.rows
.iter()
.map(|r| r.cells.iter().map(|c| c.colspan).sum::<usize>())
.max()
.unwrap_or(0)
}
pub fn num_rows(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct TableLayout {
pub column_widths: Vec<f32>,
pub row_heights: Vec<f32>,
pub total_width: f32,
pub total_height: f32,
pub cell_positions: Vec<Vec<CellPosition>>,
}
#[derive(Debug, Clone, Copy)]
pub struct CellPosition {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Table {
pub fn calculate_layout(
&self,
available_width: f32,
font_metrics: &dyn FontMetrics,
) -> TableLayout {
let num_cols = self.num_columns();
if num_cols == 0 || self.rows.is_empty() {
return TableLayout {
column_widths: vec![],
row_heights: vec![],
total_width: 0.0,
total_height: 0.0,
cell_positions: vec![],
};
}
let table_width = self.width.unwrap_or(available_width);
let column_widths = self.calculate_column_widths(table_width, num_cols, font_metrics);
let row_heights = self.calculate_row_heights(&column_widths, font_metrics);
let cell_positions = self.calculate_cell_positions(&column_widths, &row_heights);
let total_width: f32 = column_widths.iter().sum();
let total_height: f32 = row_heights.iter().sum();
TableLayout {
column_widths,
row_heights,
total_width,
total_height,
cell_positions,
}
}
fn calculate_column_widths(
&self,
table_width: f32,
num_cols: usize,
font_metrics: &dyn FontMetrics,
) -> Vec<f32> {
let padding = &self.style.cell_padding;
let mut widths = vec![0.0f32; num_cols];
let mut _fixed_width = 0.0f32;
let mut weight_total = 0.0f32;
let mut _percent_total = 0.0f32;
for (col, spec) in self.column_widths.iter().take(num_cols).enumerate() {
match spec {
ColumnWidth::Fixed(w) => {
widths[col] = *w;
_fixed_width += *w;
},
ColumnWidth::Percent(p) => {
let w = table_width * (*p / 100.0);
widths[col] = w;
_percent_total += *p;
},
ColumnWidth::Weight(w) => {
weight_total += *w;
},
ColumnWidth::Auto => {
let mut max_width = 0.0f32;
for row in &self.rows {
let mut col_idx = 0;
for cell in &row.cells {
if col_idx == col && cell.colspan == 1 {
let font_size = cell.font_size.unwrap_or(self.style.font_size);
let text_width = font_metrics.text_width(&cell.content, font_size);
let cell_padding = cell.padding.as_ref().unwrap_or(padding);
max_width = max_width.max(text_width + cell_padding.horizontal());
}
col_idx += cell.colspan;
}
}
widths[col] = max_width.max(20.0); },
}
}
for col in self.column_widths.len()..num_cols {
let mut max_width = 0.0f32;
for row in &self.rows {
let mut col_idx = 0;
for cell in &row.cells {
if col_idx == col && cell.colspan == 1 {
let font_size = cell.font_size.unwrap_or(self.style.font_size);
let text_width = font_metrics.text_width(&cell.content, font_size);
let cell_padding = cell.padding.as_ref().unwrap_or(padding);
max_width = max_width.max(text_width + cell_padding.horizontal());
}
col_idx += cell.colspan;
}
}
widths[col] = max_width.max(20.0);
}
let used_width: f32 = widths.iter().sum();
let remaining = (table_width - used_width).max(0.0);
if weight_total > 0.0 && remaining > 0.0 {
for (col, spec) in self.column_widths.iter().take(num_cols).enumerate() {
if let ColumnWidth::Weight(w) = spec {
widths[col] = remaining * (*w / weight_total);
}
}
}
let total: f32 = widths.iter().sum();
if total > table_width && total > 0.0 {
let scale = table_width / total;
for w in &mut widths {
*w *= scale;
}
}
widths
}
fn calculate_row_heights(
&self,
column_widths: &[f32],
font_metrics: &dyn FontMetrics,
) -> Vec<f32> {
let padding = &self.style.cell_padding;
let mut heights = Vec::with_capacity(self.rows.len());
for row in &self.rows {
let mut max_height = 0.0f32;
let mut col_idx = 0;
for cell in &row.cells {
if cell.rowspan == 1 {
let cell_width = if cell.colspan == 1 {
column_widths.get(col_idx).copied().unwrap_or(100.0)
} else {
column_widths[col_idx..col_idx + cell.colspan].iter().sum()
};
let cell_padding = cell.padding.as_ref().unwrap_or(padding);
let content_width = cell_width - cell_padding.horizontal();
let font_size = cell.font_size.unwrap_or(self.style.font_size);
let line_height = font_size * 1.2;
let lines = wrap_text(&cell.content, content_width, font_size, font_metrics);
let text_height = lines.len() as f32 * line_height;
let cell_height = text_height + cell_padding.vertical();
max_height = max_height.max(cell_height);
}
col_idx += cell.colspan;
}
if let Some(min_h) = row.min_height {
max_height = max_height.max(min_h);
}
heights.push(max_height.max(self.style.font_size * 1.5));
}
heights
}
fn calculate_cell_positions(
&self,
column_widths: &[f32],
row_heights: &[f32],
) -> Vec<Vec<CellPosition>> {
let mut positions = Vec::with_capacity(self.rows.len());
let mut y = 0.0;
for (row_idx, row) in self.rows.iter().enumerate() {
let mut row_positions = Vec::with_capacity(row.cells.len());
let mut x = 0.0;
let mut col_idx = 0;
for cell in &row.cells {
let width: f32 = column_widths[col_idx..col_idx + cell.colspan].iter().sum();
let height: f32 = row_heights[row_idx..row_idx + cell.rowspan].iter().sum();
row_positions.push(CellPosition {
x,
y,
width,
height,
});
x += width;
col_idx += cell.colspan;
}
positions.push(row_positions);
y += row_heights[row_idx];
}
positions
}
pub fn render(
&self,
builder: &mut ContentStreamBuilder,
x: f32,
y: f32,
layout: &TableLayout,
) -> Result<()> {
let table_top = y;
for (row_idx, row) in self.rows.iter().enumerate() {
for (cell_idx, cell) in row.cells.iter().enumerate() {
let pos = &layout.cell_positions[row_idx][cell_idx];
let cell_x = x + pos.x;
let cell_y = table_top - pos.y - pos.height;
let bg = cell.background.or({
if row.is_header {
self.style.header_background
} else if let Some(stripe) = self.style.stripe_color {
if row_idx % 2 == 1 {
Some(stripe)
} else {
row.background
}
} else {
row.background
}
});
if let Some((r, g, b)) = bg {
builder.set_fill_color(r, g, b);
builder.rect(cell_x, cell_y, pos.width, pos.height);
builder.fill();
}
let borders = cell.borders.as_ref().unwrap_or(&self.style.cell_borders);
self.draw_cell_borders(builder, cell_x, cell_y, pos.width, pos.height, borders);
}
}
for (row_idx, row) in self.rows.iter().enumerate() {
for (cell_idx, cell) in row.cells.iter().enumerate() {
if cell.content.is_empty() {
continue;
}
let pos = &layout.cell_positions[row_idx][cell_idx];
let padding = cell.padding.as_ref().unwrap_or(&self.style.cell_padding);
let cell_x = x + pos.x + padding.left;
let cell_y = table_top - pos.y - padding.top;
let content_width = pos.width - padding.horizontal();
let align = if cell.align != CellAlign::Left {
cell.align
} else {
self.column_aligns
.get(cell_idx)
.copied()
.unwrap_or(CellAlign::Left)
};
let font_name = cell.font_name.as_deref().unwrap_or(&self.style.font_name);
let font_size = cell.font_size.unwrap_or(self.style.font_size);
let actual_font = if cell.bold && cell.italic {
format!("{}-BoldOblique", font_name)
} else if cell.bold || row.is_header {
format!("{}-Bold", font_name)
} else if cell.italic {
format!("{}-Oblique", font_name)
} else {
font_name.to_string()
};
builder.begin_text().set_font(&actual_font, font_size);
let text_x = match align {
CellAlign::Left => cell_x,
CellAlign::Center => cell_x + content_width / 2.0,
CellAlign::Right => cell_x + content_width,
};
let _line_height = font_size * 1.2;
let text_y = cell_y - font_size;
builder.text(&cell.content, text_x, text_y);
builder.end_text();
}
}
if let Some(outer) = &self.style.outer_border {
if outer.width > 0.0 {
builder.set_stroke_color(outer.color.0, outer.color.1, outer.color.2);
builder.set_line_width(outer.width);
builder.rect(
x,
table_top - layout.total_height,
layout.total_width,
layout.total_height,
);
builder.stroke();
}
}
Ok(())
}
fn draw_cell_borders(
&self,
builder: &mut ContentStreamBuilder,
x: f32,
y: f32,
width: f32,
height: f32,
borders: &Borders,
) {
if let Some(border) = &borders.top {
if border.width > 0.0 {
builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
builder.set_line_width(border.width);
builder.move_to(x, y + height);
builder.line_to(x + width, y + height);
builder.stroke();
}
}
if let Some(border) = &borders.bottom {
if border.width > 0.0 {
builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
builder.set_line_width(border.width);
builder.move_to(x, y);
builder.line_to(x + width, y);
builder.stroke();
}
}
if let Some(border) = &borders.left {
if border.width > 0.0 {
builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
builder.set_line_width(border.width);
builder.move_to(x, y);
builder.line_to(x, y + height);
builder.stroke();
}
}
if let Some(border) = &borders.right {
if border.width > 0.0 {
builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
builder.set_line_width(border.width);
builder.move_to(x + width, y);
builder.line_to(x + width, y + height);
builder.stroke();
}
}
}
}
pub trait FontMetrics {
fn text_width(&self, text: &str, font_size: f32) -> f32;
}
#[derive(Debug, Clone, Copy)]
pub struct SimpleFontMetrics {
pub char_width_ratio: f32,
}
impl Default for SimpleFontMetrics {
fn default() -> Self {
Self {
char_width_ratio: 0.5, }
}
}
impl SimpleFontMetrics {
pub fn monospace() -> Self {
Self {
char_width_ratio: 0.6,
}
}
}
impl FontMetrics for SimpleFontMetrics {
fn text_width(&self, text: &str, font_size: f32) -> f32 {
text.chars().count() as f32 * font_size * self.char_width_ratio
}
}
fn wrap_text(text: &str, max_width: f32, font_size: f32, metrics: &dyn FontMetrics) -> Vec<String> {
if text.is_empty() {
return vec![String::new()];
}
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
let test_line = if current_line.is_empty() {
word.to_string()
} else {
format!("{} {}", current_line, word)
};
let width = metrics.text_width(&test_line, font_size);
if width <= max_width || current_line.is_empty() {
current_line = test_line;
} else {
lines.push(current_line);
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_cell_creation() {
let cell = TableCell::text("Hello");
assert_eq!(cell.content, "Hello");
assert_eq!(cell.colspan, 1);
assert_eq!(cell.rowspan, 1);
}
#[test]
fn test_table_cell_header() {
let cell = TableCell::header("Title");
assert!(cell.bold);
assert_eq!(cell.align, CellAlign::Center);
}
#[test]
fn test_table_cell_spanning() {
let cell = TableCell::text("Wide").colspan(2).rowspan(3);
assert_eq!(cell.colspan, 2);
assert_eq!(cell.rowspan, 3);
}
#[test]
fn test_table_creation() {
let table = Table::new(vec![
vec![TableCell::text("A"), TableCell::text("B")],
vec![TableCell::text("C"), TableCell::text("D")],
]);
assert_eq!(table.num_columns(), 2);
assert_eq!(table.num_rows(), 2);
}
#[test]
fn test_table_with_header() {
let table = Table::new(vec![
vec![TableCell::text("Name"), TableCell::text("Age")],
vec![TableCell::text("Alice"), TableCell::text("30")],
])
.with_header_row();
assert!(table.rows[0].is_header);
}
#[test]
fn test_cell_padding() {
let padding = CellPadding::uniform(10.0);
assert_eq!(padding.horizontal(), 20.0);
assert_eq!(padding.vertical(), 20.0);
let asym = CellPadding::symmetric(5.0, 10.0);
assert_eq!(asym.horizontal(), 10.0);
assert_eq!(asym.vertical(), 20.0);
}
#[test]
fn test_borders() {
let borders = Borders::all(TableBorderStyle::medium());
assert!(borders.top.is_some());
assert!(borders.right.is_some());
assert!(borders.bottom.is_some());
assert!(borders.left.is_some());
let horiz = Borders::horizontal(TableBorderStyle::thin());
assert!(horiz.top.is_some());
assert!(horiz.bottom.is_some());
assert!(horiz.left.is_none());
assert!(horiz.right.is_none());
}
#[test]
fn test_column_width_types() {
let _auto = ColumnWidth::Auto;
let _fixed = ColumnWidth::Fixed(100.0);
let _percent = ColumnWidth::Percent(25.0);
let _weight = ColumnWidth::Weight(1.0);
}
#[test]
fn test_table_style_presets() {
let minimal = TableStyle::minimal();
assert!(minimal.cell_borders.top.is_none());
assert!(minimal.outer_border.is_none());
let bordered = TableStyle::bordered();
assert!(bordered.outer_border.is_some());
}
#[test]
fn test_table_layout_calculation() {
let table = Table::new(vec![
vec![TableCell::text("Name"), TableCell::text("Value")],
vec![TableCell::text("Test"), TableCell::text("123")],
]);
let metrics = SimpleFontMetrics::default();
let layout = table.calculate_layout(400.0, &metrics);
assert_eq!(layout.column_widths.len(), 2);
assert_eq!(layout.row_heights.len(), 2);
assert!(layout.total_width > 0.0);
assert!(layout.total_height > 0.0);
}
#[test]
fn test_text_wrapping() {
let metrics = SimpleFontMetrics::default();
let lines = wrap_text("Hello World", 100.0, 12.0, &metrics);
assert!(!lines.is_empty());
}
#[test]
fn test_empty_table() {
let table = Table::empty();
assert!(table.is_empty());
assert_eq!(table.num_columns(), 0);
assert_eq!(table.num_rows(), 0);
}
#[test]
fn test_cell_alignments() {
let left = TableCell::text("Left").align(CellAlign::Left);
let center = TableCell::text("Center").align(CellAlign::Center);
let right = TableCell::number("123");
assert_eq!(left.align, CellAlign::Left);
assert_eq!(center.align, CellAlign::Center);
assert_eq!(right.align, CellAlign::Right);
}
#[test]
fn test_row_creation() {
let row = TableRow::new(vec![TableCell::text("A"), TableCell::text("B")]);
assert_eq!(row.cells.len(), 2);
assert!(!row.is_header);
let header = TableRow::header(vec![TableCell::text("Name"), TableCell::text("Value")]);
assert!(header.is_header);
}
#[test]
fn test_striped_table() {
let style = TableStyle::new().striped(0.95, 0.95, 0.95);
assert!(style.stripe_color.is_some());
}
}