1use std::cmp::Reverse;
2use std::collections::BTreeMap;
3
4use unicode_segmentation::UnicodeSegmentation;
5
6use crate::text::{
7 Font, FontFlags, FontInfo, FontStretch, FontStyle, FontVariant, FontWeight,
8 StandardAxes, is_default_ignorable,
9};
10
11#[derive(Debug, Default, Clone, Hash)]
13pub struct FontBook {
14 families: BTreeMap<String, Vec<usize>>,
16 infos: Vec<FontInfo>,
18}
19
20impl FontBook {
21 pub fn new() -> Self {
23 Self { families: BTreeMap::new(), infos: vec![] }
24 }
25
26 pub fn from_infos(infos: impl IntoIterator<Item = FontInfo>) -> Self {
28 let mut book = Self::new();
29 for info in infos {
30 book.push(info);
31 }
32 book
33 }
34
35 pub fn from_fonts<'a>(fonts: impl IntoIterator<Item = &'a Font>) -> Self {
37 Self::from_infos(fonts.into_iter().map(|font| font.info().clone()))
38 }
39
40 pub fn push(&mut self, info: FontInfo) {
42 let index = self.infos.len();
43 let family = info.family.to_lowercase();
44 self.families.entry(family).or_default().push(index);
45 self.infos.push(info);
46 }
47
48 pub fn info(&self, index: usize) -> Option<&FontInfo> {
50 self.infos.get(index)
51 }
52
53 pub fn contains_family(&self, family: &str) -> bool {
55 self.families.contains_key(family)
56 }
57
58 pub fn families(
61 &self,
62 ) -> impl Iterator<Item = (&str, impl Iterator<Item = usize>)> + '_ {
63 self.families.values().map(|ids| {
66 let family = self.infos[ids[0]].family.as_str();
67 (family, ids.iter().copied())
68 })
69 }
70
71 pub fn select(&self, family: &str, variant: FontVariant) -> Option<usize> {
76 let ids = self.families.get(family)?;
77 self.find_best_variant(None, variant, ids.iter().copied())
78 }
79
80 pub fn select_family(&self, family: &str) -> impl Iterator<Item = usize> + '_ {
82 self.families
83 .get(family)
84 .map(|vec| vec.as_slice())
85 .unwrap_or_default()
86 .iter()
87 .copied()
88 }
89
90 pub fn select_fallback(
95 &self,
96 like: Option<&FontInfo>,
97 variant: FontVariant,
98 text: &str,
99 ) -> Option<usize> {
100 let c = text
103 .chars()
104 .find(|&c| !c.is_whitespace() && !is_default_ignorable(c))?;
105
106 let ids = self
107 .infos
108 .iter()
109 .enumerate()
110 .filter(|(_, info)| info.coverage.contains(c as u32))
111 .map(|(index, _)| index);
112
113 self.find_best_variant(like, variant, ids)
115 }
116
117 fn find_best_variant(
140 &self,
141 like: Option<&FontInfo>,
142 variant: FontVariant,
143 ids: impl IntoIterator<Item = usize>,
144 ) -> Option<usize> {
145 let mut best = None;
146 let mut best_score = None;
147
148 for id in ids {
149 let current = &self.infos[id];
150 let score = (
151 like.map(|like| similarity(current, like)),
152 Reverse(distance(current, variant)),
153 current.flags.contains(FontFlags::VARIABLE),
154 );
155
156 if best_score.is_none_or(|b| score > b) {
157 best = Some(id);
158 best_score = Some(score);
159 }
160 }
161
162 best
163 }
164}
165
166fn similarity(left: &FontInfo, right: &FontInfo) -> impl Ord + Copy {
169 (
170 left.flags.contains(FontFlags::MONOSPACE)
173 == right.flags.contains(FontFlags::MONOSPACE),
174 left.flags.contains(FontFlags::SERIF) == right.flags.contains(FontFlags::SERIF),
175 shared_prefix_words(&left.family, &right.family),
180 Reverse(left.family.len()),
184 )
185}
186
187fn distance(info: &FontInfo, variant: FontVariant) -> impl Ord + Copy {
193 let axes = StandardAxes::parse(&info.axes);
199
200 let style_distance = {
201 let mut dist = info.variant.style.distance(variant.style);
202 if axes.ital.is_some() {
203 dist = dist.min(FontStyle::Italic.distance(variant.style));
204 }
205 if axes.slnt.is_some() {
206 dist = dist.min(FontStyle::Oblique.distance(variant.style));
207 }
208 dist
209 };
210
211 let stretch_distance = match axes.wdth {
212 Some(axis) => {
213 axis.distance(variant.stretch, FontStretch::from_wdth, FontStretch::distance)
214 }
215 None => info.variant.stretch.distance(variant.stretch),
216 };
217
218 let weight_distance = match axes.wght {
219 Some(axis) => {
220 axis.distance(variant.weight, FontWeight::from_wght, FontWeight::distance)
221 }
222 None => info.variant.weight.distance(variant.weight),
223 };
224
225 (style_distance, stretch_distance, weight_distance)
226}
227
228fn shared_prefix_words(left: &str, right: &str) -> usize {
230 left.unicode_words()
231 .zip(right.unicode_words())
232 .take_while(|(l, r)| l == r)
233 .count()
234}
235
236#[cfg(test)]
237mod tests {
238 use crate::layout::Ratio;
239 use crate::text::{
240 AxisValue, Coverage, FontAxis, FontBook, FontFlags, FontInfo, FontStretch,
241 FontStyle, FontVariant, FontWeight, Tag,
242 };
243
244 #[test]
245 fn test_find_best_variant() {
246 use FontStyle::*;
247
248 let s = [
249 info("s0", Normal, 400, 100.0, &[]),
250 info("s1", Normal, 500, 100.0, &[]),
251 info("s2", Normal, 800, 100.0, &[]),
252 info("s3", Italic, 400, 100.0, &[]),
253 info("s4", Italic, 800, 100.0, &[]),
254 info("s5", Oblique, 800, 100.0, &[]),
255 info("s6", Normal, 400, 110.0, &[]),
256 ];
257
258 #[rustfmt::skip]
259 let v = [
260 info("v0", Normal, 400, 100.0, &[("wght", 200.0, 700.0), ("ital", 0.0, 1.0)]),
261 info("v1", Normal, 400, 100.0, &[("slnt", -40.0, 40.0), ("wdth", 70.0, 120.0)]),
262 ];
263
264 let book = FontBook::from_infos(s.iter().chain(&v).cloned());
265 let count = s.len() + v.len();
266 let pick = |style, weight, stretch| {
267 let target = variant(style, weight, stretch);
268 let id = book.find_best_variant(None, target, 0..count).unwrap();
269 book.info(id).unwrap()
270 };
271
272 assert_eq!(pick(Normal, 100, 100.0), &v[0]);
274 assert_eq!(pick(Normal, 200, 100.0), &v[0]);
275 assert_eq!(pick(Normal, 500, 100.0), &v[0]);
276 assert_eq!(pick(Normal, 730, 100.0), &v[0]);
277 assert_eq!(pick(Italic, 400, 100.0), &v[0]);
278 assert_eq!(pick(Oblique, 400, 100.0), &v[1]);
279 assert_eq!(pick(Normal, 400, 120.0), &v[1]);
280 assert_eq!(pick(Normal, 400, 130.0), &v[1]);
281
282 assert_eq!(pick(Normal, 760, 100.0), &s[2]);
284 assert_eq!(pick(Normal, 800, 100.0), &s[2]);
285 assert_eq!(pick(Normal, 1000, 100.0), &s[2]);
286 assert_eq!(pick(Italic, 800, 110.0), &s[4]);
287 assert_eq!(pick(Oblique, 800, 100.0), &s[5]);
288 assert_eq!(pick(Oblique, 800, 100.0), &s[5]);
289 }
290
291 fn info(
292 family: &str,
293 style: FontStyle,
294 weight: u16,
295 stretch: f64,
296 axes: &[(&str, f32, f32)],
297 ) -> FontInfo {
298 FontInfo {
299 family: family.into(),
300 variant: variant(style, weight, stretch),
301 flags: if axes.is_empty() { FontFlags::empty() } else { FontFlags::VARIABLE },
302 axes: axes
303 .iter()
304 .map(|&(t, min, max)| FontAxis {
305 tag: Tag::from_bytes_lossy(t.as_bytes()),
306 min: AxisValue(min),
307 max: AxisValue(max),
308 default: AxisValue((min + max) / 2.0),
309 })
310 .collect(),
311 coverage: Coverage::from_vec(vec![]),
312 }
313 }
314
315 fn variant(style: FontStyle, weight: u16, stretch: f64) -> FontVariant {
316 FontVariant {
317 style,
318 weight: FontWeight::from_number(weight),
319 stretch: FontStretch::from_ratio(Ratio::new(stretch / 100.0)),
320 }
321 }
322}