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}