Skip to main content

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}