1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct LineHeight {
23 font_size_px: f64,
24 line_height_px: f64,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum LineHeightError {
29 InvalidFontSize,
30 InvalidLineHeight,
31 InvalidRatio,
32}
33
34fn validate_positive(value: f64, error: LineHeightError) -> Result<f64, LineHeightError> {
35 if !value.is_finite() || value <= 0.0 {
36 Err(error)
37 } else {
38 Ok(value)
39 }
40}
41
42impl LineHeight {
43 pub fn new(font_size_px: f64, line_height_px: f64) -> Result<Self, LineHeightError> {
44 Ok(Self {
45 font_size_px: validate_positive(font_size_px, LineHeightError::InvalidFontSize)?,
46 line_height_px: validate_positive(line_height_px, LineHeightError::InvalidLineHeight)?,
47 })
48 }
49
50 #[must_use]
51 pub fn ratio(&self) -> f64 {
52 self.line_height_px / self.font_size_px
53 }
54
55 #[must_use]
56 pub fn px(&self) -> f64 {
57 self.line_height_px
58 }
59}
60
61pub fn line_height_px(font_size_px: f64, ratio: f64) -> Result<f64, LineHeightError> {
62 Ok(
63 validate_positive(font_size_px, LineHeightError::InvalidFontSize)?
64 * validate_positive(ratio, LineHeightError::InvalidRatio)?,
65 )
66}
67
68pub fn line_height_ratio(font_size_px: f64, line_height_px: f64) -> Result<f64, LineHeightError> {
69 Ok(
70 validate_positive(line_height_px, LineHeightError::InvalidLineHeight)?
71 / validate_positive(font_size_px, LineHeightError::InvalidFontSize)?,
72 )
73}
74
75pub fn is_readable_line_height(ratio: f64) -> Result<bool, LineHeightError> {
76 let ratio = validate_positive(ratio, LineHeightError::InvalidRatio)?;
77 Ok((1.4..=1.8).contains(&ratio))
78}
79
80#[cfg(test)]
81mod tests {
82 use super::{
83 is_readable_line_height, line_height_px, line_height_ratio, LineHeight, LineHeightError,
84 };
85
86 #[test]
87 fn computes_line_height_ratios() {
88 let line_height = LineHeight::new(16.0, 24.0).unwrap();
89
90 assert_eq!(line_height.px(), 24.0);
91 assert!((line_height.ratio() - 1.5).abs() < 1.0e-12);
92 assert!((line_height_px(16.0, 1.5).unwrap() - 24.0).abs() < 1.0e-12);
93 assert!((line_height_ratio(16.0, 24.0).unwrap() - 1.5).abs() < 1.0e-12);
94 }
95
96 #[test]
97 fn checks_readable_line_height_thresholds() {
98 assert!(is_readable_line_height(1.4).unwrap());
99 assert!(is_readable_line_height(1.8).unwrap());
100 assert!(!is_readable_line_height(1.3).unwrap());
101 assert!(!is_readable_line_height(1.9).unwrap());
102 }
103
104 #[test]
105 fn rejects_invalid_line_height_inputs() {
106 assert_eq!(
107 LineHeight::new(0.0, 24.0),
108 Err(LineHeightError::InvalidFontSize)
109 );
110 assert_eq!(
111 line_height_px(16.0, 0.0),
112 Err(LineHeightError::InvalidRatio)
113 );
114 assert_eq!(
115 line_height_ratio(16.0, f64::NAN),
116 Err(LineHeightError::InvalidLineHeight)
117 );
118 assert_eq!(
119 is_readable_line_height(f64::NEG_INFINITY),
120 Err(LineHeightError::InvalidRatio)
121 );
122 }
123}