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 last_halfwidth_base_glyph_id: u16,
45 atlas_data: FontAtlasData,
47}
48
49impl FontAtlas {
50 pub fn load_default(gl: &web_sys::WebGl2RenderingContext) -> Result<Self, Error> {
52 let config = FontAtlasData::default();
53 Self::load(gl, config)
54 }
55
56 pub fn load(
58 gl: &web_sys::WebGl2RenderingContext,
59 config: FontAtlasData,
60 ) -> Result<Self, Error> {
61 let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, GL::RGBA, &config)?;
62 let num_slices = config.texture_dimensions.2;
63
64 let texture_layers = config
65 .glyphs
66 .iter()
67 .map(|g| g.id as i32)
68 .max()
69 .unwrap_or(0)
70 + 1;
71
72 let (cell_width, cell_height) = config.cell_size;
73 let mut layers = HashMap::new();
74 let mut symbol_lookup = HashMap::new();
75
76 config.glyphs.iter()
82 .filter(|g| g.style == FontStyle::Normal) .filter(|g| !g.is_ascii()) .for_each(|g| {
85 symbol_lookup.insert(g.id, g.symbol.clone());
86 layers.insert(g.symbol.clone(), g.id);
87 });
88
89 Ok(Self {
90 texture,
91 glyph_coords: layers,
92 last_halfwidth_base_glyph_id: config.max_halfwidth_base_glyph_id,
93 symbol_lookup,
94 cell_size: (cell_width, cell_height),
95 num_slices: num_slices as u32,
96 underline: config.underline,
97 strikethrough: config.strikethrough,
98 glyph_tracker: GlyphTracker::new(),
99 atlas_data: config,
100 })
101 }
102
103 pub fn bind(&self, gl: &web_sys::WebGl2RenderingContext, texture_unit: u32) {
105 self.texture.bind(gl, texture_unit);
106 }
107
108 pub fn cell_size(&self) -> (i32, i32) {
109 let (w, h) = self.cell_size;
110 (
111 w - 2 * FontAtlasData::PADDING,
112 h - 2 * FontAtlasData::PADDING,
113 )
114 }
115
116 pub fn underline(&self) -> beamterm_data::LineDecoration {
118 self.underline
119 }
120
121 pub fn strikethrough(&self) -> beamterm_data::LineDecoration {
123 self.strikethrough
124 }
125
126 pub fn get_symbol(&self, glyph_id: u16) -> Option<Cow<'_, str>> {
128 let base_glyph_id = if glyph_id & Glyph::EMOJI_FLAG != 0 {
129 glyph_id & Glyph::GLYPH_ID_EMOJI_MASK
130 } else {
131 glyph_id & Glyph::GLYPH_ID_MASK
132 };
133
134 if (0x20..0x80).contains(&base_glyph_id) {
135 let ch = base_glyph_id as u8 as char;
137 Some(Cow::from(ch.to_compact_string()))
138 } else {
139 self.symbol_lookup
140 .get(&base_glyph_id)
141 .map(|s| Cow::from(s.as_str()))
142 }
143 }
144
145 pub fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
147 if key.len() == 1 {
148 let ch = key.chars().next().unwrap();
149 if ch.is_ascii() {
150 let id = ch as u16;
152 return Some(id);
153 }
154 }
155
156 match self.glyph_coords.get(key) {
157 Some(id) => Some(*id),
158 None => {
159 self.glyph_tracker.record_missing(key);
160 None
161 },
162 }
163 }
164
165 pub fn get_max_halfwidth_base_glyph_id(&self) -> u16 {
167 self.last_halfwidth_base_glyph_id
168 }
169
170 pub fn glyph_tracker(&self) -> &GlyphTracker {
172 &self.glyph_tracker
173 }
174
175 pub(crate) fn glyph_count(&self) -> u32 {
178 let ascii_count = 0x80 - 0x20;
180 let non_ascii_count = self.symbol_lookup.len() as u32;
182 ascii_count + non_ascii_count
183 }
184
185 pub(crate) fn get_symbol_lookup(&self) -> &HashMap<u16, CompactString> {
186 &self.symbol_lookup
187 }
188
189 pub(crate) fn recreate_texture(
202 &mut self,
203 gl: &web_sys::WebGl2RenderingContext,
204 ) -> Result<(), Error> {
205 self.texture.delete(gl);
207
208 self.texture =
210 crate::gl::texture::Texture::from_font_atlas_data(gl, GL::RGBA, &self.atlas_data)?;
211
212 Ok(())
213 }
214}
215
216#[derive(Debug, Default)]
218pub struct GlyphTracker {
219 missing: RefCell<HashSet<CompactString>>,
220}
221
222impl GlyphTracker {
223 pub fn new() -> Self {
225 Self { missing: RefCell::new(HashSet::new()) }
226 }
227
228 pub fn record_missing(&self, glyph: &str) {
230 self.missing.borrow_mut().insert(glyph.into());
231 }
232
233 pub fn missing_glyphs(&self) -> HashSet<CompactString> {
235 self.missing.borrow().clone()
236 }
237
238 pub fn clear(&self) {
240 self.missing.borrow_mut().clear();
241 }
242
243 pub fn len(&self) -> usize {
245 self.missing.borrow().len()
246 }
247
248 pub fn is_empty(&self) -> bool {
250 self.missing.borrow().is_empty()
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_glyph_tracker() {
260 let tracker = GlyphTracker::new();
261
262 assert!(tracker.is_empty());
264 assert_eq!(tracker.len(), 0);
265
266 tracker.record_missing("🎮");
268 tracker.record_missing("🎯");
269 tracker.record_missing("🎮"); assert!(!tracker.is_empty());
272 assert_eq!(tracker.len(), 2); let missing = tracker.missing_glyphs();
276 assert!(missing.contains(&CompactString::new("🎮")));
277 assert!(missing.contains(&CompactString::new("🎯")));
278
279 tracker.clear();
281 assert!(tracker.is_empty());
282 assert_eq!(tracker.len(), 0);
283 }
284}