1
//! CSS Fragmentation Engine for Paged Media
2
//!
3
//! This module implements the CSS Fragmentation specification (css-break-3) for
4
//! breaking content across pages, columns, or regions.
5
//!
6
//! ## Key Concepts
7
//!
8
//! - **Fragmentainer**: A container (page, column, region) that holds a portion of content
9
//! - **FragmentationContext**: Tracks layout state during fragmentation
10
//! - **BoxBreakBehavior**: How a box should be handled at page breaks
11
//! - **PageTemplate**: Headers, footers, and running content for pages
12
//!
13
//! ## Algorithm Overview
14
//!
15
//! Unlike post-layout splitting, this module integrates fragmentation INTO layout:
16
//!
17
//! 1. Classify each box's break behavior (splittable, keep-together, monolithic)
18
//! 2. During layout, check if content fits in current fragmentainer
19
//! 3. Apply break-before/break-after rules
20
//! 4. Split or defer content as needed
21
//! 5. Handle orphans/widows for text content
22
//!
23
//! **Note**: `solver3/pagination.rs` provides an alternative page-layout implementation
24
//! with its own `PageGeometer`, `PageTemplate`, and `PageMargins`. See that module for
25
//! the currently active paged-layout pipeline.
26

            
27
use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec::Vec};
28
use core::fmt;
29

            
30
use azul_core::{
31
    dom::NodeId,
32
    geom::{LogicalPosition, LogicalRect, LogicalSize},
33
};
34
use azul_css::props::layout::fragmentation::{
35
    BoxDecorationBreak, BreakInside, Orphans, PageBreak, Widows,
36
};
37

            
38
#[cfg(all(feature = "text_layout", feature = "font_loading"))]
39
use crate::solver3::display_list::{DisplayList, DisplayListItem};
40

            
41
// Stub types when text_layout or font_loading is disabled
42
#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
43
#[derive(Debug, Clone, Default)]
44
pub struct DisplayList {
45
    pub items: Vec<DisplayListItem>,
46
}
47

            
48
#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
49
#[derive(Debug, Clone)]
50
pub struct DisplayListItem;
51

            
52
// Page Templates (Headers, Footers, Running Content)
53

            
54
/// Counter that tracks page numbers and other running content
55
#[derive(Debug, Clone)]
56
pub struct PageCounter {
57
    /// Current page number (1-indexed)
58
    pub page_number: usize,
59
    /// Total page count (may be unknown during first pass)
60
    pub total_pages: Option<usize>,
61
    /// Chapter or section number
62
    pub chapter: Option<usize>,
63
    /// Custom named counters (CSS counter() function)
64
    pub named_counters: BTreeMap<String, i32>,
65
}
66

            
67
impl Default for PageCounter {
68
    fn default() -> Self {
69
        Self {
70
            page_number: 1,
71
            total_pages: None,
72
            chapter: None,
73
            named_counters: BTreeMap::new(),
74
        }
75
    }
76
}
77

            
78
impl PageCounter {
79
    pub fn new() -> Self {
80
        Self::default()
81
    }
82

            
83
    pub fn with_page_number(mut self, page: usize) -> Self {
84
        self.page_number = page;
85
        self
86
    }
87

            
88
    pub fn with_total_pages(mut self, total: usize) -> Self {
89
        self.total_pages = Some(total);
90
        self
91
    }
92

            
93
    /// Format page number as string (e.g., "3", "iii", "C")
94
    pub fn format_page_number(&self, style: PageNumberStyle) -> String {
95
        match style {
96
            PageNumberStyle::Decimal => format!("{}", self.page_number),
97
            PageNumberStyle::LowerRoman => to_lower_roman(self.page_number),
98
            PageNumberStyle::UpperRoman => to_upper_roman(self.page_number),
99
            PageNumberStyle::LowerAlpha => to_lower_alpha(self.page_number),
100
            PageNumberStyle::UpperAlpha => to_upper_alpha(self.page_number),
101
        }
102
    }
103

            
104
    /// Get "Page X of Y" string
105
    pub fn format_page_of_total(&self) -> String {
106
        match self.total_pages {
107
            Some(total) => format!("Page {} of {}", self.page_number, total),
108
            None => format!("Page {}", self.page_number),
109
        }
110
    }
111
}
112

            
113
/// Style for page number formatting
114
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115
pub enum PageNumberStyle {
116
    /// 1, 2, 3, ...
117
    Decimal,
118
    /// i, ii, iii, iv, ...
119
    LowerRoman,
120
    /// I, II, III, IV, ...
121
    UpperRoman,
122
    /// a, b, c, ..., z, aa, ab, ...
123
    LowerAlpha,
124
    /// A, B, C, ..., Z, AA, AB, ...
125
    UpperAlpha,
126
}
127

            
128
impl Default for PageNumberStyle {
129
    fn default() -> Self {
130
        Self::Decimal
131
    }
132
}
133

            
134
/// Slot position for dynamic content in page template
135
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136
pub enum PageSlotPosition {
137
    /// Top-left corner
138
    TopLeft,
139
    /// Top-center
140
    TopCenter,
141
    /// Top-right corner
142
    TopRight,
143
    /// Bottom-left corner
144
    BottomLeft,
145
    /// Bottom-center
146
    BottomCenter,
147
    /// Bottom-right corner
148
    BottomRight,
149
}
150

            
151
/// Content that can be placed in a page template slot
152
#[derive(Clone)]
153
pub enum PageSlotContent {
154
    /// Static text
155
    Text(String),
156
    /// Page number with formatting
157
    PageNumber(PageNumberStyle),
158
    /// "Page X of Y"
159
    PageOfTotal,
160
    /// Chapter/section title (from running headers)
161
    RunningHeader(String),
162
    /// Custom function that generates content per page
163
    Dynamic(Arc<DynamicSlotContentFn>),
164
}
165

            
166
/// Wrapper for dynamic slot content functions to allow Debug impl.
167
///
168
/// Use [`DynamicSlotContentFn::new`] to wrap a closure, then place it
169
/// inside [`PageSlotContent::Dynamic`] via `Arc`:
170
///
171
/// ```ignore
172
/// let func = DynamicSlotContentFn::new(|counter| {
173
///     format!("Page {}", counter.page_number)
174
/// });
175
/// let content = PageSlotContent::Dynamic(Arc::new(func));
176
/// ```
177
pub struct DynamicSlotContentFn {
178
    func: Box<dyn Fn(&PageCounter) -> String + Send + Sync>,
179
}
180

            
181
impl DynamicSlotContentFn {
182
    pub fn new<F: Fn(&PageCounter) -> String + Send + Sync + 'static>(f: F) -> Self {
183
        Self { func: Box::new(f) }
184
    }
