Skip to main content

pfx_text/
fallback.rs

1use std::collections::BTreeMap;
2use std::ops::Range;
3use std::sync::Arc;
4
5use cosmic_text::{Attrs, Family, Font, LayoutRun, Metrics, fontdb};
6use unicode_script::Script;
7use unicode_segmentation::UnicodeSegmentation;
8
9use crate::{Error, Face, NumberForms, Span, TextEngine, face_attrs};
10
11pub(crate) struct Explicit;
12
13impl cosmic_text::Fallback for Explicit {
14    fn common_fallback(&self) -> &[&'static str] {
15        &[]
16    }
17
18    fn forbidden_fallback(&self) -> &[&'static str] {
19        &[]
20    }
21
22    fn script_fallback(&self, _: Script, _: &str) -> &[&'static str] {
23        &[]
24    }
25}
26
27#[derive(Clone, Debug, PartialEq)]
28pub struct Fallback {
29    pub family: String,
30    pub weight: Option<u16>,
31    pub scale: Option<f32>,
32}
33
34impl Fallback {
35    pub fn new(family: &str) -> Self {
36        Self {
37            family: family.into(),
38            weight: None,
39            scale: None,
40        }
41    }
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct Pick {
46    pub range: Range<usize>,
47    pub family: String,
48    pub weight: u16,
49    pub scale: f32,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct FaceMetrics {
54    pub ascent: f32,
55    pub descent: f32,
56    pub cap_height: f32,
57    pub x_height: f32,
58}
59
60#[derive(Default)]
61pub(crate) struct Fallbacks {
62    lists: BTreeMap<String, BTreeMap<Option<u16>, Vec<Fallback>>>,
63}
64
65impl Fallbacks {
66    fn list(&self, family: &str, weight: u16) -> Option<&[Fallback]> {
67        let lists = self.lists.get(family)?;
68        lists
69            .get(&Some(weight))
70            .or_else(|| lists.get(&None))
71            .map(Vec::as_slice)
72    }
73}
74
75#[derive(Clone)]
76struct Choice {
77    id: fontdb::ID,
78    family: String,
79    weight: u16,
80    scale: f32,
81}
82
83#[derive(Clone, Copy, Debug)]
84pub(crate) struct Piece {
85    pub span: usize,
86    pub fallback: bool,
87    pub scale: f32,
88    pub ascent: f32,
89    pub descent: f32,
90}
91
92pub(crate) struct Laid {
93    pub texts: Vec<(usize, Range<usize>)>,
94    pub faces: Vec<Face>,
95    pub pieces: Vec<Piece>,
96}
97
98impl Laid {
99    pub fn attrs(&self, scale: f32, forms: Option<NumberForms>) -> Vec<Attrs<'_>> {
100        self.faces
101            .iter()
102            .zip(&self.pieces)
103            .enumerate()
104            .map(|(index, (face, piece))| {
105                let attrs = face_attrs(face, scale, index, forms);
106                if !piece.fallback {
107                    attrs
108                } else {
109                    attrs.metrics(Metrics::new(
110                        face.size * scale * piece.scale,
111                        face.line * scale,
112                    ))
113                }
114            })
115            .collect()
116    }
117}
118
119fn ignorable(c: char) -> bool {
120    matches!(c, '\u{200c}' | '\u{200d}' | '\u{fe00}'..='\u{fe0f}' | '\u{e0100}'..='\u{e01ef}')
121        || c.is_control()
122}
123
124fn em_metrics(font: &Font) -> (f32, f32) {
125    let metrics = font.metrics();
126    let em = f32::from(metrics.units_per_em);
127    (metrics.ascent / em, -metrics.descent / em)
128}
129
130fn swash_metrics(font: &Font, weight: u16) -> FaceMetrics {
131    let swash = font.as_swash();
132    let coords = swash
133        .variations()
134        .normalized_coords([swash::Setting {
135            tag: swash::Tag::from_be_bytes(*b"wght"),
136            value: f32::from(weight),
137        }])
138        .collect::<Vec<_>>();
139    let metrics = swash.metrics(&coords);
140    let em = f32::from(metrics.units_per_em);
141    FaceMetrics {
142        ascent: metrics.ascent / em,
143        descent: metrics.descent / em,
144        cap_height: metrics.cap_height / em,
145        x_height: metrics.x_height / em,
146    }
147}
148
149pub fn matched_scale(primary: FaceMetrics, fallback: FaceMetrics) -> f32 {
150    let ratio = if primary.cap_height > 0.0 && fallback.cap_height > 0.0 {
151        primary.cap_height / fallback.cap_height
152    } else if primary.x_height > 0.0 && fallback.x_height > 0.0 {
153        primary.x_height / fallback.x_height
154    } else {
155        1.0
156    };
157    (ratio * 10_000.0).round() / 10_000.0
158}
159
160impl TextEngine {
161    pub fn set_fallbacks(
162        &mut self,
163        primary: &str,
164        weight: Option<u16>,
165        faces: &[Fallback],
166    ) -> Result<(), Error> {
167        let known = |family: &str| {
168            self.fonts
169                .db()
170                .faces()
171                .any(|face| face.families.iter().any(|(name, _)| name == family))
172        };
173        if !known(primary) || faces.iter().any(|face| !known(&face.family)) {
174            return Err(Error::InvalidFont);
175        }
176        if weight.is_some_and(|w| !(1..=1000).contains(&w))
177            || faces.iter().any(|face| {
178                face.weight.is_some_and(|w| !(1..=1000).contains(&w))
179                    || face.scale.is_some_and(|s| !s.is_finite() || s <= 0.0)
180            })
181        {
182            return Err(Error::InvalidStyle);
183        }
184        let lists = self.fallbacks.lists.entry(primary.into()).or_default();
185        if faces.is_empty() {
186            lists.remove(&weight);
187        } else {
188            lists.insert(weight, faces.to_vec());
189        }
190        if lists.is_empty() {
191            self.fallbacks.lists.remove(primary);
192        }
193        self.blocks.clear();
194        Ok(())
195    }
196
197    pub fn families(&self) -> Vec<String> {
198        let mut families = self
199            .fonts
200            .db()
201            .faces()
202            .filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
203            .collect::<Vec<_>>();
204        families.sort();
205        families.dedup();
206        families
207    }
208
209    pub fn face_metrics(&mut self, family: &str, weight: u16) -> Option<FaceMetrics> {
210        let id = self.query(family, weight, false)?;
211        let weight = self.resolved_weight(id, weight);
212        let font = self.fonts.get_font(id, fontdb::Weight(weight))?;
213        Some(swash_metrics(&font, weight))
214    }
215
216    pub fn picks(&mut self, span: &Span) -> Vec<Pick> {
217        let laid = self.split(std::slice::from_ref(span));
218        laid.texts
219            .iter()
220            .zip(&laid.faces)
221            .zip(&laid.pieces)
222            .map(|(((_, range), face), piece)| Pick {
223                range: range.clone(),
224                family: face.family.clone(),
225                weight: face.weight,
226                scale: piece.scale,
227            })
228            .collect()
229    }
230
231    fn query(&self, family: &str, weight: u16, italic: bool) -> Option<fontdb::ID> {
232        self.fonts.db().query(&fontdb::Query {
233            families: &[Family::Name(family)],
234            weight: fontdb::Weight(weight),
235            stretch: fontdb::Stretch::Normal,
236            style: if italic {
237                fontdb::Style::Italic
238            } else {
239                fontdb::Style::Normal
240            },
241        })
242    }
243
244    fn resolved_weight(&mut self, id: fontdb::ID, weight: u16) -> u16 {
245        let face_weight = self
246            .fonts
247            .db()
248            .face(id)
249            .map_or(weight, |face| face.weight.0);
250        let Some(font) = self.fonts.get_font(id, fontdb::Weight(face_weight)) else {
251            return weight;
252        };
253        let axis = font
254            .as_swash()
255            .variations()
256            .find_by_tag(swash::Tag::from_be_bytes(*b"wght"))
257            .map(|axis| (axis.min_value(), axis.max_value()));
258        match axis {
259            Some((min, max)) => f32::from(weight).clamp(min, max).round() as u16,
260            None => face_weight,
261        }
262    }
263
264    fn choices(&mut self, face: &Face) -> Option<Vec<Choice>> {
265        let list = self.fallbacks.list(&face.family, face.weight)?.to_vec();
266        let primary = self.query(&face.family, face.weight, face.italic)?;
267        let primary_metrics = {
268            let weight = self.resolved_weight(primary, face.weight);
269            let font = self.fonts.get_font(primary, fontdb::Weight(weight))?;
270            swash_metrics(&font, weight)
271        };
272        let mut choices = vec![Choice {
273            id: primary,
274            family: face.family.clone(),
275            weight: face.weight,
276            scale: 1.0,
277        }];
278        for fallback in list {
279            let wanted = fallback.weight.unwrap_or(face.weight);
280            let Some(id) = self.query(&fallback.family, wanted, face.italic) else {
281                continue;
282            };
283            let weight = self.resolved_weight(id, wanted);
284            let Some(font) = self.fonts.get_font(id, fontdb::Weight(weight)) else {
285                continue;
286            };
287            let scale = fallback
288                .scale
289                .unwrap_or_else(|| matched_scale(primary_metrics, swash_metrics(&font, weight)));
290            choices.push(Choice {
291                id,
292                family: fallback.family,
293                weight,
294                scale,
295            });
296        }
297        Some(choices)
298    }
299
300    fn fonts_of(&mut self, choices: &[Choice]) -> Vec<Option<Arc<Font>>> {
301        choices
302            .iter()
303            .map(|choice| {
304                self.fonts
305                    .get_font(choice.id, fontdb::Weight(choice.weight))
306            })
307            .collect()
308    }
309
310    pub(crate) fn split(&mut self, spans: &[Span]) -> Laid {
311        let mut laid = Laid {
312            texts: Vec::new(),
313            faces: Vec::new(),
314            pieces: Vec::new(),
315        };
316        for (index, span) in spans.iter().enumerate() {
317            let whole = |laid: &mut Laid| {
318                laid.texts.push((index, 0..span.text.len()));
319                laid.faces.push(span.face.clone());
320                laid.pieces.push(Piece {
321                    span: index,
322                    fallback: false,
323                    scale: 1.0,
324                    ascent: 0.0,
325                    descent: 0.0,
326                });
327            };
328            let Some(choices) = self.choices(&span.face) else {
329                whole(&mut laid);
330                continue;
331            };
332            let fonts = self.fonts_of(&choices);
333            let Some(primary) = fonts[0].as_ref() else {
334                whole(&mut laid);
335                continue;
336            };
337            let (ascent, descent) = em_metrics(primary);
338            let charmaps = fonts
339                .iter()
340                .map(|font| font.as_ref().map(|font| font.as_swash().charmap()))
341                .collect::<Vec<_>>();
342            let covers = |slot: usize, text: &str| {
343                charmaps[slot].as_ref().is_some_and(|map| {
344                    text.chars()
345                        .filter(|&c| !ignorable(c))
346                        .all(|c| map.map(c) != 0)
347                })
348            };
349            let base_covered = |slot: usize, text: &str| {
350                charmaps[slot].as_ref().is_some_and(|map| {
351                    text.chars()
352                        .find(|&c| !ignorable(c))
353                        .is_some_and(|c| map.map(c) != 0)
354                })
355            };
356            let mut runs: Vec<(usize, Range<usize>)> = Vec::new();
357            for (start, grapheme) in span.text.grapheme_indices(true) {
358                let end = start + grapheme.len();
359                let slot = if grapheme.chars().all(ignorable) {
360                    runs.last().map_or(0, |run| run.0)
361                } else if covers(0, grapheme) {
362                    0
363                } else {
364                    (1..choices.len())
365                        .find(|&slot| covers(slot, grapheme))
366                        .or_else(|| (1..choices.len()).find(|&slot| base_covered(slot, grapheme)))
367                        .unwrap_or(0)
368                };
369                match runs.last_mut() {
370                    Some(run) if run.0 == slot => run.1.end = end,
371                    _ => runs.push((slot, start..end)),
372                }
373            }
374            if runs.is_empty() {
375                runs.push((0, 0..0));
376            }
377            for (slot, range) in runs {
378                let choice = &choices[slot];
379                let face = if slot == 0 {
380                    span.face.clone()
381                } else {
382                    Face {
383                        family: choice.family.clone(),
384                        weight: choice.weight,
385                        spacing: span.face.spacing / choice.scale,
386                        ..span.face.clone()
387                    }
388                };
389                laid.texts.push((index, range));
390                laid.faces.push(face);
391                laid.pieces.push(Piece {
392                    span: index,
393                    fallback: slot > 0,
394                    scale: if slot == 0 { 1.0 } else { choice.scale },
395                    ascent,
396                    descent,
397                });
398            }
399        }
400        laid
401    }
402
403    pub(crate) fn line_baselines<'a>(
404        &mut self,
405        runs: impl Iterator<Item = LayoutRun<'a>>,
406        pieces: &[Piece],
407    ) -> Vec<f32> {
408        runs.map(|run| {
409            let fallback = |metadata: usize| pieces.get(metadata).filter(|piece| piece.fallback);
410            if !run
411                .glyphs
412                .iter()
413                .any(|glyph| fallback(glyph.metadata).is_some())
414            {
415                return run.line_y;
416            }
417            let mut ascent = 0.0f32;
418            let mut descent = 0.0f32;
419            for glyph in run.glyphs {
420                let (size, a, d) = match fallback(glyph.metadata) {
421                    Some(piece) => (glyph.font_size / piece.scale, piece.ascent, piece.descent),
422                    None => match self.fonts.get_font(glyph.font_id, glyph.font_weight) {
423                        Some(font) => {
424                            let (a, d) = em_metrics(&font);
425                            (glyph.font_size, a, d)
426                        }
427                        None => continue,
428                    },
429                };
430                ascent = ascent.max(size * a);
431                descent = descent.max(size * d);
432            }
433            run.line_top + (run.line_height - (ascent + descent)) / 2.0 + ascent
434        })
435        .collect()
436    }
437}