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}
73
74#[derive(Debug, Clone)]
76pub struct TableCell {
77 content: String,
79 align: TextAlign,
81 colspan: usize,
83 rowspan: usize,
85 background_color: Option<Color>,
87 border_style: Option<CellBorderStyle>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
93pub enum GridStyle {
94 None,
96 Horizontal,
98 Vertical,
100 Full,
102 Outline,
104}
105
106#[derive(Debug, Clone)]
108pub struct CellBorderStyle {
109 pub width: f64,
111 pub color: Color,
113 pub dash_pattern: Option<LineDashPattern>,
115}
116
117impl Default for CellBorderStyle {
118 fn default() -> Self {
119 Self {
120 width: 1.0,
121 color: Color::black(),
122 dash_pattern: None,
123 }
124 }
125}
126
127impl Default for TableOptions {
128 fn default() -> Self {
129 Self {
130 border_width: 1.0,
131 border_color: Color::black(),
132 cell_padding: 5.0,
133 row_height: 0.0, font: Font::Helvetica,
135 font_size: 10.0,
136 text_color: Color::black(),
137 header_style: None,
138 grid_style: GridStyle::Full,
139 cell_border_style: CellBorderStyle::default(),
140 alternating_row_colors: None,
141 background_color: None,
142 }
143 }
144}
145
146impl Table {
147 pub fn new(column_widths: Vec<f64>) -> Self {
149 Self {
150 rows: Vec::new(),
151 column_widths,
152 position: (0.0, 0.0),
153 options: TableOptions::default(),
154 }
155 }
156
157 pub fn with_equal_columns(num_columns: usize, total_width: f64) -> Self {
159 let column_width = total_width / num_columns as f64;
160 let column_widths = vec![column_width; num_columns];
161 Self::new(column_widths)
162 }
163
164 pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
166 self.position = (x, y);
167 self
168 }
169
170 pub fn set_options(&mut self, options: TableOptions) -> &mut Self {
172 self.options = options;
173 self
174 }
175
176 pub fn add_header_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
178 if cells.len() != self.column_widths.len() {
179 return Err(PdfError::InvalidStructure(
180 "Header cells count doesn't match column count".to_string(),
181 ));
182 }
183
184 let row_cells: Vec<TableCell> = cells
185 .into_iter()
186 .map(|content| TableCell {
187 content,
188 align: TextAlign::Center,
189 colspan: 1,
190 rowspan: 1,
191 background_color: None,
192 border_style: None,
193 })
194 .collect();
195
196 self.rows.push(TableRow {
197 cells: row_cells,
198 is_header: true,
199 });
200
201 Ok(self)
202 }
203
204 pub fn add_row(&mut self, cells: Vec<String>) -> Result<&mut Self, PdfError> {
206 self.add_row_with_alignment(cells, TextAlign::Left)
207 }
208
209 pub fn add_row_with_alignment(
211 &mut self,
212 cells: Vec<String>,
213 align: TextAlign,
214 ) -> Result<&mut Self, PdfError> {
215 if cells.len() != self.column_widths.len() {
216 return Err(PdfError::InvalidStructure(
217 "Row cells count doesn't match column count".to_string(),
218 ));
219 }
220
221 let row_cells: Vec<TableCell> = cells
222 .into_iter()
223 .map(|content| TableCell {
224 content,
225 align,
226 colspan: 1,
227 rowspan: 1,
228 background_color: None,
229 border_style: None,
230 })
231 .collect();
232
233 self.rows.push(TableRow {
234 cells: row_cells,
235 is_header: false,
236 });
237
238 Ok(self)
239 }
240
241 pub fn add_custom_row(&mut self, cells: Vec<TableCell>) -> Result<&mut Self, PdfError> {
243 let total_colspan: usize = cells.iter().map(|c| c.colspan).sum();
245 if total_colspan != self.column_widths.len() {
246 return Err(PdfError::InvalidStructure(
247 "Total colspan doesn't match column count".to_string(),
248 ));
249 }
250
251 self.rows.push(TableRow {
252 cells,
253 is_header: false,
254 });
255
256 Ok(self)
257 }
258
259 fn calculate_row_height(&self, _row: &TableRow) -> f64 {
261 if self.options.row_height > 0.0 {
262 self.options.row_height
263 } else {
264 self.options.font_size + (self.options.cell_padding * 2.0)
266 }
267 }
268
269 pub fn get_height(&self) -> f64 {
271 self.rows
272 .iter()
273 .map(|row| self.calculate_row_height(row))
274 .sum()
275 }
276
277 pub fn get_width(&self) -> f64 {
279 self.column_widths.iter().sum()
280 }
281
282 pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
284 let (start_x, start_y) = self.position;
285 let mut current_y = start_y;
286
287 if let Some(bg_color) = self.options.background_color {
289 graphics.save_state();
290 graphics.set_fill_color(bg_color);
291 graphics.rectangle(start_x, start_y, self.get_width(), self.get_height());
292 graphics.fill();
293 graphics.restore_state();
294 }
295
296 for (row_index, row) in self.rows.iter().enumerate() {
298 let row_height = self.calculate_row_height(row);
299 let mut current_x = start_x;
300
301 let use_header_style = row.is_header && self.options.header_style.is_some();
303 let header_style = self.options.header_style.as_ref();
304
305 let mut col_index = 0;
307 for cell in &row.cells {
308 let mut cell_width = 0.0;
310 for i in 0..cell.colspan {
311 if col_index + i < self.column_widths.len() {
312 cell_width += self.column_widths[col_index + i];
313 }
314 }
315
316 if let Some(cell_bg) = cell.background_color {
319 graphics.save_state();
320 graphics.set_fill_color(cell_bg);
321 graphics.rectangle(current_x, current_y, cell_width, row_height);
322 graphics.fill();
323 graphics.restore_state();
324 }
325 else if use_header_style {
327 if let Some(style) = header_style {
328 graphics.save_state();
329 graphics.set_fill_color(style.background_color);
330 graphics.rectangle(current_x, current_y, cell_width, row_height);
331 graphics.fill();
332 graphics.restore_state();
333 }
334 }
335 else if let Some((even_color, odd_color)) = self.options.alternating_row_colors {
337 if !row.is_header {
338 let color = if row_index % 2 == 0 {
339 even_color
340 } else {
341 odd_color
342 };
343 graphics.save_state();
344 graphics.set_fill_color(color);
345 graphics.rectangle(current_x, current_y, cell_width, row_height);
346 graphics.fill();
347 graphics.restore_state();
348 }
349 }
350
351 let should_draw_border = match self.options.grid_style {
353 GridStyle::None => false,
354 GridStyle::Full => true,
355 GridStyle::Horizontal => {
356 true
358 }
359 GridStyle::Vertical => {
360 true
362 }
363 GridStyle::Outline => {
364 col_index == 0
366 || col_index + cell.colspan >= self.column_widths.len()
367 || row_index == 0
368 || row_index == self.rows.len() - 1
369 }
370 };
371
372 if should_draw_border {
373 graphics.save_state();
374
375 let border_style = cell
377 .border_style
378 .as_ref()
379 .unwrap_or(&self.options.cell_border_style);
380
381 graphics.set_stroke_color(border_style.color);
382 graphics.set_line_width(border_style.width);
383
384 if let Some(dash_pattern) = &border_style.dash_pattern {
386 graphics.set_line_dash_pattern(dash_pattern.clone());
387 }
388
389 match self.options.grid_style {
391 GridStyle::Full | GridStyle::Outline => {
392 graphics.rectangle(current_x, current_y, cell_width, row_height);
393 graphics.stroke();
394 }
395 GridStyle::Horizontal => {
396 graphics.move_to(current_x, current_y);
398 graphics.line_to(current_x + cell_width, current_y);
399 graphics.move_to(current_x, current_y + row_height);
401 graphics.line_to(current_x + cell_width, current_y + row_height);
402 graphics.stroke();
403 }
404 GridStyle::Vertical => {
405 graphics.move_to(current_x, current_y);
407 graphics.line_to(current_x, current_y + row_height);
408 graphics.move_to(current_x + cell_width, current_y);
410 graphics.line_to(current_x + cell_width, current_y + row_height);
411 graphics.stroke();
412 }
413 GridStyle::None => {}
414 }
415
416 graphics.restore_state();
417 }
418
419 let text_x = current_x + self.options.cell_padding;
421 let text_y =
422 current_y + row_height - self.options.cell_padding - self.options.font_size;
423 let text_width = cell_width - (2.0 * self.options.cell_padding);
424
425 graphics.save_state();
426
427 if use_header_style {
429 if let Some(style) = header_style {
430 let font = if style.bold {
431 match style.font {
432 Font::Helvetica => Font::HelveticaBold,
433 Font::TimesRoman => Font::TimesBold,
434 Font::Courier => Font::CourierBold,
435 _ => style.font.clone(),
436 }
437 } else {
438 style.font.clone()
439 };
440 graphics.set_font(font, self.options.font_size);
441 graphics.set_fill_color(style.text_color);
442 }
443 } else {
444 graphics.set_font(self.options.font.clone(), self.options.font_size);
445 graphics.set_fill_color(self.options.text_color);
446 }
447
448 match cell.align {
450 TextAlign::Left => {
451 graphics.begin_text();
452 graphics.set_text_position(text_x, text_y);
453 graphics.show_text(&cell.content)?;
454 graphics.end_text();
455 }
456 TextAlign::Center => {
457 let font_to_measure = if use_header_style {
459 if let Some(style) = header_style {
460 if style.bold {
461 match style.font {
462 Font::Helvetica => Font::HelveticaBold,
463 Font::TimesRoman => Font::TimesBold,
464 Font::Courier => Font::CourierBold,
465 _ => style.font.clone(),
466 }
467 } else {
468 style.font.clone()
469 }
470 } else {
471 self.options.font.clone()
472 }
473 } else {
474 self.options.font.clone()
475 };
476
477 let text_width_measured =
478 measure_text(&cell.content, font_to_measure, self.options.font_size);
479 let centered_x = text_x + (text_width - text_width_measured) / 2.0;
480 graphics.begin_text();
481 graphics.set_text_position(centered_x, text_y);
482 graphics.show_text(&cell.content)?;
483 graphics.end_text();
484 }
485 TextAlign::Right => {
486 let font_to_measure = if use_header_style {
488 if let Some(style) = header_style {
489 if style.bold {
490 match style.font {
491 Font::Helvetica => Font::HelveticaBold,
492 Font::TimesRoman => Font::TimesBold,
493 Font::Courier => Font::CourierBold,
494 _ => style.font.clone(),
495 }
496 } else {
497 style.font.clone()
498 }
499 } else {
500 self.options.font.clone()
501 }
502 } else {
503 self.options.font.clone()
504 };
505
506 let text_width_measured =
507 measure_text(&cell.content, font_to_measure, self.options.font_size);
508 let right_x = text_x + text_width - text_width_measured;
509 graphics.begin_text();
510 graphics.set_text_position(right_x, text_y);
511 graphics.show_text(&cell.content)?;
512 graphics.end_text();
513 }
514 TextAlign::Justified => {
515 graphics.begin_text();
517 graphics.set_text_position(text_x, text_y);
518 graphics.show_text(&cell.content)?;
519 graphics.end_text();
520 }
521 }
522
523 graphics.restore_state();
524
525 current_x += cell_width;
526 col_index += cell.colspan;
527 }
528
529 current_y += row_height;
530 }
531
532 Ok(())
533 }
534}
535
536impl TableRow {
537 #[allow(dead_code)]
539 pub fn new(cells: Vec<TableCell>) -> Self {
540 Self {
541 cells,
542 is_header: false,
543 }
544 }
545
546 #[allow(dead_code)]
548 pub fn header(cells: Vec<TableCell>) -> Self {
549 Self {
550 cells,
551 is_header: true,
552 }
553 }
554}
555
556impl TableCell {
557 pub fn new(content: String) -> Self {
559 Self {
560 content,
561 align: TextAlign::Left,
562 colspan: 1,
563 rowspan: 1,
564 background_color: None,
565 border_style: None,
566 }
567 }
568
569 pub fn with_align(content: String, align: TextAlign) -> Self {
571 Self {
572 content,
573 align,
574 colspan: 1,
575 rowspan: 1,
576 background_color: None,
577 border_style: None,
578 }
579 }
580
581 pub fn with_colspan(content: String, colspan: usize) -> Self {
583 Self {
584 content,
585 align: TextAlign::Left,
586 colspan,
587 rowspan: 1,
588 background_color: None,
589 border_style: None,
590 }
591 }
592
593 pub fn set_background_color(&mut self, color: Color) -> &mut Self {
595 self.background_color = Some(color);
596 self
597 }
598
599 pub fn set_border_style(&mut self, style: CellBorderStyle) -> &mut Self {
601 self.border_style = Some(style);
602 self
603 }
604
605 pub fn set_rowspan(&mut self, rowspan: usize) -> &mut Self {
607 self.rowspan = rowspan;
608 self
609 }
610
611 pub fn set_align(&mut self, align: TextAlign) -> &mut Self {
613 self.align = align;
614 self
615 }
616
617 pub fn set_colspan(&mut self, colspan: usize) -> &mut Self {
619 self.colspan = colspan;
620 self
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn test_table_creation() {
630 let table = Table::new(vec![100.0, 150.0, 200.0]);
631 assert_eq!(table.column_widths.len(), 3);
632 assert_eq!(table.rows.len(), 0);
633 }
634
635 #[test]
636 fn test_table_equal_columns() {
637 let table = Table::with_equal_columns(4, 400.0);
638 assert_eq!(table.column_widths.len(), 4);
639 assert_eq!(table.column_widths[0], 100.0);
640 assert_eq!(table.get_width(), 400.0);
641 }
642
643 #[test]
644 fn test_add_header_row() {
645 let mut table = Table::new(vec![100.0, 100.0, 100.0]);
646 let result = table.add_header_row(vec![
647 "Name".to_string(),
648 "Age".to_string(),
649 "City".to_string(),
650 ]);
651 assert!(result.is_ok());
652 assert_eq!(table.rows.len(), 1);
653 assert!(table.rows[0].is_header);
654 }
655
656 #[test]
657 fn test_add_row_mismatch() {
658 let mut table = Table::new(vec![100.0, 100.0]);
659 let result = table.add_row(vec![
660 "John".to_string(),
661 "25".to_string(),
662 "NYC".to_string(),
663 ]);
664 assert!(result.is_err());
665 }
666
667 #[test]
668 fn test_table_cell_creation() {
669 let cell = TableCell::new("Test".to_string());
670 assert_eq!(cell.content, "Test");
671 assert_eq!(cell.align, TextAlign::Left);
672 assert_eq!(cell.colspan, 1);
673 }
674
675 #[test]
676 fn test_table_cell_with_colspan() {
677 let cell = TableCell::with_colspan("Merged".to_string(), 3);
678 assert_eq!(cell.content, "Merged");
679 assert_eq!(cell.colspan, 3);
680 }
681
682 #[test]
683 fn test_custom_row_colspan_validation() {
684 let mut table = Table::new(vec![100.0, 100.0, 100.0]);
685 let cells = vec![
686 TableCell::new("Normal".to_string()),
687 TableCell::with_colspan("Merged".to_string(), 2),
688 ];
689 let result = table.add_custom_row(cells);
690 assert!(result.is_ok());
691 assert_eq!(table.rows.len(), 1);
692 }
693
694 #[test]
695 fn test_custom_row_invalid_colspan() {
696 let mut table = Table::new(vec![100.0, 100.0, 100.0]);
697 let cells = vec![
698 TableCell::new("Normal".to_string()),
699 TableCell::with_colspan("Merged".to_string(), 3), ];
701 let result = table.add_custom_row(cells);
702 assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_table_options_default() {
707 let options = TableOptions::default();
708 assert_eq!(options.border_width, 1.0);
709 assert_eq!(options.border_color, Color::black());
710 assert_eq!(options.cell_padding, 5.0);
711 assert_eq!(options.font_size, 10.0);
712 assert_eq!(options.grid_style, GridStyle::Full);
713 assert!(options.alternating_row_colors.is_none());
714 assert!(options.background_color.is_none());
715 }
716
717 #[test]
718 fn test_header_style() {
719 let style = HeaderStyle {
720 background_color: Color::gray(0.9),
721 text_color: Color::black(),
722 font: Font::HelveticaBold,
723 bold: true,
724 };
725 assert_eq!(style.background_color, Color::gray(0.9));
726 assert!(style.bold);
727 }
728
729 #[test]
730 fn test_table_dimensions() {
731 let mut table = Table::new(vec![100.0, 150.0, 200.0]);
732 table.options.row_height = 20.0;
733
734 table
735 .add_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])
736 .unwrap();
737 table
738 .add_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])
739 .unwrap();
740
741 assert_eq!(table.get_width(), 450.0);
742 assert_eq!(table.get_height(), 40.0);
743 }
744
745 #[test]
746 fn test_table_position() {
747 let mut table = Table::new(vec![100.0]);
748 table.set_position(50.0, 100.0);
749 assert_eq!(table.position, (50.0, 100.0));
750 }
751
752 #[test]
753 fn test_row_with_alignment() {
754 let mut table = Table::new(vec![100.0, 100.0]);
755 let result = table.add_row_with_alignment(
756 vec!["Left".to_string(), "Right".to_string()],
757 TextAlign::Right,
758 );
759 assert!(result.is_ok());
760 assert_eq!(table.rows[0].cells[0].align, TextAlign::Right);
761 }
762
763 #[test]
764 fn test_table_cell_setters() {
765 let mut cell = TableCell::new("Test".to_string());
766 cell.set_align(TextAlign::Center).set_colspan(2);
767 assert_eq!(cell.align, TextAlign::Center);
768 assert_eq!(cell.colspan, 2);
769 }
770
771 #[test]
772 fn test_auto_row_height() {
773 let table = Table::new(vec![100.0]);
774 let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
775 let height = table.calculate_row_height(&row);
776 assert_eq!(height, 20.0); }
778
779 #[test]
780 fn test_fixed_row_height() {
781 let mut table = Table::new(vec![100.0]);
782 table.options.row_height = 30.0;
783 let row = TableRow::new(vec![TableCell::new("Test".to_string())]);
784 let height = table.calculate_row_height(&row);
785 assert_eq!(height, 30.0);
786 }
787
788 #[test]
789 fn test_grid_styles() {
790 let mut options = TableOptions::default();
791
792 options.grid_style = GridStyle::None;
793 assert_eq!(options.grid_style, GridStyle::None);
794
795 options.grid_style = GridStyle::Horizontal;
796 assert_eq!(options.grid_style, GridStyle::Horizontal);
797
798 options.grid_style = GridStyle::Vertical;
799 assert_eq!(options.grid_style, GridStyle::Vertical);
800
801 options.grid_style = GridStyle::Outline;
802 assert_eq!(options.grid_style, GridStyle::Outline);
803 }
804
805 #[test]
806 fn test_cell_border_style() {
807 let style = CellBorderStyle::default();
808 assert_eq!(style.width, 1.0);
809 assert_eq!(style.color, Color::black());
810 assert!(style.dash_pattern.is_none());
811
812 let custom_style = CellBorderStyle {
813 width: 2.0,
814 color: Color::rgb(1.0, 0.0, 0.0),
815 dash_pattern: Some(LineDashPattern::new(vec![5.0, 3.0], 0.0)),
816 };
817 assert_eq!(custom_style.width, 2.0);
818 assert!(custom_style.dash_pattern.is_some());
819 }
820
821 #[test]
822 fn test_table_with_alternating_colors() {
823 let mut table = Table::new(vec![100.0, 100.0]);
824 table.options.alternating_row_colors = Some((Color::gray(0.95), Color::gray(0.9)));
825
826 table
827 .add_row(vec!["Row 1".to_string(), "Data 1".to_string()])
828 .unwrap();
829 table
830 .add_row(vec!["Row 2".to_string(), "Data 2".to_string()])
831 .unwrap();
832
833 assert_eq!(table.rows.len(), 2);
834 assert!(table.options.alternating_row_colors.is_some());
835 }
836
837 #[test]
838 fn test_cell_with_background() {
839 let mut cell = TableCell::new("Test".to_string());
840 cell.set_background_color(Color::rgb(0.0, 1.0, 0.0));
841
842 assert!(cell.background_color.is_some());
843 assert_eq!(cell.background_color.unwrap(), Color::rgb(0.0, 1.0, 0.0));
844 }
845
846 #[test]
847 fn test_cell_with_custom_border() {
848 let mut cell = TableCell::new("Test".to_string());
849 let border_style = CellBorderStyle {
850 width: 2.0,
851 color: Color::rgb(0.0, 0.0, 1.0),
852 dash_pattern: None,
853 };
854 cell.set_border_style(border_style);
855
856 assert!(cell.border_style.is_some());
857 let style = cell.border_style.as_ref().unwrap();
858 assert_eq!(style.width, 2.0);
859 assert_eq!(style.color, Color::rgb(0.0, 0.0, 1.0));
860 }
861}