185

            
186
    pub fn call(&self, counter: &PageCounter) -> String {
187
        (self.func)(counter)
188
    }
189
}
190

            
191
impl fmt::Debug for DynamicSlotContentFn {
192
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193
        write!(f, "<dynamic content fn>")
194
    }
195
}
196

            
197
impl fmt::Debug for PageSlotContent {
198
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199
        match self {
200
            PageSlotContent::Text(s) => write!(f, "Text({:?})", s),
201
            PageSlotContent::PageNumber(style) => write!(f, "PageNumber({:?})", style),
202
            PageSlotContent::PageOfTotal => write!(f, "PageOfTotal"),
203
            PageSlotContent::RunningHeader(s) => write!(f, "RunningHeader({:?})", s),
204
            PageSlotContent::Dynamic(_) => write!(f, "Dynamic(<fn>)"),
205
        }
206
    }
207
}
208

            
209
/// A slot in the page template
210
#[derive(Debug, Clone)]
211
pub struct PageSlot {
212
    /// Position of this slot
213
    pub position: PageSlotPosition,
214
    /// Content to display
215
    pub content: PageSlotContent,
216
    /// Font size in points (optional override)
217
    pub font_size_pt: Option<f32>,
218
    /// Color (optional override)
219
    pub color: Option<azul_css::props::basic::ColorU>,
220
}
221

            
222
/// Template for page headers, footers, and margins
223
#[derive(Debug, Clone)]
224
pub struct PageTemplate {
225
    /// Header height in points (0 = no header)
226
    pub header_height: f32,
227
    /// Footer height in points (0 = no footer)
228
    pub footer_height: f32,
229
    /// Slots for dynamic content
230
    pub slots: Vec<PageSlot>,
231
    /// Whether to show header on first page
232
    pub header_on_first_page: bool,
233
    /// Whether to show footer on first page
234
    pub footer_on_first_page: bool,
235
    /// Different template for left (even) pages
236
    pub left_page_slots: Option<Vec<PageSlot>>,
237
    /// Different template for right (odd) pages  
238
    pub right_page_slots: Option<Vec<PageSlot>>,
239
}
240

            
241
impl Default for PageTemplate {
242
    fn default() -> Self {
243
        Self {
244
            header_height: 0.0,
245
            footer_height: 0.0,
246
            slots: Vec::new(),
247
            header_on_first_page: true,
248
            footer_on_first_page: true,
249
            left_page_slots: None,
250
            right_page_slots: None,
251
        }
252
    }
253
}
254

            
255
/// Default font size in points for page template slots
256
const DEFAULT_SLOT_FONT_SIZE_PT: f32 = 10.0;
257

            
258
impl PageTemplate {
259
    pub fn new() -> Self {
260
        Self::default()
261
    }
262

            
263
    /// Add a simple page number footer (centered)
264
    pub fn with_page_number_footer(mut self, height: f32) -> Self {
265
        self.footer_height = height;
266
        self.slots.push(PageSlot {
267
            position: PageSlotPosition::BottomCenter,
268
            content: PageSlotContent::PageNumber(PageNumberStyle::Decimal),
269
            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
270
            color: None,
271
        });
272
        self
273
    }
274

            
275
    /// Add "Page X of Y" footer
276
    pub fn with_page_of_total_footer(mut self, height: f32) -> Self {
277
        self.footer_height = height;
278
        self.slots.push(PageSlot {
279
            position: PageSlotPosition::BottomCenter,
280
            content: PageSlotContent::PageOfTotal,
281
            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
282
            color: None,
283
        });
284
        self
285
    }
286

            
287
    /// Add a header with title on left and page number on right
288
    pub fn with_book_header(mut self, title: String, height: f32) -> Self {
289
        self.header_height = height;
290
        self.slots.push(PageSlot {
291
            position: PageSlotPosition::TopLeft,
292
            content: PageSlotContent::Text(title),
293
            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
294
            color: None,
295
        });
296
        self.slots.push(PageSlot {
297
            position: PageSlotPosition::TopRight,
298
            content: PageSlotContent::PageNumber(PageNumberStyle::Decimal),
299
            font_size_pt: Some(DEFAULT_SLOT_FONT_SIZE_PT),
300
            color: None,
301
        });
302
        self
303
    }
304

            
305
    /// Get slots for a specific page (handles left/right page differences)
306
    pub fn slots_for_page(&self, page_number: usize) -> &[PageSlot] {
307
        let override_slots = if page_number % 2 == 0 {
308
            self.left_page_slots.as_deref()
309
        } else {
310
            self.right_page_slots.as_deref()
311
        };
312
        override_slots.unwrap_or(&self.slots)
313
    }
314

            
315
    /// Check if header should be shown on this page
316
    pub fn show_header(&self, page_number: usize) -> bool {
317
        if page_number == 1 && !self.header_on_first_page {
318
            return false;
319
        }
320
        self.header_height > 0.0
321
    }
322

            
323
    /// Check if footer should be shown on this page
324
    pub fn show_footer(&self, page_number: usize) -> bool {
325
        if page_number == 1 && !self.footer_on_first_page {
326
            return false;
327
        }
328
        self.footer_height > 0.0
329
    }
330

            
331
    /// Get the content area height (page height minus header and footer)
332
    pub fn content_area_height(&self, page_height: f32, page_number: usize) -> f32 {
333
        let header = if self.show_header(page_number) {
334
            self.header_height
335
        } else {
336
            0.0
337
        };
338
        let footer = if self.show_footer(page_number) {
339
            self.footer_height
340
        } else {
341
            0.0
342
        };
343
        page_height - header - footer
344
    }
345
}
346

            
347
// Box Break Behavior Classification
348

            
349
/// How a box should behave at fragmentation breaks
350
#[derive(Debug, Clone)]
351
pub enum BoxBreakBehavior {
352
    /// Can be split at any internal break point (paragraphs, containers)
353
    Splittable {
354
        /// Minimum content height before a break (orphans-like)
355
        min_before_break: f32,
356
        /// Minimum content height after a break (widows-like)
357
        min_after_break: f32,
358
    },
359
    /// Should be kept together if possible (headers, small blocks)
360
    KeepTogether {
361
        /// Estimated total height of this box
362
        estimated_height: f32,
363
        /// Priority level (higher = more important to keep together)
364
        priority: KeepTogetherPriority,
365
    },
366
    /// Cannot be split (images, replaced elements, overflow:scroll)
367
    Monolithic {
368
        /// Fixed height of this element
369
        height: f32,
370
    },
371
}
372

            
373
/// Priority for keeping content together
374
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
375
pub enum KeepTogetherPriority {
376
    /// Low priority - can break if needed
377
    Low = 0,
378
    /// Normal priority (default for break-inside: avoid)
379
    Normal = 1,
380
    /// High priority (headers with following content)
381
    High = 2,
382
    /// Critical (figure with caption, table headers)
383
    Critical = 3,
384
}
385

            
386
/// Information about a potential break point
387
#[derive(Debug, Clone)]
388
pub struct BreakPoint {
389
    /// Y position of this break point (in content coordinates)
390
    pub y_position: f32,
391
    /// Type of break point (Class A, B, or C)
392
    pub break_class: BreakClass,
393
    /// Break-before value at this point
394
    pub break_before: PageBreak,
395
    /// Break-after value at this point  
396
    pub break_after: PageBreak,
397
    /// Whether ancestors have break-inside: avoid
398
    pub ancestor_avoid_depth: usize,
399
    /// Node that precedes this break point
400
    pub preceding_node: Option<NodeId>,
401
    /// Node that follows this break point
402
    pub following_node: Option<NodeId>,
403
}
404

            
405
/// CSS Fragmentation break point class
406
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407
pub enum BreakClass {
408
    /// Between sibling block-level boxes
409
    ClassA,
410
    // +spec:block-formatting-context:a019b9 - break opportunities only between line boxes, not inside them
411
    /// Between line boxes inside a block container
412
    ClassB,
413
    /// Between content edge and child margin edge
414
    ClassC,
415
}
416

            
417
impl BreakPoint {
418
    /// Check if this break point is allowed (respecting all break rules)
419
    pub fn is_allowed(&self) -> bool {
420
        // Rule 1: Check break-after/break-before
421
        if is_forced_break(&self.break_before) || is_forced_break(&self.break_after) {
422
            return true; // Forced breaks are always allowed
423
        }
424

            
425
        if is_avoid_break(&self.break_before) || is_avoid_break(&self.break_after) {
426
            return false; // Avoid breaks
427
        }
428

            
429
        // Rule 2: Check ancestor break-inside: avoid
430
        if self.ancestor_avoid_depth > 0 {
431
            return false;
432
        }
433

            
434
        // Rules 3 & 4 are handled at a higher level (orphans/widows, etc.)
435
        true
436
    }
437

            
438
    /// Check if this is a forced break
439
    pub fn is_forced(&self) -> bool {
440
        is_forced_break(&self.break_before) || is_forced_break(&self.break_after)
441
    }
442
}
443

            
444
// Fragmentation Layout Context
445

            
446
/// A fragment of content placed on a specific page
447
#[derive(Debug)]
448
pub struct PageFragment {
449
    /// Which page this fragment belongs to (0-indexed)
450
    pub page_index: usize,
451
    /// Bounds of this fragment on the page (in page coordinates)
452
    pub bounds: LogicalRect,
453
    /// Display list items for this fragment
454
    pub items: Vec<DisplayListItem>,
455
    /// Node ID that this fragment belongs to
456
    pub source_node: Option<NodeId>,
457
    /// Whether this is a continuation from previous page
458
    pub is_continuation: bool,
459
    /// Whether this continues on the next page
460
    pub continues_on_next: bool,
461
}
462

            
463
/// Context for fragmentation-aware layout
464
#[derive(Debug)]
465
pub struct FragmentationLayoutContext {
466
    /// Page size (including margins)
467
    pub page_size: LogicalSize,
468
    /// Content area margins
469
    pub margins: PageMargins,
470
    /// Page template for headers/footers
471
    pub template: PageTemplate,
472
    /// Current page being laid out (0-indexed)
473
    pub current_page: usize,
474
    /// Y position on current page (0 = top of content area)
475
    pub current_y: f32,
476
    /// Available height remaining on current page
477
    pub available_height: f32,
478
    /// Page content height (without margins and headers/footers)
479
    pub page_content_height: f32,
480
    /// Accumulated break-inside: avoid depth from ancestors
481
    pub break_inside_avoid_depth: usize,
482
    /// Current orphans setting (inherited)
483
    pub orphans: u32,
484
    /// Current widows setting (inherited)
485
    pub widows: u32,
486
    /// All page fragments generated so far
487
    pub fragments: Vec<PageFragment>,
488
    /// Page counter for headers/footers
489
    pub counter: PageCounter,
490
    /// Fragmentation defaults (smart behavior settings)
491
    pub defaults: FragmentationDefaults,
492
    /// Break points encountered during layout
493
    pub break_points: Vec<BreakPoint>,
494
    /// Whether to avoid break before next box
495
    pub avoid_break_before_next: bool,
496
}
497

            
498
/// Page margins in points
499
#[derive(Debug, Clone, Copy, Default)]
500
pub struct PageMargins {
501
    pub top: f32,
502
    pub right: f32,
503
    pub bottom: f32,
504
    pub left: f32,
505
}
506

            
507
impl PageMargins {
508
    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
509
        Self {
510
            top,
511
            right,
512
            bottom,
513
            left,
514
        }
515
    }
