fop-layout 0.1.1

Layout engine for Apache FOP Rust implementation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
//! Supporting types for the layout engine.
//!
//! Contains marker tracking, multi-column layout, float management,
//! page context, and page region geometry structures.

use fop_core::{tree::RetrievePosition, NodeId};
use fop_types::Length;
use std::collections::HashMap;

/// Marker entry for tracking marker content on a page
#[derive(Debug, Clone)]
pub(super) struct MarkerEntry {
    /// The FO node ID of the marker
    pub(super) node_id: NodeId,
    /// Whether the marker started on the current page
    pub(super) starts_on_page: bool,
    /// Whether the marker ends on the current page
    pub(super) ends_on_page: bool,
}

/// Map for tracking markers by class name on the current page
#[derive(Debug, Default)]
pub(super) struct MarkerMap {
    /// Markers organized by class name
    /// Each class name maps to a list of markers in layout order
    pub(super) markers: HashMap<String, Vec<MarkerEntry>>,
}

impl MarkerMap {
    /// Create a new empty marker map
    pub(super) fn new() -> Self {
        Self {
            markers: HashMap::new(),
        }
    }

    /// Add a marker to the map
    #[allow(dead_code)]
    pub(super) fn add_marker(
        &mut self,
        class_name: String,
        node_id: NodeId,
        starts_on_page: bool,
        ends_on_page: bool,
    ) {
        let entry = MarkerEntry {
            node_id,
            starts_on_page,
            ends_on_page,
        };

        self.markers.entry(class_name).or_default().push(entry);
    }

    /// Retrieve a marker based on the retrieve position
    pub(super) fn retrieve_marker(
        &self,
        class_name: &str,
        position: RetrievePosition,
    ) -> Option<NodeId> {
        let entries = self.markers.get(class_name)?;

        match position {
            RetrievePosition::FirstStartingWithinPage => {
                // First marker that starts on this page
                entries.iter().find(|e| e.starts_on_page).map(|e| e.node_id)
            }
            RetrievePosition::FirstIncludingCarryover => {
                // First marker including those from previous pages
                entries.first().map(|e| e.node_id)
            }
            RetrievePosition::LastStartingWithinPage => {
                // Last marker that starts on this page
                entries
                    .iter()
                    .rev()
                    .find(|e| e.starts_on_page)
                    .map(|e| e.node_id)
            }
            RetrievePosition::LastEndingWithinPage => {
                // Last marker that ends on this page
                entries
                    .iter()
                    .rev()
                    .find(|e| e.ends_on_page)
                    .map(|e| e.node_id)
            }
        }
    }

    /// Clear all markers (for new page)
    pub(super) fn clear(&mut self) {
        self.markers.clear();
    }
}

/// Geometry of all page regions derived from a simple-page-master
///
/// Holds the computed rectangles for each of the five XSL-FO page regions.
/// Dimensions are computed from the page-master's page size, margins, and
/// region extents.
#[derive(Debug, Clone, Copy)]
pub(super) struct PageRegionGeometry {
    /// Total page width (from page-master page-width attribute)
    pub page_width: Length,
    /// Total page height (from page-master page-height attribute)
    pub page_height: Length,
    /// Rectangle for region-before (header)
    pub before_rect: fop_types::Rect,
    /// Rectangle for region-after (footer)
    pub after_rect: fop_types::Rect,
    /// Rectangle for region-start (left sidebar)
    pub start_rect: fop_types::Rect,
    /// Rectangle for region-end (right sidebar)
    pub end_rect: fop_types::Rect,
    /// Rectangle for region-body (main content)
    pub body_rect: fop_types::Rect,
}

/// Multi-column layout configuration
///
/// Handles layout of content across multiple columns per CSS Multi-column
/// Layout Module Level 1 specification.
#[derive(Debug, Clone)]
pub struct MultiColumnLayout {
    /// Number of columns
    pub column_count: i32,
    /// Gap between columns
    pub column_gap: Length,
    /// Total available width
    pub available_width: Length,
    /// Width of each column
    pub column_width: Length,
    /// Current column index (0-based)
    pub current_column: i32,
    /// Current Y position within the current column
    pub column_y: Length,
    /// Maximum height per column (when page height is known)
    pub max_column_height: Option<Length>,
}

impl MultiColumnLayout {
    /// Create a new multi-column layout
    pub fn new(column_count: i32, column_gap: Length, available_width: Length) -> Self {
        // Calculate column width: (page_width - (n-1)*gap) / n
        let total_gap = column_gap * (column_count - 1);
        let column_width = (available_width - total_gap) / column_count;

        Self {
            column_count,
            column_gap,
            available_width,
            column_width,
            current_column: 0,
            column_y: Length::ZERO,
            max_column_height: None,
        }
    }

