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> {
50 let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, &config)?;
51
52 let beamterm_data::CellSize { width: cell_width, height: cell_height } = config.cell_size();
53 let mut layers = HashMap::new();
54 let mut symbol_lookup = HashMap::new();
55
56 config.glyphs().iter()
62 .filter(|g| g.style() == FontStyle::Normal) .filter(|g| !g.is_ascii()) .for_each(|g| {
65 symbol_lookup.insert(g.id(), g.symbol().into());
66 layers.insert(CompactString::from(g.symbol()), g.id());
67 });
68
69 Ok(Self {
70 texture,
71 glyph_coords: layers,
72 last_halfwidth_base_glyph_id: config.max_halfwidth_base_glyph_id(),
73 symbol_lookup,
74 cell_size: beamterm_data::CellSize::new(cell_width, cell_height),
75 underline: config.underline(),
76 strikethrough: config.strikethrough(),
77 glyph_tracker: GlyphTracker::new(),
78 atlas_data: config,
79 })
80 }
81}
82
83impl atlas::sealed::Sealed for StaticFontAtlas {}
84
85impl Atlas for StaticFontAtlas {
86 fn get_glyph_id(&mut self, key: &str, style_bits: u16) -> Option<u16> {
87 let base_id = self.get_base_glyph_id(key)?;
88 Some(base_id | style_bits)
89 }
90
91 fn get_base_glyph_id(&mut self, key: &str) -> Option<u16> {
93 if key.len() == 1 {
94 let ch = key.chars().next().unwrap();
95 if ch.is_ascii() {
96 let id = ch as u16;
98 return Some(id);
99 }
100 }
101
102 match self.glyph_coords.get(key) {
103 Some(id) => Some(*id),
104 None => {
105 self.glyph_tracker.record_missing(key);
106 None
107 },
108 }
109 }
110
111 fn cell_size(&self) -> beamterm_data::CellSize {
112 beamterm_data::CellSize::new(
113 self.cell_size.width - 2 * FontAtlasData::PADDING,
114 self.cell_size.height - 2 * FontAtlasData::PADDING,
115 )
116 }
117
118 fn bind(&self, gl: &glow::Context) {
119 self.texture.bind(gl);
120 }
121
122 fn underline(&self) -> beamterm_data::LineDecoration {
124 self.underline
125 }
126
127 fn strikethrough(&self) -> beamterm_data::LineDecoration {
129 self.strikethrough
130 }
131
132 fn get_symbol(&self, glyph_id: u16) -> Option<CompactString> {
134 let base_glyph_id = if glyph_id & Glyph::EMOJI_FLAG != 0 {
135 glyph_id & Glyph::GLYPH_ID_EMOJI_MASK
136 } else {
137 glyph_id & Glyph::GLYPH_ID_MASK
138 };
139
140 if (0x20..0x80).contains(&base_glyph_id) {
141 let ch = base_glyph_id as u8 as char;
143 Some(ch.to_compact_string())
144 } else {
145 self.symbol_lookup.get(&base_glyph_id).cloned()
146 }
147 }
148
149 fn get_ascii_char(&self, glyph_id: u16) -> Option<char> {
150 if (0x20..0x80).contains(&glyph_id) {
152 Some(glyph_id as u8 as char)
153 } else {
154 None
155 }
156 }
157
158 fn glyph_tracker(&self) -> &GlyphTracker {
159 &self.glyph_tracker
160 }
161
162 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 fn flush(&mut self, _gl: &glow::Context) -> Result<(), Error> {
171 Ok(()) }
173
174 fn recreate_texture(&mut self, gl: &glow::Context) -> Result<(), Error> {
180 self.texture.delete(gl);
182
183 self.texture = crate::gl::texture::Texture::from_font_atlas_data(gl, &self.atlas_data)?;
185
186 Ok(())
187 }
188
189 fn for_each_symbol(&self, f: &mut dyn FnMut(u16, &str)) {
190 for code in 0x20u16..0x80 {
192 let ch = code as u8 as char;
193 let mut buf = [0u8; 4];
194 let s = ch.encode_utf8(&mut buf);
195 f(code, s);
196 }
197 for (glyph_id, symbol) in &self.symbol_lookup {
199 f(*glyph_id, symbol.as_str());
200 }
201 }
202
203 fn resolve_glyph_slot(&mut self, key: &str, style_bits: u16) -> Option<GlyphSlot> {
204 if key.len() == 1 {
205 let ch = key.chars().next().unwrap();
206 if ch.is_ascii() {
207 let id = ch as u16;
209 return Some(GlyphSlot::Normal(id | style_bits));
210 }
211 }
212
213 match self.glyph_coords.get(key) {
214 Some(base_glyph_id) => {
215 let id = base_glyph_id | style_bits;
216 if *base_glyph_id >= self.last_halfwidth_base_glyph_id {
217 Some(GlyphSlot::Wide(id))
218 } else if id & Glyph::EMOJI_FLAG != 0 {
219 Some(GlyphSlot::Emoji(id))
220 } else {
221 Some(GlyphSlot::Normal(id))
222 }
223 },
224 None => {
225 self.glyph_tracker.record_missing(key);
226 None
227 },
228 }
229 }
230
231 fn emoji_bit(&self) -> u32 {
236 12
237 }
238
239 fn delete(&self, gl: &glow::Context) {
240 self.texture.delete(gl);
241 }
242
243 fn update_pixel_ratio(&mut self, _gl: &glow::Context, pixel_ratio: f32) -> Result<f32, Error> {
244 Ok(pixel_ratio)
246 }
247
248 fn cell_scale_for_dpr(&self, pixel_ratio: f32) -> f32 {
249 if pixel_ratio <= 0.5 { 0.5 } else { pixel_ratio.round().max(1.0) }
251 }
252
253 fn texture_cell_size(&self) -> beamterm_data::CellSize {
254 self.cell_size()
256 }
257}