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::{LogicalPosition, 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.
100    ///
101    /// For Phase 1, this is unused. In later phases, we'll store layout boxes here.
102    pub content: Vec<LayoutBox>,
103}
104
105/// Placeholder for layout box content (to be implemented in later phases)
106#[derive(Debug, Clone)]
107pub struct LayoutBox {
108    // TODO: Define structure in later phases
109}
110
111impl Fragmentainer {
112    /// Create a new fragmentainer with the given size.
113    ///
114    /// # Arguments
115    ///
116    /// - `size` - The logical size (width and height) of this fragmentainer
117    /// - `is_fixed_size` - Whether this fragmentainer has a fixed size (true for pages, false for
118    ///   continuous)
119    pub fn new(size: LogicalSize, is_fixed_size: bool) -> Self {
120        Self {
121            size,
122            used_block_size: 0.0,
123            is_fixed_size,
124            content: Vec::new(),
125        }
126    }
127
128    /// Get the remaining space in this fragmentainer.
129    ///
130    /// - For continuous media, this returns infinity (f32::MAX).
131    /// - For paged media, this returns the unused space.
132    pub fn remaining_space(&self) -> f32 {
133        if self.is_fixed_size {
134            (self.size.height - self.used_block_size).max(0.0)
135        } else {
136            f32::MAX // Infinite for continuous media
137        }
138    }
139
140    /// Check if this fragmentainer is full.
141    ///
142    /// - A fragmentainer is considered full if it has less than 1px of remaining space.
143    /// - Continuous fragmentainers are never full.
144    pub fn is_full(&self) -> bool {
145        self.is_fixed_size && self.remaining_space() < 1.0
146    }
147
148    /// Check if a block of the given size can fit in this fragmentainer.
149    ///
150    /// - `block_size` - The height of the block to check
151    pub fn can_fit(&self, block_size: f32) -> bool {
152        self.remaining_space() >= block_size
153    }
154
155    /// Record that space has been used in this fragmentainer.
156    ///
157    /// - `size` - The amount of block-axis space used
158    pub fn use_space(&mut self, size: f32) {
159        self.used_block_size += size;
160    }
161}
162
163impl FragmentationContext {
164    /// Create a continuous fragmentation context for screen rendering.
165    ///
166    /// - `width` - The viewport width
167    pub fn new_continuous(width: f32) -> Self {
168        Self::Continuous {
169            width,
170            container: Fragmentainer::new(
171                LogicalSize::new(width, f32::MAX),
172                false, // Not fixed size
173            ),
174        }
175    }
176
177    /// Create a paged fragmentation context for print rendering.
178    ///
179    /// - `page_size` - The size of each page
180    pub fn new_paged(page_size: LogicalSize) -> Self {
181        Self::Paged {
182            page_size,
183            pages: vec![Fragmentainer::new(page_size, true)],
184        }
185    }
186
187    /// Get the number of fragmentainers (pages, columns, etc.) in this context.
188    pub fn fragmentainer_count(&self) -> usize {
189        match self {
190            Self::Continuous { .. } => 1,
191            Self::Paged { pages, .. } => pages.len(),
192            Self::MultiColumn { columns, .. } => columns.len(),
193            Self::Regions { regions } => regions.len(),
194        }
195    }
196
197    /// Get a reference to the current fragmentainer being filled.
198    pub fn current(&self) -> &Fragmentainer {
199        match self {
200            Self::Continuous { container, .. } => container,
201            Self::Paged { pages, .. } => pages
202                .last()
203                .expect("Paged context must have at least one page"),
204            Self::MultiColumn { columns, .. } => columns
205                .last()
206                .expect("MultiColumn context must have at least one column"),
207            Self::Regions { regions } => regions
208                .last()
209                .expect("Regions context must have at least one region"),
210        }
211    }
212
213    /// Get a mutable reference to the current fragmentainer being filled.
214    pub fn current_mut(&mut self) -> &mut Fragmentainer {
215        match self {
216            Self::Continuous { container, .. } => container,
217            Self::Paged { pages, .. } => pages
218                .last_mut()
219                .expect("Paged context must have at least one page"),
220            Self::MultiColumn { columns, .. } => columns
221                .last_mut()
222                .expect("MultiColumn context must have at least one column"),
223            Self::Regions { regions } => regions
224                .last_mut()
225                .expect("Regions context must have at least one region"),
226        }
227    }
228
229    /// Advance to the next fragmentainer, creating a new one if necessary.
230    ///
231    /// - For continuous media, this is a no-op (continuous media can't advance).
232    /// - For paged media, this creates a new page.
233    /// - For regions, this fails if no more regions are available.
234    ///
235    /// # Returns
236    ///
237    /// - `Ok(())` if the advance succeeded, `Err(String)` if it failed (e.g., no more regions).
238    pub fn advance(&mut self) -> Result<(), String> {
239        match self {
240            Self::Continuous { .. } => {
241                // Continuous media doesn't advance, it just grows
242                Ok(())
243            }
244            Self::Paged { page_size, pages } => {
245                // Create a new page
246                pages.push(Fragmentainer::new(*page_size, true));
247                Ok(())
248            }
249            Self::MultiColumn {
250                column_width,
251                column_height,
252                columns,
253                ..
254            } => {
255                // Create a new column
256                columns.push(Fragmentainer::new(
257                    LogicalSize::new(*column_width, *column_height),
258                    true,
259                ));
260                Ok(())
261            }
262            Self::Regions { .. } => {
263                // Regions are pre-defined, can't create more
264                Err("No more regions available for content overflow".to_string())
265            }
266        }
267    }
268
269    /// Get all fragmentainers in this context.
270    pub fn fragmentainers(&self) -> Vec<&Fragmentainer> {
271        match self {
272            Self::Continuous { container, .. } => vec![container],
273            Self::Paged { pages, .. } => pages.iter().collect(),
274            Self::MultiColumn { columns, .. } => columns.iter().collect(),
275            Self::Regions { regions } => regions.iter().collect(),
276        }
277    }
278
279    /// Get the page size for paged media, or None for other contexts.
280    pub fn page_size(&self) -> Option<LogicalSize> {
281        match self {
282            Self::Paged { page_size, .. } => Some(*page_size),
283            _ => None,
284        }
285    }
286
287    /// Get the page content height (page height minus margins).
288    /// For continuous media, returns f32::MAX.
289    pub fn page_content_height(&self) -> f32 {
290        match self {
291            Self::Continuous { .. } => f32::MAX,
292            Self::Paged { page_size, .. } => page_size.height,
293            Self::MultiColumn { column_height, .. } => *column_height,
294            Self::Regions { regions } => regions.first().map(|r| r.size.height).unwrap_or(f32::MAX),
295        }
296    }
297
298    /// Check if this is paged media.
299    pub fn is_paged(&self) -> bool {
300        matches!(self, Self::Paged { .. })
301    }
302}
303
304// Fragmentation State - Tracked During Layout
305
306/// State tracked during layout for fragmentation.
307/// This is created at the start of paged layout and updated as nodes are laid out.
308#[derive(Debug, Clone)]
309pub struct FragmentationState {
310    /// Current page being laid out (0-indexed)
311    pub current_page: usize,
312    /// Y position on current page (relative to page content area)
313    pub current_page_y: f32,
314    /// Available height remaining on current page
315    pub available_height: f32,
316    /// Full page content height
317    pub page_content_height: f32,
318    /// Page margins (not yet used, but needed for future)
319    pub margins_top: f32,
320    pub margins_bottom: f32,
321    /// Total number of pages so far
322    pub total_pages: usize,
323}
324
325impl FragmentationState {
326    /// Create a new fragmentation state for paged layout.
327    pub fn new(page_content_height: f32, margins_top: f32, margins_bottom: f32) -> Self {
328        Self {
329            current_page: 0,
330            current_page_y: 0.0,
331            available_height: page_content_height,
332            page_content_height,
333            margins_top,
334            margins_bottom,
335            total_pages: 1,
336        }
337    }
338
339    /// Check if content of the given height can fit on the current page.
340    pub fn can_fit(&self, height: f32) -> bool {
341        self.available_height >= height
342    }
343
344    /// Check if content would fit on an empty page.
345    pub fn would_fit_on_empty_page(&self, height: f32) -> bool {
346        height <= self.page_content_height
347    }
348
349    /// Use space on the current page.
350    pub fn use_space(&mut self, height: f32) {
351        self.current_page_y += height;
352        self.available_height = (self.page_content_height - self.current_page_y).max(0.0);
353    }
354
355    /// Advance to the next page.
356    pub fn advance_page(&mut self) {
357        self.current_page += 1;
358        self.current_page_y = 0.0;
359        self.available_height = self.page_content_height;
360        self.total_pages = self.total_pages.max(self.current_page + 1);
361    }
362
363    /// Calculate which page a Y position belongs to.
364    pub fn page_for_y(&self, y: f32) -> usize {
365        if self.page_content_height <= 0.0 {
366            return 0;
367        }
368        (y / self.page_content_height).floor() as usize
369    }
370
371    /// Calculate the Y offset for a given page (to convert to page-relative coordinates).
372    pub fn page_y_offset(&self, page: usize) -> f32 {
373        page as f32 * self.page_content_height
374    }
375}