oxidize_pdf/structure/
outline.rs

1//! Document outline (bookmarks) according to ISO 32000-1 Section 12.3.3
2
3use crate::graphics::Color;
4use crate::objects::{Array, Dictionary, Object, ObjectId};
5use crate::structure::destination::Destination;
6use std::collections::VecDeque;
7
8/// Outline item flags
9#[derive(Debug, Clone, Copy, Default)]
10pub struct OutlineFlags {
11    /// Italic text
12    pub italic: bool,
13    /// Bold text
14    pub bold: bool,
15}
16
17impl OutlineFlags {
18    /// Convert to integer flags
19    #[allow(clippy::wrong_self_convention)]
20    pub fn to_int(&self) -> i64 {
21        let mut flags = 0;
22        if self.italic {
23            flags |= 1;
24        }
25        if self.bold {
26            flags |= 2;
27        }
28        flags
29    }
30}
31
32/// Outline item (bookmark)
33#[derive(Debug, Clone)]
34pub struct OutlineItem {
35    /// Item title
36    pub title: String,
37    /// Destination
38    pub destination: Option<Destination>,
39    /// Child items
40    pub children: Vec<OutlineItem>,
41    /// Text color
42    pub color: Option<Color>,
43    /// Text style flags
44    pub flags: OutlineFlags,
45    /// Whether item is open by default
46    pub open: bool,
47}
48
49impl OutlineItem {
50    /// Create new outline item
51    pub fn new(title: impl Into<String>) -> Self {
52        Self {
53            title: title.into(),
54            destination: None,
55            children: Vec::new(),
56            color: None,
57            flags: OutlineFlags::default(),
58            open: true,
59        }
60    }
61
62    /// Set destination
63    pub fn with_destination(mut self, dest: Destination) -> Self {
64        self.destination = Some(dest);
65        self
66    }
67
68    /// Add child item
69    pub fn add_child(&mut self, child: OutlineItem) {
70        self.children.push(child);
71    }
72
73    /// Set color
74    pub fn with_color(mut self, color: Color) -> Self {
75        self.color = Some(color);
76        self
77    }
78
79    /// Set bold
80    pub fn bold(mut self) -> Self {
81        self.flags.bold = true;
82        self
83    }
84
85    /// Set italic
86    pub fn italic(mut self) -> Self {
87        self.flags.italic = true;
88        self
89    }
90
91    /// Set closed by default
92    pub fn closed(mut self) -> Self {
93        self.open = false;
94        self
95    }
96
97    /// Count total items in subtree
98    pub fn count_all(&self) -> i64 {
99        let mut count = 1; // Self
100        for child in &self.children {
101            count += child.count_all();
102        }
103        count
104    }
105
106    /// Count visible items (respecting open/closed state)
107    pub fn count_visible(&self) -> i64 {
108        let mut count = 1; // Self
109        if self.open {
110            for child in &self.children {
111                count += child.count_visible();
112            }
113        }
114        count
115    }
116}
117
118/// Outline tree structure
119pub struct OutlineTree {
120    /// Root items
121    pub items: Vec<OutlineItem>,
122}
123
124impl Default for OutlineTree {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl OutlineTree {
131    /// Create new outline tree
132    pub fn new() -> Self {
133        Self { items: Vec::new() }
134    }
135
136    /// Add root item
137    pub fn add_item(&mut self, item: OutlineItem) {
138        self.items.push(item);
139    }
140
141    /// Get total item count
142    pub fn total_count(&self) -> i64 {
143        self.items.iter().map(|item| item.count_all()).sum()
144    }
145
146    /// Get visible item count
147    pub fn visible_count(&self) -> i64 {
148        self.items.iter().map(|item| item.count_visible()).sum()
149    }
150}
151
152/// Outline builder for creating outline hierarchy
153pub struct OutlineBuilder {
154    /// Current outline tree
155    tree: OutlineTree,
156    /// Stack for building hierarchy
157    stack: VecDeque<OutlineItem>,
158}
159
160impl Default for OutlineBuilder {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl OutlineBuilder {
167    /// Create new builder
168    pub fn new() -> Self {
169        Self {
170            tree: OutlineTree::new(),
171            stack: VecDeque::new(),
172        }
173    }
174
175    /// Add item at current level
176    pub fn add_item(&mut self, item: OutlineItem) {
177        if let Some(parent) = self.stack.back_mut() {
178            parent.add_child(item);
179        } else {
180            self.tree.add_item(item);
181        }
182    }
183
184    /// Push item and make it current parent
185    pub fn push_item(&mut self, item: OutlineItem) {
186        self.stack.push_back(item);
187    }
188
189    /// Pop current parent and add to tree
190    pub fn pop_item(&mut self) {
191        if let Some(item) = self.stack.pop_back() {
192            if let Some(parent) = self.stack.back_mut() {
193                parent.add_child(item);
194            } else {
195                self.tree.add_item(item);
196            }
197        }
198    }
199
200    /// Build the outline tree
201    pub fn build(mut self) -> OutlineTree {
202        // Pop any remaining items
203        while !self.stack.is_empty() {
204            self.pop_item();
205        }
206        self.tree
207    }
208}
209
210/// Convert outline item to dictionary (for PDF generation)
211pub fn outline_item_to_dict(
212    item: &OutlineItem,
213    parent_ref: ObjectId,
214    first_ref: Option<ObjectId>,
215    last_ref: Option<ObjectId>,
216    prev_ref: Option<ObjectId>,
217    next_ref: Option<ObjectId>,
218) -> Dictionary {
219    let mut dict = Dictionary::new();
220
221    // Title
222    dict.set("Title", Object::String(item.title.clone()));
223
224    // Parent
225    dict.set("Parent", Object::Reference(parent_ref));
226
227    // Siblings
228    if let Some(prev) = prev_ref {
229        dict.set("Prev", Object::Reference(prev));
230    }
231    if let Some(next) = next_ref {
232        dict.set("Next", Object::Reference(next));
233    }
234
235    // Children
236    if !item.children.is_empty() {
237        if let Some(first) = first_ref {
238            dict.set("First", Object::Reference(first));
239        }
240        if let Some(last) = last_ref {
241            dict.set("Last", Object::Reference(last));
242        }
243
244        // Count (negative if closed)
245        let count = if item.open {
246            item.count_visible() - 1 // Exclude self
247        } else {
248            item.count_all() - 1 // For closed items, count all children
249        };
250        dict.set(
251            "Count",
252            Object::Integer(if item.open { count } else { -count }),
253        );
254    }
255
256    // Destination
257    if let Some(dest) = &item.destination {
258        dict.set("Dest", Object::Array(dest.to_array().into()));
259    }
260
261    // Color
262    if let Some(color) = &item.color {
263        let color_array = match color {
264            Color::Rgb(r, g, b) => {
265                Array::from(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
266            }
267            Color::Gray(g) => {
268                Array::from(vec![Object::Real(*g), Object::Real(*g), Object::Real(*g)])
269            }
270            Color::Cmyk(c, m, y, k) => {
271                // Convert CMYK to RGB approximation for outline color
272                let r = (1.0 - c) * (1.0 - k);
273                let g = (1.0 - m) * (1.0 - k);
274                let b = (1.0 - y) * (1.0 - k);
275                Array::from(vec![Object::Real(r), Object::Real(g), Object::Real(b)])
276            }
277        };
278        dict.set("C", Object::Array(color_array.into()));
279    }
280
281    // Flags
282    let flags = item.flags.to_int();
283    if flags != 0 {
284        dict.set("F", Object::Integer(flags));
285    }
286
287    dict
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::structure::destination::PageDestination;
294
295    #[test]
296    fn test_outline_item_new() {
297        let item = OutlineItem::new("Chapter 1");
298        assert_eq!(item.title, "Chapter 1");
299        assert!(item.destination.is_none());
300        assert!(item.children.is_empty());
301        assert!(item.color.is_none());
302        assert!(!item.flags.bold);
303        assert!(!item.flags.italic);
304        assert!(item.open);
305    }
306
307    #[test]
308    fn test_outline_item_builder() {
309        let dest = Destination::fit(PageDestination::PageNumber(0));
310        let item = OutlineItem::new("Bold Chapter")
311            .with_destination(dest)
312            .with_color(Color::rgb(1.0, 0.0, 0.0))
313            .bold()
314            .closed();
315
316        assert!(item.destination.is_some());
317        assert!(item.color.is_some());
318        assert!(item.flags.bold);
319        assert!(!item.open);
320    }
321
322    #[test]
323    fn test_outline_hierarchy() {
324        let mut chapter1 = OutlineItem::new("Chapter 1");
325        chapter1.add_child(OutlineItem::new("Section 1.1"));
326        chapter1.add_child(OutlineItem::new("Section 1.2"));
327
328        assert_eq!(chapter1.children.len(), 2);
329        assert_eq!(chapter1.count_all(), 3); // Chapter + 2 sections
330    }
331
332    #[test]
333    fn test_outline_count() {
334        let mut root = OutlineItem::new("Book");
335
336        let mut ch1 = OutlineItem::new("Chapter 1");
337        ch1.add_child(OutlineItem::new("Section 1.1"));
338        ch1.add_child(OutlineItem::new("Section 1.2"));
339
340        let mut ch2 = OutlineItem::new("Chapter 2").closed();
341        ch2.add_child(OutlineItem::new("Section 2.1"));
342
343        root.add_child(ch1);
344        root.add_child(ch2);
345
346        assert_eq!(root.count_all(), 6); // Book + 2 chapters + 3 sections
347        assert_eq!(root.count_visible(), 5); // Ch2's child hidden
348    }
349
350    #[test]
351    fn test_outline_builder() {
352        let mut builder = OutlineBuilder::new();
353
354        // Add root items
355        builder.add_item(OutlineItem::new("Preface"));
356
357        // Add chapter with sections
358        builder.push_item(OutlineItem::new("Chapter 1"));
359        builder.add_item(OutlineItem::new("Section 1.1"));
360        builder.add_item(OutlineItem::new("Section 1.2"));
361        builder.pop_item();
362
363        builder.add_item(OutlineItem::new("Chapter 2"));
364
365        let tree = builder.build();
366        assert_eq!(tree.items.len(), 3); // Preface, Ch1, Ch2
367        assert_eq!(tree.total_count(), 5); // All items
368    }
369
370    #[test]
371    fn test_outline_flags() {
372        let flags = OutlineFlags {
373            italic: true,
374            bold: true,
375        };
376        assert_eq!(flags.to_int(), 3);
377
378        let flags2 = OutlineFlags {
379            italic: true,
380            bold: false,
381        };
382        assert_eq!(flags2.to_int(), 1);
383
384        let flags3 = OutlineFlags::default();
385        assert_eq!(flags3.to_int(), 0);
386    }
387
388    #[test]
389    fn test_outline_flags_debug_clone_default() {
390        let flags = OutlineFlags {
391            italic: true,
392            bold: false,
393        };
394        let debug_str = format!("{flags:?}");
395        assert!(debug_str.contains("OutlineFlags"));
396        assert!(debug_str.contains("italic: true"));
397        assert!(debug_str.contains("bold: false"));
398
399        let cloned = flags;
400        assert_eq!(cloned.italic, flags.italic);
401        assert_eq!(cloned.bold, flags.bold);
402
403        let default_flags = OutlineFlags::default();
404        assert!(!default_flags.italic);
405        assert!(!default_flags.bold);
406    }
407
408    #[test]
409    fn test_outline_item_italic() {
410        let item = OutlineItem::new("Italic Text").italic();
411        assert!(item.flags.italic);
412        assert!(!item.flags.bold);
413    }
414
415    #[test]
416    fn test_outline_item_bold_italic() {
417        let item = OutlineItem::new("Bold Italic").bold().italic();
418        assert!(item.flags.italic);
419        assert!(item.flags.bold);
420        assert_eq!(item.flags.to_int(), 3);
421    }
422
423    #[test]
424    fn test_outline_item_with_complex_destination() {
425        use crate::geometry::{Point, Rectangle};
426
427        let dest = Destination::fit_r(
428            PageDestination::PageNumber(5),
429            Rectangle::new(Point::new(100.0, 200.0), Point::new(300.0, 400.0)),
430        );
431        let item = OutlineItem::new("Complex Destination").with_destination(dest.clone());
432
433        assert!(item.destination.is_some());
434        match &item.destination {
435            Some(d) => match &d.page {
436                PageDestination::PageNumber(n) => assert_eq!(*n, 5),
437                _ => panic!("Wrong destination type"),
438            },
439            None => panic!("Destination should be set"),
440        }
441    }
442
443    #[test]
444    fn test_outline_item_with_different_colors() {
445        let rgb_item = OutlineItem::new("RGB Color").with_color(Color::rgb(0.5, 0.7, 1.0));
446        assert!(rgb_item.color.is_some());
447
448        let gray_item = OutlineItem::new("Gray Color").with_color(Color::gray(0.5));
449        assert!(gray_item.color.is_some());
450
451        let cmyk_item = OutlineItem::new("CMYK Color").with_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
452        assert!(cmyk_item.color.is_some());
453    }
454
455    #[test]
456    fn test_outline_item_debug_clone() {
457        let item = OutlineItem::new("Test Item")
458            .bold()
459            .with_color(Color::rgb(1.0, 0.0, 0.0));
460
461        let debug_str = format!("{item:?}");
462        assert!(debug_str.contains("OutlineItem"));
463        assert!(debug_str.contains("Test Item"));
464
465        let cloned = item.clone();
466        assert_eq!(cloned.title, item.title);
467        assert_eq!(cloned.flags.bold, item.flags.bold);
468        assert_eq!(cloned.open, item.open);
469    }
470
471    #[test]
472    fn test_outline_tree_default() {
473        let tree = OutlineTree::default();
474        assert!(tree.items.is_empty());
475        assert_eq!(tree.total_count(), 0);
476        assert_eq!(tree.visible_count(), 0);
477    }
478
479    #[test]
480    fn test_outline_tree_add_multiple_items() {
481        let mut tree = OutlineTree::new();
482
483        tree.add_item(OutlineItem::new("First"));
484        tree.add_item(OutlineItem::new("Second"));
485        tree.add_item(OutlineItem::new("Third"));
486
487        assert_eq!(tree.items.len(), 3);
488        assert_eq!(tree.total_count(), 3);
489        assert_eq!(tree.visible_count(), 3);
490    }
491
492    #[test]
493    fn test_outline_tree_with_closed_items() {
494        let mut tree = OutlineTree::new();
495
496        let mut chapter = OutlineItem::new("Chapter").closed();
497        chapter.add_child(OutlineItem::new("Hidden Section 1"));
498        chapter.add_child(OutlineItem::new("Hidden Section 2"));
499
500        tree.add_item(chapter);
501        tree.add_item(OutlineItem::new("Visible Item"));
502
503        assert_eq!(tree.total_count(), 4); // All items
504        assert_eq!(tree.visible_count(), 2); // Only chapter and visible item
505    }
506
507    #[test]
508    fn test_outline_builder_default() {
509        let builder = OutlineBuilder::default();
510        let tree = builder.build();
511        assert!(tree.items.is_empty());
512    }
513
514    #[test]
515    fn test_outline_builder_nested_structure() {
516        let mut builder = OutlineBuilder::new();
517
518        // Build a complex nested structure
519        builder.push_item(OutlineItem::new("Part I"));
520        builder.push_item(OutlineItem::new("Chapter 1"));
521        builder.add_item(OutlineItem::new("Section 1.1"));
522        builder.add_item(OutlineItem::new("Section 1.2"));
523        builder.pop_item(); // Pop Chapter 1
524        builder.push_item(OutlineItem::new("Chapter 2"));
525        builder.add_item(OutlineItem::new("Section 2.1"));
526        builder.pop_item(); // Pop Chapter 2
527        builder.pop_item(); // Pop Part I
528
529        builder.add_item(OutlineItem::new("Part II"));
530
531        let tree = builder.build();
532        assert_eq!(tree.items.len(), 2); // Part I and Part II
533        assert_eq!(tree.total_count(), 7); // All items
534    }
535
536    #[test]
537    fn test_outline_builder_auto_pop() {
538        let mut builder = OutlineBuilder::new();
539
540        // Push items without popping - should auto-pop on build
541        builder.push_item(OutlineItem::new("Root"));
542        builder.push_item(OutlineItem::new("Child"));
543        builder.add_item(OutlineItem::new("Grandchild"));
544
545        let tree = builder.build();
546        assert_eq!(tree.items.len(), 1); // Only root
547        assert_eq!(tree.total_count(), 3); // Root + Child + Grandchild
548    }
549
550    #[test]
551    fn test_outline_item_count_deep_hierarchy() {
552        let mut root = OutlineItem::new("Root");
553
554        let mut level1 = OutlineItem::new("Level 1");
555        let mut level2 = OutlineItem::new("Level 2");
556        let mut level3 = OutlineItem::new("Level 3");
557        level3.add_child(OutlineItem::new("Level 4"));
558        level2.add_child(level3);
559        level1.add_child(level2);
560        root.add_child(level1);
561
562        assert_eq!(root.count_all(), 5); // All 5 levels
563        assert_eq!(root.count_visible(), 5); // All visible
564
565        // Close level2 - should hide level 3 and 4
566        root.children[0].children[0].open = false;
567        assert_eq!(root.count_visible(), 3); // Root, Level1, Level2 (closed)
568    }
569
570    #[test]
571    fn test_outline_item_to_dict_basic() {
572        let item = OutlineItem::new("Test Title");
573        let parent_ref = ObjectId::new(1, 0);
574
575        let dict = outline_item_to_dict(&item, parent_ref, None, None, None, None);
576
577        assert_eq!(
578            dict.get("Title"),
579            Some(&Object::String("Test Title".to_string()))
580        );
581        assert_eq!(dict.get("Parent"), Some(&Object::Reference(parent_ref)));
582        assert!(dict.get("Prev").is_none());
583        assert!(dict.get("Next").is_none());
584        assert!(dict.get("First").is_none());
585        assert!(dict.get("Last").is_none());
586    }
587
588    #[test]
589    fn test_outline_item_to_dict_with_siblings() {
590        let item = OutlineItem::new("Middle Child");
591        let parent_ref = ObjectId::new(1, 0);
592        let prev_ref = Some(ObjectId::new(2, 0));
593        let next_ref = Some(ObjectId::new(3, 0));
594
595        let dict = outline_item_to_dict(&item, parent_ref, None, None, prev_ref, next_ref);
596
597        assert_eq!(
598            dict.get("Prev"),
599            Some(&Object::Reference(ObjectId::new(2, 0)))
600        );
601        assert_eq!(
602            dict.get("Next"),
603            Some(&Object::Reference(ObjectId::new(3, 0)))
604        );
605    }
606
607    #[test]
608    fn test_outline_item_to_dict_with_children() {
609        let mut item = OutlineItem::new("Parent");
610        item.add_child(OutlineItem::new("Child 1"));
611        item.add_child(OutlineItem::new("Child 2"));
612
613        let parent_ref = ObjectId::new(1, 0);
614        let first_ref = Some(ObjectId::new(10, 0));
615        let last_ref = Some(ObjectId::new(11, 0));
616
617        let dict = outline_item_to_dict(&item, parent_ref, first_ref, last_ref, None, None);
618
619        assert_eq!(
620            dict.get("First"),
621            Some(&Object::Reference(ObjectId::new(10, 0)))
622        );
623        assert_eq!(
624            dict.get("Last"),
625            Some(&Object::Reference(ObjectId::new(11, 0)))
626        );
627        assert_eq!(dict.get("Count"), Some(&Object::Integer(2))); // 2 visible children
628    }
629
630    #[test]
631    fn test_outline_item_to_dict_closed_with_children() {
632        let mut item = OutlineItem::new("Closed Parent").closed();
633        item.add_child(OutlineItem::new("Hidden 1"));
634        item.add_child(OutlineItem::new("Hidden 2"));
635        item.add_child(OutlineItem::new("Hidden 3"));
636
637        let dict = outline_item_to_dict(
638            &item,
639            ObjectId::new(1, 0),
640            Some(ObjectId::new(10, 0)),
641            Some(ObjectId::new(12, 0)),
642            None,
643            None,
644        );
645
646        // Count should be negative for closed items
647        assert_eq!(dict.get("Count"), Some(&Object::Integer(-3)));
648    }
649
650    #[test]
651    fn test_outline_item_to_dict_with_destination() {
652        let dest = Destination::xyz(
653            PageDestination::PageNumber(5),
654            Some(100.0),
655            Some(200.0),
656            Some(1.5),
657        );
658        let item = OutlineItem::new("With Destination").with_destination(dest);
659
660        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
661
662        assert!(dict.get("Dest").is_some());
663        match dict.get("Dest") {
664            Some(Object::Array(arr)) => {
665                // Should be the destination array
666                assert!(!arr.is_empty());
667            }
668            _ => panic!("Dest should be an array"),
669        }
670    }
671
672    #[test]
673    fn test_outline_item_to_dict_with_color_rgb() {
674        let item = OutlineItem::new("Red Item").with_color(Color::rgb(1.0, 0.0, 0.0));
675
676        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
677
678        match dict.get("C") {
679            Some(Object::Array(arr)) => {
680                assert_eq!(arr.len(), 3);
681                assert_eq!(arr.first(), Some(&Object::Real(1.0)));
682                assert_eq!(arr.get(1), Some(&Object::Real(0.0)));
683                assert_eq!(arr.get(2), Some(&Object::Real(0.0)));
684            }
685            _ => panic!("C should be an array"),
686        }
687    }
688
689    #[test]
690    fn test_outline_item_to_dict_with_color_gray() {
691        let item = OutlineItem::new("Gray Item").with_color(Color::gray(0.5));
692
693        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
694
695        match dict.get("C") {
696            Some(Object::Array(arr)) => {
697                assert_eq!(arr.len(), 3);
698                // Gray color should be converted to RGB with equal components
699                assert_eq!(arr.first(), Some(&Object::Real(0.5)));
700                assert_eq!(arr.get(1), Some(&Object::Real(0.5)));
701                assert_eq!(arr.get(2), Some(&Object::Real(0.5)));
702            }
703            _ => panic!("C should be an array"),
704        }
705    }
706
707    #[test]
708    fn test_outline_item_to_dict_with_color_cmyk() {
709        let item = OutlineItem::new("CMYK Item").with_color(Color::cmyk(0.0, 1.0, 1.0, 0.0));
710
711        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
712
713        match dict.get("C") {
714            Some(Object::Array(arr)) => {
715                assert_eq!(arr.len(), 3);
716                // CMYK (0,1,1,0) should convert to RGB (1,0,0) - red
717                assert_eq!(arr.first(), Some(&Object::Real(1.0)));
718                assert_eq!(arr.get(1), Some(&Object::Real(0.0)));
719                assert_eq!(arr.get(2), Some(&Object::Real(0.0)));
720            }
721            _ => panic!("C should be an array"),
722        }
723    }
724
725    #[test]
726    fn test_outline_item_to_dict_with_flags() {
727        let item = OutlineItem::new("Styled Item").bold().italic();
728
729        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
730
731        assert_eq!(dict.get("F"), Some(&Object::Integer(3))); // Both bold and italic
732    }
733
734    #[test]
735    fn test_outline_item_to_dict_no_flags() {
736        let item = OutlineItem::new("Plain Item");
737
738        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
739
740        // F field should not be present when flags are 0
741        assert!(dict.get("F").is_none());
742    }
743
744    #[test]
745    fn test_outline_tree_empty_counts() {
746        let tree = OutlineTree::new();
747        assert_eq!(tree.total_count(), 0);
748        assert_eq!(tree.visible_count(), 0);
749    }
750
751    #[test]
752    fn test_outline_builder_empty_pop() {
753        let mut builder = OutlineBuilder::new();
754        // Popping from empty stack should not panic
755        builder.pop_item();
756        let tree = builder.build();
757        assert!(tree.items.is_empty());
758    }
759
760    #[test]
761    fn test_outline_complex_visibility() {
762        let mut root = OutlineItem::new("Book");
763
764        let mut part1 = OutlineItem::new("Part 1"); // open
765        let mut ch1 = OutlineItem::new("Chapter 1").closed();
766        ch1.add_child(OutlineItem::new("Section 1.1"));
767        ch1.add_child(OutlineItem::new("Section 1.2"));
768        part1.add_child(ch1);
769
770        let mut ch2 = OutlineItem::new("Chapter 2"); // open
771        ch2.add_child(OutlineItem::new("Section 2.1"));
772        part1.add_child(ch2);
773
774        root.add_child(part1);
775
776        // Structure:
777        // Book (open)
778        //   Part 1 (open)
779        //     Chapter 1 (closed)
780        //       Section 1.1 (hidden)
781        //       Section 1.2 (hidden)
782        //     Chapter 2 (open)
783        //       Section 2.1 (visible)
784
785        assert_eq!(root.count_all(), 7); // All items
786        assert_eq!(root.count_visible(), 5); // Hidden: Section 1.1, 1.2
787    }
788}