Skip to main content

beamterm_data/
atlas.rs

1use std::fmt::Debug;
2
3use compact_str::CompactString;
4
5use crate::{CellSize, Deserializer, Glyph, Serializable, SerializationError};
6
7/// Font atlas data for GPU-accelerated terminal rendering.
8///
9/// Contains a pre-rasterized font atlas stored as a 2D texture array, where each layer
10/// holds 32 glyphs in a 1×32 grid. The atlas includes multiple font styles (normal, bold,
11/// italic, bold+italic) and full Unicode support including emoji.
12#[derive(Clone, PartialEq)]
13pub struct FontAtlasData {
14    /// The name of the font
15    pub(crate) font_name: CompactString,
16    /// The font size in points
17    pub(crate) font_size: f32,
18    /// The number of single-cell (halfwidth) glyphs per layer, before fullwidth glyphs begin.
19    ///
20    /// Fullwidth glyphs (e.g., CJK characters) are assigned IDs starting from this value,
21    /// aligned to even boundaries. This allows the renderer to distinguish halfwidth from
22    /// fullwidth glyphs by comparing against this threshold.
23    pub(crate) max_halfwidth_base_glyph_id: u16,
24    /// Width, height and depth of the texture in pixels
25    pub(crate) texture_dimensions: (i32, i32, i32),
26    /// Width and height of each character cell
27    pub(crate) cell_size: CellSize,
28    /// Underline configuration
29    pub(crate) underline: LineDecoration,
30    /// Strikethrough configuration
31    pub(crate) strikethrough: LineDecoration,
32    /// The glyphs in the font
33    pub(crate) glyphs: Vec<Glyph>,
34    /// The 3d texture data containing the font glyphs
35    pub(crate) texture_data: Vec<u8>,
36}
37
38impl Debug for FontAtlasData {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("FontAtlasData")
41            .field("font_name", &self.font_name)
42            .field("font_size", &self.font_size)
43            .field("texture_dimensions", &self.texture_dimensions)
44            .field("cell_size", &self.cell_size)
45            .field("glyphs_count", &self.glyphs.len())
46            .field("texture_data_kb", &(self.texture_data.len() * 4 / 1024))
47            .finish()
48    }
49}
50
51impl FontAtlasData {
52    pub const PADDING: i32 = 1;
53    pub const CELLS_PER_SLICE: i32 = 32;
54
55    /// Creates a new font atlas with the given parameters.
56    #[allow(clippy::too_many_arguments)]
57    pub fn new(
58        font_name: CompactString,
59        font_size: f32,
60        max_halfwidth_base_glyph_id: u16,
61        texture_dimensions: (i32, i32, i32),
62        cell_size: CellSize,
63        underline: LineDecoration,
64        strikethrough: LineDecoration,
65        glyphs: Vec<Glyph>,
66        texture_data: Vec<u8>,
67    ) -> Self {
68        Self {
69            font_name,
70            font_size,
71            max_halfwidth_base_glyph_id,
72            texture_dimensions,
73            cell_size,
74            underline,
75            strikethrough,
76            glyphs,
77            texture_data,
78        }
79    }
80
81    /// Returns the font name.
82    #[inline]
83    pub fn font_name(&self) -> &str {
84        &self.font_name
85    }
86
87    /// Returns the font size in points.
88    #[inline]
89    pub fn font_size(&self) -> f32 {
90        self.font_size
91    }
92
93    /// Returns the maximum halfwidth base glyph ID.
94    ///
95    /// Fullwidth glyphs are assigned IDs starting from this value.
96    #[inline]
97    pub fn max_halfwidth_base_glyph_id(&self) -> u16 {
98        self.max_halfwidth_base_glyph_id
99    }
100
101    /// Returns the texture dimensions as (width, height, layers).
102    #[inline]
103    pub fn texture_dimensions(&self) -> (i32, i32, i32) {
104        self.texture_dimensions
105    }
106
107    /// Returns the underline decoration configuration.
108    #[inline]
109    pub fn underline(&self) -> LineDecoration {
110        self.underline
111    }
112
113    /// Returns the strikethrough decoration configuration.
114    #[inline]
115    pub fn strikethrough(&self) -> LineDecoration {
116        self.strikethrough
117    }
118
119    /// Returns a slice of all glyphs in the atlas.
120    #[inline]
121    pub fn glyphs(&self) -> &[Glyph] {
122        &self.glyphs
123    }
124
125    /// Returns the raw texture data.
126    #[inline]
127    pub fn texture_data(&self) -> &[u8] {
128        &self.texture_data
129    }
130
131    /// Consumes the atlas and returns its glyphs.
132    pub fn into_glyphs(self) -> Vec<Glyph> {
133        self.glyphs
134    }
135
136    /// Deserializes a font atlas from binary format.
137    ///
138    /// # Arguments
139    /// * `serialized` - Binary data containing the serialized font atlas
140    ///
141    /// # Returns
142    /// The deserialized font atlas or an error if deserialization fails
143    pub fn from_binary(serialized: &[u8]) -> Result<Self, SerializationError> {
144        let mut deserializer = Deserializer::new(serialized);
145        FontAtlasData::deserialize(&mut deserializer)
146    }
147
148    /// Serializes the font atlas to binary format.
149    ///
150    /// # Returns
151    /// A byte vector containing the serialized font atlas data, or an error
152    /// if serialization fails (e.g., a string field exceeds 255 bytes)
153    pub fn to_binary(&self) -> Result<Vec<u8>, SerializationError> {
154        self.serialize()
155    }
156
157    /// Calculates how many terminal columns and rows fit in the given viewport dimensions.
158    ///
159    /// # Arguments
160    /// * `viewport_width` - Width of the viewport in pixels
161    /// * `viewport_height` - Height of the viewport in pixels
162    ///
163    /// # Returns
164    /// A tuple of (columns, rows) that fit in the viewport
165    pub fn terminal_size(&self, viewport_width: i32, viewport_height: i32) -> (i32, i32) {
166        (
167            viewport_width / self.cell_size.width,
168            viewport_height / self.cell_size.height,
169        )
170    }
171
172    /// Returns the padded terminal cell size.
173    ///
174    /// The cell size includes padding (1 pixel on each side, 2 pixels total per dimension)
175    /// to prevent texture bleeding artifacts during GPU rendering.
176    ///
177    /// # Returns
178    /// The cell dimensions in pixels
179    pub fn cell_size(&self) -> CellSize {
180        self.cell_size
181    }
182}
183
184impl Default for FontAtlasData {
185    fn default() -> Self {
186        Self::from_binary(include_bytes!("../atlas/bitmap_font.atlas")).unwrap()
187    }
188}
189
190#[derive(Copy, Clone, Debug, PartialEq)]
191pub struct LineDecoration {
192    /// 0.0 to 1.0, where 0.0 is the top of the text line and 1.0 is the bottom.
193    pub(crate) position: f32,
194    /// Thickness of the line as a fraction of the cell height (0.0 to 1.0)
195    pub(crate) thickness: f32,
196}
197
198impl LineDecoration {
199    pub fn new(position: f32, thickness: f32) -> Self {
200        Self {
201            position: position.clamp(0.0, 1.0),
202            thickness: thickness.clamp(0.0, 1.0),
203        }
204    }
205
206    /// Returns the vertical position as a fraction of cell height (0.0 to 1.0).
207    #[inline]
208    pub fn position(&self) -> f32 {
209        self.position
210    }
211
212    /// Returns the thickness as a fraction of cell height (0.0 to 1.0).
213    #[inline]
214    pub fn thickness(&self) -> f32 {
215        self.thickness
216    }
217}
218
219/// Debug pattern for validating pixel-perfect rendering of cell dimensions.
220///
221/// When enabled, replaces the space glyph with a checkered pattern to help
222/// verify that cell boundaries align correctly with pixel boundaries.
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum DebugSpacePattern {
225    /// 1px alternating checkerboard pattern
226    OnePixel,
227    /// 2x2 pixel checkerboard pattern
228    TwoByTwo,
229}