Skip to main content

beamterm_core/gl/
atlas.rs

1use std::{collections::HashSet, fmt::Debug};
2
3use compact_str::CompactString;
4
5use crate::Error;
6
7/// Prevents external implementations of the [`Atlas`] trait.
8///
9/// This module is not part of the public API.
10#[doc(hidden)]
11pub mod sealed {
12    /// Sealed marker trait. Cannot be implemented outside of beamterm crates.
13    pub trait Sealed {}
14}
15
16pub type SlotId = u16;
17/// Bitmask for extracting the base glyph slot from a styled glyph ID.
18///
19/// Both static and dynamic atlases use 13 bits (0x1FFF) for texture addressing.
20/// The emoji flag lives above this mask: bit 12 for static atlas (naturally part
21/// of slot address for emoji at slots >= 4096), bit 15 for dynamic atlas.
22pub(crate) const GLYPH_SLOT_MASK: u32 = 0x1FFF;
23
24/// Trait defining the interface for font atlases.
25///
26/// This trait is **sealed** and cannot be implemented outside of beamterm crates.
27///
28/// Methods that may mutate internal state (glyph resolution, cache updates,
29/// texture uploads) take `&mut self`. Read-only accessors take `&self`.
30pub trait Atlas: sealed::Sealed {
31    /// Returns the glyph identifier for the given key and style bits.
32    ///
33    /// May mutate internal state (e.g., LRU promotion in dynamic atlases,
34    /// recording missing glyphs in static atlases).
35    fn get_glyph_id(&mut self, key: &str, style_bits: u16) -> Option<u16>;
36
37    /// Returns the base glyph identifier for the given key.
38    ///
39    /// May mutate internal state (e.g., LRU promotion, missing glyph tracking).
40    fn get_base_glyph_id(&mut self, key: &str) -> Option<u16>;
41
42    /// Returns the height of the atlas in pixels.
43    fn cell_size(&self) -> beamterm_data::CellSize;
44
45    /// Binds the font atlas texture to the currently active texture unit.
46    fn bind(&self, gl: &glow::Context);
47
48    /// Returns the underline configuration
49    fn underline(&self) -> beamterm_data::LineDecoration;
50
51    /// Returns the strikethrough configuration
52    fn strikethrough(&self) -> beamterm_data::LineDecoration;
53
54    /// Returns the symbol for the given glyph ID, if it exists
55    fn get_symbol(&self, glyph_id: u16) -> Option<CompactString>;
56
57    /// Returns the ASCII character for the given glyph ID, if it represents an ASCII char.
58    ///
59    /// This is an optimized path for URL detection that avoids string allocation.
60    fn get_ascii_char(&self, glyph_id: u16) -> Option<char>;
61
62    /// Returns a reference to the glyph tracker for accessing missing glyphs.
63    fn glyph_tracker(&self) -> &GlyphTracker;
64
65    /// Returns the number of glyphs currently in the atlas.
66    fn glyph_count(&self) -> u32;
67
68    /// Flushes any pending glyph data to the GPU texture.
69    ///
70    /// For dynamic atlases, this rasterizes and uploads queued glyphs that were
71    /// allocated during [`resolve_glyph_slot`] calls. Must be called after the
72    /// atlas texture is bound and before rendering.
73    ///
74    /// For static atlases, this is a no-op since all glyphs are pre-loaded.
75    ///
76    /// # Errors
77    /// Returns an error if texture upload fails.
78    fn flush(&mut self, gl: &glow::Context) -> Result<(), Error>;
79
80    /// Recreates the GPU texture after a context loss.
81    ///
82    /// This clears the cache - glyphs will be re-rasterized on next access.
83    ///
84    /// # Errors
85    /// Returns an error if GPU texture creation fails.
86    fn recreate_texture(&mut self, gl: &glow::Context) -> Result<(), Error>;
87
88    /// Iterates over all glyph ID to symbol mappings.
89    ///
90    /// Calls the provided closure for each (glyph_id, symbol) pair in the atlas.
91    /// This is used for debugging and exposing the atlas contents to JavaScript.
92    fn for_each_symbol(&self, f: &mut dyn FnMut(u16, &str));
93
94    /// Resolves a glyph to its texture slot.
95    ///
96    /// For static atlases, performs a lookup and returns `None` if not found.
97    ///
98    /// For dynamic atlases, allocates a slot if missing and queues for upload.
99    /// The slot is immediately valid, but [`flush`] must be called before
100    /// rendering to populate the texture.
101    fn resolve_glyph_slot(&mut self, key: &str, style_bits: u16) -> Option<GlyphSlot>;
102
103    /// Returns the bit position used for emoji detection in the fragment shader.
104    ///
105    /// The glyph ID encodes the base slot index (bits 0-12, masked by `0x1FFF`)
106    /// plus effect/flag bits above that. The emoji bit tells the shader to use
107    /// texture color (emoji) vs foreground color (regular text).
108    ///
109    /// - **`StaticFontAtlas`** returns `12`: emoji are at slots >= 4096, so bit 12
110    ///   is naturally set in their slot address.
111    /// - **`DynamicFontAtlas`** returns `15`: emoji flag is stored in bit 15,
112    ///   outside the 13-bit slot mask, leaving bits 13-14 for underline/strikethrough.
113    fn emoji_bit(&self) -> u32;
114
115    /// Deletes the GPU texture resources associated with this atlas.
116    ///
117    /// This method must be called before dropping the atlas to properly clean up
118    /// GPU resources. Failing to call this will leak GPU memory.
119    fn delete(&self, gl: &glow::Context);
120
121    /// Updates the pixel ratio for HiDPI rendering.
122    ///
123    /// Returns the effective pixel ratio that should be used for viewport scaling.
124    /// Each atlas implementation decides how to handle the ratio:
125    ///
126    /// - **Static atlas**: Returns exact ratio, no internal work needed
127    /// - **Dynamic atlas**: Returns exact ratio, reinitializes with scaled font size
128    ///
129    /// # Errors
130    /// Returns an error if GPU texture recreation fails during reinitialization.
131    fn update_pixel_ratio(&mut self, gl: &glow::Context, pixel_ratio: f32) -> Result<f32, Error>;
132
133    /// Returns the cell scale factor for layout calculations at the given DPR.
134    ///
135    /// This determines how cells from `cell_size()` should be scaled for layout:
136    ///
137    /// - **Static atlas**: Returns snapped scale values (0.5, 1.0, 2.0, 3.0, etc.)
138    ///   to avoid arbitrary fractional scaling of pre-rasterized glyphs.
139    ///   DPR <= 0.5 snaps to 0.5, otherwise rounds to nearest integer (minimum 1.0).
140    /// - **Dynamic atlas**: Returns `1.0` - glyphs are re-rasterized at the exact DPR,
141    ///   so `cell_size()` already returns the correctly-scaled physical size
142    ///
143    /// # Contract
144    ///
145    /// - Return value is always >= 0.5
146    /// - The effective cell size for layout is `cell_size() * cell_scale_for_dpr(dpr)`
147    /// - Static atlases use snapped scaling to preserve glyph sharpness
148    /// - Dynamic atlases handle DPR internally via re-rasterization
149    fn cell_scale_for_dpr(&self, pixel_ratio: f32) -> f32;
150
151    /// Returns the texture cell size in physical pixels (for fragment shader calculations).
152    ///
153    /// This is used for computing padding fractions in the shader, which need to be
154    /// based on the actual texture dimensions rather than logical layout dimensions.
155    ///
156    /// - **Static atlas**: Same as `cell_size()` (texture is at fixed resolution)
157    /// - **Dynamic atlas**: Physical cell size (before dividing by pixel_ratio)
158    fn texture_cell_size(&self) -> beamterm_data::CellSize;
159}
160
161/// Type-erased wrapper around any [`Atlas`] implementation.
162pub struct FontAtlas {
163    inner: Box<dyn Atlas>,
164}
165
166impl<A: Atlas + 'static> From<A> for FontAtlas {
167    fn from(atlas: A) -> Self {
168        FontAtlas::new(atlas)
169    }
170}
171
172impl Debug for FontAtlas {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.debug_struct("FontAtlas")
175            .finish_non_exhaustive()
176    }
177}
178
179impl FontAtlas {
180    /// Wraps an atlas implementation in a type-erased container.
181    pub fn new(inner: impl Atlas + 'static) -> Self {
182        Self { inner: Box::new(inner) }
183    }
184
185    /// Returns the styled glyph ID for the given symbol.
186    pub fn get_glyph_id(&mut self, key: &str, style_bits: u16) -> Option<u16> {
187        self.inner.get_glyph_id(key, style_bits)
188    }
189
190    /// Returns the unstyled base glyph ID for the given symbol.
191    pub fn get_base_glyph_id(&mut self, key: &str) -> Option<u16> {
192        self.inner.get_base_glyph_id(key)
193    }
194
195    /// Returns the cell dimensions used for grid layout.
196    #[must_use]
197    pub fn cell_size(&self) -> beamterm_data::CellSize {
198        self.inner.cell_size()
199    }
200
201    /// Binds the atlas texture for rendering.
202    pub fn bind(&self, gl: &glow::Context) {
203        self.inner.bind(gl);
204    }
205
206    /// Returns underline position and thickness metadata.
207    #[must_use]
208    pub fn underline(&self) -> beamterm_data::LineDecoration {
209        self.inner.underline()
210    }
211
212    /// Returns strikethrough position and thickness metadata.
213    #[must_use]
214    pub fn strikethrough(&self) -> beamterm_data::LineDecoration {
215        self.inner.strikethrough()
216    }
217
218    /// Returns the symbol string for the given glyph ID.
219    #[must_use]
220    pub fn get_symbol(&self, glyph_id: u16) -> Option<CompactString> {
221        self.inner.get_symbol(glyph_id)
222    }
223
224    /// Returns the ASCII character for the given glyph ID, if applicable.
225    #[must_use]
226    pub fn get_ascii_char(&self, glyph_id: u16) -> Option<char> {
227        self.inner.get_ascii_char(glyph_id)
228    }
229
230    /// Returns a reference to the glyph usage tracker.
231    #[must_use]
232    pub fn glyph_tracker(&self) -> &GlyphTracker {
233        self.inner.glyph_tracker()
234    }
235
236    /// Returns the total number of allocated glyphs.
237    #[must_use]
238    pub fn glyph_count(&self) -> u32 {
239        self.inner.glyph_count()
240    }
241
242    /// Recreates the GPU texture after a context loss.
243    ///
244    /// # Errors
245    /// Returns an error if GPU texture creation fails.
246    pub fn recreate_texture(&mut self, gl: &glow::Context) -> Result<(), Error> {
247        self.inner.recreate_texture(gl)
248    }
249
250    /// Iterates over all glyph ID to symbol mappings.
251    pub fn for_each_symbol(&self, f: &mut dyn FnMut(u16, &str)) {
252        self.inner.for_each_symbol(f);
253    }
254
255    /// Resolves a symbol to its glyph slot classification.
256    pub fn resolve_glyph_slot(&mut self, key: &str, style_bits: u16) -> Option<GlyphSlot> {
257        self.inner.resolve_glyph_slot(key, style_bits)
258    }
259
260    /// Flushes pending glyph data to the GPU texture.
261    ///
262    /// # Errors
263    /// Returns an error if texture upload fails.
264    pub fn flush(&mut self, gl: &glow::Context) -> Result<(), Error> {
265        self.inner.flush(gl)
266    }
267
268    pub(crate) fn emoji_bit(&self) -> u32 {
269        self.inner.emoji_bit()
270    }
271
272    pub(crate) fn space_glyph_id(&mut self) -> u16 {
273        self.get_glyph_id(" ", 0x0)
274            .expect("space glyph exists in every font atlas")
275    }
276
277    /// Deletes the GPU texture resources associated with this atlas.
278    pub fn delete(&self, gl: &glow::Context) {
279        self.inner.delete(gl);
280    }
281
282    /// Updates the pixel ratio for HiDPI rendering.
283    ///
284    /// Returns the effective pixel ratio to use for viewport scaling.
285    ///
286    /// # Errors
287    /// Returns an error if GPU texture recreation fails during reinitialization.
288    pub fn update_pixel_ratio(
289        &mut self,
290        gl: &glow::Context,
291        pixel_ratio: f32,
292    ) -> Result<f32, Error> {
293        self.inner.update_pixel_ratio(gl, pixel_ratio)
294    }
295
296    /// Returns the cell scale factor for layout calculations.
297    #[must_use]
298    pub fn cell_scale_for_dpr(&self, pixel_ratio: f32) -> f32 {
299        self.inner.cell_scale_for_dpr(pixel_ratio)
300    }
301
302    /// Returns the texture cell size in physical pixels.
303    #[must_use]
304    pub fn texture_cell_size(&self) -> beamterm_data::CellSize {
305        self.inner.texture_cell_size()
306    }
307}
308
309#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
310#[non_exhaustive]
311/// Classifies a glyph's texture slot by width category.
312pub enum GlyphSlot {
313    /// Single-width glyph slot.
314    Normal(SlotId),
315    /// Double-width glyph slot (CJK characters).
316    Wide(SlotId),
317    /// Emoji glyph slot (occupies two consecutive texture slots).
318    Emoji(SlotId),
319}
320
321impl GlyphSlot {
322    /// Returns the underlying slot ID.
323    #[must_use]
324    pub fn slot_id(&self) -> SlotId {
325        match *self {
326            GlyphSlot::Normal(id) | GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => id,
327        }
328    }
329
330    /// Returns a new slot with the given style bits applied.
331    #[must_use]
332    pub fn with_styling(self, style_bits: u16) -> Self {
333        use GlyphSlot::*;
334        match self {
335            Normal(id) => Normal(id | style_bits),
336            Wide(id) => Wide(id | style_bits),
337            Emoji(id) => Emoji(id | style_bits),
338        }
339    }
340
341    /// Returns true if this is a double-width glyph (emoji or wide CJK).
342    #[must_use]
343    pub fn is_double_width(&self) -> bool {
344        matches!(self, GlyphSlot::Wide(_) | GlyphSlot::Emoji(_))
345    }
346}
347
348/// Tracks glyphs that were requested but not found in the font atlas.
349#[derive(Debug, Default)]
350pub struct GlyphTracker {
351    missing: HashSet<CompactString>,
352}
353
354impl GlyphTracker {
355    /// Creates a new empty glyph tracker.
356    #[must_use]
357    pub fn new() -> Self {
358        Self { missing: HashSet::new() }
359    }
360
361    /// Records a glyph as missing.
362    pub fn record_missing(&mut self, glyph: &str) {
363        self.missing.insert(glyph.into());
364    }
365
366    /// Returns a copy of all missing glyphs.
367    #[must_use]
368    pub fn missing_glyphs(&self) -> HashSet<CompactString> {
369        self.missing.clone()
370    }
371
372    /// Clears all tracked missing glyphs.
373    pub fn clear(&mut self) {
374        self.missing.clear();
375    }
376
377    /// Returns the number of unique missing glyphs.
378    #[must_use]
379    pub fn len(&self) -> usize {
380        self.missing.len()
381    }
382
383    /// Returns true if no glyphs are missing.
384    #[must_use]
385    pub fn is_empty(&self) -> bool {
386        self.missing.is_empty()
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_glyph_tracker() {
396        let mut tracker = GlyphTracker::new();
397
398        // Initially empty
399        assert!(tracker.is_empty());
400        assert_eq!(tracker.len(), 0);
401
402        // Record some missing glyphs
403        tracker.record_missing("\u{1F3AE}");
404        tracker.record_missing("\u{1F3AF}");
405        tracker.record_missing("\u{1F3AE}"); // Duplicate
406
407        assert!(!tracker.is_empty());
408        assert_eq!(tracker.len(), 2); // Only unique glyphs
409
410        // Check the missing glyphs
411        let missing = tracker.missing_glyphs();
412        assert!(missing.contains(&CompactString::new("\u{1F3AE}")));
413        assert!(missing.contains(&CompactString::new("\u{1F3AF}")));
414
415        // Clear and verify
416        tracker.clear();
417        assert!(tracker.is_empty());
418        assert_eq!(tracker.len(), 0);
419    }
420}