1use crate::geom::LogicalSize;
17
18#[derive(Debug, Clone, Copy)]
24pub enum FragmentationContext {
25 Continuous {
30 width: f32,
32 },
33
34 Paged {
39 page_size: LogicalSize,
41 },
42}
43
44impl FragmentationContext {
45 #[must_use] pub const fn new_continuous(width: f32) -> Self {
47 Self::Continuous { width }
48 }
49
50 #[must_use] pub const fn new_paged(page_size: LogicalSize) -> Self {
52 Self::Paged { page_size }
53 }
54
55 #[must_use] pub const fn page_content_height(&self) -> f32 {
59 match self {
60 Self::Continuous { .. } => f32::MAX,
61 Self::Paged { page_size, .. } => page_size.height,
62 }
63 }
64
65 #[must_use] pub const fn is_paged(&self) -> bool {
67 matches!(self, Self::Paged { .. })
68 }
69}
70
71#[derive(Debug, Clone, Copy, Default)]
77pub struct PageMargins {
78 pub top: f32,
79 pub right: f32,
80 pub bottom: f32,
81 pub left: f32,
82}
83
84impl PageMargins {
85 #[must_use] pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
86 Self {
87 top,
88 right,
89 bottom,
90 left,
91 }
92 }
93
94 #[must_use] pub const fn uniform(margin: f32) -> Self {
95 Self {
96 top: margin,
97 right: margin,
98 bottom: margin,
99 left: margin,
100 }
101 }
102
103 #[must_use] pub fn horizontal(&self) -> f32 {
104 self.left + self.right
105 }
106
107 #[must_use] pub fn vertical(&self) -> f32 {
108 self.top + self.bottom
109 }
110}
111
112#[cfg(test)]
113mod autotest_generated {
114 #![allow(clippy::float_cmp)]
115
116 use super::*;
117
118 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 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 #[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 #[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 assert!(!CONT_PAGED);
195 assert!(A4_PAGED);
196 assert_eq!(UNIFORM.top, 10.0);
197 assert_eq!(EXPLICIT.left, 4.0);
198 }
199
200 #[test]
203 fn page_content_height_is_f32_max_for_every_continuous_width() {
204 for w in HOSTILE_F32 {
206 let h = FragmentationContext::new_continuous(w).page_content_height();
207 assert!(
208 same_f32(h, f32::MAX),
209 "continuous width {w:?} leaked into page_content_height: {h:?}"
210 );
211 }
212 }
213
214 #[test]
215 fn page_content_height_returns_paged_height_verbatim_and_ignores_width() {
216 for h in HOSTILE_F32 {
217 for w in HOSTILE_F32 {
219 let got = FragmentationContext::new_paged(LogicalSize::new(w, h))
220 .page_content_height();
221 assert!(
222 same_f32(got, h),
223 "page {w:?}x{h:?} -> page_content_height {got:?}, expected {h:?}"
224 );
225 }
226 }
227 }
228
229 #[test]
230 fn page_content_height_does_not_saturate_or_clamp_degenerate_pages() {
231 let zero = FragmentationContext::new_paged(LogicalSize::zero());
234 assert_eq!(zero.page_content_height(), 0.0);
235
236 let negative = FragmentationContext::new_paged(LogicalSize::new(595.0, -842.0));
237 assert_eq!(negative.page_content_height(), -842.0);
238
239 let nan = FragmentationContext::new_paged(LogicalSize::new(595.0, f32::NAN));
240 assert!(nan.page_content_height().is_nan());
241 }
242
243 #[test]
247 fn f32_max_tall_page_collides_with_continuous_sentinel_but_is_paged_disambiguates() {
248 let continuous = FragmentationContext::new_continuous(595.0);
249 let max_page =
250 FragmentationContext::new_paged(LogicalSize::new(595.0, f32::MAX));
251
252 assert_eq!(
253 continuous.page_content_height(),
254 max_page.page_content_height()
255 );
256 assert!(!continuous.is_paged());
257 assert!(max_page.is_paged());
258 }
259
260 #[test]
263 fn is_paged_is_true_only_for_paged_regardless_of_hostile_fields() {
264 for v in HOSTILE_F32 {
265 assert!(
266 !FragmentationContext::new_continuous(v).is_paged(),
267 "continuous({v:?}) reported as paged"
268 );
269 assert!(
270 FragmentationContext::new_paged(LogicalSize::new(v, v)).is_paged(),
271 "paged({v:?}x{v:?}) reported as continuous"
272 );
273 }
274 assert!(FragmentationContext::new_paged(LogicalSize::zero()).is_paged());
277 }
278
279 #[test]
280 fn is_paged_is_deterministic_across_repeated_calls_and_copies() {
281 let ctx = FragmentationContext::new_paged(LogicalSize::new(f32::NAN, f32::NAN));
282 let copied = ctx; assert!(ctx.is_paged());
284 assert!(ctx.is_paged());
285 assert!(copied.is_paged());
286 }
287
288 #[test]
291 fn new_assigns_each_argument_to_its_own_field_no_transposition() {
292 let m = PageMargins::new(1.0, 2.0, 3.0, 4.0);
294 assert_eq!(m.top, 1.0);
295 assert_eq!(m.right, 2.0);
296 assert_eq!(m.bottom, 3.0);
297 assert_eq!(m.left, 4.0);
298 }
299
300 #[test]
301 fn new_stores_hostile_floats_bit_exactly() {
302 for v in HOSTILE_F32 {
303 let m = PageMargins::new(v, 2.0, 3.0, 4.0);
306 assert!(same_f32(m.top, v));
307 assert_eq!(m.right, 2.0);
308
309 let m = PageMargins::new(1.0, v, 3.0, 4.0);
310 assert!(same_f32(m.right, v));
311 assert_eq!(m.bottom, 3.0);
312
313 let m = PageMargins::new(1.0, 2.0, v, 4.0);
314 assert!(same_f32(m.bottom, v));
315 assert_eq!(m.left, 4.0);
316
317 let m = PageMargins::new(1.0, 2.0, 3.0, v);
318 assert!(same_f32(m.left, v));
319 assert_eq!(m.top, 1.0);
320 }
321 }
322
323 #[test]
324 fn default_margins_are_positive_zero_and_sum_to_zero() {
325 let d = PageMargins::default();
326 for f in [d.top, d.right, d.bottom, d.left] {
327 assert!(same_f32(f, 0.0), "default field is not +0.0: {f:?}");
328 }
329 assert_eq!(d.horizontal(), 0.0);
330 assert_eq!(d.vertical(), 0.0);
331 }
332
333 #[test]
336 fn uniform_zero_and_negative_zero_preserve_sign() {
337 let z = PageMargins::uniform(0.0);
338 assert!(same_f32(z.top, 0.0) && same_f32(z.left, 0.0));
339 assert_eq!(z.horizontal(), 0.0);
340 assert_eq!(z.vertical(), 0.0);
341
342 let nz = PageMargins::uniform(-0.0);
344 assert!(same_f32(nz.top, -0.0), "uniform(-0.0) lost the sign bit");
345 assert!(same_f32(nz.horizontal(), -0.0));
346 assert!(same_f32(nz.vertical(), -0.0));
347 }
348
349 #[test]
350 fn uniform_replicates_hostile_value_into_all_four_fields() {
351 for v in HOSTILE_F32 {
352 let m = PageMargins::uniform(v);
353 assert!(same_f32(m.top, v), "top != {v:?}");
354 assert!(same_f32(m.right, v), "right != {v:?}");
355 assert!(same_f32(m.bottom, v), "bottom != {v:?}");
356 assert!(same_f32(m.left, v), "left != {v:?}");
357 }
358 }
359
360 #[test]
361 fn uniform_negative_margins_are_kept_not_clamped() {
362 let m = PageMargins::uniform(-10.0);
364 assert_eq!(m.top, -10.0);
365 assert_eq!(m.horizontal(), -20.0);
366 assert_eq!(m.vertical(), -20.0);
367 }
368
369 #[test]
370 fn uniform_min_max_do_not_panic_and_overflow_to_infinity_not_wrap() {
371 let hi = PageMargins::uniform(f32::MAX);
374 assert_eq!(hi.horizontal(), f32::INFINITY);
375 assert_eq!(hi.vertical(), f32::INFINITY);
376
377 let lo = PageMargins::uniform(f32::MIN);
378 assert_eq!(lo.horizontal(), f32::NEG_INFINITY);
379 assert_eq!(lo.vertical(), f32::NEG_INFINITY);
380 }
381
382 #[test]
383 fn uniform_exactly_at_the_overflow_boundary_stays_finite() {
384 let edge = PageMargins::uniform(f32::MAX / 2.0);
387 assert_eq!(edge.horizontal(), f32::MAX);
388 assert!(edge.horizontal().is_finite());
389 }
390
391 #[test]
392 fn uniform_smallest_normal_doubles_exactly_without_flushing_to_zero() {
393 let tiny = PageMargins::uniform(f32::MIN_POSITIVE);
394 assert_eq!(tiny.horizontal(), f32::MIN_POSITIVE * 2.0);
395 assert!(tiny.horizontal() > 0.0, "tiny margin flushed to zero");
396 }
397
398 #[test]
399 fn uniform_nan_and_inf_propagate_without_panicking() {
400 let nan = PageMargins::uniform(f32::NAN);
401 assert!(nan.horizontal().is_nan());
402 assert!(nan.vertical().is_nan());
403
404 let inf = PageMargins::uniform(f32::INFINITY);
405 assert_eq!(inf.horizontal(), f32::INFINITY);
406 assert_eq!(inf.vertical(), f32::INFINITY);
407
408 let neg_inf = PageMargins::uniform(f32::NEG_INFINITY);
409 assert_eq!(neg_inf.horizontal(), f32::NEG_INFINITY);
410 assert_eq!(neg_inf.vertical(), f32::NEG_INFINITY);
411 }
412
413 #[test]
414 fn uniform_agrees_with_new_given_the_same_value() {
415 for v in HOSTILE_F32 {
416 let u = PageMargins::uniform(v);
417 let n = PageMargins::new(v, v, v, v);
418 assert!(same_f32(u.top, n.top));
419 assert!(same_f32(u.right, n.right));
420 assert!(same_f32(u.bottom, n.bottom));
421 assert!(same_f32(u.left, n.left));
422 assert!(same_f32(u.horizontal(), n.horizontal()));
423 assert!(same_f32(u.vertical(), n.vertical()));
424 }
425 }
426
427 #[test]
430 fn horizontal_sums_left_and_right_vertical_sums_top_and_bottom() {
431 let m = PageMargins::new(1.0, 2.0, 3.0, 4.0);
436 assert_eq!(m.horizontal(), 6.0);
437 assert_eq!(m.vertical(), 4.0);
438 }
439
440 #[test]
441 fn horizontal_and_vertical_are_axis_independent() {
442 let h_only = PageMargins::new(0.0, 7.0, 0.0, 11.0);
444 assert_eq!(h_only.horizontal(), 18.0);
445 assert_eq!(h_only.vertical(), 0.0);
446
447 let v_only = PageMargins::new(7.0, 0.0, 11.0, 0.0);
448 assert_eq!(v_only.vertical(), 18.0);
449 assert_eq!(v_only.horizontal(), 0.0);
450 }
451
452 #[test]
453 fn opposing_infinities_yield_nan_not_a_panic() {
454 let m = PageMargins::new(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
456 assert!(m.vertical().is_nan(), "inf + -inf should be NaN");
457 assert!(m.horizontal().is_nan(), "-inf + inf should be NaN");
458 }
459
460 #[test]
461 fn opposing_extremes_cancel_to_zero_rather_than_overflowing() {
462 let m = PageMargins::new(f32::MAX, f32::MAX, f32::MIN, f32::MIN);
464 assert_eq!(m.vertical(), 0.0);
465 assert_eq!(m.horizontal(), 0.0);
466 }
467
468 #[test]
469 fn getters_absorb_a_tiny_operand_next_to_a_huge_one_without_error() {
470 let m = PageMargins::new(1.0e30, 1.0, 1.0e30, 1.0);
473 assert_eq!(m.horizontal(), 1.0e30);
474 assert_eq!(m.vertical(), 1.0e30);
475 }
476
477 #[test]
478 fn getters_do_not_panic_on_any_hostile_field_combination() {
479 for a in HOSTILE_F32 {
480 for b in HOSTILE_F32 {
481 let m = PageMargins::new(a, b, b, a);
485 assert!(
486 same_f32(m.horizontal(), m.vertical()),
487 "axes disagree for {a:?}/{b:?}: h={:?} v={:?}",
488 m.horizontal(),
489 m.vertical()
490 );
491 }
492 }
493 }
494
495 #[test]
496 fn getters_are_pure_and_do_not_mutate_the_receiver() {
497 let m = PageMargins::new(1.0, 2.0, 3.0, 4.0);
498 let first_h = m.horizontal();
499 let first_v = m.vertical();
500 assert_eq!(m.horizontal(), first_h);
502 assert_eq!(m.vertical(), first_v);
503 assert_eq!(m.top, 1.0);
504 assert_eq!(m.right, 2.0);
505 assert_eq!(m.bottom, 3.0);
506 assert_eq!(m.left, 4.0);
507 }
508
509 #[test]
510 fn page_margins_copy_does_not_alias_the_original() {
511 let original = PageMargins::uniform(5.0);
512 let mut copy = original; copy.top = 99.0;
514 assert_eq!(original.top, 5.0, "mutating a copy wrote through to the original");
515 assert_eq!(original.vertical(), 10.0);
516 assert_eq!(copy.vertical(), 104.0);
517 }
518
519 #[test]
522 fn round_trip_new_fields_reconstruct_an_identical_margin() {
523 for v in HOSTILE_F32 {
524 let a = PageMargins::new(v, 1.0, 2.0, 3.0);
525 let b = PageMargins::new(a.top, a.right, a.bottom, a.left);
526 assert!(same_f32(a.top, b.top));
527 assert!(same_f32(a.right, b.right));
528 assert!(same_f32(a.bottom, b.bottom));
529 assert!(same_f32(a.left, b.left));
530 }
531 }
532
533 #[test]
534 fn round_trip_paged_context_returns_the_height_it_was_built_from() {
535 for h in [0.0_f32, 1.0, 842.0, -842.0, f32::MAX, f32::MIN, f32::MIN_POSITIVE] {
536 let ctx = FragmentationContext::new_paged(LogicalSize::new(595.0, h));
537 assert!(same_f32(ctx.page_content_height(), h));
538 assert!(ctx.is_paged());
539 }
540 }
541}