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::{ensure_finite, 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    /// When the table is split across pages by `Document::add_paginated_table`,
51    /// repeat header rows at the top of every continuation page. Defaults to `true`.
52    pub repeat_header_on_split: bool,
53}
54
55/// Header row styling options
56#[derive(Debug, Clone)]
57pub struct HeaderStyle {
58    /// Background color for header cells
59    pub background_color: Color,
60    /// Text color for header cells
61    pub text_color: Color,
62    /// Font for header text
63    pub font: Font,
64    /// Make header text bold
65    pub bold: bool,
66}
67
68/// Represents a row in the table
69#[derive(Debug, Clone)]
70pub struct TableRow {
71    /// Cells in this row
72    cells: Vec<TableCell>,
73    /// Whether this is a header row
74    is_header: bool,
75    /// Optional per-row height (overrides global row_height)
76    row_height: Option<f64>,
77}
78
79/// Represents a cell in the table
80#[derive(Debug, Clone)]
81pub struct TableCell {
82    /// Cell content
83    content: String,
84    /// Text alignment
85    align: TextAlign,
86    /// Column span (default 1)
87    colspan: usize,
88    /// Row span (default 1)
89    rowspan: usize,
90    /// Cell background color (overrides row color)
91    background_color: Option<Color>,
92    /// Cell border style (overrides table default)
93    border_style: Option<CellBorderStyle>,
94}
95
96/// Grid layout style for tables
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub enum GridStyle {
99    /// No grid lines
100    None,
101    /// Only horizontal lines
102    Horizontal,
103    /// Only vertical lines
104    Vertical,
105    /// Full grid with all lines
106    Full,
107    /// Only outer borders
108    Outline,
109}
110
111/// Cell border style options
112#[derive(Debug, Clone)]
113pub struct CellBorderStyle {
114    /// Border width
115    pub width: f64,
116    /// Border color
117    pub color: Color,
118    /// Dash pattern (None for solid)
119    pub dash_pattern: Option<LineDashPattern>,
120}
121
122impl Default for CellBorderStyle {
123    fn default() -> Self {
124        Self {
125            width: 1.0,
126            color: Color::black(),
127            dash_pattern: None,
128        }
129    }
130}
131
132impl Default for TableOptions {
133    fn default() -> Self {
134        Self {
135            border_width: 1.0,
136            border_color: Color::black(),
137            cell_padding: 5.0,
138            row_height: 0.0, // Auto
139            font: Font::Helvetica,
140            font_size: 10.0,
141            text_color: Color::black(),
142            header_style: None,
143            grid_style: GridStyle::Full,
144            cell_border_style: CellBorderStyle::default(),
145            alternating_row_colors: None,
146            background_color: None,
147            repeat_header_on_split: true,
148        }
149    }
150}
151
152impl Table {
153    /// Create a new table with specified column widths
154    pub fn new(column_widths: Vec<f64>) -> Self {
155        Self {
156            rows: Vec::new(),
157            column_widths,
158            position: (0.0, 0.0),
159            options: TableOptions::default(),
160        }
161    }
162
163    /// Create a table with equal column widths
164    pub fn with_equal_columns(num_columns: usize, total_width: f64) -> Self {
165        let column_width = total_width / num_columns as f64;
166        let column_widths = vec![column_width; num_columns];
167        Self::new(column_widths)
168    }
169
170    /// Set table position
171    pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
172        self.position = (x, y);
173        self
174    }
175
176    /// Set table options
177    pub fn set_options(&mut self, options: TableOptions) -> &mut Self {
178        self.options = options;
179        self
180    }
181
182    /// Get a reference to the table's current options.
183    pub fn options(&self) -> &TableOptions {
184        &self.options
185    }
186
187    /// Add a header row
188    pub fn add_header_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
189        if cells.len() != self.column_widths.len() {
190            return Err(PdfError::InvalidStructure(
191                "Header cells count doesn't match column count".to_string(),
192            ));
193        }
194
195        let row_cells: Vec<TableCell> = cells
196            .into_iter()
197            .map(|content| TableCell {
198                content,
199                align: TextAlign::Center,
200                colspan: 1,
201                rowspan: 1,
202                background_color: None,
203                border_style: None,
204            })
205            .collect();
206
207        self.rows.push(TableRow {
208            cells: row_cells,
209            is_header: true,
210            row_height: None,
211        });
212
213        Ok(self)
214    }
215
216    /// Set the height of the last added row
217    pub fn set_last_row_height(&mut self, height: f64) -> &mut Self {
218        if let Some(row) = self.rows.last_mut() {
219            row.row_height = Some(height);
220        }
221        self
222    }
223
224    /// Add a data row
225    pub fn add_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
226        self.add_row_with_alignment(cells, TextAlign::Left)
227    }
228
229    /// Add a data row with specific alignment
230    pub fn add_row_with_alignment(
231        &mut self,
232        cells: Vec<String>,
233        align: TextAlign,
234    ) -> Result<&mut Self, PdfError> {
235        if cells.len() != self.column_widths.len() {
236            return Err(PdfError::InvalidStructure(
237                "Row cells count doesn't match column count".to_string(),
238            ));
239        }
240
241        let row_cells: Vec<TableCell> = cells
242            .into_iter()
243            .map(|content| TableCell {
244                content,
245                align,
246                colspan: 1,
247                rowspan: 1,
248                background_color: None,
249                border_style: None,
250            })
251            .collect();
252
253        self.rows.push(TableRow {
254            cells: row_cells,
255            is_header: false,
256            row_height: None,
257        });
258
259        Ok(self)
260    }
261
262    /// Add a row with custom cells (allows colspan)
263    pub fn add_custom_row(&mut self, cells: Vec<TableCell>) -> Result<&mut Self, PdfError> {
264        // Validate total colspan matches column count
265        let total_colspan: usize = cells.iter().map(|c| c.colspan).sum();
266        if total_colspan != self.column_widths.len() {
267            return Err(PdfError::InvalidStructure(
268                "Total colspan doesn't match column count".to_string(),
269            ));
270        }
271
272        self.rows.push(TableRow {
273            cells,
274            is_header: false,
275            row_height: None,
276        });
277
278        Ok(self)
279    }
280
281    /// Calculate the height of a row
282    fn calculate_row_height(&self, row: &TableRow) -> f64 {
283        // Priority: per-row height > global options height > auto
284        if let Some(h) = row.row_height {
285            return h;
286        }
287        if self.options.row_height > 0.0 {
288            return self.options.row_height;
289        }
290
291        // Auto height: consider multi-line content
292        let line_height = self.options.font_size * 1.2;
293        let max_lines = row
294            .cells
295            .iter()
296            .map(|cell| cell.content.split('\n').count())
297            .max()
298            .unwrap_or(1);
299
300        if max_lines <= 1 {
301            // Single line: font size + padding
302            self.options.font_size + (self.options.cell_padding * 2.0)
303        } else {
304            // Multi-line: first line height + additional lines + padding
305            self.options.font_size
306                + ((max_lines - 1) as f64 * line_height)
307                + (self.options.cell_padding * 2.0)
308        }
309    }
310
311    /// Get total table height
312    pub fn get_height(&self) -> f64 {
313        self.rows
314            .iter()
315            .map(|row| self.calculate_row_height(row))
316            .sum()
317    }
318
319    /// Get total table width
320    pub fn get_width(&self) -> f64 {
321        self.column_widths.iter().sum()
322    }
323
324    /// Number of rows in this table (header + data).
325    pub fn row_count(&self) -> usize {
326        self.rows.len()
327    }
328
329    /// Number of leading header rows in this table.
330    pub fn header_count(&self) -> usize {
331        self.rows.iter().take_while(|r| r.is_header).count()
332    }
333
334    /// Current top-left position of the table, `(x, y)`.
335    pub fn position(&self) -> (f64, f64) {
336        self.position
337    }
338
339    /// Prepend a clone of the leading header rows from `source` to this table.
340    ///
341    /// Used by `Document::add_paginated_table` to repeat headers on continuation
342    /// pages when `TableOptions::repeat_header_on_split` is true. No-op when
343    /// `source` has zero header rows.
344    ///
345    /// Crate-private: callers outside this crate have no use case for this and
346    /// passing a `source` with mismatched `column_widths` would yield malformed
347    /// rendering.
348    pub(crate) fn prepend_headers_from(&mut self, source: &Table) {
349        let header_count = source.header_count();
350        if header_count == 0 {
351            return;
352        }
353        let mut new_rows: Vec<TableRow> = source.rows[..header_count].to_vec();
354        new_rows.extend(self.rows.drain(..));
355        self.rows = new_rows;
356    }
357
358    /// Render the table to a graphics context.
359    ///
360    /// Back-compat note: this method is **vertical-overflow-unaware** by design.
361    /// Rows past the page boundary are still drawn off-page (silent overflow).
362    /// Callers concerned with overflow should use [`Table::render_with_split`]
363    /// or [`Table::render_strict`], or [`crate::page_tables::DocumentTables::add_paginated_table`].
364    pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
365        // Direct call to the row-drawing helper with all rows — preserves the
366        // pre-#218 silent-overflow behaviour callers depend on, and skips the
367        // boundary `ensure_finite` check (which the safe APIs apply).
368        self.render_rows_slice(graphics, &self.rows, self.get_height())
369    }
370
371    /// Render as many leading rows as fully fit above `bottom_y`; return the
372    /// unrendered tail as a fresh [`Table`] (with the same `column_widths` and
373    /// `options`), or `None` when everything fit.
374    ///
375    /// **Tail position is a sentinel `(start_x, 0.0)`** — the caller MUST call
376    /// [`Table::set_position`] on the returned tail before using it for
377    /// rendering or fit checks. Calling `render_with_split` on a tail without
378    /// repositioning will report 0 rows fitting (since `start_y - row_height < bottom_y`
379    /// for any positive `bottom_y`) and yield a tail identical to the input.
380    /// For batteries-included pagination, see
381    /// [`crate::page_tables::DocumentTables::add_paginated_table`].
382    ///
383    /// # Errors
384    ///
385    /// Returns [`PdfError::InvalidStructure`] when `bottom_y` is not a finite
386    /// value (NaN or ±∞).
387    pub fn render_with_split(
388        &self,
389        graphics: &mut GraphicsContext,
390        bottom_y: f64,
391    ) -> Result<Option<Table>, PdfError> {
392        ensure_finite("bottom_y", bottom_y)?;
393        let (start_x, _start_y) = self.position;
394
395        // Pre-flight: how many leading rows fully fit above the floor?
396        let rendered_count = self.fit_count(bottom_y);
397        let rendered_height = self.rows[..rendered_count]
398            .iter()
399            .map(|r| self.calculate_row_height(r))
400            .sum::<f64>();
401
402        if rendered_count > 0 {
403            self.render_rows_slice(graphics, &self.rows[..rendered_count], rendered_height)?;
404        }
405
406        if rendered_count == self.rows.len() {
407            Ok(None)
408        } else {
409            let mut tail = self.clone();
410            tail.rows = self.rows[rendered_count..].to_vec();
411            tail.position = (start_x, 0.0);
412            Ok(Some(tail))
413        }
414    }
415
416    /// Strict variant: pre-flight check the table against `bottom_y`. If any
417    /// row would overflow, return [`PdfError::TableOverflow`] **without
418    /// drawing anything**; otherwise render normally.
419    ///
420    /// # Errors
421    ///
422    /// Returns [`PdfError::InvalidStructure`] when `bottom_y` is not finite.
423    pub fn render_strict(
424        &self,
425        graphics: &mut GraphicsContext,
426        bottom_y: f64,
427    ) -> Result<(), PdfError> {
428        ensure_finite("bottom_y", bottom_y)?;
429        let rendered = self.fit_count(bottom_y);
430        if rendered < self.rows.len() {
431            return Err(PdfError::TableOverflow {
432                rendered,
433                dropped: self.rows.len() - rendered,
434                bottom_y,
435            });
436        }
437        // Skip the redundant fit_count inside render() by going straight to
438        // the helper with the full row set.
439        self.render_rows_slice(graphics, &self.rows, self.get_height())
440    }
441
442    /// Count the number of leading rows that fully fit above `bottom_y`.
443    fn fit_count(&self, bottom_y: f64) -> usize {
444        let (_start_x, start_y) = self.position;
445        let mut current_y = start_y;
446        let mut count = 0usize;
447        for row in &self.rows {
448            let row_height = self.calculate_row_height(row);
449            let next_y = current_y - row_height;
450            if next_y < bottom_y {
451                break;
452            }
453            count += 1;
454            current_y = next_y;
455        }
456        count
457    }
458
459    /// Internal: draw a slice of rows starting at `self.position`. The
460    /// `rendered_height` parameter sizes the optional table-wide background.
461    fn render_rows_slice(
462        &self,
463        graphics: &mut GraphicsContext,
464        rows: &[TableRow],
465        rendered_height: f64,
466    ) -> Result<(), PdfError> {
467        let (start_x, start_y) = self.position;
468        let mut current_y = start_y;
469
470        // Draw table background if specified, sized to the rendered subset.
471        if let Some(bg_color) = self.options.background_color {
472            graphics.save_state();
473            graphics.set_fill_color(bg_color);
474            graphics.rectangle(
475                start_x,
476                start_y - rendered_height,
477                self.get_width(),
478                rendered_height,
479            );
480            graphics.fill();
481            graphics.restore_state();
482        }
483
484        // Draw each row
485        let mut data_row_index: usize = 0; // Counts only non-header rows (for zebra stripes)
486        for (row_index, row) in rows.iter().enumerate() {
487            let row_height = self.calculate_row_height(row);
488            let mut current_x = start_x;
489
490            // Determine if we should use header styling
491            let use_header_style = row.is_header && self.options.header_style.is_some();
492            let header_style = self.options.header_style.as_ref();
493
494            // Draw cells
495            let mut col_index = 0;
496            for cell in &row.cells {
497                // Calculate cell width (considering colspan)
498                let mut cell_width = 0.0;
499                for i in 0..cell.colspan {
500                    if col_index + i < self.column_widths.len() {
501                        cell_width += self.column_widths[col_index + i];
502                    }
503                }
504
505                // Cell rectangle bottom-left Y (table grows downward)
506                let cell_rect_y = current_y - row_height;
507
508                // Draw cell background
509                // First priority: cell-specific background
510                if let Some(cell_bg) = cell.background_color {
511                    graphics.save_state();
512                    graphics.set_fill_color(cell_bg);
513                    graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
514                    graphics.fill();
515                    graphics.restore_state();
516                }
517                // Second priority: header style background
518                else if use_header_style {
519                    if let Some(style) = header_style {
520                        graphics.save_state();
521                        graphics.set_fill_color(style.background_color);
522                        graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
523                        graphics.fill();
524                        graphics.restore_state();
525                    }
526                }
527                // Third priority: alternating row colors
528                else if let Some((even_color, odd_color)) = self.options.alternating_row_colors {
529                    if !row.is_header {
530                        let color = if data_row_index % 2 == 0 {
531                            even_color
532                        } else {
533                            odd_color
534                        };
535                        graphics.save_state();
536                        graphics.set_fill_color(color);
537                        graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
538                        graphics.fill();
539                        graphics.restore_state();
540                    }
541                }
542
543                // Draw cell border based on grid style
544                let should_draw_border = match self.options.grid_style {
545                    GridStyle::None => false,
546                    GridStyle::Full => true,
547                    GridStyle::Horizontal => {
548                        // Draw top and bottom borders only
549                        true
550                    }
551                    GridStyle::Vertical => {
552                        // Draw left and right borders only
553                        true
554                    }
555                    GridStyle::Outline => {
556                        // Only draw if it's an edge cell.
557                        // For partial renders (page splits), the outline tracks the
558                        // boundary of the rendered slice, not the original full table.
559                        col_index == 0
560                            || col_index + cell.colspan >= self.column_widths.len()
561                            || row_index == 0
562                            || row_index == rows.len() - 1
563                    }
564                };
565
566                if should_draw_border {
567                    graphics.save_state();
568
569                    // Use cell-specific border style if available
570                    let border_style = cell
571                        .border_style
572                        .as_ref()
573                        .unwrap_or(&self.options.cell_border_style);
574
575                    graphics.set_stroke_color(border_style.color);
576                    graphics.set_line_width(border_style.width);
577
578                    // Apply dash pattern if specified
579                    if let Some(dash_pattern) = &border_style.dash_pattern {
580                        graphics.set_line_dash_pattern(dash_pattern.clone());
581                    }
582
583                    // Draw borders based on grid style
584                    match self.options.grid_style {
585                        GridStyle::Full | GridStyle::Outline => {
586                            graphics.rectangle(current_x, cell_rect_y, cell_width, row_height);
587                            graphics.stroke();
588                        }
589                        GridStyle::Horizontal => {
590                            // Top border
591                            graphics.move_to(current_x, current_y);
592                            graphics.line_to(current_x + cell_width, current_y);
593                            // Bottom border
594                            graphics.move_to(current_x, cell_rect_y);
595                            graphics.line_to(current_x + cell_width, cell_rect_y);
596                            graphics.stroke();
597                        }
598                        GridStyle::Vertical => {
599                            // Left border
600                            graphics.move_to(current_x, current_y);
601                            graphics.line_to(current_x, cell_rect_y);
602                            // Right border
603                            graphics.move_to(current_x + cell_width, current_y);
604                            graphics.line_to(current_x + cell_width, cell_rect_y);
605                            graphics.stroke();
606                        }
607                        GridStyle::None => {}
608                    }
609
610                    graphics.restore_state();
611                }
612
613                // Draw cell text
614                // Text baseline: near top of cell, offset by padding and font size
615                let text_x = current_x + self.options.cell_padding;
616                let text_y = current_y - self.options.cell_padding - self.options.font_size;
617                let text_width = cell_width - (2.0 * self.options.cell_padding);
618
619                graphics.save_state();
620
621                // Set font and color
622                if use_header_style {
623                    if let Some(style) = header_style {
624                        let font = if style.bold {
625                            match style.font {
626                                Font::Helvetica => Font::HelveticaBold,
627                                Font::TimesRoman => Font::TimesBold,
628                                Font::Courier => Font::CourierBold,
629                                _ => style.font.clone(),
630                            }
631                        } else {
632                            style.font.clone()
633                        };
634                        graphics.set_font(font, self.options.font_size);
635                        graphics.set_fill_color(style.text_color);
636                    }
637                } else {
638                    graphics.set_font(self.options.font.clone(), self.options.font_size);
639                    graphics.set_fill_color(self.options.text_color);
640                }
641
642                // Split content by newlines for multi-line support
643                let lines: Vec<&str> = cell.content.split('\n').collect();
644                let line_height = self.options.font_size * 1.2;
645
646                // Determine font for measurement (needed for Center/Right alignment)
647                let font_to_measure = if use_header_style {
648                    if let Some(style) = header_style {
649                        if style.bold {
650                            match style.font {
651                                Font::Helvetica => Font::HelveticaBold,
652                                Font::TimesRoman => Font::TimesBold,
653                                Font::Courier => Font::CourierBold,
654                                _ => style.font.clone(),
655                            }
656                        } else {
657                            style.font.clone()
658                        }
659                    } else {
660                        self.options.font.clone()
661                    }
662                } else {
663                    self.options.font.clone()
664                };
665
666                // Draw each line with alignment
667                for (line_idx, line) in lines.iter().enumerate() {
668                    let line_y = text_y - (line_idx as f64 * line_height);
669
670                    let line_x = match cell.align {
671                        TextAlign::Center => {
672                            let measured =
673                                measure_text(line, &font_to_measure, self.options.font_size);
674                            text_x + (text_width - measured) / 2.0
675                        }
676                        TextAlign::Right => {
677                            let measured =
678                                measure_text(line, &font_to_measure, self.options.font_size);
679                            text_x + text_width - measured
680                        }
681                        TextAlign::Left | TextAlign::Justified => text_x,
682                    };
683
684                    graphics.begin_text();
685                    graphics.set_text_position(line_x, line_y);
686                    graphics.show_text(line)?;
687                    graphics.end_text();
688                }
689
690                graphics.restore_state();
691
692                current_x += cell_width;
693                col_index += cell.colspan;
694            }
695
696            if !row.is_header {
697                data_row_index += 1;
698            }
699            current_y -= row_height;
700        }
701
702        Ok(())
703    }
704}
705
706impl TableRow {
707    /// Create a new row with cells
708    #[allow(dead_code)]
709    pub fn new(cells: Vec<TableCell>) -> Self {
710        Self {
711            cells,
712            is_header: false,
713            row_height: None,
714        }
715    }
716
717    /// Create a header row
718    #[allow(dead_code)]
719    pub fn header(cells: Vec<TableCell>) -> Self {
720        Self {
721            cells,
722            is_header: true,
723            row_height: None,
724        }
725    }
726
727    /// Set the height for this specific row (overrides global row_height)
728    pub fn set_row_height(&mut self, height: f64) -> &mut Self {
729        self.row_height = Some(height);
730        self
731    }
732}
733
734impl TableCell {
735    /// Create a new cell with content
736    pub fn new(content: String) -> Self {
737        Self {
738            content,
739            align: TextAlign::Left,
740            colspan: 1,
741            rowspan: 1,
742            background_color: None,
743            border_style: None,
744        }
745    }
746
747    /// Create a cell with specific alignment
748    pub fn with_align(content: String, align: TextAlign) -> Self {
749        Self {
750            content,
751            align,
752            colspan: 1,
753            rowspan: 1,
754            background_color: None,
755            border_style: None,
756        }
757    }
758
759    /// Create a cell with colspan
760    pub fn with_colspan(content: String, colspan: usize) -> Self {
761        Self {
762            content,
763            align: TextAlign::Left,
764            colspan,
765            rowspan: 1,
766            background_color: None,
767            border_style: None,
768        }
769    }
770
771    /// Set cell background color
772    pub fn set_background_color(&mut self, color: Color) -> &mut Self {
773        self.background_color = Some(color);
774        self
775    }
776
777    /// Set cell border style
778    pub fn set_border_style(&mut self, style: CellBorderStyle) -> &mut Self {
779        self.border_style = Some(style);
780        self
781    }
782
783    /// Set rowspan
784    pub fn set_rowspan(&mut self, rowspan: usize) -> &mut Self {
785        self.rowspan = rowspan;
786        self
787    }
788
789    /// Set cell alignment
790    pub fn set_align(&mut self, align: TextAlign) -> &mut Self {
791        self.align = align;
792        self
793    }
794
795    /// Set cell colspan
796    pub fn set_colspan(&mut self, colspan: usize) -> &mut Self {
797        self.colspan = colspan;
798        self
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805
806    #[test]
807    fn test_table_creation() {
808        let table = Table::new(vec![100.0, 150.0, 200.0]);
809        assert_eq!(table.column_widths.len(), 3);
810        assert_eq!(table.rows.len(), 0);
811    }
812
813    #[test]
814    fn test_table_equal_columns() {
815        let table = Table::with_equal_columns(4, 400.0);
816        assert_eq!(table.column_widths.len(), 4);
817        assert_eq!(table.column_widths[0], 100.0);
818        assert_eq!(table.get_width(), 400.0);
819    }
820
821    #[test]
822    fn test_add_header_row() {
823        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
824        let result = table.add_header_row(vec![
825            "Name".to_string(),
826            "Age".to_string(),
827            "City".to_string(),
828        ]);
829        assert!(result.is_ok());
830        assert_eq!(table.rows.len(), 1);
831        assert!(table.rows[0].is_header);
832    }
833
834    #[test]
835    fn test_add_row_mismatch() {
836        let mut table = Table::new(vec![100.0, 100.0]);
837        let result = table.add_row(vec![
838            "John".to_string(),
839            "25".to_string(),
840            "NYC".to_string(),
841        ]);
842        assert!(result.is_err());
843    }
844
845    #[test]
846    fn test_table_cell_creation() {
847        let cell = TableCell::new("Test".to_string());
848        assert_eq!(cell.content, "Test");
849        assert_eq!(cell.align, TextAlign::Left);
850        assert_eq!(cell.colspan, 1);
851    }
852
853    #[test]
854    fn test_table_cell_with_colspan() {
855        let cell = TableCell::with_colspan("Merged".to_string(), 3);
856        assert_eq!(cell.content, "Merged");
857        assert_eq!(cell.colspan, 3);
858    }
859
860    #[test]
861    fn test_custom_row_colspan_validation() {
862        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
863        let cells = vec![
864            TableCell::new("Normal".to_string()),
865            TableCell::with_colspan("Merged".to_string(), 2),
866        ];
867        let result = table.add_custom_row(cells);
868        assert!(result.is_ok());
869        assert_eq!(table.rows.len(), 1);
870    }
871
872    #[test]
873    fn test_custom_row_invalid_colspan() {
874        let mut table = Table::new(vec![100.0, 100.0, 100.0]);
875        let cells = vec![
876            TableCell::new("Normal".to_string()),
877            TableCell::with_colspan("Merged".to_string(), 3), // Total would be 4
878        ];
879        let result = table.add_custom_row(cells);
880        assert!(result.is_err());
881    }
882
883    #[test]
884    fn test_table_options_default() {
885        let options = TableOptions::default();
886        assert_eq!(options.border_width, 1.0);
887        assert_eq!(options.border_color, Color::black());
888        assert_eq!(options.cell_padding, 5.0);
889        assert_eq!(options.font_size, 10.0);
890        assert_eq!(options.grid_style, GridStyle::Full);
891        assert!(options.alternating_row_colors.is_none());
892        assert!(options.background_color.is_none());
893    }
894
895    #[test]
896    fn test_header_style() {
897        let style = HeaderStyle {
898            background_color: Color::gray(0.9),
899            text_color: Color::black(),
900            font: Font::HelveticaBold,
901            bold: true,
902        };
903        assert_eq!(style.background_color, Color::gray(0.9));
904        assert!(style.bold);
905    }
906
907    #[test]
908    fn test_table_dimensions() {
909        let mut table = Table::new(vec![100.0, 150.0, 200.0]);
910        table.options.row_height = 20.0;
911
912        table
913            .add_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])
914            .unwrap();
915        table
916            .add_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])
917            .unwrap();
918
919        assert_eq!(table.get_width(), 450.0);
920        assert_eq!(table.get_height(), 40.0);
921    }
922
923    #[test]
924    fn test_table_position() {
925        let mut table = Table::new(vec![100.0]);
926        table.set_position(50.0, 100.0);
927        assert_eq!(table.position, (50.0, 100.0));
928    }
929
930    #[test]
931    fn test_row_with_alignment() {
932        let mut table = Table::new(vec![100.0, 100.0]);
933        let result = table.add_row_with_alignment(
934            vec!["Left".to_string(), "Right".to_string()],
935            TextAlign::Right,
936        );
937        assert!(result.is_ok());
938        assert_eq!(table.rows[0].cells[0].align, TextAlign::Right);
939    }
940
941    #[test]
942    fn test_table_cell_setters() {
943        let mut cell = TableCell::new("Test".to_string());
944        cell.set_align(TextAlign::Center).set_colspan(2);
945        assert_eq!(cell.align, TextAlign::Center);
946        assert_eq!(cell.colspan, 2);
947    }
948
949    #[test]
950    fn test_auto_row_height() {
951        let table = Table::new(vec![100.0]);
952        let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
953        let height = table.calculate_row_height(&row);
954        assert_eq!(height, 20.0); // font_size (10) + padding*2 (5*2)
955    }
956
957    #[test]
958    fn test_fixed_row_height() {
959        let mut table = Table::new(vec![100.0]);
960        table.options.row_height = 30.0;
961        let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
962        let height = table.calculate_row_height(&row);
963        assert_eq!(height, 30.0);
964    }
965
966    #[test]
967    fn test_grid_styles() {
968        let mut options = TableOptions::default();
969
970        options.grid_style = GridStyle::None;
971        assert_eq!(options.grid_style, GridStyle::None);
972
973        options.grid_style = GridStyle::Horizontal;
974        assert_eq!(options.grid_style, GridStyle::Horizontal);
975
976        options.grid_style = GridStyle::Vertical;
977        assert_eq!(options.grid_style, GridStyle::Vertical);
978
979        options.grid_style = GridStyle::Outline;
980        assert_eq!(options.grid_style, GridStyle::Outline);
981    }
982
983    #[test]
984    fn test_cell_border_style() {
985        let style = CellBorderStyle::default();
986        assert_eq!(style.width, 1.0);
987        assert_eq!(style.color, Color::black());
988        assert!(style.dash_pattern.is_none());
989
990        let custom_style = CellBorderStyle {
991            width: 2.0,
992            color: Color::rgb(1.0, 0.0, 0.0),
993            dash_pattern: Some(LineDashPattern::new(vec![5.0, 3.0], 0.0)),
994        };
995        assert_eq!(custom_style.width, 2.0);
996        assert!(custom_style.dash_pattern.is_some());
997    }
998
999    #[test]
1000    fn test_table_with_alternating_colors() {
1001        let mut table = Table::new(vec![100.0, 100.0]);
1002        table.options.alternating_row_colors = Some((Color::gray(0.95), Color::gray(0.9)));
1003
1004        table
1005            .add_row(vec!["Row 1".to_string(), "Data 1".to_string()])
1006            .unwrap();
1007        table
1008            .add_row(vec!["Row 2".to_string(), "Data 2".to_string()])
1009            .unwrap();
1010
1011        assert_eq!(table.rows.len(), 2);
1012        assert!(table.options.alternating_row_colors.is_some());
1013    }
1014
1015    #[test]
1016    fn test_cell_with_background() {
1017        let mut cell = TableCell::new("Test".to_string());
1018        cell.set_background_color(Color::rgb(0.0, 1.0, 0.0));
1019
1020        assert!(cell.background_color.is_some());
1021        assert_eq!(cell.background_color.unwrap(), Color::rgb(0.0, 1.0, 0.0));
1022    }
1023
1024    #[test]
1025    fn test_cell_with_custom_border() {
1026        let mut cell = TableCell::new("Test".to_string());
1027        let border_style = CellBorderStyle {
1028            width: 2.0,
1029            color: Color::rgb(0.0, 0.0, 1.0),
1030            dash_pattern: None,
1031        };
1032        cell.set_border_style(border_style);
1033
1034        assert!(cell.border_style.is_some());
1035        let style = cell.border_style.as_ref().unwrap();
1036        assert_eq!(style.width, 2.0);
1037        assert_eq!(style.color, Color::rgb(0.0, 0.0, 1.0));
1038    }
1039}