1#![forbid(unsafe_code)]
2#[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}