1use crate::assets;
2use crate::bindings::ui;
3use crate::logger::warn;
4use std::cell::RefCell;
5use std::rc::Rc;
6
7const STYLE_MISMATCH_PENALTY: i32 = 1000;
8const MAX_FONT_SCORE: i32 = i32::MAX;
9const FIRST_DYNAMIC_FONT_ID: u32 = 1024;
10
11thread_local! {
12 static NEXT_DYNAMIC_FONT_ID: RefCell<u32> = const { RefCell::new(FIRST_DYNAMIC_FONT_ID) };
13 static FONT_LOADED_CALLBACKS: RefCell<Vec<FontLoadedRegistration>> = const { RefCell::new(Vec::new()) };
14 static FONT_READY_CALLBACKS: RefCell<Vec<FontReadyRegistration>> = const { RefCell::new(Vec::new()) };
15 static REGISTERED_FONT_FALLBACKS: RefCell<Vec<(u32, u32)>> = const { RefCell::new(Vec::new()) };
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
19pub enum FontStyle {
20 #[default]
21 Normal = 0,
22 Italic = 1,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
26pub enum FontWeight {
27 #[default]
28 Regular = 400,
29 Medium = 500,
30 Semibold = 600,
31 Bold = 700,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct FontFaceLoadedEventArgs {
36 pub font: FontFace,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct FontsLoadedEventArgs;
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct FontStackLoadedEventArgs {
44 pub stack: FontStack,
45}
46
47struct FontLoadedRegistration {
48 font_id: u32,
49 callback: Rc<dyn Fn(FontFaceLoadedEventArgs)>,
50}
51
52struct FontReadyRegistration {
53 font_ids: Vec<u32>,
54 callback: Rc<dyn Fn()>,
55}
56
57fn abs_i32(value: i32) -> i32 {
58 if value < 0 {
59 -value
60 } else {
61 value
62 }
63}
64
65fn allocate_dynamic_font_id() -> u32 {
66 NEXT_DYNAMIC_FONT_ID.with(|slot| {
67 let mut next = slot.borrow_mut();
68 let allocated = *next;
69 *next += 1;
70 allocated
71 })
72}
73
74fn push_unique_font_id(font_ids: &mut Vec<u32>, font_id: u32) {
75 if font_id == 0 || font_ids.contains(&font_id) {
76 return;
77 }
78 font_ids.push(font_id);
79}
80
81fn normalize_font_ids(font_ids: &[u32]) -> Vec<u32> {
82 let mut unique = Vec::with_capacity(font_ids.len());
83 for font_id in font_ids {
84 push_unique_font_id(&mut unique, *font_id);
85 }
86 unique
87}
88
89fn register_font_fallback_once(font_id: u32, fallback_font_id: u32) {
90 if font_id == 0 || fallback_font_id == 0 {
91 return;
92 }
93 let already_registered = REGISTERED_FONT_FALLBACKS.with(|pairs| {
94 pairs
95 .borrow()
96 .iter()
97 .any(|(registered_font_id, registered_fallback_id)| {
98 *registered_font_id == font_id && *registered_fallback_id == fallback_font_id
99 })
100 });
101 if already_registered {
102 return;
103 }
104 ui::register_font_fallback(font_id, fallback_font_id);
105 REGISTERED_FONT_FALLBACKS.with(|pairs| {
106 pairs.borrow_mut().push((font_id, fallback_font_id));
107 });
108}
109
110fn are_font_ids_loaded(font_ids: &[u32]) -> bool {
111 font_ids
112 .iter()
113 .all(|font_id| FontFace::is_font_loaded(*font_id))
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct FontFace {
118 id: u32,
119}
120
121impl FontFace {
122 pub(crate) fn new(id: u32) -> Self {
123 Self { id }
124 }
125
126 pub fn load(url: &str) -> Self {
127 Self::load_with_id(url, allocate_dynamic_font_id())
128 }
129
130 pub(crate) fn load_with_id(url: &str, id: u32) -> Self {
131 Self::new(id).load_into(url)
132 }
133
134 pub(crate) fn id(&self) -> u32 {
135 self.id
136 }
137
138 fn load_into(self, url: &str) -> Self {
139 if url.is_empty() {
140 warn("Typography", "FontFace.load() received an empty font URL.");
141 }
142 assets::load_font(self.id, url);
143 self
144 }
145
146 pub fn is_loaded(&self) -> bool {
147 Self::is_font_loaded(self.id)
148 }
149
150 pub(crate) fn is_font_loaded(font_id: u32) -> bool {
151 font_id == 0 || (1..=6).contains(&font_id) || assets::is_font_loaded(font_id)
152 }
153
154 pub fn on_loaded(&self, callback: impl Fn(FontFaceLoadedEventArgs) + 'static) -> Self {
155 Self::when_loaded(self.id, callback);
156 self.clone()
157 }
158
159 pub(crate) fn when_loaded(font_id: u32, callback: impl Fn(FontFaceLoadedEventArgs) + 'static) {
160 if Self::is_font_loaded(font_id) {
161 callback(FontFaceLoadedEventArgs {
162 font: FontFace::new(font_id),
163 });
164 return;
165 }
166 FONT_LOADED_CALLBACKS.with(|registrations| {
167 registrations.borrow_mut().push(FontLoadedRegistration {
168 font_id,
169 callback: Rc::new(callback),
170 });
171 });
172 }
173
174 pub(crate) fn when_fonts_loaded(
175 font_ids: &[u32],
176 callback: impl Fn(FontsLoadedEventArgs) + 'static,
177 ) {
178 let unique_font_ids = normalize_font_ids(font_ids);
179 if are_font_ids_loaded(&unique_font_ids) {
180 callback(FontsLoadedEventArgs);
181 return;
182 }
183 FONT_READY_CALLBACKS.with(|registrations| {
184 registrations.borrow_mut().push(FontReadyRegistration {
185 font_ids: unique_font_ids,
186 callback: Rc::new(move || callback(FontsLoadedEventArgs)),
187 });
188 });
189 }
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
193pub struct FontStack {
194 id: u32,
195 fallback_ids: Vec<u32>,
196}
197
198impl FontStack {
199 pub fn new(face: FontFace) -> Self {
200 Self {
201 id: face.id(),
202 fallback_ids: Vec::new(),
203 }
204 }
205
206 pub(crate) fn from_id(id: u32) -> Self {
207 Self::new(FontFace::new(id))
208 }
209
210 pub fn load(url: &str) -> Self {
211 Self::new(FontFace::load(url))
212 }
213
214 pub(crate) fn id(&self) -> u32 {
215 self.id
216 }
217
218 pub fn fallback_face(mut self, face: FontFace) -> Self {
219 self = self.fallback_id(face.id());
220 self
221 }
222
223 pub fn fallback_stack(mut self, stack: FontStack) -> Self {
224 self = self.fallback_id(stack.id());
225 self
226 }
227
228 pub fn fallback_loaded(self, url: &str) -> Self {
229 self.fallback_loaded_with_id(url, allocate_dynamic_font_id())
230 }
231
232 pub(crate) fn fallback_loaded_with_id(mut self, url: &str, font_id: u32) -> Self {
233 if url.is_empty() {
234 warn(
235 "Typography",
236 "FontStack.fallback_loaded() received an empty font URL.",
237 );
238 }
239 assets::load_font(font_id, url);
240 self = self.fallback_id(font_id);
241 self
242 }
243
244 pub(crate) fn required_font_ids(&self) -> Vec<u32> {
245 let mut font_ids = Vec::with_capacity(1 + self.fallback_ids.len());
246 push_unique_font_id(&mut font_ids, self.id);
247 for fallback_id in &self.fallback_ids {
248 push_unique_font_id(&mut font_ids, *fallback_id);
249 }
250 font_ids
251 }
252
253 pub fn is_loaded(&self) -> bool {
254 are_font_ids_loaded(&self.required_font_ids())
255 }
256
257 pub fn on_loaded(&self, callback: impl Fn(FontStackLoadedEventArgs) + 'static) -> Self {
258 let stack = self.clone();
259 let required = self.required_font_ids();
260 FontFace::when_fonts_loaded(&required, move |_| {
261 callback(FontStackLoadedEventArgs {
262 stack: stack.clone(),
263 });
264 });
265 self.clone()
266 }
267
268 fn fallback_id(mut self, font_id: u32) -> Self {
269 if font_id == 0 || font_id == self.id {
270 warn(
271 "Typography",
272 &format!(
273 "FontStack.fallback() ignored font id {} for stack {}.",
274 font_id, self.id
275 ),
276 );
277 return self;
278 }
279 if self.fallback_ids.contains(&font_id) {
280 return self;
281 }
282 register_font_fallback_once(self.id, font_id);
283 self.fallback_ids.push(font_id);
284 self
285 }
286}
287
288#[derive(Clone, Debug, PartialEq, Eq)]
289pub struct FontFamily {
290 pub regular_stack: FontStack,
291 pub bold_stack: Option<FontStack>,
292 pub italic_stack: Option<FontStack>,
293 pub bold_italic_stack: Option<FontStack>,
294 pub medium_stack: Option<FontStack>,
295 pub medium_italic_stack: Option<FontStack>,
296 pub semibold_stack: Option<FontStack>,
297 pub semibold_italic_stack: Option<FontStack>,
298}
299
300impl FontFamily {
301 #[allow(clippy::too_many_arguments)]
302 pub fn new(
303 regular_stack: FontStack,
304 bold_stack: Option<FontStack>,
305 italic_stack: Option<FontStack>,
306 bold_italic_stack: Option<FontStack>,
307 medium_stack: Option<FontStack>,
308 medium_italic_stack: Option<FontStack>,
309 semibold_stack: Option<FontStack>,
310 semibold_italic_stack: Option<FontStack>,
311 ) -> Self {
312 Self {
313 regular_stack,
314 bold_stack,
315 italic_stack,
316 bold_italic_stack,
317 medium_stack,
318 medium_italic_stack,
319 semibold_stack,
320 semibold_italic_stack,
321 }
322 }
323
324 #[cfg(test)]
325 #[allow(clippy::too_many_arguments)]
326 pub(crate) fn from_ids(
327 regular: u32,
328 bold: u32,
329 italic: u32,
330 bold_italic: u32,
331 medium: u32,
332 medium_italic: u32,
333 semibold: u32,
334 semibold_italic: u32,
335 ) -> Self {
336 Self::new(
337 FontStack::from_id(regular),
338 (bold != 0).then(|| FontStack::from_id(bold)),
339 (italic != 0).then(|| FontStack::from_id(italic)),
340 (bold_italic != 0).then(|| FontStack::from_id(bold_italic)),
341 (medium != 0).then(|| FontStack::from_id(medium)),
342 (medium_italic != 0).then(|| FontStack::from_id(medium_italic)),
343 (semibold != 0).then(|| FontStack::from_id(semibold)),
344 (semibold_italic != 0).then(|| FontStack::from_id(semibold_italic)),
345 )
346 }
347
348 pub fn with_regular_stack(regular: FontStack) -> Self {
349 Self::new(regular, None, None, None, None, None, None, None)
350 }
351
352 pub fn with_regular_face(regular: FontFace) -> Self {
353 Self::with_regular_stack(FontStack::new(regular))
354 }
355
356 pub fn regular_bold_stacks(regular: FontStack, bold: FontStack) -> Self {
357 Self::new(regular, Some(bold), None, None, None, None, None, None)
358 }
359
360 pub(crate) fn italic_stack(mut self, stack: FontStack) -> Self {
361 self.italic_stack = Some(stack);
362 self
363 }
364
365 pub(crate) fn bold_italic_stack(mut self, stack: FontStack) -> Self {
366 self.bold_italic_stack = Some(stack);
367 self
368 }
369
370 pub(crate) fn resolve(&self, weight: FontWeight, style: FontStyle) -> u32 {
371 let target_weight = weight as i32;
372 let candidates = [
373 Some((&self.regular_stack, FontWeight::Regular, FontStyle::Normal)),
374 self.bold_stack
375 .as_ref()
376 .map(|stack| (stack, FontWeight::Bold, FontStyle::Normal)),
377 self.italic_stack
378 .as_ref()
379 .map(|stack| (stack, FontWeight::Regular, FontStyle::Italic)),
380 self.bold_italic_stack
381 .as_ref()
382 .map(|stack| (stack, FontWeight::Bold, FontStyle::Italic)),
383 self.medium_stack
384 .as_ref()
385 .map(|stack| (stack, FontWeight::Medium, FontStyle::Normal)),
386 self.medium_italic_stack
387 .as_ref()
388 .map(|stack| (stack, FontWeight::Medium, FontStyle::Italic)),
389 self.semibold_stack
390 .as_ref()
391 .map(|stack| (stack, FontWeight::Semibold, FontStyle::Normal)),
392 self.semibold_italic_stack
393 .as_ref()
394 .map(|stack| (stack, FontWeight::Semibold, FontStyle::Italic)),
395 ];
396
397 let mut best_id = 0;
398 let mut best_score = MAX_FONT_SCORE;
399 for (stack, candidate_weight, candidate_style) in candidates.into_iter().flatten() {
400 let score = Self::score_candidate(
401 stack.id(),
402 candidate_weight,
403 candidate_style,
404 target_weight,
405 style,
406 );
407 if score < best_score {
408 best_score = score;
409 best_id = stack.id();
410 }
411 }
412 if best_id == 0 {
413 warn(
414 "Typography",
415 &format!(
416 "FontFamily.resolve() could not resolve a font face for weight {} and style {}; the text will use font id 0.",
417 weight as i32,
418 style as u32,
419 ),
420 );
421 }
422 best_id
423 }
424
425 fn score_candidate(
426 font_id: u32,
427 weight: FontWeight,
428 style: FontStyle,
429 target_weight: i32,
430 target_style: FontStyle,
431 ) -> i32 {
432 if font_id == 0 {
433 return MAX_FONT_SCORE;
434 }
435 let mut score = abs_i32(weight as i32 - target_weight);
436 if style != target_style {
437 score += STYLE_MISMATCH_PENALTY;
438 }
439 score
440 }
441}
442
443pub(crate) fn notify_font_loaded(font_id: u32) {
444 FONT_LOADED_CALLBACKS.with(|registrations| {
445 let callbacks: Vec<Rc<dyn Fn(FontFaceLoadedEventArgs)>> = registrations
446 .borrow()
447 .iter()
448 .filter(|registration| registration.font_id == font_id)
449 .map(|registration| registration.callback.clone())
450 .collect();
451 for callback in callbacks {
452 callback(FontFaceLoadedEventArgs {
453 font: FontFace::new(font_id),
454 });
455 }
456 });
457
458 FONT_READY_CALLBACKS.with(|registrations| {
459 let mut registrations = registrations.borrow_mut();
460 let mut ready_callbacks = Vec::new();
461 registrations.retain(|registration| {
462 if are_font_ids_loaded(®istration.font_ids) {
463 ready_callbacks.push(registration.callback.clone());
464 false
465 } else {
466 true
467 }
468 });
469 drop(registrations);
470 for callback in ready_callbacks {
471 callback();
472 }
473 });
474}
475
476#[cfg(test)]
477mod tests {
478 use super::{notify_font_loaded, FontFace, FontFamily, FontStack, FontStyle, FontWeight};
479 use crate::assets;
480 use std::cell::Cell;
481 use std::rc::Rc;
482
483 #[test]
484 fn font_face_on_loaded_waits_for_bridge_callback() {
485 assets::test_reset();
486 let fired = Rc::new(Cell::new(0));
487 FontFace::new(1024).on_loaded({
488 let fired = fired.clone();
489 move |_| fired.set(fired.get() + 1)
490 });
491 assert_eq!(fired.get(), 0);
492 assets::on_font_loaded(1024);
493 notify_font_loaded(1024);
494 assert_eq!(fired.get(), 1);
495 }
496
497 #[test]
498 fn built_in_font_faces_are_preloaded_like_fui_as() {
499 assets::test_reset();
500 for font_id in 1..=6 {
501 assert!(FontFace::new(font_id).is_loaded());
502 }
503 }
504
505 #[test]
506 fn font_stack_reports_required_font_ids() {
507 let stack = FontStack::from_id(1)
508 .fallback_face(FontFace::new(3))
509 .fallback_stack(FontStack::from_id(4));
510 assert_eq!(stack.required_font_ids(), vec![1, 3, 4]);
511 }
512
513 #[test]
514 fn font_family_resolves_best_matching_face() {
515 let family = FontFamily::from_ids(1, 2, 5, 6, 9, 10, 11, 12);
516 assert_eq!(family.resolve(FontWeight::Bold, FontStyle::Italic), 6);
517 assert_eq!(family.resolve(FontWeight::Semibold, FontStyle::Normal), 11);
518 assert_eq!(family.resolve(FontWeight::Medium, FontStyle::Italic), 10);
519 }
520}