Skip to main content

fop_types/
length.rs

1//! Length type for dimensional measurements
2//!
3//! All lengths are stored internally as millipoints (1/1000 of a point).
4//! This provides sufficient precision while using integer arithmetic.
5//!
6//! Font-relative units (em, ex) are stored as enum variants and require
7//! a FontContext to resolve to absolute lengths.
8
9use std::fmt;
10
11/// Unit type for length measurements
12///
13/// Supports both absolute units (stored as millipoints) and font-relative
14/// units (em, ex) that require a FontContext for resolution.
15#[derive(Debug, Copy, Clone, PartialEq)]
16pub enum LengthUnit {
17    /// Absolute length in millipoints
18    Absolute(i32),
19    /// Font-size relative unit (1em = font-size)
20    Em(f64),
21    /// X-height relative unit (1ex ≈ 0.5em for Latin fonts)
22    Ex(f64),
23}
24
25/// Context for resolving font-relative units
26///
27/// Contains the current font size and x-height needed to resolve
28/// em and ex units to absolute lengths.
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub struct FontContext {
31    /// Current font size
32    pub font_size: Length,
33    /// X-height of the current font (typically ~0.5em)
34    pub x_height: Length,
35}
36
37impl FontContext {
38    /// Create a new font context
39    ///
40    /// The x_height will be calculated as 0.5 times the font_size
41    /// if not explicitly provided.
42    #[must_use = "this returns a new value without modifying anything"]
43    pub fn new(font_size: Length) -> Self {
44        Self {
45            font_size,
46            x_height: font_size / 2,
47        }
48    }
49
50    /// Create a new font context with explicit x-height
51    #[must_use = "this returns a new value without modifying anything"]
52    pub fn with_x_height(font_size: Length, x_height: Length) -> Self {
53        Self {
54            font_size,
55            x_height,
56        }
57    }
58}
59
60/// Length measurement stored as millipoints (1/1000 point)
61///
62/// Conversion factors:
63/// - 1 point = 1/72 inch
64/// - 1 inch = 72 points = 72000 millipoints
65/// - 1 mm = 2.834645669 points
66/// - 1 cm = 28.34645669 points
67///
68/// Font-relative units:
69/// - 1em = current font-size
70/// - 1ex = x-height of current font (typically ~0.5em)
71#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72pub struct Length {
73    millipoints: i32,
74}
75
76impl Length {
77    /// Zero length constant
78    pub const ZERO: Self = Self { millipoints: 0 };
79
80    /// Conversion factor: points to millipoints
81    const PT_TO_MILLI: f64 = 1000.0;
82
83    /// Conversion factor: inches to millipoints (72 * 1000)
84    const IN_TO_MILLI: f64 = 72000.0;
85
86    /// Conversion factor: millimeters to millipoints
87    const MM_TO_MILLI: f64 = 2834.645669;
88
89    /// Conversion factor: centimeters to millipoints
90    const CM_TO_MILLI: f64 = 28346.45669;
91
92    /// Create a length from points
93    #[inline]
94    #[must_use = "this returns a new value without modifying anything"]
95    pub fn from_pt(pt: f64) -> Self {
96        Self {
97            millipoints: (pt * Self::PT_TO_MILLI).round() as i32,
98        }
99    }
100
101    /// Create a length from inches
102    #[inline]
103    #[must_use = "this returns a new value without modifying anything"]
104    pub fn from_inch(inch: f64) -> Self {
105        Self {
106            millipoints: (inch * Self::IN_TO_MILLI).round() as i32,
107        }
108    }
109
110    /// Create a length from millimeters
111    #[inline]
112    #[must_use = "this returns a new value without modifying anything"]
113    pub fn from_mm(mm: f64) -> Self {
114        Self {
115            millipoints: (mm * Self::MM_TO_MILLI).round() as i32,
116        }
117    }
118
119    /// Create a length from centimeters
120    #[inline]
121    #[must_use = "this returns a new value without modifying anything"]
122    pub fn from_cm(cm: f64) -> Self {
123        Self {
124            millipoints: (cm * Self::CM_TO_MILLI).round() as i32,
125        }
126    }
127
128    /// Create a length from millipoints directly
129    #[inline]
130    #[must_use = "this returns a new value without modifying anything"]
131    pub const fn from_millipoints(millipoints: i32) -> Self {
132        Self { millipoints }
133    }
134
135    /// Create a font-relative length in em units
136    ///
137    /// 1em equals the current font-size. The returned LengthUnit
138    /// must be resolved with a FontContext to get an absolute length.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use fop_types::{Length, FontContext, LengthUnit};
144    ///
145    /// let em_unit = LengthUnit::Em(1.5);
146    /// let context = FontContext::new(Length::from_pt(12.0));
147    /// let resolved = Length::resolve_unit(&em_unit, &context);
148    /// assert_eq!(resolved, Length::from_pt(18.0)); // 1.5 * 12pt
149    /// ```
150    #[inline]
151    #[must_use = "this returns a new value without modifying anything"]
152    pub fn from_em(em: f64) -> LengthUnit {
153        LengthUnit::Em(em)
154    }
155
156    /// Create a font-relative length in ex units
157    ///
158    /// 1ex equals the x-height of the current font (typically ~0.5em).
159    /// The returned LengthUnit must be resolved with a FontContext
160    /// to get an absolute length.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use fop_types::{Length, FontContext, LengthUnit};
166    ///
167    /// let ex_unit = LengthUnit::Ex(2.0);
168    /// let context = FontContext::new(Length::from_pt(12.0));
169    /// let resolved = Length::resolve_unit(&ex_unit, &context);
170    /// // 2.0 * 6pt (x-height is 0.5 * font-size)
171    /// assert_eq!(resolved, Length::from_pt(12.0));
172    /// ```
173    #[inline]
174    #[must_use = "this returns a new value without modifying anything"]
175    pub fn from_ex(ex: f64) -> LengthUnit {
176        LengthUnit::Ex(ex)
177    }
178
179    /// Resolve a LengthUnit to an absolute Length using a FontContext
180    ///
181    /// For absolute units, returns the length directly.
182    /// For em units, multiplies by the font size.
183    /// For ex units, multiplies by the x-height.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use fop_types::{Length, FontContext, LengthUnit};
189    ///
190    /// let context = FontContext::new(Length::from_pt(12.0));
191    ///
192    /// // Absolute unit
193    /// let abs = LengthUnit::Absolute(12000);
194    /// assert_eq!(Length::resolve_unit(&abs, &context), Length::from_pt(12.0));
195    ///
196    /// // Em unit
197    /// let em = LengthUnit::Em(1.5);
198    /// assert_eq!(Length::resolve_unit(&em, &context), Length::from_pt(18.0));
199    ///
200    /// // Ex unit
201    /// let ex = LengthUnit::Ex(2.0);
202    /// assert_eq!(Length::resolve_unit(&ex, &context), Length::from_pt(12.0));
203    /// ```
204    #[must_use = "computed value is not stored automatically"]
205    pub fn resolve_unit(unit: &LengthUnit, context: &FontContext) -> Self {
206        match unit {
207            LengthUnit::Absolute(millipoints) => Self::from_millipoints(*millipoints),
208            LengthUnit::Em(em) => {
209                let font_size_milli = context.font_size.millipoints() as f64;
210                Self::from_millipoints((em * font_size_milli).round() as i32)
211            }
212            LengthUnit::Ex(ex) => {
213                let x_height_milli = context.x_height.millipoints() as f64;
214                Self::from_millipoints((ex * x_height_milli).round() as i32)
215            }
216        }
217    }
218
219    /// Get the value in points
220    #[inline]
221    #[must_use = "the result should be used"]
222    pub fn to_pt(self) -> f64 {
223        self.millipoints as f64 / Self::PT_TO_MILLI
224    }
225
226    /// Get the value in inches
227    #[inline]
228    #[must_use = "the result should be used"]
229    pub fn to_inch(self) -> f64 {
230        self.millipoints as f64 / Self::IN_TO_MILLI
231    }
232
233    /// Get the value in millimeters
234    #[inline]
235    #[must_use = "the result should be used"]
236    pub fn to_mm(self) -> f64 {
237        self.millipoints as f64 / Self::MM_TO_MILLI
238    }
239
240    /// Get the value in centimeters
241    #[inline]
242    #[must_use = "the result should be used"]
243    pub fn to_cm(self) -> f64 {
244        self.millipoints as f64 / Self::CM_TO_MILLI
245    }
246
247    /// Get the raw millipoint value
248    #[inline]
249    #[must_use = "the result should be used"]
250    pub const fn millipoints(self) -> i32 {
251        self.millipoints
252    }
253
254    /// Get the absolute value
255    #[inline]
256    #[must_use = "this returns a new value without modifying the original"]
257    pub fn abs(self) -> Self {
258        Self {
259            millipoints: self.millipoints.abs(),
260        }
261    }
262}
263
264impl std::ops::Neg for Length {
265    type Output = Self;
266
267    #[inline]
268    fn neg(self) -> Self {
269        Self {
270            millipoints: -self.millipoints,
271        }
272    }
273}
274
275impl fmt::Debug for Length {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        write!(f, "Length({}pt)", self.to_pt())
278    }
279}
280
281impl fmt::Display for Length {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        write!(f, "{}pt", self.to_pt())
284    }
285}
286
287impl std::ops::Add for Length {
288    type Output = Self;
289
290    #[inline]
291    fn add(self, other: Self) -> Self {
292        Self {
293            millipoints: self.millipoints + other.millipoints,
294        }
295    }
296}
297
298impl std::ops::Sub for Length {
299    type Output = Self;
300
301    #[inline]
302    fn sub(self, other: Self) -> Self {
303        Self {
304            millipoints: self.millipoints - other.millipoints,
305        }
306    }
307}
308
309impl std::ops::Mul<i32> for Length {
310    type Output = Self;
311
312    #[inline]
313    fn mul(self, scalar: i32) -> Self {
314        Self {
315            millipoints: self.millipoints * scalar,
316        }
317    }
318}
319
320impl std::ops::Div<i32> for Length {
321    type Output = Self;
322
323    #[inline]
324    fn div(self, scalar: i32) -> Self {
325        Self {
326            millipoints: self.millipoints / scalar,
327        }
328    }
329}
330
331impl std::ops::AddAssign for Length {
332    #[inline]
333    fn add_assign(&mut self, other: Self) {
334        self.millipoints += other.millipoints;
335    }
336}
337
338impl std::ops::SubAssign for Length {
339    #[inline]
340    fn sub_assign(&mut self, other: Self) {
341        self.millipoints -= other.millipoints;
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_zero() {
351        assert_eq!(Length::ZERO.millipoints(), 0);
352        assert_eq!(Length::ZERO.to_pt(), 0.0);
353    }
354
355    #[test]
356    fn test_point_conversion() {
357        let len = Length::from_pt(72.0);
358        assert_eq!(len.millipoints(), 72000);
359        assert!((len.to_pt() - 72.0).abs() < 0.001);
360    }
361
362    #[test]
363    fn test_inch_conversion() {
364        let len = Length::from_inch(1.0);
365        assert_eq!(len.millipoints(), 72000);
366        assert!((len.to_inch() - 1.0).abs() < 0.001);
367        assert!((len.to_pt() - 72.0).abs() < 0.001);
368    }
369
370    #[test]
371    fn test_mm_conversion() {
372        let len = Length::from_mm(25.4);
373        // 25.4 mm = 1 inch = 72 points
374        assert!((len.to_inch() - 1.0).abs() < 0.001);
375        assert!((len.to_pt() - 72.0).abs() < 0.1);
376    }
377
378    #[test]
379    fn test_cm_conversion() {
380        let len = Length::from_cm(2.54);
381        // 2.54 cm = 1 inch = 72 points
382        assert!((len.to_inch() - 1.0).abs() < 0.001);
383        assert!((len.to_pt() - 72.0).abs() < 0.1);
384    }
385
386    #[test]
387    fn test_arithmetic() {
388        let a = Length::from_pt(10.0);
389        let b = Length::from_pt(5.0);
390
391        let sum = a + b;
392        assert!((sum.to_pt() - 15.0).abs() < 0.001);
393
394        let diff = a - b;
395        assert!((diff.to_pt() - 5.0).abs() < 0.001);
396
397        let prod = a * 2;
398        assert!((prod.to_pt() - 20.0).abs() < 0.001);
399
400        let quot = a / 2;
401        assert!((quot.to_pt() - 5.0).abs() < 0.001);
402    }
403
404    #[test]
405    fn test_abs_neg() {
406        let len = Length::from_pt(-10.0);
407        assert_eq!(len.abs(), Length::from_pt(10.0));
408        assert_eq!(-len, Length::from_pt(10.0));
409    }
410
411    #[test]
412    fn test_ordering() {
413        let a = Length::from_pt(10.0);
414        let b = Length::from_pt(20.0);
415        assert!(a < b);
416        assert!(b > a);
417        assert_eq!(a, a);
418    }
419
420    #[test]
421    fn test_display() {
422        let len = Length::from_pt(12.0);
423        assert_eq!(format!("{}", len), "12pt");
424
425        let zero = Length::ZERO;
426        assert_eq!(format!("{}", zero), "0pt");
427
428        let negative = Length::from_pt(-5.5);
429        assert_eq!(format!("{}", negative), "-5.5pt");
430
431        let fractional = Length::from_pt(12.345);
432        assert_eq!(format!("{}", fractional), "12.345pt");
433    }
434
435    #[test]
436    fn test_display_units() {
437        // Display always shows points
438        let from_inch = Length::from_inch(1.0);
439        assert_eq!(format!("{}", from_inch), "72pt");
440
441        let from_mm = Length::from_mm(25.4);
442        // 25.4mm ≈ 72pt (1 inch)
443        assert!(format!("{}", from_mm).contains("pt"));
444
445        let from_cm = Length::from_cm(2.54);
446        // 2.54cm ≈ 72pt (1 inch)
447        assert!(format!("{}", from_cm).contains("pt"));
448    }
449
450    // Font-relative unit tests
451    #[test]
452    fn test_font_context_new() {
453        let context = FontContext::new(Length::from_pt(12.0));
454        assert_eq!(context.font_size, Length::from_pt(12.0));
455        // x-height should be 0.5 * font-size = 6pt
456        assert_eq!(context.x_height, Length::from_pt(6.0));
457    }
458
459    #[test]
460    fn test_font_context_with_x_height() {
461        let context = FontContext::with_x_height(Length::from_pt(12.0), Length::from_pt(5.0));
462        assert_eq!(context.font_size, Length::from_pt(12.0));
463        assert_eq!(context.x_height, Length::from_pt(5.0));
464    }
465
466    #[test]
467    fn test_em_unit_creation() {
468        let em = Length::from_em(1.5);
469        assert_eq!(em, LengthUnit::Em(1.5));
470    }
471
472    #[test]
473    fn test_ex_unit_creation() {
474        let ex = Length::from_ex(2.0);
475        assert_eq!(ex, LengthUnit::Ex(2.0));
476    }
477
478    #[test]
479    fn test_resolve_absolute_unit() {
480        let context = FontContext::new(Length::from_pt(12.0));
481        let unit = LengthUnit::Absolute(12000);
482        let resolved = Length::resolve_unit(&unit, &context);
483        assert_eq!(resolved, Length::from_pt(12.0));
484    }
485
486    #[test]
487    fn test_resolve_em_unit_one_em() {
488        let context = FontContext::new(Length::from_pt(12.0));
489        let unit = Length::from_em(1.0);
490        let resolved = Length::resolve_unit(&unit, &context);
491        assert_eq!(resolved, Length::from_pt(12.0));
492    }
493
494    #[test]
495    fn test_resolve_em_unit_one_and_half_em() {
496        let context = FontContext::new(Length::from_pt(12.0));
497        let unit = Length::from_em(1.5);
498        let resolved = Length::resolve_unit(&unit, &context);
499        assert_eq!(resolved, Length::from_pt(18.0));
500    }
501
502    #[test]
503    fn test_resolve_em_unit_fractional() {
504        let context = FontContext::new(Length::from_pt(10.0));
505        let unit = Length::from_em(0.8);
506        let resolved = Length::resolve_unit(&unit, &context);
507        assert_eq!(resolved, Length::from_pt(8.0));
508    }
509
510    #[test]
511    fn test_resolve_ex_unit_one_ex() {
512        let context = FontContext::new(Length::from_pt(12.0));
513        let unit = Length::from_ex(1.0);
514        let resolved = Length::resolve_unit(&unit, &context);
515        // x-height is 6pt (0.5 * 12pt)
516        assert_eq!(resolved, Length::from_pt(6.0));
517    }
518
519    #[test]
520    fn test_resolve_ex_unit_two_ex() {
521        let context = FontContext::new(Length::from_pt(12.0));
522        let unit = Length::from_ex(2.0);
523        let resolved = Length::resolve_unit(&unit, &context);
524        // 2 * 6pt = 12pt
525        assert_eq!(resolved, Length::from_pt(12.0));
526    }
527
528    #[test]
529    fn test_resolve_ex_unit_with_custom_x_height() {
530        let context = FontContext::with_x_height(Length::from_pt(12.0), Length::from_pt(5.0));
531        let unit = Length::from_ex(2.0);
532        let resolved = Length::resolve_unit(&unit, &context);
533        assert_eq!(resolved, Length::from_pt(10.0));
534    }
535
536    #[test]
537    fn test_em_with_different_font_sizes() {
538        // Test with 16pt font
539        let context16 = FontContext::new(Length::from_pt(16.0));
540        let unit = Length::from_em(1.0);
541        assert_eq!(
542            Length::resolve_unit(&unit, &context16),
543            Length::from_pt(16.0)
544        );
545
546        // Test with 24pt font
547        let context24 = FontContext::new(Length::from_pt(24.0));
548        assert_eq!(
549            Length::resolve_unit(&unit, &context24),
550            Length::from_pt(24.0)
551        );
552    }
553
554    #[test]
555    fn test_ex_with_different_font_sizes() {
556        // Test with 16pt font (x-height = 8pt)
557        let context16 = FontContext::new(Length::from_pt(16.0));
558        let unit = Length::from_ex(1.0);
559        assert_eq!(
560            Length::resolve_unit(&unit, &context16),
561            Length::from_pt(8.0)
562        );
563
564        // Test with 20pt font (x-height = 10pt)
565        let context20 = FontContext::new(Length::from_pt(20.0));
566        assert_eq!(
567            Length::resolve_unit(&unit, &context20),
568            Length::from_pt(10.0)
569        );
570    }
571
572    #[test]
573    fn test_em_negative_values() {
574        let context = FontContext::new(Length::from_pt(12.0));
575        let unit = Length::from_em(-1.0);
576        let resolved = Length::resolve_unit(&unit, &context);
577        assert_eq!(resolved, Length::from_pt(-12.0));
578    }
579
580    #[test]
581    fn test_ex_negative_values() {
582        let context = FontContext::new(Length::from_pt(12.0));
583        let unit = Length::from_ex(-2.0);
584        let resolved = Length::resolve_unit(&unit, &context);
585        assert_eq!(resolved, Length::from_pt(-12.0));
586    }
587
588    #[test]
589    fn test_em_zero() {
590        let context = FontContext::new(Length::from_pt(12.0));
591        let unit = Length::from_em(0.0);
592        let resolved = Length::resolve_unit(&unit, &context);
593        assert_eq!(resolved, Length::ZERO);
594    }
595
596    #[test]
597    fn test_ex_zero() {
598        let context = FontContext::new(Length::from_pt(12.0));
599        let unit = Length::from_ex(0.0);
600        let resolved = Length::resolve_unit(&unit, &context);
601        assert_eq!(resolved, Length::ZERO);
602    }
603
604    #[test]
605    fn test_length_unit_equality() {
606        assert_eq!(LengthUnit::Em(1.5), LengthUnit::Em(1.5));
607        assert_eq!(LengthUnit::Ex(2.0), LengthUnit::Ex(2.0));
608        assert_eq!(LengthUnit::Absolute(12000), LengthUnit::Absolute(12000));
609
610        assert_ne!(LengthUnit::Em(1.5), LengthUnit::Em(2.0));
611        assert_ne!(LengthUnit::Ex(1.0), LengthUnit::Ex(2.0));
612        assert_ne!(LengthUnit::Em(1.0), LengthUnit::Ex(1.0));
613    }
614}
615
616#[cfg(test)]
617mod length_extra_tests {
618    use super::*;
619
620    const TOLERANCE: f64 = 0.01;
621
622    fn approx(a: f64, b: f64) -> bool {
623        (a - b).abs() < TOLERANCE
624    }
625
626    // --- Unit conversion round-trips ---
627
628    #[test]
629    fn test_mm_roundtrip() {
630        let mm = Length::from_mm(42.0);
631        assert!(approx(mm.to_mm(), 42.0));
632    }
633
634    #[test]
635    fn test_cm_roundtrip() {
636        let cm = Length::from_cm(5.5);
637        assert!(approx(cm.to_cm(), 5.5));
638    }
639
640    #[test]
641    fn test_inch_roundtrip() {
642        let inch = Length::from_inch(3.0);
643        assert!(approx(inch.to_inch(), 3.0));
644    }
645
646    #[test]
647    fn test_pt_roundtrip() {
648        let pt = Length::from_pt(144.0);
649        assert!(approx(pt.to_pt(), 144.0));
650    }
651
652    // --- Cross-unit equivalence ---
653
654    #[test]
655    fn test_25_4mm_equals_1inch() {
656        // 25.4 mm = 1 inch exactly
657        let mm = Length::from_mm(25.4);
658        let inch = Length::from_inch(1.0);
659        assert!(approx(mm.to_pt(), inch.to_pt()));
660    }
661
662    #[test]
663    fn test_2_54cm_equals_1inch() {
664        let cm = Length::from_cm(2.54);
665        let inch = Length::from_inch(1.0);
666        assert!(approx(cm.to_pt(), inch.to_pt()));
667    }
668
669    #[test]
670    fn test_1cm_equals_10mm() {
671        let cm = Length::from_cm(1.0);
672        let mm = Length::from_mm(10.0);
673        assert!(approx(cm.to_pt(), mm.to_pt()));
674    }
675
676    #[test]
677    fn test_1inch_equals_72pt() {
678        let inch = Length::from_inch(1.0);
679        assert!(approx(inch.to_pt(), 72.0));
680    }
681
682    #[test]
683    fn test_a4_width_mm_to_pt() {
684        // A4 width: 210mm ≈ 595.276 pt
685        let w = Length::from_mm(210.0);
686        assert!(approx(w.to_pt(), 595.276));
687    }
688
689    #[test]
690    fn test_a4_height_mm_to_pt() {
691        // A4 height: 297mm ≈ 841.890 pt
692        let h = Length::from_mm(297.0);
693        assert!(approx(h.to_pt(), 841.890));
694    }
695
696    #[test]
697    fn test_letter_width_in_to_pt() {
698        // US Letter: 8.5in × 11in
699        let w = Length::from_inch(8.5);
700        assert!(approx(w.to_pt(), 612.0));
701    }
702
703    #[test]
704    fn test_letter_height_in_to_pt() {
705        let h = Length::from_inch(11.0);
706        assert!(approx(h.to_pt(), 792.0));
707    }
708
709    // --- Comparison via Ord ---
710
711    #[test]
712    fn test_max_via_ord() {
713        let a = Length::from_mm(5.0);
714        let b = Length::from_mm(10.0);
715        assert_eq!(a.max(b), b);
716        assert_eq!(b.max(a), b);
717    }
718
719    #[test]
720    fn test_min_via_ord() {
721        let a = Length::from_mm(5.0);
722        let b = Length::from_mm(10.0);
723        assert_eq!(a.min(b), a);
724        assert_eq!(b.min(a), a);
725    }
726
727    #[test]
728    fn test_clamp_via_ord() {
729        let val = Length::from_mm(15.0);
730        let lo = Length::from_mm(0.0);
731        let hi = Length::from_mm(10.0);
732        assert_eq!(val.clamp(lo, hi), hi);
733
734        let val2 = Length::from_mm(-5.0);
735        assert_eq!(val2.clamp(lo, hi), lo);
736
737        let val3 = Length::from_mm(7.0);
738        assert_eq!(val3.clamp(lo, hi), val3);
739    }
740
741    // --- Arithmetic ---
742
743    #[test]
744    fn test_add_assign() {
745        let mut a = Length::from_pt(10.0);
746        a += Length::from_pt(5.0);
747        assert!(approx(a.to_pt(), 15.0));
748    }
749
750    #[test]
751    fn test_sub_assign() {
752        let mut a = Length::from_pt(10.0);
753        a -= Length::from_pt(3.0);
754        assert!(approx(a.to_pt(), 7.0));
755    }
756
757    #[test]
758    fn test_mul_i32() {
759        let a = Length::from_pt(7.0);
760        assert!(approx((a * 3).to_pt(), 21.0));
761    }
762
763    #[test]
764    fn test_div_i32() {
765        let a = Length::from_pt(15.0);
766        assert!(approx((a / 5).to_pt(), 3.0));
767    }
768
769    #[test]
770    fn test_neg_operator() {
771        let a = Length::from_pt(8.0);
772        assert!(approx((-a).to_pt(), -8.0));
773    }
774
775    #[test]
776    fn test_abs_positive() {
777        let a = Length::from_pt(5.0);
778        assert_eq!(a.abs(), a);
779    }
780
781    #[test]
782    fn test_abs_negative() {
783        let a = Length::from_pt(-5.0);
784        assert!(approx(a.abs().to_pt(), 5.0));
785    }
786
787    // --- Millipoints ---
788
789    #[test]
790    fn test_from_millipoints_and_back() {
791        let mp = 72000_i32;
792        let len = Length::from_millipoints(mp);
793        assert_eq!(len.millipoints(), mp);
794        assert!(approx(len.to_pt(), 72.0));
795    }
796
797    #[test]
798    fn test_zero_millipoints() {
799        assert_eq!(Length::ZERO.millipoints(), 0);
800    }
801
802    #[test]
803    fn test_negative_millipoints() {
804        let neg = Length::from_pt(-10.0);
805        assert!(neg.millipoints() < 0);
806    }
807
808    // --- FontContext ---
809
810    #[test]
811    fn test_font_context_x_height_is_half_font_size() {
812        let ctx = FontContext::new(Length::from_pt(20.0));
813        assert!(approx(ctx.x_height.to_pt(), 10.0));
814    }
815
816    #[test]
817    fn test_font_context_with_x_height_explicit() {
818        let ctx = FontContext::with_x_height(Length::from_pt(16.0), Length::from_pt(7.0));
819        assert!(approx(ctx.x_height.to_pt(), 7.0));
820        assert!(approx(ctx.font_size.to_pt(), 16.0));
821    }
822
823    #[test]
824    fn test_em_resolution_large_font() {
825        // 2.5em with 24pt font = 60pt
826        let ctx = FontContext::new(Length::from_pt(24.0));
827        let unit = Length::from_em(2.5);
828        let resolved = Length::resolve_unit(&unit, &ctx);
829        assert!(approx(resolved.to_pt(), 60.0));
830    }
831
832    #[test]
833    fn test_ex_resolution_custom_x_height() {
834        // 3ex with x-height=4pt = 12pt
835        let ctx = FontContext::with_x_height(Length::from_pt(12.0), Length::from_pt(4.0));
836        let unit = Length::from_ex(3.0);
837        let resolved = Length::resolve_unit(&unit, &ctx);
838        assert!(approx(resolved.to_pt(), 12.0));
839    }
840
841    #[test]
842    fn test_resolve_absolute_unit_large() {
843        let ctx = FontContext::new(Length::from_pt(12.0));
844        let unit = LengthUnit::Absolute(595276); // A4 width in millipoints approx
845        let resolved = Length::resolve_unit(&unit, &ctx);
846        assert_eq!(resolved.millipoints(), 595276);
847    }
848
849    // --- Display/Debug ---
850
851    #[test]
852    fn test_debug_format() {
853        let len = Length::from_pt(36.0);
854        let s = format!("{:?}", len);
855        assert!(s.contains("36pt"));
856    }
857
858    #[test]
859    fn test_display_zero() {
860        assert_eq!(format!("{}", Length::ZERO), "0pt");
861    }
862
863    #[test]
864    fn test_display_fractional() {
865        let len = Length::from_pt(1.5);
866        assert_eq!(format!("{}", len), "1.5pt");
867    }
868
869    // --- LengthUnit Debug ---
870
871    #[test]
872    fn test_length_unit_debug_em() {
873        let unit = LengthUnit::Em(1.5);
874        let s = format!("{:?}", unit);
875        assert!(s.contains("Em"));
876        assert!(s.contains("1.5"));
877    }
878
879    #[test]
880    fn test_length_unit_debug_ex() {
881        let unit = LengthUnit::Ex(2.0);
882        let s = format!("{:?}", unit);
883        assert!(s.contains("Ex"));
884    }
885
886    #[test]
887    fn test_length_unit_debug_absolute() {
888        let unit = LengthUnit::Absolute(12000);
889        let s = format!("{:?}", unit);
890        assert!(s.contains("Absolute"));
891    }
892}
893
894#[cfg(test)]
895mod length_unit_conversion_tests {
896    use super::*;
897
898    const TOL: f64 = 0.001;
899
900    fn approx_eq(a: f64, b: f64) -> bool {
901        (a - b).abs() < TOL
902    }
903
904    // --- 1mm = 2.8346...pt ---
905
906    #[test]
907    fn test_1mm_to_pt_approx_2_8346() {
908        let l = Length::from_mm(1.0);
909        let pts = l.to_pt();
910        assert!(
911            (pts - 2.8346).abs() < 0.01,
912            "1mm should be ~2.83pt, got {}",
913            pts
914        );
915    }
916
917    // --- 1cm = 10mm ---
918
919    #[test]
920    fn test_1cm_equals_10mm_pt_value() {
921        let cm = Length::from_cm(1.0);
922        let mm = Length::from_mm(10.0);
923        assert!(approx_eq(cm.to_pt(), mm.to_pt()), "1cm != 10mm in pt");
924    }
925
926    // --- 1in = 72pt ---
927
928    #[test]
929    fn test_1in_equals_72pt() {
930        let inch = Length::from_inch(1.0);
931        assert!(
932            approx_eq(inch.to_pt(), 72.0),
933            "1in should be 72pt, got {}",
934            inch.to_pt()
935        );
936    }
937
938    // --- pc unit: 1pc = 12pt ---
939
940    #[test]
941    fn test_pica_to_pt() {
942        // 1 pica = 12 pt (1/6 inch)
943        // Build via 12pt directly since we have no from_pc
944        let one_pica_via_pt = Length::from_pt(12.0);
945        let one_pica_via_inch = Length::from_inch(1.0 / 6.0);
946        assert!(approx_eq(
947            one_pica_via_pt.to_pt(),
948            one_pica_via_inch.to_pt()
949        ));
950    }
951
952    // --- px equivalence (CSS px = 1/96 inch) ---
953
954    #[test]
955    fn test_css_px_to_pt() {
956        // 1 CSS px = 1/96 inch = 0.75 pt
957        let one_px = Length::from_inch(1.0 / 96.0);
958        assert!(
959            (one_px.to_pt() - 0.75).abs() < 0.01,
960            "1px should be 0.75pt, got {}",
961            one_px.to_pt()
962        );
963    }
964
965    #[test]
966    fn test_96px_equals_72pt() {
967        // 96px = 1in = 72pt
968        let ninety_six_px = Length::from_inch(1.0);
969        assert!(
970            approx_eq(ninety_six_px.to_pt(), 72.0),
971            "96px (=1in) should be 72pt"
972        );
973    }
974
975    // --- millipoint precision ---
976
977    #[test]
978    fn test_millipoint_precision_1pt() {
979        let l = Length::from_pt(1.0);
980        assert_eq!(l.millipoints(), 1000);
981    }
982
983    #[test]
984    fn test_millipoint_precision_1inch() {
985        let l = Length::from_inch(1.0);
986        assert_eq!(l.millipoints(), 72_000);
987    }
988
989    // --- round trip: pt -> millipoints -> pt ---
990
991    #[test]
992    fn test_pt_millipoints_roundtrip_fractional() {
993        let l = Length::from_pt(3.5);
994        assert!(approx_eq(l.to_pt(), 3.5));
995    }
996
997    // --- zero conversions ---
998
999    #[test]
1000    fn test_zero_mm_to_pt() {
1001        let l = Length::from_mm(0.0);
1002        assert_eq!(l.to_pt(), 0.0);
1003    }
1004
1005    #[test]
1006    fn test_zero_cm_to_pt() {
1007        assert_eq!(Length::from_cm(0.0).to_pt(), 0.0);
1008    }
1009
1010    #[test]
1011    fn test_zero_inch_to_pt() {
1012        assert_eq!(Length::from_inch(0.0).to_pt(), 0.0);
1013    }
1014
1015    // --- negative values ---
1016
1017    #[test]
1018    fn test_negative_mm() {
1019        let l = Length::from_mm(-5.0);
1020        assert!(l.millipoints() < 0);
1021        assert!(l < Length::ZERO);
1022    }
1023
1024    #[test]
1025    fn test_negative_inch() {
1026        let l = Length::from_inch(-1.0);
1027        assert!((l.to_pt() - (-72.0)).abs() < 0.1, "got {}pt", l.to_pt());
1028    }
1029
1030    // --- multiplication by f64 via millipoints ---
1031
1032    #[test]
1033    fn test_mul_by_i32_scalar() {
1034        let l = Length::from_pt(12.0);
1035        let result = l * 3;
1036        assert!(approx_eq(result.to_pt(), 36.0));
1037    }
1038
1039    #[test]
1040    fn test_div_by_i32_scalar() {
1041        let l = Length::from_pt(30.0);
1042        let result = l / 5;
1043        assert!(approx_eq(result.to_pt(), 6.0));
1044    }
1045
1046    // --- max/min (inherited from Ord) ---
1047
1048    #[test]
1049    fn test_max_of_two_lengths() {
1050        let a = Length::from_mm(5.0);
1051        let b = Length::from_mm(10.0);
1052        assert_eq!(a.max(b), b);
1053    }
1054
1055    #[test]
1056    fn test_min_of_two_lengths() {
1057        let a = Length::from_mm(5.0);
1058        let b = Length::from_mm(10.0);
1059        assert_eq!(a.min(b), a);
1060    }
1061
1062    // --- ordering ---
1063
1064    #[test]
1065    fn test_ordering_less_than() {
1066        let a = Length::from_mm(5.0);
1067        let b = Length::from_mm(10.0);
1068        assert!(a < b);
1069    }
1070
1071    #[test]
1072    fn test_ordering_greater_than() {
1073        let a = Length::from_mm(10.0);
1074        let b = Length::from_mm(5.0);
1075        assert!(a > b);
1076    }
1077
1078    #[test]
1079    fn test_ordering_equal() {
1080        let a = Length::from_pt(12.0);
1081        let b = Length::from_pt(12.0);
1082        assert!(a <= b && b <= a);
1083    }
1084
1085    // --- addition and subtraction across units ---
1086
1087    #[test]
1088    fn test_add_mm_and_pt() {
1089        // 25.4mm + 0pt = 1in = 72pt
1090        let mm = Length::from_mm(25.4);
1091        let pt = Length::from_pt(0.0);
1092        let result = mm + pt;
1093        assert!((result.to_pt() - 72.0).abs() < 0.1);
1094    }
1095
1096    #[test]
1097    fn test_sub_pt_from_inch() {
1098        // 1in - 36pt = 0.5in = 36pt
1099        let inch = Length::from_inch(1.0);
1100        let half = Length::from_pt(36.0);
1101        let result = inch - half;
1102        assert!((result.to_pt() - 36.0).abs() < 0.1);
1103    }
1104
1105    // --- abs ---
1106
1107    #[test]
1108    fn test_abs_of_negative_mm() {
1109        let l = Length::from_mm(-7.5);
1110        let abs = l.abs();
1111        assert!(approx_eq(abs.to_mm(), 7.5));
1112    }
1113
1114    // --- neg operator ---
1115
1116    #[test]
1117    fn test_neg_of_positive_length() {
1118        let l = Length::from_pt(10.0);
1119        let neg = -l;
1120        assert!(approx_eq(neg.to_pt(), -10.0));
1121    }
1122
1123    #[test]
1124    fn test_neg_of_negative_length() {
1125        let l = Length::from_pt(-5.0);
1126        let pos = -l;
1127        assert!(approx_eq(pos.to_pt(), 5.0));
1128    }
1129
1130    // --- clamp ---
1131
1132    #[test]
1133    fn test_clamp_length() {
1134        let val = Length::from_pt(150.0);
1135        let lo = Length::from_pt(0.0);
1136        let hi = Length::from_pt(100.0);
1137        assert_eq!(val.clamp(lo, hi), hi);
1138    }
1139
1140    // --- add_assign / sub_assign ---
1141
1142    #[test]
1143    fn test_add_assign_mm() {
1144        let mut a = Length::from_mm(5.0);
1145        a += Length::from_mm(3.0);
1146        assert!(approx_eq(a.to_mm(), 8.0));
1147    }
1148
1149    #[test]
1150    fn test_sub_assign_pt() {
1151        let mut a = Length::from_pt(20.0);
1152        a -= Length::from_pt(8.0);
1153        assert!(approx_eq(a.to_pt(), 12.0));
1154    }
1155}