Skip to main content

oxidize_pdf/text/
table.rs

1//! Simple table rendering for PDF documents
2//!
3//! This module provides basic table functionality without CSS styling,
4//! suitable for structured data presentation in PDF documents.
5
6use crate::error::PdfError;
7use crate::graphics::{Color, GraphicsContext, LineDashPattern};
8use crate::text::{measure_text, Font, TextAlign};
9
10/// Represents a simple table in a PDF document
11#[derive(Debug, Clone)]
12pub struct Table {
13    /// Table rows
14    rows: Vec<TableRow>,
15    /// Column widths (in points)
16    column_widths: Vec<f64>,
17    /// Table position (x, y)
18    position: (f64, f64),
19    /// Table options
20    options: TableOptions,
21}
22
23/// Options for table rendering
24#[derive(Debug, Clone)]
25pub struct TableOptions {
26    /// Border width in points
27    pub border_width: f64,
28    /// Border color
29    pub border_color: Color,
30    /// Cell padding in points
31    pub cell_padding: f64,
32    /// Row height in points (0 for auto)
33    pub row_height: f64,
34    /// Font for table text
35    pub font: Font,
36    /// Font size in points
37    pub font_size: f64,
38    /// Text color
39    pub text_color: Color,
40    /// Header row styling
41    pub header_style: Option<HeaderStyle>,
42    /// Grid layout options
43    pub grid_style: GridStyle,
44    /// Cell border style
45    pub cell_border_style: CellBorderStyle,
46    /// Alternating row colors
47    pub alternating_row_colors: Option<(Color, Color)>,
48    /// Table background color
49    pub background_color: Option<Color>,
50}
51
52/// Header row styling options
53#[derive(Debug, Clone)]
54pub struct HeaderStyle {
55    /// Background color for header cells
56    pub background_color: Color,
57    /// Text color for header cells
58    pub text_color: Color,
59    /// Font for header text
60    pub font: Font,
61    /// Make header text bold
62    pub bold: bool,
63}
64
65/// Represents a row in the table
66#[derive(Debug, Clone)]
67pub struct TableRow {
68    /// Cells in this row
69    cells: Vec<TableCell>,
70    /// Whether this is a header row
71    is_header: bool,
72    /// Optional per-row height (overrides global row_height)
73    row_height: Option<f64>,
74}
75
76/// Represents a cell in the table
77#[derive(Debug, Clone)]
78pub struct TableCell {
79    /// Cell content
80    content: String,
81    /// Text alignment
82    align: TextAlign,
83    /// Column span (default 1)
84    colspan: usize,
85    /// Row span (default 1)
86    rowspan: usize,
87    /// Cell background color (overrides row color)
88    background_color: Option<Color>,
89    /// Cell border style (overrides table default)
90    border_style: Option<CellBorderStyle>,
91}
92
93/// Grid layout style for tables
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum GridStyle {
96    /// No grid lines
97    None,
98    /// Only horizontal lines
99    Horizontal,
100    /// Only vertical lines
101    Vertical,
102    /// Full grid with all lines
103    Full,
104    /// Only outer borders
105    Outline,
106}
107
108/// Cell border style options
109#[derive(Debug, Clone)]
110pub struct CellBorderStyle {
111    /// Border width
112    pub width: f64,
113    /// Border color
114    pub color: Color,
115    /// Dash pattern (None for solid)
116    pub dash_pattern: Option<LineDashPattern>,
117}
118
119impl Default for CellBorderStyle {
120    fn default() -> Self {
121        Self {
122            width: 1.0,
123            color: Color::black(),
124            dash_pattern: None,
125        }
126    }
127}
128
129impl Default for TableOptions {
130    fn default() -> Self {
131        Self {
132            border_width: 1.0,
133            border_color: Color::black(),
134            cell_padding: 5.0,
135            row_height: 0.0, // Auto
136            font: Font::Helvetica,
137            font_size: 10.0,
138            text_color: Color::black(),
139            header_style: None,
140            grid_style: GridStyle::Full,
141            cell_border_style: CellBorderStyle::default(),
142            alternating_row_colors: None,
143            background_color: None,
144        }
145    }
146}
147
148impl Table {
149    /// Create a new table with specified column widths
150    pub fn new(column_widths: Vec<f64>) -> Self {
151        Self {
152            rows: Vec::new(),
153            column_widths,
154            position: (0.0, 0.0),
155            options: TableOptions::default(),
156        }
157    }
158
159    /// Create a table with equal column widths
160    pub fn with_equal_columns(num_columns: usize, total_width: f64) -> Self {
161        let column_width = total_width / num_columns as f64;
162        let column_widths = vec![column_width; num_columns];
163        Self::new(column_widths)
164    }
165
166    /// Set table position
167    pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
168        self.position = (x, y);
169        self
170    }
171
172    /// Set table options
173    pub fn set_options(&mut self, options: TableOptions) -> &mut Self {
174        self.options = options;
175        self
176    }
177
178    /// Add a header row
179    pub fn add_header_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
180        if cells.len() != self.column_widths.len() {
181            return Err(PdfError::InvalidStructure(
182                "Header cells count doesn't match column count".to_string(),
183            ));
184        }
185
186        let row_cells: Vec<TableCell> = cells
187            .into_iter()
188            .map(|content| TableCell {
189                content,
190                align: TextAlign::Center,
191                colspan: 1,
192                rowspan: 1,
193                background_color: None,
194                border_style: None,
195            })
196            .collect();
197
198        self.rows.push(TableRow {
199            cells: row_cells,
200            is_header: true,
201            row_height: None,
202        });
203
204        Ok(self)
205    }
206
207    /// Set the height of the last added row
208    pub fn set_last_row_height(&mut self, height: f64) -> &mut Self {
209        if let Some(row) = self.rows.last_mut() {
210            row.row_height = Some(height);
211        }
212        self
213    }
214
215    /// Add a data row
216    pub fn add_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
217        self.add_row_with_alignment(cells, TextAlign::Left)
218    }
219
220    /// Add a data row with specific alignment
221    pub fn add_row_with_alignment(
222        &mut self,
223        cells: Vec<String>,
224        align: TextAlign,
225    ) -> Result<&mut Self, PdfError> {
226        if cells.len() != self.column_widths.len() {
227            return Err(PdfError::InvalidStructure(
228                "Row cells count doesn't match column count".to_string(),
229            ));
230        }
231
232        let row_cells: Vec<TableCell> = cells
233            .into_iter()
234            .map(|content| TableCell {
235                content,
236                align,
237                colspan: 1,
238                rowspan: 1,
239                background_color: None,
240                border_style: None,
241            })
242            .collect();
243
244        self.rows.push(TableRow {
245            cells: row_cells,
246            is_header: false,
247            row_height: None,
248        });
249
250        Ok(self)
251    }
252
253    /// Add a row with custom cells (allows colspan)
254    pub fn add_custom_row(&mut self, cells: Vec<TableCell>) -> Result<&mut Self, PdfError> {
255        // Validate total colspan matches column count
256        let total_colspan: usize = cells.iter().map(|c| c.colspan).sum();
257        if total_colspan != self.column_widths.len() {
258            return Err(PdfError::InvalidStructure(
259                "Total colspan doesn't match column count".to_string(),
260            ));
261        }
262
263        self.rows.push(TableRow {
264            cells,
265            is_header: false,
266            row_height: None,
267        });
268
269        Ok(self)
270    }
271
272    /// Calculate the height of a row
273    fn calculate_row_height(&self, row: &TableRow) -> f64 {
274        // Priority: per-row height > global options height > auto
275        if let Some(h) = row.row_height {
276            return h;
277        }
278        if self.options.row_height > 0.0 {
279            return self.options.row_height;
280        }
281
282        // Auto height: consider multi-line content
283        let line_height = self.options.font_size * 1.2;
284        let max_lines = row
285            .cells
286            .iter()
287            .map(|cell| cell.content.split('\n').count())
288            .max()
289            .unwrap_or(1);
290
291        if max_lines <= 1 {
292            // Single line: font size + padding
293            self.options.font_size + (self.options.cell_padding * 2.0)
294        } else {
295            // Multi-line: first line height + additional lines + padding
296            self.options.font_size
297                + ((max_lines - 1) as f64 * line_height)
298                + (self.options.cell_padding * 2.0)
299        }
300    }
301
302    /// Get total table height
303    pub fn get_height(&self) -> f64 {
304        self.rows
305            .iter()
306            .map(|row| self.calculate_row_height(row))
307            .sum()
308    }
309
310    /// Get total table width
311    pub fn get_width(&self) -> f64 {
312        self.column_widths.iter().sum()
313    }
314
315    /// Render the table to a graphics context
316    pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
317        let (start_x, start_y) = self.position;
318        let mut current_y = start_y;
319
320        // Draw table background if specified
321        // Table grows downward from start_y, so bottom-left is at start_y - height
322        if let Some(bg_color) = self.options.background_color {
323            graphics.save_state();
324            graphics.set_fill_color(bg_color);
325            graphics.rectangle(
326                start_x,
327                start_y - self.get_height(),
328                self.get_width(),
329                self.get_height(),
330            );
331            graphics.fill();
332            graphics.restore_state();
333        }
334
335        // Draw each row
336        let mut data_row_index: usize = 0; // Counts only non-header rows (for zebra stripes)
337        for (row_index, row) in self.rows.iter().enumerate() {
338            let row_height = self.calculate_row_height(row);
339            let mut current_x = start_x;
340
341            // Determine if we should use header styling
342            let use_header_style = row.is_header && self.options.header_style.is_some();
343            let header_style = self.options.header_style.as_ref();
344
345            // Draw cells
346            let mut col_index = 0;
347            for cell in &row.cells {
348                // Calculate cell width (considering colspan)
349                let mut cell_width = 0.0;
350                for i in 0..cell.colspan {
351                    if col_index + i < self.column_widths.len() {
352                        cell_width += self.column_widths[col_index + i];
353                    }
354                }
355
356                // Cell rectangle bottom-left Y (table grows downward)
357                let cell_rect_y = current_y - row_height;
358
359                // Draw cell background
360                // First priority: cell-specific background
361                if let Some(cell_bg) = cell.background_color {
362                    graphics.save_state();
363                    graphics.set_fill_color(cell_bg);
364                    graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
365                    graphics.fill();
366                    graphics.restore_state();
367                }
368                // Second priority: header style background
369                else if use_header_style {
370                    if let Some(style) = header_style {
371                        graphics.save_state();
372                        graphics.set_fill_color(style.background_color);
373                        graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
374                        graphics.fill();
375                        graphics.restore_state();
376                    }
377                }
378                // Third priority: alternating row colors
379                else if let Some((even_color, odd_color)) = self.options.alternating_row_colors {
380                    if !row.is_header {
381                        let color = if data_row_index % 2 == 0 {
382                            even_color
383                        } else {
384                            odd_color
385                        };
386                        graphics.save_state();
387                        graphics.set_fill_color(color);
388                        graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
389                        graphics.fill();
390                        graphics.restore_state();
391                    }
392                }
393
394                // Draw cell border based on grid style
395                let should_draw_border = match self.options.grid_style {
396                    GridStyle::None => false,
397                    GridStyle::Full => true,
398                    GridStyle::Horizontal => {
399                        // Draw top and bottom borders only
400                        true
401                    }
402                    GridStyle::Vertical => {
403                        // Draw left and right borders only
404                        true
405                    }
406                    GridStyle::Outline => {
407                        // Only draw if it's an edge cell
408                        col_index == 0
409                            || col_index + cell.colspan >= self.column_widths.len()
410                            || row_index == 0
411                            || row_index == self.rows.len() - 1
412                    }
413                };
414
415                if should_draw_border {
416                    graphics.save_state();
417
418                    // Use cell-specific border style if available
419                    let border_style = cell
420                        .border_style
421                        .as_ref()
422                        .unwrap_or(&self.options.cell_border_style);
423
424                    graphics.set_stroke_color(border_style.color);
425                    graphics.set_line_width(border_style.width);
426
427                    // Apply dash pattern if specified
428                    if let Some(dash_pattern) = &border_style.dash_pattern {
429                        graphics.set_line_dash_pattern(dash_pattern.clone());
430                    }
431
432                    // Draw borders based on grid style
433                    match self.options.grid_style {
434                        GridStyle::Full | GridStyle::Outline => {
435                            graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
436                            graphics.stroke();
437                        }
438                        GridStyle::Horizontal => {
439                            // Top border
440                            graphics.move_to(current_x, current_y);
441                            graphics.line_to(current_x + cell_width, current_y);
442                            // Bottom border
443                            graphics.move_to(current_x, cell_rect_y);
444                            graphics.line_to(current_x + cell_width, cell_rect_y);
445                            graphics.stroke();
446                        }
447                        GridStyle::Vertical => {
448                            // Left border
449                            graphics.move_to(current_x, current_y);
450                            graphics.line_to(current_x, cell_rect_y);
451                            // Right border
452                            graphics.move_to(current_x + cell_width, current_y);
453                            graphics.line_to(current_x + cell_width, cell_rect_y);
454                            graphics.stroke();
455                        }
456                        GridStyle::None => {}
457                    }
458
459                    graphics.restore_state();
460                }
461
462                // Draw cell text
463                // Text baseline: near top of cell, offset by padding and font size
464                let text_x = current_x + self.options.cell_padding;
465                let text_y = current_y - self.options.cell_padding - self.options.font_size;
466                let text_width = cell_width - (2.0 * self.options.cell_padding);
467
468                graphics.save_state();
469
470                // Set font and color
471                if use_header_style {
472                    if let Some(style) = header_style {
473                        let font = if style.bold {
474                            match style.font {
475                                Font::Helvetica => Font::HelveticaBold,
476                                Font::TimesRoman => Font::TimesBold,
477                                Font::Courier => Font::CourierBold,
478                                _ => style.font.clone(),
479                            }
480                        } else {
481                            style.font.clone()
482                        };
483                        graphics.set_font(font, self.options.font_size);
484                        graphics.set_fill_color(style.text_color);
485                    }
486                } else {
487                    graphics.set_font(self.options.font.clone(), self.options.font_size);
488                    graphics.set_fill_color(self.options.text_color);
489                }
490
491                // Split content by newlines for multi-line support
492                let lines: Vec<&str> = cell.content.split('\n').collect();
493                let line_height = self.options.font_size * 1.2;
494
495                // Determine font for measurement (needed for Center/Right alignment)
496                let font_to_measure = if use_header_style {
497                    if let Some(style) = header_style {
498                        if style.bold {
499                            match style.font {
500                                Font::Helvetica => Font::HelveticaBold,
501                                Font::TimesRoman => Font::TimesBold,
502                                Font::Courier => Font::CourierBold,
503                                _ => style.font.clone(),
504                            }
505                        } else {
506                            style.font.clone()
507                        }
508                    } else {
509                        self.options.font.clone()
510                    }
511                } else {
512                    self.options.font.clone()
513                };
514
515                // Draw each line with alignment
516                for (line_idx, line) in lines.iter().enumerate() {
517                    let line_y = text_y - (line_idx as f64 * line_height);
518
519                    let line_x = match cell.align {
520                        TextAlign::Center => {
521                            let measured =
522                                measure_text(line, &font_to_measure, self.options.font_size);
523                            text_x + (text_width - measured) / 2.0
524                        }
525                        TextAlign::Right => {
526                            let measured =
527                                measure_text(line, &font_to_measure, self.options.font_size);
528                            text_x + text_width - measured
529                        }
530                        TextAlign::Left | TextAlign::Justified => text_x,
531                    };
532
533                    graphics.begin_text();
534                    graphics.set_text_position(line_x, line_y);
535                    graphics.show_text(line)?;
536                    graphics.end_text();
537                }
538
539                graphics.restore_state();
540
541                current_x += cell_width;
542                col_index += cell.colspan;
543            }
544
545            if !row.is_header {
546                data_row_index += 1;
547            }
548            current_y -= row_height;
549        }
550
551        Ok(())
552    }
553}
554
555impl TableRow {
556    /// Create a new row with cells
557    #[allow(dead_code)]
558    pub fn new(cells: Vec<TableCell>) -> Self {
559        Self {
560            cells,
561            is_header: false,
562            row_height: None,
563        }
564    }
565
566    /// Create a header row
567    #[allow(dead_code)]
568    pub fn header(cells: Vec<TableCell>) -> Self {
569        Self {
570            cells,
571            is_header: true,
572            row_height: None,
573        }
574    }
575
576    /// Set the height for this specific row (overrides global row_height)
577    pub fn set_row_height(&mut self, height: f64) -> &mut Self {
578        self.row_height = Some(height);
579        self
580    }
581}
582
583impl TableCell {
584    /// Create a new cell with content
585    pub fn new(content: String) -> Self {
586        Self {
587            content,
588            align: TextAlign::Left,
589            colspan: 1,
590            rowspan: 1,
591            background_color: None,
592            border_style: None,
593        }
594    }
595
596    /// Create a cell with specific alignment
597    pub fn with_align(content: String, align: TextAlign) -> Self {
598        Self {
599            content,
600            align,
601            colspan: 1,
602            rowspan: 1,
603            background_color: None,
604            border_style: None,
605        }
606    }
607
608    /// Create a cell with colspan
609    pub fn with_colspan(content: String, colspan: usize) -> Self {
610        Self {
611            content,
612            align: TextAlign::Left,
613            colspan,
614            rowspan: 1,
615            background_color: None,
616            border_style: None,
617        }
618    }
619
620    /// Set cell background color
621    pub fn set_background_color(&mut self, color: Color) -> &mut Self {
622        self.background_color = Some(color);
623        self
624    }
625
626    /// Set cell border style
627    pub fn set_border_style(&mut self, style: CellBorderStyle) -> &mut Self {
628        self.border_style = Some(style);
629        self
630    }
631
632    /// Set rowspan
633    pub fn set_rowspan(&mut self, rowspan: usize) -> &mut Self {
634        self.rowspan = rowspan;
635        self
636    }
637
638    /// Set cell alignment
639    pub fn set_align(&mut self, align: TextAlign) -> &mut Self {
640        self.align = align;
641        self
642    }
643
644    /// Set cell colspan
645    pub fn set_colspan(&mut self, colspan: usize) -> &mut Self {
646        self.colspan = colspan;
647        self
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn test_table_creation() {
657        let table = Table::new(vec![100.0, 150.0, 200.0]);
658        assert_eq!(table.column_widths.len(), 3);
659        assert_eq!(table.rows.len(), 0);
660    }
661
662    #[test]
663    fn test_table_equal_columns() {
664        let table = Table::with_equal_columns(4, 400.0);
665        assert_eq!(table.column_widths.len(), 4);
666        assert_eq!(table.column_widths[0], 100.0);
667        assert_eq!(table.get_width(), 400.0);
668    }
669
670    #[test]
671    fn test_add_header_row() {
672        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
673        let result = table.add_header_row(vec![
674            "Name".to_string(),
675            "Age".to_string(),
676            "City".to_string(),
677        ]);
678        assert!(result.is_ok());
679        assert_eq!(table.rows.len(), 1);
680        assert!(table.rows[0].is_header);
681    }
682
683    #[test]
684    fn test_add_row_mismatch() {
685        let mut table = Table::new(vec![100.0, 100.0]);
686        let result = table.add_row(vec![
687            "John".to_string(),
688            "25".to_string(),
689            "NYC".to_string(),
690        ]);
691        assert!(result.is_err());
692    }
693
694    #[test]
695    fn test_table_cell_creation() {
696        let cell = TableCell::new("Test".to_string());
697        assert_eq!(cell.content, "Test");
698        assert_eq!(cell.align, TextAlign::Left);
699        assert_eq!(cell.colspan, 1);
700    }
701
702    #[test]
703    fn test_table_cell_with_colspan() {
704        let cell = TableCell::with_colspan("Merged".to_string(), 3);
705        assert_eq!(cell.content, "Merged");
706        assert_eq!(cell.colspan, 3);
707    }
708
709    #[test]
710    fn test_custom_row_colspan_validation() {
711        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
712        let cells = vec![
713            TableCell::new("Normal".to_string()),
714            TableCell::with_colspan("Merged".to_string(), 2),
715        ];
716        let result = table.add_custom_row(cells);
717        assert!(result.is_ok());
718        assert_eq!(table.rows.len(), 1);
719    }
720
721    #[test]
722    fn test_custom_row_invalid_colspan() {
723        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
724        let cells = vec![
725            TableCell::new("Normal".to_string()),
726            TableCell::with_colspan("Merged".to_string(), 3), // Total would be 4
727        ];
728        let result = table.add_custom_row(cells);
729        assert!(result.is_err());
730    }
731
732    #[test]
733    fn test_table_options_default() {
734        let options = TableOptions::default();
735        assert_eq!(options.border_width, 1.0);
736        assert_eq!(options.border_color, Color::black());
737        assert_eq!(options.cell_padding, 5.0);
738        assert_eq!(options.font_size, 10.0);
739        assert_eq!(options.grid_style, GridStyle::Full);
740        assert!(options.alternating_row_colors.is_none());
741        assert!(options.background_color.is_none());
742    }
743
744    #[test]
745    fn test_header_style() {
746        let style = HeaderStyle {
747            background_color: Color::gray(0.9),
748            text_color: Color::black(),
749            font: Font::HelveticaBold,
750            bold: true,
751        };
752        assert_eq!(style.background_color, Color::gray(0.9));
753        assert!(style.bold);
754    }
755
756    #[test]
757    fn test_table_dimensions() {
758        let mut table = Table::new(vec![100.0, 150.0, 200.0]);
759        table.options.row_height = 20.0;
760
761        table
762            .add_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])
763            .unwrap();
764        table
765            .add_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])
766            .unwrap();
767
768        assert_eq!(table.get_width(), 450.0);
769        assert_eq!(table.get_height(), 40.0);
770    }
771
772    #[test]
773    fn test_table_position() {
774        let mut table = Table::new(vec![100.0]);
775        table.set_position(50.0, 100.0);
776        assert_eq!(table.position, (50.0, 100.0));
777    }
778
779    #[test]
780    fn test_row_with_alignment() {
781        let mut table = Table::new(vec![100.0, 100.0]);
782        let result = table.add_row_with_alignment(
783            vec!["Left".to_string(), "Right".to_string()],
784            TextAlign::Right,
785        );
786        assert!(result.is_ok());
787        assert_eq!(table.rows[0].cells[0].align, TextAlign::Right);
788    }
789
790    #[test]
791    fn test_table_cell_setters() {
792        let mut cell = TableCell::new("Test".to_string());
793        cell.set_align(TextAlign::Center).set_colspan(2);
794        assert_eq!(cell.align, TextAlign::Center);
795        assert_eq!(cell.colspan, 2);
796    }
797
798    #[test]
799    fn test_auto_row_height() {
800        let table = Table::new(vec![100.0]);
801        let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
802        let height = table.calculate_row_height(&row);
803        assert_eq!(height, 20.0); // font_size (10) + padding*2 (5*2)
804    }
805
806    #[test]
807    fn test_fixed_row_height() {
808        let mut table = Table::new(vec![100.0]);
809        table.options.row_height = 30.0;
810        let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
811        let height = table.calculate_row_height(&row);
812        assert_eq!(height, 30.0);
813    }
814
815    #[test]
816    fn test_grid_styles() {
817        let mut options = TableOptions::default();
818
819        options.grid_style = GridStyle::None;
820        assert_eq!(options.grid_style, GridStyle::None);
821
822        options.grid_style = GridStyle::Horizontal;
823        assert_eq!(options.grid_style, GridStyle::Horizontal);
824
825        options.grid_style = GridStyle::Vertical;
826        assert_eq!(options.grid_style, GridStyle::Vertical);
827
828        options.grid_style = GridStyle::Outline;
829        assert_eq!(options.grid_style, GridStyle::Outline);
830    }
831
832    #[test]
833    fn test_cell_border_style() {
834        let style = CellBorderStyle::default();
835        assert_eq!(style.width, 1.0);
836        assert_eq!(style.color, Color::black());
837        assert!(style.dash_pattern.is_none());
838
839        let custom_style = CellBorderStyle {
840            width: 2.0,
841            color: Color::rgb(1.0, 0.0, 0.0),
842            dash_pattern: Some(LineDashPattern::new(vec![5.0, 3.0], 0.0)),
843        };
844        assert_eq!(custom_style.width, 2.0);
845        assert!(custom_style.dash_pattern.is_some());
846    }
847
848    #[test]
849    fn test_table_with_alternating_colors() {
850        let mut table = Table::new(vec![100.0, 100.0]);
851        table.options.alternating_row_colors = Some((Color::gray(0.95), Color::gray(0.9)));
852
853        table
854            .add_row(vec!["Row 1".to_string(), "Data 1".to_string()])
855            .unwrap();
856        table
857            .add_row(vec!["Row 2".to_string(), "Data 2".to_string()])
858            .unwrap();
859
860        assert_eq!(table.rows.len(), 2);
861        assert!(table.options.alternating_row_colors.is_some());
862    }
863
864    #[test]
865    fn test_cell_with_background() {
866        let mut cell = TableCell::new("Test".to_string());
867        cell.set_background_color(Color::rgb(0.0, 1.0, 0.0));
868
869        assert!(cell.background_color.is_some());
870        assert_eq!(cell.background_color.unwrap(), Color::rgb(0.0, 1.0, 0.0));
871    }
872
873    #[test]
874    fn test_cell_with_custom_border() {
875        let mut cell = TableCell::new("Test".to_string());
876        let border_style = CellBorderStyle {
877            width: 2.0,
878            color: Color::rgb(0.0, 0.0, 1.0),
879            dash_pattern: None,
880        };
881        cell.set_border_style(border_style);
882
883        assert!(cell.border_style.is_some());
884        let style = cell.border_style.as_ref().unwrap();
885        assert_eq!(style.width, 2.0);
886        assert_eq!(style.color, Color::rgb(0.0, 0.0, 1.0));
887    }
888}