    /// Set the maximum column height (for balancing and page breaks)
    pub fn with_max_height(mut self, max_height: Length) -> Self {
        self.max_column_height = Some(max_height);
        self
    }

    /// Get the X offset for the current column
    pub fn current_column_x(&self) -> Length {
        (self.column_width + self.column_gap) * self.current_column
    }

    /// Check if the current column is filled (exceeds max height)
    pub fn is_column_filled(&self, content_height: Length) -> bool {
        if let Some(max_height) = self.max_column_height {
            self.column_y + content_height > max_height
        } else {
            false
        }
    }

    /// Move to the next column
    pub fn next_column(&mut self) -> bool {
        if self.current_column + 1 < self.column_count {
            self.current_column += 1;
            self.column_y = Length::ZERO;
            true
        } else {
            // All columns are filled - need new page
            false
        }
    }

    /// Allocate space in the current column
    pub fn allocate(&mut self, height: Length) -> (Length, Length) {
        let x = self.current_column_x();
        let y = self.column_y;
        self.column_y += height;
        (x, y)
    }

    /// Reset for a new page
    pub fn reset(&mut self) {
        self.current_column = 0;
        self.column_y = Length::ZERO;
    }

    /// Get the number of columns
    pub fn column_count(&self) -> i32 {
        self.column_count
    }

    /// Get the width of each column
    pub fn column_width(&self) -> Length {
        self.column_width
    }
}

/// Float side values for float property
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FloatSide {
    /// Float to the left
    Left,
    /// Float to the right
    Right,
    /// Float to the start edge (left in LTR, right in RTL)
    Start,
    /// Float to the end edge (right in LTR, left in RTL)
    End,
    /// Float inside (start on left pages, end on right pages)
    Inside,
    /// Float outside (end on left pages, start on right pages)
    Outside,
    /// No float
    None,
}

/// Clear values for clear property
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClearSide {
    /// Clear past left floats
    Left,
    /// Clear past right floats
    Right,
    /// Clear past both left and right floats
    Both,
    /// Clear past start floats
    Start,
    /// Clear past end floats
    End,
    /// No clearing
    None,
}

/// Information about a floating element
#[derive(Debug, Clone)]
pub(super) struct FloatInfo {
    /// The area ID of the float
    #[allow(dead_code)]
    pub(super) area_id: crate::area::AreaId,
    /// Side the float is on (left or right)
    pub(super) side: FloatSide,
    /// Top Y position of the float
    pub(super) top: Length,
    /// Bottom Y position of the float
    pub(super) bottom: Length,
    /// Width of the float
    pub(super) width: Length,
}

/// Manages active floating elements and calculates available space
#[derive(Debug, Default)]
pub(super) struct FloatManager {
    /// Currently active left floats
    pub(super) left_floats: Vec<FloatInfo>,
    /// Currently active right floats
    pub(super) right_floats: Vec<FloatInfo>,
}

impl FloatManager {
    /// Create a new empty float manager
    pub(super) fn new() -> Self {
        Self {
            left_floats: Vec::new(),
            right_floats: Vec::new(),
        }
    }

    /// Add a float to the manager
    ///
    /// # Parameters
    /// - `float`: The float information to add
    /// - `is_odd_page`: Whether the current page is odd-numbered (used for inside/outside positioning)
    pub(super) fn add_float(&mut self, float: FloatInfo, is_odd_page: bool) {
        match float.side {
            FloatSide::Left | FloatSide::Start => {
                self.left_floats.push(float);
            }
            FloatSide::Right | FloatSide::End => {
                self.right_floats.push(float);
            }
            FloatSide::Inside => {
                // Inside = verso (left page) uses right side, recto (right page) uses left side
                // Odd pages are recto (right pages), even pages are verso (left pages)
                if is_odd_page {
                    // Recto page (right) → inside is left
                    self.left_floats.push(float);
                } else {
                    // Verso page (left) → inside is right
                    self.right_floats.push(float);
                }
            }
            FloatSide::Outside => {
                // Outside = verso (left page) uses left side, recto (right page) uses right side
                if is_odd_page {
                    // Recto page (right) → outside is right
                    self.right_floats.push(float);
                } else {
                    // Verso page (left) → outside is left
                    self.left_floats.push(float);
                }
            }
            FloatSide::None => {}
        }
    }

