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}