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 const _: () = assert!(!CONT_PAGED && A4_PAGED);
195 assert_eq!(UNIFORM.top, 10.0);
196 assert_eq!(EXPLICIT.left, 4.0);
197 }
198
199 #[test]
202 fn page_content_height_is_f32_max_for_every_continuous_width() {
203 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 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 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 #[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 #[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 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; assert!(ctx.is_paged());
283 assert!(ctx.is_paged());
284 assert!(copied.is_paged());
285 }
286
287 #[test]
290 fn new_assigns_each_argument_to_its_own_field_no_transposition() {
291 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 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 #[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 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 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 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 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 #[test]
429 fn horizontal_sums_left_and_right_vertical_sums_top_and_bottom() {
430 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 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 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 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 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 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 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.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 #[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}