1
//! Paged media layout primitives.
2
//!
3
//! Provides the [`FragmentationContext`] that the layout solver threads through to
4
//! distinguish continuous (screen) from paged (print) media.
5
//!
6
//! For continuous media (screens), content flows into a single infinitely tall
7
//! container. For paged media (print), content is laid out on a continuous canvas
8
//! and afterwards sliced into fixed-size pages by the display-list slicer
9
//! (`paginate_display_list_with_slicer_and_breaks` in `azul_layout::solver3::display_list`).
10
//! This lets the layout engine make break decisions while respecting CSS properties
11
//! like `break-before`, `break-after`, and `break-inside`.
12
//!
13
//! Page *decoration* (headers, footers, margin boxes, counters) lives in
14
//! `azul_layout::solver3::pagination`.
15

            
16
use crate::geom::LogicalSize;
17

            
18
/// Selects how content is fragmented during layout.
19
///
20
/// This is the core abstraction for fragmentation support:
21
/// - Screen rendering: [`Continuous`](Self::Continuous) — a single infinite container.
22
/// - Print rendering: [`Paged`](Self::Paged) — a series of fixed-size page containers.
23
#[derive(Debug, Clone, Copy)]
24
pub enum FragmentationContext {
25
    /// Continuous media (screen): a single, infinitely tall container.
26
    ///
27
    /// Used for normal screen rendering where content can scroll indefinitely;
28
    /// breaks are never forced.
29
    Continuous {
30
        /// Width of the viewport.
31
        width: f32,
32
    },
33

            
34
    /// Paged media (print): fixed-size pages.
35
    ///
36
    /// Used for PDF generation and print preview. Content flows from one page to
37
    /// the next when a page is full.
38
    Paged {
39
        /// Size of each page.
40
        page_size: LogicalSize,
41
    },
42
}
43

            
44
impl FragmentationContext {
45
    /// Create a continuous fragmentation context for screen rendering.
46
63
    #[must_use] pub const fn new_continuous(width: f32) -> Self {
47
63
        Self::Continuous { width }
48
63
    }
49

            
50
    /// Create a paged fragmentation context for print rendering.
51
2213
    #[must_use] pub const fn new_paged(page_size: LogicalSize) -> Self {
52
2213
        Self::Paged { page_size }
53
2213
    }
54

            
55
    /// Get the page content height (page height for paged media).
56
    ///
57
    /// For continuous media, returns `f32::MAX`.
58
2008
    #[must_use] pub const fn page_content_height(&self) -> f32 {
59
2008
        match self {
60
23
            Self::Continuous { .. } => f32::MAX,
61
1985
            Self::Paged { page_size, .. } => page_size.height,
62
        }
63
2008
    }
64

            
65
    /// Check if this is paged media.
66
1877
    #[must_use] pub const fn is_paged(&self) -> bool {
67
1877
        matches!(self, Self::Paged { .. })
68
1877
    }
69
}
70

            
71
/// Page margins in points.
72
///
73
/// Canonical paged-media margin type (formerly defined in the now-removed
74
/// `crate::fragmentation` module). Re-exported from the crate root as
75
/// `azul_layout::PageMargins`.
76
#[derive(Debug, Clone, Copy, Default)]
77
pub struct PageMargins {
78
    pub top: f32,
79
    pub right: f32,
80
    pub bottom: f32,
81
    pub left: f32,
82
}
83

            
84
impl PageMargins {
85
236
    #[must_use] pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
86
236
        Self {
87
236
            top,
88
236
            right,
89
236
            bottom,
90
236
            left,
91
236
        }
92
236
    }
93

            
94
35
    #[must_use] pub const fn uniform(margin: f32) -> Self {
95
35
        Self {
96
35
            top: margin,
97
35
            right: margin,
98
35
            bottom: margin,
99
35
            left: margin,
100
35
        }
101
35
    }
102

            
103
189
    #[must_use] pub fn horizontal(&self) -> f32 {
104
189
        self.left + self.right
105
189
    }
106

            
107
187
    #[must_use] pub fn vertical(&self) -> f32 {
