Skip to main content

oxidize_pdf/advanced_tables/
cell_style.rs

1//! Cell styling system for advanced tables
2
3use crate::graphics::Color;
4use crate::text::Font;
5use crate::CoordinateSystem;
6
7/// Border styles for table cells
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
9pub enum BorderStyle {
10    /// No border
11    None,
12    /// Solid line border
13    #[default]
14    Solid,
15    /// Dashed line border
16    Dashed,
17    /// Dotted line border  
18    Dotted,
19    /// Double line border
20    Double,
21}
22
23/// Cell alignment options
24#[derive(Debug, Clone, Copy, PartialEq, Default)]
25pub enum CellAlignment {
26    /// Left-aligned content
27    #[default]
28    Left,
29    /// Center-aligned content
30    Center,
31    /// Right-aligned content
32    Right,
33    /// Justified content (for multi-line text)
34    Justify,
35}
36
37/// Padding configuration for cells
38#[derive(Debug, Clone, Copy)]
39pub struct Padding {
40    /// Top padding
41    pub top: f64,
42    /// Right padding
43    pub right: f64,
44    /// Bottom padding
45    pub bottom: f64,
46    /// Left padding
47    pub left: f64,
48}
49
50impl Padding {
51    /// Create padding with individual values (top, right, bottom, left)
52    pub fn new(top: f64, right: f64, bottom: f64, left: f64) -> Self {
53        Self {
54            top,
55            right,
56            bottom,
57            left,
58        }
59    }
60
61    /// Create uniform padding on all sides
62    pub fn uniform(padding: f64) -> Self {
63        Self {
64            top: padding,
65            right: padding,
66            bottom: padding,
67            left: padding,
68        }
69    }
70
71    /// Create padding with horizontal and vertical values
72    pub fn symmetric(horizontal: f64, vertical: f64) -> Self {
73        Self {
74            top: vertical,
75            right: horizontal,
76            bottom: vertical,
77            left: horizontal,
78        }
79    }
80
81    /// Create padding with individual values for each side
82    pub fn individual(top: f64, right: f64, bottom: f64, left: f64) -> Self {
83        Self {
84            top,
85            right,
86            bottom,
87            left,
88        }
89    }
90
91    /// Get total horizontal padding (left + right)
92    pub fn horizontal_total(&self) -> f64 {
93        self.left + self.right
94    }
95
96    /// Get total vertical padding (top + bottom)
97    pub fn vertical_total(&self) -> f64 {
98        self.top + self.bottom
99    }
100
101    pub fn pad_vertically(&self, coordinate_system: &CoordinateSystem, y: f64) -> f64 {
102        let mut padded = y;
103        match coordinate_system {
104            CoordinateSystem::PdfStandard | CoordinateSystem::Custom(_) => {
105                padded -= self.top;
106                padded += self.bottom;
107            }
108            CoordinateSystem::ScreenSpace => {
109                padded += self.top;
110                padded -= self.bottom;
111            }
112        }
113
114        padded
115    }
116
117    pub fn pad_horizontally(&self, x: f64) -> f64 {
118        let mut padded = x;
119        padded -= self.right;
120        padded += self.left;
121
122        padded
123    }
124}
125
126impl Default for Padding {
127    fn default() -> Self {
128        Self::uniform(4.0)
129    }
130}
131
132/// Comprehensive cell styling configuration
133#[derive(Debug, Clone)]
134pub struct CellStyle {
135    /// Background color of the cell
136    pub background_color: Option<Color>,
137    /// Text color
138    pub text_color: Option<Color>,
139    /// Font to use for text
140    pub font: Option<Font>,
141    /// Font size
142    pub font_size: Option<f64>,
143    /// Cell padding
144    pub padding: Padding,
145    /// Text alignment within the cell
146    pub alignment: CellAlignment,
147    /// Border style configuration
148    pub border: BorderConfiguration,
149    /// Simple border style (for backward compatibility)
150    pub border_style: BorderStyle,
151    /// Whether text should wrap within the cell
152    pub text_wrap: bool,
153    /// Minimum cell height
154    pub min_height: Option<f64>,
155    /// Maximum cell height (text will be clipped if exceeded)
156    pub max_height: Option<f64>,
157}
158
159/// Border configuration for cells
160#[derive(Debug, Clone)]
161pub struct BorderConfiguration {
162    /// Top border
163    pub top: BorderEdge,
164    /// Right border
165    pub right: BorderEdge,
166    /// Bottom border
167    pub bottom: BorderEdge,
168    /// Left border
169    pub left: BorderEdge,
170}
171
172/// Individual border edge configuration
173#[derive(Debug, Clone)]
174pub struct BorderEdge {
175    /// Border style
176    pub style: BorderStyle,
177    /// Border width
178    pub width: f64,
179    /// Border color
180    pub color: Color,
181}
182
183impl BorderEdge {
184    /// Create a new border edge
185    pub fn new(style: BorderStyle, width: f64, color: Color) -> Self {
186        Self {
187            style,
188            width,
189            color,
190        }
191    }
192
193    /// Create a solid black border edge
194    pub fn solid(width: f64) -> Self {
195        Self::new(BorderStyle::Solid, width, Color::black())
196    }
197
198    /// Create a dashed border edge
199    pub fn dashed(width: f64, color: Color) -> Self {
200        Self::new(BorderStyle::Dashed, width, color)
201    }
202
203    /// Create a dotted border edge
204    pub fn dotted(width: f64, color: Color) -> Self {
205        Self::new(BorderStyle::Dotted, width, color)
206    }
207
208    /// No border
209    pub fn none() -> Self {
210        Self::new(BorderStyle::None, 0.0, Color::black())
211    }
212}
213
214impl Default for BorderEdge {
215    fn default() -> Self {
216        Self::solid(1.0)
217    }
218}
219
220impl BorderConfiguration {
221    /// Create a new border configuration
222    pub fn new() -> Self {
223        Self {
224            top: BorderEdge::default(),
225            right: BorderEdge::default(),
226            bottom: BorderEdge::default(),
227            left: BorderEdge::default(),
228        }
229    }
230
231    /// Create uniform border on all sides
232    pub fn uniform(edge: BorderEdge) -> Self {
233        Self {
234            top: edge.clone(),
235            right: edge.clone(),
236            bottom: edge.clone(),
237            left: edge,
238        }
239    }
240
241    /// Create border with only specific edges
242    pub fn edges(top: bool, right: bool, bottom: bool, left: bool) -> Self {
243        let solid_edge = BorderEdge::solid(1.0);
244        let no_edge = BorderEdge::none();
245
246        Self {
247            top: if top {
248                solid_edge.clone()
249            } else {
250                no_edge.clone()
251            },
252            right: if right {
253                solid_edge.clone()
254            } else {
255                no_edge.clone()
256            },
257            bottom: if bottom {
258                solid_edge.clone()
259            } else {
260                no_edge.clone()
261            },
262            left: if left { solid_edge } else { no_edge },
263        }
264    }
265
266    /// No borders
267    pub fn none() -> Self {
268        let no_edge = BorderEdge::none();
269        Self {
270            top: no_edge.clone(),
271            right: no_edge.clone(),
272            bottom: no_edge.clone(),
273            left: no_edge,
274        }
275    }
276}
277
278impl Default for BorderConfiguration {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284impl CellStyle {
285    /// Create a new default cell style
286    pub fn new() -> Self {
287        Self {
288            background_color: None,
289            text_color: Some(Color::black()),
290            font: Some(Font::Helvetica),
291            font_size: Some(12.0),
292            padding: Padding::default(),
293            alignment: CellAlignment::Left,
294            border: BorderConfiguration::default(),
295            border_style: BorderStyle::Solid,
296            text_wrap: true,
297            min_height: None,
298            max_height: None,
299        }
300    }
301
302    /// Set background color
303    pub fn background_color(mut self, color: Color) -> Self {
304        self.background_color = Some(color);
305        self
306    }
307
308    /// Set text color
309    pub fn text_color(mut self, color: Color) -> Self {
310        self.text_color = Some(color);
311        self
312    }
313
314    /// Set font
315    pub fn font(mut self, font: Font) -> Self {
316        self.font = Some(font);
317        self
318    }
319
320    /// Set font size
321    pub fn font_size(mut self, size: f64) -> Self {
322        self.font_size = Some(size);
323        self
324    }
325
326    /// Set padding
327    pub fn padding(mut self, padding: Padding) -> Self {
328        self.padding = padding;
329        self
330    }
331
332    /// Set alignment
333    pub fn alignment(mut self, alignment: CellAlignment) -> Self {
334        self.alignment = alignment;
335        self
336    }
337
338    /// Set border configuration
339    pub fn border_config(mut self, border: BorderConfiguration) -> Self {
340        self.border = border;
341        self
342    }
343
344    /// Set simple border (style, width, color) - used by tests
345    pub fn border(mut self, style: BorderStyle, width: f64, color: Color) -> Self {
346        self.border_style = style;
347        self.border = BorderConfiguration::uniform(BorderEdge::new(style, width, color));
348        self
349    }
350
351    /// Enable or disable text wrapping
352    pub fn text_wrap(mut self, wrap: bool) -> Self {
353        self.text_wrap = wrap;
354        self
355    }
356
357    /// Set minimum cell height
358    pub fn min_height(mut self, height: f64) -> Self {
359        self.min_height = Some(height);
360        self
361    }
362
363    /// Set maximum cell height
364    pub fn max_height(mut self, height: f64) -> Self {
365        self.max_height = Some(height);
366        self
367    }
368
369    /// Create a header style (bold, centered, with background)
370    pub fn header() -> Self {
371        Self::new()
372            .font(Font::HelveticaBold)
373            .font_size(14.0)
374            .alignment(CellAlignment::Center)
375            .background_color(Color::rgb(0.9, 0.9, 0.9))
376            .padding(Padding::uniform(8.0))
377    }
378
379    /// Create a data cell style (left-aligned, normal font)
380    pub fn data() -> Self {
381        Self::new()
382            .font(Font::Helvetica)
383            .font_size(12.0)
384            .alignment(CellAlignment::Left)
385            .padding(Padding::uniform(6.0))
386    }
387
388    /// Create a numeric cell style (right-aligned, monospace)
389    pub fn numeric() -> Self {
390        Self::new()
391            .font(Font::Courier)
392            .font_size(11.0)
393            .alignment(CellAlignment::Right)
394            .padding(Padding::uniform(6.0))
395    }
396
397    /// Create an alternating row style (with light background)
398    pub fn alternating() -> Self {
399        Self::data().background_color(Color::rgb(0.97, 0.97, 0.97))
400    }
401}
402
403impl Default for CellStyle {
404    fn default() -> Self {
405        Self::new()
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn test_pad_vertically() {
415        let cs = CoordinateSystem::PdfStandard;
416        let padding = Padding::new(6.0, 0.0, 2.0, 2.0);
417
418        assert_eq!(padding.pad_vertically(&cs, 100.0), 96.0);
419        assert_eq!(padding.pad_vertically(&cs, 50.0), 46.0);
420
421        let cs = CoordinateSystem::ScreenSpace;
422        let padding = Padding::new(6.0, 0.0, 2.0, 2.0);
423
424        assert_eq!(padding.pad_vertically(&cs, 100.0), 104.0);
425        assert_eq!(padding.pad_vertically(&cs, 50.0), 54.0);
426    }
427
428    #[test]
429    fn test_pad_horizontally() {
430        let padding = Padding::new(6.0, 12.0, 2.0, 2.0);
431
432        assert_eq!(padding.pad_horizontally(100.0), 90.0);
433        assert_eq!(padding.pad_horizontally(50.0), 40.0);
434    }
435
436    #[test]
437    fn test_border_style_default() {
438        let style = BorderStyle::default();
439        assert_eq!(style, BorderStyle::Solid);
440    }
441
442    #[test]
443    fn test_cell_alignment_default() {
444        let alignment = CellAlignment::default();
445        assert_eq!(alignment, CellAlignment::Left);
446    }
447
448    #[test]
449    fn test_padding_uniform() {
450        let padding = Padding::uniform(10.0);
451        assert_eq!(padding.top, 10.0);
452        assert_eq!(padding.right, 10.0);
453        assert_eq!(padding.bottom, 10.0);
454        assert_eq!(padding.left, 10.0);
455    }
456
457    #[test]
458    fn test_padding_symmetric() {
459        let padding = Padding::symmetric(5.0, 10.0);
460        assert_eq!(padding.top, 10.0);
461        assert_eq!(padding.right, 5.0);
462        assert_eq!(padding.bottom, 10.0);
463        assert_eq!(padding.left, 5.0);
464    }
465
466    #[test]
467    fn test_padding_individual() {
468        let padding = Padding::individual(1.0, 2.0, 3.0, 4.0);
469        assert_eq!(padding.top, 1.0);
470        assert_eq!(padding.right, 2.0);
471        assert_eq!(padding.bottom, 3.0);
472        assert_eq!(padding.left, 4.0);
473    }
474
475    #[test]
476    fn test_padding_totals() {
477        let padding = Padding::new(5.0, 10.0, 15.0, 20.0);
478        assert_eq!(padding.horizontal_total(), 30.0);
479        assert_eq!(padding.vertical_total(), 20.0);
480    }
481
482    #[test]
483    fn test_padding_default() {
484        let padding = Padding::default();
485        assert_eq!(padding.top, 4.0);
486        assert_eq!(padding.horizontal_total(), 8.0);
487    }
488
489    #[test]
490    fn test_border_edge_new() {
491        let edge = BorderEdge::new(BorderStyle::Dashed, 2.0, Color::red());
492        assert_eq!(edge.style, BorderStyle::Dashed);
493        assert_eq!(edge.width, 2.0);
494    }
495
496    #[test]
497    fn test_border_edge_solid() {
498        let edge = BorderEdge::solid(1.5);
499        assert_eq!(edge.style, BorderStyle::Solid);
500        assert_eq!(edge.width, 1.5);
501    }
502
503    #[test]
504    fn test_border_edge_dashed() {
505        let edge = BorderEdge::dashed(1.0, Color::blue());
506        assert_eq!(edge.style, BorderStyle::Dashed);
507    }
508
509    #[test]
510    fn test_border_edge_dotted() {
511        let edge = BorderEdge::dotted(0.5, Color::green());
512        assert_eq!(edge.style, BorderStyle::Dotted);
513    }
514
515    #[test]
516    fn test_border_edge_none() {
517        let edge = BorderEdge::none();
518        assert_eq!(edge.style, BorderStyle::None);
519        assert_eq!(edge.width, 0.0);
520    }
521
522    #[test]
523    fn test_border_edge_default() {
524        let edge = BorderEdge::default();
525        assert_eq!(edge.style, BorderStyle::Solid);
526        assert_eq!(edge.width, 1.0);
527    }
528
529    #[test]
530    fn test_border_configuration_new() {
531        let config = BorderConfiguration::new();
532        assert_eq!(config.top.style, BorderStyle::Solid);
533        assert_eq!(config.right.style, BorderStyle::Solid);
534        assert_eq!(config.bottom.style, BorderStyle::Solid);
535        assert_eq!(config.left.style, BorderStyle::Solid);
536    }
537
538    #[test]
539    fn test_border_configuration_uniform() {
540        let edge = BorderEdge::dashed(2.0, Color::red());
541        let config = BorderConfiguration::uniform(edge);
542        assert_eq!(config.top.style, BorderStyle::Dashed);
543        assert_eq!(config.right.width, 2.0);
544        assert_eq!(config.bottom.style, BorderStyle::Dashed);
545    }
546
547    #[test]
548    fn test_border_configuration_edges() {
549        let config = BorderConfiguration::edges(true, false, true, false);
550        assert_eq!(config.top.style, BorderStyle::Solid);
551        assert_eq!(config.right.style, BorderStyle::None);
552        assert_eq!(config.bottom.style, BorderStyle::Solid);
553        assert_eq!(config.left.style, BorderStyle::None);
554    }
555
556    #[test]
557    fn test_border_configuration_none() {
558        let config = BorderConfiguration::none();
559        assert_eq!(config.top.style, BorderStyle::None);
560        assert_eq!(config.right.style, BorderStyle::None);
561        assert_eq!(config.bottom.style, BorderStyle::None);
562        assert_eq!(config.left.style, BorderStyle::None);
563    }
564
565    #[test]
566    fn test_border_configuration_default() {
567        let config = BorderConfiguration::default();
568        assert_eq!(config.top.style, BorderStyle::Solid);
569    }
570
571    #[test]
572    fn test_cell_style_new() {
573        let style = CellStyle::new();
574        assert!(style.background_color.is_none());
575        assert!(style.text_color.is_some());
576        assert_eq!(style.font_size, Some(12.0));
577        assert!(style.text_wrap);
578    }
579
580    #[test]
581    fn test_cell_style_background_color() {
582        let style = CellStyle::new().background_color(Color::yellow());
583        assert!(style.background_color.is_some());
584    }
585
586    #[test]
587    fn test_cell_style_text_color() {
588        let style = CellStyle::new().text_color(Color::blue());
589        assert!(style.text_color.is_some());
590    }
591
592    #[test]
593    fn test_cell_style_font() {
594        let style = CellStyle::new().font(Font::CourierBold);
595        assert_eq!(style.font, Some(Font::CourierBold));
596    }
597
598    #[test]
599    fn test_cell_style_font_size() {
600        let style = CellStyle::new().font_size(18.0);
601        assert_eq!(style.font_size, Some(18.0));
602    }
603
604    #[test]
605    fn test_cell_style_padding() {
606        let style = CellStyle::new().padding(Padding::uniform(20.0));
607        assert_eq!(style.padding.top, 20.0);
608    }
609
610    #[test]
611    fn test_cell_style_alignment() {
612        let style = CellStyle::new().alignment(CellAlignment::Center);
613        assert_eq!(style.alignment, CellAlignment::Center);
614    }
615
616    #[test]
617    fn test_cell_style_border_config() {
618        let config = BorderConfiguration::none();
619        let style = CellStyle::new().border_config(config);
620        assert_eq!(style.border.top.style, BorderStyle::None);
621    }
622
623    #[test]
624    fn test_cell_style_border() {
625        let style = CellStyle::new().border(BorderStyle::Dashed, 2.0, Color::red());
626        assert_eq!(style.border_style, BorderStyle::Dashed);
627        assert_eq!(style.border.top.width, 2.0);
628    }
629
630    #[test]
631    fn test_cell_style_text_wrap() {
632        let style = CellStyle::new().text_wrap(false);
633        assert!(!style.text_wrap);
634    }
635
636    #[test]
637    fn test_cell_style_min_height() {
638        let style = CellStyle::new().min_height(50.0);
639        assert_eq!(style.min_height, Some(50.0));
640    }
641
642    #[test]
643    fn test_cell_style_max_height() {
644        let style = CellStyle::new().max_height(100.0);
645        assert_eq!(style.max_height, Some(100.0));
646    }
647
648    #[test]
649    fn test_cell_style_header() {
650        let style = CellStyle::header();
651        assert_eq!(style.font, Some(Font::HelveticaBold));
652        assert_eq!(style.font_size, Some(14.0));
653        assert_eq!(style.alignment, CellAlignment::Center);
654        assert!(style.background_color.is_some());
655    }
656
657    #[test]
658    fn test_cell_style_data() {
659        let style = CellStyle::data();
660        assert_eq!(style.font, Some(Font::Helvetica));
661        assert_eq!(style.font_size, Some(12.0));
662        assert_eq!(style.alignment, CellAlignment::Left);
663    }
664
665    #[test]
666    fn test_cell_style_numeric() {
667        let style = CellStyle::numeric();
668        assert_eq!(style.font, Some(Font::Courier));
669        assert_eq!(style.font_size, Some(11.0));
670        assert_eq!(style.alignment, CellAlignment::Right);
671    }
672
673    #[test]
674    fn test_cell_style_alternating() {
675        let style = CellStyle::alternating();
676        assert!(style.background_color.is_some());
677        assert_eq!(style.alignment, CellAlignment::Left);
678    }
679
680    #[test]
681    fn test_cell_style_default() {
682        let style = CellStyle::default();
683        assert_eq!(style.font_size, Some(12.0));
684    }
685
686    #[test]
687    fn test_border_style_variants() {
688        let styles = vec![
689            BorderStyle::None,
690            BorderStyle::Solid,
691            BorderStyle::Dashed,
692            BorderStyle::Dotted,
693            BorderStyle::Double,
694        ];
695
696        for style in styles {
697            let cloned = style;
698            assert_eq!(style, cloned);
699        }
700    }
701
702    #[test]
703    fn test_cell_alignment_variants() {
704        let alignments = vec![
705            CellAlignment::Left,
706            CellAlignment::Center,
707            CellAlignment::Right,
708            CellAlignment::Justify,
709        ];
710
711        for alignment in alignments {
712            let cloned = alignment;
713            assert_eq!(alignment, cloned);
714        }
715    }
716}