Skip to main content

repose_text/
lib.rs

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    // NOTE: concurrent store(true) either before or after swap will be preserved
37    // If it happens after swap, next call will return true. So swap is fine, keep simple but use SeqCst to ensure ordering
38    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/// Cache key for the renderer's glyph slug cache -> uniquely identifies a
284/// specific glyph in a specific font face.
285/// Cache key for the renderer's glyph slug cache -> uniquely identifies a
286/// specific glyph in a specific font face at a specific variation instance.
287#[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
295/// Canonicalize font-variation settings into an axis-sorted, deduplicated
296/// key. Axis order in the source string must not change the key, and a
297/// repeated axis resolves to its last value (matching shaping and
298/// `skrifa`).
299pub 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    // Stable sort: the merge below keeps the last entry of each equal-tag run,
314    // so equal tags must stay in source order for "last wins" to hold.
315    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
330/// Characters per canonicalized variation axis: 8 for the tag, 8 for the
331/// float bits of the user-space value.
332const VARIATION_ENTRY_CHARS: usize = 16;
333
334/// Identity of one rasterized glyph instance: font, glyph, raster px and the
335/// canonicalized font-variation settings.
336type VariationKey = (u64, u32, u32, Option<Arc<str>>);
337
338/// Identity of one outline instance. Outlines are extracted in em-space, so
339/// unlike [`VariationKey`] they carry no size.
340type OutlineKey = (u64, u32, Option<Arc<str>>);
341
342/// Decode a [`canonical_variation`] key back into `(tag, user value)` pairs
343/// for the shaping and font APIs.
344///
345/// `CacheKey::variation` is a public field, so a hand-built key need not be
346/// canonical: anything that is not a whole number of entries is ignored
347/// rather than indexed into blindly.
348pub(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        // Both halves are hex text, so both have to be parsed back out: the
359        // raw characters are not the tag bytes.
360        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
380/// User-space variation settings in the shape `swash` wants.
381fn 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
393/// Map a `u32` glyph id to the `u16` id swash accepts.
394///
395/// Glyph ids above `u16::MAX` (possible in very large fonts) cannot be
396/// rendered by swash. Remap those to `.notdef` (0) and emit a warn once per
397/// `(font_id, glyph_id)` instead of silently dropping the glyph. The public
398/// [`CacheKey`] keeps the full `u32` id so no API changes are needed.
399fn 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/// Vector path command for glyph outlines.
422#[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    /// Cache of rendered glyphs keyed by [`VariationKey`].
473    /// Contains (width, height, left, top, content, data).
474    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    /// Cache of (ascent, descent) in px keyed by
479    /// (family hash, weight, px bits). Used for baseline alignment.
480    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    /// Resolve `(ascent, descent)` in px for the primary font matching
533    /// `(family, weight)`, trying named families first and falling back to
534    /// the bundled sans. Returns a `0.8em`/`0.2em` estimate when unresolved.
535    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    /// Best-weight-match `(ascent, descent)` for a named family, or `None`
560    /// when the family (or its data) is unavailable.
561    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    // The emoji font comes from `font-awl`'s bundled set via the `emoji`
750    // feature.
751    // Also excluded from wasm, where the on-demand downloader fetches a
752    // rasterizable CBDT Noto Color Emoji instead.
753    #[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/// True once the browser's local fonts have been registered into the
789/// provider. [`init_fonts_wasm`] keeps retrying until this is true:
790/// `queryLocalFonts()` rejects unless the document has transient user
791/// activation, so the startup call can never succeed on its own.
792#[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
849/// Register `bytes` under `alias` instead of the family name in its name table.
850///
851/// Addressing a face by a name the host controls keeps it reachable when the
852/// same family is also installed system-wide: a family-name lookup resolves to
853/// whichever copy was registered first, so two machines with different font
854/// sets otherwise shape the same string with different advances. Returns
855/// `false` if the bytes are not a parseable font.
856pub 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
1079/// Load a font from a file path and register it into the global font system.
1080///
1081/// Returns an error if the file cannot be read.
1082pub 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
1088/// Vertical font metrics `(ascent, descent)` in px for the primary font
1089/// matching `(font_family, font_weight)`.
1090///
1091/// Used for text baseline alignment (`AlignItems::Baseline`): the first
1092/// baseline of a text block sits `ascent` below the top of its first line's
1093/// em box (plus half-leading, applied by the caller). Results are cached.
1094/// Falls back to a `0.8em`/`0.2em` estimate when no font resolves.
1095pub 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
1121/// Extract the family name from raw font bytes.
1122///
1123/// Tries the typographic family name first, falling back to the standard family name.
1124/// Returns `None` if the font data is invalid or contains no names.
1125pub 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            // parley clusters already grouped by text; if glyph id==0 => missing
1167            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                    // slice may be invalid if out of bounds? clamp
1178                    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                    // Fallback: if text_range empty but still missing, push replacement
1186                    if range.start == range.end {
1187                        // try to guess from glyph? skip
1188                    }
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
2033/// Rasterize through the legacy [`GlyphKey`] handle.
2034///
2035/// [`GlyphKey`] identifies a font and glyph but not a variation instance, so
2036/// this always renders the font's default instance. Callers that need
2037/// variations must go through [`CacheKey`], e.g. [`rasterize_cache_key`].
2038pub 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
2074/// [`CacheKey`] for a legacy [`GlyphKey`] handle.
2075///
2076/// The result always has `variation: None`: [`GlyphKey`] does not identify a
2077/// variation instance, so it will not match a key produced by shaping a run
2078/// that sets font-variation settings.
2079pub 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
2167/// Outline for a legacy [`GlyphKey`] handle, at the font's default
2168/// variation instance (see [`lookup_cache_key`]).
2169pub 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            // Independent expectations: axis names read out of the source
3666            // text, values from the same parser shaping uses, paired by
3667            // position. Decoding emits tag-sorted order, so sort likewise.
3668            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                    // `skrifa::Tag`'s Debug prints `Tag(wght)`.
3685                    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    /// `CacheKey::variation` is a public field, so a hand-built key need not
3698    /// be canonical. A malformed one must be ignored, not panic: panicking
3699    /// here would poison the global text-engine mutex.
3700    #[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        // A valid entry followed by a truncated one keeps the valid part.
3711        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}