Skip to main content

densa_text/
glyph_map.rs

1use rustc_hash::FxHashMap;
2
3use crate::{Font, GlyphId, LineMetrics};
4
5/// Non-ASCII characters remembered at most; past it the memory is forgotten and rebuilt.
6pub const MAX_CACHED_CHARS: usize = 4096;
7
8/// A character's glyph and advance at the map's size.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct MappedGlyph {
11    /// The glyph the font draws for the character.
12    pub id: GlyphId,
13    /// Horizontal advance in pixels.
14    pub advance: f32,
15}
16
17/// Cached character-to-glyph mapping for one font at one size.
18///
19/// [`Font::glyph_id`] re-reads the font's tables on every call; per-frame text needs the
20/// answer for thousands of characters, so this caches it: ASCII is resolved up front into
21/// a table, any other character on first use (that first use allocates, later ones don't).
22/// Characters the font lacks map to its `?` glyph.
23///
24/// Memory is bounded: after [`MAX_CACHED_CHARS`] distinct non-ASCII characters (streaming
25/// text, CJK), the non-ASCII memory is cleared, keeping its capacity, and refilled on use.
26#[derive(Debug, Clone)]
27pub struct GlyphMap {
28    font: Font,
29    px: f32,
30    ascii: [Option<MappedGlyph>; 128],
31    other: FxHashMap<char, Option<MappedGlyph>>,
32    fallback: Option<MappedGlyph>,
33}
34
35impl GlyphMap {
36    /// A map for `font` at `px` pixels per em.
37    pub fn new(font: Font, px: f32) -> Self {
38        let resolve = |ch: char| {
39            font.glyph_id(ch).map(|id| MappedGlyph {
40                id,
41                advance: font.advance(id, px),
42            })
43        };
44        let mut ascii = [None; 128];
45        for (code, slot) in (0u8..).zip(ascii.iter_mut()) {
46            *slot = resolve(char::from(code));
47        }
48        let fallback = resolve('?');
49        Self {
50            font,
51            px,
52            ascii,
53            other: FxHashMap::default(),
54            fallback,
55        }
56    }
57
58    /// The font this map resolves against.
59    pub fn font(&self) -> &Font {
60        &self.font
61    }
62
63    /// The size in pixels per em.
64    pub fn px(&self) -> f32 {
65        self.px
66    }
67
68    /// Non-ASCII characters currently remembered (at most [`MAX_CACHED_CHARS`]).
69    pub fn cached_chars(&self) -> usize {
70        self.other.len()
71    }
72
73    /// The font's line metrics at this size.
74    pub fn line_metrics(&self) -> LineMetrics {
75        self.font.line_metrics(self.px)
76    }
77
78    /// The glyph for `ch`, or the `?` glyph if the font has none; `None` only if the font
79    /// lacks `?` too.
80    #[inline]
81    pub fn get(&mut self, ch: char) -> Option<MappedGlyph> {
82        let mapped = match self.ascii.get(ch as usize) {
83            Some(slot) => *slot,
84            None => {
85                if self.other.len() >= MAX_CACHED_CHARS && !self.other.contains_key(&ch) {
86                    self.other.clear();
87                }
88                let (font, px) = (&self.font, self.px);
89                *self.other.entry(ch).or_insert_with(|| {
90                    font.glyph_id(ch).map(|id| MappedGlyph {
91                        id,
92                        advance: font.advance(id, px),
93                    })
94                })
95            }
96        };
97        mapped.or(self.fallback)
98    }
99
100    /// Total advance of `text` in pixels.
101    pub fn measure(&mut self, text: &str) -> f32 {
102        text.chars()
103            .filter_map(|ch| self.get(ch))
104            .map(|g| g.advance)
105            .sum()
106    }
107}