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() / 1024))
47            .finish()
48    }
49}
50
51impl FontAtlasData {
52    /// Padding in pixels around each glyph cell to prevent texture bleeding.
53    pub const PADDING: i32 = 1;
54    /// Number of glyph cells per texture layer.
55    pub const CELLS_PER_SLICE: i32 = 32;
56
57    /// Creates a new font atlas with the given parameters.
58    #[allow(clippy::too_many_arguments)]
59    #[must_use]
60    pub fn new(
61        font_name: CompactString,
62        font_size: f32,
63        max_halfwidth_base_glyph_id: u16,
64        texture_dimensions: (i32, i32, i32),
65        cell_size: CellSize,
66        underline: LineDecoration,
67        strikethrough: LineDecoration,
68        glyphs: Vec<Glyph>,
69        texture_data: Vec<u8>,
70    ) -> Self {
71        Self {
72            font_name,
73            font_size,
74            max_halfwidth_base_glyph_id,
75            texture_dimensions,
76            cell_size,
77            underline,
78            strikethrough,
79            glyphs,
80            texture_data,
81        }
82    }
83
84    /// Returns the font name.
85    #[inline]
86    #[must_use]
87    pub fn font_name(&self) -> &str {
88        &self.font_name
89    }
90
91    /// Returns the font size in points.
92    #[inline]
93    #[must_use]
94    pub fn font_size(&self) -> f32 {
95        self.font_size
96    }
97
98    /// Returns the maximum halfwidth base glyph ID.
99    ///
100    /// Fullwidth glyphs are assigned IDs starting from this value.
101    #[inline]
102    #[must_use]
103    pub fn max_halfwidth_base_glyph_id(&self) -> u16 {
104        self.max_halfwidth_base_glyph_id
105    }
106
107    /// Returns the texture dimensions as (width, height, layers).
108    #[inline]
109    #[must_use]
110    pub fn texture_dimensions(&self) -> (i32, i32, i32) {
111        self.texture_dimensions
112    }
113
114    /// Returns the underline decoration configuration.
115    #[inline]
116    #[must_use]
117    pub fn underline(&self) -> LineDecoration {
118        self.underline
119    }
120
121    /// Returns the strikethrough decoration configuration.
122    #[inline]
123    #[must_use]
124    pub fn strikethrough(&self) -> LineDecoration {
125        self.strikethrough
126    }
127
128    /// Returns a slice of all glyphs in the atlas.
129    #[inline]
130    #[must_use]
131    pub fn glyphs(&self) -> &[Glyph] {
132        &self.glyphs
133    }
134
135    /// Returns the raw texture data.
136    #[inline]
137    #[must_use]
138    pub fn texture_data(&self) -> &[u8] {
139        &self.texture_data
140    }
141
142    /// Consumes the atlas and returns its glyphs.
143    #[must_use]
144    pub fn into_glyphs(self) -> Vec<Glyph> {
145        self.glyphs
146    }
147
148    /// Deserializes a font atlas from binary format.
149    ///
150    /// # Arguments
151    /// * `serialized` - Binary data containing the serialized font atlas
152    ///
153    /// # Errors
154    /// Returns [`SerializationError`] if the binary data is malformed or cannot be deserialized.
155    ///
156    /// # Returns
157    /// The deserialized font atlas or an error if deserialization fails
158    pub fn from_binary(serialized: &[u8]) -> Result<Self, SerializationError> {
159        let mut deserializer = Deserializer::new(serialized);
160        FontAtlasData::deserialize(&mut deserializer)
161    }
162
163    /// Serializes the font atlas to binary format.
164    ///
165    /// # Errors
166    /// Returns [`SerializationError`] if serialization fails (e.g., a string field exceeds 255 bytes).
167    ///
168    /// # Returns
169    /// A byte vector containing the serialized font atlas data, or an error
170    /// if serialization fails (e.g., a string field exceeds 255 bytes)
171    pub fn to_binary(&self) -> Result<Vec<u8>, SerializationError> {
172        self.serialize()
173    }
174
175    /// Calculates how many terminal columns and rows fit in the given viewport dimensions.
176    ///
177    /// # Arguments
178    /// * `viewport_width` - Width of the viewport in pixels
179    /// * `viewport_height` - Height of the viewport in pixels
180    ///
181    /// # Returns
182    /// A tuple of (columns, rows) that fit in the viewport
183    #[must_use]
184    pub fn terminal_size(&self, viewport_width: i32, viewport_height: i32) -> (i32, i32) {
185        (
186            viewport_width / self.cell_size.width,
187            viewport_height / self.cell_size.height,
188        )
189    }
190
191    /// Returns the padded terminal cell size.
192    ///
193    /// The cell size includes padding (1 pixel on each side, 2 pixels total per dimension)
194    /// to prevent texture bleeding artifacts during GPU rendering.
195    ///
196    /// # Returns
197    /// The cell dimensions in pixels
198    #[must_use]
199    pub fn cell_size(&self) -> CellSize {
200        self.cell_size
201    }
202}
203
204impl Default for FontAtlasData {
205    fn default() -> Self {
206        Self::from_binary(include_bytes!("../atlas/bitmap_font.atlas")).unwrap()
207    }
208}
209
210/// Configuration for underline or strikethrough line decorations.
211#[derive(Copy, Clone, Debug, PartialEq)]
212pub struct LineDecoration {
213    /// 0.0 to 1.0, where 0.0 is the top of the text line and 1.0 is the bottom.
214    pub(crate) position: f32,
215    /// Thickness of the line as a fraction of the cell height (0.0 to 1.0)
216    pub(crate) thickness: f32,
217}
218
219impl LineDecoration {
220    /// Creates a new line decoration with the given position and thickness.
221    #[must_use]
222    pub fn new(position: f32, thickness: f32) -> Self {
223        Self {
224            position: position.clamp(0.0, 1.0),
225            thickness: thickness.clamp(0.0, 1.0),
226        }
227    }
228
229    /// Returns the vertical position as a fraction of cell height (0.0 to 1.0).
230    #[inline]
231    #[must_use]
232    pub fn position(&self) -> f32 {
233        self.position
234    }
235
236    /// Returns the thickness as a fraction of cell height (0.0 to 1.0).
237    #[inline]
238    #[must_use]
239    pub fn thickness(&self) -> f32 {
240        self.thickness
241    }
242}
243
244/// Debug pattern for validating pixel-perfect rendering of cell dimensions.
245///
246/// When enabled, replaces the space glyph with a checkered pattern to help
247/// verify that cell boundaries align correctly with pixel boundaries.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum DebugSpacePattern {
250    /// 1px alternating checkerboard pattern
251    OnePixel,
252    /// 2x2 pixel checkerboard pattern
253    TwoByTwo,
254}