Skip to main content

use_text_block/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive text-block estimation helpers.
3//!
4//! These helpers estimate text block layout using explicit widths, average
5//! character widths, and line heights.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_text_block::{TextBlock, estimated_line_count, estimated_text_height};
11//!
12//! let block = TextBlock::new(480.0, 16.0, 24.0, 120).unwrap();
13//!
14//! assert_eq!(block.estimated_characters_per_line(8.0).unwrap(), 60.0);
15//! assert_eq!(block.estimated_line_count(8.0).unwrap(), 2);
16//! assert_eq!(block.estimated_height_px(8.0).unwrap(), 48.0);
17//! assert_eq!(estimated_line_count(120, 60.0).unwrap(), 2);
18//! assert_eq!(estimated_text_height(2, 24.0).unwrap(), 48.0);
19//! ```
20
21#[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}