Skip to main content

oxidize_pdf/advanced_tables/
header_builder.rs

1//! Header builder for complex table headers with spanning support
2
3use super::cell_style::CellStyle;
4use super::error::TableError;
5
6/// A header cell that can span multiple columns and rows
7#[derive(Debug, Clone)]
8pub struct HeaderCell {
9    /// Header text
10    pub text: String,
11    /// Number of columns this header spans
12    pub colspan: usize,
13    /// Number of rows this header spans (for multi-level headers)
14    pub rowspan: usize,
15    /// Custom style for this header cell
16    pub style: Option<CellStyle>,
17    /// Column index where this header starts
18    pub start_col: usize,
19    /// Row level (0 = top level)
20    pub row_level: usize,
21}
22
23impl HeaderCell {
24    /// Create a simple header cell
25    pub fn new<S: Into<String>>(text: S) -> Self {
26        Self {
27            text: text.into(),
28            colspan: 1,
29            rowspan: 1,
30            style: None,
31            start_col: 0,
32            row_level: 0,
33        }
34    }
35
36    /// Set column span
37    pub fn colspan(mut self, span: usize) -> Self {
38        self.colspan = span.max(1);
39        self
40    }
41
42    /// Set row span
43    pub fn rowspan(mut self, span: usize) -> Self {
44        self.rowspan = span.max(1);
45        self
46    }
47
48    /// Set custom style
49    pub fn style(mut self, style: CellStyle) -> Self {
50        self.style = Some(style);
51        self
52    }
53
54    /// Set starting column
55    pub fn start_col(mut self, col: usize) -> Self {
56        self.start_col = col;
57        self
58    }
59
60    /// Set row level
61    pub fn row_level(mut self, level: usize) -> Self {
62        self.row_level = level;
63        self
64    }
65}
66
67/// Builder for creating complex multi-level table headers
68#[derive(Debug, Clone)]
69pub struct HeaderBuilder {
70    /// All header cells organized by levels
71    pub levels: Vec<Vec<HeaderCell>>,
72    /// Total number of columns in the table
73    pub total_columns: usize,
74    /// Default style for headers
75    pub default_style: CellStyle,
76}
77
78impl HeaderBuilder {
79    /// Create a new header builder
80    pub fn new(total_columns: usize) -> Self {
81        Self {
82            levels: Vec::new(),
83            total_columns,
84            default_style: CellStyle::header(),
85        }
86    }
87
88    /// Create a new header builder without specifying columns (for compatibility with tests)
89    pub fn auto() -> Self {
90        Self::new(0) // Will be calculated automatically
91    }
92
93    /// Add a header level with (text, colspan) pairs
94    pub fn add_level(mut self, headers: Vec<(&str, usize)>) -> Self {
95        let cells: Vec<HeaderCell> = headers
96            .into_iter()
97            .scan(0, |start_col, (text, colspan)| {
98                let cell = HeaderCell::new(text)
99                    .colspan(colspan)
100                    .start_col(*start_col)
101                    .row_level(self.levels.len());
102                *start_col += colspan;
103                Some(cell)
104            })
105            .collect();
106
107        // Auto-calculate total columns if not set
108        if self.total_columns == 0 {
109            self.total_columns = cells.iter().map(|c| c.colspan).sum();
110        }
111
112        self.levels.push(cells);
113        self
114    }
115
116    /// Set default header style
117    pub fn default_style(mut self, style: CellStyle) -> Self {
118        self.default_style = style;
119        self
120    }
121
122    /// Add a simple single-level header row
123    pub fn add_simple_row(mut self, headers: Vec<&str>) -> Self {
124        let cells: Vec<HeaderCell> = headers
125            .into_iter()
126            .enumerate()
127            .map(|(i, text)| {
128                HeaderCell::new(text)
129                    .start_col(i)
130                    .row_level(self.levels.len())
131            })
132            .collect();
133
134        self.levels.push(cells);
135        self
136    }
137
138    /// Add a header row with custom cells
139    pub fn add_custom_row(mut self, cells: Vec<HeaderCell>) -> Self {
140        let level = self.levels.len();
141        let updated_cells: Vec<HeaderCell> = cells
142            .into_iter()
143            .map(|mut cell| {
144                cell.row_level = level;
145                cell
146            })
147            .collect();
148
149        self.levels.push(updated_cells);
150        self
151    }
152
153    /// Add a grouped header (spans multiple columns) with sub-headers
154    ///
155    /// Example: "Sales Data" spanning 3 columns with sub-headers "Q1", "Q2", "Q3"
156    pub fn add_group(mut self, group_header: &str, sub_headers: Vec<&str>) -> Self {
157        let group_colspan = sub_headers.len();
158        let start_col = self.calculate_next_start_col();
159
160        // Add the group header at current level
161        let group_level = self.levels.len();
162        if self.levels.len() == group_level {
163            self.levels.push(Vec::new());
164        }
165
166        let group_cell = HeaderCell::new(group_header)
167            .colspan(group_colspan)
168            .start_col(start_col)
169            .row_level(group_level);
170
171        self.levels[group_level].push(group_cell);
172
173        // Add sub-headers at the next level
174        let sub_level = group_level + 1;
175        if self.levels.len() <= sub_level {
176            self.levels.push(Vec::new());
177        }
178
179        for (i, sub_header) in sub_headers.into_iter().enumerate() {
180            let sub_cell = HeaderCell::new(sub_header)
181                .start_col(start_col + i)
182                .row_level(sub_level);
183
184            self.levels[sub_level].push(sub_cell);
185        }
186
187        self
188    }
189
190    /// Add a complex header structure with manual positioning
191    pub fn add_complex_header(
192        mut self,
193        text: &str,
194        start_col: usize,
195        colspan: usize,
196        rowspan: usize,
197    ) -> Self {
198        let level = self.levels.len();
199        if self.levels.is_empty() {
200            self.levels.push(Vec::new());
201        }
202
203        let cell = HeaderCell::new(text)
204            .start_col(start_col)
205            .colspan(colspan)
206            .rowspan(rowspan)
207            .row_level(level);
208
209        // SAFETY: levels is guaranteed non-empty by the check above
210        debug_assert!(
211            !self.levels.is_empty(),
212            "levels must be non-empty after initialization"
213        );
214        if let Some(last_level) = self.levels.last_mut() {
215            last_level.push(cell);
216        }
217        self
218    }
219
220    /// Calculate the next available starting column
221    fn calculate_next_start_col(&self) -> usize {
222        if let Some(last_level) = self.levels.last() {
223            last_level
224                .iter()
225                .map(|cell| cell.start_col + cell.colspan)
226                .max()
227                .unwrap_or(0)
228        } else {
229            0
230        }
231    }
232
233    /// Get the total number of header rows
234    pub fn row_count(&self) -> usize {
235        self.levels.len()
236    }
237
238    /// Get the height needed for headers (in points, assuming default font size)
239    pub fn calculate_height(&self) -> f64 {
240        // Assume each header row needs 20 points by default
241        let base_height = 20.0;
242        let row_count = self.row_count() as f64;
243
244        // Add some padding between levels
245        let padding = if row_count > 1.0 {
246            (row_count - 1.0) * 5.0
247        } else {
248            0.0
249        };
250
251        row_count * base_height + padding
252    }
253
254    /// Validate the header structure
255    pub fn validate(&self) -> Result<(), TableError> {
256        for (level_idx, level) in self.levels.iter().enumerate() {
257            let mut column_coverage = vec![false; self.total_columns];
258
259            for cell in level {
260                // Check if cell extends beyond table width
261                if cell.start_col + cell.colspan > self.total_columns {
262                    return Err(TableError::HeaderOutOfBounds {
263                        level: level_idx,
264                        start: cell.start_col,
265                        span: cell.colspan,
266                        total: self.total_columns,
267                    });
268                }
269
270                // Check for overlapping cells
271                for (col, coverage) in column_coverage
272                    .iter_mut()
273                    .enumerate()
274                    .skip(cell.start_col)
275                    .take(cell.colspan)
276                {
277                    if *coverage {
278                        return Err(TableError::HeaderOverlap {
279                            level: level_idx,
280                            column: col,
281                        });
282                    }
283                    *coverage = true;
284                }
285            }
286        }
287
288        Ok(())
289    }
290
291    /// Get all cells that should be rendered at a specific position
292    pub fn get_cells_at_position(&self, level: usize, col: usize) -> Vec<&HeaderCell> {
293        if level >= self.levels.len() {
294            return Vec::new();
295        }
296
297        self.levels[level]
298            .iter()
299            .filter(|cell| col >= cell.start_col && col < (cell.start_col + cell.colspan))
300            .collect()
301    }
302
303    /// Create a financial report header
304    pub fn financial_report() -> Self {
305        Self::new(6)
306            .default_style(
307                CellStyle::header().background_color(crate::graphics::Color::rgb(0.2, 0.4, 0.8)),
308            )
309            .add_group("Q1 2024", vec!["Revenue", "Expenses"])
310            .add_group("Q2 2024", vec!["Revenue", "Expenses"])
311            .add_group("Total", vec!["Revenue", "Expenses"])
312    }
313
314    /// Create a product comparison header
315    pub fn product_comparison(products: Vec<&str>) -> Self {
316        let total_cols = 1 + products.len(); // Feature column + product columns
317        let mut builder = Self::new(total_cols).default_style(CellStyle::header());
318
319        // Add "Features" as first column
320        builder = builder.add_complex_header("Features", 0, 1, 2);
321
322        // Add product group header
323        builder = builder.add_complex_header("Products", 1, products.len(), 1);
324
325        // Add individual product headers
326        for (i, product) in products.into_iter().enumerate() {
327            builder = builder.add_complex_header(product, i + 1, 1, 1);
328        }
329
330        builder
331    }
332}
333
334impl Default for HeaderBuilder {
335    fn default() -> Self {
336        Self::new(1)
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    // ==================== HeaderCell Tests ====================
345
346    #[test]
347    fn test_header_cell_new() {
348        let cell = HeaderCell::new("Test Header");
349        assert_eq!(cell.text, "Test Header");
350        assert_eq!(cell.colspan, 1);
351        assert_eq!(cell.rowspan, 1);
352        assert!(cell.style.is_none());
353        assert_eq!(cell.start_col, 0);
354        assert_eq!(cell.row_level, 0);
355    }
356
357    #[test]
358    fn test_header_cell_new_from_string() {
359        let cell = HeaderCell::new(String::from("From String"));
360        assert_eq!(cell.text, "From String");
361    }
362
363    #[test]
364    fn test_header_cell_colspan() {
365        let cell = HeaderCell::new("Wide").colspan(3);
366        assert_eq!(cell.colspan, 3);
367    }
368
369    #[test]
370    fn test_header_cell_colspan_minimum() {
371        // colspan(0) should become 1 due to max(1)
372        let cell = HeaderCell::new("Test").colspan(0);
373        assert_eq!(cell.colspan, 1);
374    }
375
376    #[test]
377    fn test_header_cell_rowspan() {
378        let cell = HeaderCell::new("Tall").rowspan(2);
379        assert_eq!(cell.rowspan, 2);
380    }
381
382    #[test]
383    fn test_header_cell_rowspan_minimum() {
384        // rowspan(0) should become 1 due to max(1)
385        let cell = HeaderCell::new("Test").rowspan(0);
386        assert_eq!(cell.rowspan, 1);
387    }
388
389    #[test]
390    fn test_header_cell_style() {
391        let style = CellStyle::header();
392        let cell = HeaderCell::new("Styled").style(style.clone());
393        assert!(cell.style.is_some());
394    }
395
396    #[test]
397    fn test_header_cell_start_col() {
398        let cell = HeaderCell::new("Offset").start_col(5);
399        assert_eq!(cell.start_col, 5);
400    }
401
402    #[test]
403    fn test_header_cell_row_level() {
404        let cell = HeaderCell::new("Level 2").row_level(2);
405        assert_eq!(cell.row_level, 2);
406    }
407
408    #[test]
409    fn test_header_cell_builder_chain() {
410        let cell = HeaderCell::new("Complex")
411            .colspan(3)
412            .rowspan(2)
413            .start_col(1)
414            .row_level(1);
415
416        assert_eq!(cell.text, "Complex");
417        assert_eq!(cell.colspan, 3);
418        assert_eq!(cell.rowspan, 2);
419        assert_eq!(cell.start_col, 1);
420        assert_eq!(cell.row_level, 1);
421    }
422
423    #[test]
424    fn test_header_cell_clone() {
425        let original = HeaderCell::new("Original").colspan(2);
426        let cloned = original.clone();
427
428        assert_eq!(cloned.text, original.text);
429        assert_eq!(cloned.colspan, original.colspan);
430    }
431
432    #[test]
433    fn test_header_cell_debug() {
434        let cell = HeaderCell::new("Debug Test");
435        let debug_str = format!("{:?}", cell);
436        assert!(debug_str.contains("HeaderCell"));
437        assert!(debug_str.contains("Debug Test"));
438    }
439
440    // ==================== HeaderBuilder Tests ====================
441
442    #[test]
443    fn test_header_builder_new() {
444        let builder = HeaderBuilder::new(5);
445        assert_eq!(builder.total_columns, 5);
446        assert!(builder.levels.is_empty());
447    }
448
449    #[test]
450    fn test_header_builder_auto() {
451        let builder = HeaderBuilder::auto();
452        assert_eq!(builder.total_columns, 0);
453    }
454
455    #[test]
456    fn test_header_builder_default() {
457        let builder = HeaderBuilder::default();
458        assert_eq!(builder.total_columns, 1);
459    }
460
461    #[test]
462    fn test_simple_header() {
463        let header = HeaderBuilder::new(3).add_simple_row(vec!["Name", "Age", "Department"]);
464
465        assert_eq!(header.row_count(), 1);
466        assert_eq!(header.levels[0].len(), 3);
467        assert_eq!(header.levels[0][0].text, "Name");
468        assert_eq!(header.levels[0][1].text, "Age");
469        assert_eq!(header.levels[0][2].text, "Department");
470    }
471
472    #[test]
473    fn test_add_level() {
474        let header = HeaderBuilder::auto().add_level(vec![("A", 1), ("B", 2), ("C", 1)]);
475
476        assert_eq!(header.total_columns, 4); // auto-calculated: 1+2+1
477        assert_eq!(header.levels[0].len(), 3);
478        assert_eq!(header.levels[0][0].start_col, 0);
479        assert_eq!(header.levels[0][1].start_col, 1);
480        assert_eq!(header.levels[0][1].colspan, 2);
481        assert_eq!(header.levels[0][2].start_col, 3);
482    }
483
484    #[test]
485    fn test_add_level_with_preset_columns() {
486        let header = HeaderBuilder::new(10).add_level(vec![("A", 2), ("B", 3)]);
487
488        // total_columns should NOT change when preset
489        assert_eq!(header.total_columns, 10);
490    }
491
492    #[test]
493    fn test_default_style() {
494        let custom_style = CellStyle::default();
495        let builder = HeaderBuilder::new(3).default_style(custom_style);
496        // Style is stored
497        assert!(
498            builder.default_style.background_color.is_none()
499                || builder.default_style.background_color.is_some()
500        );
501    }
502
503    #[test]
504    fn test_add_custom_row() {
505        let cells = vec![
506            HeaderCell::new("Custom1").colspan(2),
507            HeaderCell::new("Custom2"),
508        ];
509        let header = HeaderBuilder::new(3).add_custom_row(cells);
510
511        assert_eq!(header.row_count(), 1);
512        assert_eq!(header.levels[0][0].text, "Custom1");
513        assert_eq!(header.levels[0][0].row_level, 0);
514    }
515
516    #[test]
517    fn test_grouped_header() {
518        let header = HeaderBuilder::new(4)
519            .add_group("Personal Info", vec!["Name", "Age"])
520            .add_group("Work Info", vec!["Department", "Salary"]);
521
522        assert_eq!(header.row_count(), 4); // Two groups, each creates group + sub levels
523        assert!(header.validate().is_ok());
524    }
525
526    #[test]
527    fn test_add_group_single() {
528        let header = HeaderBuilder::new(3).add_group("Group", vec!["A", "B", "C"]);
529
530        // Should have 2 levels: group header and sub-headers
531        assert_eq!(header.row_count(), 2);
532        assert_eq!(header.levels[0][0].text, "Group");
533        assert_eq!(header.levels[0][0].colspan, 3);
534        assert_eq!(header.levels[1].len(), 3);
535    }
536
537    #[test]
538    fn test_add_complex_header() {
539        let header = HeaderBuilder::new(4).add_complex_header("Complex", 1, 2, 2);
540
541        assert_eq!(header.levels[0][0].text, "Complex");
542        assert_eq!(header.levels[0][0].start_col, 1);
543        assert_eq!(header.levels[0][0].colspan, 2);
544        assert_eq!(header.levels[0][0].rowspan, 2);
545    }
546
547    #[test]
548    fn test_add_complex_header_on_empty() {
549        let header = HeaderBuilder::new(3).add_complex_header("First", 0, 1, 1);
550        assert_eq!(header.levels.len(), 1);
551        assert_eq!(header.levels[0][0].text, "First");
552    }
553
554    #[test]
555    fn test_row_count() {
556        let header = HeaderBuilder::new(3)
557            .add_simple_row(vec!["A", "B", "C"])
558            .add_simple_row(vec!["D", "E", "F"]);
559
560        assert_eq!(header.row_count(), 2);
561    }
562
563    #[test]
564    fn test_row_count_empty() {
565        let header = HeaderBuilder::new(3);
566        assert_eq!(header.row_count(), 0);
567    }
568
569    #[test]
570    fn test_calculate_height_single_row() {
571        let header = HeaderBuilder::new(3).add_simple_row(vec!["A", "B", "C"]);
572
573        // 1 row * 20 points + 0 padding
574        assert_eq!(header.calculate_height(), 20.0);
575    }
576
577    #[test]
578    fn test_calculate_height_multiple_rows() {
579        let header = HeaderBuilder::new(3)
580            .add_simple_row(vec!["A", "B", "C"])
581            .add_simple_row(vec!["D", "E", "F"]);
582
583        // 2 rows * 20 points + (2-1) * 5 padding = 40 + 5 = 45
584        assert_eq!(header.calculate_height(), 45.0);
585    }
586
587    #[test]
588    fn test_calculate_height_empty() {
589        let header = HeaderBuilder::new(3);
590        assert_eq!(header.calculate_height(), 0.0);
591    }
592
593    #[test]
594    fn test_header_validation() {
595        let header = HeaderBuilder::new(2).add_complex_header("Too Wide", 0, 3, 1); // Spans 3 columns but table only has 2
596
597        assert!(header.validate().is_err());
598    }
599
600    #[test]
601    fn test_validation_out_of_bounds_error() {
602        let header = HeaderBuilder::new(2).add_complex_header("Wide", 0, 5, 1);
603        let result = header.validate();
604        assert!(result.is_err());
605        if let Err(TableError::HeaderOutOfBounds {
606            level,
607            start,
608            span,
609            total,
610        }) = result
611        {
612            assert_eq!(level, 0);
613            assert_eq!(start, 0);
614            assert_eq!(span, 5);
615            assert_eq!(total, 2);
616        }
617    }
618
619    #[test]
620    fn test_validation_overlap_error() {
621        let cells = vec![
622            HeaderCell::new("A").start_col(0).colspan(2),
623            HeaderCell::new("B").start_col(1).colspan(1), // Overlaps with A
624        ];
625        let header = HeaderBuilder::new(3).add_custom_row(cells);
626        let result = header.validate();
627        assert!(result.is_err());
628        if let Err(TableError::HeaderOverlap { level, column }) = result {
629            assert_eq!(level, 0);
630            assert_eq!(column, 1);
631        }
632    }
633
634    #[test]
635    fn test_validation_valid_non_overlapping() {
636        let cells = vec![
637            HeaderCell::new("A").start_col(0).colspan(2),
638            HeaderCell::new("B").start_col(2).colspan(1),
639        ];
640        let header = HeaderBuilder::new(3).add_custom_row(cells);
641        assert!(header.validate().is_ok());
642    }
643
644    #[test]
645    fn test_get_cells_at_position_found() {
646        let header = HeaderBuilder::new(3).add_level(vec![("Wide", 2), ("Narrow", 1)]);
647
648        let cells = header.get_cells_at_position(0, 0);
649        assert_eq!(cells.len(), 1);
650        assert_eq!(cells[0].text, "Wide");
651
652        let cells = header.get_cells_at_position(0, 1);
653        assert_eq!(cells.len(), 1);
654        assert_eq!(cells[0].text, "Wide"); // Still in "Wide"'s span
655
656        let cells = header.get_cells_at_position(0, 2);
657        assert_eq!(cells.len(), 1);
658        assert_eq!(cells[0].text, "Narrow");
659    }
660
661    #[test]
662    fn test_get_cells_at_position_invalid_level() {
663        let header = HeaderBuilder::new(3).add_simple_row(vec!["A", "B", "C"]);
664
665        let cells = header.get_cells_at_position(5, 0); // Level 5 doesn't exist
666        assert!(cells.is_empty());
667    }
668
669    #[test]
670    fn test_get_cells_at_position_empty_builder() {
671        let header = HeaderBuilder::new(3);
672        let cells = header.get_cells_at_position(0, 0);
673        assert!(cells.is_empty());
674    }
675
676    #[test]
677    fn test_financial_header() {
678        let header = HeaderBuilder::financial_report();
679        assert!(header.validate().is_ok());
680        assert_eq!(header.total_columns, 6);
681    }
682
683    #[test]
684    fn test_product_comparison() {
685        let header = HeaderBuilder::product_comparison(vec!["Product A", "Product B", "Product C"]);
686
687        assert_eq!(header.total_columns, 4); // 1 feature col + 3 products
688    }
689
690    #[test]
691    fn test_product_comparison_single_product() {
692        let header = HeaderBuilder::product_comparison(vec!["Only Product"]);
693        assert_eq!(header.total_columns, 2);
694    }
695
696    #[test]
697    fn test_header_builder_clone() {
698        let original = HeaderBuilder::new(5).add_simple_row(vec!["A", "B"]);
699        let cloned = original.clone();
700
701        assert_eq!(cloned.total_columns, original.total_columns);
702        assert_eq!(cloned.levels.len(), original.levels.len());
703    }
704
705    #[test]
706    fn test_header_builder_debug() {
707        let builder = HeaderBuilder::new(3);
708        let debug_str = format!("{:?}", builder);
709        assert!(debug_str.contains("HeaderBuilder"));
710    }
711
712    #[test]
713    fn test_multiple_levels_integration() {
714        let header = HeaderBuilder::new(6)
715            .add_level(vec![("Sales Data", 3), ("Expenses", 3)])
716            .add_level(vec![
717                ("Q1", 1),
718                ("Q2", 1),
719                ("Q3", 1),
720                ("Q1", 1),
721                ("Q2", 1),
722                ("Q3", 1),
723            ]);
724
725        assert_eq!(header.row_count(), 2);
726        assert!(header.validate().is_ok());
727    }
728
729    #[test]
730    fn test_calculate_next_start_col_empty() {
731        let builder = HeaderBuilder::new(5);
732        assert_eq!(builder.calculate_next_start_col(), 0);
733    }
734
735    #[test]
736    fn test_calculate_next_start_col_after_cells() {
737        let header = HeaderBuilder::new(10).add_level(vec![("A", 2), ("B", 3)]);
738        // After A(0-1) and B(2-4), next start should be 5
739        assert_eq!(header.calculate_next_start_col(), 5);
740    }
741}