516

            
517
    pub fn uniform(margin: f32) -> Self {
518
        Self {
519
            top: margin,
520
            right: margin,
521
            bottom: margin,
522
            left: margin,
523
        }
524
    }
525

            
526
    pub fn horizontal(&self) -> f32 {
527
        self.left + self.right
528
    }
529

            
530
    pub fn vertical(&self) -> f32 {
531
        self.top + self.bottom
532
    }
533
}
534

            
535
/// Configuration for intelligent fragmentation defaults
536
#[derive(Debug, Clone)]
537
pub struct FragmentationDefaults {
538
    /// Keep headers (h1-h6) with following content
539
    pub keep_headers_with_content: bool,
540
    /// Minimum lines to keep together for short paragraphs
541
    pub min_paragraph_lines: u32,
542
    /// Keep figure/figcaption together
543
    pub keep_figures_together: bool,
544
    /// Keep table headers with first data row
545
    pub keep_table_headers: bool,
546
    /// Keep list item markers with content
547
    pub keep_list_markers: bool,
548
    /// Treat small blocks as monolithic (height threshold in lines)
549
    pub small_block_threshold_lines: u32,
550
    /// Default orphans value
551
    pub default_orphans: u32,
552
    /// Default widows value
553
    pub default_widows: u32,
554
}
555

            
556
impl Default for FragmentationDefaults {
557
    fn default() -> Self {
558
        Self {
559
            keep_headers_with_content: true,
560
            min_paragraph_lines: 3,
561
            keep_figures_together: true,
562
            keep_table_headers: true,
563
            keep_list_markers: true,
564
            small_block_threshold_lines: 3,
565
            default_orphans: 2,
566
            default_widows: 2,
567
        }
568
    }
569
}
570

            
571
impl FragmentationLayoutContext {
572
    /// Create a new fragmentation context for paged layout
573
    pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
574
        let template = PageTemplate::default();
575

            
576
        let page_content_height =
577
            page_size.height - margins.vertical() - template.header_height - template.footer_height;
578

            
579
        Self {
580
            page_size,
581
            margins,
582
            template,
583
            current_page: 0,
584
            current_y: 0.0,
585
            available_height: page_content_height,
586
            page_content_height,
587
            break_inside_avoid_depth: 0,
588
            orphans: 2,
589
            widows: 2,
590
            fragments: Vec::new(),
591
            counter: PageCounter::new(),
592
            defaults: FragmentationDefaults::default(),
593
            break_points: Vec::new(),
594
            avoid_break_before_next: false,
595
        }
596
    }