    /// Get available width at a given Y position
    pub(super) fn available_width(&self, y: Length, container_width: Length) -> (Length, Length) {
        let left_offset = self.get_left_offset(y);
        let right_offset = self.get_right_offset(y);
        let available = container_width - left_offset - right_offset;
        (left_offset, available)
    }

    /// Get the left offset (space taken by left floats) at a given Y position
    pub(super) fn get_left_offset(&self, y: Length) -> Length {
        self.left_floats
            .iter()
            .filter(|f| f.top <= y && y < f.bottom)
            .map(|f| f.width)
            .fold(Length::ZERO, |acc, w| acc + w)
    }

    /// Get the right offset (space taken by right floats) at a given Y position
    pub(super) fn get_right_offset(&self, y: Length) -> Length {
        self.right_floats
            .iter()
            .filter(|f| f.top <= y && y < f.bottom)
            .map(|f| f.width)
            .fold(Length::ZERO, |acc, w| acc + w)
    }

    /// Get the Y position to clear past floats
    #[allow(dead_code)]
    pub(super) fn get_clear_position(&self, clear: ClearSide, current_y: Length) -> Length {
        match clear {
            ClearSide::Left | ClearSide::Start => self
                .left_floats
                .iter()
                .filter(|f| f.bottom > current_y)
                .map(|f| f.bottom)
                .max()
                .unwrap_or(current_y),
            ClearSide::Right | ClearSide::End => self
                .right_floats
                .iter()
                .filter(|f| f.bottom > current_y)
                .map(|f| f.bottom)
                .max()
                .unwrap_or(current_y),
            ClearSide::Both => {
                let left_bottom = self
                    .left_floats
                    .iter()
                    .filter(|f| f.bottom > current_y)
                    .map(|f| f.bottom)
                    .max()
                    .unwrap_or(current_y);
                let right_bottom = self
                    .right_floats
                    .iter()
                    .filter(|f| f.bottom > current_y)
                    .map(|f| f.bottom)
                    .max()
                    .unwrap_or(current_y);
                left_bottom.max(right_bottom)
            }
            ClearSide::None => current_y,
        }
    }

    /// Remove floats that are above the given Y position (no longer affecting layout)
    pub(super) fn remove_floats_above(&mut self, y: Length) {
        self.left_floats.retain(|f| f.bottom > y);
        self.right_floats.retain(|f| f.bottom > y);
    }

    /// Clear all floats
    pub(super) fn clear(&mut self) {
        self.left_floats.clear();
        self.right_floats.clear();
    }
}

/// Page context for tracking page position within a sequence
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(super) struct PageContext {
    /// Current page number within the sequence (1-based)
    pub(super) page_number: usize,
    /// Total number of pages in the sequence (if known)
    pub(super) total_pages: Option<usize>,
    /// Whether this is the first page
    pub(super) is_first: bool,
    /// Whether this is the last page (only known if total_pages is known)
    pub(super) is_last: bool,
}

impl PageContext {
    /// Create a new page context for the first page
    #[allow(dead_code)]
    pub(super) fn new() -> Self {
        Self {
            page_number: 1,
            total_pages: None,
            is_first: true,
            is_last: false,
        }
    }

    /// Check if this is an odd-numbered page
    #[allow(dead_code)]
    pub(super) fn is_odd_page(&self) -> bool {
        self.page_number % 2 == 1
    }

    /// Check if this is an even-numbered page
    #[allow(dead_code)]
    pub(super) fn is_even_page(&self) -> bool {
        self.page_number.is_multiple_of(2)
    }

    /// Check if this is the first page
    #[allow(dead_code)]
    pub(super) fn is_first_page(&self) -> bool {
        self.is_first
    }

    /// Check if this is the last page
    #[allow(dead_code)]
    pub(super) fn is_last_page(&self) -> bool {
        self.is_last
    }
}

/// Parse an XSL-FO length string (e.g., "10mm", "72pt") to a Length value.
#[allow(dead_code)]
pub(super) fn parse_fo_length(s: &str) -> Option<Length> {
    if let Some(v) = s.strip_suffix("pt") {
        v.parse::<f64>().ok().map(Length::from_pt)
    } else if let Some(v) = s.strip_suffix("mm") {
        v.parse::<f64>().ok().map(Length::from_mm)
    } else if let Some(v) = s.strip_suffix("cm") {
        v.parse::<f64>().ok().map(Length::from_cm)
    } else if let Some(v) = s.strip_suffix("in") {
        v.parse::<f64>().ok().map(Length::from_inch)
    } else if let Some(v) = s.strip_suffix("px") {
        v.parse::<f64>().ok().map(|px| Length::from_pt(px * 0.75))
    } else {
        None
    }
}