108
187
        self.top + self.bottom
109
187
    }
110
}
111

            
112
#[cfg(test)]
113
mod autotest_generated {
114
    #![allow(clippy::float_cmp)]
115

            
116
    use super::*;
117

            
118
    /// Every hostile `f32` a caller can hand to these constructors.
119
    const HOSTILE_F32: [f32; 12] = [
120
        0.0,
121
        -0.0,
122
        1.0,
123
        -1.0,
124
        f32::MAX,
125
        f32::MIN,
126
        f32::MIN_POSITIVE,
127
        -f32::MIN_POSITIVE,
128
        f32::EPSILON,
129
        f32::INFINITY,
130
        f32::NEG_INFINITY,
131
        f32::NAN,
132
    ];
133

            
134
    /// Bit-exact float compare: distinguishes `0.0` from `-0.0`, and treats any
135
    /// NaN as equal to any other NaN (so it can be used on the hostile list).
136
    fn same_f32(a: f32, b: f32) -> bool {
137
        if a.is_nan() && b.is_nan() {
138
            return true;
139
        }
140
        a.to_bits() == b.to_bits()
141
    }
142

            
143
    // --- FragmentationContext::new_continuous / new_paged (constructor) -------
144

            
145
    #[test]
146
    fn new_continuous_stores_width_bit_exactly_for_hostile_input() {
147
        for w in HOSTILE_F32 {
148
            let ctx = FragmentationContext::new_continuous(w);
149
            match ctx {
150
                FragmentationContext::Continuous { width } => {
151
                    assert!(same_f32(width, w), "width mangled: {w:?} -> {width:?}");
152
                }
153
                FragmentationContext::Paged { .. } => {
154
                    panic!("new_continuous produced a Paged variant for width {w:?}")
155
                }
156
            }
157
        }
158
    }
159

            
160
    #[test]
161
    fn new_paged_stores_page_size_bit_exactly_for_hostile_input() {
162
        for w in HOSTILE_F32 {
163
            for h in HOSTILE_F32 {
164
                let ctx = FragmentationContext::new_paged(LogicalSize::new(w, h));
165
                match ctx {
166
                    FragmentationContext::Paged { page_size } => {
167
                        assert!(same_f32(page_size.width, w));
168
                        assert!(same_f32(page_size.height, h));
169
                    }
170
                    FragmentationContext::Continuous { .. } => {
171
                        panic!("new_paged produced a Continuous variant for {w:?}x{h:?}")
172
                    }
173
                }
174
            }
175
        }
176
    }
177

            
178
    /// The `const fn` marker is part of the public contract: a caller may put
179
    /// these in a `const` item. If constness regresses, this stops compiling.
180
    #[test]
181
    fn constructors_and_accessors_are_usable_in_const_context() {
182
        const CONT: FragmentationContext = FragmentationContext::new_continuous(1024.0);
183
        const A4: FragmentationContext =
184
            FragmentationContext::new_paged(LogicalSize::new(595.0, 842.0));
185
        const CONT_H: f32 = CONT.page_content_height();
186
        const A4_H: f32 = A4.page_content_height();
187
        const CONT_PAGED: bool = CONT.is_paged();
188
        const A4_PAGED: bool = A4.is_paged();
189
        const UNIFORM: PageMargins = PageMargins::uniform(10.0);
190
        const EXPLICIT: PageMargins = PageMargins::new(1.0, 2.0, 3.0, 4.0);
191

            
192
        assert_eq!(CONT_H, f32::MAX);
193
        assert_eq!(A4_H, 842.0);
194
        const _: () = assert!(!CONT_PAGED && A4_PAGED);
195
        assert_eq!(UNIFORM.top, 10.0);
196
        assert_eq!(EXPLICIT.left, 4.0);
197
    }
198

            
199
    // --- FragmentationContext::page_content_height (getter) ------------------
200

            
201
    #[test]
202
    fn page_content_height_is_f32_max_for_every_continuous_width() {
203
        // Continuous ignores `width` entirely — even NaN/inf must not leak out.
204
        for w in HOSTILE_F32 {
205
            let h = FragmentationContext::new_continuous(w).page_content_height();
206
            assert!(
207
                same_f32(h, f32::MAX),
208
                "continuous width {w:?} leaked into page_content_height: {h:?}"
209
            );
210
        }
211
    }
212

            
213
    #[test]
214
    fn page_content_height_returns_paged_height_verbatim_and_ignores_width() {
215
        for h in HOSTILE_F32 {
216
            // Width must never contaminate the result, however hostile it is.
217
            for w in HOSTILE_F32 {
218
                let got = FragmentationContext::new_paged(LogicalSize::new(w, h))
219
                    .page_content_height();
220
                assert!(
221
                    same_f32(got, h),
222
                    "page {w:?}x{h:?} -> page_content_height {got:?}, expected {h:?}"
223
                );
224
            }
225
        }
226
    }
227

            
228
    #[test]
229
    fn page_content_height_does_not_saturate_or_clamp_degenerate_pages() {
230
        // A zero/negative page height is nonsense for print, but the getter is a
231
        // plain accessor: it must report what it was given, not silently repair it.
232
        let zero = FragmentationContext::new_paged(LogicalSize::zero());
233
        assert_eq!(zero.page_content_height(), 0.0);
234

            
235
        let negative = FragmentationContext::new_paged(LogicalSize::new(595.0, -842.0));
236
        assert_eq!(negative.page_content_height(), -842.0);
237

            
238
        let nan = FragmentationContext::new_paged(LogicalSize::new(595.0, f32::NAN));
239
        assert!(nan.page_content_height().is_nan());
240
    }
241

            
242
    /// `f32::MAX` is the sentinel for "infinitely tall". A paged context whose page
243
    /// is exactly `f32::MAX` tall is therefore indistinguishable from a continuous
244
    /// one *by height alone* — `is_paged()` is the only safe discriminator.
245
    #[test]
246
    fn f32_max_tall_page_collides_with_continuous_sentinel_but_is_paged_disambiguates() {
247
        let continuous = FragmentationContext::new_continuous(595.0);
248
        let max_page =
249
            FragmentationContext::new_paged(LogicalSize::new(595.0, f32::MAX));
250

            
251
        assert_eq!(
252
            continuous.page_content_height(),
253
            max_page.page_content_height()
254
        );
255
        assert!(!continuous.is_paged());
256
        assert!(max_page.is_paged());
257
    }
258

            
259
    // --- FragmentationContext::is_paged (predicate) --------------------------
260

            
261
    #[test]
262
    fn is_paged_is_true_only_for_paged_regardless_of_hostile_fields() {
263
        for v in HOSTILE_F32 {
264
            assert!(
265
                !FragmentationContext::new_continuous(v).is_paged(),
266
                "continuous({v:?}) reported as paged"
267
            );
268
            assert!(
269
                FragmentationContext::new_paged(LogicalSize::new(v, v)).is_paged(),
270
                "paged({v:?}x{v:?}) reported as continuous"
271
            );
272
        }
273
        // Fully degenerate page: still paged. The predicate inspects the variant,
274
        // never the payload.
275
        assert!(FragmentationContext::new_paged(LogicalSize::zero()).is_paged());
276
    }
277

            
278
    #[test]
279
    fn is_paged_is_deterministic_across_repeated_calls_and_copies() {
280
        let ctx = FragmentationContext::new_paged(LogicalSize::new(f32::NAN, f32::NAN));
281
        let copied = ctx; // Copy
282
        assert!(ctx.is_paged());
283
        assert!(ctx.is_paged());
284
        assert!(copied.is_paged());
285
    }
286

            
287
    // --- PageMargins::new (constructor) --------------------------------------
288

            
289
    #[test]
290
    fn new_assigns_each_argument_to_its_own_field_no_transposition() {
291
        // Distinct values in every slot: catches any top/right/bottom/left swap.
292
        let m = PageMargins::new(1.0, 2.0, 3.0, 4.0);
293
        assert_eq!(m.top, 1.0);
294
        assert_eq!(m.right, 2.0);
295
        assert_eq!(m.bottom, 3.0);
296
        assert_eq!(m.left, 4.0);
297
    }
298

            
299
    #[test]
300
    fn new_stores_hostile_floats_bit_exactly() {
301
        for v in HOSTILE_F32 {
302
            // Rotate the hostile value through each slot; the other three stay sane
303
            // so a misassignment shows up as a mismatch rather than cancelling out.
304
            let m = PageMargins::new(v, 2.0, 3.0, 4.0);
305
            assert!(same_f32(m.top, v));
306
            assert_eq!(m.right, 2.0);
307

            
308
            let m = PageMargins::new(1.0, v, 3.0, 4.0);
309
            assert!(same_f32(m.right, v));
310
            assert_eq!(m.bottom, 3.0);
311

            
312
            let m = PageMargins::new(1.0, 2.0, v, 4.0);
313
            assert!(same_f32(m.bottom, v));
314
            assert_eq!(m.left, 4.0);
315

            
316
            let m = PageMargins::new(1.0, 2.0, 3.0, v);
317
            assert!(same_f32(m.left, v));
318
            assert_eq!(m.top, 1.0);
319
        }
320
    }
321

            
322
    #[test]
323
    fn default_margins_are_positive_zero_and_sum_to_zero() {
324
        let d = PageMargins::default();
325
        for f in [d.top, d.right, d.bottom, d.left] {
326
            assert!(same_f32(f, 0.0), "default field is not +0.0: {f:?}");
327
        }
328
        assert_eq!(d.horizontal(), 0.0);
329
        assert_eq!(d.vertical(), 0.0);
330
    }
331

            
332
    // --- PageMargins::uniform (numeric) --------------------------------------
333

            
334
    #[test]
335
    fn uniform_zero_and_negative_zero_preserve_sign() {
336
        let z = PageMargins::uniform(0.0);
337
        assert!(same_f32(z.top, 0.0) && same_f32(z.left, 0.0));
338
        assert_eq!(z.horizontal(), 0.0);
339
        assert_eq!(z.vertical(), 0.0);
340

            
341
        // -0.0 + -0.0 == -0.0, so the sign must survive all the way through.
342
        let nz = PageMargins::uniform(-0.0);
343
        assert!(same_f32(nz.top, -0.0), "uniform(-0.0) lost the sign bit");
344
        assert!(same_f32(nz.horizontal(), -0.0));
345
        assert!(same_f32(nz.vertical(), -0.0));
346
    }
347

            
348
    #[test]
349
    fn uniform_replicates_hostile_value_into_all_four_fields() {
350
        for v in HOSTILE_F32 {
351
            let m = PageMargins::uniform(v);
352
            assert!(same_f32(m.top, v), "top != {v:?}");
353
            assert!(same_f32(m.right, v), "right != {v:?}");
354
            assert!(same_f32(m.bottom, v), "bottom != {v:?}");
355
            assert!(same_f32(m.left, v), "left != {v:?}");
356
        }
357
    }
358

            
359
    #[test]
360
    fn uniform_negative_margins_are_kept_not_clamped() {
361
        // Negative margins are legal CSS (`margin: -10pt`); nothing here may clamp.
362
        let m = PageMargins::uniform(-10.0);
363
        assert_eq!(m.top, -10.0);
364
        assert_eq!(m.horizontal(), -20.0);
365
        assert_eq!(m.vertical(), -20.0);
366
    }
367

            
368
    #[test]
369
    fn uniform_min_max_do_not_panic_and_overflow_to_infinity_not_wrap() {
370
        // f32::MAX + f32::MAX overflows. IEEE-754 says +inf; it must not panic,
371
        // wrap to a negative, or saturate back to f32::MAX.
372
        let hi = PageMargins::uniform(f32::MAX);
373
        assert_eq!(hi.horizontal(), f32::INFINITY);
374
        assert_eq!(hi.vertical(), f32::INFINITY);
375

            
376
        let lo = PageMargins::uniform(f32::MIN);
377
        assert_eq!(lo.horizontal(), f32::NEG_INFINITY);
378
        assert_eq!(lo.vertical(), f32::NEG_INFINITY);
379
    }
380

            
381
    #[test]
382
    fn uniform_exactly_at_the_overflow_boundary_stays_finite() {
383
        // MAX/2 is exactly representable, so doubling it must land on MAX exactly
384
        // — the last finite step before the overflow tested above.
385
        let edge = PageMargins::uniform(f32::MAX / 2.0);
386
        assert_eq!(edge.horizontal(), f32::MAX);
387
        assert!(edge.horizontal().is_finite());
388
    }
389

            
390
    #[test]
391
    fn uniform_smallest_normal_doubles_exactly_without_flushing_to_zero() {
392
        let tiny = PageMargins::uniform(f32::MIN_POSITIVE);
393
        assert_eq!(tiny.horizontal(), f32::MIN_POSITIVE * 2.0);
394
        assert!(tiny.horizontal() > 0.0, "tiny margin flushed to zero");
395
    }
396

            
397
    #[test]
398
    fn uniform_nan_and_inf_propagate_without_panicking() {
399
        let nan = PageMargins::uniform(f32::NAN);
400
        assert!(nan.horizontal().is_nan());
401
        assert!(nan.vertical().is_nan());
402

            
403
        let inf = PageMargins::uniform(f32::INFINITY);
404
        assert_eq!(inf.horizontal(), f32::INFINITY);
405
        assert_eq!(inf.vertical(), f32::INFINITY);
406

            
407
        let neg_inf = PageMargins::uniform(f32::NEG_INFINITY);
408
        assert_eq!(neg_inf.horizontal(), f32::NEG_INFINITY);
409
        assert_eq!(neg_inf.vertical(), f32::NEG_INFINITY);
410
    }
411

            
412
    #[test]
413
    fn uniform_agrees_with_new_given_the_same_value() {
414
        for v in HOSTILE_F32 {
415
            let u = PageMargins::uniform(v);
416
            let n = PageMargins::new(v, v, v, v);
417
            assert!(same_f32(u.top, n.top));
418
            assert!(same_f32(u.right, n.right));
419
            assert!(same_f32(u.bottom, n.bottom));
420
            assert!(same_f32(u.left, n.left));
421
            assert!(same_f32(u.horizontal(), n.horizontal()));
422
            assert!(same_f32(u.vertical(), n.vertical()));
423
        }
424
    }
425

            
426
    // --- PageMargins::horizontal / vertical (getters) ------------------------
427

            
428
    #[test]
429
    fn horizontal_sums_left_and_right_vertical_sums_top_and_bottom() {
430
        // new(top, right, bottom, left) = (1, 2, 3, 4)
431
        //   horizontal = left + right   = 4 + 2 = 6
432
        //   vertical   = top  + bottom  = 1 + 3 = 4
433
        // Distinct values, so picking the wrong pair cannot produce these sums.
434
        let m = PageMargins::new(1.0, 2.0, 3.0, 4.0);
435
        assert_eq!(m.horizontal(), 6.0);
436
        assert_eq!(m.vertical(), 4.0);
437
    }
438

            
439
    #[test]
440
    fn horizontal_and_vertical_are_axis_independent() {
441
        // Loading one axis must not move the other.
442
        let h_only = PageMargins::new(0.0, 7.0, 0.0, 11.0);
443
        assert_eq!(h_only.horizontal(), 18.0);
444
        assert_eq!(h_only.vertical(), 0.0);
445

            
446
        let v_only = PageMargins::new(7.0, 0.0, 11.0, 0.0);
447
        assert_eq!(v_only.vertical(), 18.0);
448
        assert_eq!(v_only.horizontal(), 0.0);
449
    }
450

            
451
    #[test]
452
    fn opposing_infinities_yield_nan_not_a_panic() {
453
        // +inf + -inf is NaN by IEEE-754. The getter must return it, not trap.
454
        let m = PageMargins::new(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
455
        assert!(m.vertical().is_nan(), "inf + -inf should be NaN");
456
        assert!(m.horizontal().is_nan(), "-inf + inf should be NaN");
457
    }
458

            
459
    #[test]
460
    fn opposing_extremes_cancel_to_zero_rather_than_overflowing() {
461
        // MAX + MIN == MAX + (-MAX) == 0.0, exactly. No overflow, no panic.
462
        let m = PageMargins::new(f32::MAX, f32::MAX, f32::MIN, f32::MIN);
463
        assert_eq!(m.vertical(), 0.0);
464
        assert_eq!(m.horizontal(), 0.0);
465
    }
466

            
467
    #[test]
468
    fn getters_absorb_a_tiny_operand_next_to_a_huge_one_without_error() {
469
        // Classic float-precision trap: 1e30 + 1.0 == 1e30. Documented, not a bug —
470
        // pin it so nobody "fixes" the sum into something lossier.
471
        // horizontal() = left + right, vertical() = top + bottom -- so each axis has
472
        // to MIX magnitudes for absorption to happen at all. (top, right, bottom, left)
473
        let m = PageMargins::new(1.0e30, 1.0, 1.0, 1.0e30);
474
        assert_eq!(m.horizontal(), 1.0e30);
475
        assert_eq!(m.vertical(), 1.0e30);
476
    }
477

            
478
    #[test]
479
    fn getters_do_not_panic_on_any_hostile_field_combination() {
480
        for a in HOSTILE_F32 {
481
            for b in HOSTILE_F32 {
482
                // new(top=a, right=b, bottom=b, left=a): both axes sum the same pair,
483
                // so horizontal() and vertical() must agree bit-for-bit for every one
484
                // of the 144 hostile combinations — and neither may panic.
485
                let m = PageMargins::new(a, b, b, a);
486
                assert!(
487
                    same_f32(m.horizontal(), m.vertical()),
488
                    "axes disagree for {a:?}/{b:?}: h={:?} v={:?}",
489
                    m.horizontal(),
490
                    m.vertical()
491
                );
492
            }
493
        }
494
    }
495

            
496
    #[test]
497
    fn getters_are_pure_and_do_not_mutate_the_receiver() {
498
        let m = PageMargins::new(1.0, 2.0, 3.0, 4.0);
499
        let first_h = m.horizontal();
500
        let first_v = m.vertical();
501
        // Repeat calls: same answer, fields untouched.
502
        assert_eq!(m.horizontal(), first_h);
503
        assert_eq!(m.vertical(), first_v);
504
        assert_eq!(m.top, 1.0);
505
        assert_eq!(m.right, 2.0);
506
        assert_eq!(m.bottom, 3.0);
507
        assert_eq!(m.left, 4.0);
508
    }
509

            
510
    #[test]
511
    fn page_margins_copy_does_not_alias_the_original() {
512
        let original = PageMargins::uniform(5.0);
513
        let mut copy = original; // Copy, not a move
514
        copy.top = 99.0;
515
        assert_eq!(original.top, 5.0, "mutating a copy wrote through to the original");
516
        assert_eq!(original.vertical(), 10.0);
517
        assert_eq!(copy.vertical(), 104.0);
518
    }
519

            
520
    // --- round-trip ----------------------------------------------------------
521

            
522
    #[test]
523
    fn round_trip_new_fields_reconstruct_an_identical_margin() {
524
        for v in HOSTILE_F32 {
525
            let a = PageMargins::new(v, 1.0, 2.0, 3.0);
526
            let b = PageMargins::new(a.top, a.right, a.bottom, a.left);
527
            assert!(same_f32(a.top, b.top));
528
            assert!(same_f32(a.right, b.right));
529
            assert!(same_f32(a.bottom, b.bottom));
530
            assert!(same_f32(a.left, b.left));
531
        }
532
    }
533

            
534
    #[test]
535
    fn round_trip_paged_context_returns_the_height_it_was_built_from() {
536
        for h in [0.0_f32, 1.0, 842.0, -842.0, f32::MAX, f32::MIN, f32::MIN_POSITIVE] {
537
            let ctx = FragmentationContext::new_paged(LogicalSize::new(595.0, h));
538
            assert!(same_f32(ctx.page_content_height(), h));
539
            assert!(ctx.is_paged());
540
        }
541
    }
542
}