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)]
25pub struct StaticFontAtlas {
26 texture: crate::gl::texture::Texture,
28 glyph_coords: HashMap<CompactString, u16>,
30 symbol_lookup: HashMap<u16, CompactString>,
32 cell_size: (i32, i32),
34 underline: beamterm_data::LineDecoration,
36 strikethrough: beamterm_data::LineDecoration,
38 glyph_tracker: GlyphTracker,
40 last_halfwidth_base_glyph_id: u16,
42 atlas_data: FontAtlasData,
44}
45
46impl StaticFontAtlas {
47 pub fn load(gl: &glow::Context, config: FontAtlasData) -> Result<Self, Error> {
49 let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, &config)?;
50
51 let (cell_width, cell_height) = config.cell_size;
52 let mut layers = HashMap::new();
53 let mut symbol_lookup = HashMap::new();
54
55 config.glyphs.iter()
61 .filter(|g| g.style == FontStyle::Normal) .filter(|g| !g.is_ascii()) .for_each(|g| {
64 symbol_lookup.insert(g.id, g.symbol.clone());
65 layers.insert(g.symbol.clone(), g.id);
66 });
67
68 Ok(Self {
69 texture,
70 glyph_coords: layers,
71 last_halfwidth_base_glyph_id: config.max_halfwidth_base_glyph_id,
72 symbol_lookup,
73 cell_size: (cell_width, cell_height),
74 underline: config.underline,
75 strikethrough: config.strikethrough,
76 glyph_tracker: GlyphTracker::new(),
77 atlas_data: config,
78 })
79 }
80}
81
82impl atlas::sealed::Sealed for StaticFontAtlas {}
83
84impl Atlas for StaticFontAtlas {
85 fn get_glyph_id(&self, key: &str, style_bits: u16) -> Option<u16> {
86 let base_id = self.get_base_glyph_id(key)?;
87 Some(base_id | style_bits)
88 }
89
90 fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
92 if key.len() == 1 {
93 let ch = key.chars().next().unwrap();
94 if ch.is_ascii() {
95 let id = ch as u16;
97 return Some(id);
98 }
99 }
100
101 match self.glyph_coords.get(key) {
102 Some(id) => Some(*id),
103 None => {
104 self.glyph_tracker.record_missing(key);
105 None
106 },
107 }
108 }
109
110 fn cell_size(&self) -> (i32, i32) {
111 let (w, h) = self.cell_size;
112 (
113 w - 2 * FontAtlasData::PADDING,
114 h - 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(&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(&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 base_lookup_mask(&self) -> u32 {
236 atlas::STATIC_ATLAS_LOOKUP_MASK
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) -> (i32, i32) {
254 self.cell_size()
256 }
257}