Skip to main content

use_readable_text/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive readable text sizing and measure helpers.
3//!
4//! These are practical utility thresholds, not a full typography system.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_readable_text::{
10//!     TextSize, characters_per_line, is_line_height_readable, is_measure_readable,
11//!     line_height_ratio,
12//! };
13//!
14//! let text = TextSize::new(16.0, 24.0).unwrap();
15//! let characters = characters_per_line(560.0, 8.0).unwrap();
16//!
17//! assert_eq!(line_height_ratio(16.0, 24.0).unwrap(), 1.5);
18//! assert_eq!(text.line_height_ratio(), 1.5);
19//! assert!(is_line_height_readable(16.0, 24.0).unwrap());
20//! assert_eq!(characters, 70.0);
21//! assert!(is_measure_readable(characters).unwrap());
22//! ```
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct TextSize {
26    font_size_px: f64,
27    line_height_px: f64,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ReadableTextError {
32    InvalidFontSize,
33    InvalidLineHeight,
34    InvalidContainerWidth,
35    InvalidCharacterWidth,
36    InvalidCharactersPerLine,
37}
38
39fn validate_positive(value: f64, error: ReadableTextError) -> Result<f64, ReadableTextError> {
40    if !value.is_finite() || value <= 0.0 {
41        Err(error)
42    } else {
43        Ok(value)
44    }
45}
46
47impl TextSize {
48    pub fn new(font_size_px: f64, line_height_px: f64) -> Result<Self, ReadableTextError> {
49        Ok(Self {
50            font_size_px: validate_positive(font_size_px, ReadableTextError::InvalidFontSize)?,
51            line_height_px: validate_positive(
52                line_height_px,
53                ReadableTextError::InvalidLineHeight,
54            )?,
55        })
56    }
57
58    #[must_use]
59    pub fn line_height_ratio(&self) -> f64 {
60        self.line_height_px / self.font_size_px
61    }
62}
63
64pub fn line_height_ratio(font_size_px: f64, line_height_px: f64) -> Result<f64, ReadableTextError> {
65    Ok(
66        validate_positive(line_height_px, ReadableTextError::InvalidLineHeight)?
67            / validate_positive(font_size_px, ReadableTextError::InvalidFontSize)?,
68    )
69}
70
71pub fn is_line_height_readable(
72    font_size_px: f64,
73    line_height_px: f64,
74) -> Result<bool, ReadableTextError> {
75    Ok(line_height_ratio(font_size_px, line_height_px)? >= 1.4)
76}
77
78pub fn characters_per_line(
79    container_width_px: f64,
80    average_character_width_px: f64,
81) -> Result<f64, ReadableTextError> {
82    Ok(
83        validate_positive(container_width_px, ReadableTextError::InvalidContainerWidth)?
84            / validate_positive(
85                average_character_width_px,
86                ReadableTextError::InvalidCharacterWidth,
87            )?,
88    )
89}
90
91pub fn is_measure_readable(characters_per_line: f64) -> Result<bool, ReadableTextError> {
92    let characters_per_line = validate_positive(
93        characters_per_line,
94        ReadableTextError::InvalidCharactersPerLine,
95    )?;
96
97    Ok((45.0..=90.0).contains(&characters_per_line))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::{
103        ReadableTextError, TextSize, characters_per_line, is_line_height_readable,
104        is_measure_readable, line_height_ratio,
105    };
106
107    #[test]
108    fn checks_readable_line_height_defaults() {
109        let text = TextSize::new(16.0, 24.0).unwrap();
110
111        assert_eq!(text.line_height_ratio(), 1.5);
112        assert_eq!(line_height_ratio(16.0, 24.0).unwrap(), 1.5);
113        assert!(is_line_height_readable(16.0, 24.0).unwrap());
114        assert!(!is_line_height_readable(16.0, 20.0).unwrap());
115    }
116
117    #[test]
118    fn checks_readable_measure_defaults() {
119        let characters = characters_per_line(560.0, 8.0).unwrap();
120
121        assert_eq!(characters, 70.0);
122        assert!(is_measure_readable(characters).unwrap());
123        assert!(!is_measure_readable(30.0).unwrap());
124        assert!(!is_measure_readable(100.0).unwrap());
125    }
126
127    #[test]
128    fn rejects_invalid_text_inputs() {
129        assert_eq!(
130            TextSize::new(0.0, 24.0),
131            Err(ReadableTextError::InvalidFontSize)
132        );
133        assert_eq!(
134            line_height_ratio(16.0, f64::NAN),
135            Err(ReadableTextError::InvalidLineHeight)
136        );
137        assert_eq!(
138            characters_per_line(0.0, 8.0),
139            Err(ReadableTextError::InvalidContainerWidth)
140        );
141        assert_eq!(
142            is_measure_readable(f64::INFINITY),
143            Err(ReadableTextError::InvalidCharactersPerLine)
144        );
145    }
146}