1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct TypeRhythm {
23 base_font_size_px: f64,
24 base_line_height_px: f64,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TypeRhythmError {
29 InvalidFontSize,
30 InvalidLineHeight,
31 InvalidValue,
32}
33
34fn validate_positive(value: f64, error: TypeRhythmError) -> Result<f64, TypeRhythmError> {
35 if !value.is_finite() || value <= 0.0 {
36 Err(error)
37 } else {
38 Ok(value)
39 }
40}
41
42fn validate_non_negative(value: f64, error: TypeRhythmError) -> Result<f64, TypeRhythmError> {
43 if !value.is_finite() || value < 0.0 {
44 Err(error)
45 } else {
46 Ok(value)
47 }
48}
49
50impl TypeRhythm {
51 pub fn new(base_font_size_px: f64, base_line_height_px: f64) -> Result<Self, TypeRhythmError> {
52 Ok(Self {
53 base_font_size_px: validate_positive(
54 base_font_size_px,
55 TypeRhythmError::InvalidFontSize,
56 )?,
57 base_line_height_px: validate_positive(
58 base_line_height_px,
59 TypeRhythmError::InvalidLineHeight,
60 )?,
61 })
62 }
63
64 #[must_use]
65 pub fn baseline_unit(&self) -> f64 {
66 let _ = self.base_font_size_px;
67 self.base_line_height_px
68 }
69
70 pub fn snap_to_baseline(&self, value_px: f64) -> Result<f64, TypeRhythmError> {
71 snap_to_baseline(value_px, self.base_line_height_px)
72 }
73
74 pub fn lines_for_height(&self, height_px: f64) -> Result<f64, TypeRhythmError> {
75 Ok(
76 validate_non_negative(height_px, TypeRhythmError::InvalidValue)?
77 / self.base_line_height_px,
78 )
79 }
80}
81
82pub fn baseline_grid(base_line_height_px: f64, lines: usize) -> Result<Vec<f64>, TypeRhythmError> {
83 let base_line_height_px =
84 validate_positive(base_line_height_px, TypeRhythmError::InvalidLineHeight)?;
85
86 Ok((1..=lines)
87 .map(|line| (line as f64) * base_line_height_px)
88 .collect())
89}
90
91pub fn snap_to_baseline(value_px: f64, base_line_height_px: f64) -> Result<f64, TypeRhythmError> {
92 let value_px = validate_non_negative(value_px, TypeRhythmError::InvalidValue)?;
93 let base_line_height_px =
94 validate_positive(base_line_height_px, TypeRhythmError::InvalidLineHeight)?;
95
96 Ok((value_px / base_line_height_px).round() * base_line_height_px)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::{TypeRhythm, TypeRhythmError, baseline_grid, snap_to_baseline};
102
103 #[test]
104 fn generates_baseline_grid_values() {
105 let rhythm = TypeRhythm::new(16.0, 24.0).unwrap();
106
107 assert_eq!(rhythm.baseline_unit(), 24.0);
108 assert_eq!(rhythm.snap_to_baseline(37.0).unwrap(), 48.0);
109 assert_eq!(rhythm.lines_for_height(72.0).unwrap(), 3.0);
110 assert_eq!(baseline_grid(24.0, 3).unwrap(), vec![24.0, 48.0, 72.0]);
111 assert_eq!(snap_to_baseline(37.0, 24.0).unwrap(), 48.0);
112 }
113
114 #[test]
115 fn rejects_invalid_rhythm_inputs() {
116 assert_eq!(
117 TypeRhythm::new(0.0, 24.0),
118 Err(TypeRhythmError::InvalidFontSize)
119 );
120 assert_eq!(
121 baseline_grid(0.0, 3),
122 Err(TypeRhythmError::InvalidLineHeight)
123 );
124 assert_eq!(
125 snap_to_baseline(-1.0, 24.0),
126 Err(TypeRhythmError::InvalidValue)
127 );
128 assert_eq!(
129 TypeRhythm::new(16.0, 24.0)
130 .unwrap()
131 .lines_for_height(f64::NAN),
132 Err(TypeRhythmError::InvalidValue)
133 );
134 }
135}