Skip to main content

fop_layout/layout/
page_break.rs

1//! Page breaking logic
2//!
3//! Splits content across multiple pages when it exceeds available space.
4
5use crate::area::{Area, AreaId, AreaTree, AreaType};
6use fop_types::{Length, Point, Rect, Result, Size};
7
8/// Page breaker - splits content into pages
9pub struct PageBreaker {
10    /// Page width
11    page_width: Length,
12
13    /// Page height
14    page_height: Length,
15
16    /// Content area margins (top, right, bottom, left)
17    margins: [Length; 4],
18}
19
20impl PageBreaker {
21    /// Create a new page breaker
22    pub fn new(page_width: Length, page_height: Length, margins: [Length; 4]) -> Self {
23        Self {
24            page_width,
25            page_height,
26            margins,
27        }
28    }
29
30    /// Calculate available content height
31    pub fn content_height(&self) -> Length {
32        self.page_height - self.margins[0] - self.margins[2] // height - top - bottom
33    }
34
35    /// Calculate available content height accounting for footnotes
36    pub fn content_height_with_footnotes(&self, footnote_height: Length) -> Length {
37        self.content_height() - footnote_height
38    }
39
40    /// Calculate available content width
41    pub fn content_width(&self) -> Length {
42        self.page_width - self.margins[1] - self.margins[3] // width - right - left
43    }
44
45    /// Break an area tree into pages
46    pub fn break_into_pages(
47        &self,
48        area_tree: &mut AreaTree,
49        root_id: AreaId,
50    ) -> Result<Vec<AreaId>> {
51        let mut page_ids = Vec::new();
52        let content_height = self.content_height();
53        let _content_width = self.content_width();
54
55        // Get all block-level children
56        let children = area_tree.children(root_id);
57
58        if children.is_empty() {
59            // No content, create one empty page
60            let page_id = self.create_page(area_tree)?;
61            page_ids.push(page_id);
62            return Ok(page_ids);
63        }
64
65        // Implement multi-page breaking with overflow detection and keep constraints
66        let mut current_page_id = self.create_page(area_tree)?;
67        page_ids.push(current_page_id);
68
69        let mut current_height = Length::ZERO;
70
71        for (idx, child_id) in children.iter().enumerate() {
72            // Extract all needed information before any mutable operations
73            let (child_height, break_before_opt, break_after_opt) =
74                if let Some(child_node) = area_tree.get(*child_id) {
75                    (
76                        child_node.area.height(),
77                        child_node.area.break_before,
78                        child_node.area.break_after,
79                    )
80                } else {
81                    continue;
82                };
83
84            // Check for forced break-before
85            let mut force_break_before = false;
86            let mut need_even_page_before = false;
87            let mut need_odd_page_before = false;
88
89            if let Some(break_before) = break_before_opt {
90                if break_before.forces_page_break() {
91                    force_break_before = true;
92                    need_even_page_before = break_before.requires_even_page();
93                    need_odd_page_before = break_before.requires_odd_page();
94                }
95            }
96
97            // Handle even/odd page requirements before break
98            if need_even_page_before {
99                let current_page_num = page_ids.len();
100                if current_page_num % 2 == 1 {
101                    // Current page is odd, need to insert blank page
102                    current_page_id = self.create_page(area_tree)?;
103                    page_ids.push(current_page_id);
104                }
105            } else if need_odd_page_before {
106                let current_page_num = page_ids.len();
107                if current_page_num % 2 == 0 {
108                    // Current page is even, need to insert blank page
109                    current_page_id = self.create_page(area_tree)?;
110                    page_ids.push(current_page_id);
111                }
112            }
113
114            // Check if we can break before this area
115            let can_break = self.can_break_before(area_tree, *child_id, idx, &children);
116
117            // Check if content overflows current page or forced break
118            if ((current_height + child_height > content_height
119                && current_height > Length::ZERO
120                && can_break)
121                || force_break_before)
122                && current_height > Length::ZERO
123            {
124                // Create new page
125                current_page_id = self.create_page(area_tree)?;
126                page_ids.push(current_page_id);
127                current_height = Length::ZERO;
128            }
129
130            // Add child to current page (in real implementation, would reparent)
131            current_height += child_height;
132
133            // Check for forced break-after
134            if let Some(break_after) = break_after_opt {
135                if break_after.forces_page_break() {
136                    // Create new page for next content
137                    current_page_id = self.create_page(area_tree)?;
138                    page_ids.push(current_page_id);
139                    current_height = Length::ZERO;
140
141                    // Handle even/odd page requirements after break
142                    if break_after.requires_even_page() {
143                        let current_page_num = page_ids.len();
144                        if current_page_num % 2 == 1 {
145                            // Current page is odd, need to insert blank page
146                            current_page_id = self.create_page(area_tree)?;
147                            page_ids.push(current_page_id);
148                        }
149                    } else if break_after.requires_odd_page() {
150                        let current_page_num = page_ids.len();
151                        if current_page_num % 2 == 0 {
152                            // Current page is even, need to insert blank page
153                            current_page_id = self.create_page(area_tree)?;
154                            page_ids.push(current_page_id);
155                        }
156                    }
157                }
158            }
159        }
160
161        Ok(page_ids)
162    }
163
164    /// Check if we can break before an area, respecting keep constraints
165    fn can_break_before(
166        &self,
167        area_tree: &AreaTree,
168        area_id: AreaId,
169        index: usize,
170        all_children: &[AreaId],
171    ) -> bool {
172        // Get the current area
173        let current_area = match area_tree.get(area_id) {
174            Some(node) => &node.area,
175            None => return true, // If area doesn't exist, allow break
176        };
177
178        // Check keep-together constraint (don't split this area)
179        if let Some(constraint) = &current_area.keep_constraint {
180            if constraint.must_keep_together() {
181                // For keep-together, we would need to track if we're in the middle
182                // of splitting. For now, we prevent breaks if area is too large.
183                // This is a simplified implementation.
184                return false;
185            }
186
187            // Check keep-with-previous constraint
188            if constraint.must_keep_with_previous() && index > 0 {
189                // Don't break before this area if it has keep-with-previous
190                return false;
191            }
192        }
193
194        // Check if previous area has keep-with-next constraint
195        if index > 0 {
196            if let Some(prev_area_id) = all_children.get(index - 1) {
197                if let Some(prev_node) = area_tree.get(*prev_area_id) {
198                    if let Some(constraint) = &prev_node.area.keep_constraint {
199                        if constraint.must_keep_with_next() {
200                            // Previous area wants to stay with this one
201                            return false;
202                        }
203                    }
204                }
205            }
206        }
207
208        // Check orphans constraint - minimum lines at bottom of page before break
209        // Count line areas in previous blocks to see if we have enough lines
210        // before this break point
211        let orphans = current_area.orphans;
212        if orphans > 0 && index > 0 {
213            // Count line/text areas in the previous block (the one that would end on current page)
214            if let Some(prev_area_id) = all_children.get(index - 1) {
215                let line_count = self.count_line_areas(area_tree, *prev_area_id);
216                if line_count > 0 && line_count < orphans {
217                    // Not enough lines before the break - would create an orphan
218                    return false;
219                }
220            }
221        }
222
223        // Check widows constraint - minimum lines at top of page after break
224        // Count line areas in the current block to see if we have enough lines
225        // after this break point
226        let widows = current_area.widows;
227        if widows > 0 {
228            let line_count = self.count_line_areas(area_tree, area_id);
229            if line_count > 0 && line_count < widows {
230                // Not enough lines after the break - would create a widow
231                return false;
232            }
233        }
234
235        // No constraints prevent breaking
236        true
237    }
238
239    /// Count the number of line areas within a block area
240    ///
241    /// This is used for widow and orphan control. It counts text and line areas
242    /// that are direct or indirect children of the given area.
243    #[allow(clippy::only_used_in_recursion)]
244    fn count_line_areas(&self, area_tree: &AreaTree, area_id: AreaId) -> i32 {
245        let mut count = 0;
246
247        // Get the area
248        if let Some(node) = area_tree.get(area_id) {
249            // If this is a line or text area, count it
250            if matches!(
251                node.area.area_type,
252                AreaType::Line | AreaType::Text | AreaType::Inline
253            ) {
254                count += 1;
255            }
256
257            // Recursively count in children
258            let children = area_tree.children(area_id);
259            for child_id in children {
260                count += self.count_line_areas(area_tree, child_id);
261            }
262        }
263
264        count
265    }
266
267    /// Create a new page area
268    fn create_page(&self, area_tree: &mut AreaTree) -> Result<AreaId> {
269        // Create page area
270        let page_rect =
271            Rect::from_point_size(Point::ZERO, Size::new(self.page_width, self.page_height));
272        let page_area = Area::new(AreaType::Page, page_rect);
273        let page_id = area_tree.add_area(page_area);
274
275        // Create region-body area (content area with margins)
276        let region_rect = Rect::from_point_size(
277            Point::new(self.margins[3], self.margins[0]), // x = left margin, y = top margin
278            Size::new(self.content_width(), self.content_height()),
279        );
280        let region_area = Area::new(AreaType::Region, region_rect);
281        let region_id = area_tree.add_area(region_area);
282
283        // Attach region to page
284        area_tree
285            .append_child(page_id, region_id)
286            .map_err(fop_types::FopError::Generic)?;
287
288        Ok(page_id)
289    }
290
291    /// Place footnotes at bottom of page with separator line
292    pub fn place_footnotes(&self, area_tree: &mut AreaTree, page_id: AreaId) -> Result<()> {
293        let footnotes = match area_tree.get_footnotes(page_id) {
294            Some(f) if !f.is_empty() => f.clone(),
295            _ => return Ok(()), // No footnotes
296        };
297
298        // Calculate separator position (at bottom of main content)
299        let footnote_total_height = area_tree.footnote_height(page_id);
300        let separator_y = self.margins[0] + self.content_height() - footnote_total_height;
301
302        // Create footnote separator (thin horizontal line)
303        let separator_rect = Rect::from_point_size(
304            Point::new(self.margins[3], separator_y),
305            Size::new(Length::from_pt(72.0), Length::from_pt(1.0)), // 1 inch wide, 1pt thick
306        );
307        let mut separator_area = Area::new(AreaType::FootnoteSeparator, separator_rect);
308
309        // Set separator line style (thin black line)
310        use crate::area::{BorderStyle, TraitSet};
311        use fop_types::Color;
312        let traits = TraitSet {
313            border_width: Some([
314                Length::from_pt(1.0),
315                Length::ZERO,
316                Length::ZERO,
317                Length::ZERO,
318            ]),
319            border_color: Some([Color::BLACK, Color::BLACK, Color::BLACK, Color::BLACK]),
320            border_style: Some([
321                BorderStyle::Solid,
322                BorderStyle::None,
323                BorderStyle::None,
324                BorderStyle::None,
325            ]),
326            ..Default::default()
327        };
328        separator_area.traits = traits;
329
330        let separator_id = area_tree.add_area(separator_area);
331        area_tree
332            .append_child(page_id, separator_id)
333            .map_err(fop_types::FopError::Generic)?;
334
335        // Place footnotes below separator
336        let mut current_y = separator_y + Length::from_pt(7.0); // Separator + spacing
337
338        for footnote_id in footnotes {
339            // Get height first, then mutate
340            let footnote_height = if let Some(footnote_node) = area_tree.get(footnote_id) {
341                footnote_node.area.height()
342            } else {
343                continue;
344            };
345
346            if let Some(footnote_node) = area_tree.get_mut(footnote_id) {
347                // Position footnote
348                footnote_node.area.geometry.x = self.margins[3];
349                footnote_node.area.geometry.y = current_y;
350            }
351
352            // Attach footnote to page
353            area_tree
354                .append_child(page_id, footnote_id)
355                .map_err(fop_types::FopError::Generic)?;
356
357            current_y += footnote_height;
358        }
359
360        Ok(())
361    }
362
363    /// Check if content fits on current page
364    pub fn fits_on_page(&self, current_height: Length, content_height: Length) -> bool {
365        current_height + content_height <= self.content_height()
366    }
367
368    /// Split a block area across multiple pages (for large blocks)
369    pub fn split_area(
370        &self,
371        area_tree: &mut AreaTree,
372        area_id: AreaId,
373        available_height: Length,
374    ) -> Result<Option<AreaId>> {
375        // Get the area to split
376        if let Some(area_node) = area_tree.get(area_id) {
377            let area_height = area_node.area.height();
378
379            // If it fits, no split needed
380            if area_height <= available_height {
381                return Ok(None);
382            }
383
384            // Create continuation area with remaining height
385            let remaining_height = area_height - available_height;
386            let mut continuation_area = area_node.area.clone();
387            continuation_area.geometry.height = remaining_height;
388
389            let continuation_id = area_tree.add_area(continuation_area);
390            Ok(Some(continuation_id))
391        } else {
392            Ok(None)
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_page_breaker_dimensions() {
403        let breaker = PageBreaker::new(
404            Length::from_mm(210.0), // A4 width
405            Length::from_mm(297.0), // A4 height
406            [
407                Length::from_mm(20.0), // top
408                Length::from_mm(20.0), // right
409                Length::from_mm(20.0), // bottom
410                Length::from_mm(20.0), // left
411            ],
412        );
413
414        let content_width = breaker.content_width();
415        let content_height = breaker.content_height();
416
417        assert_eq!(content_width, Length::from_mm(170.0)); // 210 - 20 - 20
418        assert_eq!(content_height, Length::from_mm(257.0)); // 297 - 20 - 20
419    }
420
421    #[test]
422    fn test_create_empty_page() {
423        let breaker = PageBreaker::new(
424            Length::from_pt(595.0),
425            Length::from_pt(842.0),
426            [Length::from_pt(72.0); 4],
427        );
428
429        let mut tree = AreaTree::new();
430        let page_id = breaker
431            .create_page(&mut tree)
432            .expect("test: should succeed");
433
434        assert!(tree.get(page_id).is_some());
435        let page_node = tree.get(page_id).expect("test: should succeed");
436        assert_eq!(page_node.area.area_type, AreaType::Page);
437    }
438
439    #[test]
440    fn test_break_empty_tree() {
441        let breaker = PageBreaker::new(
442            Length::from_pt(595.0),
443            Length::from_pt(842.0),
444            [Length::from_pt(72.0); 4],
445        );
446
447        let mut tree = AreaTree::new();
448        let root = Area::new(
449            AreaType::Block,
450            Rect::from_point_size(Point::ZERO, Size::new(Length::ZERO, Length::ZERO)),
451        );
452        let root_id = tree.add_area(root);
453
454        let pages = breaker
455            .break_into_pages(&mut tree, root_id)
456            .expect("test: should succeed");
457
458        // Should create at least one page
459        assert_eq!(pages.len(), 1);
460    }
461
462    #[test]
463    fn test_overflow_detection() {
464        let breaker = PageBreaker::new(
465            Length::from_pt(595.0),
466            Length::from_pt(842.0),
467            [Length::from_pt(72.0); 4], // 1 inch margins
468        );
469
470        // Content height: 842 - 72 - 72 = 698pt
471        assert_eq!(breaker.content_height(), Length::from_pt(698.0));
472
473        // Check if content fits
474        assert!(breaker.fits_on_page(Length::from_pt(100.0), Length::from_pt(200.0)));
475        assert!(!breaker.fits_on_page(Length::from_pt(600.0), Length::from_pt(200.0)));
476    }
477
478    #[test]
479    fn test_multi_page_breaking() {
480        let breaker = PageBreaker::new(
481            Length::from_pt(595.0),
482            Length::from_pt(842.0),
483            [Length::from_pt(72.0); 4],
484        );
485
486        let mut tree = AreaTree::new();
487        let root = Area::new(
488            AreaType::Block,
489            Rect::from_point_size(Point::ZERO, Size::new(Length::ZERO, Length::ZERO)),
490        );
491        let root_id = tree.add_area(root);
492
493        // Add blocks that overflow one page
494        for _ in 0..5 {
495            let block = Area::new(
496                AreaType::Block,
497                Rect::from_point_size(
498                    Point::ZERO,
499                    Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
500                ),
501            );
502            let block_id = tree.add_area(block);
503            tree.append_child(root_id, block_id)
504                .expect("test: should succeed");
505        }
506
507        let pages = breaker
508            .break_into_pages(&mut tree, root_id)
509            .expect("test: should succeed");
510
511        // With 5 blocks of 200pt each (1000pt total) and content height 698pt,
512        // should create at least 2 pages
513        assert!(pages.len() >= 2);
514    }
515
516    #[test]
517    fn test_split_area() {
518        let breaker = PageBreaker::new(
519            Length::from_pt(595.0),
520            Length::from_pt(842.0),
521            [Length::from_pt(72.0); 4],
522        );
523
524        let mut tree = AreaTree::new();
525
526        // Create a large block
527        let large_block = Area::new(
528            AreaType::Block,
529            Rect::from_point_size(
530                Point::ZERO,
531                Size::new(Length::from_pt(400.0), Length::from_pt(800.0)),
532            ),
533        );
534        let block_id = tree.add_area(large_block);
535
536        // Split with 300pt available
537        let continuation = breaker
538            .split_area(&mut tree, block_id, Length::from_pt(300.0))
539            .expect("test: should succeed");
540
541        assert!(continuation.is_some());
542    }
543
544    #[test]
545    fn test_keep_with_previous_prevents_break() {
546        use crate::layout::{Keep, KeepConstraint};
547
548        let breaker = PageBreaker::new(
549            Length::from_pt(595.0),
550            Length::from_pt(842.0),
551            [Length::from_pt(72.0); 4],
552        );
553
554        let mut tree = AreaTree::new();
555
556        // Create two blocks
557        let block1 = Area::new(
558            AreaType::Block,
559            Rect::from_point_size(
560                Point::ZERO,
561                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
562            ),
563        );
564        let block1_id = tree.add_area(block1);
565
566        let mut constraint = KeepConstraint::new();
567        constraint.keep_with_previous = Keep::Always;
568
569        let block2 = Area::new(
570            AreaType::Block,
571            Rect::from_point_size(
572                Point::ZERO,
573                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
574            ),
575        )
576        .with_keep_constraint(constraint);
577        let block2_id = tree.add_area(block2);
578
579        let children = vec![block1_id, block2_id];
580
581        // Can break before first block (no previous)
582        assert!(breaker.can_break_before(&tree, block1_id, 0, &children));
583
584        // Cannot break before second block (has keep-with-previous)
585        assert!(!breaker.can_break_before(&tree, block2_id, 1, &children));
586    }
587
588    #[test]
589    fn test_keep_with_next_prevents_break() {
590        use crate::layout::{Keep, KeepConstraint};
591
592        let breaker = PageBreaker::new(
593            Length::from_pt(595.0),
594            Length::from_pt(842.0),
595            [Length::from_pt(72.0); 4],
596        );
597
598        let mut tree = AreaTree::new();
599
600        // Create two blocks
601        let mut constraint = KeepConstraint::new();
602        constraint.keep_with_next = Keep::Always;
603
604        let block1 = Area::new(
605            AreaType::Block,
606            Rect::from_point_size(
607                Point::ZERO,
608                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
609            ),
610        )
611        .with_keep_constraint(constraint);
612        let block1_id = tree.add_area(block1);
613
614        let block2 = Area::new(
615            AreaType::Block,
616            Rect::from_point_size(
617                Point::ZERO,
618                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
619            ),
620        );
621        let block2_id = tree.add_area(block2);
622
623        let children = vec![block1_id, block2_id];
624
625        // Cannot break before second block (previous has keep-with-next)
626        assert!(!breaker.can_break_before(&tree, block2_id, 1, &children));
627    }
628
629    #[test]
630    fn test_keep_together_prevents_break() {
631        use crate::layout::{Keep, KeepConstraint};
632
633        let breaker = PageBreaker::new(
634            Length::from_pt(595.0),
635            Length::from_pt(842.0),
636            [Length::from_pt(72.0); 4],
637        );
638
639        let mut tree = AreaTree::new();
640
641        let mut constraint = KeepConstraint::new();
642        constraint.keep_together = Keep::Always;
643
644        let block = Area::new(
645            AreaType::Block,
646            Rect::from_point_size(
647                Point::ZERO,
648                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
649            ),
650        )
651        .with_keep_constraint(constraint);
652        let block_id = tree.add_area(block);
653
654        let children = vec![block_id];
655
656        // Cannot break (keep-together is active)
657        assert!(!breaker.can_break_before(&tree, block_id, 0, &children));
658    }
659
660    #[test]
661    fn test_no_keep_allows_break() {
662        let breaker = PageBreaker::new(
663            Length::from_pt(595.0),
664            Length::from_pt(842.0),
665            [Length::from_pt(72.0); 4],
666        );
667
668        let mut tree = AreaTree::new();
669
670        // Create blocks without keep constraints
671        let block1 = Area::new(
672            AreaType::Block,
673            Rect::from_point_size(
674                Point::ZERO,
675                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
676            ),
677        );
678        let block1_id = tree.add_area(block1);
679
680        let block2 = Area::new(
681            AreaType::Block,
682            Rect::from_point_size(
683                Point::ZERO,
684                Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
685            ),
686        );
687        let block2_id = tree.add_area(block2);
688
689        let children = vec![block1_id, block2_id];
690
691        // Can break before both blocks (no constraints)
692        assert!(breaker.can_break_before(&tree, block1_id, 0, &children));
693        assert!(breaker.can_break_before(&tree, block2_id, 1, &children));
694    }
695}
696
697#[cfg(test)]
698mod extended_tests {
699    use super::*;
700    use crate::area::AreaType;
701
702    fn make_breaker() -> PageBreaker {
703        PageBreaker::new(
704            Length::from_pt(595.0),
705            Length::from_pt(842.0),
706            [Length::from_pt(72.0); 4],
707        )
708    }
709
710    #[test]
711    fn test_content_width_calculation() {
712        // 595 - 72 (right) - 72 (left) = 451
713        let breaker = make_breaker();
714        assert_eq!(breaker.content_width(), Length::from_pt(451.0));
715    }
716
717    #[test]
718    fn test_content_height_calculation() {
719        // 842 - 72 (top) - 72 (bottom) = 698
720        let breaker = make_breaker();
721        assert_eq!(breaker.content_height(), Length::from_pt(698.0));
722    }
723
724    #[test]
725    fn test_content_height_with_footnotes_reduces_available() {
726        let breaker = make_breaker();
727        let base = breaker.content_height();
728        let with_footnote = breaker.content_height_with_footnotes(Length::from_pt(50.0));
729        assert_eq!(with_footnote, base - Length::from_pt(50.0));
730    }
731
732    #[test]
733    fn test_fits_on_page_exactly_at_boundary() {
734        let breaker = make_breaker();
735        let content_h = breaker.content_height();
736        // Exactly at boundary should fit
737        assert!(breaker.fits_on_page(Length::ZERO, content_h));
738    }
739
740    #[test]
741    fn test_fits_on_page_over_boundary_does_not_fit() {
742        let breaker = make_breaker();
743        let content_h = breaker.content_height();
744        assert!(!breaker.fits_on_page(Length::from_pt(1.0), content_h));
745    }
746
747    #[test]
748    fn test_split_area_fits_returns_none() {
749        let breaker = make_breaker();
750        let mut tree = AreaTree::new();
751        let area = Area::new(
752            AreaType::Block,
753            Rect::from_point_size(
754                Point::ZERO,
755                Size::new(Length::from_pt(400.0), Length::from_pt(100.0)),
756            ),
757        );
758        let area_id = tree.add_area(area);
759
760        // 100pt fits in 200pt available => no split needed
761        let result = breaker
762            .split_area(&mut tree, area_id, Length::from_pt(200.0))
763            .expect("test: should succeed");
764        assert!(result.is_none());
765    }
766
767    #[test]
768    fn test_split_area_overflow_creates_continuation() {
769        let breaker = make_breaker();
770        let mut tree = AreaTree::new();
771        let area = Area::new(
772            AreaType::Block,
773            Rect::from_point_size(
774                Point::ZERO,
775                Size::new(Length::from_pt(400.0), Length::from_pt(300.0)),
776            ),
777        );
778        let area_id = tree.add_area(area);
779
780        // 300pt does not fit in 200pt available => split needed
781        let continuation = breaker
782            .split_area(&mut tree, area_id, Length::from_pt(200.0))
783            .expect("test: should succeed");
784        assert!(continuation.is_some());
785
786        let cont_id = continuation.expect("test: should succeed");
787        let cont_node = tree.get(cont_id).expect("test: should succeed");
788        // Continuation should have the remaining 100pt
789        assert_eq!(cont_node.area.height(), Length::from_pt(100.0));
790    }
791
792    #[test]
793    fn test_break_into_pages_single_block_fits() {
794        let breaker = make_breaker();
795        let mut tree = AreaTree::new();
796        let root = Area::new(
797            AreaType::Block,
798            Rect::from_point_size(Point::ZERO, Size::new(Length::ZERO, Length::ZERO)),
799        );
800        let root_id = tree.add_area(root);
801
802        let small_block = Area::new(
803            AreaType::Block,
804            Rect::from_point_size(
805                Point::ZERO,
806                Size::new(Length::from_pt(400.0), Length::from_pt(100.0)),
807            ),
808        );
809        let block_id = tree.add_area(small_block);
810        tree.append_child(root_id, block_id)
811            .expect("test: should succeed");
812
813        let pages = breaker
814            .break_into_pages(&mut tree, root_id)
815            .expect("test: should succeed");
816        // All fits on one page
817        assert_eq!(pages.len(), 1);
818    }
819
820    #[test]
821    fn test_count_line_areas_in_block_with_children() {
822        let breaker = make_breaker();
823        let mut tree = AreaTree::new();
824
825        // Create a block with line children
826        let block = Area::new(
827            AreaType::Block,
828            Rect::from_point_size(
829                Point::ZERO,
830                Size::new(Length::from_pt(400.0), Length::from_pt(50.0)),
831            ),
832        );
833        let block_id = tree.add_area(block);
834
835        let line1 = Area::new(
836            AreaType::Line,
837            Rect::from_point_size(
838                Point::ZERO,
839                Size::new(Length::from_pt(400.0), Length::from_pt(12.0)),
840            ),
841        );
842        let line2 = Area::new(
843            AreaType::Line,
844            Rect::from_point_size(
845                Point::ZERO,
846                Size::new(Length::from_pt(400.0), Length::from_pt(12.0)),
847            ),
848        );
849        let line1_id = tree.add_area(line1);
850        let line2_id = tree.add_area(line2);
851
852        tree.append_child(block_id, line1_id)
853            .expect("test: should succeed");
854        tree.append_child(block_id, line2_id)
855            .expect("test: should succeed");
856
857        let count = breaker.count_line_areas(&tree, block_id);
858        assert_eq!(count, 2, "Block with 2 line children should count 2");
859    }
860
861    #[test]
862    fn test_page_has_region_child() {
863        let breaker = make_breaker();
864        let mut tree = AreaTree::new();
865        let page_id = breaker
866            .create_page(&mut tree)
867            .expect("test: should succeed");
868
869        let children = tree.children(page_id);
870        assert_eq!(children.len(), 1, "Page should have one region child");
871
872        let region_node = tree.get(children[0]).expect("test: should succeed");
873        assert_eq!(region_node.area.area_type, AreaType::Region);
874    }
875
876    #[test]
877    fn test_asymmetric_margins() {
878        let breaker = PageBreaker::new(
879            Length::from_pt(612.0), // US Letter width
880            Length::from_pt(792.0), // US Letter height
881            [
882                Length::from_pt(72.0), // top 1 inch
883                Length::from_pt(54.0), // right 0.75 inch
884                Length::from_pt(72.0), // bottom 1 inch
885                Length::from_pt(54.0), // left 0.75 inch
886            ],
887        );
888
889        // content_width = 612 - 54 - 54 = 504
890        // content_height = 792 - 72 - 72 = 648
891        assert_eq!(breaker.content_width(), Length::from_pt(504.0));
892        assert_eq!(breaker.content_height(), Length::from_pt(648.0));
893    }
894}