1use super::cell_style::{BorderConfiguration, BorderStyle, CellAlignment, CellStyle};
4use super::header_builder::HeaderBuilder;
5use super::table_builder::{AdvancedTable, CellData, RowData};
6use crate::error::PdfError;
7use crate::graphics::Color;
8use crate::page::Page;
9use crate::text::{measure_text, Font};
10
11pub struct TableRenderer {
13 pub default_row_height: f64,
15 pub default_header_height: f64,
17 pub auto_height: bool,
19}
20
21impl TableRenderer {
22 pub fn new() -> Self {
24 Self {
25 default_row_height: 25.0,
26 default_header_height: 30.0,
27 auto_height: true,
28 }
29 }
30
31 pub fn calculate_table_height(&self, table: &AdvancedTable) -> f64 {
36 let mut total_height = 0.0;
37
38 if table.show_header {
40 if let Some(header) = &table.header {
41 total_height += header.calculate_height();
43 } else if !table.columns.is_empty() {
44 total_height += self.default_header_height;
46 }
47 }
48
49 let mut rows_to_skip: usize = 0;
53 for (row_idx, row) in table.rows.iter().enumerate() {
54 if rows_to_skip > 0 {
55 rows_to_skip -= 1;
56 continue;
57 }
58
59 let base_height = row.min_height.unwrap_or(self.default_row_height);
60 let row_height = if self.auto_height {
61 let content_height = self.calculate_content_row_height(table, row, row_idx);
62 base_height.max(content_height)
63 } else {
64 base_height
65 };
66
67 let max_rowspan = row.cells.iter().map(|cell| cell.rowspan).max().unwrap_or(1);
68 if max_rowspan > 1 {
69 total_height += row_height * max_rowspan as f64;
70 rows_to_skip = max_rowspan - 1;
71 } else {
72 total_height += row_height;
73 }
74 }
75
76 if table.table_border {
78 total_height += 2.0; }
80
81 total_height
82 }
83
84 pub fn render_table(
86 &self,
87 page: &mut Page,
88 table: &AdvancedTable,
89 x: f64,
90 y: f64,
91 ) -> Result<f64, PdfError> {
92 table
94 .validate()
95 .map_err(|e| PdfError::InvalidOperation(e.to_string()))?;
96
97 let mut current_y = y;
98
99 if table.show_header {
101 if let Some(header) = &table.header {
102 current_y = self.render_header(page, table, header, x, current_y)?;
103 } else if !table.columns.is_empty() {
104 current_y = self.render_simple_header(page, table, x, current_y)?;
106 }
107 }
108
109 current_y = self.render_rows(page, table, x, current_y)?;
111
112 if table.table_border {
114 self.render_table_border(page, table, x, y, current_y)?;
115 }
116
117 Ok(current_y)
118 }
119
120 fn render_header(
122 &self,
123 page: &mut Page,
124 table: &AdvancedTable,
125 header: &HeaderBuilder,
126 x: f64,
127 start_y: f64,
128 ) -> Result<f64, PdfError> {
129 let mut current_y = start_y;
130 let column_positions = self.calculate_column_positions(table, x);
131
132 for level in header.levels.iter() {
133 let row_height = self.default_header_height;
134
135 for cell in level {
136 let cell_x = column_positions[cell.start_col];
137 let cell_width = self.calculate_span_width(table, cell.start_col, cell.colspan);
138 let cell_height = row_height * cell.rowspan as f64;
139
140 let style = cell.style.as_ref().unwrap_or(&table.header_style);
141
142 self.render_cell(
143 page,
144 &cell.text,
145 cell_x,
146 current_y - cell_height,
147 cell_width,
148 cell_height,
149 style,
150 )?;
151 }
152
153 current_y -= row_height;
154 }
155
156 Ok(current_y)
157 }
158
159 fn render_simple_header(
161 &self,
162 page: &mut Page,
163 table: &AdvancedTable,
164 x: f64,
165 start_y: f64,
166 ) -> Result<f64, PdfError> {
167 let column_positions = self.calculate_column_positions(table, x);
168 let header_height = self.default_header_height;
169
170 for (col_idx, column) in table.columns.iter().enumerate() {
171 let cell_x = column_positions[col_idx];
172 let cell_width = column.width;
173
174 self.render_cell(
175 page,
176 &column.header,
177 cell_x,
178 start_y - header_height,
179 cell_width,
180 header_height,
181 &table.header_style,
182 )?;
183 }
184
185 Ok(start_y - header_height)
186 }
187
188 fn calculate_content_row_height(
190 &self,
191 table: &AdvancedTable,
192 row: &RowData,
193 row_idx: usize,
194 ) -> f64 {
195 let mut max_height = self.default_row_height;
196
197 let mut actual_col = 0usize;
198 for cell in row.cells.iter() {
199 let style = self.resolve_cell_style(table, cell, row_idx, actual_col);
200 let font = style.font.clone().unwrap_or(Font::Helvetica);
201 let font_size = style.font_size.unwrap_or(12.0);
202 let col_width: f64 = (actual_col..actual_col + cell.colspan)
204 .filter_map(|c| table.columns.get(c).map(|col| col.width))
205 .sum();
206 let available_width = col_width - style.padding.left - style.padding.right;
207
208 if available_width > 0.0 && style.text_wrap {
209 let lines =
210 self.wrap_text_to_lines(&cell.content, available_width, &font, font_size);
211 let line_height = font_size * 1.2;
212 let needed =
213 (lines.len() as f64 * line_height) + style.padding.top + style.padding.bottom;
214 if needed > max_height {
215 max_height = needed;
216 }
217 }
218
219 actual_col += cell.colspan;
220 }
221
222 max_height
223 }
224
225 fn render_rows(
227 &self,
228 page: &mut Page,
229 table: &AdvancedTable,
230 x: f64,
231 start_y: f64,
232 ) -> Result<f64, PdfError> {
233 let mut current_y = start_y;
234 let column_positions = self.calculate_column_positions(table, x);
235 let num_cols = table.columns.len();
236 let mut rowspan_end: Vec<usize> = vec![0; num_cols];
239
240 for (row_idx, row) in table.rows.iter().enumerate() {
241 let base_height = row.min_height.unwrap_or(self.default_row_height);
243 let row_height = if self.auto_height {
244 let content_height = self.calculate_content_row_height(table, row, row_idx);
245 base_height.max(content_height)
246 } else {
247 base_height
248 };
249
250 let mut actual_col = 0usize;
252 for cell in row.cells.iter() {
253 while actual_col < num_cols && rowspan_end[actual_col] > row_idx {
255 actual_col += 1;
256 }
257
258 if actual_col >= column_positions.len() {
259 break;
260 }
261
262 let cell_x = column_positions[actual_col];
263 let cell_width = self.calculate_span_width(table, actual_col, cell.colspan);
264 let cell_height = row_height * cell.rowspan as f64;
265
266 let style = self.resolve_cell_style(table, cell, row_idx, actual_col);
267
268 self.render_cell(
269 page,
270 &cell.content,
271 cell_x,
272 current_y - cell_height,
273 cell_width,
274 cell_height,
275 &style,
276 )?;
277
278 if cell.rowspan > 1 {
280 for c in actual_col..(actual_col + cell.colspan).min(num_cols) {
281 rowspan_end[c] = row_idx + cell.rowspan;
282 }
283 }
284
285 actual_col += cell.colspan;
286 }
287
288 current_y -= row_height;
289 }
290
291 Ok(current_y)
292 }
293
294 #[allow(clippy::too_many_arguments)]
296 fn render_cell(
297 &self,
298 page: &mut Page,
299 content: &str,
300 x: f64,
301 y: f64,
302 width: f64,
303 height: f64,
304 style: &CellStyle,
305 ) -> Result<(), PdfError> {
306 if let Some(bg_color) = style.background_color {
308 page.graphics()
309 .save_state()
310 .set_fill_color(bg_color)
311 .rectangle(x, y, width, height)
312 .fill()
313 .restore_state();
314 }
315
316 self.render_cell_borders(page, x, y, width, height, &style.border)?;
318
319 if !content.is_empty() {
321 self.render_cell_text(page, content, x, y, width, height, style)?;
322 }
323
324 Ok(())
325 }
326
327 fn render_cell_borders(
329 &self,
330 page: &mut Page,
331 x: f64,
332 y: f64,
333 width: f64,
334 height: f64,
335 border_config: &BorderConfiguration,
336 ) -> Result<(), PdfError> {
337 let graphics = page.graphics();
338
339 if border_config.top.style != BorderStyle::None {
341 graphics
342 .save_state()
343 .set_stroke_color(border_config.top.color)
344 .set_line_width(border_config.top.width);
345
346 self.apply_line_style(graphics, border_config.top.style);
347
348 graphics
349 .move_to(x, y + height)
350 .line_to(x + width, y + height)
351 .stroke()
352 .restore_state();
353 }
354
355 if border_config.bottom.style != BorderStyle::None {
357 graphics
358 .save_state()
359 .set_stroke_color(border_config.bottom.color)
360 .set_line_width(border_config.bottom.width);
361
362 self.apply_line_style(graphics, border_config.bottom.style);
363
364 graphics
365 .move_to(x, y)
366 .line_to(x + width, y)
367 .stroke()
368 .restore_state();
369 }
370
371 if border_config.left.style != BorderStyle::None {
373 graphics
374 .save_state()
375 .set_stroke_color(border_config.left.color)
376 .set_line_width(border_config.left.width);
377
378 self.apply_line_style(graphics, border_config.left.style);
379
380 graphics
381 .move_to(x, y)
382 .line_to(x, y + height)
383 .stroke()
384 .restore_state();
385 }
386
387 if border_config.right.style != BorderStyle::None {
389 graphics
390 .save_state()
391 .set_stroke_color(border_config.right.color)
392 .set_line_width(border_config.right.width);
393
394 self.apply_line_style(graphics, border_config.right.style);
395
396 graphics
397 .move_to(x + width, y)
398 .line_to(x + width, y + height)
399 .stroke()
400 .restore_state();
401 }
402
403 Ok(())
404 }
405
406 fn apply_line_style(
408 &self,
409 _graphics: &mut crate::graphics::GraphicsContext,
410 _style: BorderStyle,
411 ) {
412 }
415
416 fn truncate_text_to_width(
418 &self,
419 text: &str,
420 max_width: f64,
421 font: &Font,
422 font_size: f64,
423 ) -> String {
424 let full_width = measure_text(text, font, font_size);
426 if full_width <= max_width {
427 return text.to_string();
428 }
429
430 let ellipsis = "...";
432 let ellipsis_width = measure_text(ellipsis, font, font_size);
433 if ellipsis_width > max_width {
434 return String::new();
435 }
436
437 if ellipsis_width == max_width {
439 return ellipsis.to_string();
440 }
441
442 let available_width = max_width - ellipsis_width;
445 let mut last_fit_end = 0usize;
446 let mut width_so_far = 0.0f64;
447
448 for (byte_pos, ch) in text.char_indices() {
449 let ch_len = ch.len_utf8();
450 let ch_str = &text[byte_pos..byte_pos + ch_len];
451 let ch_width = measure_text(ch_str, font, font_size);
452 if width_so_far + ch_width > available_width {
453 break;
454 }
455 width_so_far += ch_width;
456 last_fit_end = byte_pos + ch_len;
457 }
458
459 if last_fit_end == 0 {
460 ellipsis.to_string()
461 } else {
462 format!("{}{}", &text[..last_fit_end], ellipsis)
463 }
464 }
465
466 fn wrap_text_to_lines(
471 &self,
472 text: &str,
473 max_width: f64,
474 font: &Font,
475 font_size: f64,
476 ) -> Vec<String> {
477 let mut lines = Vec::new();
478
479 for paragraph in text.split('\n') {
481 if paragraph.is_empty() {
482 lines.push(String::new());
483 continue;
484 }
485
486 let paragraph_width = measure_text(paragraph, font, font_size);
488 if paragraph_width <= max_width {
489 lines.push(paragraph.to_string());
490 continue;
491 }
492
493 let words: Vec<&str> = paragraph.split_whitespace().collect();
495 if words.is_empty() {
496 continue;
497 }
498
499 let mut current_line = String::new();
500 let mut current_line_width = 0.0f64;
501 let space_width = measure_text(" ", font, font_size);
502
503 for word in words {
504 let word_width = measure_text(word, font, font_size);
505
506 if current_line.is_empty() {
507 if word_width <= max_width {
509 current_line = word.to_string();
510 current_line_width = word_width;
511 } else {
512 let chars: Vec<char> = word.chars().collect();
514 let mut char_line = String::new();
515 let mut char_line_width = 0.0f64;
516 for c in chars {
517 let char_width = measure_text(&c.to_string(), font, font_size);
518 if char_line_width + char_width <= max_width {
519 char_line.push(c);
520 char_line_width += char_width;
521 } else {
522 if !char_line.is_empty() {
523 lines.push(char_line);
524 }
525 char_line = c.to_string();
526 char_line_width = char_width;
527 }
528 }
529 current_line = char_line;
530 current_line_width = char_line_width;
531 }
532 } else {
533 let test_width = current_line_width + space_width + word_width;
535
536 if test_width <= max_width {
537 current_line.push(' ');
538 current_line.push_str(word);
539 current_line_width = test_width;
540 } else {
541 lines.push(current_line);
543 if word_width <= max_width {
544 current_line = word.to_string();
545 current_line_width = word_width;
546 } else {
547 let chars: Vec<char> = word.chars().collect();
549 let mut char_line = String::new();
550 let mut char_line_width = 0.0f64;
551 for c in chars {
552 let char_width = measure_text(&c.to_string(), font, font_size);
553 if char_line_width + char_width <= max_width {
554 char_line.push(c);
555 char_line_width += char_width;
556 } else {
557 if !char_line.is_empty() {
558 lines.push(char_line);
559 }
560 char_line = c.to_string();
561 char_line_width = char_width;
562 }
563 }
564 current_line = char_line;
565 current_line_width = char_line_width;
566 }
567 }
568 }
569 }
570
571 if !current_line.is_empty() {
573 lines.push(current_line);
574 }
575 }
576
577 if lines.is_empty() {
578 lines.push(String::new());
579 }
580
581 lines
582 }
583
584 #[allow(clippy::too_many_arguments)]
586 fn render_cell_text(
587 &self,
588 page: &mut Page,
589 content: &str,
590 x: f64,
591 y: f64,
592 width: f64,
593 height: f64,
594 style: &CellStyle,
595 ) -> Result<(), PdfError> {
596 let font = style.font.clone().unwrap_or(Font::Helvetica);
597 let font_size = style.font_size.unwrap_or(12.0);
598 let text_color = style.text_color.unwrap_or(Color::black());
599
600 let available_width = width - style.padding.left - style.padding.right;
602
603 if available_width <= 0.0 {
604 return Ok(());
605 }
606
607 if style.text_wrap {
609 let lines = self.wrap_text_to_lines(content, available_width, &font, font_size);
611
612 if lines.is_empty() || (lines.len() == 1 && lines[0].is_empty()) {
613 return Ok(());
614 }
615
616 let line_height = font_size * 1.2;
618 let total_text_height = lines.len() as f64 * line_height;
619
620 let available_height = height - style.padding.top - style.padding.bottom;
622
623 let text_block_top =
626 y + height - style.padding.top - (available_height - total_text_height) / 2.0;
627
628 for (line_idx, line) in lines.iter().enumerate() {
630 if line.is_empty() {
631 continue;
632 }
633
634 let text_x = match style.alignment {
636 CellAlignment::Left => x + style.padding.left,
637 CellAlignment::Center => {
638 let line_width = measure_text(line, &font, font_size);
639 x + style.padding.left + (available_width - line_width) / 2.0
640 }
641 CellAlignment::Right => {
642 let line_width = measure_text(line, &font, font_size);
643 x + width - style.padding.right - line_width
644 }
645 CellAlignment::Justify => x + style.padding.left,
646 };
647
648 let text_y = text_block_top - (line_idx as f64 + 0.8) * line_height;
650
651 if text_y >= y + style.padding.bottom {
653 page.text()
654 .set_font(font.clone(), font_size)
655 .set_fill_color(text_color)
656 .at(text_x, text_y)
657 .write(line)?;
658 }
659 }
660 } else {
661 let display_text =
663 self.truncate_text_to_width(content, available_width, &font, font_size);
664
665 let text_x = match style.alignment {
667 CellAlignment::Left => x + style.padding.left,
668 CellAlignment::Center => {
669 let text_width = measure_text(&display_text, &font, font_size);
671 x + style.padding.left + (available_width - text_width) / 2.0
672 }
673 CellAlignment::Right => {
674 let text_width = measure_text(&display_text, &font, font_size);
675 x + width - style.padding.right - text_width
676 }
677 CellAlignment::Justify => x + style.padding.left,
678 };
679
680 let text_y = style
682 .padding
683 .pad_vertically(&page.coordinate_system(), y + height / 2.0);
684
685 if !display_text.is_empty() {
687 let text_obj = page
688 .text()
689 .set_font(font, font_size)
690 .set_fill_color(text_color);
691
692 text_obj.at(text_x, text_y).write(&display_text)?;
693 }
694 }
695
696 Ok(())
697 }
698
699 fn calculate_column_positions(&self, table: &AdvancedTable, start_x: f64) -> Vec<f64> {
701 let mut positions = Vec::new();
702 let mut current_x = start_x;
703
704 for column in &table.columns {
705 positions.push(current_x);
706 current_x += column.width + table.cell_spacing;
707 }
708
709 positions
710 }
711
712 fn calculate_span_width(&self, table: &AdvancedTable, start_col: usize, colspan: usize) -> f64 {
714 let mut total_width = 0.0;
715
716 for i in 0..colspan {
717 if let Some(column) = table.columns.get(start_col + i) {
718 total_width += column.width;
719 if i > 0 {
720 total_width += table.cell_spacing;
721 }
722 }
723 }
724
725 total_width
726 }
727
728 fn resolve_cell_style(
730 &self,
731 table: &AdvancedTable,
732 cell: &CellData,
733 row_idx: usize,
734 col_idx: usize,
735 ) -> CellStyle {
736 if let Some(cell_style) = &cell.style {
739 return cell_style.clone();
740 }
741
742 table.get_cell_style(row_idx, col_idx)
743 }
744
745 fn render_table_border(
747 &self,
748 page: &mut Page,
749 table: &AdvancedTable,
750 x: f64,
751 start_y: f64,
752 end_y: f64,
753 ) -> Result<(), PdfError> {
754 let total_width = table.calculate_width();
755 let height = start_y - end_y;
756
757 page.graphics()
758 .save_state()
759 .set_stroke_color(Color::black())
760 .set_line_width(1.0)
761 .rectangle(x, end_y, total_width, height)
762 .stroke()
763 .restore_state();
764
765 Ok(())
766 }
767}
768
769impl Default for TableRenderer {
770 fn default() -> Self {
771 Self::new()
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778 use crate::text::Font;
779
780 #[test]
781 fn test_truncate_text_to_width_no_truncation_needed() {
782 let renderer = TableRenderer::new();
783 let text = "Short";
784 let max_width = 100.0;
785 let font = Font::Helvetica;
786 let font_size = 12.0;
787
788 let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
789 assert_eq!(result, "Short");
790 }
791
792 #[test]
793 fn test_truncate_text_to_width_with_truncation() {
794 let renderer = TableRenderer::new();
795 let text = "This is a very long text that should be truncated";
796 let max_width = 50.0; let font = Font::Helvetica;
798 let font_size = 12.0;
799
800 let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
801 assert!(result.ends_with("..."));
802 assert!(result.len() < text.len());
803
804 let truncated_width = measure_text(&result, &font, font_size);
806 assert!(truncated_width <= max_width);
807 }
808
809 #[test]
810 fn test_truncate_text_to_width_empty_when_too_narrow() {
811 let renderer = TableRenderer::new();
812 let text = "Any text";
813 let max_width = 5.0; let font = Font::Helvetica;
815 let font_size = 12.0;
816
817 let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
818 assert_eq!(result, "");
819 }
820
821 #[test]
822 fn test_truncate_text_to_width_exactly_ellipsis_width() {
823 let renderer = TableRenderer::new();
824 let text = "Some text";
825 let font = Font::Helvetica;
826 let font_size = 12.0;
827
828 let ellipsis_width = measure_text("...", &font, font_size);
830
831 let result = renderer.truncate_text_to_width(text, ellipsis_width, &font, font_size);
832 assert_eq!(result, "...");
833 }
834
835 #[test]
836 fn test_truncate_text_to_width_single_character() {
837 let renderer = TableRenderer::new();
838 let text = "A";
839 let max_width = 50.0;
840 let font = Font::Helvetica;
841 let font_size = 12.0;
842
843 let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
844 assert_eq!(result, "A");
845 }
846
847 #[test]
848 fn test_truncate_text_to_width_different_fonts() {
849 let renderer = TableRenderer::new();
850 let text = "This text will be truncated";
851 let max_width = 60.0;
852 let font_size = 12.0;
853
854 let helvetica_result =
856 renderer.truncate_text_to_width(text, max_width, &Font::Helvetica, font_size);
857 let courier_result =
858 renderer.truncate_text_to_width(text, max_width, &Font::Courier, font_size);
859 let times_result =
860 renderer.truncate_text_to_width(text, max_width, &Font::TimesRoman, font_size);
861
862 for result in [&helvetica_result, &courier_result, ×_result] {
864 assert!(result.ends_with("..."));
865 assert!(result.len() < text.len());
866 }
867
868 assert!(!helvetica_result.is_empty());
871 assert!(!courier_result.is_empty());
872 assert!(!times_result.is_empty());
873 }
874
875 #[test]
876 fn test_truncate_text_to_width_empty_input() {
877 let renderer = TableRenderer::new();
878 let text = "";
879 let max_width = 100.0;
880 let font = Font::Helvetica;
881 let font_size = 12.0;
882
883 let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
884 assert_eq!(result, "");
885 }
886
887 #[test]
888 fn test_truncate_text_to_width_unicode_characters() {
889 let renderer = TableRenderer::new();
890 let text = "Héllö Wørld with ümlauts and émojis 🚀🎉";
891 let max_width = 80.0;
892 let font = Font::Helvetica;
893 let font_size = 12.0;
894
895 let result = renderer.truncate_text_to_width(text, max_width, &font, font_size);
896
897 if result != text {
899 assert!(result.ends_with("..."));
900 }
901
902 let result_width = measure_text(&result, &font, font_size);
904 assert!(result_width <= max_width);
905 }
906
907 #[test]
910 fn test_truncate_linear_short_text_fits() {
911 let renderer = TableRenderer::new();
912 let text = "Hi";
913 let font = Font::Helvetica;
914 let font_size = 12.0;
915 let result = renderer.truncate_text_to_width(text, 500.0, &font, font_size);
917 assert_eq!(result, "Hi");
918 }
919
920 #[test]
921 fn test_truncate_linear_overflow_adds_ellipsis() {
922 let renderer = TableRenderer::new();
923 let text = "This is a long sentence that will not fit";
924 let font = Font::Helvetica;
925 let font_size = 12.0;
926 let result = renderer.truncate_text_to_width(text, 40.0, &font, font_size);
927 assert!(
928 result.ends_with("..."),
929 "Expected ellipsis suffix, got: {result}"
930 );
931 assert!(result.len() < text.len());
932 let result_width = measure_text(&result, &font, font_size);
933 assert!(result_width <= 40.0, "Truncated text exceeds max_width");
934 }
935
936 #[test]
937 fn test_truncate_linear_unicode() {
938 let renderer = TableRenderer::new();
939 let text = "日本語テスト文字列";
941 let font = Font::Helvetica;
942 let font_size = 12.0;
943 let result = renderer.truncate_text_to_width(text, 50.0, &font, font_size);
944 assert!(std::str::from_utf8(result.as_bytes()).is_ok());
946 if result != text {
947 assert!(
948 result.ends_with("..."),
949 "Truncated unicode should end with ellipsis"
950 );
951 }
952 let result_width = measure_text(&result, &font, font_size);
953 assert!(result_width <= 50.0);
954 }
955
956 #[test]
959 fn test_wrap_text_to_lines_no_wrap_needed() {
960 let renderer = TableRenderer::new();
961 let text = "Short text";
962 let max_width = 200.0;
963 let font = Font::Helvetica;
964 let font_size = 12.0;
965
966 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
967 assert_eq!(lines.len(), 1);
968 assert_eq!(lines[0], "Short text");
969 }
970
971 #[test]
972 fn test_wrap_text_to_lines_simple_wrap() {
973 let renderer = TableRenderer::new();
974 let text = "This is a longer text that should wrap to multiple lines";
975 let max_width = 80.0; let font = Font::Helvetica;
977 let font_size = 12.0;
978
979 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
980 assert!(lines.len() > 1, "Text should wrap to multiple lines");
981
982 for line in &lines {
984 let line_width = measure_text(line, &font, font_size);
985 assert!(
986 line_width <= max_width + 1.0, "Line '{}' exceeds max_width (width: {}, max: {})",
988 line,
989 line_width,
990 max_width
991 );
992 }
993 }
994
995 #[test]
996 fn test_wrap_text_to_lines_preserves_newlines() {
997 let renderer = TableRenderer::new();
998 let text = "Line one\nLine two\nLine three";
999 let max_width = 200.0; let font = Font::Helvetica;
1001 let font_size = 12.0;
1002
1003 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
1004 assert_eq!(lines.len(), 3);
1005 assert_eq!(lines[0], "Line one");
1006 assert_eq!(lines[1], "Line two");
1007 assert_eq!(lines[2], "Line three");
1008 }
1009
1010 #[test]
1011 fn test_wrap_text_to_lines_empty_input() {
1012 let renderer = TableRenderer::new();
1013 let text = "";
1014 let max_width = 100.0;
1015 let font = Font::Helvetica;
1016 let font_size = 12.0;
1017
1018 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
1019 assert_eq!(lines.len(), 1);
1020 assert_eq!(lines[0], "");
1021 }
1022
1023 #[test]
1024 fn test_wrap_text_to_lines_single_word_too_long() {
1025 let renderer = TableRenderer::new();
1026 let text = "Supercalifragilisticexpialidocious";
1027 let max_width = 50.0; let font = Font::Helvetica;
1029 let font_size = 12.0;
1030
1031 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
1032 assert!(lines.len() >= 1, "Should produce at least one line");
1034
1035 let joined: String = lines.join("");
1037 assert_eq!(joined, text);
1038 }
1039
1040 #[test]
1041 fn test_wrap_text_to_lines_multiple_spaces() {
1042 let renderer = TableRenderer::new();
1043 let text = "Word with spaces";
1044 let max_width = 200.0;
1045 let font = Font::Helvetica;
1046 let font_size = 12.0;
1047
1048 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
1049 assert_eq!(lines.len(), 1);
1050 assert!(!lines[0].is_empty());
1052 }
1053
1054 #[test]
1055 fn test_wrap_text_to_lines_unicode() {
1056 let renderer = TableRenderer::new();
1057 let text = "日本語テキスト with English words";
1058 let max_width = 100.0;
1059 let font = Font::Helvetica;
1060 let font_size = 12.0;
1061
1062 let lines = renderer.wrap_text_to_lines(text, max_width, &font, font_size);
1063 assert!(!lines.is_empty());
1065 }
1066}