Skip to main content

use_type_rhythm/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive vertical rhythm helpers.
3//!
4//! These helpers expose a small baseline grid model for deterministic layout
5//! calculations.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_type_rhythm::{TypeRhythm, baseline_grid, snap_to_baseline};
11//!
12//! let rhythm = TypeRhythm::new(16.0, 24.0).unwrap();
13//!
14//! assert_eq!(rhythm.baseline_unit(), 24.0);
15//! assert_eq!(rhythm.snap_to_baseline(37.0).unwrap(), 48.0);
16//! assert_eq!(rhythm.lines_for_height(72.0).unwrap(), 3.0);
17//! assert_eq!(baseline_grid(24.0, 3).unwrap(), vec![24.0, 48.0, 72.0]);
18//! assert_eq!(snap_to_baseline(37.0, 24.0).unwrap(), 48.0);
19//! ```
20
21#[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}