Skip to main content

azul_layout/
paged.rs

1//! Paged media layout engine.
2//!
3//! This module provides infrastructure for multi-page document
4//! layout with CSS Paged Media support.
5//!
6//! The core concept is a **FragmentationContext**, which represents
7//! a series of containers (fragmentainers) that content flows into
8//! during layout. For continuous media (screens), we use a single
9//! infinite container. For paged media (print), we use a series of
10//! page-sized containers.
11//!
12//! This approach allows the layout engine to make break decisions
13//! during layout, respecting CSS properties like `break-before`,
14//! `break-after`, and `break-inside`.
15
16use azul_core::geom::LogicalSize;
17
18#[cfg(all(feature = "text_layout", feature = "font_loading"))]
19use crate::solver3::display_list::DisplayList;
20
21// Stub type when text_layout or font_loading is disabled
22#[cfg(not(all(feature = "text_layout", feature = "font_loading")))]
23#[derive(Debug, Clone, Default)]
24pub struct DisplayList;
25
26/// Represents a series of containers that content flows into during layout.
27///
28/// This is the core abstraction for fragmentation support. Different media types
29/// use different fragmentation contexts:
30/// - Screen rendering: Continuous (single infinite container)
31/// - Print rendering: Paged (series of fixed-size page containers)
32/// - Multi-column layout: MultiColumn (series of column containers)
33#[derive(Debug, Clone)]
34pub enum FragmentationContext {
35    /// Continuous media (screen): single infinite container.
36    ///
37    /// Used for normal screen rendering where content can scroll indefinitely.
38    /// The container grows as needed and never forces breaks.
39    Continuous {
40        /// Width of the viewport
41        width: f32,
42        /// The single fragmentainer (grows infinitely)
43        container: Fragmentainer,
44    },
45
46    /// Paged media (print): series of page boxes.
47    ///
48    /// Used for PDF generation and print preview. Content flows from one
49    /// page to the next when a page is full.
50    Paged {
51        /// Size of each page
52        page_size: LogicalSize,
53        /// All pages (fragmentainers) that have been created
54        pages: Vec<Fragmentainer>,
55    },
56
57    /// Multi-column layout: series of column boxes.
58    ///
59    /// Future support for CSS multi-column layout.
60    #[allow(dead_code)]
61    MultiColumn {
62        /// Width of each column
63        column_width: f32,
64        /// Height of each column
65        column_height: f32,
66        /// Gap between columns
67        gap: f32,
68        /// All columns that have been created
69        columns: Vec<Fragmentainer>,
70    },
71
72    /// CSS Regions: series of region boxes.
73    ///
74    /// Future support for CSS Regions specification.
75    #[allow(dead_code)]
76    Regions {
77        /// Pre-defined region boxes
78        regions: Vec<Fragmentainer>,
79    },
80}
81
82/// A single container (fragmentainer) in a fragmentation context.
83///
84/// Each fragmentainer has a logical size and tracks how much of that space
85/// has been used. For continuous media, the fragmentainer can grow infinitely.
86/// For paged media, fragmentainers have fixed sizes.
87#[derive(Debug, Clone)]
88pub struct Fragmentainer {
89    /// Logical size of this container (width and height)
90    pub size: LogicalSize,
91
92    /// How much block-axis space has been used (typically vertical space)
93    pub used_block_size: f32,
94
95    /// Whether this container has a fixed size (true for pages) or can
96    /// grow (false for continuous)
97    pub is_fixed_size: bool,
98
99    /// Content that has been placed in this fragmentainer (populated during layout).
100    pub content: Vec<LayoutBox>,
101}
102
103/// Layout box content placed within a fragmentainer.
104#[derive(Debug, Clone)]
105pub struct LayoutBox {
106    // Fields to be defined when fragmentation content tracking is implemented
107}
108
109impl Fragmentainer {
110    /// Create a new fragmentainer with the given size.
111    pub fn new(size: LogicalSize, is_fixed_size: bool) -> Self {
112        Self {
113            size,
114            used_block_size: 0.0,
115            is_fixed_size,
116            content: Vec::new(),
117        }
118    }
119
120    /// Get the remaining block-axis space (infinite for continuous, bounded for paged).
121    pub fn remaining_space(&self) -> f32 {
122        if self.is_fixed_size {
123            (self.size.height - self.used_block_size).max(0.0)
124        } else {
125            f32::MAX // Infinite for continuous media
126        }
127    }
128
129    /// Check if this fragmentainer is full (less than 1px remaining).
130    pub fn is_full(&self) -> bool {
131        self.is_fixed_size && self.remaining_space() < 1.0
132    }
133
134    /// Check if a block of the given size can fit in this fragmentainer.
135    pub fn can_fit(&self, block_size: f32) -> bool {
136        self.remaining_space() >= block_size
137    }
138
139    /// Record that block-axis space has been used in this fragmentainer.
140    pub fn use_space(&mut self, size: f32) {
141        self.used_block_size += size;
142    }
143}
144
145impl FragmentationContext {
146    /// Create a continuous fragmentation context for screen rendering.
147    pub fn new_continuous(width: f32) -> Self {
148        Self::Continuous {
149            width,
150            container: Fragmentainer::new(
151                LogicalSize::new(width, f32::MAX),
152                false, // Not fixed size
153            ),
154        }
155    }
156
157    /// Create a paged fragmentation context for print rendering.
158    pub fn new_paged(page_size: LogicalSize) -> Self {
159        Self::Paged {
160            page_size,
161            pages: vec![Fragmentainer::new(page_size, true)],
162        }
163    }
164
165    /// Get the number of fragmentainers (pages, columns, etc.) in this context.
166    pub fn fragmentainer_count(&self) -> usize {
167        match self {
168            Self::Continuous { .. } => 1,
169            Self::Paged { pages, .. } => pages.len(),
170            Self::MultiColumn { columns, .. } => columns.len(),
171            Self::Regions { regions } => regions.len(),
172        }
173    }
174
175    /// Get a reference to the current fragmentainer being filled.
176    pub fn current(&self) -> &Fragmentainer {
177        match self {
178            Self::Continuous { container, .. } => container,
179            Self::Paged { pages, .. } => pages
180                .last()
181                .expect("Paged context must have at least one page"),
182            Self::MultiColumn { columns, .. } => columns
183                .last()
184                .expect("MultiColumn context must have at least one column"),
185            Self::Regions { regions } => regions
186                .last()
187                .expect("Regions context must have at least one region"),
188        }
189    }
190
191    /// Get a mutable reference to the current fragmentainer being filled.
192    pub fn current_mut(&mut self) -> &mut Fragmentainer {
193        match self {
194            Self::Continuous { container, .. } => container,
195            Self::Paged { pages, .. } => pages
196                .last_mut()
197                .expect("Paged context must have at least one page"),
198            Self::MultiColumn { columns, .. } => columns
199                .last_mut()
200                .expect("MultiColumn context must have at least one column"),
201            Self::Regions { regions } => regions
202                .last_mut()
203                .expect("Regions context must have at least one region"),
204        }
205    }
206
207    /// Advance to the next fragmentainer, creating a new one if necessary.
208    ///
209    /// - For continuous media, this is a no-op (continuous media can't advance).
210    /// - For paged media, this creates a new page.
211    /// - For regions, this fails if no more regions are available.
212    ///
213    /// # Returns
214    ///
215    /// - `Ok(())` if the advance succeeded, `Err(String)` if it failed (e.g., no more regions).
216    pub fn advance(&mut self) -> Result<(), String> {
217        match self {
218            Self::Continuous { .. } => {
219                // Continuous media doesn't advance, it just grows
220                Ok(())
221            }
222            Self::Paged { page_size, pages } => {
223                // Create a new page
224                pages.push(Fragmentainer::new(*page_size, true));
225                Ok(())
226            }
227            Self::MultiColumn {
228                column_width,
229                column_height,
230                columns,
231                ..
232            } => {
233                // Create a new column
234                columns.push(Fragmentainer::new(
235                    LogicalSize::new(*column_width, *column_height),
236                    true,
237                ));
238                Ok(())
239            }
240            Self::Regions { .. } => {
241                // Regions are pre-defined, can't create more
242                Err("No more regions available for content overflow".to_string())
243            }
244        }
245    }
246
247    /// Get all fragmentainers in this context.
248    pub fn fragmentainers(&self) -> Vec<&Fragmentainer> {
249        match self {
250            Self::Continuous { container, .. } => vec![container],
251            Self::Paged { pages, .. } => pages.iter().collect(),
252            Self::MultiColumn { columns, .. } => columns.iter().collect(),
253            Self::Regions { regions } => regions.iter().collect(),
254        }
255    }
256
257    /// Get the page size for paged media, or None for other contexts.
258    pub fn page_size(&self) -> Option<LogicalSize> {
259        match self {
260            Self::Paged { page_size, .. } => Some(*page_size),
261            _ => None,
262        }
263    }
264
265    /// Get the page content height (page height minus margins).
266    /// For continuous media, returns f32::MAX.
267    pub fn page_content_height(&self) -> f32 {
268        match self {
269            Self::Continuous { .. } => f32::MAX,
270            Self::Paged { page_size, .. } => page_size.height,
271            Self::MultiColumn { column_height, .. } => *column_height,
272            Self::Regions { regions } => regions.first().map(|r| r.size.height).unwrap_or(f32::MAX),
273        }
274    }
275
276    /// Check if this is paged media.
277    pub fn is_paged(&self) -> bool {
278        matches!(self, Self::Paged { .. })
279    }
280}
281
282// Fragmentation State - Tracked During Layout
283
284/// State tracked during layout for fragmentation.
285/// This is created at the start of paged layout and updated as nodes are laid out.
286#[derive(Debug, Clone)]
287pub struct FragmentationState {
288    /// Current page being laid out (0-indexed)
289    pub current_page: usize,
290    /// Y position on current page (relative to page content area)
291    pub current_page_y: f32,
292    /// Available height remaining on current page
293    pub available_height: f32,
294    /// Full page content height
295    pub page_content_height: f32,
296    /// Page margins (not yet used, but needed for future)
297    pub margins_top: f32,
298    pub margins_bottom: f32,
299    /// Total number of pages so far
300    pub total_pages: usize,
301}
302
303impl FragmentationState {
304    /// Create a new fragmentation state for paged layout.
305    pub fn new(page_content_height: f32, margins_top: f32, margins_bottom: f32) -> Self {
306        Self {
307            current_page: 0,
308            current_page_y: 0.0,
309            available_height: page_content_height,
310            page_content_height,
311            margins_top,
312            margins_bottom,
313            total_pages: 1,
314        }
315    }
316
317    /// Check if content of the given height can fit on the current page.
318    pub fn can_fit(&self, height: f32) -> bool {
319        self.available_height >= height
320    }
321
322    /// Check if content would fit on an empty page.
323    pub fn would_fit_on_empty_page(&self, height: f32) -> bool {
324        height <= self.page_content_height
325    }
326
327    /// Use space on the current page.
328    pub fn use_space(&mut self, height: f32) {
329        self.current_page_y += height;
330        self.available_height = (self.page_content_height - self.current_page_y).max(0.0);
331    }
332
333    /// Advance to the next page.
334    pub fn advance_page(&mut self) {
335        self.current_page += 1;
336        self.current_page_y = 0.0;
337        self.available_height = self.page_content_height;
338        self.total_pages = self.total_pages.max(self.current_page + 1);
339    }
340
341    /// Calculate which page a Y position belongs to.
342    pub fn page_for_y(&self, y: f32) -> usize {
343        if self.page_content_height <= 0.0 {
344            return 0;
345        }
346        (y / self.page_content_height).floor() as usize
347    }
348
349    /// Calculate the Y offset for a given page (to convert to page-relative coordinates).
350    pub fn page_y_offset(&self, page: usize) -> f32 {
351        page as f32 * self.page_content_height
352    }
353}