Skip to main content

pixelcoords_core/
font.rs

1//! Embedded vector font — antialiased text for the overlay.
2//!
3//! `JetBrains Mono` Regular, rasterized on demand with `fontdue`. The font
4//! is monospace, so every layout computation stays "count x advance" — the
5//! same model the old bitmap table used, with the blockiness gone. The
6//! typeface is (c) 2020 The `JetBrains Mono` Project Authors, SIL Open Font
7//! License 1.1; the license text ships in `assets/JetBrainsMono-OFL.txt`.
8
9use std::sync::OnceLock;
10
11const FONT_BYTES: &[u8] = include_bytes!("../assets/JetBrainsMono-Regular.ttf");
12
13/// Text size in pixels at UI scale 1; `scale` multiplies it.
14const 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    // The cap keeps rasterization sane for absurd caller values.
26    BASE_PX * scale.clamp(1, 64) as f32
27}
28
29/// Horizontal advance of one glyph — monospace, so every glyph's.
30pub fn advance(scale: i32) -> i32 {
31    font().metrics('M', px_size(scale)).advance_width.round() as i32
32}
33
34/// Vertical space one line of text occupies.
35pub 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
42/// Baseline offset from the top of the line box.
43pub 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
50/// Pixel width of `len` glyphs at `scale`.
51pub fn text_width(len: usize, scale: i32) -> i32 {
52    (len as i32) * advance(scale)
53}
54
55/// How many glyphs fit in `max_width` pixels at `scale`.
56pub 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
60/// `text` shortened to fit `max_width` pixels, marking a cut with `..`.
61///
62/// Drawing clips silently at the buffer edge, so an over-long message —
63/// an error naming a path, most of all — would lose its tail without any
64/// sign that it had been cut.
65pub 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    // Two glyphs of the budget go to the marker; below that there is no
71    // room to say anything useful at all.
72    if budget <= 2 {
73        return String::new();
74    }
75    let kept: String = text.chars().take(budget - 2).collect();
76    format!("{kept}..")
77}
78
79/// Rasterize one glyph at `scale`: placement metrics plus a row-major
80/// coverage bitmap (0 = transparent, 255 = full ink).
81pub 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        // Ten glyphs' worth of scale-1 pixels holds fewer larger glyphs.
107        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        // The typeface covers far more than ASCII now.
141        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        // Every printable ASCII glyph's ink stays within one advance and
148        // one line box — the guarantee the column layout rests on.
149        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}