597

            
598
    /// Create context with a page template
599
    pub fn with_template(mut self, template: PageTemplate) -> Self {
600
        self.template = template;
601
        self.recalculate_content_height();
602
        self
603
    }
604

            
605
    /// Create context with custom defaults
606
    pub fn with_defaults(mut self, defaults: FragmentationDefaults) -> Self {
607
        self.orphans = defaults.default_orphans;
608
        self.widows = defaults.default_widows;
609
        self.defaults = defaults;
610
        self
611
    }
612

            
613
    /// Recalculate content height based on template
614
    fn recalculate_content_height(&mut self) {
615
        let page_height = self.page_size.height - self.margins.vertical();
616
        self.page_content_height =
617
            self.template.content_area_height(page_height, self.current_page + 1);
618
        self.available_height = self.page_content_height - self.current_y;
619
    }
620

            
621
    /// Get the content area origin for the current page
622
    pub fn content_origin(&self) -> LogicalPosition {
623
        let header = if self.template.show_header(self.current_page + 1) {
624
            self.template.header_height
625
        } else {
626
            0.0
627
        };
628
        LogicalPosition {
629
            x: self.margins.left,
630
            y: self.margins.top + header,
631
        }
632
    }
633

            
634
    /// Get the content area size for the current page
635
    pub fn content_size(&self) -> LogicalSize {
636
        LogicalSize {
637
            width: self.page_size.width - self.margins.horizontal(),
638
            height: self.page_content_height,
639
        }
640
    }
