Skip to main content

oxidize_pdf/
page_lists.rs

1//! Page extension for list rendering
2//!
3//! This module provides traits and implementations to easily add lists to PDF pages.
4
5use crate::error::PdfError;
6use crate::graphics::Color;
7use crate::page::Page;
8use crate::text::{BulletStyle, Font, ListOptions, OrderedList, OrderedListStyle, UnorderedList};
9
10/// Extension trait for adding lists to pages
11pub trait PageLists {
12    /// Add an ordered list to the page
13    fn add_ordered_list(
14        &mut self,
15        list: &OrderedList,
16        x: f64,
17        y: f64,
18    ) -> Result<&mut Self, PdfError>;
19
20    /// Add an unordered list to the page
21    fn add_unordered_list(
22        &mut self,
23        list: &UnorderedList,
24        x: f64,
25        y: f64,
26    ) -> Result<&mut Self, PdfError>;
27
28    /// Create and add a quick ordered list with default styling
29    fn add_quick_ordered_list(
30        &mut self,
31        items: Vec<String>,
32        x: f64,
33        y: f64,
34        style: OrderedListStyle,
35    ) -> Result<&mut Self, PdfError>;
36
37    /// Create and add a quick unordered list with default styling
38    fn add_quick_unordered_list(
39        &mut self,
40        items: Vec<String>,
41        x: f64,
42        y: f64,
43        bullet: BulletStyle,
44    ) -> Result<&mut Self, PdfError>;
45
46    /// Create and add a styled ordered list
47    fn add_styled_ordered_list(
48        &mut self,
49        items: Vec<String>,
50        x: f64,
51        y: f64,
52        style: ListStyle,
53    ) -> Result<&mut Self, PdfError>;
54
55    /// Create and add a styled unordered list
56    fn add_styled_unordered_list(
57        &mut self,
58        items: Vec<String>,
59        x: f64,
60        y: f64,
61        style: ListStyle,
62    ) -> Result<&mut Self, PdfError>;
63}
64
65/// Predefined list styles
66#[derive(Debug, Clone)]
67pub struct ListStyle {
68    /// List type
69    pub list_type: ListType,
70    /// Font for text
71    pub font: Font,
72    /// Font size
73    pub font_size: f64,
74    /// Text color
75    pub text_color: Color,
76    /// Marker color (None = same as text)
77    pub marker_color: Option<Color>,
78    /// Maximum width for text wrapping
79    pub max_width: Option<f64>,
80    /// Line spacing multiplier
81    pub line_spacing: f64,
82    /// Indentation per level
83    pub indent: f64,
84    /// Paragraph spacing after items
85    pub paragraph_spacing: f64,
86    /// Whether to draw separators
87    pub draw_separator: bool,
88}
89
90/// List type for styling
91#[derive(Debug, Clone, Copy)]
92pub enum ListType {
93    /// Ordered list with specific style
94    Ordered(OrderedListStyle),
95    /// Unordered list with specific bullet
96    Unordered(BulletStyle),
97}
98
99impl ListStyle {
100    /// Create a minimal list style
101    pub fn minimal(list_type: ListType) -> Self {
102        Self {
103            list_type,
104            font: Font::Helvetica,
105            font_size: 10.0,
106            text_color: Color::black(),
107            marker_color: None,
108            max_width: None,
109            line_spacing: 1.2,
110            indent: 20.0,
111            paragraph_spacing: 0.0,
112            draw_separator: false,
113        }
114    }
115
116    /// Create a professional list style
117    pub fn professional(list_type: ListType) -> Self {
118        Self {
119            list_type,
120            font: Font::Helvetica,
121            font_size: 11.0,
122            text_color: Color::gray(0.1),
123            marker_color: Some(Color::rgb(0.2, 0.4, 0.7)),
124            max_width: Some(500.0),
125            line_spacing: 1.3,
126            indent: 25.0,
127            paragraph_spacing: 3.0,
128            draw_separator: false,
129        }
130    }
131
132    /// Create a document list style (for formal documents)
133    pub fn document(list_type: ListType) -> Self {
134        Self {
135            list_type,
136            font: Font::TimesRoman,
137            font_size: 12.0,
138            text_color: Color::black(),
139            marker_color: None,
140            max_width: Some(450.0),
141            line_spacing: 1.5,
142            indent: 30.0,
143            paragraph_spacing: 5.0,
144            draw_separator: false,
145        }
146    }
147
148    /// Create a presentation list style
149    pub fn presentation(list_type: ListType) -> Self {
150        Self {
151            list_type,
152            font: Font::HelveticaBold,
153            font_size: 14.0,
154            text_color: Color::gray(0.2),
155            marker_color: Some(Color::rgb(0.8, 0.2, 0.2)),
156            max_width: Some(600.0),
157            line_spacing: 1.6,
158            indent: 35.0,
159            paragraph_spacing: 8.0,
160            draw_separator: false,
161        }
162    }
163
164    /// Create a checklist style (with checkboxes)
165    pub fn checklist() -> Self {
166        Self {
167            list_type: ListType::Unordered(BulletStyle::Square),
168            font: Font::Helvetica,
169            font_size: 11.0,
170            text_color: Color::gray(0.1),
171            marker_color: Some(Color::gray(0.4)),
172            max_width: Some(500.0),
173            line_spacing: 1.4,
174            indent: 25.0,
175            paragraph_spacing: 5.0,
176            draw_separator: true,
177        }
178    }
179}
180
181impl PageLists for Page {
182    fn add_ordered_list(
183        &mut self,
184        list: &OrderedList,
185        x: f64,
186        y: f64,
187    ) -> Result<&mut Self, PdfError> {
188        let mut list_clone = list.clone();
189        list_clone.set_position(x, y);
190        list_clone.render(self.graphics())?;
191        Ok(self)
192    }
193
194    fn add_unordered_list(
195        &mut self,
196        list: &UnorderedList,
197        x: f64,
198        y: f64,
199    ) -> Result<&mut Self, PdfError> {
200        let mut list_clone = list.clone();
201        list_clone.set_position(x, y);
202        list_clone.render(self.graphics())?;
203        Ok(self)
204    }
205
206    fn add_quick_ordered_list(
207        &mut self,
208        items: Vec<String>,
209        x: f64,
210        y: f64,
211        style: OrderedListStyle,
212    ) -> Result<&mut Self, PdfError> {
213        let mut list = OrderedList::new(style);
214        for item in items {
215            list.add_item(item);
216        }
217        self.add_ordered_list(&list, x, y)
218    }
219
220    fn add_quick_unordered_list(
221        &mut self,
222        items: Vec<String>,
223        x: f64,
224        y: f64,
225        bullet: BulletStyle,
226    ) -> Result<&mut Self, PdfError> {
227        let mut list = UnorderedList::new(bullet);
228        for item in items {
229            list.add_item(item);
230        }
231        self.add_unordered_list(&list, x, y)
232    }
233
234    fn add_styled_ordered_list(
235        &mut self,
236        items: Vec<String>,
237        x: f64,
238        y: f64,
239        style: ListStyle,
240    ) -> Result<&mut Self, PdfError> {
241        if let ListType::Ordered(ordered_style) = style.list_type {
242            let mut list = OrderedList::new(ordered_style);
243
244            // Apply style options
245            let options = ListOptions {
246                font: style.font,
247                font_size: style.font_size,
248                text_color: style.text_color,
249                marker_color: style.marker_color,
250                max_width: style.max_width,
251                line_spacing: style.line_spacing,
252                indent: style.indent,
253                paragraph_spacing: style.paragraph_spacing,
254                draw_separator: style.draw_separator,
255                ..Default::default()
256            };
257
258            list.set_options(options);
259
260            for item in items {
261                list.add_item(item);
262            }
263
264            self.add_ordered_list(&list, x, y)
265        } else {
266            Err(PdfError::InvalidFormat(
267                "Expected ordered list style".to_string(),
268            ))
269        }
270    }
271
272    fn add_styled_unordered_list(
273        &mut self,
274        items: Vec<String>,
275        x: f64,
276        y: f64,
277        style: ListStyle,
278    ) -> Result<&mut Self, PdfError> {
279        if let ListType::Unordered(bullet_style) = style.list_type {
280            let mut list = UnorderedList::new(bullet_style);
281
282            // Apply style options
283            let options = ListOptions {
284                font: style.font,
285                font_size: style.font_size,
286                text_color: style.text_color,
287                marker_color: style.marker_color,
288                max_width: style.max_width,
289                line_spacing: style.line_spacing,
290                indent: style.indent,
291                paragraph_spacing: style.paragraph_spacing,
292                draw_separator: style.draw_separator,
293                ..Default::default()
294            };
295
296            list.set_options(options);
297
298            for item in items {
299                list.add_item(item);
300            }
301
302            self.add_unordered_list(&list, x, y)
303        } else {
304            Err(PdfError::InvalidFormat(
305                "Expected unordered list style".to_string(),
306            ))
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    // ==================== ListType Tests ====================
316
317    #[test]
318    fn test_list_type_ordered_variants() {
319        let decimal = ListType::Ordered(OrderedListStyle::Decimal);
320        let upper_alpha = ListType::Ordered(OrderedListStyle::UpperAlpha);
321        let lower_alpha = ListType::Ordered(OrderedListStyle::LowerAlpha);
322        let upper_roman = ListType::Ordered(OrderedListStyle::UpperRoman);
323        let lower_roman = ListType::Ordered(OrderedListStyle::LowerRoman);
324
325        // Verify they are distinct
326        if let ListType::Ordered(style) = decimal {
327            assert_eq!(style, OrderedListStyle::Decimal);
328        }
329        if let ListType::Ordered(style) = upper_alpha {
330            assert_eq!(style, OrderedListStyle::UpperAlpha);
331        }
332        if let ListType::Ordered(style) = lower_alpha {
333            assert_eq!(style, OrderedListStyle::LowerAlpha);
334        }
335        if let ListType::Ordered(style) = upper_roman {
336            assert_eq!(style, OrderedListStyle::UpperRoman);
337        }
338        if let ListType::Ordered(style) = lower_roman {
339            assert_eq!(style, OrderedListStyle::LowerRoman);
340        }
341    }
342
343    #[test]
344    fn test_list_type_unordered_variants() {
345        let disc = ListType::Unordered(BulletStyle::Disc);
346        let circle = ListType::Unordered(BulletStyle::Circle);
347        let square = ListType::Unordered(BulletStyle::Square);
348        let dash = ListType::Unordered(BulletStyle::Dash);
349
350        if let ListType::Unordered(style) = disc {
351            assert_eq!(style, BulletStyle::Disc);
352        }
353        if let ListType::Unordered(style) = circle {
354            assert_eq!(style, BulletStyle::Circle);
355        }
356        if let ListType::Unordered(style) = square {
357            assert_eq!(style, BulletStyle::Square);
358        }
359        if let ListType::Unordered(style) = dash {
360            assert_eq!(style, BulletStyle::Dash);
361        }
362    }
363
364    #[test]
365    fn test_list_type_clone() {
366        let original = ListType::Ordered(OrderedListStyle::Decimal);
367        let cloned = original;
368        if let ListType::Ordered(style) = cloned {
369            assert_eq!(style, OrderedListStyle::Decimal);
370        }
371    }
372
373    #[test]
374    fn test_list_type_debug() {
375        let list_type = ListType::Ordered(OrderedListStyle::UpperRoman);
376        let debug_str = format!("{:?}", list_type);
377        assert!(debug_str.contains("Ordered"));
378    }
379
380    // ==================== ListStyle Tests ====================
381
382    #[test]
383    fn test_list_style_minimal_ordered() {
384        let style = ListStyle::minimal(ListType::Ordered(OrderedListStyle::Decimal));
385
386        assert_eq!(style.font, Font::Helvetica);
387        assert_eq!(style.font_size, 10.0);
388        assert_eq!(style.text_color, Color::black());
389        assert!(style.marker_color.is_none());
390        assert!(style.max_width.is_none());
391        assert_eq!(style.line_spacing, 1.2);
392        assert_eq!(style.indent, 20.0);
393        assert_eq!(style.paragraph_spacing, 0.0);
394        assert!(!style.draw_separator);
395
396        if let ListType::Ordered(ordered_style) = style.list_type {
397            assert_eq!(ordered_style, OrderedListStyle::Decimal);
398        } else {
399            panic!("Expected Ordered list type");
400        }
401    }
402
403    #[test]
404    fn test_list_style_minimal_unordered() {
405        let style = ListStyle::minimal(ListType::Unordered(BulletStyle::Circle));
406
407        assert_eq!(style.font, Font::Helvetica);
408        assert_eq!(style.font_size, 10.0);
409
410        if let ListType::Unordered(bullet_style) = style.list_type {
411            assert_eq!(bullet_style, BulletStyle::Circle);
412        } else {
413            panic!("Expected Unordered list type");
414        }
415    }
416
417    #[test]
418    fn test_list_style_professional() {
419        let style = ListStyle::professional(ListType::Ordered(OrderedListStyle::UpperAlpha));
420
421        assert_eq!(style.font, Font::Helvetica);
422        assert_eq!(style.font_size, 11.0);
423        assert_eq!(style.text_color, Color::gray(0.1));
424        assert!(style.marker_color.is_some());
425        assert_eq!(style.max_width, Some(500.0));
426        assert_eq!(style.line_spacing, 1.3);
427        assert_eq!(style.indent, 25.0);
428        assert_eq!(style.paragraph_spacing, 3.0);
429        assert!(!style.draw_separator);
430    }
431
432    #[test]
433    fn test_list_style_professional_marker_color() {
434        let style = ListStyle::professional(ListType::Unordered(BulletStyle::Disc));
435
436        if let Some(color) = style.marker_color {
437            // Professional uses blue-ish color (0.2, 0.4, 0.7)
438            assert!(color.r() < 0.3);
439            assert!(color.g() > 0.3 && color.g() < 0.5);
440            assert!(color.b() > 0.6);
441        } else {
442            panic!("Professional style should have marker color");
443        }
444    }
445
446    #[test]
447    fn test_list_style_document() {
448        let style = ListStyle::document(ListType::Ordered(OrderedListStyle::UpperRoman));
449
450        assert_eq!(style.font, Font::TimesRoman);
451        assert_eq!(style.font_size, 12.0);
452        assert_eq!(style.text_color, Color::black());
453        assert!(style.marker_color.is_none());
454        assert_eq!(style.max_width, Some(450.0));
455        assert_eq!(style.line_spacing, 1.5);
456        assert_eq!(style.indent, 30.0);
457        assert_eq!(style.paragraph_spacing, 5.0);
458        assert!(!style.draw_separator);
459    }
460
461    #[test]
462    fn test_list_style_presentation() {
463        let style = ListStyle::presentation(ListType::Unordered(BulletStyle::Dash));
464
465        assert_eq!(style.font, Font::HelveticaBold);
466        assert_eq!(style.font_size, 14.0);
467        assert_eq!(style.text_color, Color::gray(0.2));
468        assert!(style.marker_color.is_some());
469        assert_eq!(style.max_width, Some(600.0));
470        assert_eq!(style.line_spacing, 1.6);
471        assert_eq!(style.indent, 35.0);
472        assert_eq!(style.paragraph_spacing, 8.0);
473        assert!(!style.draw_separator);
474    }
475
476    #[test]
477    fn test_list_style_presentation_marker_color() {
478        let style = ListStyle::presentation(ListType::Ordered(OrderedListStyle::Decimal));
479
480        if let Some(color) = style.marker_color {
481            // Presentation uses red-ish color (0.8, 0.2, 0.2)
482            assert!(color.r() > 0.7);
483            assert!(color.g() < 0.3);
484            assert!(color.b() < 0.3);
485        } else {
486            panic!("Presentation style should have marker color");
487        }
488    }
489
490    #[test]
491    fn test_list_style_checklist() {
492        let style = ListStyle::checklist();
493
494        assert_eq!(style.font, Font::Helvetica);
495        assert_eq!(style.font_size, 11.0);
496        assert_eq!(style.text_color, Color::gray(0.1));
497        assert!(style.marker_color.is_some());
498        assert_eq!(style.max_width, Some(500.0));
499        assert_eq!(style.line_spacing, 1.4);
500        assert_eq!(style.indent, 25.0);
501        assert_eq!(style.paragraph_spacing, 5.0);
502        assert!(style.draw_separator);
503
504        // Checklist uses Square bullets
505        if let ListType::Unordered(bullet_style) = style.list_type {
506            assert_eq!(bullet_style, BulletStyle::Square);
507        } else {
508            panic!("Checklist should be unordered with Square bullets");
509        }
510    }
511
512    #[test]
513    fn test_list_style_checklist_marker_color() {
514        let style = ListStyle::checklist();
515
516        if let Some(color) = style.marker_color {
517            // Checklist uses gray marker (0.4)
518            assert!(color.r() > 0.3 && color.r() < 0.5);
519            assert!(color.g() > 0.3 && color.g() < 0.5);
520            assert!(color.b() > 0.3 && color.b() < 0.5);
521        } else {
522            panic!("Checklist style should have marker color");
523        }
524    }
525
526    #[test]
527    fn test_list_style_clone() {
528        let original = ListStyle::professional(ListType::Ordered(OrderedListStyle::Decimal));
529        let cloned = original.clone();
530
531        assert_eq!(cloned.font, Font::Helvetica);
532        assert_eq!(cloned.font_size, 11.0);
533        assert_eq!(cloned.indent, 25.0);
534    }
535
536    #[test]
537    fn test_list_style_debug() {
538        let style = ListStyle::minimal(ListType::Ordered(OrderedListStyle::Decimal));
539        let debug_str = format!("{:?}", style);
540        assert!(debug_str.contains("ListStyle"));
541    }
542
543    #[test]
544    fn test_list_style_mutability() {
545        let mut style = ListStyle::minimal(ListType::Ordered(OrderedListStyle::Decimal));
546
547        style.font = Font::CourierBold;
548        style.font_size = 16.0;
549        style.text_color = Color::blue();
550        style.marker_color = Some(Color::red());
551        style.max_width = Some(400.0);
552        style.line_spacing = 2.0;
553        style.indent = 50.0;
554        style.paragraph_spacing = 10.0;
555        style.draw_separator = true;
556
557        assert_eq!(style.font, Font::CourierBold);
558        assert_eq!(style.font_size, 16.0);
559        assert_eq!(style.text_color, Color::blue());
560        assert_eq!(style.marker_color, Some(Color::red()));
561        assert_eq!(style.max_width, Some(400.0));
562        assert_eq!(style.line_spacing, 2.0);
563        assert_eq!(style.indent, 50.0);
564        assert_eq!(style.paragraph_spacing, 10.0);
565        assert!(style.draw_separator);
566    }
567
568    // ==================== Page Integration Tests ====================
569
570    #[test]
571    fn test_page_lists_trait() {
572        let mut page = Page::a4();
573
574        // Test quick ordered list
575        let items = vec![
576            "First item".to_string(),
577            "Second item".to_string(),
578            "Third item".to_string(),
579        ];
580
581        let result = page.add_quick_ordered_list(items, 50.0, 700.0, OrderedListStyle::Decimal);
582        assert!(result.is_ok());
583    }
584
585    #[test]
586    fn test_quick_unordered_list() {
587        let mut page = Page::a4();
588
589        let items = vec![
590            "Apple".to_string(),
591            "Banana".to_string(),
592            "Cherry".to_string(),
593        ];
594
595        let result = page.add_quick_unordered_list(items, 50.0, 700.0, BulletStyle::Disc);
596        assert!(result.is_ok());
597    }
598
599    #[test]
600    fn test_list_styles() {
601        let minimal = ListStyle::minimal(ListType::Ordered(OrderedListStyle::Decimal));
602        assert_eq!(minimal.font_size, 10.0);
603        assert!(minimal.marker_color.is_none());
604
605        let professional = ListStyle::professional(ListType::Unordered(BulletStyle::Circle));
606        assert_eq!(professional.font_size, 11.0);
607        assert!(professional.marker_color.is_some());
608
609        let document = ListStyle::document(ListType::Ordered(OrderedListStyle::UpperRoman));
610        assert_eq!(document.line_spacing, 1.5);
611
612        let presentation = ListStyle::presentation(ListType::Unordered(BulletStyle::Dash));
613        assert_eq!(presentation.font_size, 14.0);
614
615        let checklist = ListStyle::checklist();
616        assert!(checklist.draw_separator);
617    }
618
619    #[test]
620    fn test_styled_lists() {
621        let mut page = Page::a4();
622
623        let items = vec![
624            "Executive Summary".to_string(),
625            "Market Analysis".to_string(),
626            "Financial Projections".to_string(),
627        ];
628
629        let style = ListStyle::professional(ListType::Ordered(OrderedListStyle::UpperAlpha));
630        let result = page.add_styled_ordered_list(items, 50.0, 700.0, style);
631        assert!(result.is_ok());
632    }
633
634    #[test]
635    fn test_empty_list() {
636        let mut page = Page::a4();
637
638        let items: Vec<String> = vec![];
639        let result = page.add_quick_ordered_list(items, 50.0, 700.0, OrderedListStyle::Decimal);
640        assert!(result.is_ok());
641    }
642
643    #[test]
644    fn test_list_with_long_text() {
645        let mut page = Page::a4();
646
647        let items = vec![
648            "This is a very long list item that should wrap to multiple lines when rendered with a maximum width constraint".to_string(),
649            "Short item".to_string(),
650        ];
651
652        let mut style = ListStyle::professional(ListType::Ordered(OrderedListStyle::Decimal));
653        style.max_width = Some(300.0);
654
655        let result = page.add_styled_ordered_list(items, 50.0, 700.0, style);
656        assert!(result.is_ok());
657    }
658
659    #[test]
660    fn test_styled_unordered_list() {
661        let mut page = Page::a4();
662
663        let items = vec!["Red".to_string(), "Green".to_string(), "Blue".to_string()];
664
665        let style = ListStyle::presentation(ListType::Unordered(BulletStyle::Circle));
666        let result = page.add_styled_unordered_list(items, 50.0, 700.0, style);
667        assert!(result.is_ok());
668    }
669
670    #[test]
671    fn test_styled_ordered_with_wrong_type_fails() {
672        let mut page = Page::a4();
673
674        let items = vec!["Item".to_string()];
675
676        // Try to use unordered style with add_styled_ordered_list
677        let style = ListStyle::checklist(); // This is Unordered
678        let result = page.add_styled_ordered_list(items, 50.0, 700.0, style);
679        assert!(result.is_err());
680    }
681
682    #[test]
683    fn test_styled_unordered_with_wrong_type_fails() {
684        let mut page = Page::a4();
685
686        let items = vec!["Item".to_string()];
687
688        // Try to use ordered style with add_styled_unordered_list
689        let style = ListStyle::document(ListType::Ordered(OrderedListStyle::Decimal));
690        let result = page.add_styled_unordered_list(items, 50.0, 700.0, style);
691        assert!(result.is_err());
692    }
693
694    #[test]
695    fn test_all_ordered_list_styles() {
696        let mut page = Page::a4();
697
698        let styles = vec![
699            OrderedListStyle::Decimal,
700            OrderedListStyle::LowerAlpha,
701            OrderedListStyle::UpperAlpha,
702            OrderedListStyle::LowerRoman,
703            OrderedListStyle::UpperRoman,
704        ];
705
706        for style in styles {
707            let items = vec!["A".to_string(), "B".to_string()];
708            let result = page.add_quick_ordered_list(items, 50.0, 700.0, style);
709            assert!(result.is_ok(), "Failed for style {:?}", style);
710        }
711    }
712
713    #[test]
714    fn test_all_bullet_styles() {
715        let mut page = Page::a4();
716
717        let styles = vec![
718            BulletStyle::Disc,
719            BulletStyle::Circle,
720            BulletStyle::Square,
721            BulletStyle::Dash,
722        ];
723
724        for style in styles {
725            let items = vec!["X".to_string(), "Y".to_string()];
726            let result = page.add_quick_unordered_list(items, 50.0, 700.0, style);
727            assert!(result.is_ok(), "Failed for bullet {:?}", style);
728        }
729    }
730}