beamterm_renderer/gl/
atlas.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use beamterm_data::{FontAtlasData, FontStyle, Glyph};
4use compact_str::{CompactString, ToCompactString};
5use web_sys::console;
6
7use crate::{error::Error, gl::GL};
8
9/// A texture atlas containing font glyphs for efficient WebGL text rendering.
10///
11/// `FontAtlas` manages a WebGL 2D texture array where each layer contains a single
12/// character glyph. This design enables efficient instanced rendering of text by
13/// allowing the GPU to select the appropriate character layer for each rendered cell.
14///
15/// # Architecture
16/// The atlas uses a **WebGL 2D texture array** where:
17/// - Each layer contains one character glyph
18/// - ASCII characters use their ASCII value as the layer index
19/// - Non-ASCII characters are stored in a hash map for layer lookup
20/// - All glyphs have uniform cell dimensions for consistent spacing
21#[derive(Debug)]
22pub struct FontAtlas {
23    /// The underlying texture
24    texture: crate::gl::texture::Texture,
25    /// Symbol to 3d texture index
26    glyph_coords: HashMap<CompactString, u16>,
27    /// Base glyph identifier to symbol mapping
28    symbol_lookup: HashMap<u16, CompactString>,
29    /// The size of each character cell in pixels
30    cell_size: (i32, i32),
31    /// The number of slices in the atlas texture
32    num_slices: u32,
33    /// Underline configuration
34    underline: beamterm_data::LineDecoration,
35    /// Strikethrough configuration  
36    strikethrough: beamterm_data::LineDecoration,
37}
38
39impl FontAtlas {
40    /// Loads the default embedded font atlas.
41    pub fn load_default(gl: &web_sys::WebGl2RenderingContext) -> Result<Self, Error> {
42        let config = FontAtlasData::default();
43        Self::load(gl, config)
44    }
45
46    /// Creates a TextureAtlas from a grid of equal-sized cells
47    pub fn load(
48        gl: &web_sys::WebGl2RenderingContext,
49        config: FontAtlasData,
50    ) -> Result<Self, Error> {
51        let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, GL::RGBA, &config)?;
52        let num_slices = config.texture_dimensions.2;
53
54        let texture_layers = config.glyphs.iter().map(|g| g.id as i32).max().unwrap_or(0) + 1;
55        console::log_1(
56            &format!("Creating atlas grid with {}/{texture_layers} layers", config.glyphs.len())
57                .into(),
58        );
59
60        let (cell_width, cell_height) = config.cell_size;
61        let mut layers = HashMap::new();
62        let mut symbol_lookup = HashMap::new();
63
64        // we only store the normal-styled glyphs (incl emoji) in the atlas lookup,
65        // as the correct layer id can be derived from the base glyph id plus font style
66        config.glyphs.iter()
67            .filter(|g| g.style == FontStyle::Normal) // only normal style glyphs
68            .filter(|g| !g.is_ascii())                // only non-ascii glyphs
69            .for_each(|g| {
70                symbol_lookup.insert(g.id, g.symbol.clone());
71                layers.insert(g.symbol.clone(), g.id);
72            });
73
74        Ok(Self {
75            texture,
76            glyph_coords: layers,
77            symbol_lookup,
78            cell_size: (cell_width, cell_height),
79            num_slices: num_slices as u32,
80            underline: config.underline,
81            strikethrough: config.strikethrough,
82        })
83    }
84
85    /// Binds the atlas texture to the specified texture unit
86    pub fn bind(&self, gl: &web_sys::WebGl2RenderingContext, texture_unit: u32) {
87        self.texture.bind(gl, texture_unit);
88    }
89
90    pub fn cell_size(&self) -> (i32, i32) {
91        let (w, h) = self.cell_size;
92        (w - 2 * FontAtlasData::PADDING, h - 2 * FontAtlasData::PADDING)
93    }
94
95    /// Returns the underline configuration
96    pub fn underline(&self) -> beamterm_data::LineDecoration {
97        self.underline
98    }
99
100    /// Returns the strikethrough configuration
101    pub fn strikethrough(&self) -> beamterm_data::LineDecoration {
102        self.strikethrough
103    }
104
105    /// Returns the symbol for the given glyph ID, if it exists
106    pub fn get_symbol(&self, glyph_id: u16) -> Option<Cow<str>> {
107        let base_glyph_id = glyph_id & (Glyph::GLYPH_ID_MASK | Glyph::EMOJI_FLAG);
108
109        if (0x20..0x80).contains(&base_glyph_id) {
110            // ASCII characters are directly mapped to their code point
111            let ch = base_glyph_id as u8 as char;
112            Some(Cow::from(ch.to_compact_string()))
113        } else {
114            self.symbol_lookup.get(&base_glyph_id).map(|s| Cow::from(s.as_str()))
115        }
116    }
117
118    /// Returns the base glyph identifier for the given key
119    pub fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
120        if key.len() == 1 {
121            let ch = key.chars().next().unwrap();
122            if ch.is_ascii() {
123                // 0x00..0x7f double as layer
124                let id = ch as u16;
125                return Some(id);
126            }
127        }
128
129        self.glyph_coords.get(key).copied()
130    }
131}