641

            
642
    /// Use space on the current page
643
    pub fn use_space(&mut self, height: f32) {
644
        self.current_y += height;
645
        self.available_height = (self.page_content_height - self.current_y).max(0.0);
646
    }
647

            
648
    /// Check if content of given height can fit on current page
649
    pub fn can_fit(&self, height: f32) -> bool {
650
        self.available_height >= height
651
    }
652

            
653
    /// Check if content would fit on an empty page
654
    pub fn would_fit_on_empty_page(&self, height: f32) -> bool {
655
        height <= self.page_content_height
656
    }
657

            
658
    /// Advance to the next page
659
    pub fn advance_page(&mut self) {
660
        self.current_page += 1;
661
        self.current_y = 0.0;
662
        self.counter.page_number += 1;
663
        self.recalculate_content_height();
664
        self.avoid_break_before_next = false;
665
    }
666

            
667
    /// Advance to a left (even) page.
668
    ///
669
    /// May insert a blank page if the current page is already even,
670
    /// in order to land on the next even-numbered page (standard
671
    /// recto/verso paged-media behavior).
672
    pub fn advance_to_left_page(&mut self) {
673
        self.advance_page();
674
        if self.current_page % 2 != 0 {
675
            // Current page is odd (right), advance one more
676
            self.advance_page();
677
        }
678
    }
