Skip to main content

use_line_height/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive line-height helpers.
3//!
4//! These helpers expose explicit line-height calculations and a small readable
5//! threshold check.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_line_height::{LineHeight, is_readable_line_height, line_height_px, line_height_ratio};
11//!
12//! let line_height = LineHeight::new(16.0, 24.0).unwrap();
13//!
14//! assert_eq!(line_height.px(), 24.0);
15//! assert!((line_height.ratio() - 1.5).abs() < 1.0e-12);
16//! assert!((line_height_px(16.0, 1.5).unwrap() - 24.0).abs() < 1.0e-12);
17//! assert!((line_height_ratio(16.0, 24.0).unwrap() - 1.5).abs() < 1.0e-12);
18//! assert!(is_readable_line_height(1.5).unwrap());
19//! ```
20
21#[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}