1use std::{borrow::Cow, cell::RefCell, io, rc::Rc, sync::Arc};
8
9use glyphon::fontdb::{self, Database, FaceInfo};
10use tessera_ui::{EntryRegistry, LoadState, Resource, Source, TesseraPackage};
11
12pub use tessera_ui::FontAsset as Font;
13
14use crate::{text::write_font_system, theme::TextStyle};
15
16pub struct Fonts(
18 pub &'static [Font],
20);
21
22pub struct FontStylesheet(
26 pub &'static str,
28);
29
30impl TesseraPackage for Fonts {
31 fn register(self, _: &mut EntryRegistry) {
32 let registry = registry();
33 let mut registry = registry.borrow_mut();
34 for font in self.0 {
35 let font = FontSource::from(*font);
36 if !registry
37 .sources
38 .iter()
39 .any(|source| source.source == font.source)
40 {
41 registry.sources.push(font);
42 }
43 }
44 }
45}
46
47impl TesseraPackage for FontStylesheet {
48 fn register(self, _: &mut EntryRegistry) {
49 let registry = registry();
50 let mut registry = registry.borrow_mut();
51 if !registry.stylesheets.iter().any(|sheet| sheet.url == self.0) {
52 registry.stylesheets.push(Stylesheet::new(self.0));
53 }
54 }
55}
56
57struct Stylesheet {
58 url: &'static str,
59 faces: Option<Resource<Arc<[FontFace]>>>,
60}
61
62impl Stylesheet {
63 fn new(url: &'static str) -> Self {
64 Self { url, faces: None }
65 }
66}
67
68#[derive(Clone)]
69struct FontFace {
70 family: String,
71 src: String,
72 ranges: Vec<(u32, u32)>,
73}
74
75#[derive(Clone)]
76struct FontSource {
77 source: Source,
78 family: Cow<'static, str>,
79 fallback: Cow<'static, [(u32, u32)]>,
80 coverage: Cow<'static, [(u32, u32)]>,
81}
82
83impl From<Font> for FontSource {
84 fn from(font: Font) -> Self {
85 Self {
86 source: font.asset.into(),
87 family: font.family.into(),
88 fallback: font.fallback.into(),
89 coverage: font.coverage.into(),
90 }
91 }
92}
93
94struct FontRequest {
95 font: FontSource,
96 faces: Resource<Arc<[FaceInfo]>>,
97 installed: Option<u64>,
99}
100
101struct FontRegistry {
102 sources: Vec<FontSource>,
103 stylesheets: Vec<Stylesheet>,
104 requests: Vec<FontRequest>,
105 revision: u64,
106}
107
108impl Default for FontRegistry {
109 fn default() -> Self {
110 Self {
111 sources: Vec::new(),
112 #[cfg(target_family = "wasm")]
113 stylesheets: [
114 "https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto/index.css",
115 "https://cdn.jsdelivr.net/npm/@fontsource-variable/noto-sans-sc/index.css",
116 ]
117 .map(Stylesheet::new)
118 .into(),
119 #[cfg(not(target_family = "wasm"))]
120 stylesheets: Vec::new(),
121 requests: Vec::new(),
122 revision: 0,
123 }
124 }
125}
126
127fn registry() -> Rc<RefCell<FontRegistry>> {
128 tessera_ui::__private::local_runtime_resource::<RefCell<FontRegistry>>()
129}
130
131pub(crate) fn prepare(text: &str, style: TextStyle) -> Option<u64> {
136 let registry = registry();
137 let mut registry = registry.borrow_mut();
138 let FontRegistry {
139 sources,
140 stylesheets,
141 requests,
142 revision,
143 } = &mut *registry;
144 let mut pending = false;
145 stylesheets.retain_mut(|sheet| {
146 match sheet
147 .faces
148 .get_or_insert_with(|| Resource::load(sheet.url, parse_stylesheet))
149 .get()
150 {
151 LoadState::Loading => {
152 pending = true;
153 return true;
154 }
155 LoadState::Ready(faces) => {
156 sources.extend(faces.iter().map(|face| face.font(sheet.url)));
157 }
158 LoadState::Failed(_) => {}
159 }
160 false
161 });
162 let family = style.font_family;
163 let mut key = 0;
164 for font in sources.iter().filter(|font| needed(font, family, text)) {
165 let index = match requests
166 .iter()
167 .position(|request| request.font.source == font.source)
168 {
169 Some(index) => index,
170 None => {
171 requests.push(FontRequest {
172 font: font.clone(),
173 faces: Resource::load(font.source.clone(), parse_font),
174 installed: None,
175 });
176 requests.len() - 1
177 }
178 };
179 let installed = match requests[index].installed {
180 Some(installed) => installed,
181 None => match requests[index].faces.get() {
182 LoadState::Loading => {
183 pending = true;
184 continue;
185 }
186 LoadState::Ready(faces) => {
187 let replace = !requests.iter().any(|request| {
188 request.installed.is_some_and(|installed| installed > 0)
189 && request.font.family == font.family
190 });
191 let mut fonts = write_font_system();
192 let db = fonts.db_mut();
193 if replace {
194 let existing = db
195 .faces()
196 .filter(|face| {
197 face.families.iter().any(|(name, _)| *name == font.family)
198 })
199 .map(|face| face.id)
200 .collect::<Vec<_>>();
201 for id in existing {
202 db.remove_face(id);
203 }
204 }
205 for face in faces.iter() {
206 db.push_face_info(face.clone());
207 }
208 *revision += 1;
209 *revision
210 }
211 LoadState::Failed(_) => 0,
212 },
213 };
214 requests[index].installed = Some(installed);
215 key = key.max(installed);
216 }
217 (!pending).then_some(key)
218}
219
220fn needed(font: &FontSource, family: Option<&str>, text: &str) -> bool {
221 let contains = |ranges: &[(u32, u32)], c: char| {
222 ranges
223 .iter()
224 .any(|&(start, end)| (start..=end).contains(&(c as u32)))
225 };
226 let named = family == Some(&*font.family);
227 (named && font.coverage.is_empty())
228 || text.chars().any(|c| {
229 (font.coverage.is_empty() || contains(&font.coverage, c))
230 && (named || contains(&font.fallback, c))
231 })
232}
233
234fn parse_font(bytes: Arc<[u8]>) -> io::Result<Arc<[FaceInfo]>> {
235 let bytes: Arc<[u8]> = if bytes.starts_with(b"wOF2") {
236 wuff::decompress_woff2(&bytes)
237 .map_err(|error| io::Error::other(format!("{error:?}")))?
238 .into()
239 } else {
240 bytes
241 };
242 let mut db = Database::new();
243 db.load_font_source(fontdb::Source::Binary(Arc::new(bytes)));
244 let faces = db.faces().cloned().collect::<Arc<[_]>>();
245 if faces.is_empty() {
246 return Err(io::Error::other("font contains no supported faces"));
247 }
248 Ok(faces)
249}
250
251fn parse_stylesheet(bytes: Arc<[u8]>) -> io::Result<Arc<[FontFace]>> {
252 let css = std::str::from_utf8(&bytes).map_err(io::Error::other)?;
253 Ok(css
254 .split("@font-face")
255 .skip(1)
256 .filter_map(|rule| {
257 let rule = &rule[rule.find('{')? + 1..rule.find('}')?];
258 let property = |name: &str| {
259 rule.split(';').find_map(|declaration| {
260 let (key, value) = declaration.split_once(':')?;
261 (key.trim() == name).then(|| value.trim())
262 })
263 };
264 let src = property("src")?;
265 let src = &src[src.find("url(")? + 4..];
266 let src = src[..src.find(')')?].trim_matches(['"', '\'']);
267 let ranges = property("unicode-range").map_or_else(
268 || vec![(0, 0x10ffff)],
269 |ranges| {
270 ranges
271 .split(',')
272 .filter_map(|range| {
273 let range = range.trim().strip_prefix("U+")?;
274 let (start, end) = range.split_once('-').unwrap_or((range, range));
275 let parse = |value: &str, fill| {
276 u32::from_str_radix(&value.replace('?', fill), 16).ok()
277 };
278 Some((parse(start, "0")?, parse(end, "F")?))
279 })
280 .collect()
281 },
282 );
283 Some(FontFace {
284 family: property("font-family")?
285 .trim_matches(['"', '\''])
286 .to_owned(),
287 src: src.to_owned(),
288 ranges,
289 })
290 })
291 .collect())
292}
293
294impl FontFace {
295 fn font(&self, stylesheet: &str) -> FontSource {
296 let url = if self.src.contains("://") {
297 self.src.clone()
298 } else {
299 let base = &stylesheet[..stylesheet.rfind('/').map_or(0, |end| end + 1)];
300 format!("{base}{}", self.src.trim_start_matches("./"))
301 };
302 FontSource {
303 source: url.into(),
304 family: self.family.clone().into(),
305 fallback: self.ranges.clone().into(),
306 coverage: self.ranges.clone().into(),
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn stylesheet_rules_resolve_files_and_unicode_ranges() {
317 let css = br#"
318 /* latin */
319 @font-face {
320 font-family: 'Example Sans';
321 src: url(./files/latin.woff2) format('woff2');
322 unicode-range: U+0000-00FF, U+4??, U+2014;
323 }
324 @font-face { font-family: "Other"; src: url("https://cdn.test/other.ttf"); }
325 "#;
326 let fonts = parse_stylesheet(Arc::from(&css[..]))
327 .unwrap()
328 .iter()
329 .map(|face| face.font("https://cdn.test/sans/index.css"))
330 .collect::<Vec<_>>();
331 assert!(fonts[0].source == Source::from("https://cdn.test/sans/files/latin.woff2"));
332 assert_eq!(fonts[0].family, "Example Sans");
333 assert_eq!(
334 *fonts[0].coverage,
335 [(0, 0xff), (0x400, 0x4ff), (0x2014, 0x2014)]
336 );
337 assert!(fonts[1].source == Source::from("https://cdn.test/other.ttf"));
338 assert_eq!(*fonts[1].coverage, [(0, 0x10ffff)]);
339 }
340}