679

            
680
    /// Advance to a right (odd) page.
681
    ///
682
    /// May insert a blank page if the current page is already odd,
683
    /// in order to land on the next odd-numbered page (standard
684
    /// recto/verso paged-media behavior).
685
    pub fn advance_to_right_page(&mut self) {
686
        self.advance_page();
687
        if self.current_page % 2 == 0 {
688
            // Current page is even (left), advance one more
689
            self.advance_page();
690
        }
691
    }
692

            
693
    /// Enter a box with break-inside: avoid
694
    pub fn enter_avoid_break(&mut self) {
695
        self.break_inside_avoid_depth += 1;
696
    }
697

            
698
    /// Exit a box with break-inside: avoid
699
    pub fn exit_avoid_break(&mut self) {
700
        self.break_inside_avoid_depth = self.break_inside_avoid_depth.saturating_sub(1);
701
    }
702

            
703
    /// Set flag to avoid break before next content
704
    pub fn set_avoid_break_before_next(&mut self) {
705
        self.avoid_break_before_next = true;
706
    }
707

            
708
    /// Add a page fragment
709
    pub fn add_fragment(&mut self, fragment: PageFragment) {
710
        self.fragments.push(fragment);
711
    }
712

            
713
    /// Get the total number of pages so far
714
    pub fn page_count(&self) -> usize {
715
        self.current_page + 1
716
    }
717

            
718
    /// Set total page count (for "Page X of Y" footers)
719
    pub fn set_total_pages(&mut self, total: usize) {
720
        self.counter.total_pages = Some(total);
721
    }
722

            
723
    /// Convert fragments to display lists (one per page)
724
    pub fn into_display_lists(self) -> Vec<DisplayList> {
725
        let page_count = self.page_count();
726
        let mut display_lists: Vec<DisplayList> =
727
            (0..page_count).map(|_| DisplayList::default()).collect();
728

            
729
        for fragment in self.fragments {
730
            if fragment.page_index < display_lists.len() {
731
                display_lists[fragment.page_index]
732
                    .items
733
                    .extend(fragment.items);
734
            }
735
        }
736

            
737
        display_lists
738
    }
