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-9 | GLYPH_ID | `0x03FF` | `0000_0011_1111_1111` | Base glyph identifier |
19/// | 10 | BOLD | `0x0400` | `0000_0100_0000_0000` | Bold font style |
20/// | 11 | ITALIC | `0x0800` | `0000_1000_0000_0000` | Italic font style |
21/// | 12 | EMOJI | `0x1000` | `0001_0000_0000_0000` | Emoji character flag |
22/// | 13 | UNDERLINE | `0x2000` | `0010_0000_0000_0000` | Underline effect |
23/// | 14 | STRIKETHROUGH | `0x4000` | `0100_0000_0000_0000` | Strikethrough effect |
24/// | 15 | RESERVED | `0x8000` | `1000_0000_0000_0000` | Reserved for future use |
25///
26/// - The first 10 bits (0-9) represent the base glyph ID, allowing for 1024 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_0100_0100_0001` | `0x0441` | Bold 'A' |
36/// | 'A' (0x41) | Bold + Italic | `0000_1100_0100_0001` | `0x0C41` | Bold italic 'A' |
37/// | 'A' (0x41) | Bold + Underline | `0010_0100_0100_0001` | `0x2441` | Bold underlined 'A' |
38/// | '🚀' (0x81) | Emoji | `0001_0000_1000_0001` | `0x1081` | "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-9).
59 /// Supports 1024 unique base glyphs (0x000 to 0x3FF) in the texture atlas.
60 pub const GLYPH_ID_MASK: u16 = 0b0000_0011_1111_1111; // 0x03FF
61 /// Bold flag - selects the bold variant of the glyph from the texture atlas.
62 pub const BOLD_FLAG: u16 = 0b0000_0100_0000_0000; // 0x0400
63 /// Italic flag - selects the italic variant of the glyph from the texture atlas.
64 pub const ITALIC_FLAG: u16 = 0b0000_1000_0000_0000; // 0x0800
65 /// Emoji flag - indicates this glyph represents an emoji character requiring special handling.
66 pub const EMOJI_FLAG: u16 = 0b0001_0000_0000_0000; // 0x1000
67 /// Underline flag - renders a horizontal line below the character baseline.
68 pub const UNDERLINE_FLAG: u16 = 0b0010_0000_0000_0000; // 0x2000
69 /// Strikethrough flag - renders a horizontal line through the middle of the character.
70 pub const STRIKETHROUGH_FLAG: u16 = 0b0100_0000_0000_0000; // 0x4000
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: (base_id & Self::EMOJI_FLAG) != 0,
105 }
106 }
107
108 pub fn new_emoji(base_id: u16, symbol: &str, pixel_coords: (i32, i32)) -> Self {
109 Self {
110 id: base_id | Self::EMOJI_FLAG,
111 symbol: symbol.to_compact_string(),
112 style: FontStyle::Normal, // Emoji glyphs do not have style variants
113 pixel_coords,
114 is_emoji: true,
115 }
116 }
117
118 /// Returns true if this glyph represents a single ASCII character.
119 pub fn is_ascii(&self) -> bool {
120 self.symbol.len() == 1 && self.symbol.chars().next().unwrap().is_ascii()
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum GlyphEffect {
126 /// No special effect applied to the glyph.
127 None = 0x0,
128 /// Underline effect applied below the glyph.
129 Underline = 0x2000,
130 /// Strikethrough effect applied through the glyph.
131 Strikethrough = 0x4000,
132}
133
134impl GlyphEffect {
135 pub fn from_u16(v: u16) -> GlyphEffect {
136 match v {
137 0x0000 => GlyphEffect::None,
138 0x2000 => GlyphEffect::Underline,
139 0x4000 => GlyphEffect::Strikethrough,
140 0x6000 => GlyphEffect::Strikethrough,
141 _ => GlyphEffect::None,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum FontStyle {
148 Normal = 0x0000,
149 Bold = 0x0400,
150 Italic = 0x0800,
151 BoldItalic = 0x0C00,
152}
153
154impl FontStyle {
155 pub const ALL: [FontStyle; 4] =
156 [FontStyle::Normal, FontStyle::Bold, FontStyle::Italic, FontStyle::BoldItalic];
157
158 pub fn from_u16(v: u16) -> FontStyle {
159 match v {
160 0x0000 => FontStyle::Normal,
161 0x0400 => FontStyle::Bold,
162 0x0800 => FontStyle::Italic,
163 0x0C00 => FontStyle::BoldItalic,
164 _ => panic!("Invalid font style value: {v}"),
165 }
166 }
167
168 pub(super) fn from_ordinal(ordinal: u8) -> FontStyle {
169 match ordinal {
170 0 => FontStyle::Normal,
171 1 => FontStyle::Bold,
172 2 => FontStyle::Italic,
173 3 => FontStyle::BoldItalic,
174 _ => panic!("Invalid font style ordinal: {ordinal}"),
175 }
176 }
177
178 pub(super) const fn ordinal(&self) -> usize {
179 match self {
180 FontStyle::Normal => 0,
181 FontStyle::Bold => 1,
182 FontStyle::Italic => 2,
183 FontStyle::BoldItalic => 3,
184 }
185 }
186
187 /// Returns the style bits for this font style, used to encode the style in the glyph ID.
188 pub const fn style_mask(&self) -> u16 {
189 *self as u16
190 }
191}