beamterm_data/
glyph.rs

1use compact_str::{CompactString, ToCompactString};
2
3/// Represents a single character glyph in a font atlas texture.
4///
5/// A `Glyph` contains the metadata needed to locate and identify a character
6/// within a font atlas texture. Each glyph has a unique ID that maps
7/// to its coordinates in a WebGL `TEXTURE_2D_ARRAY`.
8///
9/// # ASCII Optimization
10/// For ASCII characters, the glyph ID directly corresponds to the character's
11/// ASCII value, enabling fast lookups without hash table lookups. Non-ASCII
12/// characters are assigned sequential IDs starting from a base value.
13///
14/// # Glyph ID Bit Layout (16-bit)
15///
16/// | Bit(s) | Flag Name     | Hex Mask | Binary Mask           | Description               |
17/// |--------|---------------|----------|-----------------------|---------------------------|
18/// | 0-8    | GLYPH_ID      | `0x01FF` | `0000_0001_1111_1111` | Base glyph identifier     |
19/// | 9      | BOLD          | `0x0200` | `0000_0010_0000_0000` | Bold font style           |
20/// | 10     | ITALIC        | `0x0400` | `0000_0100_0000_0000` | Italic font style         |
21/// | 11     | EMOJI         | `0x0800` | `0000_1000_0000_0000` | Emoji character flag      |
22/// | 12     | UNDERLINE     | `0x1000` | `0001_0000_0000_0000` | Underline effect          |
23/// | 13     | STRIKETHROUGH | `0x2000` | `0010_0000_0000_0000` | Strikethrough effect      |
24/// | 14-15  | RESERVED      | `0xC000` | `1100_0000_0000_0000` | Reserved for future use   |
25///
26/// - The first 9 bits (0-8) represent the base glyph ID, allowing for 512 unique glyphs.
27/// - Emoji glyphs implicitly clear any other font style bits.
28/// - The fragment shader uses the glyph ID to decode the texture coordinates and effects.
29///
30/// ## Glyph ID Encoding Examples
31///
32/// | Character   | Style            | Binary Representation | Hex Value | Description         |
33/// |-------------|------------------|-----------------------|-----------|---------------------|
34/// | 'A' (0x41)  | Normal           | `0000_0000_0100_0001` | `0x0041`  | Plain 'A'           |
35/// | 'A' (0x41)  | Bold             | `0000_0010_0100_0001` | `0x0241`  | Bold 'A'            |
36/// | 'A' (0x41)  | Bold + Italic    | `0000_0110_0100_0001` | `0x0641`  | Bold italic 'A'     |
37/// | 'A' (0x41)  | Bold + Underline | `0000_1010_0100_0001` | `0x0A41`  | Bold underlined 'A' |
38/// | '🚀' (0x81) | Emoji            | `1000_0000_1000_0001` | `0x0881`  | "rocket" emoji      |
39#[derive(Debug, Eq, PartialEq)]
40pub struct Glyph {
41    /// The glyph ID; encodes the 3d texture coordinates
42    pub id: u16,
43    /// The style of the glyph, e.g., bold, italic
44    pub style: FontStyle,
45    /// The character
46    pub symbol: CompactString,
47    /// The pixel coordinates of the glyph in the texture
48    pub pixel_coords: (i32, i32),
49    /// Indicates if the glyph is an emoji
50    pub is_emoji: bool,
51}
52
53#[rustfmt::skip]
54impl Glyph {
55    /// The ID is used as a short-lived placeholder until the actual ID is assigned.
56    pub const UNASSIGNED_ID: u16 = 0xFFFF;
57
58    /// Glyph ID mask - extracts the base glyph identifier (bits 0-8).
59    /// Supports 512 unique base glyphs (0x000 to 0x1FF) in the texture atlas.
60    pub const GLYPH_ID_MASK: u16      = 0b0000_0001_1111_1111; // 0x01FF
61    /// Bold flag - selects the bold variant of the glyph from the texture atlas.
62    pub const BOLD_FLAG: u16          = 0b0000_0010_0000_0000; // 0x0200
63    /// Italic flag - selects the italic variant of the glyph from the texture atlas.
64    pub const ITALIC_FLAG: u16        = 0b0000_0100_0000_0000; // 0x0400
65    /// Emoji flag - indicates this glyph represents an emoji character requiring special handling.
66    pub const EMOJI_FLAG: u16         = 0b0000_1000_0000_0000; // 0x0800
67    /// Underline flag - renders a horizontal line below the character baseline.
68    pub const UNDERLINE_FLAG: u16     = 0b0001_0000_0000_0000; // 0x1000
69    /// Strikethrough flag - renders a horizontal line through the middle of the character.
70    pub const STRIKETHROUGH_FLAG: u16 = 0b0010_0000_0000_0000; // 0x2000
71}
72
73impl Glyph {
74    /// Creates a new glyph with the specified symbol and pixel coordinates.
75    pub fn new(symbol: &str, style: FontStyle, pixel_coords: (i32, i32)) -> Self {
76        let first_char = symbol.chars().next().unwrap();
77        let id = if symbol.len() == 1 && first_char.is_ascii() {
78            // Use a different ID for non-ASCII characters
79            first_char as u16 | style.style_mask()
80        } else {
81            Self::UNASSIGNED_ID
82        };
83
84        Self {
85            id,
86            symbol: symbol.to_compact_string(),
87            style,
88            pixel_coords,
89            is_emoji: false,
90        }
91    }
92
93    pub fn new_with_id(
94        base_id: u16,
95        symbol: &str,
96        style: FontStyle,
97        pixel_coords: (i32, i32),
98    ) -> Self {
99        Self {
100            id: base_id | style.style_mask(),
101            symbol: symbol.to_compact_string(),
102            style,
103            pixel_coords,
104            is_emoji: false,
105        }
106    }
107
108    /// Returns true if this glyph represents a single ASCII character.
109    pub fn is_ascii(&self) -> bool {
110        self.symbol.len() == 1 && self.symbol.chars().next().unwrap().is_ascii()
111    }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum GlyphEffect {
116    /// No special effect applied to the glyph.
117    None = 0x0,
118    /// Underline effect applied below the glyph.
119    Underline = 0x1000,
120    /// Strikethrough effect applied through the glyph.
121    Strikethrough = 0x2000,
122}
123
124impl GlyphEffect {
125    pub fn from_u16(v: u16) -> GlyphEffect {
126        match v {
127            0x0000 => GlyphEffect::None,
128            0x1000 => GlyphEffect::Underline,
129            0x2000 => GlyphEffect::Strikethrough,
130            0x3000 => GlyphEffect::Strikethrough,
131            _ => {
132                println!("Unknown glyph effect 0x{v:x}");
133                panic!("yolo panic");
134            },
135        }
136    }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum FontStyle {
141    Normal = 0x0000,
142    Bold = 0x0200,
143    Italic = 0x0400,
144    BoldItalic = 0x0600,
145}
146
147impl FontStyle {
148    pub const ALL: [FontStyle; 4] =
149        [FontStyle::Normal, FontStyle::Bold, FontStyle::Italic, FontStyle::BoldItalic];
150
151    pub fn from_u16(v: u16) -> FontStyle {
152        match v {
153            0x0000 => FontStyle::Normal,
154            0x0200 => FontStyle::Bold,
155            0x0400 => FontStyle::Italic,
156            0x0600 => FontStyle::BoldItalic,
157            _ => panic!("Invalid font style value: {v}"),
158        }
159    }
160
161    pub(super) fn from_ordinal(ordinal: u8) -> FontStyle {
162        match ordinal {
163            0 => FontStyle::Normal,
164            1 => FontStyle::Bold,
165            2 => FontStyle::Italic,
166            3 => FontStyle::BoldItalic,
167            _ => panic!("Invalid font style ordinal: {ordinal}"),
168        }
169    }
170
171    pub(super) const fn ordinal(&self) -> usize {
172        match self {
173            FontStyle::Normal => 0,
174            FontStyle::Bold => 1,
175            FontStyle::Italic => 2,
176            FontStyle::BoldItalic => 3,
177        }
178    }
179
180    /// Returns the style bits for this font style, used to encode the style in the glyph ID.
181    pub const fn style_mask(&self) -> u16 {
182        *self as u16
183    }
184}