beamterm_renderer/gl/
atlas.rs1use std::{
2 borrow::Cow,
3 cell::RefCell,
4 collections::{HashMap, HashSet},
5};
6
7use beamterm_data::{FontAtlasData, FontStyle, Glyph};
8use compact_str::{CompactString, ToCompactString};
9use web_sys::console;
10
11use crate::{error::Error, gl::GL};
12
13#[derive(Debug)]
26pub struct FontAtlas {
27 texture: crate::gl::texture::Texture,
29 glyph_coords: HashMap<CompactString, u16>,
31 symbol_lookup: HashMap<u16, CompactString>,
33 cell_size: (i32, i32),
35 num_slices: u32,
37 underline: beamterm_data::LineDecoration,
39 strikethrough: beamterm_data::LineDecoration,
41 glyph_tracker: GlyphTracker,
43}
44
45impl FontAtlas {
46 pub fn load_default(gl: &web_sys::WebGl2RenderingContext) -> Result<Self, Error> {
48 let config = FontAtlasData::default();
49 Self::load(gl, config)
50 }
51
52 pub fn load(
54 gl: &web_sys::WebGl2RenderingContext,
55 config: FontAtlasData,
56 ) -> Result<Self, Error> {
57 let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, GL::RGBA, &config)?;
58 let num_slices = config.texture_dimensions.2;
59
60 let texture_layers = config
61 .glyphs
62 .iter()
63 .map(|g| g.id as i32)
64 .max()
65 .unwrap_or(0)
66 + 1;
67
68 let (cell_width, cell_height) = config.cell_size;
69 let mut layers = HashMap::new();
70 let mut symbol_lookup = HashMap::new();
71
72 config.glyphs.iter()
75 .filter(|g| g.style == FontStyle::Normal) .filter(|g| !g.is_ascii()) .for_each(|g| {
78 symbol_lookup.insert(g.id, g.symbol.clone());
79 layers.insert(g.symbol.clone(), g.id);
80 });
81
82 Ok(Self {
83 texture,
84 glyph_coords: layers,
85 symbol_lookup,
86 cell_size: (cell_width, cell_height),
87 num_slices: num_slices as u32,
88 underline: config.underline,
89 strikethrough: config.strikethrough,
90 glyph_tracker: GlyphTracker::new(),
91 })
92 }
93
94 pub fn bind(&self, gl: &web_sys::WebGl2RenderingContext, texture_unit: u32) {
96 self.texture.bind(gl, texture_unit);
97 }
98
99 pub fn cell_size(&self) -> (i32, i32) {
100 let (w, h) = self.cell_size;
101 (
102 w - 2 * FontAtlasData::PADDING,
103 h - 2 * FontAtlasData::PADDING,
104 )
105 }
106
107 pub fn underline(&self) -> beamterm_data::LineDecoration {
109 self.underline
110 }
111
112 pub fn strikethrough(&self) -> beamterm_data::LineDecoration {
114 self.strikethrough
115 }
116
117 pub fn get_symbol(&self, glyph_id: u16) -> Option<Cow<'_, str>> {
119 let base_glyph_id = glyph_id & (Glyph::GLYPH_ID_MASK | Glyph::EMOJI_FLAG);
120
121 if (0x20..0x80).contains(&base_glyph_id) {
122 let ch = base_glyph_id as u8 as char;
124 Some(Cow::from(ch.to_compact_string()))
125 } else {
126 self.symbol_lookup
127 .get(&base_glyph_id)
128 .map(|s| Cow::from(s.as_str()))
129 }
130 }
131
132 pub fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
134 if key.len() == 1 {
135 let ch = key.chars().next().unwrap();
136 if ch.is_ascii() {
137 let id = ch as u16;
139 return Some(id);
140 }
141 }
142
143 match self.glyph_coords.get(key) {
144 Some(id) => Some(*id),
145 None => {
146 self.glyph_tracker.record_missing(key);
147 None
148 },
149 }
150 }
151
152 pub fn glyph_tracker(&self) -> &GlyphTracker {
154 &self.glyph_tracker
155 }
156
157 pub(crate) fn glyph_count(&self) -> u32 {
160 let ascii_count = 0x80 - 0x20;
162 let non_ascii_count = self.symbol_lookup.len() as u32;
164 ascii_count + non_ascii_count
165 }
166}
167
168#[derive(Debug, Default)]
170pub struct GlyphTracker {
171 missing: RefCell<HashSet<CompactString>>,
172}
173
174impl GlyphTracker {
175 pub fn new() -> Self {
177 Self { missing: RefCell::new(HashSet::new()) }
178 }
179
180 pub fn record_missing(&self, glyph: &str) {
182 self.missing.borrow_mut().insert(glyph.into());
183 }
184
185 pub fn missing_glyphs(&self) -> HashSet<CompactString> {
187 self.missing.borrow().clone()
188 }
189
190 pub fn clear(&self) {
192 self.missing.borrow_mut().clear();
193 }
194
195 pub fn len(&self) -> usize {
197 self.missing.borrow().len()
198 }
199
200 pub fn is_empty(&self) -> bool {
202 self.missing.borrow().is_empty()
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_glyph_tracker() {
212 let tracker = GlyphTracker::new();
213
214 assert!(tracker.is_empty());
216 assert_eq!(tracker.len(), 0);
217
218 tracker.record_missing("🎮");
220 tracker.record_missing("🎯");
221 tracker.record_missing("🎮"); assert!(!tracker.is_empty());
224 assert_eq!(tracker.len(), 2); let missing = tracker.missing_glyphs();
228 assert!(missing.contains(&CompactString::new("🎮")));
229 assert!(missing.contains(&CompactString::new("🎯")));
230
231 tracker.clear();
233 assert!(tracker.is_empty());
234 assert_eq!(tracker.len(), 0);
235 }
236}