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 size = if size.is_finite() { size.max(0.0) } else { 0.0 };
254 if size <= 1e-9 {
255 return BezPath::new();
256 }
257 let tracking = if tracking.is_finite() { tracking } else { 0.0 };
258 let leading = if leading.is_finite() { leading } else { 0.0 };
259 let scale = size / upem.max(1.0);
260 let line_height = ((face.ascender() as f64 - face.descender() as f64 + face.line_gap() as f64)
261 * scale
262 + leading)
263 .max(size * 0.2);
264 let mut out = BezPath::new();
265 for (line_idx, line) in text
266 .split('\n')
267 .map(|l| l.strip_suffix('\r').unwrap_or(l))
268 .enumerate()
269 {
270 let baseline = line_idx as f64 * line_height;
271 let width = line_advance(&face, line) * scale
272 + tracking * line.chars().count().saturating_sub(1) as f64;
273 let start_x = match align {
274 TextAlign::Left => 0.0,
275 TextAlign::Center => -width / 2.0,
276 TextAlign::Right => -width,
277 };
278 let mut pen = start_x;
279 for ch in line.chars() {
280 let gid = face.glyph_index(ch).unwrap_or(GlyphId(0));
281 let mut sink = PathSink {
282 path: BezPath::new(),
283 scale,
284 dx: pen,
285 dy: baseline,
286 };
287 let _ = face.outline_glyph(gid, &mut sink);
288 out.extend(sink.path);
289 pen += face.glyph_hor_advance(gid).unwrap_or(0) as f64 * scale + tracking;
290 }
291 }
292 out
293}
294
295fn line_advance(face: &Face, line: &str) -> f64 {
297 line.chars()
298 .map(|ch| {
299 let gid = face.glyph_index(ch).unwrap_or(GlyphId(0));
300 face.glyph_hor_advance(gid).unwrap_or(0) as f64
301 })
302 .sum()
303}