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}