739

            
740
    /// Generate header/footer display list items for a specific page
741
    pub fn generate_page_chrome(&self, page_index: usize) -> Vec<DisplayListItem> {
742
        let mut items = Vec::new();
743
        let page_number = page_index + 1;
744

            
745
        let counter = PageCounter {
746
            page_number,
747
            total_pages: self.counter.total_pages,
748
            chapter: self.counter.chapter,
749
            named_counters: self.counter.named_counters.clone(),
750
        };
751

            
752
        let slots = self.template.slots_for_page(page_number);
753

            
754
        for slot in slots {
755
            let _text = match &slot.content {
756
                PageSlotContent::Text(s) => s.clone(),
757
                PageSlotContent::PageNumber(style) => counter.format_page_number(*style),
758
                PageSlotContent::PageOfTotal => counter.format_page_of_total(),
759
                PageSlotContent::RunningHeader(s) => s.clone(),
760
                PageSlotContent::Dynamic(f) => f.call(&counter),
761
            };
762

            
763
            // Calculate position based on slot
764
            let (_x, _y) = self.slot_position(slot.position, page_number);
765

            
766
            // TODO: Create proper text DisplayListItem
767
            // For now we'll need to integrate with text layout
768
            // This is a placeholder that shows where the text would go
769
        }
770

            
771
        items
772
    }
773

            
774
    /// Calculate position for a page slot
775
    fn slot_position(&self, position: PageSlotPosition, page_number: usize) -> (f32, f32) {
776
        let content_width = self.page_size.width - self.margins.horizontal();
777

            
778
        let x = match position {
779
            PageSlotPosition::TopLeft | PageSlotPosition::BottomLeft => self.margins.left,
780
            PageSlotPosition::TopCenter | PageSlotPosition::BottomCenter => {
781
                self.margins.left + content_width / 2.0
782
            }
783
            PageSlotPosition::TopRight | PageSlotPosition::BottomRight => {
784
                self.page_size.width - self.margins.right
785
            }
786
        };
787

            
788
        let y = match position {
789
            PageSlotPosition::TopLeft
790
            | PageSlotPosition::TopCenter
791
            | PageSlotPosition::TopRight => self.margins.top + self.template.header_height / 2.0,
792
            PageSlotPosition::BottomLeft
793
            | PageSlotPosition::BottomCenter
794
            | PageSlotPosition::BottomRight => {
795
                self.page_size.height - self.margins.bottom - self.template.footer_height / 2.0
796
            }
797
        };
798

            
799
        (x, y)
800
    }
