beamterm_data/glyph.rs
1use compact_str::{CompactString, ToCompactString};
2
3use crate::serialization::SerializationError;
4
5/// Represents a single character glyph in a font atlas texture.
6///
7/// A `Glyph` contains the metadata needed to locate and identify a character
8/// within a font atlas texture. Each glyph has a unique ID that maps
9/// to its coordinates in a GL `TEXTURE_2D_ARRAY`.
10///
11/// # ASCII Optimization
12/// For ASCII characters, the glyph ID directly corresponds to the character's
13/// ASCII value, enabling fast lookups without hash table lookups. Non-ASCII
14/// characters are assigned sequential IDs starting from a base value.
15///
16/// # Glyph ID Bit Layout (16-bit)
17///
18/// | Bit(s) | Flag Name | Hex Mask | Binary Mask | Description |
19/// |--------|---------------|----------|-----------------------|---------------------------|
20/// | 0-9 | GLYPH_ID | `0x03FF` | `0000_0011_1111_1111` | Base glyph identifier |
21/// | 10 | BOLD | `0x0400` | `0000_0100_0000_0000` | Bold font style |
22/// | 11 | ITALIC | `0x0800` | `0000_1000_0000_0000` | Italic font style |
23/// | 12 | EMOJI | `0x1000` | `0001_0000_0000_0000` | Emoji character flag |
24/// | 13 | UNDERLINE | `0x2000` | `0010_0000_0000_0000` | Underline effect |
25/// | 14 | STRIKETHROUGH | `0x4000` | `0100_0000_0000_0000` | Strikethrough effect |
26/// | 15 | RESERVED | `0x8000` | `1000_0000_0000_0000` | Reserved for future use |
27///
28/// - The first 10 bits (0-9) represent the base glyph ID, allowing for 1024 unique glyphs.
29/// - Emoji glyphs implicitly clear any other font style bits.
30/// - The fragment shader uses the glyph ID to decode the texture coordinates and effects.
31///
32/// ## Glyph ID Encoding Examples
33///
34/// | Character | Style | Binary Representation | Hex Value | Description |
35/// |-------------|------------------|-----------------------|-----------|---------------------|
36/// | 'A' (0x41) | Normal | `0000_0000_0100_0001` | `0x0041` | Plain 'A' |
37/// | 'A' (0x41) | Bold | `0000_0100_0100_0001` | `0x0441` | Bold 'A' |
38/// | 'A' (0x41) | Bold + Italic | `0000_1100_0100_0001` | `0x0C41` | Bold italic 'A' |
39/// | 'A' (0x41) | Bold + Underline | `0010_0100_0100_0001` | `0x2441` | Bold underlined 'A' |
40/// | '🚀' (0x81) | Emoji | `0001_0000_1000_0001` | `0x1081` | "rocket" emoji |
41#[derive(Debug, Eq, Clone, PartialEq)]
42pub struct Glyph {
43 /// The glyph ID; encodes the 3d texture coordinates
44 pub id: u16,
45 /// The style of the glyph, e.g., bold, italic
46 pub style: FontStyle,
47 /// The character
48 pub symbol: CompactString,
49 /// The pixel coordinates of the glyph in the texture
50 pub pixel_coords: (i32, i32),
51 /// Indicates if the glyph is an emoji
52 pub is_emoji: bool,
53}
54
55#[rustfmt::skip]
56impl Glyph {
57 /// The ID is used as a short-lived placeholder until the actual ID is assigned.
58 pub const UNASSIGNED_ID: u16 = 0xFFFF;
59
60 /// Glyph ID mask - extracts the base glyph identifier (bits 0-9).
61 /// Supports 1024 unique base glyphs (0x000 to 0x3FF) in the texture atlas.
62 pub const GLYPH_ID_MASK: u16 = 0b0000_0011_1111_1111; // 0x03FF
63 /// Glyph ID mask for emoji - extracts the base glyph identifier (bits 0-11).
64 /// Supports 2048 emoji glyphs (0x000 to 0xFFF) occupying two slots each in the texture atlas.
65 pub const GLYPH_ID_EMOJI_MASK: u16 = 0b0001_1111_1111_1111; // 0x1FFF
66 /// Bold flag - selects the bold variant of the glyph from the texture atlas.
67 pub const BOLD_FLAG: u16 = 0b0000_0100_0000_0000; // 0x0400
68 /// Italic flag - selects the italic variant of the glyph from the texture atlas.
69 pub const ITALIC_FLAG: u16 = 0b0000_1000_0000_0000; // 0x0800
70 /// Emoji flag - indicates this glyph represents an emoji character requiring special handling.
71 pub const EMOJI_FLAG: u16 = 0b0001_0000_0000_0000; // 0x1000
72 /// Underline flag - renders a horizontal line below the character baseline.
73 pub const UNDERLINE_FLAG: u16 = 0b0010_0000_0000_0000; // 0x2000
74 /// Strikethrough flag - renders a horizontal line through the middle of the character.
75 pub const STRIKETHROUGH_FLAG: u16 = 0b0100_0000_0000_0000; // 0x4000
76}
77
78impl Glyph {
79 /// Creates a new glyph with the specified symbol and pixel coordinates.
80 pub fn new(symbol: &str, style: FontStyle, pixel_coords: (i32, i32)) -> Self {
81 let first_char = symbol.chars().next().unwrap();
82 let id = if symbol.len() == 1 && first_char.is_ascii() {
83 // Use a different ID for non-ASCII characters
84 first_char as u16 | style.style_mask()
85 } else {
86 Self::UNASSIGNED_ID
87 };
88
89 Self {
90 id,
91 symbol: symbol.to_compact_string(),
92 style,
93 pixel_coords,
94 is_emoji: false,
95 }
96 }
97
98 pub fn new_with_id(
99 base_id: u16,
100 symbol: &str,
101 style: FontStyle,
102 pixel_coords: (i32, i32),
103 ) -> Self {
104 Self {
105 id: base_id | style.style_mask(),
106 symbol: symbol.to_compact_string(),
107 style,
108 pixel_coords,
109 is_emoji: (base_id & Self::EMOJI_FLAG) != 0,
110 }
111 }
112
113 pub fn new_emoji(base_id: u16, symbol: &str, pixel_coords: (i32, i32)) -> Self {
114 Self {
115 id: base_id | Self::EMOJI_FLAG,
116 symbol: symbol.to_compact_string(),
117 style: FontStyle::Normal, // Emoji glyphs do not have style variants
118 pixel_coords,
119 is_emoji: true,
120 }
121 }
122
123 /// Returns true if this glyph represents a single ASCII character.
124 pub fn is_ascii(&self) -> bool {
125 self.symbol.len() == 1
126 }
127
128 /// Returns the base glyph ID without style flags.
129 ///
130 /// For non-emoji glyphs, this masks off the style bits (bold/italic) using
131 /// [`GLYPH_ID_MASK`](Self::GLYPH_ID_MASK) to extract just the base identifier (bits 0-9).
132 /// For emoji glyphs, returns the full ID since emoji don't use style variants.
133 ///
134 /// # Examples
135 ///
136 /// ```
137 /// use beamterm_data::{Glyph, FontStyle};
138 ///
139 /// // Bold 'A' (0x0441) -> base ID 0x41
140 /// let bold_a = Glyph::new_with_id(0x41, "A", FontStyle::Bold, (0, 0));
141 /// assert_eq!(bold_a.id, 0x441);
142 /// assert_eq!(bold_a.base_id(), 0x041);
143 ///
144 /// // Emoji retains full ID
145 /// let emoji = Glyph::new_emoji(0x00, "🚀", (0, 0));
146 /// assert_eq!(emoji.base_id(), 0x1000); // includes EMOJI_FLAG
147 /// ```
148 pub fn base_id(&self) -> u16 {
149 if self.is_emoji {
150 self.id & Self::GLYPH_ID_EMOJI_MASK
151 } else {
152 self.id & Self::GLYPH_ID_MASK
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum GlyphEffect {
159 /// No special effect applied to the glyph.
160 None = 0x0,
161 /// Underline effect applied below the glyph.
162 Underline = 0x2000,
163 /// Strikethrough effect applied through the glyph.
164 Strikethrough = 0x4000,
165}
166
167impl GlyphEffect {
168 pub fn from_u16(v: u16) -> GlyphEffect {
169 match v {
170 0x0000 => GlyphEffect::None,
171 0x2000 => GlyphEffect::Underline,
172 0x4000 => GlyphEffect::Strikethrough,
173 0x6000 => GlyphEffect::Strikethrough,
174 _ => GlyphEffect::None,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
180pub enum FontStyle {
181 Normal = 0x0000,
182 Bold = 0x0400,
183 Italic = 0x0800,
184 BoldItalic = 0x0C00,
185}
186
187impl FontStyle {
188 pub const MASK: u16 = 0x0C00;
189
190 pub const ALL: [FontStyle; 4] =
191 [FontStyle::Normal, FontStyle::Bold, FontStyle::Italic, FontStyle::BoldItalic];
192
193 pub fn from_u16(v: u16) -> Result<FontStyle, SerializationError> {
194 match v {
195 0x0000 => Ok(FontStyle::Normal),
196 0x0400 => Ok(FontStyle::Bold),
197 0x0800 => Ok(FontStyle::Italic),
198 0x0C00 => Ok(FontStyle::BoldItalic),
199 _ => Err(SerializationError {
200 message: CompactString::new(format!("Invalid font style value: {v:#06x}")),
201 }),
202 }
203 }
204
205 pub(super) fn from_ordinal(ordinal: u8) -> Result<FontStyle, SerializationError> {
206 match ordinal {
207 0 => Ok(FontStyle::Normal),
208 1 => Ok(FontStyle::Bold),
209 2 => Ok(FontStyle::Italic),
210 3 => Ok(FontStyle::BoldItalic),
211 _ => Err(SerializationError {
212 message: CompactString::new(format!("Invalid font style ordinal: {ordinal}")),
213 }),
214 }
215 }
216
217 pub(super) const fn ordinal(&self) -> usize {
218 match self {
219 FontStyle::Normal => 0,
220 FontStyle::Bold => 1,
221 FontStyle::Italic => 2,
222 FontStyle::BoldItalic => 3,
223 }
224 }
225
226 /// Returns the style bits for this font style, used to encode the style in the glyph ID.
227 pub const fn style_mask(&self) -> u16 {
228 *self as u16
229 }
230}