1use crate::error::PdfError;
7use crate::graphics::{Color, GraphicsContext, LineDashPattern};
8use crate::text::{measure_text, Font, TextAlign};
9
10#[derive(Debug, Clone)]
12pub struct Table {
13 rows: Vec<TableRow>,
15 column_widths: Vec<f64>,
17 position: (f64, f64),
19 options: TableOptions,
21}
22
23#[derive(Debug, Clone)]
25pub struct TableOptions {
26 pub border_width: f64,
28 pub border_color: Color,
30 pub cell_padding: f64,
32 pub row_height: f64,
34 pub font: Font,
36 pub font_size: f64,
38 pub text_color: Color,
40 pub header_style: Option<HeaderStyle>,
42 pub grid_style: GridStyle,
44 pub cell_border_style: CellBorderStyle,
46 pub alternating_row_colors: Option<(Color, Color)>,
48 pub background_color: Option<Color>,
50}
51
52#[derive(Debug, Clone)]
54pub struct HeaderStyle {
55 pub background_color: Color,
57 pub text_color: Color,
59 pub font: Font,
61 pub bold: bool,
63}
64
65#[derive(Debug, Clone)]
67pub struct TableRow {
68 cells: Vec<TableCell>,
70 is_header: bool,
72 row_height: Option<f64>,
74}
75
76#[derive(Debug, Clone)]
78pub struct TableCell {
79 content: String,
81 align: TextAlign,
83 colspan: usize,
85 rowspan: usize,
87 background_color: Option<Color>,
89 border_style: Option<CellBorderStyle>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum GridStyle {
96 None,
98 Horizontal,
100 Vertical,
102 Full,
104 Outline,
106}
107
108#[derive(Debug, Clone)]
110pub struct CellBorderStyle {
111 pub width: f64,
113 pub color: Color,
115 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, 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 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 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 pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
168 self.position = (x, y);
169 self
170 }
171
172 pub fn set_options(&mut self, options: TableOptions) -> &mut Self {
174 self.options = options;
175 self
176 }
177
178 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 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 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 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 pub fn add_custom_row(&mut self, cells: Vec<TableCell>) -> Result<&mut Self, PdfError> {
255 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 fn calculate_row_height(&self, row: &TableRow) -> f64 {
274 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 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 self.options.font_size + (self.options.cell_padding * 2.0)
294 } else {
295 self.options.font_size
297 + ((max_lines - 1) as f64 * line_height)
298 + (self.options.cell_padding * 2.0)
299 }
300 }
301
302 pub fn get_height(&self) -> f64 {
304 self.rows
305 .iter()
306 .map(|row| self.calculate_row_height(row))
307 .sum()
308 }
309
310 pub fn get_width(&self) -> f64 {
312 self.column_widths.iter().sum()
313 }
314
315 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 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 let mut data_row_index: usize = 0; 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 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 let mut col_index = 0;
347 for cell in &row.cells {
348 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 let cell_rect_y = current_y - row_height;
358
359 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 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 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 let should_draw_border = match self.options.grid_style {
396 GridStyle::None => false,
397 GridStyle::Full => true,
398 GridStyle::Horizontal => {
399 true
401 }
402 GridStyle::Vertical => {
403 true
405 }
406 GridStyle::Outline => {
407 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 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 if let Some(dash_pattern) = &border_style.dash_pattern {
429 graphics.set_line_dash_pattern(dash_pattern.clone());
430 }
431
432 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 graphics.move_to(current_x, current_y);
441 graphics.line_to(current_x + cell_width, current_y);
442 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 graphics.move_to(current_x, current_y);
450 graphics.line_to(current_x, cell_rect_y);
451 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 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 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 let lines: Vec<&str> = cell.content.split('\n').collect();
493 let line_height = self.options.font_size * 1.2;
494
495 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 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 #[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 #[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 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 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 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 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 pub fn set_background_color(&mut self, color: Color) -> &mut Self {
622 self.background_color = Some(color);
623 self
624 }
625
626 pub fn set_border_style(&mut self, style: CellBorderStyle) -> &mut Self {
628 self.border_style = Some(style);
629 self
630 }
631
632 pub fn set_rowspan(&mut self, rowspan: usize) -> &mut Self {
634 self.rowspan = rowspan;
635 self
636 }
637
638 pub fn set_align(&mut self, align: TextAlign) -> &mut Self {
640 self.align = align;
641 self
642 }
643
644 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), ];
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); }
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}