1use std::collections::HashMap;
16use std::sync::{Arc, OnceLock};
17use web_workers::sync::Mutex;
18
19use kurbo::BezPath;
20use ttf_parser::name::name_id;
21use ttf_parser::{Face, GlyphId, OutlineBuilder};
22
23static DEFAULT_FONT: &[u8] = include_bytes!("../assets/default.ttf");
25
26#[derive(Debug, thiserror::Error)]
27pub enum TextError {
28 #[error("font failed to parse")]
29 BadFont,
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34pub enum TextAlign {
35 #[default]
36 Left,
37 Center,
38 Right,
39}
40
41#[derive(Clone)]
44pub struct FontRef {
45 data: Arc<[u8]>,
46}
47
48impl FontRef {
49 pub fn parse(data: &[u8]) -> Result<Self, TextError> {
52 Face::parse(data, 0).map_err(|_| TextError::BadFont)?;
53 Ok(Self {
54 data: Arc::from(data),
55 })
56 }
57
58 pub fn default_font() -> Self {
62 Self {
63 data: default_font_data().clone(),
64 }
65 }
66
67 pub fn for_family(name: Option<&str>) -> Self {
70 match name.and_then(|n| registry().lock_sync().fonts.get(n).cloned()) {
71 Some(data) => Self { data },
72 None => Self::default_font(),
73 }
74 }
75
76 pub fn face(&self) -> Face<'_> {
79 Face::parse(&self.data, 0).expect("font data validated at construction")
80 }
81}
82
83fn default_font_data() -> &'static Arc<[u8]> {
84 static DEFAULT: OnceLock<Arc<[u8]>> = OnceLock::new();
85 DEFAULT.get_or_init(|| Arc::from(DEFAULT_FONT))
86}
87
88pub fn default_font_bytes() -> &'static [u8] {
91 DEFAULT_FONT
92}
93
94struct Registry {
95 fonts: HashMap<String, Arc<[u8]>>,
96}
97
98fn registry() -> &'static Mutex<Registry> {
99 static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
100 REGISTRY.get_or_init(|| {
101 let mut fonts = HashMap::new();
102 let default = default_font_data().clone();
103 if let Some(name) = font_family_name(&default) {
104 fonts.insert(name, default);
105 }
106 Mutex::new(Registry { fonts })
107 })
108}
109
110pub fn font_family_name(bytes: &[u8]) -> Option<String> {
114 let face = Face::parse(bytes, 0).ok()?;
115 let mut fallback = None;
116 for name in face.names() {
117 match name.name_id {
118 name_id::TYPOGRAPHIC_FAMILY => {
119 if let Some(s) = name.to_string() {
120 return Some(s);
121 }
122 }
123 name_id::FAMILY if fallback.is_none() => {
124 fallback = name.to_string();
125 }
126 _ => {}
127 }
128 }
129 fallback
130}
131
132pub fn register_font_data(bytes: Vec<u8>) -> Option<String> {
136 let family = font_family_name(&bytes)?;
137 registry()
138 .lock_sync()
139 .fonts
140 .insert(family.clone(), Arc::from(bytes));
141 Some(family)
142}
143
144pub fn registered_families() -> Vec<String> {
146 let mut names: Vec<String> = registry().lock_sync().fonts.keys().cloned().collect();
147 names.sort();
148 names
149}
150
151struct PathSink {
155 path: BezPath,
156 scale: f64,
157 dx: f64,
158 dy: f64,
159}
160
161impl OutlineBuilder for PathSink {
162 fn move_to(&mut self, x: f32, y: f32) {
163 self.path.move_to((
164 self.dx + x as f64 * self.scale,
165 self.dy - y as f64 * self.scale,
166 ));
167 }
168 fn line_to(&mut self, x: f32, y: f32) {
169 self.path.line_to((
170 self.dx + x as f64 * self.scale,
171 self.dy - y as f64 * self.scale,
172 ));
173 }
174 fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
175 self.path.quad_to(
176 (
177 self.dx + x1 as f64 * self.scale,
178 self.dy - y1 as f64 * self.scale,
179 ),
180 (
181 self.dx + x as f64 * self.scale,
182 self.dy - y as f64 * self.scale,
183 ),
184 );
185 }
186 fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
187 self.path.curve_to(
188 (
189 self.dx + x1 as f64 * self.scale,
190 self.dy - y1 as f64 * self.scale,
191 ),
192 (
193 self.dx + x2 as f64 * self.scale,
194 self.dy - y2 as f64 * self.scale,
195 ),
196 (
197 self.dx + x as f64 * self.scale,
198 self.dy - y as f64 * self.scale,
199 ),
200 );
201 }
202 fn close(&mut self) {
203 self.path.close_path();
204 }
205}
206
207pub fn shape_text_from_bytes(
210 bytes: &[u8],
211 text: &str,
212 size: f64,
213 align: TextAlign,
214 tracking: f64,
215 leading: f64,
216) -> Result<BezPath, TextError> {
217 let font = FontRef::parse(bytes)?;
218 Ok(shape_text(&font, text, size, align, tracking, leading))
219}
220
221pub fn shape_text_default(
223 text: &str,
224 size: f64,
225 align: TextAlign,
226 tracking: f64,
227 leading: f64,
228) -> BezPath {
229 shape_text(
230 &FontRef::default_font(),
231 text,
232 size,
233 align,
234 tracking,
235 leading,
236 )
237}
238
239pub fn shape_text(
244 font: &FontRef,
245 text: &str,
246 size: f64,
247 align: TextAlign,
248 tracking: f64,
249 leading: f64,
250) -> BezPath {
251 let face = font.face();
252 let upem = face.units_per_em() as f64;
253 let scale = size / upem.max(1.0);
254 let line_height = (face.ascender() as f64 - face.descender() as f64 + face.line_gap() as f64)
255 * scale
256 + leading;
257 let mut out = BezPath::new();
258 for (line_idx, line) in text.split('\n').enumerate() {
259 let baseline = line_idx as f64 * line_height;
260 let width = line_advance(&face, line) * scale
261 + tracking * line.chars().count().saturating_sub(1) as f64;
262 let start_x = match align {
263 TextAlign::Left => 0.0,
264 TextAlign::Center => -width / 2.0,
265 TextAlign::Right => -width,
266 };
267 let mut pen = start_x;
268 for ch in line.chars() {
269 let gid = face.glyph_index(ch).unwrap_or(GlyphId(0));
270 let mut sink = PathSink {
271 path: BezPath::new(),
272 scale,
273 dx: pen,
274 dy: baseline,
275 };
276 let _ = face.outline_glyph(gid, &mut sink);
277 out.extend(sink.path);
278 pen += face.glyph_hor_advance(gid).unwrap_or(0) as f64 * scale + tracking;
279 }
280 }
281 out
282}
283
284fn line_advance(face: &Face, line: &str) -> f64 {
286 line.chars()
287 .map(|ch| {
288 let gid = face.glyph_index(ch).unwrap_or(GlyphId(0));
289 face.glyph_hor_advance(gid).unwrap_or(0) as f64
290 })
291 .sum()
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use kurbo::Shape;
298
299 #[test]
300 fn nonempty_text_produces_outline() {
301 let font = FontRef::default_font();
302 let p = shape_text(&font, "Ab", 48.0, TextAlign::Left, 0.0, 0.0);
303 assert!(!p.elements().is_empty());
304 assert!(p.bounding_box().width() > 10.0);
305 }
306
307 #[test]
308 fn newline_advances_baseline_downward() {
309 let font = FontRef::default_font();
310 let one = shape_text(&font, "A", 48.0, TextAlign::Left, 0.0, 0.0).bounding_box();
311 let two = shape_text(&font, "A\nA", 48.0, TextAlign::Left, 0.0, 0.0).bounding_box();
312 assert!(two.height() > one.height() + 10.0);
313 }
314
315 #[test]
316 fn center_align_straddles_origin() {
317 let font = FontRef::default_font();
318 let bb = shape_text(&font, "WW", 48.0, TextAlign::Center, 0.0, 0.0).bounding_box();
319 assert!(bb.x0 < 0.0 && bb.x1 > 0.0);
320 }
321
322 #[test]
323 fn shaping_is_deterministic() {
324 let font = FontRef::default_font();
325 let a = shape_text(&font, "Renamite", 32.0, TextAlign::Left, 0.0, 0.0);
326 let b = shape_text(&font, "Renamite", 32.0, TextAlign::Left, 0.0, 0.0);
327 assert_eq!(a.elements(), b.elements());
328 }
329
330 #[test]
331 fn bundled_font_reports_family_name() {
332 let name = font_family_name(DEFAULT_FONT).expect("bundled font has a name");
333 assert!(!name.is_empty());
334 assert!(
335 registered_families().contains(&name),
336 "default should be registered under its own family name, got {:?}",
337 registered_families()
338 );
339 }
340
341 #[test]
342 fn for_family_none_resolves_to_default() {
343 let font = FontRef::for_family(None);
344 let p = shape_text(&font, "Ab", 48.0, TextAlign::Left, 0.0, 0.0);
345 assert!(!p.elements().is_empty());
346 }
347
348 #[test]
349 fn for_family_unknown_falls_back_to_default() {
350 let font = FontRef::for_family(Some("Definitely Not A Font"));
351 let p = shape_text(&font, "Ab", 48.0, TextAlign::Left, 0.0, 0.0);
352 assert!(!p.elements().is_empty());
353 }
354
355 #[test]
356 fn register_font_data_keys_by_family_and_resolves() {
357 let name = register_font_data(DEFAULT_FONT.to_vec()).expect("valid font registers");
358 assert_eq!(
359 name,
360 font_family_name(DEFAULT_FONT).unwrap(),
361 "register returns the same family the font reports"
362 );
363 assert!(registered_families().contains(&name));
364
365 let by_name = FontRef::for_family(Some(&name));
366 let by_default = FontRef::default_font();
367 let a = shape_text(&by_name, "Renamite", 32.0, TextAlign::Left, 0.0, 0.0);
368 let b = shape_text(&by_default, "Renamite", 32.0, TextAlign::Left, 0.0, 0.0);
369 assert_eq!(
370 a.elements(),
371 b.elements(),
372 "registry hit shapes identically to the default face"
373 );
374 }
375
376 #[test]
377 fn register_invalid_bytes_returns_none() {
378 assert_eq!(register_font_data(b"not a font".to_vec()), None);
379 assert_eq!(font_family_name(b"not a font"), None);
380 }
381}