1use std::sync::OnceLock;
10
11const FONT_BYTES: &[u8] = include_bytes!("../assets/JetBrainsMono-Regular.ttf");
12
13const BASE_PX: f32 = 12.0;
15
16fn font() -> &'static fontdue::Font {
17 static FONT: OnceLock<fontdue::Font> = OnceLock::new();
18 FONT.get_or_init(|| {
19 fontdue::Font::from_bytes(FONT_BYTES, fontdue::FontSettings::default())
20 .expect("the embedded font parses")
21 })
22}
23
24fn px_size(scale: i32) -> f32 {
25 BASE_PX * scale.clamp(1, 64) as f32
27}
28
29pub fn advance(scale: i32) -> i32 {
31 font().metrics('M', px_size(scale)).advance_width.round() as i32
32}
33
34pub fn line_height(scale: i32) -> i32 {
36 let m = font()
37 .horizontal_line_metrics(px_size(scale))
38 .expect("a horizontal font has line metrics");
39 (m.ascent - m.descent).round() as i32
40}
41
42pub fn ascent(scale: i32) -> i32 {
44 let m = font()
45 .horizontal_line_metrics(px_size(scale))
46 .expect("a horizontal font has line metrics");
47 m.ascent.round() as i32
48}
49
50pub fn text_width(len: usize, scale: i32) -> i32 {
52 (len as i32) * advance(scale)
53}
54
55pub fn fits_in_width(max_width: i32, scale: i32) -> usize {
57 usize::try_from(max_width / advance(scale).max(1)).unwrap_or(0)
58}
59
60pub fn fit_to_width(text: &str, max_width: i32, scale: i32) -> String {
66 let budget = fits_in_width(max_width, scale);
67 if text.chars().count() <= budget {
68 return text.to_string();
69 }
70 if budget <= 2 {
73 return String::new();
74 }
75 let kept: String = text.chars().take(budget - 2).collect();
76 format!("{kept}..")
77}
78
79pub fn rasterize(ch: char, scale: i32) -> (fontdue::Metrics, Vec<u8>) {
82 font().rasterize(ch, px_size(scale))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn text_that_fits_is_returned_whole() {
91 let width = text_width(20, 1);
92 assert_eq!(fit_to_width("Saved", width, 1), "Saved");
93 }
94
95 #[test]
96 fn over_long_text_is_cut_visibly() {
97 let width = text_width(10, 1);
98 let fitted = fit_to_width("Save failed: no space left on device", width, 1);
99 assert_eq!(fitted.chars().count(), 10);
100 assert!(fitted.ends_with(".."), "{fitted}");
101 assert!(text_width(fitted.chars().count(), 1) <= width);
102 }
103
104 #[test]
105 fn fitting_accounts_for_scale() {
106 let width = text_width(10, 1);
108 assert!(fits_in_width(width, 2) < 10);
109 assert!(fits_in_width(width, 2) >= 4);
110 }
111
112 #[test]
113 fn a_hopeless_budget_yields_nothing_rather_than_junk() {
114 assert_eq!(fit_to_width("Save failed", text_width(2, 1), 1), "");
115 assert_eq!(fit_to_width("Save failed", 0, 1), "");
116 }
117
118 #[test]
119 fn metrics_grow_with_scale() {
120 assert!(advance(1) > 0);
121 assert!(advance(2) > advance(1));
122 assert!(line_height(2) > line_height(1));
123 assert!(ascent(1) > 0 && ascent(1) < line_height(1));
124 }
125
126 #[test]
127 fn glyphs_rasterize_with_ink_and_space_without() {
128 let (_, cov) = rasterize('A', 2);
129 assert!(cov.contains(&255), "solid ink somewhere in 'A'");
130 let (m, cov) = rasterize(' ', 2);
131 assert!(cov.iter().all(|&a| a == 0), "space has no ink");
132 assert_eq!(m.width * m.height, cov.len());
133 }
134
135 #[test]
136 fn lowercase_and_unicode_have_distinct_glyphs() {
137 let (_, a) = rasterize('a', 2);
138 let (_, upper) = rasterize('A', 2);
139 assert_ne!(a, upper);
140 let (_, e_acute) = rasterize('\u{00E9}', 2);
142 assert!(e_acute.iter().any(|&v| v > 0));
143 }
144
145 #[test]
146 fn glyphs_fit_the_monospace_cell() {
147 for b in 0x20u8..=0x7E {
150 let (m, _) = rasterize(b as char, 2);
151 assert!(
152 m.xmin >= -1 && m.xmin + m.width as i32 <= advance(2) + 1,
153 "{:?} escapes its cell horizontally",
154 b as char
155 );
156 assert!(
157 m.height as i32 <= line_height(2),
158 "{:?} escapes its line box",
159 b as char
160 );
161 }
162 }
163}