Skip to main content

azul_layout/solver3/
pagination.rs

1//! CSS Paged Media Pagination Engine - "Infinite Canvas with Physical Spacers"
2//!
3//! This module implements a pagination architecture where content is laid out
4//! on a single "infinite" vertical canvas, with "dead zones" representing page
5//! breaks (including headers, footers, and margins).
6//!
7//! ## Core Concept: Physical Spacers
8//!
9//! Instead of assigning nodes to logical pages, we map pages onto a single vertical
10//! coordinate system where page breaks are physical empty spaces:
11//!
12//! ```text
13//! 0px      ─────────────────────────────
14//!          │ Page 1 Content             │
15//! 1000px   ─────────────────────────────
16//!          │ Dead Space (Footer+Margin) │  ← Page break zone
17//! 1100px   ─────────────────────────────
18//!          │ Page 2 Content             │
19//! 2100px   ─────────────────────────────
20//!          │ Dead Space (Footer+Margin) │
21//! 2200px   ─────────────────────────────
22//! ```
23//!
24//! ## CSS Generated Content for Paged Media (GCPM) Level 3 Support
25//!
26//! This module provides the foundation for CSS GCPM Level 3 features:
27//!
28//! - **Running Elements** (`position: running(name)`) - Elements extracted from flow and displayed
29//!   in margin boxes (headers/footers)
30//! - **Page Selectors** (`@page :first`, `@page :left/:right`) - Per-page styling
31//! - **Named Strings** (`string-set`, `content: string(name)`) - Captured text for headers
32//! - **Page Counters** (`counter(page)`, `counter(pages)`) - Page numbering
33//!
34//! **Note:** Running elements, named strings, and page selectors are currently
35//! stub implementations. Only page counters and header/footer configuration
36//! are functional.
37//!
38//! See: https://www.w3.org/TR/css-gcpm-3/
39
40use std::{collections::BTreeMap, sync::Arc};
41
42use azul_core::geom::LogicalSize;
43use azul_css::props::{
44    basic::ColorU,
45    layout::fragmentation::PageBreak,
46};
47
48/// Manages the infinite canvas coordinate system with page boundaries.
49///
50/// The `PageGeometer` tracks page dimensions and provides utilities for:
51///
52/// - Determining which page a Y coordinate falls on
53/// - Calculating the next page start position
54/// - Checking if content crosses page boundaries
55#[derive(Debug, Clone)]
56pub struct PageGeometer {
57    /// Total height of each page (including margins, headers, footers)
58    pub page_size: LogicalSize,
59    /// Content area margins (space reserved at top/bottom of each page)
60    pub page_margins: PageMargins,
61    /// Height reserved for page header (if any)
62    pub header_height: f32,
63    /// Height reserved for page footer (if any)
64    pub footer_height: f32,
65    /// Current Y position on the infinite canvas
66    pub current_y: f32,
67}
68
69/// Page margin configuration
70#[derive(Debug, Clone, Copy, Default)]
71pub struct PageMargins {
72    pub top: f32,
73    pub right: f32,
74    pub bottom: f32,
75    pub left: f32,
76}
77
78impl PageMargins {
79    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
80        Self {
81            top,
82            right,
83            bottom,
84            left,
85        }
86    }
87
88    pub fn uniform(margin: f32) -> Self {
89        Self {
90            top: margin,
91            right: margin,
92            bottom: margin,
93            left: margin,
94        }
95    }
96}
97
98impl PageGeometer {
99    /// Create a new PageGeometer for paged layout.
100    pub fn new(page_size: LogicalSize, margins: PageMargins) -> Self {
101        Self {
102            page_size,
103            page_margins: margins,
104            header_height: 0.0,
105            footer_height: 0.0,
106            current_y: 0.0,
107        }
108    }
109
110    /// Create with header and footer space reserved.
111    pub fn with_header_footer(mut self, header: f32, footer: f32) -> Self {
112        self.header_height = header;
113        self.footer_height = footer;
114        self
115    }
116
117    /// Get the usable content height per page (page height minus margins/headers/footers).
118    pub fn content_height(&self) -> f32 {
119        self.page_size.height
120            - self.page_margins.top
121            - self.page_margins.bottom
122            - self.header_height
123            - self.footer_height
124    }
125
126    /// Get the usable content width per page (page width minus left/right margins).
127    pub fn content_width(&self) -> f32 {
128        self.page_size.width - self.page_margins.left - self.page_margins.right
129    }
130
131    /// Calculate which page a given Y coordinate falls on (0-indexed).
132    ///
133    /// Negative Y values are clamped to page 0.
134    pub fn page_for_y(&self, y: f32) -> usize {
135        if y < 0.0 {
136            return 0;
137        }
138        let content_h = self.content_height();
139        if content_h <= 0.0 {
140            return 0;
141        }
142
143        // Account for dead zones between pages
144        let full_page_slot = content_h + self.dead_zone_height();
145        (y / full_page_slot).floor() as usize
146    }
147
148    /// Get the Y coordinate where a page's content area starts.
149    pub fn page_content_start_y(&self, page_index: usize) -> f32 {
150        let full_page_slot = self.content_height() + self.dead_zone_height();
151        page_index as f32 * full_page_slot
152    }
153
154    /// Get the Y coordinate where a page's content area ends.
155    pub fn page_content_end_y(&self, page_index: usize) -> f32 {
156        self.page_content_start_y(page_index) + self.content_height()
157    }
158
159    /// Get the height of the "dead zone" between pages (footer + margin + header of next page).
160    pub fn dead_zone_height(&self) -> f32 {
161        self.footer_height + self.page_margins.bottom + self.page_margins.top + self.header_height
162    }
163
164    /// Calculate the Y coordinate where the NEXT page's content starts from a given position.
165    pub fn next_page_start_y(&self, current_y: f32) -> f32 {
166        let current_page = self.page_for_y(current_y);
167        self.page_content_start_y(current_page + 1)
168    }
169
170    /// Check if a range [start_y, end_y) crosses a page boundary.
171    pub fn crosses_page_break(&self, start_y: f32, end_y: f32) -> bool {
172        let start_page = self.page_for_y(start_y);
173        let end_page = self.page_for_y(end_y - 0.01); // Subtract epsilon for exclusive end
174        start_page != end_page
175    }
176
177    /// Get remaining space on the current page from a given Y position.
178    pub fn remaining_on_page(&self, y: f32) -> f32 {
179        let page = self.page_for_y(y);
180        let page_end = self.page_content_end_y(page);
181        (page_end - y).max(0.0)
182    }
183
184    /// Check if content of given height can fit starting at Y position.
185    pub fn can_fit(&self, y: f32, height: f32) -> bool {
186        self.remaining_on_page(y) >= height
187    }
188
189    /// Calculate the additional Y offset needed to push content to the next page.
190    /// Returns 0 if content fits on current page.
191    pub fn page_break_offset(&self, y: f32, height: f32) -> f32 {
192        if self.can_fit(y, height) {
193            return 0.0;
194        }
195
196        // Content doesn't fit - calculate offset to move to next page
197        let next_start = self.next_page_start_y(y);
198        next_start - y
199    }
200
201    /// Get the number of pages needed to contain content ending at Y.
202    pub fn page_count(&self, total_content_height: f32) -> usize {
203        if total_content_height <= 0.0 {
204            return 1;
205        }
206        self.page_for_y(total_content_height - 0.01) + 1
207    }
208}
209
210/// CSS break behavior classification for a box.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum BreakBehavior {
213    /// Box can be split at internal break points (paragraphs, containers)
214    Splittable,
215    /// Box should be kept together if possible (break-inside: avoid)
216    AvoidBreak,
217    /// Box cannot be split (replaced elements, overflow:scroll, etc.)
218    Monolithic,
219}
220
221/// Result of evaluating break properties for a box.
222#[derive(Debug, Clone)]
223pub struct BreakEvaluation {
224    /// Whether to force a page break before this element
225    pub force_break_before: bool,
226    /// Whether to force a page break after this element  
227    pub force_break_after: bool,
228    /// How this box should behave at potential break points
229    pub behavior: BreakBehavior,
230    /// For text: minimum lines to keep at page start (orphans)
231    pub orphans: u32,
232    /// For text: minimum lines to keep at page end (widows)
233    pub widows: u32,
234}
235
236impl Default for BreakEvaluation {
237    fn default() -> Self {
238        Self {
239            force_break_before: false,
240            force_break_after: false,
241            behavior: BreakBehavior::Splittable,
242            orphans: 2,
243            widows: 2,
244        }
245    }
246}
247
248/// Check if a break-before/after value forces a page break.
249pub fn is_forced_break(page_break: PageBreak) -> bool {
250    matches!(
251        page_break,
252        PageBreak::Always
253            | PageBreak::Page
254            | PageBreak::Left
255            | PageBreak::Right
256            | PageBreak::Recto
257            | PageBreak::Verso
258            | PageBreak::All
259    )
260}
261
262/// Check if a break-before/after value avoids breaks.
263pub fn is_avoid_break(page_break: PageBreak) -> bool {
264    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
265}
266
267/// Metadata about table header repetition for a specific page.
268#[derive(Debug, Clone)]
269pub struct RepeatedTableHeader {
270    /// The Y position on the infinite canvas where this header should appear
271    pub inject_at_y: f32,
272    /// The display list items for the table header (cloned from original)
273    pub header_items: Vec<usize>, // Indices into the original display list
274    /// The height of the header
275    pub header_height: f32,
276}
277
278/// Context for pagination during layout.
279///
280/// This is passed into layout functions to allow them to make page-aware decisions.
281#[derive(Debug)]
282pub struct PaginationContext<'a> {
283    /// The page geometry calculator
284    pub geometer: &'a PageGeometer,
285    /// Accumulated break-inside: avoid depth from ancestors
286    pub break_avoid_depth: usize,
287    /// Track table headers that need to repeat on new pages
288    pub repeated_headers: Vec<RepeatedTableHeader>,
289}
290
291impl<'a> PaginationContext<'a> {
292    pub fn new(geometer: &'a PageGeometer) -> Self {
293        Self {
294            geometer,
295            break_avoid_depth: 0,
296            repeated_headers: Vec::new(),
297        }
298    }
299
300    /// Enter a box with break-inside: avoid
301    pub fn enter_avoid_break(&mut self) {
302        self.break_avoid_depth += 1;
303    }
304
305    /// Exit a box with break-inside: avoid
306    pub fn exit_avoid_break(&mut self) {
307        self.break_avoid_depth = self.break_avoid_depth.saturating_sub(1);
308    }
309
310    /// Check if we're inside an ancestor with break-inside: avoid
311    pub fn is_avoiding_breaks(&self) -> bool {
312        self.break_avoid_depth > 0
313    }
314
315    /// Register a table header for repetition on subsequent pages.
316    pub fn register_repeated_header(
317        &mut self,
318        inject_at_y: f32,
319        header_items: Vec<usize>,
320        header_height: f32,
321    ) {
322        self.repeated_headers.push(RepeatedTableHeader {
323            inject_at_y,
324            header_items,
325            header_height,
326        });
327    }
328}
329
330/// Calculate the position adjustment for a child element considering pagination.
331///
332/// This is called during BFC/IFC layout to determine if content needs to be
333/// pushed to the next page.
334///
335/// # Arguments
336/// * `geometer` - Page geometry calculator
337/// * `main_pen` - Current Y position in infinite canvas coordinates
338/// * `child_height` - Estimated height of the child element
339/// * `break_eval` - Break property evaluation for the child
340/// * `is_avoiding_breaks` - Whether an ancestor has break-inside: avoid
341///
342/// # Returns
343/// The Y offset to add to `main_pen` (0 if no adjustment needed, positive if pushing to next page)
344pub fn calculate_pagination_offset(
345    geometer: &PageGeometer,
346    main_pen: f32,
347    child_height: f32,
348    break_eval: &BreakEvaluation,
349    is_avoiding_breaks: bool,
350) -> f32 {
351    // 1. Handle forced break-before
352    if break_eval.force_break_before {
353        let remaining = geometer.remaining_on_page(main_pen);
354        if remaining < geometer.content_height() {
355            // Not at the start of a page - force break
356            return geometer.page_break_offset(main_pen, f32::MAX);
357        }
358    }
359
360    // 2. Check if content fits on current page
361    let remaining = geometer.remaining_on_page(main_pen);
362
363    // 3. Handle monolithic content (cannot be split)
364    // +spec:inline-formatting-context:cb2a20 - initial letter boxes are monolithic for block-axis fragmentation; breaks between lines alongside an initial letter should be avoided (like widows/orphans), but forced breaks take precedence
365    if break_eval.behavior == BreakBehavior::Monolithic {
366        if child_height <= remaining {
367            // Fits on current page
368            return 0.0;
369        }
370        if child_height <= geometer.content_height() {
371            // Doesn't fit but would fit on empty page - move to next
372            return geometer.page_break_offset(main_pen, child_height);
373        }
374        // Too large for any page - let it overflow (no adjustment)
375        return 0.0;
376    }
377
378    // 4. Handle avoid-break content
379    if break_eval.behavior == BreakBehavior::AvoidBreak || is_avoiding_breaks {
380        if child_height <= remaining {
381            // Fits on current page
382            return 0.0;
383        }
384        if child_height <= geometer.content_height() {
385            // Move to next page to keep together
386            return geometer.page_break_offset(main_pen, child_height);
387        }
388        // Too large to keep together - must allow splitting
389    }
390
391    // 5. Splittable content - check orphans/widows constraints
392    // For now, just ensure we have at least some minimum space
393    let min_before_break = 20.0; // ~1-2 lines minimum
394    if remaining < min_before_break && remaining < geometer.content_height() {
395        // Not enough space for even a small amount - move to next page
396        return geometer.page_break_offset(main_pen, child_height);
397    }
398
399    0.0
400}
401
402// CSS GCPM Level 3: Running Elements & Page Margin Boxes
403//
404// This section provides infrastructure for CSS Generated Content for Paged Media
405// Level 3 (https://www.w3.org/TR/css-gcpm-3/).
406//
407// Key concepts:
408//
409// 1. **Running Elements** - Elements with `position: running(header)` are removed from the normal
410//    flow and available for display in page margin boxes.
411//
412// 2. **Page Margin Boxes** - 16 margin boxes around each page (@top-left, @top-center, @top-right,
413//    @bottom-left, etc.) that can contain running elements or generated content.
414//
415// 3. **Named Strings** - Text captured with `string-set: header content(text)` and displayed with
416//    `content: string(header)`.
417//
418// 4. **Page Counters** - `counter(page)` and `counter(pages)` for page numbering.
419
420/// Position of a margin box on a page (CSS GCPM margin box names).
421///
422/// CSS defines 16 margin boxes around the page content area:
423/// ```text
424/// ┌─────────┬─────────────────┬─────────┐
425/// │top-left │   top-center    │top-right│
426/// ├─────────┼─────────────────┼─────────┤
427/// │         │                 │         │
428/// │  left   │                 │  right  │
429/// │  -top   │                 │  -top   │
430/// │         │                 │         │
431/// │  left   │    CONTENT      │  right  │
432/// │-middle  │      AREA       │-middle  │
433/// │         │                 │         │
434/// │  left   │                 │  right  │
435/// │-bottom  │                 │-bottom  │
436/// │         │                 │         │
437/// ├─────────┼─────────────────┼─────────┤
438/// │bot-left │  bottom-center  │bot-right│
439/// └─────────┴─────────────────┴─────────┘
440/// ```
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
442pub enum MarginBoxPosition {
443    // Top row
444    TopLeftCorner,
445    TopLeft,
446    TopCenter,
447    TopRight,
448    TopRightCorner,
449    // Left column
450    LeftTop,
451    LeftMiddle,
452    LeftBottom,
453    // Right column
454    RightTop,
455    RightMiddle,
456    RightBottom,
457    // Bottom row
458    BottomLeftCorner,
459    BottomLeft,
460    BottomCenter,
461    BottomRight,
462    BottomRightCorner,
463}
464
465impl MarginBoxPosition {
466    /// Returns true if this margin box is in the top margin area.
467    pub fn is_top(&self) -> bool {
468        matches!(
469            self,
470            Self::TopLeftCorner
471                | Self::TopLeft
472                | Self::TopCenter
473                | Self::TopRight
474                | Self::TopRightCorner
475        )
476    }
477
478    /// Returns true if this margin box is in the bottom margin area.
479    pub fn is_bottom(&self) -> bool {
480        matches!(
481            self,
482            Self::BottomLeftCorner
483                | Self::BottomLeft
484                | Self::BottomCenter
485                | Self::BottomRight
486                | Self::BottomRightCorner
487        )
488    }
489}
490
491/// A running element that was extracted from the document flow.
492///
493/// CSS GCPM allows elements to be "running" - removed from normal flow
494/// and made available for display in page margin boxes.
495///
496/// ```css
497/// h1 { position: running(chapter-title); }
498/// @page { @top-center { content: element(chapter-title); } }
499/// ```
500#[derive(Debug, Clone)]
501pub struct RunningElement {
502    /// The name of this running element (e.g., "chapter-title")
503    pub name: String,
504    /// The display list items for this element (captured when encountered in flow)
505    pub display_items: Vec<super::display_list::DisplayListItem>,
506    /// The size of this element when rendered
507    pub size: LogicalSize,
508    /// Which page this element was defined on (for `running()` selector specificity)
509    pub source_page: usize,
510}
511
512/// Content that can appear in a page margin box.
513///
514/// This enum represents the various types of content that CSS GCPM
515/// allows in margin boxes.
516#[derive(Clone)]
517pub enum MarginBoxContent {
518    /// Empty margin box
519    None,
520    /// A running element referenced by name: `content: element(header)`
521    RunningElement(String),
522    /// A named string: `content: string(chapter)`
523    NamedString(String),
524    /// Page counter: `content: counter(page)`
525    PageCounter,
526    /// Total pages counter: `content: counter(pages)`
527    PagesCounter,
528    /// Page counter with format: `content: counter(page, lower-roman)`
529    PageCounterFormatted { format: CounterFormat },
530    /// Combined content (e.g., "Page " counter(page) " of " counter(pages))
531    Combined(Vec<MarginBoxContent>),
532    /// Literal text
533    Text(String),
534    /// Custom callback for dynamic content generation
535    Custom(Arc<dyn Fn(PageInfo) -> String + Send + Sync>),
536}
537
538impl std::fmt::Debug for MarginBoxContent {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        match self {
541            Self::None => write!(f, "None"),
542            Self::RunningElement(s) => f.debug_tuple("RunningElement").field(s).finish(),
543            Self::NamedString(s) => f.debug_tuple("NamedString").field(s).finish(),
544            Self::PageCounter => write!(f, "PageCounter"),
545            Self::PagesCounter => write!(f, "PagesCounter"),
546            Self::PageCounterFormatted { format } => f
547                .debug_struct("PageCounterFormatted")
548                .field("format", format)
549                .finish(),
550            Self::Combined(v) => f.debug_tuple("Combined").field(v).finish(),
551            Self::Text(s) => f.debug_tuple("Text").field(s).finish(),
552            Self::Custom(_) => write!(f, "Custom(<fn>)"),
553        }
554    }
555}
556
557/// Counter formatting styles (subset of CSS list-style-type).
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum CounterFormat {
560    Decimal,
561    DecimalLeadingZero,
562    LowerRoman,
563    UpperRoman,
564    LowerAlpha,
565    UpperAlpha,
566    LowerGreek,
567}
568
569impl Default for CounterFormat {
570    fn default() -> Self {
571        Self::Decimal
572    }
573}
574
575impl CounterFormat {
576    /// Format a number according to this counter style.
577    pub fn format(&self, n: usize) -> String {
578        match self {
579            Self::Decimal => n.to_string(),
580            Self::DecimalLeadingZero => format!("{:02}", n),
581            Self::LowerRoman => to_roman(n, false),
582            Self::UpperRoman => to_roman(n, true),
583            Self::LowerAlpha => to_alpha(n, false),
584            Self::UpperAlpha => to_alpha(n, true),
585            Self::LowerGreek => to_greek(n),
586        }
587    }
588}
589
590/// Convert number to roman numerals.
591fn to_roman(mut n: usize, uppercase: bool) -> String {
592    if n == 0 {
593        return "0".to_string();
594    }
595
596    let numerals = [
597        (1000, "m"),
598        (900, "cm"),
599        (500, "d"),
600        (400, "cd"),
601        (100, "c"),
602        (90, "xc"),
603        (50, "l"),
604        (40, "xl"),
605        (10, "x"),
606        (9, "ix"),
607        (5, "v"),
608        (4, "iv"),
609        (1, "i"),
610    ];
611
612    let mut result = String::new();
613    for (value, numeral) in &numerals {
614        while n >= *value {
615            result.push_str(numeral);
616            n -= value;
617        }
618    }
619
620    if uppercase {
621        result.to_uppercase()
622    } else {
623        result
624    }
625}
626
627/// Convert number to alphabetic (a-z, aa-az, etc.).
628fn to_alpha(n: usize, uppercase: bool) -> String {
629    if n == 0 {
630        return "0".to_string();
631    }
632
633    let mut result = String::new();
634    let mut remaining = n;
635
636    while remaining > 0 {
637        remaining -= 1;
638        let c = ((remaining % 26) as u8 + if uppercase { b'A' } else { b'a' }) as char;
639        result.insert(0, c);
640        remaining /= 26;
641    }
642
643    result
644}
645
646/// Convert number to Greek letters (α, β, γ, ...).
647fn to_greek(n: usize) -> String {
648    const GREEK: &[char] = &[
649        'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ',
650        'τ', 'υ', 'φ', 'χ', 'ψ', 'ω',
651    ];
652    if n == 0 {
653        return "0".to_string();
654    }
655    if n <= GREEK.len() {
656        return GREEK[n - 1].to_string();
657    }
658
659    // For numbers > 24, use αα, αβ, etc.
660    let mut result = String::new();
661    let mut remaining = n;
662    while remaining > 0 {
663        remaining -= 1;
664        result.insert(0, GREEK[remaining % GREEK.len()]);
665        remaining /= GREEK.len();
666    }
667    result
668}
669
670/// Information about the current page, passed to content generators.
671#[derive(Debug, Clone, Copy)]
672pub struct PageInfo {
673    /// Current page number (1-indexed for display)
674    pub page_number: usize,
675    /// Total number of pages (may be 0 if unknown during first pass)
676    pub total_pages: usize,
677    /// Whether this is the first page
678    pub is_first: bool,
679    /// Whether this is the last page
680    pub is_last: bool,
681    /// Whether this is a left (verso) page (for duplex printing)
682    pub is_left: bool,
683    /// Whether this is a right (recto) page
684    pub is_right: bool,
685    /// Whether this is a blank page (inserted for left/right alignment)
686    pub is_blank: bool,
687}
688
689impl PageInfo {
690    /// Create PageInfo for a specific page.
691    pub fn new(page_number: usize, total_pages: usize) -> Self {
692        Self {
693            page_number,
694            total_pages,
695            is_first: page_number == 1,
696            is_last: total_pages > 0 && page_number == total_pages,
697            is_left: page_number % 2 == 0, // Even pages are left (verso)
698            is_right: page_number % 2 == 1, // Odd pages are right (recto)
699            is_blank: false,
700        }
701    }
702}
703
704/// Default height for page headers and footers (in points).
705const DEFAULT_HEADER_FOOTER_HEIGHT: f32 = 30.0;
706
707/// Default font size for header/footer text (in points).
708const DEFAULT_HEADER_FOOTER_FONT_SIZE: f32 = 10.0;
709
710/// Configuration for page headers and footers.
711///
712/// This is a simplified interface for the common case of adding
713/// headers and footers. For full GCPM support, use `PageTemplate`.
714#[derive(Debug, Clone)]
715pub struct HeaderFooterConfig {
716    /// Whether to show a header on each page
717    pub show_header: bool,
718    /// Whether to show a footer on each page
719    pub show_footer: bool,
720    /// Height of the header area (if shown)
721    pub header_height: f32,
722    /// Height of the footer area (if shown)  
723    pub footer_height: f32,
724    /// Content generator for the header
725    pub header_content: MarginBoxContent,
726    /// Content generator for the footer
727    pub footer_content: MarginBoxContent,
728    /// Font size for header/footer text
729    pub font_size: f32,
730    /// Text color for header/footer
731    pub text_color: ColorU,
732    /// Whether to skip header/footer on first page
733    pub skip_first_page: bool,
734}
735
736impl Default for HeaderFooterConfig {
737    fn default() -> Self {
738        Self {
739            show_header: false,
740            show_footer: false,
741            header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
742            footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
743            header_content: MarginBoxContent::None,
744            footer_content: MarginBoxContent::None,
745            font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
746            text_color: ColorU {
747                r: 0,
748                g: 0,
749                b: 0,
750                a: 255,
751            },
752            skip_first_page: false,
753        }
754    }
755}
756
757impl HeaderFooterConfig {
758    /// Create a config with page numbers in the footer.
759    pub fn with_page_numbers() -> Self {
760        Self {
761            show_footer: true,
762            footer_content: MarginBoxContent::Combined(vec![
763                MarginBoxContent::Text("Page ".to_string()),
764                MarginBoxContent::PageCounter,
765                MarginBoxContent::Text(" of ".to_string()),
766                MarginBoxContent::PagesCounter,
767            ]),
768            ..Default::default()
769        }
770    }
771
772    /// Create a config with page numbers in both header and footer.
773    pub fn with_header_and_footer_page_numbers() -> Self {
774        Self {
775            show_header: true,
776            show_footer: true,
777            header_content: MarginBoxContent::Combined(vec![
778                MarginBoxContent::Text("Page ".to_string()),
779                MarginBoxContent::PageCounter,
780            ]),
781            footer_content: MarginBoxContent::Combined(vec![
782                MarginBoxContent::Text("Page ".to_string()),
783                MarginBoxContent::PageCounter,
784                MarginBoxContent::Text(" of ".to_string()),
785                MarginBoxContent::PagesCounter,
786            ]),
787            ..Default::default()
788        }
789    }
790
791    /// Set custom header text.
792    pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
793        self.show_header = true;
794        self.header_content = MarginBoxContent::Text(text.into());
795        self
796    }
797
798    /// Set custom footer text.
799    pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
800        self.show_footer = true;
801        self.footer_content = MarginBoxContent::Text(text.into());
802        self
803    }
804
805    /// Generate the text content for a margin box given page info.
806    pub fn generate_content(&self, content: &MarginBoxContent, info: PageInfo) -> String {
807        match content {
808            MarginBoxContent::None => String::new(),
809            MarginBoxContent::Text(s) => s.clone(),
810            MarginBoxContent::PageCounter => info.page_number.to_string(),
811            MarginBoxContent::PagesCounter => {
812                if info.total_pages > 0 {
813                    info.total_pages.to_string()
814                } else {
815                    "?".to_string()
816                }
817            }
818            MarginBoxContent::PageCounterFormatted { format } => format.format(info.page_number),
819            MarginBoxContent::Combined(parts) => parts
820                .iter()
821                .map(|p| self.generate_content(p, info))
822                .collect(),
823            MarginBoxContent::NamedString(name) => {
824                // TODO: Look up named string from document context
825                format!("[string:{}]", name)
826            }
827            MarginBoxContent::RunningElement(name) => {
828                // Running elements are rendered as display items, not text
829                format!("[element:{}]", name)
830            }
831            MarginBoxContent::Custom(f) => f(info),
832        }
833    }
834
835    /// Get the header text for a specific page.
836    pub fn header_text(&self, info: PageInfo) -> String {
837        if !self.show_header {
838            return String::new();
839        }
840        if self.skip_first_page && info.is_first {
841            return String::new();
842        }
843        self.generate_content(&self.header_content, info)
844    }
845
846    /// Get the footer text for a specific page.
847    pub fn footer_text(&self, info: PageInfo) -> String {
848        if !self.show_footer {
849            return String::new();
850        }
851        if self.skip_first_page && info.is_first {
852            return String::new();
853        }
854        self.generate_content(&self.footer_content, info)
855    }
856}
857
858/// Full page template with all 16 margin boxes (CSS GCPM @page support).
859///
860/// This provides complete control over page layout following the CSS
861/// Paged Media and GCPM specifications.
862#[derive(Debug, Clone, Default)]
863pub struct PageTemplate {
864    /// Content for each margin box position
865    pub margin_boxes: BTreeMap<MarginBoxPosition, MarginBoxContent>,
866    /// Page margins (space allocated for margin boxes)
867    pub margins: PageMargins,
868    /// Named strings captured from the document
869    pub named_strings: BTreeMap<String, String>,
870    /// Running elements available for this page
871    pub running_elements: BTreeMap<String, RunningElement>,
872}
873
874impl PageTemplate {
875    /// Create a new empty page template.
876    pub fn new() -> Self {
877        Self::default()
878    }
879
880    /// Set content for a specific margin box.
881    pub fn set_margin_box(&mut self, position: MarginBoxPosition, content: MarginBoxContent) {
882        self.margin_boxes.insert(position, content);
883    }
884
885    /// Create a simple template with centered page numbers in the footer.
886    pub fn with_centered_page_numbers() -> Self {
887        let mut template = Self::new();
888        template.set_margin_box(
889            MarginBoxPosition::BottomCenter,
890            MarginBoxContent::PageCounter,
891        );
892        template
893    }
894
895    /// Create a template with "Page X of Y" in the bottom right.
896    pub fn with_page_x_of_y() -> Self {
897        let mut template = Self::new();
898        template.set_margin_box(
899            MarginBoxPosition::BottomRight,
900            MarginBoxContent::Combined(vec![
901                MarginBoxContent::Text("Page ".to_string()),
902                MarginBoxContent::PageCounter,
903                MarginBoxContent::Text(" of ".to_string()),
904                MarginBoxContent::PagesCounter,
905            ]),
906        );
907        template
908    }
909}
910
911/// Temporary configuration for page headers/footers without CSS `@page` parsing.
912///
913/// Provides programmatic control over page decoration until full CSS `@page`
914/// rule support is implemented.
915///
916/// ## Supported Features
917///
918/// - Page numbers in header and/or footer
919/// - Custom text in header and/or footer
920/// - Number format (decimal, roman numerals, alphabetic, greek)
921/// - Skip first page option
922///
923/// ## Example
924///
925/// ```rust
926/// use azul_layout::solver3::pagination::FakePageConfig;
927///
928/// let config = FakePageConfig::new()
929///     .with_footer_page_numbers()
930///     .with_header_text("My Document")
931///     .skip_first_page(true);
932///
933/// let header_footer = config.to_header_footer_config();
934/// ```
935#[derive(Debug, Clone)]
936pub struct FakePageConfig {
937    /// Show header on pages
938    pub show_header: bool,
939    /// Show footer on pages
940    pub show_footer: bool,
941    /// Header text (static text, or None for page numbers only)
942    pub header_text: Option<String>,
943    /// Footer text (static text, or None for page numbers only)
944    pub footer_text: Option<String>,
945    /// Include page number in header
946    pub header_page_number: bool,
947    /// Include page number in footer
948    pub footer_page_number: bool,
949    /// Include total pages count ("of Y") in header
950    pub header_total_pages: bool,
951    /// Include total pages count ("of Y") in footer
952    pub footer_total_pages: bool,
953    /// Number format for page counters
954    pub number_format: CounterFormat,
955    /// Skip header/footer on first page
956    pub skip_first_page: bool,
957    /// Header height in points
958    pub header_height: f32,
959    /// Footer height in points
960    pub footer_height: f32,
961    /// Font size for header/footer text
962    pub font_size: f32,
963    /// Text color for header/footer
964    pub text_color: ColorU,
965}
966
967impl Default for FakePageConfig {
968    fn default() -> Self {
969        Self {
970            show_header: false,
971            show_footer: false,
972            header_text: None,
973            footer_text: None,
974            header_page_number: false,
975            footer_page_number: false,
976            header_total_pages: false,
977            footer_total_pages: false,
978            number_format: CounterFormat::Decimal,
979            skip_first_page: false,
980            header_height: DEFAULT_HEADER_FOOTER_HEIGHT,
981            footer_height: DEFAULT_HEADER_FOOTER_HEIGHT,
982            font_size: DEFAULT_HEADER_FOOTER_FONT_SIZE,
983            text_color: ColorU {
984                r: 0,
985                g: 0,
986                b: 0,
987                a: 255,
988            },
989        }
990    }
991}
992
993impl FakePageConfig {
994    /// Create a new empty configuration (no headers/footers).
995    pub fn new() -> Self {
996        Self::default()
997    }
998
999    /// Enable footer with "Page X of Y" format.
1000    pub fn with_footer_page_numbers(mut self) -> Self {
1001        self.show_footer = true;
1002        self.footer_page_number = true;
1003        self.footer_total_pages = true;
1004        self
1005    }
1006
1007    /// Enable header with "Page X" format.
1008    pub fn with_header_page_numbers(mut self) -> Self {
1009        self.show_header = true;
1010        self.header_page_number = true;
1011        self
1012    }
1013
1014    /// Enable both header and footer with page numbers.
1015    pub fn with_header_and_footer_page_numbers(mut self) -> Self {
1016        self.show_header = true;
1017        self.show_footer = true;
1018        self.header_page_number = true;
1019        self.footer_page_number = true;
1020        self.footer_total_pages = true;
1021        self
1022    }
1023
1024    /// Set custom header text.
1025    pub fn with_header_text(mut self, text: impl Into<String>) -> Self {
1026        self.show_header = true;
1027        self.header_text = Some(text.into());
1028        self
1029    }
1030
1031    /// Set custom footer text.
1032    pub fn with_footer_text(mut self, text: impl Into<String>) -> Self {
1033        self.show_footer = true;
1034        self.footer_text = Some(text.into());
1035        self
1036    }
1037
1038    /// Set the number format for page counters.
1039    pub fn with_number_format(mut self, format: CounterFormat) -> Self {
1040        self.number_format = format;
1041        self
1042    }
1043
1044    /// Skip header/footer on the first page.
1045    pub fn skip_first_page(mut self, skip: bool) -> Self {
1046        self.skip_first_page = skip;
1047        self
1048    }
1049
1050    /// Set header height.
1051    pub fn with_header_height(mut self, height: f32) -> Self {
1052        self.header_height = height;
1053        self
1054    }
1055
1056    /// Set footer height.
1057    pub fn with_footer_height(mut self, height: f32) -> Self {
1058        self.footer_height = height;
1059        self
1060    }
1061
1062    /// Set font size for header/footer text.
1063    pub fn with_font_size(mut self, size: f32) -> Self {
1064        self.font_size = size;
1065        self
1066    }
1067
1068    /// Set text color for header/footer.
1069    pub fn with_text_color(mut self, color: ColorU) -> Self {
1070        self.text_color = color;
1071        self
1072    }
1073
1074    /// Convert this fake config to the internal HeaderFooterConfig.
1075    ///
1076    /// This is the bridge between the user-facing API and the internal
1077    /// pagination engine.
1078    pub fn to_header_footer_config(&self) -> HeaderFooterConfig {
1079        HeaderFooterConfig {
1080            show_header: self.show_header,
1081            show_footer: self.show_footer,
1082            header_height: self.header_height,
1083            footer_height: self.footer_height,
1084            header_content: self.build_header_content(),
1085            footer_content: self.build_footer_content(),
1086            skip_first_page: self.skip_first_page,
1087            font_size: self.font_size,
1088            text_color: self.text_color,
1089        }
1090    }
1091
1092    /// Build the MarginBoxContent for the header.
1093    fn build_header_content(&self) -> MarginBoxContent {
1094        Self::build_margin_content(
1095            &self.header_text,
1096            self.header_page_number,
1097            self.header_total_pages,
1098            self.number_format,
1099        )
1100    }
1101
1102    /// Build the MarginBoxContent for the footer.
1103    fn build_footer_content(&self) -> MarginBoxContent {
1104        Self::build_margin_content(
1105            &self.footer_text,
1106            self.footer_page_number,
1107            self.footer_total_pages,
1108            self.number_format,
1109        )
1110    }
1111
1112    /// Shared helper for building header/footer margin box content.
1113    fn build_margin_content(
1114        text: &Option<String>,
1115        page_number: bool,
1116        total_pages: bool,
1117        number_format: CounterFormat,
1118    ) -> MarginBoxContent {
1119        let mut parts = Vec::new();
1120
1121        if let Some(ref text) = text {
1122            parts.push(MarginBoxContent::Text(text.clone()));
1123            if page_number {
1124                parts.push(MarginBoxContent::Text(" - ".to_string()));
1125            }
1126        }
1127
1128        if page_number {
1129            if number_format == CounterFormat::Decimal {
1130                parts.push(MarginBoxContent::Text("Page ".to_string()));
1131                parts.push(MarginBoxContent::PageCounter);
1132            } else {
1133                parts.push(MarginBoxContent::Text("Page ".to_string()));
1134                parts.push(MarginBoxContent::PageCounterFormatted {
1135                    format: number_format,
1136                });
1137            }
1138
1139            if total_pages {
1140                parts.push(MarginBoxContent::Text(" of ".to_string()));
1141                parts.push(MarginBoxContent::PagesCounter);
1142            }
1143        }
1144
1145        if parts.is_empty() {
1146            MarginBoxContent::None
1147        } else if parts.len() == 1 {
1148            parts.pop().unwrap()
1149        } else {
1150            MarginBoxContent::Combined(parts)
1151        }
1152    }
1153}
1154
1155/// Information about a table that may need header repetition.
1156#[derive(Debug, Clone)]
1157pub struct TableHeaderInfo {
1158    /// The table's node index in the layout tree
1159    pub table_node_index: usize,
1160    /// The Y position where the table starts
1161    pub table_start_y: f32,
1162    /// The Y position where the table ends
1163    pub table_end_y: f32,
1164    /// The thead's display list items (captured during initial render)
1165    pub thead_items: Vec<super::display_list::DisplayListItem>,
1166    /// Height of the thead
1167    pub thead_height: f32,
1168    /// The Y position of the thead relative to table start
1169    pub thead_offset_y: f32,
1170}
1171
1172/// Context for tracking table headers across pages.
1173#[derive(Debug, Default, Clone)]
1174pub struct TableHeaderTracker {
1175    /// All tables with theads that might need repetition
1176    pub tables: Vec<TableHeaderInfo>,
1177}
1178
1179impl TableHeaderTracker {
1180    pub fn new() -> Self {
1181        Self::default()
1182    }
1183
1184    /// Register a table's thead for potential repetition.
1185    pub fn register_table_header(&mut self, info: TableHeaderInfo) {
1186        self.tables.push(info);
1187    }
1188
1189    /// Get theads that should be repeated on a specific page.
1190    ///
1191    /// Returns the thead items that need to be injected at the top of the page,
1192    /// along with the Y offset where they should appear.
1193    pub fn get_repeated_headers_for_page(
1194        &self,
1195        page_index: usize,
1196        page_top_y: f32,
1197        page_bottom_y: f32,
1198    ) -> Vec<(f32, &[super::display_list::DisplayListItem], f32)> {
1199        let mut headers = Vec::new();
1200
1201        for table in &self.tables {
1202            // Check if this table spans into this page (but didn't start on this page)
1203            let table_starts_before_page = table.table_start_y < page_top_y;
1204            let table_continues_on_page = table.table_end_y > page_top_y;
1205
1206            if table_starts_before_page && table_continues_on_page {
1207                // This table needs its header repeated on this page
1208                // The header should appear at the top of the page content area
1209                headers.push((
1210                    0.0, // Y offset from page top (header goes at very top)
1211                    table.thead_items.as_slice(),
1212                    table.thead_height,
1213                ));
1214            }
1215        }
1216
1217        headers
1218    }
1219}