Skip to main content

fop_layout/layout/engine/
types.rs

1//! Supporting types for the layout engine.
2//!
3//! Contains marker tracking, multi-column layout, float management,
4//! page context, and page region geometry structures.
5
6use fop_core::{tree::RetrievePosition, NodeId};
7use fop_types::Length;
8use std::collections::HashMap;
9
10/// Marker entry for tracking marker content on a page
11#[derive(Debug, Clone)]
12pub(super) struct MarkerEntry {
13    /// The FO node ID of the marker
14    pub(super) node_id: NodeId,
15    /// Whether the marker started on the current page
16    pub(super) starts_on_page: bool,
17    /// Whether the marker ends on the current page
18    pub(super) ends_on_page: bool,
19}
20
21/// Map for tracking markers by class name on the current page
22#[derive(Debug, Default)]
23pub(super) struct MarkerMap {
24    /// Markers organized by class name
25    /// Each class name maps to a list of markers in layout order
26    pub(super) markers: HashMap<String, Vec<MarkerEntry>>,
27}
28
29impl MarkerMap {
30    /// Create a new empty marker map
31    pub(super) fn new() -> Self {
32        Self {
33            markers: HashMap::new(),
34        }
35    }
36
37    /// Add a marker to the map
38    #[allow(dead_code)]
39    pub(super) fn add_marker(
40        &mut self,
41        class_name: String,
42        node_id: NodeId,
43        starts_on_page: bool,
44        ends_on_page: bool,
45    ) {
46        let entry = MarkerEntry {
47            node_id,
48            starts_on_page,
49            ends_on_page,
50        };
51
52        self.markers.entry(class_name).or_default().push(entry);
53    }
54
55    /// Retrieve a marker based on the retrieve position
56    pub(super) fn retrieve_marker(
57        &self,
58        class_name: &str,
59        position: RetrievePosition,
60    ) -> Option<NodeId> {
61        let entries = self.markers.get(class_name)?;
62
63        match position {
64            RetrievePosition::FirstStartingWithinPage => {
65                // First marker that starts on this page
66                entries.iter().find(|e| e.starts_on_page).map(|e| e.node_id)
67            }
68            RetrievePosition::FirstIncludingCarryover => {
69                // First marker including those from previous pages
70                entries.first().map(|e| e.node_id)
71            }
72            RetrievePosition::LastStartingWithinPage => {
73                // Last marker that starts on this page
74                entries
75                    .iter()
76                    .rev()
77                    .find(|e| e.starts_on_page)
78                    .map(|e| e.node_id)
79            }
80            RetrievePosition::LastEndingWithinPage => {
81                // Last marker that ends on this page
82                entries
83                    .iter()
84                    .rev()
85                    .find(|e| e.ends_on_page)
86                    .map(|e| e.node_id)
87            }
88        }
89    }
90
91    /// Clear all markers (for new page)
92    pub(super) fn clear(&mut self) {
93        self.markers.clear();
94    }
95}
96
97/// Geometry of all page regions derived from a simple-page-master
98///
99/// Holds the computed rectangles for each of the five XSL-FO page regions.
100/// Dimensions are computed from the page-master's page size, margins, and
101/// region extents.
102#[derive(Debug, Clone, Copy)]
103pub(super) struct PageRegionGeometry {
104    /// Total page width (from page-master page-width attribute)
105    pub page_width: Length,
106    /// Total page height (from page-master page-height attribute)
107    pub page_height: Length,
108    /// Rectangle for region-before (header)
109    pub before_rect: fop_types::Rect,
110    /// Rectangle for region-after (footer)
111    pub after_rect: fop_types::Rect,
112    /// Rectangle for region-start (left sidebar)
113    pub start_rect: fop_types::Rect,
114    /// Rectangle for region-end (right sidebar)
115    pub end_rect: fop_types::Rect,
116    /// Rectangle for region-body (main content)
117    pub body_rect: fop_types::Rect,
118}
119
120/// Multi-column layout configuration
121///
122/// Handles layout of content across multiple columns per CSS Multi-column
123/// Layout Module Level 1 specification.
124#[derive(Debug, Clone)]
125pub struct MultiColumnLayout {
126    /// Number of columns
127    pub column_count: i32,
128    /// Gap between columns
129    pub column_gap: Length,
130    /// Total available width
131    pub available_width: Length,
132    /// Width of each column
133    pub column_width: Length,
134    /// Current column index (0-based)
135    pub current_column: i32,
136    /// Current Y position within the current column
137    pub column_y: Length,
138    /// Maximum height per column (when page height is known)
139    pub max_column_height: Option<Length>,
140}
141
142impl MultiColumnLayout {
143    /// Create a new multi-column layout
144    pub fn new(column_count: i32, column_gap: Length, available_width: Length) -> Self {
145        // Calculate column width: (page_width - (n-1)*gap) / n
146        let total_gap = column_gap * (column_count - 1);
147        let column_width = (available_width - total_gap) / column_count;
148
149        Self {
150            column_count,
151            column_gap,
152            available_width,
153            column_width,
154            current_column: 0,
155            column_y: Length::ZERO,
156            max_column_height: None,
157        }
158    }
159
160    /// Set the maximum column height (for balancing and page breaks)
161    pub fn with_max_height(mut self, max_height: Length) -> Self {
162        self.max_column_height = Some(max_height);
163        self
164    }
165
166    /// Get the X offset for the current column
167    pub fn current_column_x(&self) -> Length {
168        (self.column_width + self.column_gap) * self.current_column
169    }
170
171    /// Check if the current column is filled (exceeds max height)
172    pub fn is_column_filled(&self, content_height: Length) -> bool {
173        if let Some(max_height) = self.max_column_height {
174            self.column_y + content_height > max_height
175        } else {
176            false
177        }
178    }
179
180    /// Move to the next column
181    pub fn next_column(&mut self) -> bool {
182        if self.current_column + 1 < self.column_count {
183            self.current_column += 1;
184            self.column_y = Length::ZERO;
185            true
186        } else {
187            // All columns are filled - need new page
188            false
189        }
190    }
191
192    /// Allocate space in the current column
193    pub fn allocate(&mut self, height: Length) -> (Length, Length) {
194        let x = self.current_column_x();
195        let y = self.column_y;
196        self.column_y += height;
197        (x, y)
198    }
199
200    /// Reset for a new page
201    pub fn reset(&mut self) {
202        self.current_column = 0;
203        self.column_y = Length::ZERO;
204    }
205
206    /// Get the number of columns
207    pub fn column_count(&self) -> i32 {
208        self.column_count
209    }
210
211    /// Get the width of each column
212    pub fn column_width(&self) -> Length {
213        self.column_width
214    }
215}
216
217/// Float side values for float property
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum FloatSide {
220    /// Float to the left
221    Left,
222    /// Float to the right
223    Right,
224    /// Float to the start edge (left in LTR, right in RTL)
225    Start,
226    /// Float to the end edge (right in LTR, left in RTL)
227    End,
228    /// Float inside (start on left pages, end on right pages)
229    Inside,
230    /// Float outside (end on left pages, start on right pages)
231    Outside,
232    /// No float
233    None,
234}
235
236/// Clear values for clear property
237#[allow(dead_code)]
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum ClearSide {
240    /// Clear past left floats
241    Left,
242    /// Clear past right floats
243    Right,
244    /// Clear past both left and right floats
245    Both,
246    /// Clear past start floats
247    Start,
248    /// Clear past end floats
249    End,
250    /// No clearing
251    None,
252}
253
254/// Information about a floating element
255#[derive(Debug, Clone)]
256pub(super) struct FloatInfo {
257    /// The area ID of the float
258    #[allow(dead_code)]
259    pub(super) area_id: crate::area::AreaId,
260    /// Side the float is on (left or right)
261    pub(super) side: FloatSide,
262    /// Top Y position of the float
263    pub(super) top: Length,
264    /// Bottom Y position of the float
265    pub(super) bottom: Length,
266    /// Width of the float
267    pub(super) width: Length,
268}
269
270/// Manages active floating elements and calculates available space
271#[derive(Debug, Default)]
272pub(super) struct FloatManager {
273    /// Currently active left floats
274    pub(super) left_floats: Vec<FloatInfo>,
275    /// Currently active right floats
276    pub(super) right_floats: Vec<FloatInfo>,
277}
278
279impl FloatManager {
280    /// Create a new empty float manager
281    pub(super) fn new() -> Self {
282        Self {
283            left_floats: Vec::new(),
284            right_floats: Vec::new(),
285        }
286    }
287
288    /// Add a float to the manager
289    ///
290    /// # Parameters
291    /// - `float`: The float information to add
292    /// - `is_odd_page`: Whether the current page is odd-numbered (used for inside/outside positioning)
293    pub(super) fn add_float(&mut self, float: FloatInfo, is_odd_page: bool) {
294        match float.side {
295            FloatSide::Left | FloatSide::Start => {
296                self.left_floats.push(float);
297            }
298            FloatSide::Right | FloatSide::End => {
299                self.right_floats.push(float);
300            }
301            FloatSide::Inside => {
302                // Inside = verso (left page) uses right side, recto (right page) uses left side
303                // Odd pages are recto (right pages), even pages are verso (left pages)
304                if is_odd_page {
305                    // Recto page (right) → inside is left
306                    self.left_floats.push(float);
307                } else {
308                    // Verso page (left) → inside is right
309                    self.right_floats.push(float);
310                }
311            }
312            FloatSide::Outside => {
313                // Outside = verso (left page) uses left side, recto (right page) uses right side
314                if is_odd_page {
315                    // Recto page (right) → outside is right
316                    self.right_floats.push(float);
317                } else {
318                    // Verso page (left) → outside is left
319                    self.left_floats.push(float);
320                }
321            }
322            FloatSide::None => {}
323        }
324    }
325
326    /// Get available width at a given Y position
327    pub(super) fn available_width(&self, y: Length, container_width: Length) -> (Length, Length) {
328        let left_offset = self.get_left_offset(y);
329        let right_offset = self.get_right_offset(y);
330        let available = container_width - left_offset - right_offset;
331        (left_offset, available)
332    }
333
334    /// Get the left offset (space taken by left floats) at a given Y position
335    pub(super) fn get_left_offset(&self, y: Length) -> Length {
336        self.left_floats
337            .iter()
338            .filter(|f| f.top <= y && y < f.bottom)
339            .map(|f| f.width)
340            .fold(Length::ZERO, |acc, w| acc + w)
341    }
342
343    /// Get the right offset (space taken by right floats) at a given Y position
344    pub(super) fn get_right_offset(&self, y: Length) -> Length {
345        self.right_floats
346            .iter()
347            .filter(|f| f.top <= y && y < f.bottom)
348            .map(|f| f.width)
349            .fold(Length::ZERO, |acc, w| acc + w)
350    }
351
352    /// Get the Y position to clear past floats
353    #[allow(dead_code)]
354    pub(super) fn get_clear_position(&self, clear: ClearSide, current_y: Length) -> Length {
355        match clear {
356            ClearSide::Left | ClearSide::Start => self
357                .left_floats
358                .iter()
359                .filter(|f| f.bottom > current_y)
360                .map(|f| f.bottom)
361                .max()
362                .unwrap_or(current_y),
363            ClearSide::Right | ClearSide::End => self
364                .right_floats
365                .iter()
366                .filter(|f| f.bottom > current_y)
367                .map(|f| f.bottom)
368                .max()
369                .unwrap_or(current_y),
370            ClearSide::Both => {
371                let left_bottom = self
372                    .left_floats
373                    .iter()
374                    .filter(|f| f.bottom > current_y)
375                    .map(|f| f.bottom)
376                    .max()
377                    .unwrap_or(current_y);
378                let right_bottom = self
379                    .right_floats
380                    .iter()
381                    .filter(|f| f.bottom > current_y)
382                    .map(|f| f.bottom)
383                    .max()
384                    .unwrap_or(current_y);
385                left_bottom.max(right_bottom)
386            }
387            ClearSide::None => current_y,
388        }
389    }
390
391    /// Remove floats that are above the given Y position (no longer affecting layout)
392    pub(super) fn remove_floats_above(&mut self, y: Length) {
393        self.left_floats.retain(|f| f.bottom > y);
394        self.right_floats.retain(|f| f.bottom > y);
395    }
396
397    /// Clear all floats
398    pub(super) fn clear(&mut self) {
399        self.left_floats.clear();
400        self.right_floats.clear();
401    }
402}
403
404/// Page context for tracking page position within a sequence
405#[derive(Debug, Clone)]
406#[allow(dead_code)]
407pub(super) struct PageContext {
408    /// Current page number within the sequence (1-based)
409    pub(super) page_number: usize,
410    /// Total number of pages in the sequence (if known)
411    pub(super) total_pages: Option<usize>,
412    /// Whether this is the first page
413    pub(super) is_first: bool,
414    /// Whether this is the last page (only known if total_pages is known)
415    pub(super) is_last: bool,
416}
417
418impl PageContext {
419    /// Create a new page context for the first page
420    #[allow(dead_code)]
421    pub(super) fn new() -> Self {
422        Self {
423            page_number: 1,
424            total_pages: None,
425            is_first: true,
426            is_last: false,
427        }
428    }
429
430    /// Check if this is an odd-numbered page
431    #[allow(dead_code)]
432    pub(super) fn is_odd_page(&self) -> bool {
433        self.page_number % 2 == 1
434    }
435
436    /// Check if this is an even-numbered page
437    #[allow(dead_code)]
438    pub(super) fn is_even_page(&self) -> bool {
439        self.page_number.is_multiple_of(2)
440    }
441
442    /// Check if this is the first page
443    #[allow(dead_code)]
444    pub(super) fn is_first_page(&self) -> bool {
445        self.is_first
446    }
447
448    /// Check if this is the last page
449    #[allow(dead_code)]
450    pub(super) fn is_last_page(&self) -> bool {
451        self.is_last
452    }
453}
454
455/// Parse an XSL-FO length string (e.g., "10mm", "72pt") to a Length value.
456#[allow(dead_code)]
457pub(super) fn parse_fo_length(s: &str) -> Option<Length> {
458    if let Some(v) = s.strip_suffix("pt") {
459        v.parse::<f64>().ok().map(Length::from_pt)
460    } else if let Some(v) = s.strip_suffix("mm") {
461        v.parse::<f64>().ok().map(Length::from_mm)
462    } else if let Some(v) = s.strip_suffix("cm") {
463        v.parse::<f64>().ok().map(Length::from_cm)
464    } else if let Some(v) = s.strip_suffix("in") {
465        v.parse::<f64>().ok().map(Length::from_inch)
466    } else if let Some(v) = s.strip_suffix("px") {
467        v.parse::<f64>().ok().map(|px| Length::from_pt(px * 0.75))
468    } else {
469        None
470    }
471}