Skip to main content

fop_layout/layout/
block.rs

1//! Block-level layout algorithms
2//!
3//! Handles vertical stacking of block-level areas.
4
5use fop_types::{Length, Point, Rect, Result, Size};
6
7/// Block layout context
8pub struct BlockLayoutContext {
9    /// Available width for blocks
10    pub available_width: Length,
11
12    /// Current Y position
13    pub current_y: Length,
14
15    /// Maximum width seen
16    pub max_width: Length,
17}
18
19impl BlockLayoutContext {
20    /// Create a new block layout context
21    pub fn new(available_width: Length) -> Self {
22        Self {
23            available_width,
24            current_y: Length::ZERO,
25            max_width: Length::ZERO,
26        }
27    }
28
29    /// Allocate space for a block with optional space-before/space-after
30    pub fn allocate(&mut self, width: Length, height: Length) -> Rect {
31        let rect = Rect::from_point_size(
32            Point::new(Length::ZERO, self.current_y),
33            Size::new(width, height),
34        );
35
36        self.current_y += height;
37        if width > self.max_width {
38            self.max_width = width;
39        }
40
41        rect
42    }
43
44    /// Allocate space with space-before and space-after
45    pub fn allocate_with_spacing(
46        &mut self,
47        width: Length,
48        height: Length,
49        space_before: Length,
50        space_after: Length,
51    ) -> Rect {
52        // Add space before
53        self.current_y += space_before;
54
55        let rect = Rect::from_point_size(
56            Point::new(Length::ZERO, self.current_y),
57            Size::new(width, height),
58        );
59
60        self.current_y += height;
61        self.current_y += space_after;
62
63        if width > self.max_width {
64            self.max_width = width;
65        }
66
67        rect
68    }
69
70    /// Add vertical space (for explicit spacing)
71    pub fn add_space(&mut self, space: Length) {
72        self.current_y += space;
73    }
74
75    /// Get the total height allocated
76    pub fn total_height(&self) -> Length {
77        self.current_y
78    }
79}
80
81/// Stack blocks vertically
82pub fn stack_blocks(
83    blocks: Vec<(Length, Length)>, // (width, height) pairs
84    available_width: Length,
85) -> Result<Vec<Rect>> {
86    let mut context = BlockLayoutContext::new(available_width);
87    let mut rects = Vec::new();
88
89    for (width, height) in blocks {
90        let rect = context.allocate(width, height);
91        rects.push(rect);
92    }
93
94    Ok(rects)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    // ── BlockLayoutContext construction ──────────────────────────────────────
102
103    #[test]
104    fn test_block_context_new_initial_state() {
105        let ctx = BlockLayoutContext::new(Length::from_pt(200.0));
106        assert_eq!(ctx.available_width, Length::from_pt(200.0));
107        assert_eq!(ctx.current_y, Length::ZERO);
108        assert_eq!(ctx.max_width, Length::ZERO);
109    }
110
111    #[test]
112    fn test_block_context_zero_width() {
113        let ctx = BlockLayoutContext::new(Length::ZERO);
114        assert_eq!(ctx.available_width, Length::ZERO);
115        assert_eq!(ctx.current_y, Length::ZERO);
116        assert_eq!(ctx.max_width, Length::ZERO);
117    }
118
119    // ── allocate: position and size ─────────────────────────────────────────
120
121    #[test]
122    fn test_allocate_first_block_starts_at_origin() {
123        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
124        let rect = ctx.allocate(Length::from_pt(80.0), Length::from_pt(25.0));
125        assert_eq!(rect.x, Length::ZERO);
126        assert_eq!(rect.y, Length::ZERO);
127        assert_eq!(rect.width, Length::from_pt(80.0));
128        assert_eq!(rect.height, Length::from_pt(25.0));
129    }
130
131    #[test]
132    fn test_allocate_advances_current_y() {
133        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
134        ctx.allocate(Length::from_pt(100.0), Length::from_pt(20.0));
135        assert_eq!(ctx.current_y, Length::from_pt(20.0));
136    }
137
138    #[test]
139    fn test_allocate_second_block_stacks_below_first() {
140        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
141        let _r1 = ctx.allocate(Length::from_pt(100.0), Length::from_pt(20.0));
142        let r2 = ctx.allocate(Length::from_pt(100.0), Length::from_pt(30.0));
143        assert_eq!(r2.y, Length::from_pt(20.0));
144        assert_eq!(r2.height, Length::from_pt(30.0));
145    }
146
147    #[test]
148    fn test_allocate_total_height_accumulates() {
149        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
150        ctx.allocate(Length::from_pt(100.0), Length::from_pt(10.0));
151        ctx.allocate(Length::from_pt(100.0), Length::from_pt(20.0));
152        ctx.allocate(Length::from_pt(100.0), Length::from_pt(30.0));
153        assert_eq!(ctx.total_height(), Length::from_pt(60.0));
154    }
155
156    // ── max_width tracking ───────────────────────────────────────────────────
157
158    #[test]
159    fn test_max_width_updated_on_wider_block() {
160        let mut ctx = BlockLayoutContext::new(Length::from_pt(200.0));
161        ctx.allocate(Length::from_pt(80.0), Length::from_pt(10.0));
162        assert_eq!(ctx.max_width, Length::from_pt(80.0));
163        ctx.allocate(Length::from_pt(120.0), Length::from_pt(10.0));
164        assert_eq!(ctx.max_width, Length::from_pt(120.0));
165    }
166
167    #[test]
168    fn test_max_width_not_reduced_by_narrower_block() {
169        let mut ctx = BlockLayoutContext::new(Length::from_pt(200.0));
170        ctx.allocate(Length::from_pt(120.0), Length::from_pt(10.0));
171        ctx.allocate(Length::from_pt(60.0), Length::from_pt(10.0));
172        assert_eq!(ctx.max_width, Length::from_pt(120.0));
173    }
174
175    #[test]
176    fn test_max_width_equal_block_does_not_change() {
177        let mut ctx = BlockLayoutContext::new(Length::from_pt(200.0));
178        ctx.allocate(Length::from_pt(100.0), Length::from_pt(10.0));
179        ctx.allocate(Length::from_pt(100.0), Length::from_pt(10.0));
180        assert_eq!(ctx.max_width, Length::from_pt(100.0));
181    }
182
183    // ── allocate_with_spacing: space-before / space-after ───────────────────
184
185    #[test]
186    fn test_spacing_space_before_offsets_rect_y() {
187        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
188        let rect = ctx.allocate_with_spacing(
189            Length::from_pt(100.0),
190            Length::from_pt(20.0),
191            Length::from_pt(10.0), // space-before
192            Length::ZERO,
193        );
194        assert_eq!(rect.y, Length::from_pt(10.0));
195    }
196
197    #[test]
198    fn test_spacing_space_after_advances_current_y() {
199        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
200        ctx.allocate_with_spacing(
201            Length::from_pt(100.0),
202            Length::from_pt(20.0),
203            Length::ZERO,
204            Length::from_pt(5.0), // space-after
205        );
206        // 0 (space-before) + 20 (height) + 5 (space-after) = 25
207        assert_eq!(ctx.current_y, Length::from_pt(25.0));
208    }
209
210    #[test]
211    fn test_spacing_combined_space_before_and_after() {
212        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
213        let rect = ctx.allocate_with_spacing(
214            Length::from_pt(100.0),
215            Length::from_pt(20.0),
216            Length::from_pt(10.0),
217            Length::from_pt(5.0),
218        );
219        // rect starts at space-before = 10
220        assert_eq!(rect.y, Length::from_pt(10.0));
221        assert_eq!(rect.height, Length::from_pt(20.0));
222        // current_y = 10 + 20 + 5 = 35
223        assert_eq!(ctx.current_y, Length::from_pt(35.0));
224        assert_eq!(ctx.total_height(), Length::from_pt(35.0));
225    }
226
227    #[test]
228    fn test_spacing_second_block_stacks_correctly() {
229        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
230        ctx.allocate_with_spacing(
231            Length::from_pt(100.0),
232            Length::from_pt(20.0),
233            Length::from_pt(10.0),
234            Length::from_pt(5.0),
235        );
236        // current_y = 35 after first block
237        let rect2 = ctx.allocate_with_spacing(
238            Length::from_pt(100.0),
239            Length::from_pt(15.0),
240            Length::from_pt(8.0),
241            Length::from_pt(12.0),
242        );
243        // rect2.y = 35 + 8 = 43
244        assert_eq!(rect2.y, Length::from_pt(43.0));
245        // current_y = 43 + 15 + 12 = 70
246        assert_eq!(ctx.total_height(), Length::from_pt(70.0));
247    }
248
249    #[test]
250    fn test_spacing_zero_behaves_like_plain_allocate() {
251        let mut ctx_plain = BlockLayoutContext::new(Length::from_pt(100.0));
252        let mut ctx_spaced = BlockLayoutContext::new(Length::from_pt(100.0));
253
254        let r1 = ctx_plain.allocate(Length::from_pt(80.0), Length::from_pt(20.0));
255        let r2 = ctx_spaced.allocate_with_spacing(
256            Length::from_pt(80.0),
257            Length::from_pt(20.0),
258            Length::ZERO,
259            Length::ZERO,
260        );
261
262        assert_eq!(r1.x, r2.x);
263        assert_eq!(r1.y, r2.y);
264        assert_eq!(r1.width, r2.width);
265        assert_eq!(r1.height, r2.height);
266        assert_eq!(ctx_plain.total_height(), ctx_spaced.total_height());
267    }
268
269    // ── add_space: explicit spacing ──────────────────────────────────────────
270
271    #[test]
272    fn test_add_space_increases_current_y() {
273        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
274        ctx.add_space(Length::from_pt(15.0));
275        assert_eq!(ctx.current_y, Length::from_pt(15.0));
276        assert_eq!(ctx.total_height(), Length::from_pt(15.0));
277    }
278
279    #[test]
280    fn test_add_space_cumulative() {
281        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
282        ctx.add_space(Length::from_pt(10.0));
283        ctx.add_space(Length::from_pt(5.0));
284        assert_eq!(ctx.total_height(), Length::from_pt(15.0));
285    }
286
287    #[test]
288    fn test_add_space_after_block() {
289        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
290        ctx.allocate(Length::from_pt(100.0), Length::from_pt(20.0));
291        ctx.add_space(Length::from_pt(10.0));
292        assert_eq!(ctx.total_height(), Length::from_pt(30.0));
293    }
294
295    // ── overflow detection ───────────────────────────────────────────────────
296
297    #[test]
298    fn test_overflow_detected_when_content_exceeds_page_height() {
299        let page_height = Length::from_pt(100.0);
300        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
301        ctx.allocate(Length::from_pt(100.0), Length::from_pt(60.0));
302        ctx.allocate(Length::from_pt(100.0), Length::from_pt(50.0));
303        // Total = 110 > 100 (page height)
304        assert!(ctx.total_height() > page_height);
305    }
306
307    #[test]
308    fn test_no_overflow_when_content_fits_exactly() {
309        let page_height = Length::from_pt(100.0);
310        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
311        ctx.allocate(Length::from_pt(100.0), Length::from_pt(40.0));
312        ctx.allocate(Length::from_pt(100.0), Length::from_pt(60.0));
313        // Total = 100 exactly fits
314        assert!(ctx.total_height() <= page_height);
315    }
316
317    // ── stack_blocks convenience function ───────────────────────────────────
318
319    #[test]
320    fn test_stack_blocks_empty_input() {
321        let rects = stack_blocks(vec![], Length::from_pt(100.0)).expect("test: should succeed");
322        assert!(rects.is_empty());
323    }
324
325    #[test]
326    fn test_stack_blocks_single_block() {
327        let blocks = vec![(Length::from_pt(80.0), Length::from_pt(30.0))];
328        let rects = stack_blocks(blocks, Length::from_pt(100.0)).expect("test: should succeed");
329        assert_eq!(rects.len(), 1);
330        assert_eq!(rects[0].x, Length::ZERO);
331        assert_eq!(rects[0].y, Length::ZERO);
332        assert_eq!(rects[0].width, Length::from_pt(80.0));
333        assert_eq!(rects[0].height, Length::from_pt(30.0));
334    }
335
336    #[test]
337    fn test_stack_blocks_three_blocks() {
338        let blocks = vec![
339            (Length::from_pt(100.0), Length::from_pt(20.0)),
340            (Length::from_pt(100.0), Length::from_pt(30.0)),
341            (Length::from_pt(100.0), Length::from_pt(25.0)),
342        ];
343        let rects = stack_blocks(blocks, Length::from_pt(100.0)).expect("test: should succeed");
344        assert_eq!(rects.len(), 3);
345        assert_eq!(rects[0].y, Length::ZERO);
346        assert_eq!(rects[1].y, Length::from_pt(20.0));
347        assert_eq!(rects[2].y, Length::from_pt(50.0));
348    }
349
350    #[test]
351    fn test_stack_blocks_x_always_zero() {
352        let blocks = vec![
353            (Length::from_pt(50.0), Length::from_pt(10.0)),
354            (Length::from_pt(70.0), Length::from_pt(10.0)),
355        ];
356        let rects = stack_blocks(blocks, Length::from_pt(100.0)).expect("test: should succeed");
357        for rect in &rects {
358            assert_eq!(rect.x, Length::ZERO);
359        }
360    }
361
362    // ── padding/margin box vs content box ───────────────────────────────────
363
364    #[test]
365    fn test_padding_box_wider_than_content_box() {
366        // content width = 80, padding left = 10, padding right = 10 -> padding box = 100
367        let content_width = Length::from_pt(80.0);
368        let padding_left = Length::from_pt(10.0);
369        let padding_right = Length::from_pt(10.0);
370        let padding_box_width = content_width + padding_left + padding_right;
371        assert_eq!(padding_box_width, Length::from_pt(100.0));
372
373        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
374        let rect = ctx.allocate(padding_box_width, Length::from_pt(20.0));
375        assert_eq!(rect.width, Length::from_pt(100.0));
376    }
377
378    #[test]
379    fn test_block_height_from_border_and_content() {
380        // content height = 20, border-top = 2, border-bottom = 2
381        let border_top = Length::from_pt(2.0);
382        let border_bottom = Length::from_pt(2.0);
383        let content_height = Length::from_pt(20.0);
384        let margin_top = Length::from_pt(5.0);
385        let margin_bottom = Length::from_pt(5.0);
386
387        let mut ctx = BlockLayoutContext::new(Length::from_pt(100.0));
388        let rect = ctx.allocate_with_spacing(
389            Length::from_pt(100.0),
390            border_top + content_height + border_bottom,
391            margin_top,
392            margin_bottom,
393        );
394        // rect.y = margin_top = 5
395        assert_eq!(rect.y, Length::from_pt(5.0));
396        // rect.height = 2 + 20 + 2 = 24
397        assert_eq!(rect.height, Length::from_pt(24.0));
398        // total = 5 + 24 + 5 = 34
399        assert_eq!(ctx.total_height(), Length::from_pt(34.0));
400    }
401
402    // ── background bounds ────────────────────────────────────────────────────
403
404    #[test]
405    fn test_background_area_bounds_match_allocated_rect() {
406        // Background covers the padding box (content + padding, not margins)
407        let padding_w = Length::from_pt(100.0);
408        let padding_h = Length::from_pt(30.0);
409        let mut ctx = BlockLayoutContext::new(Length::from_pt(120.0));
410        let rect = ctx.allocate(padding_w, padding_h);
411        // The background rect should match the allocated rect (padding box)
412        assert_eq!(rect.width, padding_w);
413        assert_eq!(rect.height, padding_h);
414        assert_eq!(rect.x, Length::ZERO);
415        assert_eq!(rect.y, Length::ZERO);
416    }
417
418    // ── keep-together / keep-with-next logic ────────────────────────────────
419
420    #[test]
421    fn test_keep_constraint_keep_together_active() {
422        use crate::layout::{Keep, KeepConstraint};
423        let mut kc = KeepConstraint::new();
424        kc.keep_together = Keep::Always;
425        assert!(kc.must_keep_together());
426        assert!(kc.has_constraint());
427    }
428
429    #[test]
430    fn test_keep_constraint_keep_with_next_active() {
431        use crate::layout::{Keep, KeepConstraint};
432        let mut kc = KeepConstraint::new();
433        kc.keep_with_next = Keep::Always;
434        assert!(kc.must_keep_with_next());
435        assert!(kc.has_constraint());
436    }
437
438    #[test]
439    fn test_keep_constraint_default_all_inactive() {
440        use crate::layout::KeepConstraint;
441        let kc = KeepConstraint::new();
442        assert!(!kc.has_constraint());
443        assert!(!kc.must_keep_together());
444        assert!(!kc.must_keep_with_next());
445        assert!(!kc.must_keep_with_previous());
446    }
447
448    #[test]
449    fn test_keep_constraint_keep_with_previous_active() {
450        use crate::layout::{Keep, KeepConstraint};
451        let mut kc = KeepConstraint::new();
452        kc.keep_with_previous = Keep::Always;
453        assert!(kc.must_keep_with_previous());
454        assert!(kc.has_constraint());
455    }
456
457    #[test]
458    fn test_keep_integer_strength_ordering() {
459        use crate::layout::Keep;
460        let weak = Keep::Integer(3);
461        let strong = Keep::Integer(7);
462        assert!(weak.is_active());
463        assert!(strong.is_active());
464        assert!(strong.strength() > weak.strength());
465    }
466
467    // ── break handling ───────────────────────────────────────────────────────
468
469    #[test]
470    fn test_break_value_page_is_page_break() {
471        use crate::layout::BreakValue;
472        assert!(BreakValue::Page.forces_page_break());
473        assert!(BreakValue::Page.forces_break());
474    }
475
476    #[test]
477    fn test_break_value_auto_is_not_active() {
478        use crate::layout::BreakValue;
479        assert!(!BreakValue::Auto.forces_break());
480        assert!(!BreakValue::Auto.forces_page_break());
481    }
482
483    #[test]
484    fn test_break_value_column_active_not_page() {
485        use crate::layout::BreakValue;
486        assert!(BreakValue::Column.forces_break());
487        assert!(!BreakValue::Column.forces_page_break());
488    }
489
490    #[test]
491    fn test_break_value_even_page_is_page_break() {
492        use crate::layout::BreakValue;
493        assert!(BreakValue::EvenPage.forces_page_break());
494        assert!(BreakValue::EvenPage.requires_even_page());
495    }
496
497    #[test]
498    fn test_break_value_odd_page_is_page_break() {
499        use crate::layout::BreakValue;
500        assert!(BreakValue::OddPage.forces_page_break());
501        assert!(BreakValue::OddPage.requires_odd_page());
502    }
503}