beamterm_core/gl/
static_atlas.rs1use std::collections::HashMap;
2
3use beamterm_data::{FontAtlasData, FontStyle, Glyph};
4use compact_str::{CompactString, ToCompactString};
5
6use super::{
7 atlas,
8 atlas::{Atlas, GlyphSlot, GlyphTracker},
9};
10use crate::error::Error;
11
12#[derive(Debug)]
25#[must_use = "call `delete(gl)` before dropping to avoid GPU resource leaks"]
26pub struct StaticFontAtlas {
27 texture: crate::gl::texture::Texture,
29 glyph_coords: HashMap<CompactString, u16>,
31 symbol_lookup: HashMap<u16, CompactString>,
33 cell_size: beamterm_data::CellSize,
35 underline: beamterm_data::LineDecoration,
37 strikethrough: beamterm_data::LineDecoration,
39 glyph_tracker: GlyphTracker,
41 last_halfwidth_base_glyph_id: u16,
43 atlas_data: FontAtlasData,
45}
46
47impl StaticFontAtlas {
48 pub fn load(gl: &glow::Context, config: FontAtlasData) -> Result<Self, Error> {
53 let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, &config)?;
54
55 let beamterm_data::CellSize { width: cell_width, height: cell_height } = config.cell_size();
56 let mut layers = HashMap::new();
57 let mut symbol_lookup = HashMap::new();
58
59 config.glyphs().iter()
65 .filter(|g| g.style() == FontStyle::Normal) .filter(|g| !g.is_ascii()) .for_each(|g| {
68 symbol_lookup.insert(g.id(), g.symbol().into());
69 layers.insert(CompactString::from(g.symbol()), g.id());
70 });
71
72 Ok(Self {
73 texture,
74 glyph_coords: layers,
75 last_halfwidth_base_glyph_id: config.max_halfwidth_base_glyph_id(),
76 symbol_lookup,
77 cell_size: beamterm_data::CellSize::new(cell_width, cell_height),
78 underline: config.underline(),
79 strikethrough: config.strikethrough(),
80 glyph_tracker: GlyphTracker::new(),
81 atlas_data: config,
82 })
83 }
84}
85
86impl atlas::sealed::Sealed for StaticFontAtlas {}
87
88impl Atlas for StaticFontAtlas {
89 fn get_glyph_id(&mut self, key: &str, style_bits: u16) -> Option<u16> {
90 let base_id = self.get_base_glyph_id(key)?;
91 Some(base_id | style_bits)
92 }
93
94 fn get_base_glyph_id(&mut self, key: &str) -> Option<u16> {
96 if key.len() == 1 {
97 let ch = key.chars().next().unwrap();
98 if ch.is_ascii() {
99 let id = ch as u16;
101 return Some(id);
102 }
103 }
104
105 match self.glyph_coords.get(key) {
106 Some(id) => Some(*id),
107 None => {
108 self.glyph_tracker.record_missing(key);
109 None
110 },
111 }
112 }
113
114 fn cell_size(&self) -> beamterm_data::CellSize {
115 beamterm_data::CellSize::new(
116 self.cell_size.width - 2 * FontAtlasData::PADDING,
117 self.cell_size.height - 2 * FontAtlasData::PADDING,
118 )
119 }
120
121 fn bind(&self, gl: &glow::Context) {
122 self.texture.bind(gl);
123 }
124
125 fn underline(&self) -> beamterm_data::LineDecoration {
127 self.underline
128 }
129
130 fn strikethrough(&self) -> beamterm_data::LineDecoration {
132 self.strikethrough
133 }
134
135 fn get_symbol(&self, glyph_id: u16) -> Option<CompactString> {
137 let glyph_id = glyph_id & !(Glyph::UNDERLINE_FLAG | Glyph::STRIKETHROUGH_FLAG);
138 let base_glyph_id = if glyph_id & Glyph::EMOJI_FLAG != 0 {
139 glyph_id & Glyph::GLYPH_ID_EMOJI_MASK
140 } else {
141 glyph_id & Glyph::GLYPH_ID_MASK
142 };
143
144 if (0x20..0x80).contains(&base_glyph_id) {
145 let ch = base_glyph_id as u8 as char;
147 Some(ch.to_compact_string())
148 } else {
149 self.symbol_lookup.get(&base_glyph_id).cloned()
150 }
151 }
152
153 fn get_ascii_char(&self, glyph_id: u16) -> Option<char> {
154 let glyph_id = glyph_id & Glyph::GLYPH_ID_MASK;
156 if (0x20..0x80).contains(&glyph_id) {
157 Some(glyph_id as u8 as char)
158 } else {
159 None
160 }
161 }
162
163 fn glyph_tracker(&self) -> &GlyphTracker {
164 &self.glyph_tracker
165 }
166
167 fn glyph_count(&self) -> u32 {
168 let ascii_count = 0x80 - 0x20;
170 let non_ascii_count = self.symbol_lookup.len() as u32;
172 ascii_count + non_ascii_count
173 }
174
175 fn flush(&mut self, _gl: &glow::Context) -> Result<(), Error> {
176 Ok(()) }
178
179 fn recreate_texture(&mut self, gl: &glow::Context) -> Result<(), Error> {
185 self.texture.delete(gl);
187
188 self.texture = crate::gl::texture::Texture::from_font_atlas_data(gl, &self.atlas_data)?;
190
191 Ok(())
192 }
193
194 fn for_each_symbol(&self, f: &mut dyn FnMut(u16, &str)) {
195 for code in 0x20u16..0x80 {
197 let ch = code as u8 as char;
198 let mut buf = [0u8; 4];
199 let s = ch.encode_utf8(&mut buf);
200 f(code, s);
201 }
202 for (glyph_id, symbol) in &self.symbol_lookup {
204 f(*glyph_id, symbol.as_str());
205 }
206 }
207
208 fn resolve_glyph_slot(&mut self, key: &str, style_bits: u16) -> Option<GlyphSlot> {
209 if key.len() == 1 {
210 let ch = key.chars().next().unwrap();
211 if ch.is_ascii() {
212 let id = ch as u16;
214 return Some(GlyphSlot::Normal(id | style_bits));
215 }
216 }
217
218 match self.glyph_coords.get(key) {
219 Some(base_glyph_id) => {
220 let id = base_glyph_id | style_bits;
221 if *base_glyph_id >= self.last_halfwidth_base_glyph_id {
222 Some(GlyphSlot::Wide(id))
223 } else if id & Glyph::EMOJI_FLAG != 0 {
224 Some(GlyphSlot::Emoji(id))
225 } else {
226 Some(GlyphSlot::Normal(id))
227 }
228 },
229 None => {
230 self.glyph_tracker.record_missing(key);
231 None
232 },
233 }
234 }
235
236 fn emoji_bit(&self) -> u32 {
241 12
242 }
243
244 fn delete(&self, gl: &glow::Context) {
245 self.texture.delete(gl);
246 }
247
248 fn update_pixel_ratio(&mut self, _gl: &glow::Context, pixel_ratio: f32) -> Result<f32, Error> {
249 Ok(pixel_ratio)
251 }
252
253 fn cell_scale_for_dpr(&self, pixel_ratio: f32) -> f32 {
254 if pixel_ratio <= 0.5 { 0.5 } else { pixel_ratio.round().max(1.0) }
256 }
257
258 fn texture_cell_size(&self) -> beamterm_data::CellSize {
259 self.cell_size()
261 }
262}