1use font_awl::FontProvider;
2use rapidhash::{HashMapExt, RapidHashMap, fast::RapidHasher};
3use skrifa::MetadataProvider;
4use skrifa::outline::OutlinePen;
5use std::sync::OnceLock;
6
7pub mod fallback;
8pub mod fallback_data;
9pub mod unresolved;
10
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::{
13 collections::{BTreeMap, HashMap, HashSet},
14 hash::{Hash, Hasher},
15 sync::{Arc, Mutex},
16};
17use unicode_segmentation::UnicodeSegmentation;
18
19static FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
20static FONT_GENERATION: AtomicU64 = AtomicU64::new(0);
21static FALLBACK_DIRTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
22
23pub fn begin_frame() {
24 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
25}
26
27pub fn current_frame() -> u64 {
28 FRAME_COUNTER.load(Ordering::Relaxed)
29}
30
31pub fn font_generation() -> u64 {
32 FONT_GENERATION.load(Ordering::Relaxed)
33}
34
35pub fn take_fallback_dirty() -> bool {
36 FALLBACK_DIRTY.swap(false, Ordering::SeqCst)
39}
40
41const GLYPH_CACHE_CAP: usize = 4096;
42const GLYPH_CACHE_BYTES_CAP: usize = 64 * 1024 * 1024;
43const MAX_RASTER_BYTES: usize = 16 * 1024 * 1024;
44const WRAP_CACHE_CAP: usize = 1024;
45const ELLIP_CACHE_CAP: usize = 2048;
46const SHAPE_CACHE_CAP: usize = 1024;
47const LEGACY_SHAPE_CACHE_CAP: usize = 512;
48const RASTER_MIN_PX: f32 = 0.25;
49const RASTER_MAX_PX: f32 = 4096.0;
50const RASTER_QUANTUM: f32 = 1.0 / 4.0;
51const MAX_RASTER_DIMENSION: u32 = 4_096;
52const VERTICAL_METRICS_CACHE_CAP: usize = 256;
53const OUTLINE_CACHE_CAP: usize = 2048;
54
55#[derive(Hash, Eq, PartialEq)]
56struct MetricsCacheKey {
57 text: Arc<str>,
58 px_bits: u32,
59 family: Option<Arc<str>>,
60 font_weight: u16,
61 font_style: u8,
62 letter_spacing_bits: u32,
63 variation: Option<Arc<str>>,
64 generation: u64,
65}
66
67#[derive(Hash, Eq, PartialEq)]
68struct WrapCacheKey {
69 text: Arc<str>,
70 px_bits: u32,
71 max_width_bits: u32,
72 max_lines: Option<usize>,
73 family: Option<Arc<str>>,
74 soft_wrap: bool,
75 font_weight: u16,
76 font_style: u8,
77 letter_spacing_bits: u32,
78 variation: Option<Arc<str>>,
79 generation: u64,
80}
81
82#[derive(Hash, Eq, PartialEq)]
83struct EllipCacheKey {
84 text: Arc<str>,
85 px_bits: u32,
86 max_width_bits: u32,
87 family: Option<Arc<str>>,
88 font_weight: u16,
89 font_style: u8,
90 letter_spacing_bits: u32,
91 variation: Option<Arc<str>>,
92 generation: u64,
93}
94
95static METRICS_LRU: OnceLock<Mutex<Lru<MetricsCacheKey, Arc<TextMetrics>>>> = OnceLock::new();
96fn metrics_cache() -> &'static Mutex<Lru<MetricsCacheKey, Arc<TextMetrics>>> {
97 METRICS_LRU.get_or_init(|| Mutex::new(Lru::new(4096)))
98}
99
100struct LruNode<K, V> {
101 key: Arc<K>,
102 value: V,
103 newer: Option<usize>,
104 older: Option<usize>,
105}
106
107struct Lru<K, V> {
108 map: RapidHashMap<Arc<K>, usize>,
109 nodes: Vec<Option<LruNode<K, V>>>,
110 free: Vec<usize>,
111 head: Option<usize>,
112 tail: Option<usize>,
113 cap: usize,
114 len: usize,
115}
116
117impl<K: std::hash::Hash + Eq, V> Lru<K, V> {
118 fn new(cap: usize) -> Self {
119 Self {
120 map: RapidHashMap::new(),
121 nodes: Vec::new(),
122 free: Vec::new(),
123 head: None,
124 tail: None,
125 cap,
126 len: 0,
127 }
128 }
129
130 fn node(&self, index: usize) -> Option<&LruNode<K, V>> {
131 self.nodes.get(index).and_then(Option::as_ref)
132 }
133
134 fn node_mut(&mut self, index: usize) -> Option<&mut LruNode<K, V>> {
135 self.nodes.get_mut(index).and_then(Option::as_mut)
136 }
137
138 fn unlink(&mut self, index: usize) {
139 let Some((newer, older)) = self.node(index).map(|node| (node.newer, node.older)) else {
140 return;
141 };
142 if let Some(newer) = newer {
143 if let Some(node) = self.node_mut(newer) {
144 node.older = older;
145 }
146 } else {
147 self.head = older;
148 }
149 if let Some(older) = older {
150 if let Some(node) = self.node_mut(older) {
151 node.newer = newer;
152 }
153 } else {
154 self.tail = newer;
155 }
156 if let Some(node) = self.node_mut(index) {
157 node.newer = None;
158 node.older = None;
159 }
160 }
161
162 fn link_head(&mut self, index: usize) {
163 let head = self.head;
164 if let Some(node) = self.node_mut(index) {
165 node.newer = None;
166 node.older = head;
167 }
168 if let Some(head) = self.head
169 && let Some(node) = self.node_mut(head)
170 {
171 node.newer = Some(index);
172 }
173 self.head = Some(index);
174 if self.tail.is_none() {
175 self.tail = Some(index);
176 }
177 }
178
179 fn touch(&mut self, index: usize) {
180 if self.head == Some(index) {
181 return;
182 }
183 self.unlink(index);
184 self.link_head(index);
185 }
186
187 fn get(&mut self, key: &K) -> Option<&V> {
188 let index = *self.map.get(key)?;
189 self.touch(index);
190 self.node(index).map(|node| &node.value)
191 }
192
193 fn get_mut(&mut self, key: &K) -> Option<&mut V> {
194 let index = *self.map.get(key)?;
195 self.touch(index);
196 self.node_mut(index).map(|node| &mut node.value)
197 }
198
199 fn put(&mut self, key: K, value: V) {
200 if let Some(index) = self.map.get(&key).copied() {
201 if let Some(node) = self.node_mut(index) {
202 node.value = value;
203 }
204 self.touch(index);
205 return;
206 }
207 if self.cap == 0 {
208 return;
209 }
210 while self.len >= self.cap
211 && let Some(victim) = self.tail
212 {
213 self.remove_index(victim);
214 }
215 let key = Arc::new(key);
216 let index = if let Some(index) = self.free.pop() {
217 self.nodes[index] = Some(LruNode {
218 key: key.clone(),
219 value,
220 newer: None,
221 older: None,
222 });
223 index
224 } else {
225 let index = self.nodes.len();
226 self.nodes.push(Some(LruNode {
227 key: key.clone(),
228 value,
229 newer: None,
230 older: None,
231 }));
232 index
233 };
234 self.map.insert(key, index);
235 self.link_head(index);
236 self.len += 1;
237 }
238
239 fn remove_index(&mut self, index: usize) {
240 let Some(node) = self.nodes.get_mut(index).and_then(Option::as_mut) else {
241 return;
242 };
243 let key = node.key.clone();
244 self.unlink(index);
245 self.nodes[index] = None;
246 self.map.remove(&key);
247 self.free.push(index);
248 self.len = self.len.saturating_sub(1);
249 }
250
251 fn clear_both(&mut self) {
252 self.map.clear();
253 self.nodes.clear();
254 self.free.clear();
255 self.head = None;
256 self.tail = None;
257 self.len = 0;
258 }
259}
260
261static WRAP_LRU: OnceLock<Mutex<Lru<WrapCacheKey, Arc<(Vec<String>, bool)>>>> = OnceLock::new();
262
263static WRAP_RANGES_LRU: OnceLock<Mutex<Lru<WrapCacheKey, Arc<(Vec<(usize, usize)>, bool)>>>> =
264 OnceLock::new();
265
266static ELLIP_LRU: OnceLock<Mutex<Lru<EllipCacheKey, Arc<String>>>> = OnceLock::new();
267
268fn wrap_cache() -> &'static Mutex<Lru<WrapCacheKey, Arc<(Vec<String>, bool)>>> {
269 WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
270}
271
272fn wrap_ranges_cache() -> &'static Mutex<Lru<WrapCacheKey, Arc<(Vec<(usize, usize)>, bool)>>> {
273 WRAP_RANGES_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
274}
275
276fn ellip_cache() -> &'static Mutex<Lru<EllipCacheKey, Arc<String>>> {
277 ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
278}
279
280#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
281pub struct GlyphKey(pub u64);
282
283#[derive(Clone, Debug, PartialEq, Eq, Hash)]
288pub struct CacheKey {
289 pub font_id: u64,
290 pub glyph_id: u32,
291 pub font_size_bits: u32,
292 pub variation: Option<Arc<str>>,
293}
294
295pub fn canonical_variation(settings: Option<&str>) -> Option<Arc<str>> {
300 let settings = settings?;
301 let mut parsed: Vec<(u32, u32)> = parley::setting::FontVariation::parse_css_list(settings)
302 .filter_map(Result::ok)
303 .map(|setting| {
304 (
305 u32::from_be_bytes(setting.tag.to_bytes()),
306 setting.value.to_bits(),
307 )
308 })
309 .collect();
310 if parsed.is_empty() {
311 return None;
312 }
313 parsed.sort_by_key(|(tag, _)| *tag);
316 let mut merged: Vec<(u32, u32)> = Vec::with_capacity(parsed.len());
317 for entry in parsed {
318 match merged.last_mut() {
319 Some(last) if last.0 == entry.0 => *last = entry,
320 _ => merged.push(entry),
321 }
322 }
323 let mut canonical = String::with_capacity(merged.len() * 16);
324 for (tag, bits) in merged {
325 canonical.push_str(&format!("{tag:08x}{bits:08x}"));
326 }
327 Some(Arc::from(canonical.as_str()))
328}
329
330const VARIATION_ENTRY_CHARS: usize = 16;
333
334type VariationKey = (u64, u32, u32, Option<Arc<str>>);
337
338type OutlineKey = (u64, u32, Option<Arc<str>>);
341
342pub(crate) fn decode_variation(canonical: Option<&str>) -> Vec<(skrifa::Tag, f32)> {
349 let Some(canonical) = canonical else {
350 return Vec::new();
351 };
352 let bytes = canonical.as_bytes();
353 let mut out = Vec::with_capacity(bytes.len() / VARIATION_ENTRY_CHARS);
354 for chunk in bytes.chunks(VARIATION_ENTRY_CHARS) {
355 if chunk.len() < VARIATION_ENTRY_CHARS {
356 break;
357 }
358 let Ok(tag_text) = std::str::from_utf8(&chunk[..8]) else {
361 continue;
362 };
363 let Ok(tag) = u32::from_str_radix(tag_text, 16) else {
364 continue;
365 };
366 let Ok(value_text) = std::str::from_utf8(&chunk[8..VARIATION_ENTRY_CHARS]) else {
367 continue;
368 };
369 let Ok(bits) = u32::from_str_radix(value_text, 16) else {
370 continue;
371 };
372 out.push((
373 skrifa::Tag::from_be_bytes(tag.to_be_bytes()),
374 f32::from_bits(bits),
375 ));
376 }
377 out
378}
379
380fn swash_settings(canonical: Option<&str>) -> Vec<swash::Setting<f32>> {
382 decode_variation(canonical)
383 .into_iter()
384 .map(|(tag, value)| swash::Setting {
385 tag: u32::from_be_bytes(tag.to_be_bytes()),
386 value,
387 })
388 .collect()
389}
390
391static GLYPH_ID_WARNED: OnceLock<Mutex<std::collections::HashSet<(u64, u32)>>> = OnceLock::new();
392
393fn swash_glyph_id(font_id: u64, glyph_id: u32) -> u16 {
400 match u16::try_from(glyph_id) {
401 Ok(v) => v,
402 Err(_) => {
403 let warned =
404 GLYPH_ID_WARNED.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
405 let is_new = warned
406 .lock()
407 .map(|mut g| g.insert((font_id, glyph_id)))
408 .unwrap_or(false);
409 if is_new {
410 log::warn!(
411 "glyph id {} (font {}) exceeds u16::MAX; falling back to .notdef (0)",
412 glyph_id,
413 font_id
414 );
415 }
416 0
417 }
418 }
419}
420
421#[derive(Clone, Debug)]
423pub enum Command {
424 MoveTo(f32, f32),
425 LineTo(f32, f32),
426 QuadTo(f32, f32, f32, f32),
427 CurveTo(f32, f32, f32, f32, f32, f32),
428 Close,
429}
430
431#[derive(Clone)]
432pub struct ShapedGlyph {
433 pub key: GlyphKey,
434 pub cache_key: CacheKey,
435 pub font_id: u64,
436 pub glyph_id: u32,
437 pub px: f32,
438 pub x: f32,
439 pub y: f32,
440 pub w: f32,
441 pub h: f32,
442 pub bearing_x: f32,
443 pub bearing_y: f32,
444 pub advance: f32,
445 pub rasterized: bool,
446}
447
448pub use swash::scale::image::Content as SwashContent;
449
450pub struct GlyphBitmap {
451 pub key: GlyphKey,
452 pub w: u32,
453 pub h: u32,
454 pub content: SwashContent,
455 pub data: Vec<u8>,
456}
457
458struct FontRecord {
459 id: u64,
460 data_bytes: Arc<[u8]>,
461 face_index: u32,
462}
463
464struct Engine {
465 font_cx: parley::FontContext,
466 layout_cx: parley::LayoutContext<()>,
467 swash_cx: swash::scale::ScaleContext,
468 key_map: RapidHashMap<GlyphKey, (u64, u32)>,
469 font_registry: Vec<FontRecord>,
470 font_registry_index: RapidHashMap<(u64, u32), u64>,
471 next_font_id: u64,
472 glyph_cache:
475 RapidHashMap<VariationKey, (u32, u32, i32, i32, swash::scale::image::Content, Vec<u8>)>,
476 glyph_cache_bytes: usize,
477 outline_cache: Lru<OutlineKey, Arc<[Command]>>,
478 ascent_cache: RapidHashMap<(Option<Arc<str>>, u16, u32, u64), (f32, f32)>,
481}
482
483fn normalized_raster_px(px: f32) -> Option<f32> {
484 if !px.is_finite() || px <= 0.0 {
485 return None;
486 }
487 let clamped = px.clamp(RASTER_MIN_PX, RASTER_MAX_PX);
488 let quantized = (clamped / RASTER_QUANTUM).round() * RASTER_QUANTUM;
489 Some(quantized.clamp(RASTER_MIN_PX, RASTER_MAX_PX))
490}
491
492impl Engine {
493 fn ensure_font(&mut self, fd: &parley::FontData) -> u64 {
494 let blob_id = fd.data.id();
495 let lookup_key = (blob_id, fd.index);
496 if let Some(id) = self.font_registry_index.get(&lookup_key).copied() {
497 return id;
498 }
499 if let Some(existing) = self.font_registry.iter().find(|record| {
500 record.face_index == fd.index && record.data_bytes.as_ref() == fd.data.as_ref()
501 }) {
502 self.font_registry_index.insert(lookup_key, existing.id);
503 return existing.id;
504 }
505 let id = self.next_font_id;
506 self.next_font_id += 1;
507 let bytes: Arc<[u8]> = Arc::from(fd.data.as_ref());
508 log::debug!("[font] register id={} len={}", id, bytes.len());
509 self.font_registry.push(FontRecord {
510 id,
511 data_bytes: bytes,
512 face_index: fd.index,
513 });
514 self.font_registry_index.insert(lookup_key, id);
515 id
516 }
517
518 fn trim_glyph_cache(&mut self) {
519 while self.glyph_cache.len() > GLYPH_CACHE_CAP
520 || self.glyph_cache_bytes > GLYPH_CACHE_BYTES_CAP
521 {
522 let Some((key, value)) = self.glyph_cache.iter().next() else {
523 break;
524 };
525 let key = key.clone();
526 let bytes = value.5.len();
527 self.glyph_cache.remove(&key);
528 self.glyph_cache_bytes = self.glyph_cache_bytes.saturating_sub(bytes);
529 }
530 }
531
532 fn resolve_vertical_metrics(
536 &mut self,
537 font_family: Option<&str>,
538 font_weight: u16,
539 px: f32,
540 ) -> (f32, f32) {
541 let px = px.clamp(RASTER_MIN_PX, RASTER_MAX_PX);
542 let mut candidates: Vec<&str> = Vec::new();
543 match font_family {
544 Some("monospace") => candidates.push("JetBrains Mono"),
545 Some("sans-serif") => candidates.push("Open Sans"),
546 Some("emoji") => candidates.push("Noto Color Emoji"),
547 Some(other) => candidates.push(other),
548 None => {}
549 }
550 candidates.push("Open Sans");
551 for name in candidates {
552 if let Some(m) = self.metrics_for_family(name, font_weight, px) {
553 return m;
554 }
555 }
556 (px * 0.8, px * 0.2)
557 }
558
559 fn metrics_for_family(&mut self, name: &str, font_weight: u16, px: f32) -> Option<(f32, f32)> {
562 let info = self.font_cx.collection.family_by_name(name)?;
563 let target = font_weight as f32;
564 let mut best: Option<(f32, Arc<[u8]>, u32)> = None;
565 for font in info.fonts() {
566 let dist = (font.weight().value() - target).abs();
567 if best.as_ref().is_some_and(|(bd, _, _)| *bd <= dist) {
568 continue;
569 }
570 let bytes: Arc<[u8]> = match font.source().kind() {
571 parley::fontique::SourceKind::Memory(blob) => Arc::from(blob.as_ref()),
572 #[cfg(not(target_arch = "wasm32"))]
573 parley::fontique::SourceKind::Path(path) => Arc::from(std::fs::read(path).ok()?),
574 #[cfg(target_arch = "wasm32")]
575 parley::fontique::SourceKind::Path(_) => continue,
576 };
577 best = Some((dist, bytes, font.index()));
578 }
579 let (_, bytes, index) = best?;
580 let font = skrifa::FontRef::from_index(bytes.as_ref(), index).ok()?;
581 let metrics = font.metrics(
582 skrifa::instance::Size::new(px),
583 skrifa::instance::LocationRef::default(),
584 );
585 let limit = px * 8.0;
586 let ascent = if metrics.ascent.is_finite() {
587 metrics.ascent.max(0.0).min(limit)
588 } else {
589 0.0
590 };
591 let descent = if metrics.descent.is_finite() {
592 metrics.descent.abs().max(0.0).min(limit)
593 } else {
594 0.0
595 };
596 Some((ascent, descent))
597 }
598
599 fn raster_placement(
600 &mut self,
601 font_id: u64,
602 glyph_id: u32,
603 px: f32,
604 variation: Option<Arc<str>>,
605 ) -> Option<(f32, f32, f32, f32)> {
606 use swash::scale::{Render, Source, StrikeWith};
607 let px = normalized_raster_px(px)?;
608 let cache_key = (font_id, glyph_id, px.to_bits(), variation.clone());
609 if let Some(cached) = self.glyph_cache.get(&cache_key) {
610 log::debug!(
611 "[raster_placement] HIT fid={} gid={} px={} => {}x{} {}x{}",
612 font_id,
613 glyph_id,
614 px,
615 cached.0,
616 cached.1,
617 cached.2,
618 cached.3
619 );
620 return Some((
621 cached.0 as f32,
622 cached.1 as f32,
623 cached.2 as f32,
624 cached.3 as f32,
625 ));
626 }
627 let (data_bytes, face_index) = {
628 let record = self.font_registry.iter().find(|r| r.id == font_id)?;
629 (record.data_bytes.clone(), record.face_index)
630 };
631 let face_index = usize::try_from(face_index).ok()?;
632 let font = swash::FontRef::from_index(data_bytes.as_ref(), face_index)?;
633 let mut builder = self.swash_cx.builder(font).size(px).hint(true);
634 let settings = swash_settings(variation.as_deref());
635 if !settings.is_empty() {
636 builder = builder.variations(settings);
637 }
638 let mut scaler = builder.build();
639 let image = Render::new(&[
640 Source::Outline,
641 Source::ColorBitmap(StrikeWith::BestFit),
642 Source::ColorOutline(0),
643 ])
644 .render(&mut scaler, swash_glyph_id(font_id, glyph_id))?;
645 if image.placement.width > MAX_RASTER_DIMENSION
646 || image.placement.height > MAX_RASTER_DIMENSION
647 || image.data.len() > MAX_RASTER_BYTES
648 {
649 return None;
650 }
651 log::debug!(
652 "[raster_placement] MISS fid={} gid={} px={} => {}x{} {}x{}",
653 font_id,
654 glyph_id,
655 px,
656 image.placement.width,
657 image.placement.height,
658 image.placement.left,
659 image.placement.top
660 );
661 let width = image.placement.width;
662 let height = image.placement.height;
663 let left = image.placement.left;
664 let top = image.placement.top;
665 let data_len = image.data.len();
666 let previous = self.glyph_cache.insert(
667 cache_key,
668 (width, height, left, top, image.content, image.data),
669 );
670 if let Some(previous) = previous {
671 self.glyph_cache_bytes = self.glyph_cache_bytes.saturating_sub(previous.5.len());
672 }
673 self.glyph_cache_bytes = self.glyph_cache_bytes.saturating_add(data_len);
674 self.trim_glyph_cache();
675 Some((width as f32, height as f32, left as f32, top as f32))
676 }
677}
678
679static ENGINE: OnceLock<Mutex<Engine>> = OnceLock::new();
680
681pub static FONT_PROVIDER: OnceLock<Mutex<font_awl::Provider>> = OnceLock::new();
682#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
683static RETAINED_FONT_DATA: OnceLock<Mutex<Vec<Arc<[u8]>>>> = OnceLock::new();
684
685#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
686static WASM_FONT_CONTEXT_READY: std::sync::atomic::AtomicBool =
687 std::sync::atomic::AtomicBool::new(false);
688
689#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
690static WASM_FONT_INIT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
691
692#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
693static WEB_FONTS_LOADED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
694
695fn append_generic_family_once(
696 collection: &mut parley::fontique::Collection,
697 generic: parley::fontique::GenericFamily,
698 id: parley::fontique::FamilyId,
699) {
700 let mut families: Vec<parley::fontique::FamilyId> =
701 collection.generic_families(generic).collect();
702 if !families.contains(&id) {
703 families.push(id);
704 collection.set_generic_families(generic, families.into_iter());
705 }
706}
707
708fn configure_collection(collection: &mut parley::fontique::Collection) {
709 for name in [
710 "Noto Sans Symbols",
711 "Noto Sans Symbols 2",
712 "Material Symbols Outlined",
713 ] {
714 if let Some(info) = collection.family_by_name(name) {
715 append_generic_family_once(
716 collection,
717 parley::fontique::GenericFamily::SansSerif,
718 info.id(),
719 );
720 }
721 }
722 if let Some(info) = collection.family_by_name("Noto Color Emoji") {
723 append_generic_family_once(
724 collection,
725 parley::fontique::GenericFamily::Emoji,
726 info.id(),
727 );
728 }
729}
730
731fn font_blob(bytes: Arc<[u8]>) -> parley::fontique::Blob<u8> {
732 let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(bytes);
733 parley::fontique::Blob::new(data)
734}
735
736fn register_asset_if_missing(provider: &mut font_awl::Provider, bytes: &[u8]) {
737 if collection_font_data_families(provider.collection_mut(), bytes).is_empty() {
738 let blob = font_blob(Arc::from(bytes));
739 let families = provider.collection_mut().register_fonts(blob, None);
740 append_registered_families(provider.collection_mut(), &families);
741 }
742}
743
744fn init_provider_sync() -> font_awl::Provider {
745 let mut provider = font_awl::Provider::new();
746 provider.load_bundled_fonts();
747 static MATERIAL_SYMBOLS_TTF: &[u8] = include_bytes!("assets/MaterialSymbolsOutlined.ttf");
748 register_asset_if_missing(&mut provider, MATERIAL_SYMBOLS_TTF);
749 #[cfg(all(
754 not(target_arch = "wasm32"),
755 feature = "system-fonts",
756 not(feature = "probe_no_system")
757 ))]
758 if let Err(e) = provider.load_system_fonts_best_effort() {
759 log::warn!("font-awl: failed to load system fonts: {e}");
760 }
761 configure_collection(provider.collection_mut());
762 provider
763}
764
765fn provider() -> &'static Mutex<font_awl::Provider> {
766 FONT_PROVIDER.get_or_init(|| Mutex::new(init_provider_sync()))
767}
768
769fn init_engine_sync() -> Engine {
770 let mut font_cx = provider().lock().unwrap().new_parley_context();
771 configure_collection(&mut font_cx.collection);
772
773 Engine {
774 font_cx,
775 layout_cx: parley::LayoutContext::new(),
776 swash_cx: swash::scale::ScaleContext::new(),
777 key_map: RapidHashMap::new(),
778 font_registry: Vec::new(),
779 font_registry_index: RapidHashMap::new(),
780 next_font_id: 1,
781 glyph_cache: RapidHashMap::new(),
782 glyph_cache_bytes: 0,
783 outline_cache: Lru::new(OUTLINE_CACHE_CAP),
784 ascent_cache: RapidHashMap::new(),
785 }
786}
787
788#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
793pub fn web_fonts_loaded() -> bool {
794 WEB_FONTS_LOADED.load(Ordering::Acquire)
795}
796
797#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
798pub async fn init_fonts_wasm() {
799 if WEB_FONTS_LOADED.load(Ordering::Acquire) {
800 return;
801 }
802 if WASM_FONT_INIT
803 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
804 .is_err()
805 {
806 return;
807 }
808
809 let startup_attempt = !WASM_FONT_CONTEXT_READY.load(Ordering::Acquire);
810
811 let mut candidate = init_provider_sync();
812 let web_fonts = candidate.load_web_fonts().await;
813 if let Err(error) = &web_fonts {
814 if startup_attempt {
815 log::info!("font-awl: local fonts deferred to a user gesture: {error}");
816 } else {
817 log::warn!("font-awl: local font access failed: {error} (bundled fonts kept)");
818 }
819 }
820 restore_retained_font_data(candidate.collection_mut());
821 configure_collection(candidate.collection_mut());
822 {
823 let mut p = provider().lock().unwrap();
824 *p = candidate;
825 }
826 let font_cx = provider().lock().unwrap().new_parley_context();
827 if let Some(eng) = ENGINE.get() {
828 let mut eng = eng.lock().unwrap();
829 eng.font_cx = font_cx;
830 clear_caches_for_fallback_in(&mut eng);
831 } else {
832 drop(font_cx);
833 }
834 if web_fonts.is_ok() {
835 WEB_FONTS_LOADED.store(true, Ordering::Release);
836 }
837 WASM_FONT_CONTEXT_READY.store(true, Ordering::Release);
838 WASM_FONT_INIT.store(false, Ordering::Release);
839}
840
841fn engine() -> &'static Mutex<Engine> {
842 ENGINE.get_or_init(|| Mutex::new(init_engine_sync()))
843}
844
845pub fn register_font_data(bytes: &[u8]) {
846 let _ = register_font_data_if_usable(bytes);
847}
848
849pub fn register_font_as(bytes: &[u8], alias: &str) -> bool {
857 if alias.is_empty() {
858 return false;
859 }
860 let (font_cx, newly_registered) = {
861 let mut p = provider().lock().unwrap();
862 if p.collection_mut().family_by_name(alias).is_some() {
863 return true;
864 }
865 let blob = font_blob(Arc::from(bytes));
866 let families = p.collection_mut().register_fonts(
867 blob,
868 Some(parley::fontique::FontInfoOverride {
869 family_name: Some(alias),
870 ..Default::default()
871 }),
872 );
873 if families.is_empty() {
874 return false;
875 }
876 append_registered_families(p.collection_mut(), &families);
877 configure_collection(p.collection_mut());
878 (p.new_parley_context(), true)
879 };
880 if newly_registered {
881 let mut eng = engine().lock().unwrap();
882 eng.font_cx = font_cx;
883 clear_caches_for_fallback_in(&mut eng);
884 }
885 true
886}
887
888fn font_source_bytes(font: &parley::fontique::FontInfo) -> Option<&[u8]> {
889 match font.source().kind() {
890 parley::fontique::SourceKind::Memory(blob) => Some(blob.as_ref()),
891 parley::fontique::SourceKind::Path(_) => None,
892 }
893}
894
895fn collection_font_data_families(
896 collection: &mut parley::fontique::Collection,
897 bytes: &[u8],
898) -> Vec<parley::fontique::FamilyId> {
899 let mut names: Vec<String> = collection.family_names().map(str::to_owned).collect();
900 names.sort_unstable();
901 let mut result = Vec::new();
902 for name in names {
903 let Some(info) = collection.family_by_name(&name) else {
904 continue;
905 };
906 if info
907 .fonts()
908 .iter()
909 .any(|font| font_source_bytes(font).is_some_and(|source| source == bytes))
910 {
911 result.push(info.id());
912 }
913 }
914 result
915}
916
917fn append_registered_families(
918 collection: &mut parley::fontique::Collection,
919 families: &[(parley::fontique::FamilyId, Vec<parley::fontique::FontInfo>)],
920) {
921 let mut ids: Vec<parley::fontique::FamilyId> = families.iter().map(|(id, _)| *id).collect();
922 ids.sort_unstable();
923 for id in ids {
924 let Some(name) = collection.family_name(id).map(str::to_owned) else {
925 continue;
926 };
927 let generic = if name.starts_with("Noto Color Emoji") {
928 parley::fontique::GenericFamily::Emoji
929 } else {
930 parley::fontique::GenericFamily::SansSerif
931 };
932 append_generic_family_once(collection, generic, id);
933 }
934}
935
936#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
937fn retain_font_data(bytes: Arc<[u8]>) {
938 let retained = RETAINED_FONT_DATA.get_or_init(|| Mutex::new(Vec::new()));
939 let Ok(mut retained) = retained.lock() else {
940 return;
941 };
942 if retained
943 .iter()
944 .all(|existing| existing.as_ref() != bytes.as_ref())
945 {
946 retained.push(bytes);
947 }
948}
949
950#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
951fn restore_retained_font_data(collection: &mut parley::fontique::Collection) {
952 let retained = RETAINED_FONT_DATA.get().and_then(|retained| {
953 retained
954 .lock()
955 .ok()
956 .map(|retained| retained.as_slice().to_vec())
957 });
958 let Some(retained) = retained else {
959 return;
960 };
961 for bytes in retained {
962 if collection_font_data_families(collection, bytes.as_ref()).is_empty() {
963 let blob = font_blob(bytes);
964 let families = collection.register_fonts(blob, None);
965 append_registered_families(collection, &families);
966 }
967 }
968}
969
970pub(crate) fn register_font_data_if_usable(bytes: &[u8]) -> bool {
971 #[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
972 let mut retained_bytes = None;
973 let (font_cx, newly_registered) = {
974 let mut p = provider().lock().unwrap();
975 let existing = collection_font_data_families(p.collection_mut(), bytes);
976 if !existing.is_empty() {
977 for id in existing {
978 let is_emoji = p
979 .collection_mut()
980 .family_name(id)
981 .is_some_and(|name| name.starts_with("Noto Color Emoji"));
982 let generic = if is_emoji {
983 parley::fontique::GenericFamily::Emoji
984 } else {
985 parley::fontique::GenericFamily::SansSerif
986 };
987 append_generic_family_once(p.collection_mut(), generic, id);
988 }
989 (p.new_parley_context(), false)
990 } else {
991 let shared_bytes: Arc<[u8]> = Arc::from(bytes);
992 let blob = font_blob(shared_bytes.clone());
993 let families = p.collection_mut().register_fonts(blob, None);
994 if families.is_empty() {
995 return false;
996 }
997 #[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
998 {
999 retained_bytes = Some(shared_bytes);
1000 }
1001 append_registered_families(p.collection_mut(), &families);
1002 configure_collection(p.collection_mut());
1003 (p.new_parley_context(), true)
1004 }
1005 };
1006
1007 if newly_registered {
1008 #[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
1009 if let Some(bytes) = retained_bytes {
1010 retain_font_data(bytes);
1011 }
1012 let mut eng = engine().lock().unwrap();
1013 eng.font_cx = font_cx;
1014 clear_caches_for_fallback_in(&mut eng);
1015 }
1016
1017 true
1018}
1019
1020pub(crate) fn clear_caches_for_fallback_in(eng: &mut Engine) {
1021 clear_lru_caches();
1022 eng.key_map.clear();
1023 eng.font_registry.clear();
1024 eng.font_registry_index.clear();
1025 eng.glyph_cache.clear();
1026 eng.glyph_cache_bytes = 0;
1027 eng.outline_cache.clear_both();
1028 eng.ascent_cache.clear();
1029 bump_frame_for_fallback();
1030}
1031
1032fn clear_lru_caches() {
1033 if let Some(c) = METRICS_LRU.get()
1034 && let Ok(mut g) = c.lock()
1035 {
1036 g.clear_both();
1037 }
1038 if let Some(c) = WRAP_LRU.get()
1039 && let Ok(mut g) = c.lock()
1040 {
1041 g.clear_both();
1042 }
1043 if let Some(c) = WRAP_RANGES_LRU.get()
1044 && let Ok(mut g) = c.lock()
1045 {
1046 g.clear_both();
1047 }
1048 if let Some(c) = ELLIP_LRU.get()
1049 && let Ok(mut g) = c.lock()
1050 {
1051 g.clear_both();
1052 }
1053 if let Some(c) = SHAPED_LRU.get()
1054 && let Ok(mut g) = c.lock()
1055 {
1056 g.clear_both();
1057 }
1058 if let Some(c) = LEGACY_SHAPED_LRU.get()
1059 && let Ok(mut g) = c.lock()
1060 {
1061 g.clear_both();
1062 }
1063}
1064
1065pub(crate) fn bump_frame_for_fallback() {
1066 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
1067 FONT_GENERATION.fetch_add(1, Ordering::Relaxed);
1068 FALLBACK_DIRTY.store(true, Ordering::Relaxed);
1069}
1070
1071#[cfg(target_arch = "wasm32")]
1072pub fn ensure_web_fallback_initialized() {
1073 crate::fallback::wasm_fallback::ensure_fallback_initialized();
1074}
1075
1076#[cfg(not(target_arch = "wasm32"))]
1077pub fn ensure_web_fallback_initialized() {}
1078
1079pub fn load_font_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
1083 let bytes = std::fs::read(path)?;
1084 register_font_data(&bytes);
1085 Ok(())
1086}
1087
1088pub fn primary_font_vertical_metrics(
1096 font_family: Option<&str>,
1097 font_weight: u16,
1098 px: f32,
1099) -> (f32, f32) {
1100 let Some(px) = normalized_raster_px(px) else {
1101 return (0.0, 0.0);
1102 };
1103 let key = (
1104 font_family.map(Arc::from),
1105 font_weight,
1106 px.to_bits(),
1107 font_generation(),
1108 );
1109 let mut eng = engine().lock().unwrap();
1110 if let Some(&m) = eng.ascent_cache.get(&key) {
1111 return m;
1112 }
1113 let m = eng.resolve_vertical_metrics(font_family, font_weight, px);
1114 if m.0.is_finite() && m.1.is_finite() && eng.ascent_cache.len() >= VERTICAL_METRICS_CACHE_CAP {
1115 eng.ascent_cache.clear();
1116 }
1117 eng.ascent_cache.insert(key, m);
1118 m
1119}
1120
1121pub fn font_family_name(bytes: &[u8]) -> Option<String> {
1126 use skrifa::string::StringId;
1127
1128 for face_index in 0..4096u32 {
1129 let Ok(font) = skrifa::FontRef::from_index(bytes, face_index) else {
1130 break;
1131 };
1132 let name = font
1133 .localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME)
1134 .english_or_first()
1135 .map(|s| s.to_string())
1136 .or_else(|| {
1137 font.localized_strings(StringId::FAMILY_NAME)
1138 .english_or_first()
1139 .map(|s| s.to_string())
1140 });
1141 if name.is_some() {
1142 return name;
1143 }
1144 }
1145 None
1146}
1147
1148fn key_from_pair(font_id: u64, glyph_id: u32) -> GlyphKey {
1149 let mut h = RapidHasher::default();
1150 font_id.hash(&mut h);
1151 glyph_id.hash(&mut h);
1152 GlyphKey(h.finish())
1153}
1154
1155#[cfg(target_arch = "wasm32")]
1156fn collect_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) -> Vec<u32> {
1157 use parley::layout::PositionedLayoutItem;
1158 let mut out = Vec::new();
1159 let mut total_glyphs: usize = 0;
1160 for line in layout.lines() {
1161 for item in line.items() {
1162 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
1163 continue;
1164 };
1165 let run = glyph_run.run();
1166 for cluster in run.clusters() {
1168 let mut glyph_count = 0usize;
1169 let mut has_missing = false;
1170 for glyph in cluster.glyphs() {
1171 glyph_count += 1;
1172 has_missing |= glyph.id == 0;
1173 }
1174 total_glyphs += glyph_count;
1175 if has_missing {
1176 let range = cluster.text_range();
1177 let end = range.end.min(text.len());
1179 let start = range.start.min(end);
1180 if let Some(slice) = text.get(start..end) {
1181 for ch in slice.chars() {
1182 out.push(ch as u32);
1183 }
1184 }
1185 if range.start == range.end {
1187 }
1189 }
1190 }
1191 }
1192 }
1193 if out.is_empty() && !text.is_empty() && total_glyphs == 0 {
1194 if text.chars().any(|c| !c.is_whitespace()) {
1195 for ch in text.chars() {
1196 if !ch.is_whitespace() && ch != '\n' && ch != '\r' && ch != '\t' {
1197 out.push(ch as u32);
1198 }
1199 }
1200 }
1201 }
1202 out
1203}
1204
1205fn push_font_family<'a>(
1206 builder: &mut parley::RangedBuilder<'a, ()>,
1207 family: Option<&str>,
1208 text_len: usize,
1209) {
1210 use parley::style::{FontFamilyName, GenericFamily};
1211
1212 let mut names = Vec::with_capacity(10);
1213 match family {
1214 Some("monospace") => {
1215 names.push(FontFamilyName::named("JetBrains Mono"));
1216 names.push(GenericFamily::Monospace.into());
1217 }
1218 Some("sans-serif") => names.push(FontFamilyName::named("Open Sans")),
1219 Some("emoji") => names.push(FontFamilyName::named("Noto Color Emoji")),
1220 Some("serif") => names.push(GenericFamily::Serif.into()),
1221 Some("cursive") => names.push(GenericFamily::Cursive.into()),
1222 Some("fantasy") => names.push(GenericFamily::Fantasy.into()),
1223 Some("system-ui") => names.push(GenericFamily::SystemUi.into()),
1224 Some("math") => names.push(GenericFamily::Math.into()),
1225 Some(family) => names.push(FontFamilyName::named(family)),
1226 None => {}
1227 }
1228 names.push(GenericFamily::SansSerif.into());
1229 names.push(GenericFamily::Emoji.into());
1230 names.push(FontFamilyName::named("Noto Color Emoji"));
1231 names.push(FontFamilyName::named("Noto Sans Symbols 2"));
1232 names.push(FontFamilyName::named("Noto Sans Symbols2"));
1233 names.push(FontFamilyName::named("Noto Sans Symbols"));
1234 names.push(FontFamilyName::named("Material Symbols Outlined"));
1235 builder.push(names.as_slice(), 0..text_len);
1236}
1237
1238fn build_layout(
1239 eng: &mut Engine,
1240 text: &str,
1241 px: f32,
1242 line_height_ratio: f32,
1243 font_family: Option<&str>,
1244 font_weight: u16,
1245 font_style: u8,
1246 letter_spacing: f32,
1247 font_variation_settings: Option<&str>,
1248) -> parley::Layout<()> {
1249 use parley::FontWeight;
1250 use parley::style::StyleProperty;
1251
1252 let mut builder = eng
1253 .layout_cx
1254 .ranged_builder(&mut eng.font_cx, text, 1.0, true);
1255 builder.push_default(StyleProperty::FontSize(px));
1256 if line_height_ratio > 0.0 {
1257 builder.push_default(StyleProperty::LineHeight(
1258 parley::LineHeight::FontSizeRelative(line_height_ratio),
1259 ));
1260 }
1261 builder.push_default(StyleProperty::FontWeight(FontWeight::new(
1262 font_weight as f32,
1263 )));
1264 builder.push_default(StyleProperty::FontStyle(match font_style {
1265 1 => parley::FontStyle::Italic,
1266 _ => parley::FontStyle::Normal,
1267 }));
1268 builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
1269 if let Some(settings) = font_variation_settings {
1270 builder.push_default(StyleProperty::FontVariations(
1271 parley::style::FontVariations::from(settings),
1272 ));
1273 }
1274 push_font_family(&mut builder, font_family, text.len());
1275 let mut layout = builder.build(text);
1276 layout.break_all_lines(None);
1277 layout.align(
1278 parley::Alignment::Start,
1279 parley::AlignmentOptions::default(),
1280 );
1281 layout
1282}
1283
1284#[cfg(target_arch = "wasm32")]
1285fn report_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) {
1286 let unresolved = collect_unresolved_codepoints(layout, text);
1287 if unresolved.is_empty() {
1288 return;
1289 }
1290 let reg = crate::unresolved::web_unresolved_registry();
1291 if unresolved.iter().any(|cp| !reg.contains(*cp)) {
1292 crate::fallback::wasm_fallback::ensure_fallback_initialized();
1293 reg.add_unresolved_vec(unresolved);
1294 }
1295}
1296
1297#[cfg(not(target_arch = "wasm32"))]
1298fn report_unresolved_codepoints(_layout: &parley::Layout<()>, _text: &str) {}
1299
1300fn collect_shaped_layout(
1301 eng: &mut Engine,
1302 layout: parley::Layout<()>,
1303 px: f32,
1304 collect_runs: bool,
1305 variation: Option<Arc<str>>,
1306) -> (Vec<ShapedGlyph>, Vec<ShapedRun>) {
1307 use parley::layout::PositionedLayoutItem;
1308
1309 let raster_px = normalized_raster_px(px).unwrap_or(0.0);
1310 let mut glyphs = Vec::new();
1311 let mut runs = Vec::new();
1312 for line in layout.lines() {
1313 for item in line.items() {
1314 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
1315 continue;
1316 };
1317 let run = glyph_run.run();
1318 if collect_runs {
1319 runs.push(ShapedRun {
1320 text_range: run.text_range(),
1321 rtl: run.is_rtl(),
1322 synthesis: run.synthesis(),
1323 });
1324 }
1325 let font_data = run.font();
1326 let fid = eng.ensure_font(font_data);
1327 let run_cache_key = CacheKey {
1328 font_id: fid,
1329 glyph_id: 0,
1330 font_size_bits: raster_px.to_bits(),
1331 variation: variation.clone(),
1332 };
1333 log::debug!(
1334 "[shape] run: fid={} font_data_len={}",
1335 fid,
1336 font_data.data.as_ref().len()
1337 );
1338 for glyph in glyph_run.positioned_glyphs() {
1339 let gid = glyph.id;
1340 let key = key_from_pair(fid, gid);
1341 eng.key_map.insert(key, (fid, gid));
1342 let cache_key = CacheKey {
1343 glyph_id: gid,
1344 ..run_cache_key.clone()
1345 };
1346 glyphs.push(ShapedGlyph {
1347 key,
1348 cache_key,
1349 font_id: fid,
1350 glyph_id: gid,
1351 px,
1352 x: glyph.x,
1353 y: glyph.y,
1354 w: 0.0,
1355 h: 0.0,
1356 bearing_x: 0.0,
1357 bearing_y: 0.0,
1358 advance: glyph.advance,
1359 rasterized: false,
1360 });
1361 }
1362 }
1363 }
1364 (glyphs, runs)
1365}
1366
1367#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1368pub enum TextDirection {
1369 #[default]
1370 Auto,
1371 Ltr,
1372 Rtl,
1373}
1374
1375#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1376pub enum FontSynthesis {
1377 #[default]
1378 Unspecified,
1379 None,
1380 Weight,
1381 Style,
1382 All,
1383}
1384
1385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1386pub enum ShapeCapabilityError {
1387 RequestedDirectionMismatch,
1388 SynthesisModeUnsupported,
1389}
1390
1391#[derive(Clone, Copy, Debug)]
1392pub struct ShapeOptions<'a> {
1393 pub font_family: Option<&'a str>,
1394 pub font_weight: u16,
1395 pub font_style: u8,
1396 pub letter_spacing: f32,
1397 pub font_variation_settings: Option<&'a str>,
1398 pub text_direction: TextDirection,
1399 pub font_synthesis: FontSynthesis,
1400}
1401
1402impl Default for ShapeOptions<'_> {
1403 fn default() -> Self {
1404 Self {
1405 font_family: None,
1406 font_weight: 400,
1407 font_style: 0,
1408 letter_spacing: 0.0,
1409 font_variation_settings: None,
1410 text_direction: TextDirection::Auto,
1411 font_synthesis: FontSynthesis::Unspecified,
1412 }
1413 }
1414}
1415
1416#[derive(Clone)]
1417pub struct ShapedRun {
1418 pub text_range: std::ops::Range<usize>,
1419 pub rtl: bool,
1420 pub synthesis: parley::fontique::Synthesis,
1421}
1422
1423#[derive(Clone)]
1424pub struct ShapedText {
1425 pub glyphs: Vec<ShapedGlyph>,
1426 pub requested_text_direction: TextDirection,
1427 pub resolved_text_direction: TextDirection,
1428 pub runs: Vec<ShapedRun>,
1429}
1430
1431#[derive(Clone)]
1432pub struct SharedShapedText {
1433 pub glyphs: Arc<[ShapedGlyph]>,
1434 pub requested_text_direction: TextDirection,
1435 pub resolved_text_direction: TextDirection,
1436 pub runs: Arc<[ShapedRun]>,
1437}
1438
1439pub type CachedShapedText = SharedShapedText;
1440pub type VectorShapedText = SharedShapedText;
1441pub type VectorShapedLine = SharedShapedText;
1442pub type ShapedLine = SharedShapedText;
1443
1444#[derive(Clone)]
1445struct ShapeCacheKey {
1446 text: Arc<str>,
1447 px_bits: u32,
1448 line_height_bits: u32,
1449 family: Option<Arc<str>>,
1450 font_weight: u16,
1451 font_style: u8,
1452 letter_spacing_bits: u32,
1453 variation: Option<Arc<str>>,
1454 generation: u64,
1455 text_direction: TextDirection,
1456 font_synthesis: FontSynthesis,
1457}
1458
1459impl ShapeCacheKey {
1460 fn new(
1461 text: &str,
1462 px: f32,
1463 line_height_ratio: f32,
1464 options: ShapeOptions<'_>,
1465 generation: u64,
1466 ) -> Self {
1467 Self {
1468 text: Arc::from(text),
1469 px_bits: px.to_bits(),
1470 line_height_bits: line_height_ratio.to_bits(),
1471 family: options.font_family.map(Arc::from),
1472 font_weight: options.font_weight,
1473 font_style: options.font_style,
1474 letter_spacing_bits: options.letter_spacing.to_bits(),
1475 variation: options.font_variation_settings.map(Arc::from),
1476 generation,
1477 text_direction: options.text_direction,
1478 font_synthesis: options.font_synthesis,
1479 }
1480 }
1481
1482 fn matches(
1483 &self,
1484 text: &str,
1485 px: f32,
1486 line_height_ratio: f32,
1487 options: ShapeOptions<'_>,
1488 ) -> bool {
1489 self.text.as_ref() == text
1490 && self.px_bits == px.to_bits()
1491 && self.line_height_bits == line_height_ratio.to_bits()
1492 && self.family.as_deref() == options.font_family
1493 && self.font_weight == options.font_weight
1494 && self.font_style == options.font_style
1495 && self.letter_spacing_bits == options.letter_spacing.to_bits()
1496 && self.variation.as_deref() == options.font_variation_settings
1497 && self.text_direction == options.text_direction
1498 && self.font_synthesis == options.font_synthesis
1499 }
1500}
1501
1502struct ShapeCacheEntry {
1503 key: ShapeCacheKey,
1504 value: Arc<SharedShapedText>,
1505}
1506
1507static SHAPED_LRU: OnceLock<Mutex<Lru<u64, Vec<ShapeCacheEntry>>>> = OnceLock::new();
1508
1509struct LegacyShapeCacheEntry {
1510 key: ShapeCacheKey,
1511 value: Arc<[ShapedGlyph]>,
1512}
1513
1514static LEGACY_SHAPED_LRU: OnceLock<Mutex<Lru<u64, Vec<LegacyShapeCacheEntry>>>> = OnceLock::new();
1515
1516fn shaped_cache() -> &'static Mutex<Lru<u64, Vec<ShapeCacheEntry>>> {
1517 SHAPED_LRU.get_or_init(|| Mutex::new(Lru::new(SHAPE_CACHE_CAP)))
1518}
1519
1520fn legacy_shaped_cache() -> &'static Mutex<Lru<u64, Vec<LegacyShapeCacheEntry>>> {
1521 LEGACY_SHAPED_LRU.get_or_init(|| Mutex::new(Lru::new(LEGACY_SHAPE_CACHE_CAP)))
1522}
1523
1524fn shape_fingerprint(
1525 text: &str,
1526 px: f32,
1527 line_height_ratio: f32,
1528 options: ShapeOptions<'_>,
1529 generation: u64,
1530) -> u64 {
1531 let mut hasher = RapidHasher::default();
1532 text.hash(&mut hasher);
1533 px.to_bits().hash(&mut hasher);
1534 line_height_ratio.to_bits().hash(&mut hasher);
1535 options.font_family.hash(&mut hasher);
1536 options.font_weight.hash(&mut hasher);
1537 options.font_style.hash(&mut hasher);
1538 options.letter_spacing.to_bits().hash(&mut hasher);
1539 options.font_variation_settings.hash(&mut hasher);
1540 options.text_direction.hash(&mut hasher);
1541 options.font_synthesis.hash(&mut hasher);
1542 generation.hash(&mut hasher);
1543 hasher.finish()
1544}
1545
1546fn cached_shaped(
1547 text: &str,
1548 px: f32,
1549 line_height_ratio: f32,
1550 options: ShapeOptions<'_>,
1551 generation: u64,
1552 fingerprint: u64,
1553) -> Option<Arc<SharedShapedText>> {
1554 let mut cache = shaped_cache().lock().ok()?;
1555 let entries = cache.get_mut(&fingerprint)?;
1556 let entry = entries.iter().find(|entry| {
1557 entry.key.generation == generation
1558 && entry.key.matches(text, px, line_height_ratio, options)
1559 })?;
1560 Some(entry.value.clone())
1561}
1562
1563fn cache_shaped(fingerprint: u64, key: ShapeCacheKey, value: Arc<SharedShapedText>) {
1564 let mut cache = shaped_cache().lock().unwrap();
1565 if let Some(entries) = cache.get_mut(&fingerprint) {
1566 if let Some(entry) = entries.iter_mut().find(|entry| {
1567 entry.key.text == key.text
1568 && entry.key.px_bits == key.px_bits
1569 && entry.key.line_height_bits == key.line_height_bits
1570 && entry.key.family == key.family
1571 && entry.key.font_weight == key.font_weight
1572 && entry.key.font_style == key.font_style
1573 && entry.key.letter_spacing_bits == key.letter_spacing_bits
1574 && entry.key.variation == key.variation
1575 && entry.key.text_direction == key.text_direction
1576 && entry.key.font_synthesis == key.font_synthesis
1577 }) {
1578 entry.key.generation = key.generation;
1579 entry.value = value;
1580 return;
1581 }
1582 entries.push(ShapeCacheEntry { key, value });
1583 return;
1584 }
1585 cache.put(fingerprint, vec![ShapeCacheEntry { key, value }]);
1586}
1587
1588fn cached_legacy_glyphs(
1589 text: &str,
1590 px: f32,
1591 line_height_ratio: f32,
1592 options: ShapeOptions<'_>,
1593 generation: u64,
1594 fingerprint: u64,
1595) -> Option<Arc<[ShapedGlyph]>> {
1596 let mut cache = legacy_shaped_cache().lock().ok()?;
1597 let entries = cache.get_mut(&fingerprint)?;
1598 let entry = entries.iter().find(|entry| {
1599 entry.key.generation == generation
1600 && entry.key.matches(text, px, line_height_ratio, options)
1601 })?;
1602 Some(entry.value.clone())
1603}
1604
1605fn cache_legacy_glyphs(fingerprint: u64, key: ShapeCacheKey, value: Arc<[ShapedGlyph]>) {
1606 let mut cache = legacy_shaped_cache().lock().unwrap();
1607 if let Some(entries) = cache.get_mut(&fingerprint) {
1608 if let Some(entry) = entries.iter_mut().find(|entry| {
1609 entry.key.text == key.text
1610 && entry.key.px_bits == key.px_bits
1611 && entry.key.line_height_bits == key.line_height_bits
1612 && entry.key.family == key.family
1613 && entry.key.font_weight == key.font_weight
1614 && entry.key.font_style == key.font_style
1615 && entry.key.letter_spacing_bits == key.letter_spacing_bits
1616 && entry.key.variation == key.variation
1617 && entry.key.text_direction == key.text_direction
1618 && entry.key.font_synthesis == key.font_synthesis
1619 }) {
1620 entry.key.generation = key.generation;
1621 entry.value = value;
1622 return;
1623 }
1624 entries.push(LegacyShapeCacheEntry { key, value });
1625 return;
1626 }
1627 cache.put(fingerprint, vec![LegacyShapeCacheEntry { key, value }]);
1628}
1629
1630fn synthesis_has_weight(synthesis: &parley::fontique::Synthesis) -> bool {
1631 synthesis.embolden()
1632 || synthesis
1633 .variation_settings()
1634 .iter()
1635 .any(|(tag, _)| tag.to_be_bytes() == *b"wght")
1636}
1637
1638fn synthesis_has_style(synthesis: &parley::fontique::Synthesis) -> bool {
1639 synthesis.skew().is_some()
1640 || synthesis.variation_settings().iter().any(|(tag, _)| {
1641 matches!(
1642 tag.to_be_bytes(),
1643 [b'i', b't', b'a', b'l'] | [b's', b'l', b'n', b't']
1644 )
1645 })
1646}
1647
1648fn validate_shape_options(
1649 options: ShapeOptions<'_>,
1650 resolved_direction: TextDirection,
1651 runs: &[ShapedRun],
1652) -> Result<(), ShapeCapabilityError> {
1653 if options.text_direction != TextDirection::Auto && options.text_direction != resolved_direction
1654 {
1655 return Err(ShapeCapabilityError::RequestedDirectionMismatch);
1656 }
1657 let invalid = match options.font_synthesis {
1658 FontSynthesis::Unspecified | FontSynthesis::All => false,
1659 FontSynthesis::None => runs.iter().any(|run| run.synthesis.any()),
1660 FontSynthesis::Weight => runs.iter().any(|run| synthesis_has_style(&run.synthesis)),
1661 FontSynthesis::Style => runs.iter().any(|run| synthesis_has_weight(&run.synthesis)),
1662 };
1663 if invalid {
1664 Err(ShapeCapabilityError::SynthesisModeUnsupported)
1665 } else {
1666 Ok(())
1667 }
1668}
1669
1670fn shape_vector_inner(
1671 text: &str,
1672 px: f32,
1673 line_height_ratio: f32,
1674 options: ShapeOptions<'_>,
1675 collect_runs: bool,
1676) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1677 let generation = font_generation();
1678 let fingerprint = shape_fingerprint(text, px, line_height_ratio, options, generation);
1679 if let Some(value) = cached_shaped(
1680 text,
1681 px,
1682 line_height_ratio,
1683 options,
1684 generation,
1685 fingerprint,
1686 ) {
1687 return Ok(value);
1688 }
1689 let mut eng = engine().lock().unwrap();
1690 let layout = build_layout(
1691 &mut eng,
1692 text,
1693 px,
1694 line_height_ratio,
1695 options.font_family,
1696 options.font_weight,
1697 options.font_style,
1698 options.letter_spacing,
1699 options.font_variation_settings,
1700 );
1701 report_unresolved_codepoints(&layout, text);
1702 let resolved_direction = if layout.is_rtl() {
1703 TextDirection::Rtl
1704 } else {
1705 TextDirection::Ltr
1706 };
1707 let variation = canonical_variation(options.font_variation_settings);
1708 let (glyphs, runs) = collect_shaped_layout(&mut eng, layout, px, collect_runs, variation);
1709 validate_shape_options(options, resolved_direction, &runs)?;
1710 let value = Arc::new(SharedShapedText {
1711 glyphs: Arc::from(glyphs.into_boxed_slice()),
1712 requested_text_direction: options.text_direction,
1713 resolved_text_direction: resolved_direction,
1714 runs: Arc::from(runs.into_boxed_slice()),
1715 });
1716 let key = ShapeCacheKey::new(text, px, line_height_ratio, options, generation);
1717 drop(eng);
1718 cache_shaped(fingerprint, key, value.clone());
1719 Ok(value)
1720}
1721
1722fn materialize_legacy_glyphs(
1723 shaped: &SharedShapedText,
1724 text: &str,
1725 px: f32,
1726 line_height_ratio: f32,
1727 options: ShapeOptions<'_>,
1728) -> Vec<ShapedGlyph> {
1729 let generation = font_generation();
1730 let fingerprint = shape_fingerprint(text, px, line_height_ratio, options, generation);
1731 if let Some(cached) = cached_legacy_glyphs(
1732 text,
1733 px,
1734 line_height_ratio,
1735 options,
1736 generation,
1737 fingerprint,
1738 ) {
1739 return cached.as_ref().to_vec();
1740 }
1741 let mut glyphs = shaped.glyphs.as_ref().to_vec();
1742 let mut eng = engine().lock().unwrap();
1743 for glyph in &mut glyphs {
1744 if let Some((width, height, left, top)) =
1745 eng.raster_placement(glyph.font_id, glyph.glyph_id, glyph.px, None)
1746 {
1747 glyph.w = width;
1748 glyph.h = height;
1749 glyph.bearing_x = left;
1750 glyph.bearing_y = top;
1751 glyph.rasterized = true;
1752 }
1753 }
1754 drop(eng);
1755 let value: Arc<[ShapedGlyph]> = Arc::from(glyphs.into_boxed_slice());
1756 cache_legacy_glyphs(
1757 fingerprint,
1758 ShapeCacheKey::new(text, px, line_height_ratio, options, generation),
1759 value.clone(),
1760 );
1761 value.as_ref().to_vec()
1762}
1763
1764pub fn shape_line_vector(
1765 text: &str,
1766 px: f32,
1767 line_height_ratio: f32,
1768 font_family: Option<&str>,
1769 font_weight: u16,
1770 font_style: u8,
1771 letter_spacing: f32,
1772 font_variation_settings: Option<&str>,
1773) -> Arc<SharedShapedText> {
1774 shape_vector_inner(
1775 text,
1776 px,
1777 line_height_ratio,
1778 ShapeOptions {
1779 font_family,
1780 font_weight,
1781 font_style,
1782 letter_spacing,
1783 font_variation_settings,
1784 ..ShapeOptions::default()
1785 },
1786 true,
1787 )
1788 .unwrap_or_else(|_| {
1789 Arc::new(SharedShapedText {
1790 glyphs: Arc::from(Vec::new()),
1791 requested_text_direction: TextDirection::Auto,
1792 resolved_text_direction: TextDirection::Ltr,
1793 runs: Arc::from(Vec::new()),
1794 })
1795 })
1796}
1797
1798pub fn shape_line_cached(
1799 text: &str,
1800 px: f32,
1801 line_height_ratio: f32,
1802 font_family: Option<&str>,
1803 font_weight: u16,
1804 font_style: u8,
1805 letter_spacing: f32,
1806 font_variation_settings: Option<&str>,
1807) -> Arc<SharedShapedText> {
1808 shape_line_vector(
1809 text,
1810 px,
1811 line_height_ratio,
1812 font_family,
1813 font_weight,
1814 font_style,
1815 letter_spacing,
1816 font_variation_settings,
1817 )
1818}
1819
1820pub fn shape_line_shared(
1821 text: &str,
1822 px: f32,
1823 line_height_ratio: f32,
1824 font_family: Option<&str>,
1825 font_weight: u16,
1826 font_style: u8,
1827 letter_spacing: f32,
1828 font_variation_settings: Option<&str>,
1829) -> Arc<SharedShapedText> {
1830 shape_line_vector(
1831 text,
1832 px,
1833 line_height_ratio,
1834 font_family,
1835 font_weight,
1836 font_style,
1837 letter_spacing,
1838 font_variation_settings,
1839 )
1840}
1841
1842pub fn shape_text_with_options_vector(
1843 text: &str,
1844 px: f32,
1845 line_height_ratio: f32,
1846 options: ShapeOptions<'_>,
1847) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1848 shape_vector_inner(text, px, line_height_ratio, options, true)
1849}
1850
1851pub fn shape_text_with_options_shared(
1852 text: &str,
1853 px: f32,
1854 line_height_ratio: f32,
1855 options: ShapeOptions<'_>,
1856) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1857 shape_text_with_options_vector(text, px, line_height_ratio, options)
1858}
1859
1860pub fn shape_text_with_options_cached(
1861 text: &str,
1862 px: f32,
1863 line_height_ratio: f32,
1864 options: ShapeOptions<'_>,
1865) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1866 shape_text_with_options_vector(text, px, line_height_ratio, options)
1867}
1868
1869pub fn shape_line(
1870 text: &str,
1871 px: f32,
1872 line_height_ratio: f32,
1873 font_family: Option<&str>,
1874 font_weight: u16,
1875 font_style: u8,
1876 letter_spacing: f32,
1877 font_variation_settings: Option<&str>,
1878) -> Vec<ShapedGlyph> {
1879 let options = ShapeOptions {
1880 font_family,
1881 font_weight,
1882 font_style,
1883 letter_spacing,
1884 font_variation_settings,
1885 ..ShapeOptions::default()
1886 };
1887 materialize_legacy_glyphs(
1888 &shape_line_vector(
1889 text,
1890 px,
1891 line_height_ratio,
1892 font_family,
1893 font_weight,
1894 font_style,
1895 letter_spacing,
1896 font_variation_settings,
1897 ),
1898 text,
1899 px,
1900 line_height_ratio,
1901 options,
1902 )
1903}
1904
1905pub fn shape_text_with_options(
1906 text: &str,
1907 px: f32,
1908 line_height_ratio: f32,
1909 options: ShapeOptions<'_>,
1910) -> Result<ShapedText, ShapeCapabilityError> {
1911 let shaped = shape_text_with_options_vector(text, px, line_height_ratio, options)?;
1912 Ok(ShapedText {
1913 glyphs: materialize_legacy_glyphs(&shaped, text, px, line_height_ratio, options),
1914 requested_text_direction: shaped.requested_text_direction,
1915 resolved_text_direction: shaped.resolved_text_direction,
1916 runs: shaped.runs.as_ref().to_vec(),
1917 })
1918}
1919
1920pub fn shape_line_with_options_vector(
1921 text: &str,
1922 px: f32,
1923 line_height_ratio: f32,
1924 options: ShapeOptions<'_>,
1925) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1926 shape_text_with_options_vector(text, px, line_height_ratio, options)
1927}
1928
1929pub fn shape_line_with_options_cached(
1930 text: &str,
1931 px: f32,
1932 line_height_ratio: f32,
1933 options: ShapeOptions<'_>,
1934) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1935 shape_line_with_options_vector(text, px, line_height_ratio, options)
1936}
1937
1938pub fn shape_line_with_options(
1939 text: &str,
1940 px: f32,
1941 line_height_ratio: f32,
1942 options: ShapeOptions<'_>,
1943) -> Result<Vec<ShapedGlyph>, ShapeCapabilityError> {
1944 shape_text_with_options(text, px, line_height_ratio, options).map(|shaped| shaped.glyphs)
1945}
1946
1947fn rasterize_locked(
1948 eng: &mut Engine,
1949 key: GlyphKey,
1950 cache_key: &CacheKey,
1951 px: f32,
1952) -> Option<GlyphBitmap> {
1953 use swash::scale::{Render, Source, StrikeWith};
1954 let fid = cache_key.font_id;
1955 let gid = cache_key.glyph_id;
1956 let raster_key = (fid, gid, px.to_bits(), cache_key.variation.clone());
1957 if let Some(cached) = eng.glyph_cache.get(&raster_key) {
1958 log::debug!(
1959 "[rasterize] HIT fid={} gid={} px={} => {}x{}",
1960 fid,
1961 gid,
1962 px,
1963 cached.0,
1964 cached.1
1965 );
1966 return Some(GlyphBitmap {
1967 key,
1968 w: cached.0,
1969 h: cached.1,
1970 content: cached.4,
1971 data: cached.5.clone(),
1972 });
1973 }
1974 let (data_bytes, face_index) = {
1975 let record = eng.font_registry.iter().find(|r| r.id == fid)?;
1976 (record.data_bytes.clone(), record.face_index)
1977 };
1978 let face_index = usize::try_from(face_index).ok()?;
1979 let font = swash::FontRef::from_index(data_bytes.as_ref(), face_index)?;
1980 let mut builder = eng.swash_cx.builder(font).size(px).hint(true);
1981 let settings = swash_settings(cache_key.variation.as_deref());
1982 if !settings.is_empty() {
1983 builder = builder.variations(settings);
1984 }
1985 let mut scaler = builder.build();
1986 let image = Render::new(&[
1987 Source::Outline,
1988 Source::ColorBitmap(StrikeWith::BestFit),
1989 Source::ColorOutline(0),
1990 ])
1991 .render(&mut scaler, swash_glyph_id(fid, gid))?;
1992 if image.placement.width > MAX_RASTER_DIMENSION
1993 || image.placement.height > MAX_RASTER_DIMENSION
1994 || image.data.len() > MAX_RASTER_BYTES
1995 {
1996 return None;
1997 }
1998 log::debug!(
1999 "[rasterize] MISS fid={} gid={} px={} => {}x{}",
2000 fid,
2001 gid,
2002 px,
2003 image.placement.width,
2004 image.placement.height
2005 );
2006 let bitmap = GlyphBitmap {
2007 key,
2008 w: image.placement.width,
2009 h: image.placement.height,
2010 content: image.content,
2011 data: image.data,
2012 };
2013 let data_len = bitmap.data.len();
2014 let previous = eng.glyph_cache.insert(
2015 raster_key,
2016 (
2017 bitmap.w,
2018 bitmap.h,
2019 image.placement.left,
2020 image.placement.top,
2021 bitmap.content,
2022 bitmap.data.clone(),
2023 ),
2024 );
2025 if let Some(previous) = previous {
2026 eng.glyph_cache_bytes = eng.glyph_cache_bytes.saturating_sub(previous.5.len());
2027 }
2028 eng.glyph_cache_bytes = eng.glyph_cache_bytes.saturating_add(data_len);
2029 eng.trim_glyph_cache();
2030 Some(bitmap)
2031}
2032
2033pub fn rasterize(key: GlyphKey, px: f32) -> Option<GlyphBitmap> {
2039 let mut eng = engine().lock().unwrap();
2040 let &(fid, gid) = eng.key_map.get(&key)?;
2041 let px = normalized_raster_px(px)?;
2042 let cache_key = CacheKey {
2043 font_id: fid,
2044 glyph_id: gid,
2045 font_size_bits: px.to_bits(),
2046 variation: None,
2047 };
2048 rasterize_locked(&mut eng, key, &cache_key, px)
2049}
2050
2051pub fn rasterize_cache_key(cache_key: &CacheKey) -> Option<GlyphBitmap> {
2052 let px = f32::from_bits(cache_key.font_size_bits);
2053 let normalized = normalized_raster_px(px)?;
2054 let mut eng = engine().lock().unwrap();
2055 rasterize_locked(
2056 &mut eng,
2057 key_from_pair(cache_key.font_id, cache_key.glyph_id),
2058 cache_key,
2059 normalized,
2060 )
2061}
2062
2063pub fn raster_placement(cache_key: &CacheKey) -> Option<(f32, f32, f32, f32)> {
2064 let px = f32::from_bits(cache_key.font_size_bits);
2065 let mut eng = engine().lock().unwrap();
2066 eng.raster_placement(
2067 cache_key.font_id,
2068 cache_key.glyph_id,
2069 px,
2070 cache_key.variation.clone(),
2071 )
2072}
2073
2074pub fn lookup_cache_key(key: GlyphKey, px: f32) -> Option<CacheKey> {
2080 let eng = engine().lock().unwrap();
2081 let &(fid, gid) = eng.key_map.get(&key)?;
2082 let px = normalized_raster_px(px)?;
2083 Some(CacheKey {
2084 font_id: fid,
2085 glyph_id: gid,
2086 font_size_bits: px.to_bits(),
2087 variation: None,
2088 })
2089}
2090
2091fn extract_outlines_for(
2092 data_bytes: &[u8],
2093 face_index: u32,
2094 glyph_id: u32,
2095 variation: Option<&str>,
2096) -> Option<Box<[Command]>> {
2097 let font = skrifa::FontRef::from_index(data_bytes, face_index).ok()?;
2098 let mut pen = OutlinePenCollector(Vec::new());
2099 let glyph = font.outline_glyphs().get(skrifa::GlyphId::new(glyph_id))?;
2100 let settings = decode_variation(variation);
2101 if settings.is_empty() {
2102 glyph
2103 .draw(skrifa::instance::Size::new(1.0), &mut pen)
2104 .ok()?;
2105 } else {
2106 let location = font.axes().location(settings);
2107 glyph
2108 .draw(
2109 skrifa::outline::DrawSettings::unhinted(
2110 skrifa::instance::Size::new(1.0),
2111 &location,
2112 ),
2113 &mut pen,
2114 )
2115 .ok()?;
2116 }
2117 Some(pen.0.into_boxed_slice())
2118}
2119
2120pub fn extract_outline_commands_shared(cache_key: &CacheKey) -> Option<Arc<[Command]>> {
2121 let cache_identity = (
2122 cache_key.font_id,
2123 cache_key.glyph_id,
2124 cache_key.variation.clone(),
2125 );
2126 if let Some(cached) = engine()
2127 .lock()
2128 .unwrap()
2129 .outline_cache
2130 .get(&cache_identity)
2131 .cloned()
2132 {
2133 return Some(cached);
2134 }
2135 let (data_bytes, face_index) = {
2136 let eng = engine().lock().unwrap();
2137 let record = eng
2138 .font_registry
2139 .iter()
2140 .find(|record| record.id == cache_key.font_id)?;
2141 (record.data_bytes.clone(), record.face_index)
2142 };
2143 let commands = extract_outlines_for(
2144 data_bytes.as_ref(),
2145 face_index,
2146 cache_key.glyph_id,
2147 cache_key.variation.as_deref(),
2148 )?;
2149 let commands: Arc<[Command]> = Arc::from(commands);
2150 engine()
2151 .lock()
2152 .unwrap()
2153 .outline_cache
2154 .put(cache_identity, commands.clone());
2155 Some(commands)
2156}
2157
2158pub fn extract_outline_commands(cache_key: &CacheKey) -> Option<Box<[Command]>> {
2159 extract_outline_commands_shared(cache_key)
2160 .map(|commands| commands.as_ref().to_vec().into_boxed_slice())
2161}
2162
2163pub fn extract_outline_commands_for(cache_key: &CacheKey) -> Option<Box<[Command]>> {
2164 extract_outline_commands(cache_key)
2165}
2166
2167pub fn lookup_and_extract_outline(key: GlyphKey, px: f32) -> Option<(CacheKey, Box<[Command]>)> {
2170 let (fid, gid) = {
2171 let eng = engine().lock().unwrap();
2172 *eng.key_map.get(&key)?
2173 };
2174 let px = normalized_raster_px(px)?;
2175 let ck = CacheKey {
2176 font_id: fid,
2177 glyph_id: gid,
2178 font_size_bits: px.to_bits(),
2179 variation: None,
2180 };
2181 let commands = extract_outline_commands_shared(&ck)?;
2182 Some((ck, commands.as_ref().to_vec().into_boxed_slice()))
2183}
2184
2185struct OutlinePenCollector(Vec<Command>);
2186
2187impl OutlinePen for OutlinePenCollector {
2188 fn move_to(&mut self, x: f32, y: f32) {
2189 self.0.push(Command::MoveTo(x, y));
2190 }
2191 fn line_to(&mut self, x: f32, y: f32) {
2192 self.0.push(Command::LineTo(x, y));
2193 }
2194 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
2195 self.0.push(Command::QuadTo(cx0, cy0, x, y));
2196 }
2197 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
2198 self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y));
2199 }
2200 fn close(&mut self) {
2201 self.0.push(Command::Close);
2202 }
2203}
2204
2205#[derive(Clone)]
2206pub struct TextMetrics {
2207 pub positions: Vec<f32>,
2208 pub byte_offsets: Vec<usize>,
2209}
2210
2211struct SegmentExtrema {
2212 len: usize,
2213 base: usize,
2214 min: Vec<f32>,
2215 max: Vec<f32>,
2216 prefix_min: Vec<f32>,
2217 prefix_max: Vec<f32>,
2218 has_prefix: bool,
2219}
2220
2221impl SegmentExtrema {
2222 fn new(len: usize, has_prefix: bool) -> Self {
2223 let len = len.max(1);
2224 let base = len.next_power_of_two();
2225 Self {
2226 len,
2227 base,
2228 min: vec![f32::INFINITY; base * 2],
2229 max: vec![f32::NEG_INFINITY; base * 2],
2230 prefix_min: Vec::with_capacity(if has_prefix { len + 1 } else { 0 }),
2231 prefix_max: Vec::with_capacity(if has_prefix { len + 1 } else { 0 }),
2232 has_prefix,
2233 }
2234 }
2235
2236 fn set(&mut self, index: usize, value: f32) {
2237 if value.is_finite() && index < self.len {
2238 self.min[self.base + index] = value;
2239 self.max[self.base + index] = value;
2240 }
2241 }
2242
2243 fn apply(&mut self, start: usize, end: usize, min_value: f32, max_value: f32) {
2244 if start >= end {
2245 return;
2246 }
2247 let mut left = start.min(self.len) + self.base;
2248 let mut right = end.min(self.len) + self.base;
2249 while left < right {
2250 if left & 1 == 1 {
2251 self.min[left] = self.min[left].min(min_value);
2252 self.max[left] = self.max[left].max(max_value);
2253 left += 1;
2254 }
2255 if right & 1 == 1 {
2256 right -= 1;
2257 self.min[right] = self.min[right].min(min_value);
2258 self.max[right] = self.max[right].max(max_value);
2259 }
2260 left >>= 1;
2261 right >>= 1;
2262 }
2263 }
2264
2265 fn finish(&mut self) {
2266 for index in 1..self.base {
2267 let min_value = self.min[index];
2268 let max_value = self.max[index];
2269 if min_value.is_finite() {
2270 self.min[index * 2] = self.min[index * 2].min(min_value);
2271 self.min[index * 2 + 1] = self.min[index * 2 + 1].min(min_value);
2272 }
2273 if max_value.is_finite() {
2274 self.max[index * 2] = self.max[index * 2].max(max_value);
2275 self.max[index * 2 + 1] = self.max[index * 2 + 1].max(max_value);
2276 }
2277 }
2278 for index in (1..self.base).rev() {
2279 self.min[index] = self.min[index * 2].min(self.min[index * 2 + 1]);
2280 self.max[index] = self.max[index * 2].max(self.max[index * 2 + 1]);
2281 }
2282 if self.has_prefix {
2283 self.prefix_min.push(f32::INFINITY);
2284 self.prefix_max.push(f32::NEG_INFINITY);
2285 for index in 0..self.len {
2286 let value_min = self.min[self.base + index];
2287 let value_max = self.max[self.base + index];
2288 let previous_min = *self.prefix_min.last().unwrap_or(&f32::INFINITY);
2289 let previous_max = *self.prefix_max.last().unwrap_or(&f32::NEG_INFINITY);
2290 self.prefix_min.push(previous_min.min(value_min));
2291 self.prefix_max.push(previous_max.max(value_max));
2292 }
2293 }
2294 }
2295
2296 fn query(&self, start: usize, end: usize) -> (f32, f32) {
2297 let start = start.min(self.len);
2298 let end = end.min(self.len).max(start);
2299 if start == end {
2300 return (f32::INFINITY, f32::NEG_INFINITY);
2301 }
2302 if start == 0 && self.has_prefix {
2303 return (self.prefix_min[end], self.prefix_max[end]);
2304 }
2305 let mut left = start + self.base;
2306 let mut right = end + self.base;
2307 let mut min_value = f32::INFINITY;
2308 let mut max_value = f32::NEG_INFINITY;
2309 while left < right {
2310 if left & 1 == 1 {
2311 min_value = min_value.min(self.min[left]);
2312 max_value = max_value.max(self.max[left]);
2313 left += 1;
2314 }
2315 if right & 1 == 1 {
2316 right -= 1;
2317 min_value = min_value.min(self.min[right]);
2318 max_value = max_value.max(self.max[right]);
2319 }
2320 left >>= 1;
2321 right >>= 1;
2322 }
2323 (min_value, max_value)
2324 }
2325}
2326
2327struct RangeExtremum {
2328 positions: SegmentExtrema,
2329 extents: SegmentExtrema,
2330 coordinates: Vec<usize>,
2331}
2332
2333impl RangeExtremum {
2334 fn from_measurement(text: &str, metrics: &TextMetrics, extents: &[VisualExtent]) -> Self {
2335 let mut coordinates = metrics.byte_offsets.clone();
2336 coordinates.extend(text.char_indices().map(|(index, _)| index));
2337 coordinates.push(text.len());
2338 for extent in extents {
2339 coordinates.push(extent.start);
2340 coordinates.push(extent.end);
2341 }
2342 coordinates.sort_unstable();
2343 coordinates.dedup();
2344 let mut positions = SegmentExtrema::new(metrics.positions.len(), true);
2345 for (index, value) in metrics.positions.iter().copied().enumerate() {
2346 positions.set(index, value);
2347 }
2348 let mut range_extents = SegmentExtrema::new(coordinates.len().saturating_sub(1), false);
2349 for extent in extents {
2350 let left = extent.left.min(extent.right);
2351 let right = extent.left.max(extent.right);
2352 if !left.is_finite() || !right.is_finite() {
2353 continue;
2354 }
2355 let start = coordinates.partition_point(|byte| *byte < extent.start);
2356 let end = coordinates.partition_point(|byte| *byte < extent.end);
2357 range_extents.apply(start, end, left, right);
2358 }
2359 positions.finish();
2360 range_extents.finish();
2361 Self {
2362 positions,
2363 extents: range_extents,
2364 coordinates,
2365 }
2366 }
2367
2368 fn query_bytes(&self, offsets: &[usize], start: usize, end: usize) -> (f32, f32) {
2369 let position_start = offsets.partition_point(|byte| *byte < start);
2370 let position_end = offsets.partition_point(|byte| *byte <= end);
2371 let (position_min, position_max) = self.positions.query(position_start, position_end);
2372 let extent_start = self.coordinates.partition_point(|byte| *byte < start);
2373 let extent_end = self.coordinates.partition_point(|byte| *byte < end);
2374 let (extent_min, extent_max) = self.extents.query(extent_start, extent_end);
2375 (position_min.min(extent_min), position_max.max(extent_max))
2376 }
2377}
2378
2379#[derive(Clone, Copy)]
2380struct LogicalPart {
2381 start: usize,
2382 end: usize,
2383 advance: f32,
2384}
2385
2386struct LogicalGroup {
2387 start: usize,
2388 end: usize,
2389 first_part: Option<LogicalPart>,
2390 extra_parts: Vec<LogicalPart>,
2391 has_ligature_start: bool,
2392 advance: f32,
2393}
2394
2395impl LogicalGroup {
2396 fn parts(&self) -> impl Iterator<Item = &LogicalPart> {
2397 self.first_part.iter().chain(self.extra_parts.iter())
2398 }
2399}
2400
2401#[derive(Clone, Copy, PartialEq, Eq, Debug)]
2402struct AtomicRange {
2403 start: usize,
2404 end: usize,
2405}
2406
2407#[derive(Clone, Copy)]
2408struct VisualExtent {
2409 start: usize,
2410 end: usize,
2411 left: f32,
2412 right: f32,
2413}
2414
2415#[derive(Clone, Copy, PartialEq, Eq)]
2416enum EdgeKind {
2417 Start,
2418 Internal,
2419 End,
2420}
2421
2422#[derive(Clone, Copy)]
2423struct EdgeCandidate {
2424 byte: usize,
2425 position: f32,
2426 kind: EdgeKind,
2427}
2428
2429struct LayoutEdges {
2430 edges: Vec<(usize, f32)>,
2431 extents: Vec<VisualExtent>,
2432 atomic_ranges: Vec<AtomicRange>,
2433}
2434
2435struct LineMeasurement {
2436 metrics: TextMetrics,
2437 atomic_ranges: Vec<AtomicRange>,
2438 extrema: RangeExtremum,
2439}
2440
2441#[derive(Clone, Copy)]
2442struct HardLine {
2443 start: usize,
2444 end: usize,
2445 next_start: usize,
2446}
2447
2448fn hard_lines(text: &str) -> Vec<HardLine> {
2449 let mut lines = Vec::new();
2450 let mut start = 0usize;
2451 let mut chars = text.char_indices().peekable();
2452 while let Some((index, ch)) = chars.next() {
2453 let next_start = match ch {
2454 '\n' => Some(index + 1),
2455 '\u{2028}' | '\u{2029}' => Some(index + ch.len_utf8()),
2456 '\r' => {
2457 if text[index + 1..].starts_with('\n') {
2458 chars.next();
2459 Some(index + 2)
2460 } else {
2461 Some(index + 1)
2462 }
2463 }
2464 _ => None,
2465 };
2466 if let Some(next_start) = next_start {
2467 lines.push(HardLine {
2468 start,
2469 end: index,
2470 next_start,
2471 });
2472 start = next_start;
2473 }
2474 }
2475 lines.push(HardLine {
2476 start,
2477 end: text.len(),
2478 next_start: text.len(),
2479 });
2480 lines
2481}
2482
2483fn finish_logical_group(mut group: LogicalGroup) -> LogicalGroup {
2484 if let Some(part) = &mut group.first_part
2485 && !part.advance.is_finite()
2486 {
2487 part.advance = 0.0;
2488 }
2489 for part in &mut group.extra_parts {
2490 if !part.advance.is_finite() {
2491 part.advance = 0.0;
2492 }
2493 }
2494 group.advance = group.parts().map(|part| part.advance).sum();
2495 if !group.advance.is_finite() {
2496 group.advance = 0.0;
2497 }
2498 group.end = group.end.max(group.start);
2499 group
2500}
2501
2502fn logical_groups_for_run<B: parley::style::Brush>(
2503 run: parley::layout::Run<'_, B>,
2504) -> Vec<LogicalGroup> {
2505 let mut groups = Vec::new();
2506 let mut current: Option<LogicalGroup> = None;
2507 for cluster in run.clusters() {
2508 let range = cluster.text_range();
2509 let part = LogicalPart {
2510 start: range.start,
2511 end: range.end,
2512 advance: cluster.advance(),
2513 };
2514 let is_ligature = cluster.is_ligature_start() || cluster.is_ligature_continuation();
2515 if !is_ligature {
2516 if let Some(group) = current.take() {
2517 groups.push(finish_logical_group(group));
2518 }
2519 groups.push(finish_logical_group(LogicalGroup {
2520 start: part.start,
2521 end: part.end,
2522 first_part: Some(part),
2523 extra_parts: Vec::new(),
2524 has_ligature_start: false,
2525 advance: 0.0,
2526 }));
2527 continue;
2528 }
2529 let starts_new_group = cluster.is_ligature_start()
2530 && current
2531 .as_ref()
2532 .is_some_and(|group| group.has_ligature_start);
2533 if starts_new_group && let Some(group) = current.take() {
2534 groups.push(finish_logical_group(group));
2535 }
2536 let group = current.get_or_insert_with(|| LogicalGroup {
2537 start: part.start,
2538 end: part.end,
2539 first_part: None,
2540 extra_parts: Vec::new(),
2541 has_ligature_start: false,
2542 advance: 0.0,
2543 });
2544 group.start = group.start.min(part.start);
2545 group.end = group.end.max(part.end);
2546 if group.first_part.is_none() {
2547 group.first_part = Some(part);
2548 } else {
2549 group.extra_parts.push(part);
2550 }
2551 group.has_ligature_start |= cluster.is_ligature_start();
2552 }
2553 if let Some(group) = current {
2554 groups.push(finish_logical_group(group));
2555 }
2556 groups
2557}
2558
2559fn add_grapheme_edges(
2560 text: &str,
2561 range: std::ops::Range<usize>,
2562 start_x: f32,
2563 end_x: f32,
2564 candidates: &mut Vec<EdgeCandidate>,
2565) {
2566 let start = range.start.min(text.len());
2567 let end = range.end.min(text.len()).max(start);
2568 let Some(slice) = text.get(start..end) else {
2569 return;
2570 };
2571 let count = slice.graphemes(true).count();
2572 if count == 0 {
2573 candidates.push(EdgeCandidate {
2574 byte: start,
2575 position: start_x,
2576 kind: EdgeKind::Start,
2577 });
2578 candidates.push(EdgeCandidate {
2579 byte: end,
2580 position: end_x,
2581 kind: EdgeKind::End,
2582 });
2583 return;
2584 }
2585 candidates.push(EdgeCandidate {
2586 byte: start,
2587 position: start_x,
2588 kind: EdgeKind::Start,
2589 });
2590 for (index, (offset, grapheme)) in slice.grapheme_indices(true).enumerate() {
2591 let fraction = (index + 1) as f32 / count as f32;
2592 let position = start_x + (end_x - start_x) * fraction;
2593 let kind = if index + 1 == count {
2594 EdgeKind::End
2595 } else {
2596 EdgeKind::Internal
2597 };
2598 candidates.push(EdgeCandidate {
2599 byte: start + offset + grapheme.len(),
2600 position,
2601 kind,
2602 });
2603 }
2604}
2605
2606fn edge_priority(kind: EdgeKind) -> u8 {
2607 match kind {
2608 EdgeKind::Internal => 0,
2609 EdgeKind::Start => 1,
2610 EdgeKind::End => 2,
2611 }
2612}
2613
2614fn resolve_edge_candidates(
2615 mut candidates: Vec<EdgeCandidate>,
2616 text_len: usize,
2617) -> Vec<(usize, f32)> {
2618 candidates.sort_by_key(|candidate| candidate.byte);
2619 let mut edges = BTreeMap::new();
2620 for candidate in candidates {
2621 let replace = match edges.get(&candidate.byte) {
2622 None => true,
2623 Some((_, kind)) => edge_priority(candidate.kind) > edge_priority(*kind),
2624 };
2625 if replace {
2626 edges.insert(candidate.byte, (candidate.position, candidate.kind));
2627 }
2628 }
2629 edges.entry(0).or_insert((0.0, EdgeKind::Start));
2630 edges.entry(text_len).or_insert((0.0, EdgeKind::End));
2631 edges
2632 .into_iter()
2633 .map(|(byte, (position, _))| (byte, position))
2634 .collect()
2635}
2636
2637fn normalized_atomic_ranges(mut ranges: Vec<AtomicRange>) -> Vec<AtomicRange> {
2638 ranges.retain(|range| range.end > range.start);
2639 ranges.sort_by_key(|range| (range.start, range.end));
2640 let mut result: Vec<AtomicRange> = Vec::new();
2641 for range in ranges {
2642 if let Some(previous) = result.last_mut()
2643 && (range.start < previous.end || range.start == previous.start)
2644 {
2645 previous.end = previous.end.max(range.end);
2646 } else {
2647 result.push(range);
2648 }
2649 }
2650 result
2651}
2652
2653fn atomic_ranges_for_layout(text: &str, layout: &parley::Layout<()>) -> Vec<AtomicRange> {
2654 let mut clusters = Vec::new();
2655 let mut seen = HashSet::new();
2656 for line in layout.lines() {
2657 for run in line.runs() {
2658 for cluster in run.clusters() {
2659 let range = cluster.text_range();
2660 let candidate = (
2661 range.start.min(text.len()),
2662 range.end.min(text.len()),
2663 cluster.is_ligature_start(),
2664 cluster.is_ligature_continuation(),
2665 );
2666 if seen.insert(candidate) {
2667 clusters.push(candidate);
2668 }
2669 }
2670 }
2671 }
2672 clusters.sort_by_key(|(start, end, _, _)| (*start, *end));
2673 let mut result = Vec::new();
2674 let mut current: Option<AtomicRange> = None;
2675 let mut has_start = false;
2676 for (start, end, is_start, is_continuation) in clusters {
2677 if end <= start {
2678 continue;
2679 }
2680 if !is_start && !is_continuation {
2681 if let Some(range) = current.take() {
2682 result.push(range);
2683 }
2684 has_start = false;
2685 result.push(AtomicRange { start, end });
2686 continue;
2687 }
2688 if is_start
2689 && has_start
2690 && let Some(range) = current.take()
2691 {
2692 result.push(range);
2693 has_start = false;
2694 }
2695 let range = current.get_or_insert(AtomicRange { start, end });
2696 range.start = range.start.min(start);
2697 range.end = range.end.max(end);
2698 has_start |= is_start;
2699 }
2700 if let Some(range) = current {
2701 result.push(range);
2702 }
2703 normalized_atomic_ranges(result)
2704}
2705
2706fn layout_edges_for_layout(text: &str, layout: &parley::Layout<()>) -> LayoutEdges {
2707 let mut candidates = Vec::new();
2708 let mut extents = Vec::new();
2709
2710 for line in layout.lines() {
2711 let line_range = line.text_range();
2712 let line_start = line_range.start.min(text.len());
2713 let line_end = line_range.end.min(text.len()).max(line_start);
2714 candidates.push(EdgeCandidate {
2715 byte: line_start,
2716 position: 0.0,
2717 kind: EdgeKind::Internal,
2718 });
2719 if line_end > line_start {
2720 candidates.push(EdgeCandidate {
2721 byte: line_end,
2722 position: 0.0,
2723 kind: EdgeKind::Internal,
2724 });
2725 }
2726
2727 let inline_min = line.metrics().inline_min_coord;
2728 for run in line.runs() {
2729 let mut visual_clusters = HashMap::with_capacity(run.len());
2730 for cluster in run.visual_clusters() {
2731 let range = cluster.text_range();
2732 visual_clusters
2733 .entry((range.start, range.end))
2734 .or_insert_with(|| cluster.visual_offset());
2735 }
2736 let groups = logical_groups_for_run(run);
2737 for group in &groups {
2738 let mut left = f32::INFINITY;
2739 let mut right = f32::NEG_INFINITY;
2740 for part in group.parts() {
2741 let Some(offset) = visual_clusters
2742 .get(&(part.start, part.end))
2743 .copied()
2744 .flatten()
2745 else {
2746 continue;
2747 };
2748 let offset = offset + inline_min;
2749 let advance = if part.advance.is_finite() {
2750 part.advance
2751 } else {
2752 0.0
2753 };
2754 left = left.min(offset).min(offset + advance);
2755 right = right.max(offset).max(offset + advance);
2756 let (start_x, end_x) = if run.is_rtl() {
2757 (offset + advance, offset)
2758 } else {
2759 (offset, offset + advance)
2760 };
2761 add_grapheme_edges(text, part.start..part.end, start_x, end_x, &mut candidates);
2762 }
2763 if left.is_finite() && right.is_finite() {
2764 extents.push(VisualExtent {
2765 start: group.start.min(text.len()),
2766 end: group.end.min(text.len()),
2767 left,
2768 right,
2769 });
2770 }
2771 }
2772 }
2773 }
2774
2775 LayoutEdges {
2776 edges: resolve_edge_candidates(candidates, text.len()),
2777 extents,
2778 atomic_ranges: atomic_ranges_for_layout(text, layout),
2779 }
2780}
2781
2782fn visual_width(measurement: &LineMeasurement, start: usize, end: usize) -> f32 {
2783 let (min, max) = measurement
2784 .extrema
2785 .query_bytes(&measurement.metrics.byte_offsets, start, end);
2786 if min.is_finite() && max.is_finite() {
2787 (max - min).max(0.0)
2788 } else {
2789 0.0
2790 }
2791}
2792
2793fn edge_position(edges: &[(usize, f32)], byte: usize) -> f32 {
2794 match edges.binary_search_by_key(&byte, |edge| edge.0) {
2795 Ok(index) => edges[index].1,
2796 Err(0) => edges.first().map(|edge| edge.1).unwrap_or(0.0),
2797 Err(index) => edges
2798 .get(index.saturating_sub(1))
2799 .map(|edge| edge.1)
2800 .unwrap_or(0.0),
2801 }
2802}
2803
2804fn metrics_from_edges(text: &str, edges: &[(usize, f32)]) -> TextMetrics {
2805 let mut byte_offsets = Vec::with_capacity(text.graphemes(true).count() + 1);
2806 byte_offsets.push(0);
2807 for (byte, _) in text.grapheme_indices(true) {
2808 if byte != 0 {
2809 byte_offsets.push(byte);
2810 }
2811 }
2812 if *byte_offsets.last().unwrap_or(&0) != text.len() {
2813 byte_offsets.push(text.len());
2814 }
2815 let positions = byte_offsets
2816 .iter()
2817 .map(|byte| edge_position(edges, *byte))
2818 .collect();
2819 TextMetrics {
2820 positions,
2821 byte_offsets,
2822 }
2823}
2824
2825fn measure_line_with_engine(
2826 eng: &mut Engine,
2827 text: &str,
2828 px: f32,
2829 font_family: Option<&str>,
2830 font_weight: u16,
2831 font_style: u8,
2832 letter_spacing: f32,
2833 font_variation_settings: Option<&str>,
2834) -> LineMeasurement {
2835 let layout = build_layout(
2836 eng,
2837 text,
2838 px,
2839 0.0,
2840 font_family,
2841 font_weight,
2842 font_style,
2843 letter_spacing,
2844 font_variation_settings,
2845 );
2846 report_unresolved_codepoints(&layout, text);
2847 let layout_edges = layout_edges_for_layout(text, &layout);
2848 let metrics = metrics_from_edges(text, &layout_edges.edges);
2849 let extrema = RangeExtremum::from_measurement(text, &metrics, &layout_edges.extents);
2850 LineMeasurement {
2851 metrics,
2852 atomic_ranges: layout_edges.atomic_ranges,
2853 extrema,
2854 }
2855}
2856
2857fn measure_text_with_engine(
2858 eng: &mut Engine,
2859 text: &str,
2860 px: f32,
2861 font_family: Option<&str>,
2862 font_weight: u16,
2863 font_style: u8,
2864 letter_spacing: f32,
2865 font_variation_settings: Option<&str>,
2866) -> TextMetrics {
2867 let mut edges = BTreeMap::new();
2868 let hard_line_list = hard_lines(text);
2869 for line in &hard_line_list {
2870 let local_text = &text[line.start..line.end];
2871 let layout = build_layout(
2872 eng,
2873 local_text,
2874 px,
2875 0.0,
2876 font_family,
2877 font_weight,
2878 font_style,
2879 letter_spacing,
2880 font_variation_settings,
2881 );
2882 report_unresolved_codepoints(&layout, local_text);
2883 let local_edges = layout_edges_for_layout(local_text, &layout);
2884 for (byte, position) in local_edges.edges {
2885 edges.insert(line.start + byte, position);
2886 }
2887 }
2888 for line in &hard_line_list {
2889 if line.next_start > line.end {
2890 edges.insert(line.next_start, 0.0);
2891 }
2892 }
2893 let edges = edges.into_iter().collect::<Vec<_>>();
2894 metrics_from_edges(text, &edges)
2895}
2896
2897pub fn metrics_for_textfield_shared(
2898 text: &str,
2899 px: f32,
2900 font_family: Option<&str>,
2901 font_weight: u16,
2902 font_style: u8,
2903 letter_spacing: f32,
2904 font_variation_settings: Option<&str>,
2905) -> Arc<TextMetrics> {
2906 let key = MetricsCacheKey {
2907 text: Arc::from(text),
2908 px_bits: px.to_bits(),
2909 family: font_family.map(Arc::from),
2910 font_weight,
2911 font_style,
2912 letter_spacing_bits: letter_spacing.to_bits(),
2913 variation: font_variation_settings.map(Arc::from),
2914 generation: font_generation(),
2915 };
2916 if let Some(metrics) = metrics_cache().lock().unwrap().get(&key).cloned() {
2917 return metrics;
2918 }
2919 let metrics = {
2920 let mut eng = engine().lock().unwrap();
2921 measure_text_with_engine(
2922 &mut eng,
2923 text,
2924 px,
2925 font_family,
2926 font_weight,
2927 font_style,
2928 letter_spacing,
2929 font_variation_settings,
2930 )
2931 };
2932 let metrics = Arc::new(metrics);
2933 metrics_cache().lock().unwrap().put(key, metrics.clone());
2934 metrics
2935}
2936
2937pub fn metrics_for_textfield(
2938 text: &str,
2939 px: f32,
2940 font_family: Option<&str>,
2941 font_weight: u16,
2942 font_style: u8,
2943 letter_spacing: f32,
2944 font_variation_settings: Option<&str>,
2945) -> TextMetrics {
2946 (*metrics_for_textfield_shared(
2947 text,
2948 px,
2949 font_family,
2950 font_weight,
2951 font_style,
2952 letter_spacing,
2953 font_variation_settings,
2954 ))
2955 .clone()
2956}
2957
2958fn is_wrapping_space(ch: char) -> bool {
2959 ch.is_whitespace() && !matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2007}')
2960}
2961
2962fn trim_start_byte(text: &str, mut byte: usize) -> usize {
2963 byte = byte.min(text.len());
2964 while byte < text.len() {
2965 let ch = text[byte..].chars().next().unwrap();
2966 if !is_wrapping_space(ch) {
2967 break;
2968 }
2969 byte += ch.len_utf8();
2970 }
2971 byte
2972}
2973
2974fn trim_end_byte(text: &str, mut byte: usize) -> usize {
2975 byte = byte.min(text.len());
2976 while byte > 0 {
2977 let previous = text[..byte].char_indices().next_back();
2978 let Some((start, ch)) = previous else {
2979 break;
2980 };
2981 if !is_wrapping_space(ch) {
2982 break;
2983 }
2984 byte = start;
2985 }
2986 byte
2987}
2988
2989fn containing_atomic_range(ranges: &[AtomicRange], byte: usize) -> Option<AtomicRange> {
2990 let index = ranges.partition_point(|range| range.start < byte);
2991 let range = ranges.get(index.checked_sub(1)?)?;
2992 (range.start < byte && byte < range.end).then_some(*range)
2993}
2994
2995fn snap_start_to_atomic(ranges: &[AtomicRange], byte: usize) -> usize {
2996 containing_atomic_range(ranges, byte)
2997 .map(|range| range.start)
2998 .unwrap_or(byte)
2999}
3000
3001fn snap_end_to_atomic(ranges: &[AtomicRange], byte: usize) -> usize {
3002 containing_atomic_range(ranges, byte)
3003 .map(|range| range.end)
3004 .unwrap_or(byte)
3005}
3006
3007fn next_safe_boundary(measurement: &LineMeasurement, start: usize, limit: usize) -> usize {
3008 let offsets = &measurement.metrics.byte_offsets;
3009 let start = start.min(offsets.last().copied().unwrap_or(0));
3010 let limit = limit.min(offsets.last().copied().unwrap_or(0)).max(start);
3011 if start >= limit {
3012 return start;
3013 }
3014 let mut index = offsets.partition_point(|byte| *byte <= start);
3015 let mut next = offsets.get(index).copied().unwrap_or(limit);
3016 while next < limit {
3017 let Some(range) = containing_atomic_range(&measurement.atomic_ranges, next) else {
3018 break;
3019 };
3020 if range.end >= limit {
3021 return limit;
3022 }
3023 index = offsets.partition_point(|byte| *byte <= range.end);
3024 next = offsets.get(index).copied().unwrap_or(limit);
3025 }
3026 next.min(limit)
3027}
3028
3029fn push_trimmed_range_atomic(
3030 text: &str,
3031 start: usize,
3032 end: usize,
3033 ranges: &[AtomicRange],
3034 out: &mut Vec<(usize, usize)>,
3035) {
3036 let mut start = start.min(text.len());
3037 let mut end = end.min(text.len()).max(start);
3038 let whitespace_only = text.get(start..end).is_some_and(|slice| {
3039 slice
3040 .chars()
3041 .all(|ch| is_wrapping_space(ch) || matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2007}'))
3042 });
3043 if end == start {
3044 out.push((start, end));
3045 return;
3046 }
3047 if whitespace_only {
3048 out.push((start, end));
3049 return;
3050 }
3051 start = trim_start_byte(text, start);
3052 end = trim_end_byte(text, end.max(start));
3053 start = snap_start_to_atomic(ranges, start);
3054 end = snap_end_to_atomic(ranges, end).max(start);
3055 out.push((start, end));
3056}
3057
3058fn wrap_one_hard_line_ranges(
3059 text: &str,
3060 max_width: f32,
3061 max_lines: Option<usize>,
3062 measurement: &LineMeasurement,
3063) -> (Vec<(usize, usize)>, bool) {
3064 if text.is_empty() {
3065 return (vec![(0, 0)], false);
3066 }
3067 if max_lines == Some(0) {
3068 return (Vec::new(), true);
3069 }
3070 let atomic_ranges = &measurement.atomic_ranges;
3071 if visual_width(measurement, 0, text.len()) <= max_width + 0.5 {
3072 let mut out = Vec::new();
3073 push_trimmed_range_atomic(text, 0, text.len(), atomic_ranges, &mut out);
3074 return (out, false);
3075 }
3076
3077 let mut out = Vec::new();
3078 let mut truncated = false;
3079 let mut line_start = trim_start_byte(text, 0);
3080 line_start = snap_start_to_atomic(atomic_ranges, line_start);
3081 let mut last_fit = Some(line_start);
3082
3083 for (raw_token_start, token) in text.split_word_bound_indices() {
3084 let raw_token_start = raw_token_start.min(text.len());
3085 let raw_token_end = (raw_token_start + token.len()).min(text.len());
3086 let token_start = snap_start_to_atomic(atomic_ranges, raw_token_start);
3087 let token_end = snap_end_to_atomic(atomic_ranges, raw_token_end).max(token_start);
3088 if token_end <= line_start {
3089 continue;
3090 }
3091
3092 let mut content_end = trim_end_byte(text, token_end);
3093 content_end = snap_end_to_atomic(atomic_ranges, content_end);
3094 if content_end <= line_start {
3095 last_fit = Some(token_end);
3096 continue;
3097 }
3098
3099 if visual_width(measurement, line_start, content_end) <= max_width + 0.5 {
3100 last_fit = Some(token_end);
3101 continue;
3102 }
3103
3104 if let Some(fit) = last_fit.filter(|fit| *fit > line_start) {
3105 push_trimmed_range_atomic(text, line_start, fit, atomic_ranges, &mut out);
3106 if max_lines.is_some_and(|limit| out.len() >= limit) {
3107 truncated = fit < text.len();
3108 return (out, truncated);
3109 }
3110 line_start = trim_start_byte(text, fit);
3111 line_start = snap_start_to_atomic(atomic_ranges, line_start);
3112 last_fit = None;
3113 if token_end <= line_start {
3114 continue;
3115 }
3116 }
3117
3118 let mut remaining_start = line_start.max(token_start);
3119 while remaining_start < token_end {
3120 remaining_start = trim_start_byte(text, remaining_start);
3121 remaining_start = snap_start_to_atomic(atomic_ranges, remaining_start);
3122 if remaining_start >= token_end {
3123 break;
3124 }
3125 let mut cut = line_start;
3126 let mut probe = remaining_start;
3127 while probe < token_end {
3128 let next = next_safe_boundary(measurement, probe, token_end);
3129 if next <= probe {
3130 break;
3131 }
3132 if visual_width(measurement, line_start, next) <= max_width + 0.5 {
3133 cut = next;
3134 probe = next;
3135 } else {
3136 break;
3137 }
3138 }
3139 if cut <= line_start {
3140 cut = next_safe_boundary(measurement, remaining_start, token_end);
3141 }
3142 if cut <= line_start {
3143 break;
3144 }
3145 push_trimmed_range_atomic(text, line_start, cut, atomic_ranges, &mut out);
3146 line_start = cut;
3147 if max_lines.is_some_and(|limit| out.len() >= limit) {
3148 truncated = line_start < text.len();
3149 return (out, truncated);
3150 }
3151 line_start = trim_start_byte(text, line_start);
3152 line_start = snap_start_to_atomic(atomic_ranges, line_start);
3153 last_fit = None;
3154 remaining_start = line_start;
3155 }
3156 }
3157
3158 if line_start < text.len() && max_lines.is_none_or(|limit| out.len() < limit) {
3159 push_trimmed_range_atomic(text, line_start, text.len(), atomic_ranges, &mut out);
3160 }
3161 if out.is_empty() {
3162 out.push((text.len(), text.len()));
3163 }
3164 (out, truncated)
3165}
3166
3167fn wrap_cache_key(
3168 text: &str,
3169 px: f32,
3170 max_width: f32,
3171 max_lines: Option<usize>,
3172 soft_wrap: bool,
3173 font_family: Option<&str>,
3174 font_weight: u16,
3175 font_style: u8,
3176 letter_spacing: f32,
3177 font_variation_settings: Option<&str>,
3178) -> WrapCacheKey {
3179 WrapCacheKey {
3180 text: Arc::from(text),
3181 px_bits: px.to_bits(),
3182 max_width_bits: max_width.to_bits(),
3183 max_lines,
3184 family: font_family.map(Arc::from),
3185 soft_wrap,
3186 font_weight,
3187 font_style,
3188 letter_spacing_bits: letter_spacing.to_bits(),
3189 variation: font_variation_settings.map(Arc::from),
3190 generation: font_generation(),
3191 }
3192}
3193
3194pub fn wrap_lines_with_family(
3195 text: &str,
3196 px: f32,
3197 max_width: f32,
3198 max_lines: Option<usize>,
3199 soft_wrap: bool,
3200 font_family: Option<&str>,
3201 font_weight: u16,
3202 font_style: u8,
3203 letter_spacing: f32,
3204 font_variation_settings: Option<&str>,
3205) -> (Vec<String>, bool) {
3206 if text.is_empty() {
3207 return if max_lines == Some(0) {
3208 (Vec::new(), true)
3209 } else {
3210 (vec![String::new()], false)
3211 };
3212 }
3213 if max_width <= 0.0 && soft_wrap {
3214 return if max_lines == Some(0) {
3215 (Vec::new(), true)
3216 } else {
3217 (vec![String::new()], false)
3218 };
3219 }
3220 let key = wrap_cache_key(
3221 text,
3222 px,
3223 max_width,
3224 max_lines,
3225 soft_wrap,
3226 font_family,
3227 font_weight,
3228 font_style,
3229 letter_spacing,
3230 font_variation_settings,
3231 );
3232 if let Some(cached) = wrap_cache().lock().unwrap().get(&key).cloned() {
3233 return (*cached).clone();
3234 }
3235 let (ranges, truncated) = wrap_line_ranges_with_family(
3236 text,
3237 px,
3238 max_width,
3239 max_lines,
3240 soft_wrap,
3241 font_family,
3242 font_weight,
3243 font_style,
3244 letter_spacing,
3245 font_variation_settings,
3246 );
3247 let lines = ranges
3248 .iter()
3249 .map(|(start, end)| text.get(*start..*end).unwrap_or_default().to_string())
3250 .collect::<Vec<_>>();
3251 let result = Arc::new((lines, truncated));
3252 wrap_cache().lock().unwrap().put(key, result.clone());
3253 (result.0.clone(), result.1)
3254}
3255
3256pub fn wrap_lines(
3257 text: &str,
3258 px: f32,
3259 max_width: f32,
3260 max_lines: Option<usize>,
3261 soft_wrap: bool,
3262 font_weight: u16,
3263 font_style: u8,
3264 letter_spacing: f32,
3265 font_variation_settings: Option<&str>,
3266) -> (Vec<String>, bool) {
3267 wrap_lines_with_family(
3268 text,
3269 px,
3270 max_width,
3271 max_lines,
3272 soft_wrap,
3273 None,
3274 font_weight,
3275 font_style,
3276 letter_spacing,
3277 font_variation_settings,
3278 )
3279}
3280
3281pub fn wrap_lines_with_style(
3282 text: &str,
3283 px: f32,
3284 max_width: f32,
3285 max_lines: Option<usize>,
3286 soft_wrap: bool,
3287 font_family: Option<&str>,
3288 font_weight: u16,
3289 font_style: u8,
3290 letter_spacing: f32,
3291 font_variation_settings: Option<&str>,
3292) -> (Vec<String>, bool) {
3293 wrap_lines_with_family(
3294 text,
3295 px,
3296 max_width,
3297 max_lines,
3298 soft_wrap,
3299 font_family,
3300 font_weight,
3301 font_style,
3302 letter_spacing,
3303 font_variation_settings,
3304 )
3305}
3306
3307pub fn wrap_line_ranges_with_family(
3308 text: &str,
3309 px: f32,
3310 max_width: f32,
3311 max_lines: Option<usize>,
3312 soft_wrap: bool,
3313 font_family: Option<&str>,
3314 font_weight: u16,
3315 font_style: u8,
3316 letter_spacing: f32,
3317 font_variation_settings: Option<&str>,
3318) -> (Vec<(usize, usize)>, bool) {
3319 if text.is_empty() {
3320 return if max_lines == Some(0) {
3321 (Vec::new(), true)
3322 } else {
3323 (vec![(0, 0)], false)
3324 };
3325 }
3326 if max_width <= 0.0 && soft_wrap {
3327 return if max_lines == Some(0) {
3328 (Vec::new(), true)
3329 } else {
3330 (vec![(0, 0)], false)
3331 };
3332 }
3333 let key = wrap_cache_key(
3334 text,
3335 px,
3336 max_width,
3337 max_lines,
3338 soft_wrap,
3339 font_family,
3340 font_weight,
3341 font_style,
3342 letter_spacing,
3343 font_variation_settings,
3344 );
3345 if let Some(cached) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
3346 return (*cached).clone();
3347 }
3348
3349 if !soft_wrap {
3350 let lines = hard_lines(text);
3351 let count = max_lines.map_or(lines.len(), |limit| limit.min(lines.len()));
3352 let truncated = count < lines.len();
3353 let result = (
3354 lines
3355 .into_iter()
3356 .take(count)
3357 .map(|line| (line.start, line.end))
3358 .collect(),
3359 truncated,
3360 );
3361 let result = Arc::new(result);
3362 wrap_ranges_cache().lock().unwrap().put(key, result.clone());
3363 return (result.0.clone(), result.1);
3364 }
3365
3366 let mut output = Vec::new();
3367 let mut truncated = false;
3368 let hard_line_list = hard_lines(text);
3369 let hard_line_count = hard_line_list.len();
3370 let mut eng = engine().lock().unwrap();
3371 for line in hard_line_list {
3372 if max_lines.is_some_and(|limit| output.len() >= limit) {
3373 truncated = output.len() < hard_line_count;
3374 break;
3375 }
3376 let remaining = max_lines.map(|limit| limit.saturating_sub(output.len()));
3377 let segment = &text[line.start..line.end];
3378 let measurement = measure_line_with_engine(
3379 &mut eng,
3380 segment,
3381 px,
3382 font_family,
3383 font_weight,
3384 font_style,
3385 letter_spacing,
3386 font_variation_settings,
3387 );
3388 let (ranges, segment_truncated) =
3389 wrap_one_hard_line_ranges(segment, max_width, remaining, &measurement);
3390 output.extend(
3391 ranges
3392 .into_iter()
3393 .map(|(start, end)| (line.start + start, line.start + end)),
3394 );
3395 if segment_truncated {
3396 truncated = true;
3397 break;
3398 }
3399 if max_lines.is_some_and(|limit| output.len() >= limit) && line.next_start < text.len() {
3400 truncated = true;
3401 break;
3402 }
3403 }
3404 drop(eng);
3405 let result = Arc::new((output, truncated));
3406 wrap_ranges_cache().lock().unwrap().put(key, result.clone());
3407 (result.0.clone(), result.1)
3408}
3409
3410pub fn wrap_line_ranges(
3411 text: &str,
3412 px: f32,
3413 max_width: f32,
3414 max_lines: Option<usize>,
3415 soft_wrap: bool,
3416 font_weight: u16,
3417 font_style: u8,
3418 letter_spacing: f32,
3419 font_variation_settings: Option<&str>,
3420) -> (Vec<(usize, usize)>, bool) {
3421 wrap_line_ranges_with_family(
3422 text,
3423 px,
3424 max_width,
3425 max_lines,
3426 soft_wrap,
3427 None,
3428 font_weight,
3429 font_style,
3430 letter_spacing,
3431 font_variation_settings,
3432 )
3433}
3434
3435pub fn wrap_line_ranges_with_style(
3436 text: &str,
3437 px: f32,
3438 max_width: f32,
3439 max_lines: Option<usize>,
3440 soft_wrap: bool,
3441 font_family: Option<&str>,
3442 font_weight: u16,
3443 font_style: u8,
3444 letter_spacing: f32,
3445 font_variation_settings: Option<&str>,
3446) -> (Vec<(usize, usize)>, bool) {
3447 wrap_line_ranges_with_family(
3448 text,
3449 px,
3450 max_width,
3451 max_lines,
3452 soft_wrap,
3453 font_family,
3454 font_weight,
3455 font_style,
3456 letter_spacing,
3457 font_variation_settings,
3458 )
3459}
3460
3461pub fn ellipsize_line_with_family(
3462 text: &str,
3463 px: f32,
3464 max_width: f32,
3465 font_family: Option<&str>,
3466 font_weight: u16,
3467 font_style: u8,
3468 letter_spacing: f32,
3469 font_variation_settings: Option<&str>,
3470) -> String {
3471 if text.is_empty() || max_width <= 0.0 {
3472 return String::new();
3473 }
3474 let key = EllipCacheKey {
3475 text: Arc::from(text),
3476 px_bits: px.to_bits(),
3477 max_width_bits: max_width.to_bits(),
3478 family: font_family.map(Arc::from),
3479 font_weight,
3480 font_style,
3481 letter_spacing_bits: letter_spacing.to_bits(),
3482 variation: font_variation_settings.map(Arc::from),
3483 generation: font_generation(),
3484 };
3485 if let Some(cached) = ellip_cache().lock().unwrap().get(&key).cloned() {
3486 return (*cached).clone();
3487 }
3488 let measurement = {
3489 let mut eng = engine().lock().unwrap();
3490 measure_line_with_engine(
3491 &mut eng,
3492 text,
3493 px,
3494 font_family,
3495 font_weight,
3496 font_style,
3497 letter_spacing,
3498 font_variation_settings,
3499 )
3500 };
3501 if visual_width(&measurement, 0, text.len()) <= max_width + 0.5 {
3502 let result = text.to_owned();
3503 ellip_cache()
3504 .lock()
3505 .unwrap()
3506 .put(key, Arc::new(result.clone()));
3507 return result;
3508 }
3509 let ellipsis_width = ellipsis_width_with_style(
3510 px,
3511 font_family,
3512 font_weight,
3513 font_style,
3514 letter_spacing,
3515 font_variation_settings,
3516 );
3517 if ellipsis_width >= max_width {
3518 let result = Arc::new(String::new());
3519 ellip_cache().lock().unwrap().put(key, result);
3520 return String::new();
3521 }
3522 let mut byte = 0usize;
3523 for (offset, grapheme) in text.grapheme_indices(true) {
3524 let candidate = offset + grapheme.len();
3525 if containing_atomic_range(&measurement.atomic_ranges, candidate).is_some() {
3526 continue;
3527 }
3528 if visual_width(&measurement, 0, candidate) + ellipsis_width <= max_width + 0.5 {
3529 byte = candidate;
3530 }
3531 }
3532 let mut result = String::with_capacity(byte + '…'.len_utf8());
3533 result.push_str(&text[..byte]);
3534 result.push('…');
3535 ellip_cache()
3536 .lock()
3537 .unwrap()
3538 .put(key, Arc::new(result.clone()));
3539 result
3540}
3541
3542pub fn ellipsize_line(
3543 text: &str,
3544 px: f32,
3545 max_width: f32,
3546 font_weight: u16,
3547 font_style: u8,
3548 letter_spacing: f32,
3549 font_variation_settings: Option<&str>,
3550) -> String {
3551 ellipsize_line_with_family(
3552 text,
3553 px,
3554 max_width,
3555 None,
3556 font_weight,
3557 font_style,
3558 letter_spacing,
3559 font_variation_settings,
3560 )
3561}
3562
3563pub fn ellipsize_line_with_style(
3564 text: &str,
3565 px: f32,
3566 max_width: f32,
3567 font_family: Option<&str>,
3568 font_weight: u16,
3569 font_style: u8,
3570 letter_spacing: f32,
3571 font_variation_settings: Option<&str>,
3572) -> String {
3573 ellipsize_line_with_family(
3574 text,
3575 px,
3576 max_width,
3577 font_family,
3578 font_weight,
3579 font_style,
3580 letter_spacing,
3581 font_variation_settings,
3582 )
3583}
3584
3585fn ellipsis_width_with_style(
3586 px: f32,
3587 font_family: Option<&str>,
3588 font_weight: u16,
3589 font_style: u8,
3590 letter_spacing: f32,
3591 font_variation_settings: Option<&str>,
3592) -> f32 {
3593 static ELLIP_W_LRU: OnceLock<
3594 Mutex<Lru<(u32, Option<Arc<str>>, u16, u8, u32, Option<Arc<str>>, u64), f32>>,
3595 > = OnceLock::new();
3596 static ELLIP_W_GEN: AtomicU64 = AtomicU64::new(0);
3597 let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
3598 let generation = font_generation();
3599 if ELLIP_W_GEN.load(Ordering::Relaxed) != generation {
3600 if let Ok(mut guard) = cache.lock() {
3601 guard.clear_both();
3602 }
3603 ELLIP_W_GEN.store(generation, Ordering::Relaxed);
3604 }
3605 let key = (
3606 px.to_bits(),
3607 font_family.map(Arc::from),
3608 font_weight,
3609 font_style,
3610 letter_spacing.to_bits(),
3611 font_variation_settings.map(Arc::from),
3612 generation,
3613 );
3614 if let Some(width) = cache.lock().unwrap().get(&key).copied() {
3615 return width;
3616 }
3617 let width = {
3618 let mut eng = engine().lock().unwrap();
3619 let measurement = measure_line_with_engine(
3620 &mut eng,
3621 "…",
3622 px,
3623 font_family,
3624 font_weight,
3625 font_style,
3626 letter_spacing,
3627 font_variation_settings,
3628 );
3629 visual_width(&measurement, 0, "…".len())
3630 };
3631 cache.lock().unwrap().put(key, width);
3632 width
3633}
3634
3635#[cfg(test)]
3636mod tests {
3637 use super::*;
3638
3639 #[test]
3640 fn variation_keys_are_order_independent_and_last_wins() {
3641 let a = canonical_variation(Some("\"wght\" 400, \"wdth\" 75")).expect("canonical key");
3642 let b = canonical_variation(Some("\"wdth\" 75 , \"wght\" 400")).expect("canonical key");
3643 assert_eq!(a, b, "axis order must not change the key");
3644 let last = canonical_variation(Some("\"wght\" 400, \"wght\" 700")).expect("canonical key");
3645 let single = canonical_variation(Some("\"wght\" 700")).expect("canonical key");
3646 assert_eq!(last, single, "a repeated axis resolves to its last value");
3647 assert_ne!(
3648 canonical_variation(Some("\"wght\" 400")),
3649 canonical_variation(Some("\"wght\" 700"))
3650 );
3651 assert!(canonical_variation(None).is_none());
3652 assert!(canonical_variation(Some("")).is_none());
3653 assert!(canonical_variation(Some("!!! nonsense")).is_none());
3654 }
3655
3656 #[test]
3657 fn variation_keys_round_trip_exactly() {
3658 for source in [
3659 "\"wght\" 700",
3660 "\"wght\" 250, \"wdth\" 75, \"slnt\" -12.5",
3661 "\"opsz\" 14.25",
3662 ] {
3663 let key = canonical_variation(Some(source)).expect("canonical key");
3664 let decoded = decode_variation(Some(&key));
3665 let names: Vec<String> = source
3669 .split(',')
3670 .filter_map(|entry| {
3671 let quoted = entry.trim().strip_prefix('"')?;
3672 quoted.split_once('"').map(|(tag, _)| tag.to_string())
3673 })
3674 .collect();
3675 let values: Vec<u32> = parley::setting::FontVariation::parse_css_list(source)
3676 .filter_map(Result::ok)
3677 .map(|setting| setting.value.to_bits())
3678 .collect();
3679 let mut expected: Vec<(String, u32)> = names.into_iter().zip(values).collect();
3680 expected.sort_by(|a, b| a.0.cmp(&b.0));
3681 let got: Vec<(String, u32)> = decoded
3682 .iter()
3683 .map(|(tag, value)| {
3684 let name = format!("{tag:?}");
3686 let name = name
3687 .trim_start_matches("Tag(")
3688 .trim_end_matches(')')
3689 .to_string();
3690 (name, value.to_bits())
3691 })
3692 .collect();
3693 assert_eq!(got, expected, "{source}");
3694 }
3695 }
3696
3697 #[test]
3701 fn malformed_variation_keys_are_ignored_not_fatal() {
3702 assert!(decode_variation(None).is_empty());
3703 assert!(decode_variation(Some("")).is_empty());
3704 for malformed in ["wght", "abc", "0123456789ab", "zzzz", "wght!!", "\"wght\""] {
3705 assert!(
3706 decode_variation(Some(malformed)).is_empty(),
3707 "{malformed:?} must decode to nothing"
3708 );
3709 }
3710 let valid = canonical_variation(Some("\"wght\" 700")).expect("canonical key");
3712 let padded = format!("{valid}abc");
3713 assert_eq!(decode_variation(Some(&padded)).len(), 1);
3714 }
3715
3716 #[test]
3717 fn bidi_caret_positions_follow_visual_order() {
3718 let text = "אבג";
3719 let metrics = metrics_for_textfield(text, 32.0, None, 400, 0, 0.0, None);
3720 assert!(metrics.positions.windows(2).all(|w| w[0] > w[1]));
3721 assert!(
3722 metrics
3723 .positions
3724 .iter()
3725 .all(|position| position.is_finite())
3726 );
3727
3728 let mixed = "אב abc";
3729 let metrics = metrics_for_textfield(mixed, 32.0, None, 400, 0, 0.0, None);
3730 assert!(metrics.positions[0] > metrics.positions[1]);
3731 assert!(metrics.positions[1] > metrics.positions[2]);
3732 assert!(metrics.positions[4] < metrics.positions[5]);
3733 assert!(metrics.positions[5] < metrics.positions[6]);
3734 }
3735
3736 #[test]
3737 fn utf8_layout_and_wrapping_do_not_panic() {
3738 let text = "e\u{301} 👩👩👧👦 🇺🇸 אב\u{00A0}界";
3739 let result = std::panic::catch_unwind(|| {
3740 let _ = metrics_for_textfield(text, 16.0, None, 400, 0, 0.0, None);
3741 let _ = wrap_line_ranges_with_family(
3742 text,
3743 16.0,
3744 12.0,
3745 Some(2),
3746 true,
3747 None,
3748 400,
3749 0,
3750 0.0,
3751 None,
3752 );
3753 });
3754 assert!(result.is_ok());
3755 }
3756}