801
}
802

            
803
// Break Decision Logic
804

            
805
/// Result of deciding how to handle a box at a potential break point
806
#[derive(Debug, Clone)]
807
pub enum BreakDecision {
808
    /// Place the entire box on the current page
809
    FitOnCurrentPage,
810
    /// Move the entire box to the next page
811
    MoveToNextPage,
812
    /// Split the box across pages
813
    SplitAcrossPages {
814
        /// Height to place on current page
815
        height_on_current: f32,
816
        /// Height to place on next page(s)
817
        height_remaining: f32,
818
    },
819
    /// Force a page break before this box
820
    ForceBreakBefore,
821
    /// Force a page break after this box
822
    ForceBreakAfter,
823
}
824

            
825
/// Make a break decision for a box with given behavior
826
pub fn decide_break(
827
    behavior: &BoxBreakBehavior,
828
    ctx: &FragmentationLayoutContext,
829
    break_before: PageBreak,
830
    break_after: PageBreak,
831
) -> BreakDecision {
832
    // Check for forced break before
833
    if is_forced_break(&break_before) {
834
        if ctx.current_y > 0.0 {
835
            return BreakDecision::ForceBreakBefore;
836
        }
837
    }
838

            
839
    match behavior {
840
        BoxBreakBehavior::Monolithic { height } => {
841
            decide_monolithic_break(*height, ctx, break_before)
842
        }
843
        BoxBreakBehavior::KeepTogether {
844
            estimated_height,
845
            priority,
846
        } => decide_keep_together_break(*estimated_height, *priority, ctx, break_before),
847
        BoxBreakBehavior::Splittable {
848
            min_before_break,
849
            min_after_break,
850
        } => decide_splittable_break(*min_before_break, *min_after_break, ctx, break_before),
851
    }
852
}
853

            
854
fn decide_monolithic_break(
855
    height: f32,
856
    ctx: &FragmentationLayoutContext,
857
    _break_before: PageBreak,
858
) -> BreakDecision {
859
    // Monolithic content cannot be split
860
    if ctx.can_fit(height) {
861
        BreakDecision::FitOnCurrentPage
862
    } else if ctx.current_y > 0.0 && ctx.would_fit_on_empty_page(height) {
863
        // Doesn't fit but would fit on empty page
864
        BreakDecision::MoveToNextPage
865
    } else {
866
        // Too large for any page - place anyway (will overflow)
867
        BreakDecision::FitOnCurrentPage
868
    }
869
}
870

            
871
fn decide_keep_together_break(
872
    height: f32,
873
    _priority: KeepTogetherPriority,
874
    ctx: &FragmentationLayoutContext,
875
    _break_before: PageBreak,
876
) -> BreakDecision {
877
    if ctx.can_fit(height) {
878
        BreakDecision::FitOnCurrentPage
879
    } else if ctx.would_fit_on_empty_page(height) {
880
        // Would fit on empty page, move there
881
        BreakDecision::MoveToNextPage
882
    } else {
883
        // Too tall for any page - must split despite keep-together
884
        // Calculate split point
885
        let height_on_current = ctx.available_height;
886
        let height_remaining = height - height_on_current;
887
        BreakDecision::SplitAcrossPages {
888
            height_on_current,
889
            height_remaining,
890
        }
891
    }
892
}
893

            
894
fn decide_splittable_break(
895
    min_before: f32,
896
    _min_after: f32,
897
    ctx: &FragmentationLayoutContext,
898
    _break_before: PageBreak,
899
) -> BreakDecision {
900
    // For splittable content, we need to consider orphans/widows
901
    let available = ctx.available_height;
902

            
903
    if available < min_before && ctx.current_y > 0.0 {
904
        // Can't fit minimum orphan content, move to next page
905
        BreakDecision::MoveToNextPage
906
    } else {
907
        // Can split - but actual split point determined during text layout
908
        BreakDecision::FitOnCurrentPage
909
    }
910
}
911

            
912
// Helper Functions
913

            
914
fn is_forced_break(page_break: &PageBreak) -> bool {
915
    matches!(
916
        page_break,
917
        PageBreak::Always
918
            | PageBreak::Page
919
            | PageBreak::Left
920
            | PageBreak::Right
921
            | PageBreak::Recto
922
            | PageBreak::Verso
923
            | PageBreak::All
924
    )
925
}
926

            
927
fn is_avoid_break(page_break: &PageBreak) -> bool {
928
    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
929
}
930

            
931
// Roman numeral conversion
932
//
933
// Note: Roman numerals and alphabetic numbering have no representation for
934
// zero. These functions return `"0"` as a fallback when `n == 0`.
935

            
936
fn to_lower_roman(n: usize) -> String {
937
    to_upper_roman(n).to_lowercase()
938
}
939

            
940
fn to_upper_roman(mut n: usize) -> String {
941
    if n == 0 {
942
        return String::from("0");
943
    }
944

            
945
    let numerals = [
946
        (1000, "M"),
947
        (900, "CM"),
948
        (500, "D"),
949
        (400, "CD"),
950
        (100, "C"),
951
        (90, "XC"),
952
        (50, "L"),
953
        (40, "XL"),
954
        (10, "X"),
955
        (9, "IX"),
956
        (5, "V"),
957
        (4, "IV"),
958
        (1, "I"),
959
    ];
960

            
961
    let mut result = String::new();
962
    for (value, numeral) in numerals.iter() {
963
        while n >= *value {
964
            result.push_str(numeral);
965
            n -= value;
966
        }
967
    }
968
    result
969
}
970

            
971
fn to_lower_alpha(n: usize) -> String {
972
    to_upper_alpha(n).to_lowercase()
973
}
974

            
975
fn to_upper_alpha(mut n: usize) -> String {
976
    if n == 0 {
977
        return String::from("0");
978
    }
979

            
980
    let mut result = String::new();
981
    while n > 0 {
982
        n -= 1;
983
        result.insert(0, (b'A' + (n % 26) as u8) as char);
984
        n /= 26;
985
    }
986
    result
987
}