1use crate::error::PdfError;
6use crate::graphics::Color;
7use crate::page::Page;
8use crate::text::{BulletStyle, Font, ListOptions, OrderedList, OrderedListStyle, UnorderedList};
9
10pub trait PageLists {
12 fn add_ordered_list(
14 &mut self,
15 list: &OrderedList,
16 x: f64,
17 y: f64,
18 ) -> Result<&mut Self, PdfError>;
19
20 fn add_unordered_list(
22 &mut self,
23 list: &UnorderedList,
24 x: f64,
25 y: f64,
26 ) -> Result<&mut Self, PdfError>;
27
28 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 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 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 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#[derive(Debug, Clone)]
67pub struct ListStyle {
68 pub list_type: ListType,
70 pub font: Font,
72 pub font_size: f64,
74 pub text_color: Color,
76 pub marker_color: Option<Color>,
78 pub max_width: Option<f64>,
80 pub line_spacing: f64,
82 pub indent: f64,
84 pub paragraph_spacing: f64,
86 pub draw_separator: bool,
88}
89
90#[derive(Debug, Clone, Copy)]
92pub enum ListType {
93 Ordered(OrderedListStyle),
95 Unordered(BulletStyle),
97}
98
99impl ListStyle {
100 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 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 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 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 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 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 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 #[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 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 #[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 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 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 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 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 #[test]
571 fn test_page_lists_trait() {
572 let mut page = Page::a4();
573
574 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 let style = ListStyle::checklist(); 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 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}