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}