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(crate) id: u16,
45 /// The style of the glyph, e.g., bold, italic
46 pub(crate) style: FontStyle,
47 /// The character
48 pub(crate) symbol: CompactString,
49 /// The pixel coordinates of the glyph in the texture
50 pub(crate) pixel_coords: (i32, i32),
51 /// Indicates if the glyph is an emoji
52 pub(crate) 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 #[doc(hidden)]
61 pub const GLYPH_ID_MASK: u16 = 0b0000_0011_1111_1111; // 0x03FF
62 #[doc(hidden)]
63 pub const GLYPH_ID_EMOJI_MASK: u16 = 0b0001_1111_1111_1111; // 0x1FFF
64 #[doc(hidden)]
65 pub const BOLD_FLAG: u16 = 0b0000_0100_0000_0000; // 0x0400
66 #[doc(hidden)]
67 pub const ITALIC_FLAG: u16 = 0b0000_1000_0000_0000; // 0x0800
68 #[doc(hidden)]
69 pub const EMOJI_FLAG: u16 = 0b0001_0000_0000_0000; // 0x1000
70 #[doc(hidden)]
71 pub const UNDERLINE_FLAG: u16 = 0b0010_0000_0000_0000; // 0x2000
72 #[doc(hidden)]
73 pub const STRIKETHROUGH_FLAG: u16 = 0b0100_0000_0000_0000; // 0x4000
74}
75
76impl Glyph {
77 /// Returns the glyph ID encoding texture coordinates and style flags.
78 #[inline]
79 #[must_use]
80 pub fn id(&self) -> u16 {
81 self.id
82 }
83
84 /// Returns the font style of this glyph.
85 #[inline]
86 #[must_use]
87 pub fn style(&self) -> FontStyle {
88 self.style
89 }
90
91 /// Returns the character or grapheme this glyph represents.
92 #[inline]
93 #[must_use]
94 pub fn symbol(&self) -> &str {
95 &self.symbol
96 }
97
98 /// Returns the pixel coordinates of the glyph in the texture.
99 #[inline]
100 #[must_use]
101 pub fn pixel_coords(&self) -> (i32, i32) {
102 self.pixel_coords
103 }
104
105 /// Returns true if this glyph is an emoji.
106 #[inline]
107 #[must_use]
108 pub fn is_emoji(&self) -> bool {
109 self.is_emoji
110 }
111
112 /// Sets the pixel coordinates of the glyph in the texture.
113 #[inline]
114 pub fn set_pixel_coords(&mut self, pixel_coords: (i32, i32)) {
115 self.pixel_coords = pixel_coords;
116 }
117
118 /// Creates a new glyph with the specified symbol and pixel coordinates.
119 ///
120 /// # Panics
121 /// Panics if `symbol` is empty.
122 #[must_use]
123 pub fn new(symbol: &str, style: FontStyle, pixel_coords: (i32, i32)) -> Self {
124 let first_char = symbol.chars().next().unwrap();
125 let id = if symbol.len() == 1 && first_char.is_ascii() {
126 // Use a different ID for non-ASCII characters
127 first_char as u16 | style.style_mask()
128 } else {
129 Self::UNASSIGNED_ID
130 };
131
132 Self {
133 id,
134 symbol: symbol.to_compact_string(),
135 style,
136 pixel_coords,
137 is_emoji: false,
138 }
139 }
140
141 /// Creates a new glyph with an explicit base ID and style.
142 #[must_use]
143 pub fn new_with_id(
144 base_id: u16,
145 symbol: &str,
146 style: FontStyle,
147 pixel_coords: (i32, i32),
148 ) -> Self {
149 Self {
150 id: base_id | style.style_mask(),
151 symbol: symbol.to_compact_string(),
152 style,
153 pixel_coords,
154 is_emoji: (base_id & Self::EMOJI_FLAG) != 0,
155 }
156 }
157
158 /// Creates a new emoji glyph with the given base ID and symbol.
159 #[must_use]
160 pub fn new_emoji(base_id: u16, symbol: &str, pixel_coords: (i32, i32)) -> Self {
161 Self {
162 id: base_id | Self::EMOJI_FLAG,
163 symbol: symbol.to_compact_string(),
164 style: FontStyle::Normal, // Emoji glyphs do not have style variants
165 pixel_coords,
166 is_emoji: true,
167 }
168 }
169
170 /// Returns true if this glyph represents a single ASCII character.
171 #[must_use]
172 pub fn is_ascii(&self) -> bool {
173 self.symbol.len() == 1
174 }
175
176 /// Returns the base glyph ID without style flags.
177 ///
178 /// For non-emoji glyphs, this masks off the style bits (bold/italic) using
179 /// [`GLYPH_ID_MASK`](Self::GLYPH_ID_MASK) to extract just the base identifier (bits 0-9).
180 /// For emoji glyphs, returns the full ID since emoji don't use style variants.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// use beamterm_data::{Glyph, FontStyle};
186 ///
187 /// // Bold 'A' (0x0441) -> base ID 0x41
188 /// let bold_a = Glyph::new_with_id(0x41, "A", FontStyle::Bold, (0, 0));
189 /// assert_eq!(bold_a.id(), 0x441);
190 /// assert_eq!(bold_a.base_id(), 0x041);
191 ///
192 /// // Emoji retains full ID
193 /// let emoji = Glyph::new_emoji(0x00, "🚀", (0, 0));
194 /// assert_eq!(emoji.base_id(), 0x1000); // includes EMOJI_FLAG
195 /// ```
196 #[must_use]
197 pub fn base_id(&self) -> u16 {
198 if self.is_emoji {
199 self.id & Self::GLYPH_ID_EMOJI_MASK
200 } else {
201 self.id & Self::GLYPH_ID_MASK
202 }
203 }
204}
205
206/// Text decoration effect applied to a glyph.
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum GlyphEffect {
209 /// No special effect applied to the glyph.
210 None = 0x0,
211 /// Underline effect applied below the glyph.
212 Underline = 0x2000,
213 /// Strikethrough effect applied through the glyph.
214 Strikethrough = 0x4000,
215}
216
217impl GlyphEffect {
218 /// Decodes a glyph effect from the effect bits of a 16-bit glyph ID.
219 #[must_use]
220 pub fn from_u16(v: u16) -> GlyphEffect {
221 match v {
222 0x0000 => GlyphEffect::None,
223 0x2000 => GlyphEffect::Underline,
224 0x4000 => GlyphEffect::Strikethrough,
225 0x6000 => GlyphEffect::Strikethrough,
226 _ => GlyphEffect::None,
227 }
228 }
229}
230
231/// Font style variant encoded in glyph IDs.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
233pub enum FontStyle {
234 /// Regular weight, upright style.
235 Normal = 0x0000,
236 /// Bold weight.
237 Bold = 0x0400,
238 /// Italic style.
239 Italic = 0x0800,
240 /// Bold weight with italic style.
241 BoldItalic = 0x0C00,
242}
243
244impl FontStyle {
245 /// Bitmask for extracting font style bits from a glyph ID.
246 pub const MASK: u16 = 0x0C00;
247
248 /// All font style variants.
249 pub const ALL: [FontStyle; 4] =
250 [FontStyle::Normal, FontStyle::Bold, FontStyle::Italic, FontStyle::BoldItalic];
251
252 /// # Errors
253 /// Returns [`SerializationError`] if `v` is not a valid font style value.
254 pub fn from_u16(v: u16) -> Result<FontStyle, SerializationError> {
255 match v {
256 0x0000 => Ok(FontStyle::Normal),
257 0x0400 => Ok(FontStyle::Bold),
258 0x0800 => Ok(FontStyle::Italic),
259 0x0C00 => Ok(FontStyle::BoldItalic),
260 _ => Err(SerializationError {
261 message: CompactString::new(format!("Invalid font style value: {v:#06x}")),
262 }),
263 }
264 }
265
266 pub(super) fn from_ordinal(ordinal: u8) -> Result<FontStyle, SerializationError> {
267 match ordinal {
268 0 => Ok(FontStyle::Normal),
269 1 => Ok(FontStyle::Bold),
270 2 => Ok(FontStyle::Italic),
271 3 => Ok(FontStyle::BoldItalic),
272 _ => Err(SerializationError {
273 message: CompactString::new(format!("Invalid font style ordinal: {ordinal}")),
274 }),
275 }
276 }
277
278 pub(super) const fn ordinal(self) -> usize {
279 match self {
280 FontStyle::Normal => 0,
281 FontStyle::Bold => 1,
282 FontStyle::Italic => 2,
283 FontStyle::BoldItalic => 3,
284 }
285 }
286
287 /// Returns the style bits for this font style, used to encode the style in the glyph ID.
288 #[must_use]
289 pub const fn style_mask(&self) -> u16 {
290 *self as u16
291 }
292}