Skip to main content

fui/
typography.rs

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(&registration.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}