1use crate::error::PdfError;
13use crate::graphics::{Color, GraphicsContext, LineDashPattern};
14use crate::text::{Font, TextAlign};
15
16#[derive(Debug, Clone)]
18pub struct AdvancedTable {
19 rows: Vec<TableRow>,
21 columns: Vec<ColumnDefinition>,
23 position: (f64, f64),
25 options: AdvancedTableOptions,
27 header_rows: Vec<TableRow>,
29 footer_rows: Vec<TableRow>,
31}
32
33#[derive(Debug, Clone)]
35pub struct AdvancedTableOptions {
36 pub border_style: BorderStyle,
38 pub cell_padding: CellPadding,
40 pub row_height: f64,
42 pub font: Font,
44 pub font_size: f64,
46 pub text_color: Color,
48 pub alternating_rows: Option<AlternatingRowColors>,
50 pub background_color: Option<Color>,
52 pub max_height: Option<f64>,
54 pub cell_spacing: f64,
56 pub draw_outer_border: bool,
58}
59
60#[derive(Debug, Clone)]
62pub struct ColumnDefinition {
63 pub width: ColumnWidth,
65 pub default_align: TextAlign,
67 pub min_width: Option<f64>,
69 pub max_width: Option<f64>,
71}
72
73#[derive(Debug, Clone)]
75pub enum ColumnWidth {
76 Fixed(f64),
78 Relative(f64),
80 Auto,
82}
83
84#[derive(Debug, Clone)]
86pub struct BorderStyle {
87 pub top: Option<BorderLine>,
89 pub right: Option<BorderLine>,
91 pub bottom: Option<BorderLine>,
93 pub left: Option<BorderLine>,
95}
96
97#[derive(Debug, Clone)]
99pub struct BorderLine {
100 pub width: f64,
102 pub color: Color,
104 pub style: LineStyle,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq)]
110pub enum LineStyle {
111 Solid,
113 Dashed,
115 Dotted,
117}
118
119#[derive(Debug, Clone, Copy)]
121pub struct CellPadding {
122 pub top: f64,
123 pub right: f64,
124 pub bottom: f64,
125 pub left: f64,
126}
127
128#[derive(Debug, Clone)]
130pub struct AlternatingRowColors {
131 pub even_color: Color,
133 pub odd_color: Color,
135 pub include_header: bool,
137}
138
139#[derive(Debug, Clone)]
141pub struct TableRow {
142 cells: Vec<AdvancedTableCell>,
144 height: Option<f64>,
146 background_color: Option<Color>,
148 can_split: bool,
150}
151
152#[derive(Debug, Clone)]
154pub struct AdvancedTableCell {
155 content: CellContent,
157 align: TextAlign,
159 vertical_align: VerticalAlign,
161 colspan: usize,
163 rowspan: usize,
165 background_color: Option<Color>,
167 border_style: Option<BorderStyle>,
169 padding: Option<CellPadding>,
171 font: Option<Font>,
173 font_size: Option<f64>,
175 text_color: Option<Color>,
177}
178
179#[derive(Debug, Clone)]
181pub enum CellContent {
182 Text(String),
184 Paragraphs(Vec<String>),
186}
187
188#[derive(Debug, Clone, Copy, PartialEq)]
190pub enum VerticalAlign {
191 Top,
192 Middle,
193 Bottom,
194}
195
196impl Default for AdvancedTableOptions {
197 fn default() -> Self {
198 Self {
199 border_style: BorderStyle::default(),
200 cell_padding: CellPadding::uniform(5.0),
201 row_height: 0.0,
202 font: Font::Helvetica,
203 font_size: 10.0,
204 text_color: Color::black(),
205 alternating_rows: None,
206 background_color: None,
207 max_height: None,
208 cell_spacing: 0.0,
209 draw_outer_border: true,
210 }
211 }
212}
213
214impl Default for BorderStyle {
215 fn default() -> Self {
216 let default_line = BorderLine {
217 width: 1.0,
218 color: Color::black(),
219 style: LineStyle::Solid,
220 };
221 Self {
222 top: Some(default_line.clone()),
223 right: Some(default_line.clone()),
224 bottom: Some(default_line.clone()),
225 left: Some(default_line),
226 }
227 }
228}
229
230impl BorderStyle {
231 pub fn none() -> Self {
233 Self {
234 top: None,
235 right: None,
236 bottom: None,
237 left: None,
238 }
239 }
240
241 pub fn horizontal_only(width: f64, color: Color) -> Self {
243 let line = BorderLine {
244 width,
245 color,
246 style: LineStyle::Solid,
247 };
248 Self {
249 top: Some(line.clone()),
250 right: None,
251 bottom: Some(line),
252 left: None,
253 }
254 }
255
256 pub fn vertical_only(width: f64, color: Color) -> Self {
258 let line = BorderLine {
259 width,
260 color,
261 style: LineStyle::Solid,
262 };
263 Self {
264 top: None,
265 right: Some(line.clone()),
266 bottom: None,
267 left: Some(line),
268 }
269 }
270}
271
272impl CellPadding {
273 pub fn uniform(padding: f64) -> Self {
275 Self {
276 top: padding,
277 right: padding,
278 bottom: padding,
279 left: padding,
280 }
281 }
282
283 pub fn symmetric(horizontal: f64, vertical: f64) -> Self {
285 Self {
286 top: vertical,
287 right: horizontal,
288 bottom: vertical,
289 left: horizontal,
290 }
291 }
292}
293
294impl AdvancedTable {
295 pub fn new(columns: Vec<ColumnDefinition>) -> Self {
297 Self {
298 rows: Vec::new(),
299 columns,
300 position: (0.0, 0.0),
301 options: AdvancedTableOptions::default(),
302 header_rows: Vec::new(),
303 footer_rows: Vec::new(),
304 }
305 }
306
307 pub fn with_equal_columns(num_columns: usize, total_width: f64) -> Self {
309 let column_width = total_width / num_columns as f64;
310 let columns = (0..num_columns)
311 .map(|_| ColumnDefinition {
312 width: ColumnWidth::Fixed(column_width),
313 default_align: TextAlign::Left,
314 min_width: None,
315 max_width: None,
316 })
317 .collect();
318 Self::new(columns)
319 }
320
321 pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
323 self.position = (x, y);
324 self
325 }
326
327 pub fn set_options(&mut self, options: AdvancedTableOptions) -> &mut Self {
329 self.options = options;
330 self
331 }
332
333 pub fn add_header_row(&mut self, cells: Vec<AdvancedTableCell>) -> Result<&mut Self, PdfError> {
335 self.validate_row_cells(&cells)?;
336 self.header_rows.push(TableRow {
337 cells,
338 height: None,
339 background_color: None,
340 can_split: false,
341 });
342 Ok(self)
343 }
344
345 pub fn add_footer_row(&mut self, cells: Vec<AdvancedTableCell>) -> Result<&mut Self, PdfError> {
347 self.validate_row_cells(&cells)?;
348 self.footer_rows.push(TableRow {
349 cells,
350 height: None,
351 background_color: None,
352 can_split: false,
353 });
354 Ok(self)
355 }
356
357 pub fn add_row(&mut self, row: TableRow) -> Result<&mut Self, PdfError> {
359 self.validate_row_cells(&row.cells)?;
360 self.rows.push(row);
361 Ok(self)
362 }
363
364 pub fn add_text_row(&mut self, texts: Vec<String>) -> Result<&mut Self, PdfError> {
366 if texts.len() != self.columns.len() {
367 return Err(PdfError::InvalidStructure(
368 "Text count doesn't match column count".to_string(),
369 ));
370 }
371
372 let cells: Vec<AdvancedTableCell> = texts
373 .into_iter()
374 .enumerate()
375 .map(|(i, text)| AdvancedTableCell {
376 content: CellContent::Text(text),
377 align: self.columns[i].default_align,
378 vertical_align: VerticalAlign::Middle,
379 colspan: 1,
380 rowspan: 1,
381 background_color: None,
382 border_style: None,
383 padding: None,
384 font: None,
385 font_size: None,
386 text_color: None,
387 })
388 .collect();
389
390 self.add_row(TableRow {
391 cells,
392 height: None,
393 background_color: None,
394 can_split: true,
395 })
396 }
397
398 fn validate_row_cells(&self, cells: &[AdvancedTableCell]) -> Result<(), PdfError> {
400 let total_colspan: usize = cells.iter().map(|c| c.colspan).sum();
401 if total_colspan != self.columns.len() {
402 return Err(PdfError::InvalidStructure(format!(
403 "Total colspan {} doesn't match column count {}",
404 total_colspan,
405 self.columns.len()
406 )));
407 }
408 Ok(())
409 }
410
411 pub fn calculate_column_widths(&self, available_width: f64) -> Vec<f64> {
413 let mut widths = vec![0.0; self.columns.len()];
414 let mut fixed_width = 0.0;
415 let mut relative_total = 0.0;
416 let mut auto_columns = Vec::new();
417
418 for (i, col) in self.columns.iter().enumerate() {
420 match &col.width {
421 ColumnWidth::Fixed(w) => {
422 widths[i] = *w;
423 fixed_width += *w;
424 }
425 ColumnWidth::Relative(pct) => {
426 relative_total += *pct;
427 }
428 ColumnWidth::Auto => {
429 auto_columns.push(i);
430 }
431 }
432 }
433
434 let remaining_width = available_width - fixed_width;
436
437 for (i, col) in self.columns.iter().enumerate() {
439 if let ColumnWidth::Relative(pct) = col.width {
440 widths[i] = remaining_width * (pct / relative_total);
441 }
442 }
443
444 if !auto_columns.is_empty() {
446 let auto_width = remaining_width / auto_columns.len() as f64;
447 for &i in &auto_columns {
448 widths[i] = auto_width;
449
450 if let Some(min) = self.columns[i].min_width {
452 widths[i] = widths[i].max(min);
453 }
454 if let Some(max) = self.columns[i].max_width {
455 widths[i] = widths[i].min(max);
456 }
457 }
458 }
459
460 widths
461 }
462
463 pub fn get_width(&self, available_width: f64) -> f64 {
465 self.calculate_column_widths(available_width).iter().sum()
466 }
467
468 fn calculate_row_height(&self, row: &TableRow, _column_widths: &[f64]) -> f64 {
470 if let Some(height) = row.height {
471 return height;
472 }
473
474 if self.options.row_height > 0.0 {
475 return self.options.row_height;
476 }
477
478 let padding = self.options.cell_padding;
480 let font_size = self.options.font_size;
481
482 font_size + padding.top + padding.bottom
484 }
485
486 pub fn render(
488 &self,
489 graphics: &mut GraphicsContext,
490 available_width: f64,
491 ) -> Result<(), PdfError> {
492 let column_widths = self.calculate_column_widths(available_width);
493 let (start_x, mut current_y) = self.position;
494
495 if let Some(bg_color) = self.options.background_color {
497 let table_width: f64 = column_widths.iter().sum();
498 let table_height = self.calculate_total_height(&column_widths);
499
500 graphics.save_state();
501 graphics.set_fill_color(bg_color);
502 graphics.rectangle(start_x, current_y, table_width, table_height);
503 graphics.fill();
504 graphics.restore_state();
505 }
506
507 for header_row in &self.header_rows {
509 self.render_row(
510 graphics,
511 header_row,
512 &column_widths,
513 start_x,
514 &mut current_y,
515 None,
516 )?;
517 }
518
519 for (row_index, row) in self.rows.iter().enumerate() {
521 let row_color = self.get_row_background_color(row, row_index);
522 self.render_row(
523 graphics,
524 row,
525 &column_widths,
526 start_x,
527 &mut current_y,
528 row_color,
529 )?;
530 }
531
532 for footer_row in &self.footer_rows {
534 self.render_row(
535 graphics,
536 footer_row,
537 &column_widths,
538 start_x,
539 &mut current_y,
540 None,
541 )?;
542 }
543
544 Ok(())
545 }
546
547 fn render_row(
549 &self,
550 graphics: &mut GraphicsContext,
551 row: &TableRow,
552 column_widths: &[f64],
553 start_x: f64,
554 current_y: &mut f64,
555 row_background: Option<Color>,
556 ) -> Result<(), PdfError> {
557 let row_height = self.calculate_row_height(row, column_widths);
558 let mut current_x = start_x;
559
560 if let Some(color) = row_background.or(row.background_color) {
562 let row_width: f64 = column_widths.iter().sum();
563 graphics.save_state();
564 graphics.set_fill_color(color);
565 graphics.rectangle(start_x, *current_y, row_width, row_height);
566 graphics.fill();
567 graphics.restore_state();
568 }
569
570 let mut col_index = 0;
572 for cell in &row.cells {
573 let mut cell_width = 0.0;
574 for i in 0..cell.colspan {
575 if col_index + i < column_widths.len() {
576 cell_width += column_widths[col_index + i];
577 }
578 }
579
580 self.render_cell(
581 graphics, cell, current_x, *current_y, cell_width, row_height,
582 )?;
583
584 current_x += cell_width;
585 col_index += cell.colspan;
586 }
587
588 *current_y += row_height;
589 Ok(())
590 }
591
592 fn render_cell(
594 &self,
595 graphics: &mut GraphicsContext,
596 cell: &AdvancedTableCell,
597 x: f64,
598 y: f64,
599 width: f64,
600 height: f64,
601 ) -> Result<(), PdfError> {
602 if let Some(bg_color) = cell.background_color {
604 graphics.save_state();
605 graphics.set_fill_color(bg_color);
606 graphics.rectangle(x, y, width, height);
607 graphics.fill();
608 graphics.restore_state();
609 }
610
611 let border_style = cell
613 .border_style
614 .as_ref()
615 .unwrap_or(&self.options.border_style);
616 self.draw_cell_borders(graphics, border_style, x, y, width, height)?;
617
618 let padding = cell.padding.unwrap_or(self.options.cell_padding);
620 let content_x = x + padding.left;
621 let content_y = y + padding.bottom;
622 let content_width = width - padding.left - padding.right;
623 let content_height = height - padding.top - padding.bottom;
624
625 match &cell.content {
626 CellContent::Text(text) => {
627 self.render_text_content(
628 graphics,
629 text,
630 cell,
631 content_x,
632 content_y,
633 content_width,
634 content_height,
635 )?;
636 }
637 CellContent::Paragraphs(paragraphs) => {
638 self.render_paragraphs_content(
639 graphics,
640 paragraphs,
641 cell,
642 content_x,
643 content_y,
644 content_width,
645 content_height,
646 )?;
647 }
648 }
649
650 Ok(())
651 }
652
653 fn draw_cell_borders(
655 &self,
656 graphics: &mut GraphicsContext,
657 border_style: &BorderStyle,
658 x: f64,
659 y: f64,
660 width: f64,
661 height: f64,
662 ) -> Result<(), PdfError> {
663 graphics.save_state();
664
665 if let Some(border) = &border_style.top {
667 self.draw_border_line(graphics, border, x, y, x + width, y)?;
668 }
669
670 if let Some(border) = &border_style.right {
672 self.draw_border_line(graphics, border, x + width, y, x + width, y + height)?;
673 }
674
675 if let Some(border) = &border_style.bottom {
677 self.draw_border_line(graphics, border, x, y + height, x + width, y + height)?;
678 }
679
680 if let Some(border) = &border_style.left {
682 self.draw_border_line(graphics, border, x, y, x, y + height)?;
683 }
684
685 graphics.restore_state();
686 Ok(())
687 }
688
689 fn draw_border_line(
691 &self,
692 graphics: &mut GraphicsContext,
693 border: &BorderLine,
694 x1: f64,
695 y1: f64,
696 x2: f64,
697 y2: f64,
698 ) -> Result<(), PdfError> {
699 graphics.set_stroke_color(border.color);
700 graphics.set_line_width(border.width);
701
702 match border.style {
703 LineStyle::Solid => {
704 graphics.move_to(x1, y1);
705 graphics.line_to(x2, y2);
706 graphics.stroke();
707 }
708 LineStyle::Dashed => {
709 graphics.set_line_dash_pattern(LineDashPattern::dashed(3.0, 3.0));
710 graphics.move_to(x1, y1);
711 graphics.line_to(x2, y2);
712 graphics.stroke();
713 graphics.set_line_solid(); }
715 LineStyle::Dotted => {
716 graphics.set_line_dash_pattern(LineDashPattern::dotted(1.0, 2.0));
717 graphics.move_to(x1, y1);
718 graphics.line_to(x2, y2);
719 graphics.stroke();
720 graphics.set_line_solid(); }
722 }
723
724 Ok(())
725 }
726
727 #[allow(clippy::too_many_arguments)]
729 fn render_text_content(
730 &self,
731 graphics: &mut GraphicsContext,
732 text: &str,
733 cell: &AdvancedTableCell,
734 x: f64,
735 y: f64,
736 width: f64,
737 height: f64,
738 ) -> Result<(), PdfError> {
739 graphics.save_state();
740
741 let font = cell.font.clone().unwrap_or(self.options.font.clone());
743 let font_size = cell.font_size.unwrap_or(self.options.font_size);
744 let text_color = cell.text_color.unwrap_or(self.options.text_color);
745
746 graphics.set_font(font, font_size);
747 graphics.set_fill_color(text_color);
748
749 let text_y = match cell.vertical_align {
751 VerticalAlign::Top => y + height - font_size,
752 VerticalAlign::Middle => y + (height - font_size) / 2.0,
753 VerticalAlign::Bottom => y + font_size,
754 };
755
756 match cell.align {
758 TextAlign::Left => {
759 graphics.begin_text();
760 graphics.set_text_position(x, text_y);
761 graphics.show_text(text)?;
762 graphics.end_text();
763 }
764 TextAlign::Center => {
765 let text_width = text.len() as f64 * font_size * 0.5; let centered_x = x + (width - text_width) / 2.0;
767 graphics.begin_text();
768 graphics.set_text_position(centered_x, text_y);
769 graphics.show_text(text)?;
770 graphics.end_text();
771 }
772 TextAlign::Right => {
773 let text_width = text.len() as f64 * font_size * 0.5; let right_x = x + width - text_width;
775 graphics.begin_text();
776 graphics.set_text_position(right_x, text_y);
777 graphics.show_text(text)?;
778 graphics.end_text();
779 }
780 TextAlign::Justified => {
781 graphics.begin_text();
783 graphics.set_text_position(x, text_y);
784 graphics.show_text(text)?;
785 graphics.end_text();
786 }
787 }
788
789 graphics.restore_state();
790 Ok(())
791 }
792
793 #[allow(clippy::too_many_arguments)]
795 fn render_paragraphs_content(
796 &self,
797 graphics: &mut GraphicsContext,
798 paragraphs: &[String],
799 cell: &AdvancedTableCell,
800 x: f64,
801 y: f64,
802 width: f64,
803 height: f64,
804 ) -> Result<(), PdfError> {
805 let font_size = cell.font_size.unwrap_or(self.options.font_size);
806 let line_height = font_size * 1.2;
807 let mut current_y = y + height - font_size;
808
809 for paragraph in paragraphs {
810 if current_y < y {
811 break; }
813
814 self.render_text_content(
815 graphics,
816 paragraph,
817 cell,
818 x,
819 current_y - (height - font_size),
820 width,
821 font_size,
822 )?;
823
824 current_y -= line_height;
825 }
826
827 Ok(())
828 }
829
830 fn calculate_total_height(&self, column_widths: &[f64]) -> f64 {
832 let mut height = 0.0;
833
834 for row in &self.header_rows {
836 height += self.calculate_row_height(row, column_widths);
837 }
838
839 for row in &self.rows {
841 height += self.calculate_row_height(row, column_widths);
842 }
843
844 for row in &self.footer_rows {
846 height += self.calculate_row_height(row, column_widths);
847 }
848
849 height
850 }
851
852 fn get_row_background_color(&self, row: &TableRow, row_index: usize) -> Option<Color> {
854 if row.background_color.is_some() {
855 return row.background_color;
856 }
857
858 if let Some(alt_colors) = &self.options.alternating_rows {
859 let index = if alt_colors.include_header {
860 row_index + self.header_rows.len()
861 } else {
862 row_index
863 };
864
865 if index % 2 == 0 {
866 Some(alt_colors.even_color)
867 } else {
868 Some(alt_colors.odd_color)
869 }
870 } else {
871 None
872 }
873 }
874}
875
876impl AdvancedTableCell {
877 pub fn text(content: String) -> Self {
879 Self {
880 content: CellContent::Text(content),
881 align: TextAlign::Left,
882 vertical_align: VerticalAlign::Middle,
883 colspan: 1,
884 rowspan: 1,
885 background_color: None,
886 border_style: None,
887 padding: None,
888 font: None,
889 font_size: None,
890 text_color: None,
891 }
892 }
893
894 pub fn paragraphs(paragraphs: Vec<String>) -> Self {
896 Self {
897 content: CellContent::Paragraphs(paragraphs),
898 align: TextAlign::Left,
899 vertical_align: VerticalAlign::Top,
900 colspan: 1,
901 rowspan: 1,
902 background_color: None,
903 border_style: None,
904 padding: None,
905 font: None,
906 font_size: None,
907 text_color: None,
908 }
909 }
910
911 pub fn with_align(mut self, align: TextAlign) -> Self {
913 self.align = align;
914 self
915 }
916
917 pub fn with_vertical_align(mut self, align: VerticalAlign) -> Self {
919 self.vertical_align = align;
920 self
921 }
922
923 pub fn with_colspan(mut self, colspan: usize) -> Self {
925 self.colspan = colspan;
926 self
927 }
928
929 pub fn with_rowspan(mut self, rowspan: usize) -> Self {
931 self.rowspan = rowspan;
932 self
933 }
934
935 pub fn with_background(mut self, color: Color) -> Self {
937 self.background_color = Some(color);
938 self
939 }
940
941 pub fn with_padding(mut self, padding: CellPadding) -> Self {
943 self.padding = Some(padding);
944 self
945 }
946
947 pub fn with_font(mut self, font: Font, size: f64) -> Self {
949 self.font = Some(font);
950 self.font_size = Some(size);
951 self
952 }
953
954 pub fn with_text_color(mut self, color: Color) -> Self {
956 self.text_color = Some(color);
957 self
958 }
959}
960
961impl TableRow {
962 pub fn new(cells: Vec<AdvancedTableCell>) -> Self {
964 Self {
965 cells,
966 height: None,
967 background_color: None,
968 can_split: true,
969 }
970 }
971
972 pub fn with_height(mut self, height: f64) -> Self {
974 self.height = Some(height);
975 self
976 }
977
978 pub fn with_background(mut self, color: Color) -> Self {
980 self.background_color = Some(color);
981 self
982 }
983
984 pub fn with_can_split(mut self, can_split: bool) -> Self {
986 self.can_split = can_split;
987 self
988 }
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994
995 #[test]
996 fn test_advanced_table_creation() {
997 let columns = vec![
998 ColumnDefinition {
999 width: ColumnWidth::Fixed(100.0),
1000 default_align: TextAlign::Left,
1001 min_width: None,
1002 max_width: None,
1003 },
1004 ColumnDefinition {
1005 width: ColumnWidth::Relative(0.5),
1006 default_align: TextAlign::Center,
1007 min_width: None,
1008 max_width: None,
1009 },
1010 ColumnDefinition {
1011 width: ColumnWidth::Auto,
1012 default_align: TextAlign::Right,
1013 min_width: Some(50.0),
1014 max_width: Some(200.0),
1015 },
1016 ];
1017
1018 let table = AdvancedTable::new(columns);
1019 assert_eq!(table.columns.len(), 3);
1020 assert_eq!(table.rows.len(), 0);
1021 }
1022
1023 #[test]
1024 fn test_column_width_calculation() {
1025 let columns = vec![
1026 ColumnDefinition {
1027 width: ColumnWidth::Fixed(100.0),
1028 default_align: TextAlign::Left,
1029 min_width: None,
1030 max_width: None,
1031 },
1032 ColumnDefinition {
1033 width: ColumnWidth::Relative(0.6),
1034 default_align: TextAlign::Center,
1035 min_width: None,
1036 max_width: None,
1037 },
1038 ColumnDefinition {
1039 width: ColumnWidth::Relative(0.4),
1040 default_align: TextAlign::Right,
1041 min_width: None,
1042 max_width: None,
1043 },
1044 ];
1045
1046 let table = AdvancedTable::new(columns);
1047 let widths = table.calculate_column_widths(500.0);
1048
1049 assert_eq!(widths[0], 100.0); assert_eq!(widths[1], 240.0); assert_eq!(widths[2], 160.0); }
1053
1054 #[test]
1055 fn test_cell_padding() {
1056 let uniform = CellPadding::uniform(10.0);
1057 assert_eq!(uniform.top, 10.0);
1058 assert_eq!(uniform.right, 10.0);
1059 assert_eq!(uniform.bottom, 10.0);
1060 assert_eq!(uniform.left, 10.0);
1061
1062 let symmetric = CellPadding::symmetric(5.0, 15.0);
1063 assert_eq!(symmetric.top, 15.0);
1064 assert_eq!(symmetric.right, 5.0);
1065 assert_eq!(symmetric.bottom, 15.0);
1066 assert_eq!(symmetric.left, 5.0);
1067 }
1068
1069 #[test]
1070 fn test_border_styles() {
1071 let none = BorderStyle::none();
1072 assert!(none.top.is_none());
1073 assert!(none.right.is_none());
1074 assert!(none.bottom.is_none());
1075 assert!(none.left.is_none());
1076
1077 let horizontal = BorderStyle::horizontal_only(2.0, Color::red());
1078 assert!(horizontal.top.is_some());
1079 assert!(horizontal.right.is_none());
1080 assert!(horizontal.bottom.is_some());
1081 assert!(horizontal.left.is_none());
1082
1083 let vertical = BorderStyle::vertical_only(1.5, Color::blue());
1084 assert!(vertical.top.is_none());
1085 assert!(vertical.right.is_some());
1086 assert!(vertical.bottom.is_none());
1087 assert!(vertical.left.is_some());
1088 }
1089
1090 #[test]
1091 fn test_advanced_cell_creation() {
1092 let cell = AdvancedTableCell::text("Hello".to_string())
1093 .with_align(TextAlign::Center)
1094 .with_vertical_align(VerticalAlign::Top)
1095 .with_colspan(2)
1096 .with_background(Color::gray(0.9))
1097 .with_font(Font::HelveticaBold, 12.0)
1098 .with_text_color(Color::blue());
1099
1100 match &cell.content {
1101 CellContent::Text(text) => assert_eq!(text, "Hello"),
1102 _ => panic!("Expected text content"),
1103 }
1104 assert_eq!(cell.align, TextAlign::Center);
1105 assert_eq!(cell.vertical_align, VerticalAlign::Top);
1106 assert_eq!(cell.colspan, 2);
1107 assert!(cell.background_color.is_some());
1108 assert!(cell.font.is_some());
1109 assert!(cell.font_size.is_some());
1110 assert!(cell.text_color.is_some());
1111 }
1112
1113 #[test]
1114 fn test_table_row_creation() {
1115 let cells = vec![
1116 AdvancedTableCell::text("Cell 1".to_string()),
1117 AdvancedTableCell::text("Cell 2".to_string()),
1118 ];
1119
1120 let row = TableRow::new(cells)
1121 .with_height(30.0)
1122 .with_background(Color::yellow())
1123 .with_can_split(false);
1124
1125 assert_eq!(row.cells.len(), 2);
1126 assert_eq!(row.height, Some(30.0));
1127 assert!(row.background_color.is_some());
1128 assert!(!row.can_split);
1129 }
1130
1131 #[test]
1132 fn test_add_text_row() {
1133 let mut table = AdvancedTable::with_equal_columns(3, 300.0);
1134 let result = table.add_text_row(vec![
1135 "Name".to_string(),
1136 "Age".to_string(),
1137 "City".to_string(),
1138 ]);
1139
1140 assert!(result.is_ok());
1141 assert_eq!(table.rows.len(), 1);
1142 }
1143
1144 #[test]
1145 fn test_add_text_row_mismatch() {
1146 let mut table = AdvancedTable::with_equal_columns(2, 200.0);
1147 let result = table.add_text_row(vec![
1148 "One".to_string(),
1149 "Two".to_string(),
1150 "Three".to_string(),
1151 ]);
1152
1153 assert!(result.is_err());
1154 }
1155
1156 #[test]
1157 fn test_validate_row_cells() {
1158 let table = AdvancedTable::with_equal_columns(3, 300.0);
1159
1160 let cells1 = vec![
1162 AdvancedTableCell::text("A".to_string()),
1163 AdvancedTableCell::text("B".to_string()),
1164 AdvancedTableCell::text("C".to_string()),
1165 ];
1166 assert!(table.validate_row_cells(&cells1).is_ok());
1167
1168 let cells2 = vec![
1170 AdvancedTableCell::text("AB".to_string()).with_colspan(2),
1171 AdvancedTableCell::text("C".to_string()),
1172 ];
1173 assert!(table.validate_row_cells(&cells2).is_ok());
1174
1175 let cells3 = vec![
1177 AdvancedTableCell::text("A".to_string()).with_colspan(2),
1178 AdvancedTableCell::text("B".to_string()).with_colspan(2),
1179 ];
1180 assert!(table.validate_row_cells(&cells3).is_err());
1181 }
1182
1183 #[test]
1184 fn test_alternating_row_colors() {
1185 let alt_colors = AlternatingRowColors {
1186 even_color: Color::gray(0.95),
1187 odd_color: Color::white(),
1188 include_header: false,
1189 };
1190
1191 let mut options = AdvancedTableOptions::default();
1192 options.alternating_rows = Some(alt_colors);
1193
1194 let mut table = AdvancedTable::with_equal_columns(2, 200.0);
1195 table.set_options(options);
1196
1197 table
1199 .add_text_row(vec!["Row 0".to_string(), "Data".to_string()])
1200 .unwrap();
1201 table
1202 .add_text_row(vec!["Row 1".to_string(), "Data".to_string()])
1203 .unwrap();
1204
1205 let row = &table.rows[0];
1207 let color = table.get_row_background_color(row, 0);
1208 assert!(color.is_some());
1209 assert_eq!(color.unwrap(), Color::gray(0.95)); let row = &table.rows[1];
1212 let color = table.get_row_background_color(row, 1);
1213 assert!(color.is_some());
1214 assert_eq!(color.unwrap(), Color::white()); }
1216
1217 #[test]
1218 fn test_cell_content_types() {
1219 let text_cell = AdvancedTableCell::text("Simple text".to_string());
1221 match text_cell.content {
1222 CellContent::Text(ref t) => assert_eq!(t, "Simple text"),
1223 _ => panic!("Expected text content"),
1224 }
1225
1226 let para_cell =
1228 AdvancedTableCell::paragraphs(vec!["Line 1".to_string(), "Line 2".to_string()]);
1229 match para_cell.content {
1230 CellContent::Paragraphs(ref p) => assert_eq!(p.len(), 2),
1231 _ => panic!("Expected paragraphs content"),
1232 }
1233 }
1234
1235 #[test]
1236 fn test_line_styles() {
1237 assert_eq!(LineStyle::Solid, LineStyle::Solid);
1238 assert_ne!(LineStyle::Solid, LineStyle::Dashed);
1239 assert_ne!(LineStyle::Dashed, LineStyle::Dotted);
1240 }
1241
1242 #[test]
1243 fn test_vertical_alignment() {
1244 assert_eq!(VerticalAlign::Top, VerticalAlign::Top);
1245 assert_ne!(VerticalAlign::Top, VerticalAlign::Middle);
1246 assert_ne!(VerticalAlign::Middle, VerticalAlign::Bottom);
1247 }
1248
1249 #[test]
1250 fn test_table_dimensions() {
1251 let mut table = AdvancedTable::with_equal_columns(3, 300.0);
1252
1253 table.options.row_height = 25.0;
1255
1256 table
1258 .add_text_row(vec!["A".to_string(), "B".to_string(), "C".to_string()])
1259 .unwrap();
1260 table
1261 .add_text_row(vec!["D".to_string(), "E".to_string(), "F".to_string()])
1262 .unwrap();
1263
1264 let widths = table.calculate_column_widths(300.0);
1265 let total_width: f64 = widths.iter().sum();
1266 assert_eq!(total_width, 300.0);
1267
1268 let total_height = table.calculate_total_height(&widths);
1269 assert_eq!(total_height, 50.0); }
1271
1272 #[test]
1273 fn test_auto_column_constraints() {
1274 let columns = vec![
1275 ColumnDefinition {
1276 width: ColumnWidth::Fixed(100.0),
1277 default_align: TextAlign::Left,
1278 min_width: None,
1279 max_width: None,
1280 },
1281 ColumnDefinition {
1282 width: ColumnWidth::Auto,
1283 default_align: TextAlign::Center,
1284 min_width: Some(80.0),
1285 max_width: Some(120.0),
1286 },
1287 ColumnDefinition {
1288 width: ColumnWidth::Auto,
1289 default_align: TextAlign::Right,
1290 min_width: Some(50.0),
1291 max_width: None,
1292 },
1293 ];
1294
1295 let table = AdvancedTable::new(columns);
1296 let widths = table.calculate_column_widths(400.0);
1297
1298 assert_eq!(widths[0], 100.0); assert!(widths[1] >= 80.0 && widths[1] <= 120.0); assert!(widths[2] >= 50.0); }
1302
1303 #[test]
1304 fn test_header_footer_rows() {
1305 let mut table = AdvancedTable::with_equal_columns(2, 200.0);
1306
1307 let header_cells = vec![
1309 AdvancedTableCell::text("Header 1".to_string()),
1310 AdvancedTableCell::text("Header 2".to_string()),
1311 ];
1312 assert!(table.add_header_row(header_cells).is_ok());
1313 assert_eq!(table.header_rows.len(), 1);
1314
1315 let footer_cells = vec![
1317 AdvancedTableCell::text("Footer 1".to_string()),
1318 AdvancedTableCell::text("Footer 2".to_string()),
1319 ];
1320 assert!(table.add_footer_row(footer_cells).is_ok());
1321 assert_eq!(table.footer_rows.len(), 1);
1322
1323 assert!(table
1325 .add_text_row(vec!["Data 1".to_string(), "Data 2".to_string()])
1326 .is_ok());
1327 assert_eq!(table.rows.len(), 1);
1328 }
1329}