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()
78 .filter(|g| g.style == FontStyle::Normal) .filter(|g| !g.is_ascii()) .for_each(|g| {
81 symbol_lookup.insert(g.id, g.symbol.clone());
82 layers.insert(g.symbol.clone(), g.id);
83 });
84
85 Ok(Self {
86 texture,
87 glyph_coords: layers,
88 symbol_lookup,
89 cell_size: (cell_width, cell_height),
90 num_slices: num_slices as u32,
91 underline: config.underline,
92 strikethrough: config.strikethrough,
93 glyph_tracker: GlyphTracker::new(),
94 })
95 }
96
97 pub fn bind(&self, gl: &web_sys::WebGl2RenderingContext, texture_unit: u32) {
99 self.texture.bind(gl, texture_unit);
100 }
101
102 pub fn cell_size(&self) -> (i32, i32) {
103 let (w, h) = self.cell_size;
104 (
105 w - 2 * FontAtlasData::PADDING,
106 h - 2 * FontAtlasData::PADDING,
107 )
108 }
109
110 pub fn underline(&self) -> beamterm_data::LineDecoration {
112 self.underline
113 }
114
115 pub fn strikethrough(&self) -> beamterm_data::LineDecoration {
117 self.strikethrough
118 }
119
120 pub fn get_symbol(&self, glyph_id: u16) -> Option<Cow<'_, str>> {
122 let base_glyph_id = glyph_id & (Glyph::GLYPH_ID_MASK | Glyph::EMOJI_FLAG);
123
124 if (0x20..0x80).contains(&base_glyph_id) {
125 let ch = base_glyph_id as u8 as char;
127 Some(Cow::from(ch.to_compact_string()))
128 } else {
129 self.symbol_lookup
130 .get(&base_glyph_id)
131 .map(|s| Cow::from(s.as_str()))
132 }
133 }
134
135 pub fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
137 if key.len() == 1 {
138 let ch = key.chars().next().unwrap();
139 if ch.is_ascii() {
140 let id = ch as u16;
142 return Some(id);
143 }
144 }
145
146 match self.glyph_coords.get(key) {
147 Some(id) => Some(*id),
148 None => {
149 self.glyph_tracker.record_missing(key);
150 None
151 },
152 }
153 }
154
155 pub fn glyph_tracker(&self) -> &GlyphTracker {
157 &self.glyph_tracker
158 }
159
160 pub(crate) fn glyph_count(&self) -> u32 {
163 let ascii_count = 0x80 - 0x20;
165 let non_ascii_count = self.symbol_lookup.len() as u32;
167 ascii_count + non_ascii_count
168 }
169
170 pub(crate) fn get_symbol_lookup(&self) -> &HashMap<u16, CompactString> {
171 &self.symbol_lookup
172 }
173}
174
175#[derive(Debug, Default)]
177pub struct GlyphTracker {
178 missing: RefCell<HashSet<CompactString>>,
179}
180
181impl GlyphTracker {
182 pub fn new() -> Self {
184 Self { missing: RefCell::new(HashSet::new()) }
185 }
186
187 pub fn record_missing(&self, glyph: &str) {
189 self.missing.borrow_mut().insert(glyph.into());
190 }
191
192 pub fn missing_glyphs(&self) -> HashSet<CompactString> {
194 self.missing.borrow().clone()
195 }
196
197 pub fn clear(&self) {
199 self.missing.borrow_mut().clear();
200 }
201
202 pub fn len(&self) -> usize {
204 self.missing.borrow().len()
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.missing.borrow().is_empty()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_glyph_tracker() {
219 let tracker = GlyphTracker::new();
220
221 assert!(tracker.is_empty());
223 assert_eq!(tracker.len(), 0);
224
225 tracker.record_missing("🎮");
227 tracker.record_missing("🎯");
228 tracker.record_missing("🎮"); assert!(!tracker.is_empty());
231 assert_eq!(tracker.len(), 2); let missing = tracker.missing_glyphs();
235 assert!(missing.contains(&CompactString::new("🎮")));
236 assert!(missing.contains(&CompactString::new("🎯")));
237
238 tracker.clear();
240 assert!(tracker.is_empty());
241 assert_eq!(tracker.len(), 0);
242 }
243}