beamterm_renderer/gl/
atlas.rs

1use std::collections::HashMap;
2
3use beamterm_data::{FontAtlasData, FontStyle};
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    /// The size of each character cell in pixels
28    cell_size: (i32, i32),
29    /// The number of slices in the atlas texture
30    num_slices: u32,
31    /// Underline configuration
32    underline: beamterm_data::LineDecoration,
33    /// Strikethrough configuration  
34    strikethrough: beamterm_data::LineDecoration,
35}
36
37impl FontAtlas {
38    /// Loads the default embedded font atlas.
39    pub fn load_default(gl: &web_sys::WebGl2RenderingContext) -> Result<Self, Error> {
40        let config = FontAtlasData::default();
41        Self::load(gl, config)
42    }
43
44    /// Creates a TextureAtlas from a grid of equal-sized cells
45    pub fn load(
46        gl: &web_sys::WebGl2RenderingContext,
47        config: FontAtlasData,
48    ) -> Result<Self, Error> {
49        let texture = crate::gl::texture::Texture::from_font_atlas_data(gl, GL::RGBA, &config)?;
50        let num_slices = config.texture_dimensions.2;
51
52        let texture_layers = config.glyphs.iter().map(|g| g.id as i32).max().unwrap_or(0) + 1;
53        console::log_1(
54            &format!("Creating atlas grid with {}/{texture_layers} layers", config.glyphs.len())
55                .into(),
56        );
57
58        let (cell_width, cell_height) = config.cell_size;
59        let mut layers = HashMap::new();
60
61        // we only store the normal-styled glyphs (incl emoji) in the atlas lookup,
62        // as the correct layer id can be derived from the base glyph id plus font style
63        config.glyphs.iter()
64            .filter(|g| g.style == FontStyle::Normal) // only normal style glyphs
65            .filter(|g| !g.is_ascii())                // only non-ascii glyphs
66            .for_each(|g| {
67                layers.insert(g.symbol.to_compact_string(), g.id);
68            });
69
70        Ok(Self {
71            texture,
72            glyph_coords: layers,
73            cell_size: (cell_width, cell_height),
74            num_slices: num_slices as u32,
75            underline: config.underline,
76            strikethrough: config.strikethrough,
77        })
78    }
79
80    /// Binds the atlas texture to the specified texture unit
81    pub fn bind(&self, gl: &web_sys::WebGl2RenderingContext, texture_unit: u32) {
82        self.texture.bind(gl, texture_unit);
83    }
84
85    pub fn cell_size(&self) -> (i32, i32) {
86        let (w, h) = self.cell_size;
87        (w - 2 * FontAtlasData::PADDING, h - 2 * FontAtlasData::PADDING)
88    }
89
90    /// Returns the underline configuration
91    pub fn underline(&self) -> beamterm_data::LineDecoration {
92        self.underline
93    }
94
95    /// Returns the strikethrough configuration
96    pub fn strikethrough(&self) -> beamterm_data::LineDecoration {
97        self.strikethrough
98    }
99
100    /// Returns the base glyph identifier for the given key
101    pub(super) fn get_base_glyph_id(&self, key: &str) -> Option<u16> {
102        if key.len() == 1 {
103            let ch = key.chars().next().unwrap();
104            if ch.is_ascii() {
105                // 0x00..0x7f double as layer
106                let id = ch as u16;
107                return Some(id);
108            }
109        }
110
111        self.glyph_coords.get(key).copied()
112    }
113}