1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct TextBlock {
23 width_px: f64,
24 font_size_px: f64,
25 line_height_px: f64,
26 character_count: usize,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum TextBlockError {
31 InvalidWidth,
32 InvalidFontSize,
33 InvalidLineHeight,
34 InvalidCharacterWidth,
35 InvalidCharactersPerLine,
36}
37
38fn validate_positive(value: f64, error: TextBlockError) -> Result<f64, TextBlockError> {
39 if !value.is_finite() || value <= 0.0 {
40 Err(error)
41 } else {
42 Ok(value)
43 }
44}
45
46impl TextBlock {
47 pub fn new(
48 width_px: f64,
49 font_size_px: f64,
50 line_height_px: f64,
51 character_count: usize,
52 ) -> Result<Self, TextBlockError> {
53 Ok(Self {
54 width_px: validate_positive(width_px, TextBlockError::InvalidWidth)?,
55 font_size_px: validate_positive(font_size_px, TextBlockError::InvalidFontSize)?,
56 line_height_px: validate_positive(line_height_px, TextBlockError::InvalidLineHeight)?,
57 character_count,
58 })
59 }
60
61 pub fn estimated_characters_per_line(
62 &self,
63 average_character_width_px: f64,
64 ) -> Result<f64, TextBlockError> {
65 let _ = self.font_size_px;
66
67 Ok(self.width_px
68 / validate_positive(
69 average_character_width_px,
70 TextBlockError::InvalidCharacterWidth,
71 )?)
72 }
73
74 pub fn estimated_line_count(
75 &self,
76 average_character_width_px: f64,
77 ) -> Result<usize, TextBlockError> {
78 estimated_line_count(
79 self.character_count,
80 self.estimated_characters_per_line(average_character_width_px)?,
81 )
82 }
83
84 pub fn estimated_height_px(
85 &self,
86 average_character_width_px: f64,
87 ) -> Result<f64, TextBlockError> {
88 estimated_text_height(
89 self.estimated_line_count(average_character_width_px)?,
90 self.line_height_px,
91 )
92 }
93}
94
95pub fn estimated_line_count(
96 character_count: usize,
97 characters_per_line: f64,
98) -> Result<usize, TextBlockError> {
99 let characters_per_line = validate_positive(
100 characters_per_line,
101 TextBlockError::InvalidCharactersPerLine,
102 )?;
103
104 if character_count == 0 {
105 Ok(0)
106 } else {
107 Ok(((character_count as f64) / characters_per_line).ceil() as usize)
108 }
109}
110
111pub fn estimated_text_height(
112 line_count: usize,
113 line_height_px: f64,
114) -> Result<f64, TextBlockError> {
115 Ok((line_count as f64) * validate_positive(line_height_px, TextBlockError::InvalidLineHeight)?)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::{TextBlock, TextBlockError, estimated_line_count, estimated_text_height};
121
122 #[test]
123 fn estimates_text_block_lines_and_height() {
124 let block = TextBlock::new(480.0, 16.0, 24.0, 120).unwrap();
125
126 assert_eq!(block.estimated_characters_per_line(8.0).unwrap(), 60.0);
127 assert_eq!(block.estimated_line_count(8.0).unwrap(), 2);
128 assert_eq!(block.estimated_height_px(8.0).unwrap(), 48.0);
129 assert_eq!(estimated_line_count(125, 60.0).unwrap(), 3);
130 assert_eq!(estimated_text_height(3, 24.0).unwrap(), 72.0);
131 }
132
133 #[test]
134 fn supports_empty_text_blocks() {
135 let block = TextBlock::new(480.0, 16.0, 24.0, 0).unwrap();
136
137 assert_eq!(block.estimated_line_count(8.0).unwrap(), 0);
138 assert_eq!(block.estimated_height_px(8.0).unwrap(), 0.0);
139 }
140
141 #[test]
142 fn rejects_invalid_text_block_inputs() {
143 assert_eq!(
144 TextBlock::new(0.0, 16.0, 24.0, 10),
145 Err(TextBlockError::InvalidWidth)
146 );
147 assert_eq!(
148 estimated_line_count(10, 0.0),
149 Err(TextBlockError::InvalidCharactersPerLine)
150 );
151 assert_eq!(
152 estimated_text_height(2, f64::NAN),
153 Err(TextBlockError::InvalidLineHeight)
154 );
155 }
156}