1use super::content_stream::ContentStreamBuilder;
21use crate::error::Result;
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub enum CellAlign {
26 #[default]
28 Left,
29 Center,
31 Right,
33}
34
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
37pub enum CellVAlign {
38 #[default]
40 Top,
41 Middle,
43 Bottom,
45}
46
47#[derive(Debug, Clone, Copy, Default)]
49pub enum ColumnWidth {
50 #[default]
52 Auto,
53 Fixed(f32),
55 Percent(f32),
57 Weight(f32),
59}
60
61#[derive(Debug, Clone, Copy)]
63pub struct TableBorderStyle {
64 pub width: f32,
66 pub color: (f32, f32, f32),
68}
69
70impl Default for TableBorderStyle {
71 fn default() -> Self {
72 Self {
73 width: 0.5,
74 color: (0.0, 0.0, 0.0), }
76 }
77}
78
79impl TableBorderStyle {
80 pub fn new(width: f32) -> Self {
82 Self {
83 width,
84 ..Default::default()
85 }
86 }
87
88 pub fn with_color(mut self, r: f32, g: f32, b: f32) -> Self {
90 self.color = (r, g, b);
91 self
92 }
93
94 pub fn thin() -> Self {
96 Self::new(0.25)
97 }
98
99 pub fn medium() -> Self {
101 Self::new(0.5)
102 }
103
104 pub fn thick() -> Self {
106 Self::new(1.0)
107 }
108
109 pub fn none() -> Self {
111 Self::new(0.0)
112 }
113}
114
115#[derive(Debug, Clone, Copy, Default)]
117pub struct Borders {
118 pub top: Option<TableBorderStyle>,
120 pub right: Option<TableBorderStyle>,
122 pub bottom: Option<TableBorderStyle>,
124 pub left: Option<TableBorderStyle>,
126}
127
128impl Borders {
129 pub fn none() -> Self {
131 Self::default()
132 }
133
134 pub fn all(style: TableBorderStyle) -> Self {
136 Self {
137 top: Some(style),
138 right: Some(style),
139 bottom: Some(style),
140 left: Some(style),
141 }
142 }
143
144 pub fn horizontal(style: TableBorderStyle) -> Self {
146 Self {
147 top: Some(style),
148 bottom: Some(style),
149 ..Default::default()
150 }
151 }
152
153 pub fn vertical(style: TableBorderStyle) -> Self {
155 Self {
156 left: Some(style),
157 right: Some(style),
158 ..Default::default()
159 }
160 }
161
162 pub fn with_top(mut self, style: TableBorderStyle) -> Self {
164 self.top = Some(style);
165 self
166 }
167
168 pub fn with_bottom(mut self, style: TableBorderStyle) -> Self {
170 self.bottom = Some(style);
171 self
172 }
173
174 pub fn with_left(mut self, style: TableBorderStyle) -> Self {
176 self.left = Some(style);
177 self
178 }
179
180 pub fn with_right(mut self, style: TableBorderStyle) -> Self {
182 self.right = Some(style);
183 self
184 }
185}
186
187#[derive(Debug, Clone, Copy)]
189pub struct CellPadding {
190 pub top: f32,
192 pub right: f32,
194 pub bottom: f32,
196 pub left: f32,
198}
199
200impl Default for CellPadding {
201 fn default() -> Self {
202 Self {
203 top: 4.0,
204 right: 4.0,
205 bottom: 4.0,
206 left: 4.0,
207 }
208 }
209}
210
211impl CellPadding {
212 pub fn uniform(padding: f32) -> Self {
214 Self {
215 top: padding,
216 right: padding,
217 bottom: padding,
218 left: padding,
219 }
220 }
221
222 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
224 Self {
225 top: vertical,
226 right: horizontal,
227 bottom: vertical,
228 left: horizontal,
229 }
230 }
231
232 pub fn none() -> Self {
234 Self::uniform(0.0)
235 }
236
237 pub fn horizontal(&self) -> f32 {
239 self.left + self.right
240 }
241
242 pub fn vertical(&self) -> f32 {
244 self.top + self.bottom
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct TableCell {
251 pub content: String,
253 pub colspan: usize,
255 pub rowspan: usize,
257 pub align: CellAlign,
259 pub valign: CellVAlign,
261 pub padding: Option<CellPadding>,
263 pub borders: Option<Borders>,
265 pub background: Option<(f32, f32, f32)>,
267 pub font_name: Option<String>,
269 pub font_size: Option<f32>,
271 pub bold: bool,
273 pub italic: bool,
275}
276
277impl TableCell {
278 pub fn text(content: impl Into<String>) -> Self {
280 Self {
281 content: content.into(),
282 colspan: 1,
283 rowspan: 1,
284 align: CellAlign::default(),
285 valign: CellVAlign::default(),
286 padding: None,
287 borders: None,
288 background: None,
289 font_name: None,
290 font_size: None,
291 bold: false,
292 italic: false,
293 }
294 }
295
296 pub fn empty() -> Self {
298 Self::text("")
299 }
300
301 pub fn colspan(mut self, span: usize) -> Self {
303 self.colspan = span.max(1);
304 self
305 }
306
307 pub fn rowspan(mut self, span: usize) -> Self {
309 self.rowspan = span.max(1);
310 self
311 }
312
313 pub fn align(mut self, align: CellAlign) -> Self {
315 self.align = align;
316 self
317 }
318
319 pub fn valign(mut self, valign: CellVAlign) -> Self {
321 self.valign = valign;
322 self
323 }
324
325 pub fn padding(mut self, padding: CellPadding) -> Self {
327 self.padding = Some(padding);
328 self
329 }
330
331 pub fn borders(mut self, borders: Borders) -> Self {
333 self.borders = Some(borders);
334 self
335 }
336
337 pub fn background(mut self, r: f32, g: f32, b: f32) -> Self {
339 self.background = Some((r, g, b));
340 self
341 }
342
343 pub fn font(mut self, name: impl Into<String>, size: f32) -> Self {
345 self.font_name = Some(name.into());
346 self.font_size = Some(size);
347 self
348 }
349
350 pub fn bold(mut self) -> Self {
352 self.bold = true;
353 self
354 }
355
356 pub fn italic(mut self) -> Self {
358 self.italic = true;
359 self
360 }
361
362 pub fn header(content: impl Into<String>) -> Self {
364 Self::text(content).align(CellAlign::Center).bold()
365 }
366
367 pub fn number(content: impl Into<String>) -> Self {
369 Self::text(content).align(CellAlign::Right)
370 }
371}
372
373#[derive(Debug, Clone)]
375pub struct TableRow {
376 pub cells: Vec<TableCell>,
378 pub min_height: Option<f32>,
380 pub background: Option<(f32, f32, f32)>,
382 pub is_header: bool,
384}
385
386impl TableRow {
387 pub fn new(cells: Vec<TableCell>) -> Self {
389 Self {
390 cells,
391 min_height: None,
392 background: None,
393 is_header: false,
394 }
395 }
396
397 pub fn header(cells: Vec<TableCell>) -> Self {
399 Self {
400 cells,
401 min_height: None,
402 background: None,
403 is_header: true,
404 }
405 }
406
407 pub fn min_height(mut self, height: f32) -> Self {
409 self.min_height = Some(height);
410 self
411 }
412
413 pub fn background(mut self, r: f32, g: f32, b: f32) -> Self {
415 self.background = Some((r, g, b));
416 self
417 }
418
419 pub fn as_header(mut self) -> Self {
421 self.is_header = true;
422 self
423 }
424}
425
426#[derive(Debug, Clone)]
428pub struct TableStyle {
429 pub cell_padding: CellPadding,
431 pub cell_borders: Borders,
433 pub outer_border: Option<TableBorderStyle>,
435 pub font_name: String,
437 pub font_size: f32,
439 pub header_background: Option<(f32, f32, f32)>,
441 pub stripe_color: Option<(f32, f32, f32)>,
443 pub row_spacing: f32,
445}
446
447impl Default for TableStyle {
448 fn default() -> Self {
449 Self {
450 cell_padding: CellPadding::default(),
451 cell_borders: Borders::all(TableBorderStyle::thin()),
452 outer_border: None,
453 font_name: "Helvetica".to_string(),
454 font_size: 10.0,
455 header_background: Some((0.9, 0.9, 0.9)), stripe_color: None,
457 row_spacing: 0.0,
458 }
459 }
460}
461
462impl TableStyle {
463 pub fn new() -> Self {
465 Self::default()
466 }
467
468 pub fn cell_padding(mut self, padding: CellPadding) -> Self {
470 self.cell_padding = padding;
471 self
472 }
473
474 pub fn cell_borders(mut self, borders: Borders) -> Self {
476 self.cell_borders = borders;
477 self
478 }
479
480 pub fn outer_border(mut self, border: TableBorderStyle) -> Self {
482 self.outer_border = Some(border);
483 self
484 }
485
486 pub fn font(mut self, name: impl Into<String>, size: f32) -> Self {
488 self.font_name = name.into();
489 self.font_size = size;
490 self
491 }
492
493 pub fn header_background(mut self, r: f32, g: f32, b: f32) -> Self {
495 self.header_background = Some((r, g, b));
496 self
497 }
498
499 pub fn striped(mut self, r: f32, g: f32, b: f32) -> Self {
501 self.stripe_color = Some((r, g, b));
502 self
503 }
504
505 pub fn minimal() -> Self {
507 Self {
508 cell_borders: Borders::none(),
509 outer_border: None,
510 header_background: None,
511 ..Default::default()
512 }
513 }
514
515 pub fn bordered() -> Self {
517 Self {
518 cell_borders: Borders::all(TableBorderStyle::medium()),
519 outer_border: Some(TableBorderStyle::thick()),
520 ..Default::default()
521 }
522 }
523}
524
525#[derive(Debug, Clone)]
527pub struct Table {
528 pub rows: Vec<TableRow>,
530 pub column_widths: Vec<ColumnWidth>,
532 pub style: TableStyle,
534 pub width: Option<f32>,
536 pub column_aligns: Vec<CellAlign>,
538}
539
540impl Table {
541 pub fn new(rows: Vec<Vec<TableCell>>) -> Self {
543 let rows: Vec<TableRow> = rows.into_iter().map(TableRow::new).collect();
544 Self::from_rows(rows)
545 }
546
547 pub fn from_rows(rows: Vec<TableRow>) -> Self {
549 let num_cols = rows
550 .iter()
551 .map(|r| r.cells.iter().map(|c| c.colspan).sum::<usize>())
552 .max()
553 .unwrap_or(0);
554
555 Self {
556 rows,
557 column_widths: vec![ColumnWidth::Auto; num_cols],
558 style: TableStyle::default(),
559 width: None,
560 column_aligns: vec![CellAlign::Left; num_cols],
561 }
562 }
563
564 pub fn empty() -> Self {
566 Self {
567 rows: Vec::new(),
568 column_widths: Vec::new(),
569 style: TableStyle::default(),
570 width: None,
571 column_aligns: Vec::new(),
572 }
573 }
574
575 pub fn add_row(&mut self, row: TableRow) {
577 self.rows.push(row);
578 }
579
580 pub fn with_header_row(mut self) -> Self {
582 if let Some(row) = self.rows.first_mut() {
583 row.is_header = true;
584 }
585 self
586 }
587
588 pub fn with_style(mut self, style: TableStyle) -> Self {
590 self.style = style;
591 self
592 }
593
594 pub fn with_width(mut self, width: f32) -> Self {
596 self.width = Some(width);
597 self
598 }
599
600 pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
602 self.column_widths = widths;
603 self
604 }
605
606 pub fn with_column_aligns(mut self, aligns: Vec<CellAlign>) -> Self {
608 self.column_aligns = aligns;
609 self
610 }
611
612 pub fn num_columns(&self) -> usize {
614 self.rows
615 .iter()
616 .map(|r| r.cells.iter().map(|c| c.colspan).sum::<usize>())
617 .max()
618 .unwrap_or(0)
619 }
620
621 pub fn num_rows(&self) -> usize {
623 self.rows.len()
624 }
625
626 pub fn is_empty(&self) -> bool {
628 self.rows.is_empty()
629 }
630}
631
632#[derive(Debug, Clone)]
634pub struct TableLayout {
635 pub column_widths: Vec<f32>,
637 pub row_heights: Vec<f32>,
639 pub total_width: f32,
641 pub total_height: f32,
643 pub cell_positions: Vec<Vec<CellPosition>>,
645}
646
647#[derive(Debug, Clone, Copy)]
649pub struct CellPosition {
650 pub x: f32,
652 pub y: f32,
654 pub width: f32,
656 pub height: f32,
658}
659
660impl Table {
661 pub fn calculate_layout(
663 &self,
664 available_width: f32,
665 font_metrics: &dyn FontMetrics,
666 ) -> TableLayout {
667 let num_cols = self.num_columns();
668 if num_cols == 0 || self.rows.is_empty() {
669 return TableLayout {
670 column_widths: vec![],
671 row_heights: vec![],
672 total_width: 0.0,
673 total_height: 0.0,
674 cell_positions: vec![],
675 };
676 }
677
678 let table_width = self.width.unwrap_or(available_width);
679
680 let column_widths = self.calculate_column_widths(table_width, num_cols, font_metrics);
682
683 let row_heights = self.calculate_row_heights(&column_widths, font_metrics);
685
686 let cell_positions = self.calculate_cell_positions(&column_widths, &row_heights);
688
689 let total_width: f32 = column_widths.iter().sum();
690 let total_height: f32 = row_heights.iter().sum();
691
692 TableLayout {
693 column_widths,
694 row_heights,
695 total_width,
696 total_height,
697 cell_positions,
698 }
699 }
700
701 fn calculate_column_widths(
702 &self,
703 table_width: f32,
704 num_cols: usize,
705 font_metrics: &dyn FontMetrics,
706 ) -> Vec<f32> {
707 let padding = &self.style.cell_padding;
708 let mut widths = vec![0.0f32; num_cols];
709 let mut _fixed_width = 0.0f32;
710 let mut weight_total = 0.0f32;
711 let mut _percent_total = 0.0f32;
712
713 for (col, spec) in self.column_widths.iter().take(num_cols).enumerate() {
715 match spec {
716 ColumnWidth::Fixed(w) => {
717 widths[col] = *w;
718 _fixed_width += *w;
719 },
720 ColumnWidth::Percent(p) => {
721 let w = table_width * (*p / 100.0);
722 widths[col] = w;
723 _percent_total += *p;
724 },
725 ColumnWidth::Weight(w) => {
726 weight_total += *w;
727 },
728 ColumnWidth::Auto => {
729 let mut max_width = 0.0f32;
731 for row in &self.rows {
732 let mut col_idx = 0;
733 for cell in &row.cells {
734 if col_idx == col && cell.colspan == 1 {
735 let font_size = cell.font_size.unwrap_or(self.style.font_size);
736 let text_width = font_metrics.text_width(&cell.content, font_size);
737 let cell_padding = cell.padding.as_ref().unwrap_or(padding);
738 max_width = max_width.max(text_width + cell_padding.horizontal());
739 }
740 col_idx += cell.colspan;
741 }
742 }
743 widths[col] = max_width.max(20.0); },
745 }
746 }
747
748 for col in self.column_widths.len()..num_cols {
750 let mut max_width = 0.0f32;
751 for row in &self.rows {
752 let mut col_idx = 0;
753 for cell in &row.cells {
754 if col_idx == col && cell.colspan == 1 {
755 let font_size = cell.font_size.unwrap_or(self.style.font_size);
756 let text_width = font_metrics.text_width(&cell.content, font_size);
757 let cell_padding = cell.padding.as_ref().unwrap_or(padding);
758 max_width = max_width.max(text_width + cell_padding.horizontal());
759 }
760 col_idx += cell.colspan;
761 }
762 }
763 widths[col] = max_width.max(20.0);
764 }
765
766 let used_width: f32 = widths.iter().sum();
768 let remaining = (table_width - used_width).max(0.0);
769
770 if weight_total > 0.0 && remaining > 0.0 {
771 for (col, spec) in self.column_widths.iter().take(num_cols).enumerate() {
772 if let ColumnWidth::Weight(w) = spec {
773 widths[col] = remaining * (*w / weight_total);
774 }
775 }
776 }
777
778 let total: f32 = widths.iter().sum();
780 if total > table_width && total > 0.0 {
781 let scale = table_width / total;
782 for w in &mut widths {
783 *w *= scale;
784 }
785 }
786
787 widths
788 }
789
790 fn calculate_row_heights(
791 &self,
792 column_widths: &[f32],
793 font_metrics: &dyn FontMetrics,
794 ) -> Vec<f32> {
795 let padding = &self.style.cell_padding;
796 let mut heights = Vec::with_capacity(self.rows.len());
797
798 for row in &self.rows {
799 let mut max_height = 0.0f32;
800 let mut col_idx = 0;
801
802 for cell in &row.cells {
803 if cell.rowspan == 1 {
804 let cell_width = if cell.colspan == 1 {
805 column_widths.get(col_idx).copied().unwrap_or(100.0)
806 } else {
807 column_widths[col_idx..col_idx + cell.colspan].iter().sum()
808 };
809
810 let cell_padding = cell.padding.as_ref().unwrap_or(padding);
811 let content_width = cell_width - cell_padding.horizontal();
812
813 let font_size = cell.font_size.unwrap_or(self.style.font_size);
814 let line_height = font_size * 1.2;
815
816 let lines = wrap_text(&cell.content, content_width, font_size, font_metrics);
818 let text_height = lines.len() as f32 * line_height;
819
820 let cell_height = text_height + cell_padding.vertical();
821 max_height = max_height.max(cell_height);
822 }
823 col_idx += cell.colspan;
824 }
825
826 if let Some(min_h) = row.min_height {
828 max_height = max_height.max(min_h);
829 }
830
831 heights.push(max_height.max(self.style.font_size * 1.5));
832 }
833
834 heights
835 }
836
837 fn calculate_cell_positions(
838 &self,
839 column_widths: &[f32],
840 row_heights: &[f32],
841 ) -> Vec<Vec<CellPosition>> {
842 let mut positions = Vec::with_capacity(self.rows.len());
843 let mut y = 0.0;
844
845 for (row_idx, row) in self.rows.iter().enumerate() {
846 let mut row_positions = Vec::with_capacity(row.cells.len());
847 let mut x = 0.0;
848 let mut col_idx = 0;
849
850 for cell in &row.cells {
851 let width: f32 = column_widths[col_idx..col_idx + cell.colspan].iter().sum();
852 let height: f32 = row_heights[row_idx..row_idx + cell.rowspan].iter().sum();
853
854 row_positions.push(CellPosition {
855 x,
856 y,
857 width,
858 height,
859 });
860
861 x += width;
862 col_idx += cell.colspan;
863 }
864
865 positions.push(row_positions);
866 y += row_heights[row_idx];
867 }
868
869 positions
870 }
871
872 pub fn render(
874 &self,
875 builder: &mut ContentStreamBuilder,
876 x: f32,
877 y: f32,
878 layout: &TableLayout,
879 ) -> Result<()> {
880 let table_top = y;
882
883 for (row_idx, row) in self.rows.iter().enumerate() {
885 for (cell_idx, cell) in row.cells.iter().enumerate() {
886 let pos = &layout.cell_positions[row_idx][cell_idx];
887 let cell_x = x + pos.x;
888 let cell_y = table_top - pos.y - pos.height;
889
890 let bg = cell.background.or({
892 if row.is_header {
893 self.style.header_background
894 } else if let Some(stripe) = self.style.stripe_color {
895 if row_idx % 2 == 1 {
896 Some(stripe)
897 } else {
898 row.background
899 }
900 } else {
901 row.background
902 }
903 });
904
905 if let Some((r, g, b)) = bg {
907 builder.set_fill_color(r, g, b);
908 builder.rect(cell_x, cell_y, pos.width, pos.height);
909 builder.fill();
910 }
911
912 let borders = cell.borders.as_ref().unwrap_or(&self.style.cell_borders);
914 self.draw_cell_borders(builder, cell_x, cell_y, pos.width, pos.height, borders);
915 }
916 }
917
918 for (row_idx, row) in self.rows.iter().enumerate() {
920 for (cell_idx, cell) in row.cells.iter().enumerate() {
921 if cell.content.is_empty() {
922 continue;
923 }
924
925 let pos = &layout.cell_positions[row_idx][cell_idx];
926 let padding = cell.padding.as_ref().unwrap_or(&self.style.cell_padding);
927
928 let cell_x = x + pos.x + padding.left;
929 let cell_y = table_top - pos.y - padding.top;
930 let content_width = pos.width - padding.horizontal();
931
932 let align = if cell.align != CellAlign::Left {
934 cell.align
935 } else {
936 self.column_aligns
937 .get(cell_idx)
938 .copied()
939 .unwrap_or(CellAlign::Left)
940 };
941
942 let font_name = cell.font_name.as_deref().unwrap_or(&self.style.font_name);
943 let font_size = cell.font_size.unwrap_or(self.style.font_size);
944
945 let actual_font = if cell.bold && cell.italic {
947 format!("{}-BoldOblique", font_name)
948 } else if cell.bold || row.is_header {
949 format!("{}-Bold", font_name)
950 } else if cell.italic {
951 format!("{}-Oblique", font_name)
952 } else {
953 font_name.to_string()
954 };
955
956 builder.begin_text().set_font(&actual_font, font_size);
957
958 let text_x = match align {
960 CellAlign::Left => cell_x,
961 CellAlign::Center => cell_x + content_width / 2.0,
962 CellAlign::Right => cell_x + content_width,
963 };
964
965 let _line_height = font_size * 1.2;
966 let text_y = cell_y - font_size;
967
968 builder.text(&cell.content, text_x, text_y);
970 builder.end_text();
971 }
972 }
973
974 if let Some(outer) = &self.style.outer_border {
976 if outer.width > 0.0 {
977 builder.set_stroke_color(outer.color.0, outer.color.1, outer.color.2);
978 builder.set_line_width(outer.width);
979 builder.rect(
980 x,
981 table_top - layout.total_height,
982 layout.total_width,
983 layout.total_height,
984 );
985 builder.stroke();
986 }
987 }
988
989 Ok(())
990 }
991
992 fn draw_cell_borders(
993 &self,
994 builder: &mut ContentStreamBuilder,
995 x: f32,
996 y: f32,
997 width: f32,
998 height: f32,
999 borders: &Borders,
1000 ) {
1001 if let Some(border) = &borders.top {
1003 if border.width > 0.0 {
1004 builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
1005 builder.set_line_width(border.width);
1006 builder.move_to(x, y + height);
1007 builder.line_to(x + width, y + height);
1008 builder.stroke();
1009 }
1010 }
1011
1012 if let Some(border) = &borders.bottom {
1014 if border.width > 0.0 {
1015 builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
1016 builder.set_line_width(border.width);
1017 builder.move_to(x, y);
1018 builder.line_to(x + width, y);
1019 builder.stroke();
1020 }
1021 }
1022
1023 if let Some(border) = &borders.left {
1025 if border.width > 0.0 {
1026 builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
1027 builder.set_line_width(border.width);
1028 builder.move_to(x, y);
1029 builder.line_to(x, y + height);
1030 builder.stroke();
1031 }
1032 }
1033
1034 if let Some(border) = &borders.right {
1036 if border.width > 0.0 {
1037 builder.set_stroke_color(border.color.0, border.color.1, border.color.2);
1038 builder.set_line_width(border.width);
1039 builder.move_to(x + width, y);
1040 builder.line_to(x + width, y + height);
1041 builder.stroke();
1042 }
1043 }
1044 }
1045}
1046
1047pub trait FontMetrics {
1049 fn text_width(&self, text: &str, font_size: f32) -> f32;
1051}
1052
1053#[derive(Debug, Clone, Copy)]
1055pub struct SimpleFontMetrics {
1056 pub char_width_ratio: f32,
1058}
1059
1060impl Default for SimpleFontMetrics {
1061 fn default() -> Self {
1062 Self {
1063 char_width_ratio: 0.5, }
1065 }
1066}
1067
1068impl SimpleFontMetrics {
1069 pub fn monospace() -> Self {
1071 Self {
1072 char_width_ratio: 0.6,
1073 }
1074 }
1075}
1076
1077impl FontMetrics for SimpleFontMetrics {
1078 fn text_width(&self, text: &str, font_size: f32) -> f32 {
1079 text.chars().count() as f32 * font_size * self.char_width_ratio
1080 }
1081}
1082
1083fn wrap_text(text: &str, max_width: f32, font_size: f32, metrics: &dyn FontMetrics) -> Vec<String> {
1085 if text.is_empty() {
1086 return vec![String::new()];
1087 }
1088
1089 let mut lines = Vec::new();
1090 let mut current_line = String::new();
1091
1092 for word in text.split_whitespace() {
1093 let test_line = if current_line.is_empty() {
1094 word.to_string()
1095 } else {
1096 format!("{} {}", current_line, word)
1097 };
1098
1099 let width = metrics.text_width(&test_line, font_size);
1100
1101 if width <= max_width || current_line.is_empty() {
1102 current_line = test_line;
1103 } else {
1104 lines.push(current_line);
1105 current_line = word.to_string();
1106 }
1107 }
1108
1109 if !current_line.is_empty() {
1110 lines.push(current_line);
1111 }
1112
1113 if lines.is_empty() {
1114 lines.push(String::new());
1115 }
1116
1117 lines
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122 use super::*;
1123
1124 #[test]
1125 fn test_table_cell_creation() {
1126 let cell = TableCell::text("Hello");
1127 assert_eq!(cell.content, "Hello");
1128 assert_eq!(cell.colspan, 1);
1129 assert_eq!(cell.rowspan, 1);
1130 }
1131
1132 #[test]
1133 fn test_table_cell_header() {
1134 let cell = TableCell::header("Title");
1135 assert!(cell.bold);
1136 assert_eq!(cell.align, CellAlign::Center);
1137 }
1138
1139 #[test]
1140 fn test_table_cell_spanning() {
1141 let cell = TableCell::text("Wide").colspan(2).rowspan(3);
1142 assert_eq!(cell.colspan, 2);
1143 assert_eq!(cell.rowspan, 3);
1144 }
1145
1146 #[test]
1147 fn test_table_creation() {
1148 let table = Table::new(vec![
1149 vec![TableCell::text("A"), TableCell::text("B")],
1150 vec![TableCell::text("C"), TableCell::text("D")],
1151 ]);
1152
1153 assert_eq!(table.num_columns(), 2);
1154 assert_eq!(table.num_rows(), 2);
1155 }
1156
1157 #[test]
1158 fn test_table_with_header() {
1159 let table = Table::new(vec![
1160 vec![TableCell::text("Name"), TableCell::text("Age")],
1161 vec![TableCell::text("Alice"), TableCell::text("30")],
1162 ])
1163 .with_header_row();
1164
1165 assert!(table.rows[0].is_header);
1166 }
1167
1168 #[test]
1169 fn test_cell_padding() {
1170 let padding = CellPadding::uniform(10.0);
1171 assert_eq!(padding.horizontal(), 20.0);
1172 assert_eq!(padding.vertical(), 20.0);
1173
1174 let asym = CellPadding::symmetric(5.0, 10.0);
1175 assert_eq!(asym.horizontal(), 10.0);
1176 assert_eq!(asym.vertical(), 20.0);
1177 }
1178
1179 #[test]
1180 fn test_borders() {
1181 let borders = Borders::all(TableBorderStyle::medium());
1182 assert!(borders.top.is_some());
1183 assert!(borders.right.is_some());
1184 assert!(borders.bottom.is_some());
1185 assert!(borders.left.is_some());
1186
1187 let horiz = Borders::horizontal(TableBorderStyle::thin());
1188 assert!(horiz.top.is_some());
1189 assert!(horiz.bottom.is_some());
1190 assert!(horiz.left.is_none());
1191 assert!(horiz.right.is_none());
1192 }
1193
1194 #[test]
1195 fn test_column_width_types() {
1196 let _auto = ColumnWidth::Auto;
1197 let _fixed = ColumnWidth::Fixed(100.0);
1198 let _percent = ColumnWidth::Percent(25.0);
1199 let _weight = ColumnWidth::Weight(1.0);
1200 }
1201
1202 #[test]
1203 fn test_table_style_presets() {
1204 let minimal = TableStyle::minimal();
1205 assert!(minimal.cell_borders.top.is_none());
1206 assert!(minimal.outer_border.is_none());
1207
1208 let bordered = TableStyle::bordered();
1209 assert!(bordered.outer_border.is_some());
1210 }
1211
1212 #[test]
1213 fn test_table_layout_calculation() {
1214 let table = Table::new(vec![
1215 vec![TableCell::text("Name"), TableCell::text("Value")],
1216 vec![TableCell::text("Test"), TableCell::text("123")],
1217 ]);
1218
1219 let metrics = SimpleFontMetrics::default();
1220 let layout = table.calculate_layout(400.0, &metrics);
1221
1222 assert_eq!(layout.column_widths.len(), 2);
1223 assert_eq!(layout.row_heights.len(), 2);
1224 assert!(layout.total_width > 0.0);
1225 assert!(layout.total_height > 0.0);
1226 }
1227
1228 #[test]
1229 fn test_text_wrapping() {
1230 let metrics = SimpleFontMetrics::default();
1231 let lines = wrap_text("Hello World", 100.0, 12.0, &metrics);
1232 assert!(!lines.is_empty());
1233 }
1234
1235 #[test]
1236 fn test_empty_table() {
1237 let table = Table::empty();
1238 assert!(table.is_empty());
1239 assert_eq!(table.num_columns(), 0);
1240 assert_eq!(table.num_rows(), 0);
1241 }
1242
1243 #[test]
1244 fn test_cell_alignments() {
1245 let left = TableCell::text("Left").align(CellAlign::Left);
1246 let center = TableCell::text("Center").align(CellAlign::Center);
1247 let right = TableCell::number("123");
1248
1249 assert_eq!(left.align, CellAlign::Left);
1250 assert_eq!(center.align, CellAlign::Center);
1251 assert_eq!(right.align, CellAlign::Right);
1252 }
1253
1254 #[test]
1255 fn test_row_creation() {
1256 let row = TableRow::new(vec![TableCell::text("A"), TableCell::text("B")]);
1257 assert_eq!(row.cells.len(), 2);
1258 assert!(!row.is_header);
1259
1260 let header = TableRow::header(vec![TableCell::text("Name"), TableCell::text("Value")]);
1261 assert!(header.is_header);
1262 }
1263
1264 #[test]
1265 fn test_striped_table() {
1266 let style = TableStyle::new().striped(0.95, 0.95, 0.95);
1267 assert!(style.stripe_color.is_some());
1268 }
1269}