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(not(target_arch = "wasm32"), feature = "system-fonts"))]
754    if let Err(e) = provider.load_system_fonts_best_effort() {
755        log::warn!("font-awl: failed to load system fonts: {e}");
756    }
757    configure_collection(provider.collection_mut());
758    provider
759}
760
761fn provider() -> &'static Mutex<font_awl::Provider> {
762    FONT_PROVIDER.get_or_init(|| Mutex::new(init_provider_sync()))
763}
764
765fn init_engine_sync() -> Engine {
766    let mut font_cx = provider().lock().unwrap().new_parley_context();
767    configure_collection(&mut font_cx.collection);
768
769    Engine {
770        font_cx,
771        layout_cx: parley::LayoutContext::new(),
772        swash_cx: swash::scale::ScaleContext::new(),
773        key_map: RapidHashMap::new(),
774        font_registry: Vec::new(),
775        font_registry_index: RapidHashMap::new(),
776        next_font_id: 1,
777        glyph_cache: RapidHashMap::new(),
778        glyph_cache_bytes: 0,
779        outline_cache: Lru::new(OUTLINE_CACHE_CAP),
780        ascent_cache: RapidHashMap::new(),
781    }
782}
783
784/// True once the browser's local fonts have been registered into the
785/// provider. [`init_fonts_wasm`] keeps retrying until this is true:
786/// `queryLocalFonts()` rejects unless the document has transient user
787/// activation, so the startup call can never succeed on its own.
788#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
789pub fn web_fonts_loaded() -> bool {
790    WEB_FONTS_LOADED.load(Ordering::Acquire)
791}
792
793#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
794pub async fn init_fonts_wasm() {
795    if WEB_FONTS_LOADED.load(Ordering::Acquire) {
796        return;
797    }
798    if WASM_FONT_INIT
799        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
800        .is_err()
801    {
802        return;
803    }
804
805    let startup_attempt = !WASM_FONT_CONTEXT_READY.load(Ordering::Acquire);
806
807    let mut candidate = init_provider_sync();
808    let web_fonts = candidate.load_web_fonts().await;
809    if let Err(error) = &web_fonts {
810        if startup_attempt {
811            log::info!("font-awl: local fonts deferred to a user gesture: {error}");
812        } else {
813            log::warn!("font-awl: local font access failed: {error} (bundled fonts kept)");
814        }
815    }
816    restore_retained_font_data(candidate.collection_mut());
817    configure_collection(candidate.collection_mut());
818    {
819        let mut p = provider().lock().unwrap();
820        *p = candidate;
821    }
822    let font_cx = provider().lock().unwrap().new_parley_context();
823    if let Some(eng) = ENGINE.get() {
824        let mut eng = eng.lock().unwrap();
825        eng.font_cx = font_cx;
826        clear_caches_for_fallback_in(&mut eng);
827    } else {
828        drop(font_cx);
829    }
830    if web_fonts.is_ok() {
831        WEB_FONTS_LOADED.store(true, Ordering::Release);
832    }
833    WASM_FONT_CONTEXT_READY.store(true, Ordering::Release);
834    WASM_FONT_INIT.store(false, Ordering::Release);
835}
836
837fn engine() -> &'static Mutex<Engine> {
838    ENGINE.get_or_init(|| Mutex::new(init_engine_sync()))
839}
840
841pub fn register_font_data(bytes: &[u8]) {
842    let _ = register_font_data_if_usable(bytes);
843}
844
845fn font_source_bytes(font: &parley::fontique::FontInfo) -> Option<&[u8]> {
846    match font.source().kind() {
847        parley::fontique::SourceKind::Memory(blob) => Some(blob.as_ref()),
848        parley::fontique::SourceKind::Path(_) => None,
849    }
850}
851
852fn collection_font_data_families(
853    collection: &mut parley::fontique::Collection,
854    bytes: &[u8],
855) -> Vec<parley::fontique::FamilyId> {
856    let mut names: Vec<String> = collection.family_names().map(str::to_owned).collect();
857    names.sort_unstable();
858    let mut result = Vec::new();
859    for name in names {
860        let Some(info) = collection.family_by_name(&name) else {
861            continue;
862        };
863        if info
864            .fonts()
865            .iter()
866            .any(|font| font_source_bytes(font).is_some_and(|source| source == bytes))
867        {
868            result.push(info.id());
869        }
870    }
871    result
872}
873
874fn append_registered_families(
875    collection: &mut parley::fontique::Collection,
876    families: &[(parley::fontique::FamilyId, Vec<parley::fontique::FontInfo>)],
877) {
878    let mut ids: Vec<parley::fontique::FamilyId> = families.iter().map(|(id, _)| *id).collect();
879    ids.sort_unstable();
880    for id in ids {
881        let Some(name) = collection.family_name(id).map(str::to_owned) else {
882            continue;
883        };
884        let generic = if name.starts_with("Noto Color Emoji") {
885            parley::fontique::GenericFamily::Emoji
886        } else {
887            parley::fontique::GenericFamily::SansSerif
888        };
889        append_generic_family_once(collection, generic, id);
890    }
891}
892
893#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
894fn retain_font_data(bytes: Arc<[u8]>) {
895    let retained = RETAINED_FONT_DATA.get_or_init(|| Mutex::new(Vec::new()));
896    let Ok(mut retained) = retained.lock() else {
897        return;
898    };
899    if retained
900        .iter()
901        .all(|existing| existing.as_ref() != bytes.as_ref())
902    {
903        retained.push(bytes);
904    }
905}
906
907#[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
908fn restore_retained_font_data(collection: &mut parley::fontique::Collection) {
909    let retained = RETAINED_FONT_DATA.get().and_then(|retained| {
910        retained
911            .lock()
912            .ok()
913            .map(|retained| retained.as_slice().to_vec())
914    });
915    let Some(retained) = retained else {
916        return;
917    };
918    for bytes in retained {
919        if collection_font_data_families(collection, bytes.as_ref()).is_empty() {
920            let blob = font_blob(bytes);
921            let families = collection.register_fonts(blob, None);
922            append_registered_families(collection, &families);
923        }
924    }
925}
926
927pub(crate) fn register_font_data_if_usable(bytes: &[u8]) -> bool {
928    #[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
929    let mut retained_bytes = None;
930    let (font_cx, newly_registered) = {
931        let mut p = provider().lock().unwrap();
932        let existing = collection_font_data_families(p.collection_mut(), bytes);
933        if !existing.is_empty() {
934            for id in existing {
935                let is_emoji = p
936                    .collection_mut()
937                    .family_name(id)
938                    .is_some_and(|name| name.starts_with("Noto Color Emoji"));
939                let generic = if is_emoji {
940                    parley::fontique::GenericFamily::Emoji
941                } else {
942                    parley::fontique::GenericFamily::SansSerif
943                };
944                append_generic_family_once(p.collection_mut(), generic, id);
945            }
946            (p.new_parley_context(), false)
947        } else {
948            let shared_bytes: Arc<[u8]> = Arc::from(bytes);
949            let blob = font_blob(shared_bytes.clone());
950            let families = p.collection_mut().register_fonts(blob, None);
951            if families.is_empty() {
952                return false;
953            }
954            #[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
955            {
956                retained_bytes = Some(shared_bytes);
957            }
958            append_registered_families(p.collection_mut(), &families);
959            configure_collection(p.collection_mut());
960            (p.new_parley_context(), true)
961        }
962    };
963
964    if newly_registered {
965        #[cfg(all(target_arch = "wasm32", feature = "local-fonts"))]
966        if let Some(bytes) = retained_bytes {
967            retain_font_data(bytes);
968        }
969        let mut eng = engine().lock().unwrap();
970        eng.font_cx = font_cx;
971        clear_caches_for_fallback_in(&mut eng);
972    }
973
974    true
975}
976
977pub(crate) fn clear_caches_for_fallback_in(eng: &mut Engine) {
978    clear_lru_caches();
979    eng.key_map.clear();
980    eng.font_registry.clear();
981    eng.font_registry_index.clear();
982    eng.glyph_cache.clear();
983    eng.glyph_cache_bytes = 0;
984    eng.outline_cache.clear_both();
985    eng.ascent_cache.clear();
986    bump_frame_for_fallback();
987}
988
989fn clear_lru_caches() {
990    if let Some(c) = METRICS_LRU.get()
991        && let Ok(mut g) = c.lock()
992    {
993        g.clear_both();
994    }
995    if let Some(c) = WRAP_LRU.get()
996        && let Ok(mut g) = c.lock()
997    {
998        g.clear_both();
999    }
1000    if let Some(c) = WRAP_RANGES_LRU.get()
1001        && let Ok(mut g) = c.lock()
1002    {
1003        g.clear_both();
1004    }
1005    if let Some(c) = ELLIP_LRU.get()
1006        && let Ok(mut g) = c.lock()
1007    {
1008        g.clear_both();
1009    }
1010    if let Some(c) = SHAPED_LRU.get()
1011        && let Ok(mut g) = c.lock()
1012    {
1013        g.clear_both();
1014    }
1015    if let Some(c) = LEGACY_SHAPED_LRU.get()
1016        && let Ok(mut g) = c.lock()
1017    {
1018        g.clear_both();
1019    }
1020}
1021
1022pub(crate) fn bump_frame_for_fallback() {
1023    FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
1024    FONT_GENERATION.fetch_add(1, Ordering::Relaxed);
1025    FALLBACK_DIRTY.store(true, Ordering::Relaxed);
1026}
1027
1028#[cfg(target_arch = "wasm32")]
1029pub fn ensure_web_fallback_initialized() {
1030    crate::fallback::wasm_fallback::ensure_fallback_initialized();
1031}
1032
1033#[cfg(not(target_arch = "wasm32"))]
1034pub fn ensure_web_fallback_initialized() {}
1035
1036/// Load a font from a file path and register it into the global font system.
1037///
1038/// Returns an error if the file cannot be read.
1039pub fn load_font_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
1040    let bytes = std::fs::read(path)?;
1041    register_font_data(&bytes);
1042    Ok(())
1043}
1044
1045/// Vertical font metrics `(ascent, descent)` in px for the primary font
1046/// matching `(font_family, font_weight)`.
1047///
1048/// Used for text baseline alignment (`AlignItems::Baseline`): the first
1049/// baseline of a text block sits `ascent` below the top of its first line's
1050/// em box (plus half-leading, applied by the caller). Results are cached.
1051/// Falls back to a `0.8em`/`0.2em` estimate when no font resolves.
1052pub fn primary_font_vertical_metrics(
1053    font_family: Option<&str>,
1054    font_weight: u16,
1055    px: f32,
1056) -> (f32, f32) {
1057    let Some(px) = normalized_raster_px(px) else {
1058        return (0.0, 0.0);
1059    };
1060    let key = (
1061        font_family.map(Arc::from),
1062        font_weight,
1063        px.to_bits(),
1064        font_generation(),
1065    );
1066    let mut eng = engine().lock().unwrap();
1067    if let Some(&m) = eng.ascent_cache.get(&key) {
1068        return m;
1069    }
1070    let m = eng.resolve_vertical_metrics(font_family, font_weight, px);
1071    if m.0.is_finite() && m.1.is_finite() && eng.ascent_cache.len() >= VERTICAL_METRICS_CACHE_CAP {
1072        eng.ascent_cache.clear();
1073    }
1074    eng.ascent_cache.insert(key, m);
1075    m
1076}
1077
1078/// Extract the family name from raw font bytes.
1079///
1080/// Tries the typographic family name first, falling back to the standard family name.
1081/// Returns `None` if the font data is invalid or contains no names.
1082pub fn font_family_name(bytes: &[u8]) -> Option<String> {
1083    use skrifa::string::StringId;
1084
1085    for face_index in 0..4096u32 {
1086        let Ok(font) = skrifa::FontRef::from_index(bytes, face_index) else {
1087            break;
1088        };
1089        let name = font
1090            .localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME)
1091            .english_or_first()
1092            .map(|s| s.to_string())
1093            .or_else(|| {
1094                font.localized_strings(StringId::FAMILY_NAME)
1095                    .english_or_first()
1096                    .map(|s| s.to_string())
1097            });
1098        if name.is_some() {
1099            return name;
1100        }
1101    }
1102    None
1103}
1104
1105fn key_from_pair(font_id: u64, glyph_id: u32) -> GlyphKey {
1106    let mut h = RapidHasher::default();
1107    font_id.hash(&mut h);
1108    glyph_id.hash(&mut h);
1109    GlyphKey(h.finish())
1110}
1111
1112#[cfg(target_arch = "wasm32")]
1113fn collect_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) -> Vec<u32> {
1114    use parley::layout::PositionedLayoutItem;
1115    let mut out = Vec::new();
1116    let mut total_glyphs: usize = 0;
1117    for line in layout.lines() {
1118        for item in line.items() {
1119            let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
1120                continue;
1121            };
1122            let run = glyph_run.run();
1123            // parley clusters already grouped by text; if glyph id==0 => missing
1124            for cluster in run.clusters() {
1125                let mut glyph_count = 0usize;
1126                let mut has_missing = false;
1127                for glyph in cluster.glyphs() {
1128                    glyph_count += 1;
1129                    has_missing |= glyph.id == 0;
1130                }
1131                total_glyphs += glyph_count;
1132                if has_missing {
1133                    let range = cluster.text_range();
1134                    // slice may be invalid if out of bounds? clamp
1135                    let end = range.end.min(text.len());
1136                    let start = range.start.min(end);
1137                    if let Some(slice) = text.get(start..end) {
1138                        for ch in slice.chars() {
1139                            out.push(ch as u32);
1140                        }
1141                    }
1142                    // Fallback: if text_range empty but still missing, push replacement
1143                    if range.start == range.end {
1144                        // try to guess from glyph? skip
1145                    }
1146                }
1147            }
1148        }
1149    }
1150    if out.is_empty() && !text.is_empty() && total_glyphs == 0 {
1151        if text.chars().any(|c| !c.is_whitespace()) {
1152            for ch in text.chars() {
1153                if !ch.is_whitespace() && ch != '\n' && ch != '\r' && ch != '\t' {
1154                    out.push(ch as u32);
1155                }
1156            }
1157        }
1158    }
1159    out
1160}
1161
1162fn push_font_family<'a>(
1163    builder: &mut parley::RangedBuilder<'a, ()>,
1164    family: Option<&str>,
1165    text_len: usize,
1166) {
1167    use parley::style::{FontFamilyName, GenericFamily};
1168
1169    let mut names = Vec::with_capacity(10);
1170    match family {
1171        Some("monospace") => {
1172            names.push(FontFamilyName::named("JetBrains Mono"));
1173            names.push(GenericFamily::Monospace.into());
1174        }
1175        Some("sans-serif") => names.push(FontFamilyName::named("Open Sans")),
1176        Some("emoji") => names.push(FontFamilyName::named("Noto Color Emoji")),
1177        Some("serif") => names.push(GenericFamily::Serif.into()),
1178        Some("cursive") => names.push(GenericFamily::Cursive.into()),
1179        Some("fantasy") => names.push(GenericFamily::Fantasy.into()),
1180        Some("system-ui") => names.push(GenericFamily::SystemUi.into()),
1181        Some("math") => names.push(GenericFamily::Math.into()),
1182        Some(family) => names.push(FontFamilyName::named(family)),
1183        None => {}
1184    }
1185    names.push(GenericFamily::SansSerif.into());
1186    names.push(GenericFamily::Emoji.into());
1187    names.push(FontFamilyName::named("Noto Color Emoji"));
1188    names.push(FontFamilyName::named("Noto Sans Symbols 2"));
1189    names.push(FontFamilyName::named("Noto Sans Symbols2"));
1190    names.push(FontFamilyName::named("Noto Sans Symbols"));
1191    names.push(FontFamilyName::named("Material Symbols Outlined"));
1192    builder.push(names.as_slice(), 0..text_len);
1193}
1194
1195fn build_layout(
1196    eng: &mut Engine,
1197    text: &str,
1198    px: f32,
1199    line_height_ratio: f32,
1200    font_family: Option<&str>,
1201    font_weight: u16,
1202    font_style: u8,
1203    letter_spacing: f32,
1204    font_variation_settings: Option<&str>,
1205) -> parley::Layout<()> {
1206    use parley::FontWeight;
1207    use parley::style::StyleProperty;
1208
1209    let mut builder = eng
1210        .layout_cx
1211        .ranged_builder(&mut eng.font_cx, text, 1.0, true);
1212    builder.push_default(StyleProperty::FontSize(px));
1213    if line_height_ratio > 0.0 {
1214        builder.push_default(StyleProperty::LineHeight(
1215            parley::LineHeight::FontSizeRelative(line_height_ratio),
1216        ));
1217    }
1218    builder.push_default(StyleProperty::FontWeight(FontWeight::new(
1219        font_weight as f32,
1220    )));
1221    builder.push_default(StyleProperty::FontStyle(match font_style {
1222        1 => parley::FontStyle::Italic,
1223        _ => parley::FontStyle::Normal,
1224    }));
1225    builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
1226    if let Some(settings) = font_variation_settings {
1227        builder.push_default(StyleProperty::FontVariations(
1228            parley::style::FontVariations::from(settings),
1229        ));
1230    }
1231    push_font_family(&mut builder, font_family, text.len());
1232    let mut layout = builder.build(text);
1233    layout.break_all_lines(None);
1234    layout.align(
1235        parley::Alignment::Start,
1236        parley::AlignmentOptions::default(),
1237    );
1238    layout
1239}
1240
1241#[cfg(target_arch = "wasm32")]
1242fn report_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) {
1243    let unresolved = collect_unresolved_codepoints(layout, text);
1244    if unresolved.is_empty() {
1245        return;
1246    }
1247    let reg = crate::unresolved::web_unresolved_registry();
1248    if unresolved.iter().any(|cp| !reg.contains(*cp)) {
1249        crate::fallback::wasm_fallback::ensure_fallback_initialized();
1250        reg.add_unresolved_vec(unresolved);
1251    }
1252}
1253
1254#[cfg(not(target_arch = "wasm32"))]
1255fn report_unresolved_codepoints(_layout: &parley::Layout<()>, _text: &str) {}
1256
1257fn collect_shaped_layout(
1258    eng: &mut Engine,
1259    layout: parley::Layout<()>,
1260    px: f32,
1261    collect_runs: bool,
1262    variation: Option<Arc<str>>,
1263) -> (Vec<ShapedGlyph>, Vec<ShapedRun>) {
1264    use parley::layout::PositionedLayoutItem;
1265
1266    let raster_px = normalized_raster_px(px).unwrap_or(0.0);
1267    let mut glyphs = Vec::new();
1268    let mut runs = Vec::new();
1269    for line in layout.lines() {
1270        for item in line.items() {
1271            let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
1272                continue;
1273            };
1274            let run = glyph_run.run();
1275            if collect_runs {
1276                runs.push(ShapedRun {
1277                    text_range: run.text_range(),
1278                    rtl: run.is_rtl(),
1279                    synthesis: run.synthesis(),
1280                });
1281            }
1282            let font_data = run.font();
1283            let fid = eng.ensure_font(font_data);
1284            let run_cache_key = CacheKey {
1285                font_id: fid,
1286                glyph_id: 0,
1287                font_size_bits: raster_px.to_bits(),
1288                variation: variation.clone(),
1289            };
1290            log::debug!(
1291                "[shape] run: fid={} font_data_len={}",
1292                fid,
1293                font_data.data.as_ref().len()
1294            );
1295            for glyph in glyph_run.positioned_glyphs() {
1296                let gid = glyph.id;
1297                let key = key_from_pair(fid, gid);
1298                eng.key_map.insert(key, (fid, gid));
1299                let cache_key = CacheKey {
1300                    glyph_id: gid,
1301                    ..run_cache_key.clone()
1302                };
1303                glyphs.push(ShapedGlyph {
1304                    key,
1305                    cache_key,
1306                    font_id: fid,
1307                    glyph_id: gid,
1308                    px,
1309                    x: glyph.x,
1310                    y: glyph.y,
1311                    w: 0.0,
1312                    h: 0.0,
1313                    bearing_x: 0.0,
1314                    bearing_y: 0.0,
1315                    advance: glyph.advance,
1316                    rasterized: false,
1317                });
1318            }
1319        }
1320    }
1321    (glyphs, runs)
1322}
1323
1324#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1325pub enum TextDirection {
1326    #[default]
1327    Auto,
1328    Ltr,
1329    Rtl,
1330}
1331
1332#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1333pub enum FontSynthesis {
1334    #[default]
1335    Unspecified,
1336    None,
1337    Weight,
1338    Style,
1339    All,
1340}
1341
1342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1343pub enum ShapeCapabilityError {
1344    RequestedDirectionMismatch,
1345    SynthesisModeUnsupported,
1346}
1347
1348#[derive(Clone, Copy, Debug)]
1349pub struct ShapeOptions<'a> {
1350    pub font_family: Option<&'a str>,
1351    pub font_weight: u16,
1352    pub font_style: u8,
1353    pub letter_spacing: f32,
1354    pub font_variation_settings: Option<&'a str>,
1355    pub text_direction: TextDirection,
1356    pub font_synthesis: FontSynthesis,
1357}
1358
1359impl Default for ShapeOptions<'_> {
1360    fn default() -> Self {
1361        Self {
1362            font_family: None,
1363            font_weight: 400,
1364            font_style: 0,
1365            letter_spacing: 0.0,
1366            font_variation_settings: None,
1367            text_direction: TextDirection::Auto,
1368            font_synthesis: FontSynthesis::Unspecified,
1369        }
1370    }
1371}
1372
1373#[derive(Clone)]
1374pub struct ShapedRun {
1375    pub text_range: std::ops::Range<usize>,
1376    pub rtl: bool,
1377    pub synthesis: parley::fontique::Synthesis,
1378}
1379
1380#[derive(Clone)]
1381pub struct ShapedText {
1382    pub glyphs: Vec<ShapedGlyph>,
1383    pub requested_text_direction: TextDirection,
1384    pub resolved_text_direction: TextDirection,
1385    pub runs: Vec<ShapedRun>,
1386}
1387
1388#[derive(Clone)]
1389pub struct SharedShapedText {
1390    pub glyphs: Arc<[ShapedGlyph]>,
1391    pub requested_text_direction: TextDirection,
1392    pub resolved_text_direction: TextDirection,
1393    pub runs: Arc<[ShapedRun]>,
1394}
1395
1396pub type CachedShapedText = SharedShapedText;
1397pub type VectorShapedText = SharedShapedText;
1398pub type VectorShapedLine = SharedShapedText;
1399pub type ShapedLine = SharedShapedText;
1400
1401#[derive(Clone)]
1402struct ShapeCacheKey {
1403    text: Arc<str>,
1404    px_bits: u32,
1405    line_height_bits: u32,
1406    family: Option<Arc<str>>,
1407    font_weight: u16,
1408    font_style: u8,
1409    letter_spacing_bits: u32,
1410    variation: Option<Arc<str>>,
1411    generation: u64,
1412    text_direction: TextDirection,
1413    font_synthesis: FontSynthesis,
1414}
1415
1416impl ShapeCacheKey {
1417    fn new(
1418        text: &str,
1419        px: f32,
1420        line_height_ratio: f32,
1421        options: ShapeOptions<'_>,
1422        generation: u64,
1423    ) -> Self {
1424        Self {
1425            text: Arc::from(text),
1426            px_bits: px.to_bits(),
1427            line_height_bits: line_height_ratio.to_bits(),
1428            family: options.font_family.map(Arc::from),
1429            font_weight: options.font_weight,
1430            font_style: options.font_style,
1431            letter_spacing_bits: options.letter_spacing.to_bits(),
1432            variation: options.font_variation_settings.map(Arc::from),
1433            generation,
1434            text_direction: options.text_direction,
1435            font_synthesis: options.font_synthesis,
1436        }
1437    }
1438
1439    fn matches(
1440        &self,
1441        text: &str,
1442        px: f32,
1443        line_height_ratio: f32,
1444        options: ShapeOptions<'_>,
1445    ) -> bool {
1446        self.text.as_ref() == text
1447            && self.px_bits == px.to_bits()
1448            && self.line_height_bits == line_height_ratio.to_bits()
1449            && self.family.as_deref() == options.font_family
1450            && self.font_weight == options.font_weight
1451            && self.font_style == options.font_style
1452            && self.letter_spacing_bits == options.letter_spacing.to_bits()
1453            && self.variation.as_deref() == options.font_variation_settings
1454            && self.text_direction == options.text_direction
1455            && self.font_synthesis == options.font_synthesis
1456    }
1457}
1458
1459struct ShapeCacheEntry {
1460    key: ShapeCacheKey,
1461    value: Arc<SharedShapedText>,
1462}
1463
1464static SHAPED_LRU: OnceLock<Mutex<Lru<u64, Vec<ShapeCacheEntry>>>> = OnceLock::new();
1465
1466struct LegacyShapeCacheEntry {
1467    key: ShapeCacheKey,
1468    value: Arc<[ShapedGlyph]>,
1469}
1470
1471static LEGACY_SHAPED_LRU: OnceLock<Mutex<Lru<u64, Vec<LegacyShapeCacheEntry>>>> = OnceLock::new();
1472
1473fn shaped_cache() -> &'static Mutex<Lru<u64, Vec<ShapeCacheEntry>>> {
1474    SHAPED_LRU.get_or_init(|| Mutex::new(Lru::new(SHAPE_CACHE_CAP)))
1475}
1476
1477fn legacy_shaped_cache() -> &'static Mutex<Lru<u64, Vec<LegacyShapeCacheEntry>>> {
1478    LEGACY_SHAPED_LRU.get_or_init(|| Mutex::new(Lru::new(LEGACY_SHAPE_CACHE_CAP)))
1479}
1480
1481fn shape_fingerprint(
1482    text: &str,
1483    px: f32,
1484    line_height_ratio: f32,
1485    options: ShapeOptions<'_>,
1486    generation: u64,
1487) -> u64 {
1488    let mut hasher = RapidHasher::default();
1489    text.hash(&mut hasher);
1490    px.to_bits().hash(&mut hasher);
1491    line_height_ratio.to_bits().hash(&mut hasher);
1492    options.font_family.hash(&mut hasher);
1493    options.font_weight.hash(&mut hasher);
1494    options.font_style.hash(&mut hasher);
1495    options.letter_spacing.to_bits().hash(&mut hasher);
1496    options.font_variation_settings.hash(&mut hasher);
1497    options.text_direction.hash(&mut hasher);
1498    options.font_synthesis.hash(&mut hasher);
1499    generation.hash(&mut hasher);
1500    hasher.finish()
1501}
1502
1503fn cached_shaped(
1504    text: &str,
1505    px: f32,
1506    line_height_ratio: f32,
1507    options: ShapeOptions<'_>,
1508    generation: u64,
1509    fingerprint: u64,
1510) -> Option<Arc<SharedShapedText>> {
1511    let mut cache = shaped_cache().lock().ok()?;
1512    let entries = cache.get_mut(&fingerprint)?;
1513    let entry = entries.iter().find(|entry| {
1514        entry.key.generation == generation
1515            && entry.key.matches(text, px, line_height_ratio, options)
1516    })?;
1517    Some(entry.value.clone())
1518}
1519
1520fn cache_shaped(fingerprint: u64, key: ShapeCacheKey, value: Arc<SharedShapedText>) {
1521    let mut cache = shaped_cache().lock().unwrap();
1522    if let Some(entries) = cache.get_mut(&fingerprint) {
1523        if let Some(entry) = entries.iter_mut().find(|entry| {
1524            entry.key.text == key.text
1525                && entry.key.px_bits == key.px_bits
1526                && entry.key.line_height_bits == key.line_height_bits
1527                && entry.key.family == key.family
1528                && entry.key.font_weight == key.font_weight
1529                && entry.key.font_style == key.font_style
1530                && entry.key.letter_spacing_bits == key.letter_spacing_bits
1531                && entry.key.variation == key.variation
1532                && entry.key.text_direction == key.text_direction
1533                && entry.key.font_synthesis == key.font_synthesis
1534        }) {
1535            entry.key.generation = key.generation;
1536            entry.value = value;
1537            return;
1538        }
1539        entries.push(ShapeCacheEntry { key, value });
1540        return;
1541    }
1542    cache.put(fingerprint, vec![ShapeCacheEntry { key, value }]);
1543}
1544
1545fn cached_legacy_glyphs(
1546    text: &str,
1547    px: f32,
1548    line_height_ratio: f32,
1549    options: ShapeOptions<'_>,
1550    generation: u64,
1551    fingerprint: u64,
1552) -> Option<Arc<[ShapedGlyph]>> {
1553    let mut cache = legacy_shaped_cache().lock().ok()?;
1554    let entries = cache.get_mut(&fingerprint)?;
1555    let entry = entries.iter().find(|entry| {
1556        entry.key.generation == generation
1557            && entry.key.matches(text, px, line_height_ratio, options)
1558    })?;
1559    Some(entry.value.clone())
1560}
1561
1562fn cache_legacy_glyphs(fingerprint: u64, key: ShapeCacheKey, value: Arc<[ShapedGlyph]>) {
1563    let mut cache = legacy_shaped_cache().lock().unwrap();
1564    if let Some(entries) = cache.get_mut(&fingerprint) {
1565        if let Some(entry) = entries.iter_mut().find(|entry| {
1566            entry.key.text == key.text
1567                && entry.key.px_bits == key.px_bits
1568                && entry.key.line_height_bits == key.line_height_bits
1569                && entry.key.family == key.family
1570                && entry.key.font_weight == key.font_weight
1571                && entry.key.font_style == key.font_style
1572                && entry.key.letter_spacing_bits == key.letter_spacing_bits
1573                && entry.key.variation == key.variation
1574                && entry.key.text_direction == key.text_direction
1575                && entry.key.font_synthesis == key.font_synthesis
1576        }) {
1577            entry.key.generation = key.generation;
1578            entry.value = value;
1579            return;
1580        }
1581        entries.push(LegacyShapeCacheEntry { key, value });
1582        return;
1583    }
1584    cache.put(fingerprint, vec![LegacyShapeCacheEntry { key, value }]);
1585}
1586
1587fn synthesis_has_weight(synthesis: &parley::fontique::Synthesis) -> bool {
1588    synthesis.embolden()
1589        || synthesis
1590            .variation_settings()
1591            .iter()
1592            .any(|(tag, _)| tag.to_be_bytes() == *b"wght")
1593}
1594
1595fn synthesis_has_style(synthesis: &parley::fontique::Synthesis) -> bool {
1596    synthesis.skew().is_some()
1597        || synthesis.variation_settings().iter().any(|(tag, _)| {
1598            matches!(
1599                tag.to_be_bytes(),
1600                [b'i', b't', b'a', b'l'] | [b's', b'l', b'n', b't']
1601            )
1602        })
1603}
1604
1605fn validate_shape_options(
1606    options: ShapeOptions<'_>,
1607    resolved_direction: TextDirection,
1608    runs: &[ShapedRun],
1609) -> Result<(), ShapeCapabilityError> {
1610    if options.text_direction != TextDirection::Auto && options.text_direction != resolved_direction
1611    {
1612        return Err(ShapeCapabilityError::RequestedDirectionMismatch);
1613    }
1614    let invalid = match options.font_synthesis {
1615        FontSynthesis::Unspecified | FontSynthesis::All => false,
1616        FontSynthesis::None => runs.iter().any(|run| run.synthesis.any()),
1617        FontSynthesis::Weight => runs.iter().any(|run| synthesis_has_style(&run.synthesis)),
1618        FontSynthesis::Style => runs.iter().any(|run| synthesis_has_weight(&run.synthesis)),
1619    };
1620    if invalid {
1621        Err(ShapeCapabilityError::SynthesisModeUnsupported)
1622    } else {
1623        Ok(())
1624    }
1625}
1626
1627fn shape_vector_inner(
1628    text: &str,
1629    px: f32,
1630    line_height_ratio: f32,
1631    options: ShapeOptions<'_>,
1632    collect_runs: bool,
1633) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1634    let generation = font_generation();
1635    let fingerprint = shape_fingerprint(text, px, line_height_ratio, options, generation);
1636    if let Some(value) = cached_shaped(
1637        text,
1638        px,
1639        line_height_ratio,
1640        options,
1641        generation,
1642        fingerprint,
1643    ) {
1644        return Ok(value);
1645    }
1646    let mut eng = engine().lock().unwrap();
1647    let layout = build_layout(
1648        &mut eng,
1649        text,
1650        px,
1651        line_height_ratio,
1652        options.font_family,
1653        options.font_weight,
1654        options.font_style,
1655        options.letter_spacing,
1656        options.font_variation_settings,
1657    );
1658    report_unresolved_codepoints(&layout, text);
1659    let resolved_direction = if layout.is_rtl() {
1660        TextDirection::Rtl
1661    } else {
1662        TextDirection::Ltr
1663    };
1664    let variation = canonical_variation(options.font_variation_settings);
1665    let (glyphs, runs) = collect_shaped_layout(&mut eng, layout, px, collect_runs, variation);
1666    validate_shape_options(options, resolved_direction, &runs)?;
1667    let value = Arc::new(SharedShapedText {
1668        glyphs: Arc::from(glyphs.into_boxed_slice()),
1669        requested_text_direction: options.text_direction,
1670        resolved_text_direction: resolved_direction,
1671        runs: Arc::from(runs.into_boxed_slice()),
1672    });
1673    let key = ShapeCacheKey::new(text, px, line_height_ratio, options, generation);
1674    drop(eng);
1675    cache_shaped(fingerprint, key, value.clone());
1676    Ok(value)
1677}
1678
1679fn materialize_legacy_glyphs(
1680    shaped: &SharedShapedText,
1681    text: &str,
1682    px: f32,
1683    line_height_ratio: f32,
1684    options: ShapeOptions<'_>,
1685) -> Vec<ShapedGlyph> {
1686    let generation = font_generation();
1687    let fingerprint = shape_fingerprint(text, px, line_height_ratio, options, generation);
1688    if let Some(cached) = cached_legacy_glyphs(
1689        text,
1690        px,
1691        line_height_ratio,
1692        options,
1693        generation,
1694        fingerprint,
1695    ) {
1696        return cached.as_ref().to_vec();
1697    }
1698    let mut glyphs = shaped.glyphs.as_ref().to_vec();
1699    let mut eng = engine().lock().unwrap();
1700    for glyph in &mut glyphs {
1701        if let Some((width, height, left, top)) =
1702            eng.raster_placement(glyph.font_id, glyph.glyph_id, glyph.px, None)
1703        {
1704            glyph.w = width;
1705            glyph.h = height;
1706            glyph.bearing_x = left;
1707            glyph.bearing_y = top;
1708            glyph.rasterized = true;
1709        }
1710    }
1711    drop(eng);
1712    let value: Arc<[ShapedGlyph]> = Arc::from(glyphs.into_boxed_slice());
1713    cache_legacy_glyphs(
1714        fingerprint,
1715        ShapeCacheKey::new(text, px, line_height_ratio, options, generation),
1716        value.clone(),
1717    );
1718    value.as_ref().to_vec()
1719}
1720
1721pub fn shape_line_vector(
1722    text: &str,
1723    px: f32,
1724    line_height_ratio: f32,
1725    font_family: Option<&str>,
1726    font_weight: u16,
1727    font_style: u8,
1728    letter_spacing: f32,
1729    font_variation_settings: Option<&str>,
1730) -> Arc<SharedShapedText> {
1731    shape_vector_inner(
1732        text,
1733        px,
1734        line_height_ratio,
1735        ShapeOptions {
1736            font_family,
1737            font_weight,
1738            font_style,
1739            letter_spacing,
1740            font_variation_settings,
1741            ..ShapeOptions::default()
1742        },
1743        true,
1744    )
1745    .unwrap_or_else(|_| {
1746        Arc::new(SharedShapedText {
1747            glyphs: Arc::from(Vec::new()),
1748            requested_text_direction: TextDirection::Auto,
1749            resolved_text_direction: TextDirection::Ltr,
1750            runs: Arc::from(Vec::new()),
1751        })
1752    })
1753}
1754
1755pub fn shape_line_cached(
1756    text: &str,
1757    px: f32,
1758    line_height_ratio: f32,
1759    font_family: Option<&str>,
1760    font_weight: u16,
1761    font_style: u8,
1762    letter_spacing: f32,
1763    font_variation_settings: Option<&str>,
1764) -> Arc<SharedShapedText> {
1765    shape_line_vector(
1766        text,
1767        px,
1768        line_height_ratio,
1769        font_family,
1770        font_weight,
1771        font_style,
1772        letter_spacing,
1773        font_variation_settings,
1774    )
1775}
1776
1777pub fn shape_line_shared(
1778    text: &str,
1779    px: f32,
1780    line_height_ratio: f32,
1781    font_family: Option<&str>,
1782    font_weight: u16,
1783    font_style: u8,
1784    letter_spacing: f32,
1785    font_variation_settings: Option<&str>,
1786) -> Arc<SharedShapedText> {
1787    shape_line_vector(
1788        text,
1789        px,
1790        line_height_ratio,
1791        font_family,
1792        font_weight,
1793        font_style,
1794        letter_spacing,
1795        font_variation_settings,
1796    )
1797}
1798
1799pub fn shape_text_with_options_vector(
1800    text: &str,
1801    px: f32,
1802    line_height_ratio: f32,
1803    options: ShapeOptions<'_>,
1804) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1805    shape_vector_inner(text, px, line_height_ratio, options, true)
1806}
1807
1808pub fn shape_text_with_options_shared(
1809    text: &str,
1810    px: f32,
1811    line_height_ratio: f32,
1812    options: ShapeOptions<'_>,
1813) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1814    shape_text_with_options_vector(text, px, line_height_ratio, options)
1815}
1816
1817pub fn shape_text_with_options_cached(
1818    text: &str,
1819    px: f32,
1820    line_height_ratio: f32,
1821    options: ShapeOptions<'_>,
1822) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1823    shape_text_with_options_vector(text, px, line_height_ratio, options)
1824}
1825
1826pub fn shape_line(
1827    text: &str,
1828    px: f32,
1829    line_height_ratio: f32,
1830    font_family: Option<&str>,
1831    font_weight: u16,
1832    font_style: u8,
1833    letter_spacing: f32,
1834    font_variation_settings: Option<&str>,
1835) -> Vec<ShapedGlyph> {
1836    let options = ShapeOptions {
1837        font_family,
1838        font_weight,
1839        font_style,
1840        letter_spacing,
1841        font_variation_settings,
1842        ..ShapeOptions::default()
1843    };
1844    materialize_legacy_glyphs(
1845        &shape_line_vector(
1846            text,
1847            px,
1848            line_height_ratio,
1849            font_family,
1850            font_weight,
1851            font_style,
1852            letter_spacing,
1853            font_variation_settings,
1854        ),
1855        text,
1856        px,
1857        line_height_ratio,
1858        options,
1859    )
1860}
1861
1862pub fn shape_text_with_options(
1863    text: &str,
1864    px: f32,
1865    line_height_ratio: f32,
1866    options: ShapeOptions<'_>,
1867) -> Result<ShapedText, ShapeCapabilityError> {
1868    let shaped = shape_text_with_options_vector(text, px, line_height_ratio, options)?;
1869    Ok(ShapedText {
1870        glyphs: materialize_legacy_glyphs(&shaped, text, px, line_height_ratio, options),
1871        requested_text_direction: shaped.requested_text_direction,
1872        resolved_text_direction: shaped.resolved_text_direction,
1873        runs: shaped.runs.as_ref().to_vec(),
1874    })
1875}
1876
1877pub fn shape_line_with_options_vector(
1878    text: &str,
1879    px: f32,
1880    line_height_ratio: f32,
1881    options: ShapeOptions<'_>,
1882) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1883    shape_text_with_options_vector(text, px, line_height_ratio, options)
1884}
1885
1886pub fn shape_line_with_options_cached(
1887    text: &str,
1888    px: f32,
1889    line_height_ratio: f32,
1890    options: ShapeOptions<'_>,
1891) -> Result<Arc<SharedShapedText>, ShapeCapabilityError> {
1892    shape_line_with_options_vector(text, px, line_height_ratio, options)
1893}
1894
1895pub fn shape_line_with_options(
1896    text: &str,
1897    px: f32,
1898    line_height_ratio: f32,
1899    options: ShapeOptions<'_>,
1900) -> Result<Vec<ShapedGlyph>, ShapeCapabilityError> {
1901    shape_text_with_options(text, px, line_height_ratio, options).map(|shaped| shaped.glyphs)
1902}
1903
1904fn rasterize_locked(
1905    eng: &mut Engine,
1906    key: GlyphKey,
1907    cache_key: &CacheKey,
1908    px: f32,
1909) -> Option<GlyphBitmap> {
1910    use swash::scale::{Render, Source, StrikeWith};
1911    let fid = cache_key.font_id;
1912    let gid = cache_key.glyph_id;
1913    let raster_key = (fid, gid, px.to_bits(), cache_key.variation.clone());
1914    if let Some(cached) = eng.glyph_cache.get(&raster_key) {
1915        log::debug!(
1916            "[rasterize] HIT fid={} gid={} px={} => {}x{}",
1917            fid,
1918            gid,
1919            px,
1920            cached.0,
1921            cached.1
1922        );
1923        return Some(GlyphBitmap {
1924            key,
1925            w: cached.0,
1926            h: cached.1,
1927            content: cached.4,
1928            data: cached.5.clone(),
1929        });
1930    }
1931    let (data_bytes, face_index) = {
1932        let record = eng.font_registry.iter().find(|r| r.id == fid)?;
1933        (record.data_bytes.clone(), record.face_index)
1934    };
1935    let face_index = usize::try_from(face_index).ok()?;
1936    let font = swash::FontRef::from_index(data_bytes.as_ref(), face_index)?;
1937    let mut builder = eng.swash_cx.builder(font).size(px).hint(true);
1938    let settings = swash_settings(cache_key.variation.as_deref());
1939    if !settings.is_empty() {
1940        builder = builder.variations(settings);
1941    }
1942    let mut scaler = builder.build();
1943    let image = Render::new(&[
1944        Source::Outline,
1945        Source::ColorBitmap(StrikeWith::BestFit),
1946        Source::ColorOutline(0),
1947    ])
1948    .render(&mut scaler, swash_glyph_id(fid, gid))?;
1949    if image.placement.width > MAX_RASTER_DIMENSION
1950        || image.placement.height > MAX_RASTER_DIMENSION
1951        || image.data.len() > MAX_RASTER_BYTES
1952    {
1953        return None;
1954    }
1955    log::debug!(
1956        "[rasterize] MISS fid={} gid={} px={} => {}x{}",
1957        fid,
1958        gid,
1959        px,
1960        image.placement.width,
1961        image.placement.height
1962    );
1963    let bitmap = GlyphBitmap {
1964        key,
1965        w: image.placement.width,
1966        h: image.placement.height,
1967        content: image.content,
1968        data: image.data,
1969    };
1970    let data_len = bitmap.data.len();
1971    let previous = eng.glyph_cache.insert(
1972        raster_key,
1973        (
1974            bitmap.w,
1975            bitmap.h,
1976            image.placement.left,
1977            image.placement.top,
1978            bitmap.content,
1979            bitmap.data.clone(),
1980        ),
1981    );
1982    if let Some(previous) = previous {
1983        eng.glyph_cache_bytes = eng.glyph_cache_bytes.saturating_sub(previous.5.len());
1984    }
1985    eng.glyph_cache_bytes = eng.glyph_cache_bytes.saturating_add(data_len);
1986    eng.trim_glyph_cache();
1987    Some(bitmap)
1988}
1989
1990/// Rasterize through the legacy [`GlyphKey`] handle.
1991///
1992/// [`GlyphKey`] identifies a font and glyph but not a variation instance, so
1993/// this always renders the font's default instance. Callers that need
1994/// variations must go through [`CacheKey`], e.g. [`rasterize_cache_key`].
1995pub fn rasterize(key: GlyphKey, px: f32) -> Option<GlyphBitmap> {
1996    let mut eng = engine().lock().unwrap();
1997    let &(fid, gid) = eng.key_map.get(&key)?;
1998    let px = normalized_raster_px(px)?;
1999    let cache_key = CacheKey {
2000        font_id: fid,
2001        glyph_id: gid,
2002        font_size_bits: px.to_bits(),
2003        variation: None,
2004    };
2005    rasterize_locked(&mut eng, key, &cache_key, px)
2006}
2007
2008pub fn rasterize_cache_key(cache_key: &CacheKey) -> Option<GlyphBitmap> {
2009    let px = f32::from_bits(cache_key.font_size_bits);
2010    let normalized = normalized_raster_px(px)?;
2011    let mut eng = engine().lock().unwrap();
2012    rasterize_locked(
2013        &mut eng,
2014        key_from_pair(cache_key.font_id, cache_key.glyph_id),
2015        cache_key,
2016        normalized,
2017    )
2018}
2019
2020pub fn raster_placement(cache_key: &CacheKey) -> Option<(f32, f32, f32, f32)> {
2021    let px = f32::from_bits(cache_key.font_size_bits);
2022    let mut eng = engine().lock().unwrap();
2023    eng.raster_placement(
2024        cache_key.font_id,
2025        cache_key.glyph_id,
2026        px,
2027        cache_key.variation.clone(),
2028    )
2029}
2030
2031/// [`CacheKey`] for a legacy [`GlyphKey`] handle.
2032///
2033/// The result always has `variation: None`: [`GlyphKey`] does not identify a
2034/// variation instance, so it will not match a key produced by shaping a run
2035/// that sets font-variation settings.
2036pub fn lookup_cache_key(key: GlyphKey, px: f32) -> Option<CacheKey> {
2037    let eng = engine().lock().unwrap();
2038    let &(fid, gid) = eng.key_map.get(&key)?;
2039    let px = normalized_raster_px(px)?;
2040    Some(CacheKey {
2041        font_id: fid,
2042        glyph_id: gid,
2043        font_size_bits: px.to_bits(),
2044        variation: None,
2045    })
2046}
2047
2048fn extract_outlines_for(
2049    data_bytes: &[u8],
2050    face_index: u32,
2051    glyph_id: u32,
2052    variation: Option<&str>,
2053) -> Option<Box<[Command]>> {
2054    let font = skrifa::FontRef::from_index(data_bytes, face_index).ok()?;
2055    let mut pen = OutlinePenCollector(Vec::new());
2056    let glyph = font.outline_glyphs().get(skrifa::GlyphId::new(glyph_id))?;
2057    let settings = decode_variation(variation);
2058    if settings.is_empty() {
2059        glyph
2060            .draw(skrifa::instance::Size::new(1.0), &mut pen)
2061            .ok()?;
2062    } else {
2063        let location = font.axes().location(settings);
2064        glyph
2065            .draw(
2066                skrifa::outline::DrawSettings::unhinted(
2067                    skrifa::instance::Size::new(1.0),
2068                    &location,
2069                ),
2070                &mut pen,
2071            )
2072            .ok()?;
2073    }
2074    Some(pen.0.into_boxed_slice())
2075}
2076
2077pub fn extract_outline_commands_shared(cache_key: &CacheKey) -> Option<Arc<[Command]>> {
2078    let cache_identity = (
2079        cache_key.font_id,
2080        cache_key.glyph_id,
2081        cache_key.variation.clone(),
2082    );
2083    if let Some(cached) = engine()
2084        .lock()
2085        .unwrap()
2086        .outline_cache
2087        .get(&cache_identity)
2088        .cloned()
2089    {
2090        return Some(cached);
2091    }
2092    let (data_bytes, face_index) = {
2093        let eng = engine().lock().unwrap();
2094        let record = eng
2095            .font_registry
2096            .iter()
2097            .find(|record| record.id == cache_key.font_id)?;
2098        (record.data_bytes.clone(), record.face_index)
2099    };
2100    let commands = extract_outlines_for(
2101        data_bytes.as_ref(),
2102        face_index,
2103        cache_key.glyph_id,
2104        cache_key.variation.as_deref(),
2105    )?;
2106    let commands: Arc<[Command]> = Arc::from(commands);
2107    engine()
2108        .lock()
2109        .unwrap()
2110        .outline_cache
2111        .put(cache_identity, commands.clone());
2112    Some(commands)
2113}
2114
2115pub fn extract_outline_commands(cache_key: &CacheKey) -> Option<Box<[Command]>> {
2116    extract_outline_commands_shared(cache_key)
2117        .map(|commands| commands.as_ref().to_vec().into_boxed_slice())
2118}
2119
2120pub fn extract_outline_commands_for(cache_key: &CacheKey) -> Option<Box<[Command]>> {
2121    extract_outline_commands(cache_key)
2122}
2123
2124/// Outline for a legacy [`GlyphKey`] handle, at the font's default
2125/// variation instance (see [`lookup_cache_key`]).
2126pub fn lookup_and_extract_outline(key: GlyphKey, px: f32) -> Option<(CacheKey, Box<[Command]>)> {
2127    let (fid, gid) = {
2128        let eng = engine().lock().unwrap();
2129        *eng.key_map.get(&key)?
2130    };
2131    let px = normalized_raster_px(px)?;
2132    let ck = CacheKey {
2133        font_id: fid,
2134        glyph_id: gid,
2135        font_size_bits: px.to_bits(),
2136        variation: None,
2137    };
2138    let commands = extract_outline_commands_shared(&ck)?;
2139    Some((ck, commands.as_ref().to_vec().into_boxed_slice()))
2140}
2141
2142struct OutlinePenCollector(Vec<Command>);
2143
2144impl OutlinePen for OutlinePenCollector {
2145    fn move_to(&mut self, x: f32, y: f32) {
2146        self.0.push(Command::MoveTo(x, y));
2147    }
2148    fn line_to(&mut self, x: f32, y: f32) {
2149        self.0.push(Command::LineTo(x, y));
2150    }
2151    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
2152        self.0.push(Command::QuadTo(cx0, cy0, x, y));
2153    }
2154    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
2155        self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y));
2156    }
2157    fn close(&mut self) {
2158        self.0.push(Command::Close);
2159    }
2160}
2161
2162#[derive(Clone)]
2163pub struct TextMetrics {
2164    pub positions: Vec<f32>,
2165    pub byte_offsets: Vec<usize>,
2166}
2167
2168struct SegmentExtrema {
2169    len: usize,
2170    base: usize,
2171    min: Vec<f32>,
2172    max: Vec<f32>,
2173    prefix_min: Vec<f32>,
2174    prefix_max: Vec<f32>,
2175    has_prefix: bool,
2176}
2177
2178impl SegmentExtrema {
2179    fn new(len: usize, has_prefix: bool) -> Self {
2180        let len = len.max(1);
2181        let base = len.next_power_of_two();
2182        Self {
2183            len,
2184            base,
2185            min: vec![f32::INFINITY; base * 2],
2186            max: vec![f32::NEG_INFINITY; base * 2],
2187            prefix_min: Vec::with_capacity(if has_prefix { len + 1 } else { 0 }),
2188            prefix_max: Vec::with_capacity(if has_prefix { len + 1 } else { 0 }),
2189            has_prefix,
2190        }
2191    }
2192
2193    fn set(&mut self, index: usize, value: f32) {
2194        if value.is_finite() && index < self.len {
2195            self.min[self.base + index] = value;
2196            self.max[self.base + index] = value;
2197        }
2198    }
2199
2200    fn apply(&mut self, start: usize, end: usize, min_value: f32, max_value: f32) {
2201        if start >= end {
2202            return;
2203        }
2204        let mut left = start.min(self.len) + self.base;
2205        let mut right = end.min(self.len) + self.base;
2206        while left < right {
2207            if left & 1 == 1 {
2208                self.min[left] = self.min[left].min(min_value);
2209                self.max[left] = self.max[left].max(max_value);
2210                left += 1;
2211            }
2212            if right & 1 == 1 {
2213                right -= 1;
2214                self.min[right] = self.min[right].min(min_value);
2215                self.max[right] = self.max[right].max(max_value);
2216            }
2217            left >>= 1;
2218            right >>= 1;
2219        }
2220    }
2221
2222    fn finish(&mut self) {
2223        for index in 1..self.base {
2224            let min_value = self.min[index];
2225            let max_value = self.max[index];
2226            if min_value.is_finite() {
2227                self.min[index * 2] = self.min[index * 2].min(min_value);
2228                self.min[index * 2 + 1] = self.min[index * 2 + 1].min(min_value);
2229            }
2230            if max_value.is_finite() {
2231                self.max[index * 2] = self.max[index * 2].max(max_value);
2232                self.max[index * 2 + 1] = self.max[index * 2 + 1].max(max_value);
2233            }
2234        }
2235        for index in (1..self.base).rev() {
2236            self.min[index] = self.min[index * 2].min(self.min[index * 2 + 1]);
2237            self.max[index] = self.max[index * 2].max(self.max[index * 2 + 1]);
2238        }
2239        if self.has_prefix {
2240            self.prefix_min.push(f32::INFINITY);
2241            self.prefix_max.push(f32::NEG_INFINITY);
2242            for index in 0..self.len {
2243                let value_min = self.min[self.base + index];
2244                let value_max = self.max[self.base + index];
2245                let previous_min = *self.prefix_min.last().unwrap_or(&f32::INFINITY);
2246                let previous_max = *self.prefix_max.last().unwrap_or(&f32::NEG_INFINITY);
2247                self.prefix_min.push(previous_min.min(value_min));
2248                self.prefix_max.push(previous_max.max(value_max));
2249            }
2250        }
2251    }
2252
2253    fn query(&self, start: usize, end: usize) -> (f32, f32) {
2254        let start = start.min(self.len);
2255        let end = end.min(self.len).max(start);
2256        if start == end {
2257            return (f32::INFINITY, f32::NEG_INFINITY);
2258        }
2259        if start == 0 && self.has_prefix {
2260            return (self.prefix_min[end], self.prefix_max[end]);
2261        }
2262        let mut left = start + self.base;
2263        let mut right = end + self.base;
2264        let mut min_value = f32::INFINITY;
2265        let mut max_value = f32::NEG_INFINITY;
2266        while left < right {
2267            if left & 1 == 1 {
2268                min_value = min_value.min(self.min[left]);
2269                max_value = max_value.max(self.max[left]);
2270                left += 1;
2271            }
2272            if right & 1 == 1 {
2273                right -= 1;
2274                min_value = min_value.min(self.min[right]);
2275                max_value = max_value.max(self.max[right]);
2276            }
2277            left >>= 1;
2278            right >>= 1;
2279        }
2280        (min_value, max_value)
2281    }
2282}
2283
2284struct RangeExtremum {
2285    positions: SegmentExtrema,
2286    extents: SegmentExtrema,
2287    coordinates: Vec<usize>,
2288}
2289
2290impl RangeExtremum {
2291    fn from_measurement(text: &str, metrics: &TextMetrics, extents: &[VisualExtent]) -> Self {
2292        let mut coordinates = metrics.byte_offsets.clone();
2293        coordinates.extend(text.char_indices().map(|(index, _)| index));
2294        coordinates.push(text.len());
2295        for extent in extents {
2296            coordinates.push(extent.start);
2297            coordinates.push(extent.end);
2298        }
2299        coordinates.sort_unstable();
2300        coordinates.dedup();
2301        let mut positions = SegmentExtrema::new(metrics.positions.len(), true);
2302        for (index, value) in metrics.positions.iter().copied().enumerate() {
2303            positions.set(index, value);
2304        }
2305        let mut range_extents = SegmentExtrema::new(coordinates.len().saturating_sub(1), false);
2306        for extent in extents {
2307            let left = extent.left.min(extent.right);
2308            let right = extent.left.max(extent.right);
2309            if !left.is_finite() || !right.is_finite() {
2310                continue;
2311            }
2312            let start = coordinates.partition_point(|byte| *byte < extent.start);
2313            let end = coordinates.partition_point(|byte| *byte < extent.end);
2314            range_extents.apply(start, end, left, right);
2315        }
2316        positions.finish();
2317        range_extents.finish();
2318        Self {
2319            positions,
2320            extents: range_extents,
2321            coordinates,
2322        }
2323    }
2324
2325    fn query_bytes(&self, offsets: &[usize], start: usize, end: usize) -> (f32, f32) {
2326        let position_start = offsets.partition_point(|byte| *byte < start);
2327        let position_end = offsets.partition_point(|byte| *byte <= end);
2328        let (position_min, position_max) = self.positions.query(position_start, position_end);
2329        let extent_start = self.coordinates.partition_point(|byte| *byte < start);
2330        let extent_end = self.coordinates.partition_point(|byte| *byte < end);
2331        let (extent_min, extent_max) = self.extents.query(extent_start, extent_end);
2332        (position_min.min(extent_min), position_max.max(extent_max))
2333    }
2334}
2335
2336#[derive(Clone, Copy)]
2337struct LogicalPart {
2338    start: usize,
2339    end: usize,
2340    advance: f32,
2341}
2342
2343struct LogicalGroup {
2344    start: usize,
2345    end: usize,
2346    first_part: Option<LogicalPart>,
2347    extra_parts: Vec<LogicalPart>,
2348    has_ligature_start: bool,
2349    advance: f32,
2350}
2351
2352impl LogicalGroup {
2353    fn parts(&self) -> impl Iterator<Item = &LogicalPart> {
2354        self.first_part.iter().chain(self.extra_parts.iter())
2355    }
2356}
2357
2358#[derive(Clone, Copy, PartialEq, Eq, Debug)]
2359struct AtomicRange {
2360    start: usize,
2361    end: usize,
2362}
2363
2364#[derive(Clone, Copy)]
2365struct VisualExtent {
2366    start: usize,
2367    end: usize,
2368    left: f32,
2369    right: f32,
2370}
2371
2372#[derive(Clone, Copy, PartialEq, Eq)]
2373enum EdgeKind {
2374    Start,
2375    Internal,
2376    End,
2377}
2378
2379#[derive(Clone, Copy)]
2380struct EdgeCandidate {
2381    byte: usize,
2382    position: f32,
2383    kind: EdgeKind,
2384}
2385
2386struct LayoutEdges {
2387    edges: Vec<(usize, f32)>,
2388    extents: Vec<VisualExtent>,
2389    atomic_ranges: Vec<AtomicRange>,
2390}
2391
2392struct LineMeasurement {
2393    metrics: TextMetrics,
2394    atomic_ranges: Vec<AtomicRange>,
2395    extrema: RangeExtremum,
2396}
2397
2398#[derive(Clone, Copy)]
2399struct HardLine {
2400    start: usize,
2401    end: usize,
2402    next_start: usize,
2403}
2404
2405fn hard_lines(text: &str) -> Vec<HardLine> {
2406    let mut lines = Vec::new();
2407    let mut start = 0usize;
2408    let mut chars = text.char_indices().peekable();
2409    while let Some((index, ch)) = chars.next() {
2410        let next_start = match ch {
2411            '\n' => Some(index + 1),
2412            '\u{2028}' | '\u{2029}' => Some(index + ch.len_utf8()),
2413            '\r' => {
2414                if text[index + 1..].starts_with('\n') {
2415                    chars.next();
2416                    Some(index + 2)
2417                } else {
2418                    Some(index + 1)
2419                }
2420            }
2421            _ => None,
2422        };
2423        if let Some(next_start) = next_start {
2424            lines.push(HardLine {
2425                start,
2426                end: index,
2427                next_start,
2428            });
2429            start = next_start;
2430        }
2431    }
2432    lines.push(HardLine {
2433        start,
2434        end: text.len(),
2435        next_start: text.len(),
2436    });
2437    lines
2438}
2439
2440fn finish_logical_group(mut group: LogicalGroup) -> LogicalGroup {
2441    if let Some(part) = &mut group.first_part
2442        && !part.advance.is_finite()
2443    {
2444        part.advance = 0.0;
2445    }
2446    for part in &mut group.extra_parts {
2447        if !part.advance.is_finite() {
2448            part.advance = 0.0;
2449        }
2450    }
2451    group.advance = group.parts().map(|part| part.advance).sum();
2452    if !group.advance.is_finite() {
2453        group.advance = 0.0;
2454    }
2455    group.end = group.end.max(group.start);
2456    group
2457}
2458
2459fn logical_groups_for_run<B: parley::style::Brush>(
2460    run: parley::layout::Run<'_, B>,
2461) -> Vec<LogicalGroup> {
2462    let mut groups = Vec::new();
2463    let mut current: Option<LogicalGroup> = None;
2464    for cluster in run.clusters() {
2465        let range = cluster.text_range();
2466        let part = LogicalPart {
2467            start: range.start,
2468            end: range.end,
2469            advance: cluster.advance(),
2470        };
2471        let is_ligature = cluster.is_ligature_start() || cluster.is_ligature_continuation();
2472        if !is_ligature {
2473            if let Some(group) = current.take() {
2474                groups.push(finish_logical_group(group));
2475            }
2476            groups.push(finish_logical_group(LogicalGroup {
2477                start: part.start,
2478                end: part.end,
2479                first_part: Some(part),
2480                extra_parts: Vec::new(),
2481                has_ligature_start: false,
2482                advance: 0.0,
2483            }));
2484            continue;
2485        }
2486        let starts_new_group = cluster.is_ligature_start()
2487            && current
2488                .as_ref()
2489                .is_some_and(|group| group.has_ligature_start);
2490        if starts_new_group && let Some(group) = current.take() {
2491            groups.push(finish_logical_group(group));
2492        }
2493        let group = current.get_or_insert_with(|| LogicalGroup {
2494            start: part.start,
2495            end: part.end,
2496            first_part: None,
2497            extra_parts: Vec::new(),
2498            has_ligature_start: false,
2499            advance: 0.0,
2500        });
2501        group.start = group.start.min(part.start);
2502        group.end = group.end.max(part.end);
2503        if group.first_part.is_none() {
2504            group.first_part = Some(part);
2505        } else {
2506            group.extra_parts.push(part);
2507        }
2508        group.has_ligature_start |= cluster.is_ligature_start();
2509    }
2510    if let Some(group) = current {
2511        groups.push(finish_logical_group(group));
2512    }
2513    groups
2514}
2515
2516fn add_grapheme_edges(
2517    text: &str,
2518    range: std::ops::Range<usize>,
2519    start_x: f32,
2520    end_x: f32,
2521    candidates: &mut Vec<EdgeCandidate>,
2522) {
2523    let start = range.start.min(text.len());
2524    let end = range.end.min(text.len()).max(start);
2525    let Some(slice) = text.get(start..end) else {
2526        return;
2527    };
2528    let count = slice.graphemes(true).count();
2529    if count == 0 {
2530        candidates.push(EdgeCandidate {
2531            byte: start,
2532            position: start_x,
2533            kind: EdgeKind::Start,
2534        });
2535        candidates.push(EdgeCandidate {
2536            byte: end,
2537            position: end_x,
2538            kind: EdgeKind::End,
2539        });
2540        return;
2541    }
2542    candidates.push(EdgeCandidate {
2543        byte: start,
2544        position: start_x,
2545        kind: EdgeKind::Start,
2546    });
2547    for (index, (offset, grapheme)) in slice.grapheme_indices(true).enumerate() {
2548        let fraction = (index + 1) as f32 / count as f32;
2549        let position = start_x + (end_x - start_x) * fraction;
2550        let kind = if index + 1 == count {
2551            EdgeKind::End
2552        } else {
2553            EdgeKind::Internal
2554        };
2555        candidates.push(EdgeCandidate {
2556            byte: start + offset + grapheme.len(),
2557            position,
2558            kind,
2559        });
2560    }
2561}
2562
2563fn edge_priority(kind: EdgeKind) -> u8 {
2564    match kind {
2565        EdgeKind::Internal => 0,
2566        EdgeKind::Start => 1,
2567        EdgeKind::End => 2,
2568    }
2569}
2570
2571fn resolve_edge_candidates(
2572    mut candidates: Vec<EdgeCandidate>,
2573    text_len: usize,
2574) -> Vec<(usize, f32)> {
2575    candidates.sort_by_key(|candidate| candidate.byte);
2576    let mut edges = BTreeMap::new();
2577    for candidate in candidates {
2578        let replace = match edges.get(&candidate.byte) {
2579            None => true,
2580            Some((_, kind)) => edge_priority(candidate.kind) > edge_priority(*kind),
2581        };
2582        if replace {
2583            edges.insert(candidate.byte, (candidate.position, candidate.kind));
2584        }
2585    }
2586    edges.entry(0).or_insert((0.0, EdgeKind::Start));
2587    edges.entry(text_len).or_insert((0.0, EdgeKind::End));
2588    edges
2589        .into_iter()
2590        .map(|(byte, (position, _))| (byte, position))
2591        .collect()
2592}
2593
2594fn normalized_atomic_ranges(mut ranges: Vec<AtomicRange>) -> Vec<AtomicRange> {
2595    ranges.retain(|range| range.end > range.start);
2596    ranges.sort_by_key(|range| (range.start, range.end));
2597    let mut result: Vec<AtomicRange> = Vec::new();
2598    for range in ranges {
2599        if let Some(previous) = result.last_mut()
2600            && (range.start < previous.end || range.start == previous.start)
2601        {
2602            previous.end = previous.end.max(range.end);
2603        } else {
2604            result.push(range);
2605        }
2606    }
2607    result
2608}
2609
2610fn atomic_ranges_for_layout(text: &str, layout: &parley::Layout<()>) -> Vec<AtomicRange> {
2611    let mut clusters = Vec::new();
2612    let mut seen = HashSet::new();
2613    for line in layout.lines() {
2614        for run in line.runs() {
2615            for cluster in run.clusters() {
2616                let range = cluster.text_range();
2617                let candidate = (
2618                    range.start.min(text.len()),
2619                    range.end.min(text.len()),
2620                    cluster.is_ligature_start(),
2621                    cluster.is_ligature_continuation(),
2622                );
2623                if seen.insert(candidate) {
2624                    clusters.push(candidate);
2625                }
2626            }
2627        }
2628    }
2629    clusters.sort_by_key(|(start, end, _, _)| (*start, *end));
2630    let mut result = Vec::new();
2631    let mut current: Option<AtomicRange> = None;
2632    let mut has_start = false;
2633    for (start, end, is_start, is_continuation) in clusters {
2634        if end <= start {
2635            continue;
2636        }
2637        if !is_start && !is_continuation {
2638            if let Some(range) = current.take() {
2639                result.push(range);
2640            }
2641            has_start = false;
2642            result.push(AtomicRange { start, end });
2643            continue;
2644        }
2645        if is_start
2646            && has_start
2647            && let Some(range) = current.take()
2648        {
2649            result.push(range);
2650            has_start = false;
2651        }
2652        let range = current.get_or_insert(AtomicRange { start, end });
2653        range.start = range.start.min(start);
2654        range.end = range.end.max(end);
2655        has_start |= is_start;
2656    }
2657    if let Some(range) = current {
2658        result.push(range);
2659    }
2660    normalized_atomic_ranges(result)
2661}
2662
2663fn layout_edges_for_layout(text: &str, layout: &parley::Layout<()>) -> LayoutEdges {
2664    let mut candidates = Vec::new();
2665    let mut extents = Vec::new();
2666
2667    for line in layout.lines() {
2668        let line_range = line.text_range();
2669        let line_start = line_range.start.min(text.len());
2670        let line_end = line_range.end.min(text.len()).max(line_start);
2671        candidates.push(EdgeCandidate {
2672            byte: line_start,
2673            position: 0.0,
2674            kind: EdgeKind::Internal,
2675        });
2676        if line_end > line_start {
2677            candidates.push(EdgeCandidate {
2678                byte: line_end,
2679                position: 0.0,
2680                kind: EdgeKind::Internal,
2681            });
2682        }
2683
2684        let inline_min = line.metrics().inline_min_coord;
2685        for run in line.runs() {
2686            let mut visual_clusters = HashMap::with_capacity(run.len());
2687            for cluster in run.visual_clusters() {
2688                let range = cluster.text_range();
2689                visual_clusters
2690                    .entry((range.start, range.end))
2691                    .or_insert_with(|| cluster.visual_offset());
2692            }
2693            let groups = logical_groups_for_run(run);
2694            for group in &groups {
2695                let mut left = f32::INFINITY;
2696                let mut right = f32::NEG_INFINITY;
2697                for part in group.parts() {
2698                    let Some(offset) = visual_clusters
2699                        .get(&(part.start, part.end))
2700                        .copied()
2701                        .flatten()
2702                    else {
2703                        continue;
2704                    };
2705                    let offset = offset + inline_min;
2706                    let advance = if part.advance.is_finite() {
2707                        part.advance
2708                    } else {
2709                        0.0
2710                    };
2711                    left = left.min(offset).min(offset + advance);
2712                    right = right.max(offset).max(offset + advance);
2713                    let (start_x, end_x) = if run.is_rtl() {
2714                        (offset + advance, offset)
2715                    } else {
2716                        (offset, offset + advance)
2717                    };
2718                    add_grapheme_edges(text, part.start..part.end, start_x, end_x, &mut candidates);
2719                }
2720                if left.is_finite() && right.is_finite() {
2721                    extents.push(VisualExtent {
2722                        start: group.start.min(text.len()),
2723                        end: group.end.min(text.len()),
2724                        left,
2725                        right,
2726                    });
2727                }
2728            }
2729        }
2730    }
2731
2732    LayoutEdges {
2733        edges: resolve_edge_candidates(candidates, text.len()),
2734        extents,
2735        atomic_ranges: atomic_ranges_for_layout(text, layout),
2736    }
2737}
2738
2739fn visual_width(measurement: &LineMeasurement, start: usize, end: usize) -> f32 {
2740    let (min, max) = measurement
2741        .extrema
2742        .query_bytes(&measurement.metrics.byte_offsets, start, end);
2743    if min.is_finite() && max.is_finite() {
2744        (max - min).max(0.0)
2745    } else {
2746        0.0
2747    }
2748}
2749
2750fn edge_position(edges: &[(usize, f32)], byte: usize) -> f32 {
2751    match edges.binary_search_by_key(&byte, |edge| edge.0) {
2752        Ok(index) => edges[index].1,
2753        Err(0) => edges.first().map(|edge| edge.1).unwrap_or(0.0),
2754        Err(index) => edges
2755            .get(index.saturating_sub(1))
2756            .map(|edge| edge.1)
2757            .unwrap_or(0.0),
2758    }
2759}
2760
2761fn metrics_from_edges(text: &str, edges: &[(usize, f32)]) -> TextMetrics {
2762    let mut byte_offsets = Vec::with_capacity(text.graphemes(true).count() + 1);
2763    byte_offsets.push(0);
2764    for (byte, _) in text.grapheme_indices(true) {
2765        if byte != 0 {
2766            byte_offsets.push(byte);
2767        }
2768    }
2769    if *byte_offsets.last().unwrap_or(&0) != text.len() {
2770        byte_offsets.push(text.len());
2771    }
2772    let positions = byte_offsets
2773        .iter()
2774        .map(|byte| edge_position(edges, *byte))
2775        .collect();
2776    TextMetrics {
2777        positions,
2778        byte_offsets,
2779    }
2780}
2781
2782fn measure_line_with_engine(
2783    eng: &mut Engine,
2784    text: &str,
2785    px: f32,
2786    font_family: Option<&str>,
2787    font_weight: u16,
2788    font_style: u8,
2789    letter_spacing: f32,
2790    font_variation_settings: Option<&str>,
2791) -> LineMeasurement {
2792    let layout = build_layout(
2793        eng,
2794        text,
2795        px,
2796        0.0,
2797        font_family,
2798        font_weight,
2799        font_style,
2800        letter_spacing,
2801        font_variation_settings,
2802    );
2803    report_unresolved_codepoints(&layout, text);
2804    let layout_edges = layout_edges_for_layout(text, &layout);
2805    let metrics = metrics_from_edges(text, &layout_edges.edges);
2806    let extrema = RangeExtremum::from_measurement(text, &metrics, &layout_edges.extents);
2807    LineMeasurement {
2808        metrics,
2809        atomic_ranges: layout_edges.atomic_ranges,
2810        extrema,
2811    }
2812}
2813
2814fn measure_text_with_engine(
2815    eng: &mut Engine,
2816    text: &str,
2817    px: f32,
2818    font_family: Option<&str>,
2819    font_weight: u16,
2820    font_style: u8,
2821    letter_spacing: f32,
2822    font_variation_settings: Option<&str>,
2823) -> TextMetrics {
2824    let mut edges = BTreeMap::new();
2825    let hard_line_list = hard_lines(text);
2826    for line in &hard_line_list {
2827        let local_text = &text[line.start..line.end];
2828        let layout = build_layout(
2829            eng,
2830            local_text,
2831            px,
2832            0.0,
2833            font_family,
2834            font_weight,
2835            font_style,
2836            letter_spacing,
2837            font_variation_settings,
2838        );
2839        report_unresolved_codepoints(&layout, local_text);
2840        let local_edges = layout_edges_for_layout(local_text, &layout);
2841        for (byte, position) in local_edges.edges {
2842            edges.insert(line.start + byte, position);
2843        }
2844    }
2845    for line in &hard_line_list {
2846        if line.next_start > line.end {
2847            edges.insert(line.next_start, 0.0);
2848        }
2849    }
2850    let edges = edges.into_iter().collect::<Vec<_>>();
2851    metrics_from_edges(text, &edges)
2852}
2853
2854pub fn metrics_for_textfield_shared(
2855    text: &str,
2856    px: f32,
2857    font_family: Option<&str>,
2858    font_weight: u16,
2859    font_style: u8,
2860    letter_spacing: f32,
2861    font_variation_settings: Option<&str>,
2862) -> Arc<TextMetrics> {
2863    let key = MetricsCacheKey {
2864        text: Arc::from(text),
2865        px_bits: px.to_bits(),
2866        family: font_family.map(Arc::from),
2867        font_weight,
2868        font_style,
2869        letter_spacing_bits: letter_spacing.to_bits(),
2870        variation: font_variation_settings.map(Arc::from),
2871        generation: font_generation(),
2872    };
2873    if let Some(metrics) = metrics_cache().lock().unwrap().get(&key).cloned() {
2874        return metrics;
2875    }
2876    let metrics = {
2877        let mut eng = engine().lock().unwrap();
2878        measure_text_with_engine(
2879            &mut eng,
2880            text,
2881            px,
2882            font_family,
2883            font_weight,
2884            font_style,
2885            letter_spacing,
2886            font_variation_settings,
2887        )
2888    };
2889    let metrics = Arc::new(metrics);
2890    metrics_cache().lock().unwrap().put(key, metrics.clone());
2891    metrics
2892}
2893
2894pub fn metrics_for_textfield(
2895    text: &str,
2896    px: f32,
2897    font_family: Option<&str>,
2898    font_weight: u16,
2899    font_style: u8,
2900    letter_spacing: f32,
2901    font_variation_settings: Option<&str>,
2902) -> TextMetrics {
2903    (*metrics_for_textfield_shared(
2904        text,
2905        px,
2906        font_family,
2907        font_weight,
2908        font_style,
2909        letter_spacing,
2910        font_variation_settings,
2911    ))
2912    .clone()
2913}
2914
2915fn is_wrapping_space(ch: char) -> bool {
2916    ch.is_whitespace() && !matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2007}')
2917}
2918
2919fn trim_start_byte(text: &str, mut byte: usize) -> usize {
2920    byte = byte.min(text.len());
2921    while byte < text.len() {
2922        let ch = text[byte..].chars().next().unwrap();
2923        if !is_wrapping_space(ch) {
2924            break;
2925        }
2926        byte += ch.len_utf8();
2927    }
2928    byte
2929}
2930
2931fn trim_end_byte(text: &str, mut byte: usize) -> usize {
2932    byte = byte.min(text.len());
2933    while byte > 0 {
2934        let previous = text[..byte].char_indices().next_back();
2935        let Some((start, ch)) = previous else {
2936            break;
2937        };
2938        if !is_wrapping_space(ch) {
2939            break;
2940        }
2941        byte = start;
2942    }
2943    byte
2944}
2945
2946fn containing_atomic_range(ranges: &[AtomicRange], byte: usize) -> Option<AtomicRange> {
2947    let index = ranges.partition_point(|range| range.start < byte);
2948    let range = ranges.get(index.checked_sub(1)?)?;
2949    (range.start < byte && byte < range.end).then_some(*range)
2950}
2951
2952fn snap_start_to_atomic(ranges: &[AtomicRange], byte: usize) -> usize {
2953    containing_atomic_range(ranges, byte)
2954        .map(|range| range.start)
2955        .unwrap_or(byte)
2956}
2957
2958fn snap_end_to_atomic(ranges: &[AtomicRange], byte: usize) -> usize {
2959    containing_atomic_range(ranges, byte)
2960        .map(|range| range.end)
2961        .unwrap_or(byte)
2962}
2963
2964fn next_safe_boundary(measurement: &LineMeasurement, start: usize, limit: usize) -> usize {
2965    let offsets = &measurement.metrics.byte_offsets;
2966    let start = start.min(offsets.last().copied().unwrap_or(0));
2967    let limit = limit.min(offsets.last().copied().unwrap_or(0)).max(start);
2968    if start >= limit {
2969        return start;
2970    }
2971    let mut index = offsets.partition_point(|byte| *byte <= start);
2972    let mut next = offsets.get(index).copied().unwrap_or(limit);
2973    while next < limit {
2974        let Some(range) = containing_atomic_range(&measurement.atomic_ranges, next) else {
2975            break;
2976        };
2977        if range.end >= limit {
2978            return limit;
2979        }
2980        index = offsets.partition_point(|byte| *byte <= range.end);
2981        next = offsets.get(index).copied().unwrap_or(limit);
2982    }
2983    next.min(limit)
2984}
2985
2986fn push_trimmed_range_atomic(
2987    text: &str,
2988    start: usize,
2989    end: usize,
2990    ranges: &[AtomicRange],
2991    out: &mut Vec<(usize, usize)>,
2992) {
2993    let mut start = start.min(text.len());
2994    let mut end = end.min(text.len()).max(start);
2995    let whitespace_only = text.get(start..end).is_some_and(|slice| {
2996        slice
2997            .chars()
2998            .all(|ch| is_wrapping_space(ch) || matches!(ch, '\u{00A0}' | '\u{202F}' | '\u{2007}'))
2999    });
3000    if end == start {
3001        out.push((start, end));
3002        return;
3003    }
3004    if whitespace_only {
3005        out.push((start, end));
3006        return;
3007    }
3008    start = trim_start_byte(text, start);
3009    end = trim_end_byte(text, end.max(start));
3010    start = snap_start_to_atomic(ranges, start);
3011    end = snap_end_to_atomic(ranges, end).max(start);
3012    out.push((start, end));
3013}
3014
3015fn wrap_one_hard_line_ranges(
3016    text: &str,
3017    max_width: f32,
3018    max_lines: Option<usize>,
3019    measurement: &LineMeasurement,
3020) -> (Vec<(usize, usize)>, bool) {
3021    if text.is_empty() {
3022        return (vec![(0, 0)], false);
3023    }
3024    if max_lines == Some(0) {
3025        return (Vec::new(), true);
3026    }
3027    let atomic_ranges = &measurement.atomic_ranges;
3028    if visual_width(measurement, 0, text.len()) <= max_width + 0.5 {
3029        let mut out = Vec::new();
3030        push_trimmed_range_atomic(text, 0, text.len(), atomic_ranges, &mut out);
3031        return (out, false);
3032    }
3033
3034    let mut out = Vec::new();
3035    let mut truncated = false;
3036    let mut line_start = trim_start_byte(text, 0);
3037    line_start = snap_start_to_atomic(atomic_ranges, line_start);
3038    let mut last_fit = Some(line_start);
3039
3040    for (raw_token_start, token) in text.split_word_bound_indices() {
3041        let raw_token_start = raw_token_start.min(text.len());
3042        let raw_token_end = (raw_token_start + token.len()).min(text.len());
3043        let token_start = snap_start_to_atomic(atomic_ranges, raw_token_start);
3044        let token_end = snap_end_to_atomic(atomic_ranges, raw_token_end).max(token_start);
3045        if token_end <= line_start {
3046            continue;
3047        }
3048
3049        let mut content_end = trim_end_byte(text, token_end);
3050        content_end = snap_end_to_atomic(atomic_ranges, content_end);
3051        if content_end <= line_start {
3052            last_fit = Some(token_end);
3053            continue;
3054        }
3055
3056        if visual_width(measurement, line_start, content_end) <= max_width + 0.5 {
3057            last_fit = Some(token_end);
3058            continue;
3059        }
3060
3061        if let Some(fit) = last_fit.filter(|fit| *fit > line_start) {
3062            push_trimmed_range_atomic(text, line_start, fit, atomic_ranges, &mut out);
3063            if max_lines.is_some_and(|limit| out.len() >= limit) {
3064                truncated = fit < text.len();
3065                return (out, truncated);
3066            }
3067            line_start = trim_start_byte(text, fit);
3068            line_start = snap_start_to_atomic(atomic_ranges, line_start);
3069            last_fit = None;
3070            if token_end <= line_start {
3071                continue;
3072            }
3073        }
3074
3075        let mut remaining_start = line_start.max(token_start);
3076        while remaining_start < token_end {
3077            remaining_start = trim_start_byte(text, remaining_start);
3078            remaining_start = snap_start_to_atomic(atomic_ranges, remaining_start);
3079            if remaining_start >= token_end {
3080                break;
3081            }
3082            let mut cut = line_start;
3083            let mut probe = remaining_start;
3084            while probe < token_end {
3085                let next = next_safe_boundary(measurement, probe, token_end);
3086                if next <= probe {
3087                    break;
3088                }
3089                if visual_width(measurement, line_start, next) <= max_width + 0.5 {
3090                    cut = next;
3091                    probe = next;
3092                } else {
3093                    break;
3094                }
3095            }
3096            if cut <= line_start {
3097                cut = next_safe_boundary(measurement, remaining_start, token_end);
3098            }
3099            if cut <= line_start {
3100                break;
3101            }
3102            push_trimmed_range_atomic(text, line_start, cut, atomic_ranges, &mut out);
3103            line_start = cut;
3104            if max_lines.is_some_and(|limit| out.len() >= limit) {
3105                truncated = line_start < text.len();
3106                return (out, truncated);
3107            }
3108            line_start = trim_start_byte(text, line_start);
3109            line_start = snap_start_to_atomic(atomic_ranges, line_start);
3110            last_fit = None;
3111            remaining_start = line_start;
3112        }
3113    }
3114
3115    if line_start < text.len() && max_lines.is_none_or(|limit| out.len() < limit) {
3116        push_trimmed_range_atomic(text, line_start, text.len(), atomic_ranges, &mut out);
3117    }
3118    if out.is_empty() {
3119        out.push((text.len(), text.len()));
3120    }
3121    (out, truncated)
3122}
3123
3124fn wrap_cache_key(
3125    text: &str,
3126    px: f32,
3127    max_width: f32,
3128    max_lines: Option<usize>,
3129    soft_wrap: bool,
3130    font_family: Option<&str>,
3131    font_weight: u16,
3132    font_style: u8,
3133    letter_spacing: f32,
3134    font_variation_settings: Option<&str>,
3135) -> WrapCacheKey {
3136    WrapCacheKey {
3137        text: Arc::from(text),
3138        px_bits: px.to_bits(),
3139        max_width_bits: max_width.to_bits(),
3140        max_lines,
3141        family: font_family.map(Arc::from),
3142        soft_wrap,
3143        font_weight,
3144        font_style,
3145        letter_spacing_bits: letter_spacing.to_bits(),
3146        variation: font_variation_settings.map(Arc::from),
3147        generation: font_generation(),
3148    }
3149}
3150
3151pub fn wrap_lines_with_family(
3152    text: &str,
3153    px: f32,
3154    max_width: f32,
3155    max_lines: Option<usize>,
3156    soft_wrap: bool,
3157    font_family: Option<&str>,
3158    font_weight: u16,
3159    font_style: u8,
3160    letter_spacing: f32,
3161    font_variation_settings: Option<&str>,
3162) -> (Vec<String>, bool) {
3163    if text.is_empty() {
3164        return if max_lines == Some(0) {
3165            (Vec::new(), true)
3166        } else {
3167            (vec![String::new()], false)
3168        };
3169    }
3170    if max_width <= 0.0 && soft_wrap {
3171        return if max_lines == Some(0) {
3172            (Vec::new(), true)
3173        } else {
3174            (vec![String::new()], false)
3175        };
3176    }
3177    let key = wrap_cache_key(
3178        text,
3179        px,
3180        max_width,
3181        max_lines,
3182        soft_wrap,
3183        font_family,
3184        font_weight,
3185        font_style,
3186        letter_spacing,
3187        font_variation_settings,
3188    );
3189    if let Some(cached) = wrap_cache().lock().unwrap().get(&key).cloned() {
3190        return (*cached).clone();
3191    }
3192    let (ranges, truncated) = wrap_line_ranges_with_family(
3193        text,
3194        px,
3195        max_width,
3196        max_lines,
3197        soft_wrap,
3198        font_family,
3199        font_weight,
3200        font_style,
3201        letter_spacing,
3202        font_variation_settings,
3203    );
3204    let lines = ranges
3205        .iter()
3206        .map(|(start, end)| text.get(*start..*end).unwrap_or_default().to_string())
3207        .collect::<Vec<_>>();
3208    let result = Arc::new((lines, truncated));
3209    wrap_cache().lock().unwrap().put(key, result.clone());
3210    (result.0.clone(), result.1)
3211}
3212
3213pub fn wrap_lines(
3214    text: &str,
3215    px: f32,
3216    max_width: f32,
3217    max_lines: Option<usize>,
3218    soft_wrap: bool,
3219    font_weight: u16,
3220    font_style: u8,
3221    letter_spacing: f32,
3222    font_variation_settings: Option<&str>,
3223) -> (Vec<String>, bool) {
3224    wrap_lines_with_family(
3225        text,
3226        px,
3227        max_width,
3228        max_lines,
3229        soft_wrap,
3230        None,
3231        font_weight,
3232        font_style,
3233        letter_spacing,
3234        font_variation_settings,
3235    )
3236}
3237
3238pub fn wrap_lines_with_style(
3239    text: &str,
3240    px: f32,
3241    max_width: f32,
3242    max_lines: Option<usize>,
3243    soft_wrap: bool,
3244    font_family: Option<&str>,
3245    font_weight: u16,
3246    font_style: u8,
3247    letter_spacing: f32,
3248    font_variation_settings: Option<&str>,
3249) -> (Vec<String>, bool) {
3250    wrap_lines_with_family(
3251        text,
3252        px,
3253        max_width,
3254        max_lines,
3255        soft_wrap,
3256        font_family,
3257        font_weight,
3258        font_style,
3259        letter_spacing,
3260        font_variation_settings,
3261    )
3262}
3263
3264pub fn wrap_line_ranges_with_family(
3265    text: &str,
3266    px: f32,
3267    max_width: f32,
3268    max_lines: Option<usize>,
3269    soft_wrap: bool,
3270    font_family: Option<&str>,
3271    font_weight: u16,
3272    font_style: u8,
3273    letter_spacing: f32,
3274    font_variation_settings: Option<&str>,
3275) -> (Vec<(usize, usize)>, bool) {
3276    if text.is_empty() {
3277        return if max_lines == Some(0) {
3278            (Vec::new(), true)
3279        } else {
3280            (vec![(0, 0)], false)
3281        };
3282    }
3283    if max_width <= 0.0 && soft_wrap {
3284        return if max_lines == Some(0) {
3285            (Vec::new(), true)
3286        } else {
3287            (vec![(0, 0)], false)
3288        };
3289    }
3290    let key = wrap_cache_key(
3291        text,
3292        px,
3293        max_width,
3294        max_lines,
3295        soft_wrap,
3296        font_family,
3297        font_weight,
3298        font_style,
3299        letter_spacing,
3300        font_variation_settings,
3301    );
3302    if let Some(cached) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
3303        return (*cached).clone();
3304    }
3305
3306    if !soft_wrap {
3307        let lines = hard_lines(text);
3308        let count = max_lines.map_or(lines.len(), |limit| limit.min(lines.len()));
3309        let truncated = count < lines.len();
3310        let result = (
3311            lines
3312                .into_iter()
3313                .take(count)
3314                .map(|line| (line.start, line.end))
3315                .collect(),
3316            truncated,
3317        );
3318        let result = Arc::new(result);
3319        wrap_ranges_cache().lock().unwrap().put(key, result.clone());
3320        return (result.0.clone(), result.1);
3321    }
3322
3323    let mut output = Vec::new();
3324    let mut truncated = false;
3325    let hard_line_list = hard_lines(text);
3326    let hard_line_count = hard_line_list.len();
3327    let mut eng = engine().lock().unwrap();
3328    for line in hard_line_list {
3329        if max_lines.is_some_and(|limit| output.len() >= limit) {
3330            truncated = output.len() < hard_line_count;
3331            break;
3332        }
3333        let remaining = max_lines.map(|limit| limit.saturating_sub(output.len()));
3334        let segment = &text[line.start..line.end];
3335        let measurement = measure_line_with_engine(
3336            &mut eng,
3337            segment,
3338            px,
3339            font_family,
3340            font_weight,
3341            font_style,
3342            letter_spacing,
3343            font_variation_settings,
3344        );
3345        let (ranges, segment_truncated) =
3346            wrap_one_hard_line_ranges(segment, max_width, remaining, &measurement);
3347        output.extend(
3348            ranges
3349                .into_iter()
3350                .map(|(start, end)| (line.start + start, line.start + end)),
3351        );
3352        if segment_truncated {
3353            truncated = true;
3354            break;
3355        }
3356        if max_lines.is_some_and(|limit| output.len() >= limit) && line.next_start < text.len() {
3357            truncated = true;
3358            break;
3359        }
3360    }
3361    drop(eng);
3362    let result = Arc::new((output, truncated));
3363    wrap_ranges_cache().lock().unwrap().put(key, result.clone());
3364    (result.0.clone(), result.1)
3365}
3366
3367pub fn wrap_line_ranges(
3368    text: &str,
3369    px: f32,
3370    max_width: f32,
3371    max_lines: Option<usize>,
3372    soft_wrap: bool,
3373    font_weight: u16,
3374    font_style: u8,
3375    letter_spacing: f32,
3376    font_variation_settings: Option<&str>,
3377) -> (Vec<(usize, usize)>, bool) {
3378    wrap_line_ranges_with_family(
3379        text,
3380        px,
3381        max_width,
3382        max_lines,
3383        soft_wrap,
3384        None,
3385        font_weight,
3386        font_style,
3387        letter_spacing,
3388        font_variation_settings,
3389    )
3390}
3391
3392pub fn wrap_line_ranges_with_style(
3393    text: &str,
3394    px: f32,
3395    max_width: f32,
3396    max_lines: Option<usize>,
3397    soft_wrap: bool,
3398    font_family: Option<&str>,
3399    font_weight: u16,
3400    font_style: u8,
3401    letter_spacing: f32,
3402    font_variation_settings: Option<&str>,
3403) -> (Vec<(usize, usize)>, bool) {
3404    wrap_line_ranges_with_family(
3405        text,
3406        px,
3407        max_width,
3408        max_lines,
3409        soft_wrap,
3410        font_family,
3411        font_weight,
3412        font_style,
3413        letter_spacing,
3414        font_variation_settings,
3415    )
3416}
3417
3418pub fn ellipsize_line_with_family(
3419    text: &str,
3420    px: f32,
3421    max_width: f32,
3422    font_family: Option<&str>,
3423    font_weight: u16,
3424    font_style: u8,
3425    letter_spacing: f32,
3426    font_variation_settings: Option<&str>,
3427) -> String {
3428    if text.is_empty() || max_width <= 0.0 {
3429        return String::new();
3430    }
3431    let key = EllipCacheKey {
3432        text: Arc::from(text),
3433        px_bits: px.to_bits(),
3434        max_width_bits: max_width.to_bits(),
3435        family: font_family.map(Arc::from),
3436        font_weight,
3437        font_style,
3438        letter_spacing_bits: letter_spacing.to_bits(),
3439        variation: font_variation_settings.map(Arc::from),
3440        generation: font_generation(),
3441    };
3442    if let Some(cached) = ellip_cache().lock().unwrap().get(&key).cloned() {
3443        return (*cached).clone();
3444    }
3445    let measurement = {
3446        let mut eng = engine().lock().unwrap();
3447        measure_line_with_engine(
3448            &mut eng,
3449            text,
3450            px,
3451            font_family,
3452            font_weight,
3453            font_style,
3454            letter_spacing,
3455            font_variation_settings,
3456        )
3457    };
3458    if visual_width(&measurement, 0, text.len()) <= max_width + 0.5 {
3459        let result = text.to_owned();
3460        ellip_cache()
3461            .lock()
3462            .unwrap()
3463            .put(key, Arc::new(result.clone()));
3464        return result;
3465    }
3466    let ellipsis_width = ellipsis_width_with_style(
3467        px,
3468        font_family,
3469        font_weight,
3470        font_style,
3471        letter_spacing,
3472        font_variation_settings,
3473    );
3474    if ellipsis_width >= max_width {
3475        let result = Arc::new(String::new());
3476        ellip_cache().lock().unwrap().put(key, result);
3477        return String::new();
3478    }
3479    let mut byte = 0usize;
3480    for (offset, grapheme) in text.grapheme_indices(true) {
3481        let candidate = offset + grapheme.len();
3482        if containing_atomic_range(&measurement.atomic_ranges, candidate).is_some() {
3483            continue;
3484        }
3485        if visual_width(&measurement, 0, candidate) + ellipsis_width <= max_width + 0.5 {
3486            byte = candidate;
3487        }
3488    }
3489    let mut result = String::with_capacity(byte + '…'.len_utf8());
3490    result.push_str(&text[..byte]);
3491    result.push('…');
3492    ellip_cache()
3493        .lock()
3494        .unwrap()
3495        .put(key, Arc::new(result.clone()));
3496    result
3497}
3498
3499pub fn ellipsize_line(
3500    text: &str,
3501    px: f32,
3502    max_width: f32,
3503    font_weight: u16,
3504    font_style: u8,
3505    letter_spacing: f32,
3506    font_variation_settings: Option<&str>,
3507) -> String {
3508    ellipsize_line_with_family(
3509        text,
3510        px,
3511        max_width,
3512        None,
3513        font_weight,
3514        font_style,
3515        letter_spacing,
3516        font_variation_settings,
3517    )
3518}
3519
3520pub fn ellipsize_line_with_style(
3521    text: &str,
3522    px: f32,
3523    max_width: f32,
3524    font_family: Option<&str>,
3525    font_weight: u16,
3526    font_style: u8,
3527    letter_spacing: f32,
3528    font_variation_settings: Option<&str>,
3529) -> String {
3530    ellipsize_line_with_family(
3531        text,
3532        px,
3533        max_width,
3534        font_family,
3535        font_weight,
3536        font_style,
3537        letter_spacing,
3538        font_variation_settings,
3539    )
3540}
3541
3542fn ellipsis_width_with_style(
3543    px: f32,
3544    font_family: Option<&str>,
3545    font_weight: u16,
3546    font_style: u8,
3547    letter_spacing: f32,
3548    font_variation_settings: Option<&str>,
3549) -> f32 {
3550    static ELLIP_W_LRU: OnceLock<
3551        Mutex<Lru<(u32, Option<Arc<str>>, u16, u8, u32, Option<Arc<str>>, u64), f32>>,
3552    > = OnceLock::new();
3553    static ELLIP_W_GEN: AtomicU64 = AtomicU64::new(0);
3554    let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
3555    let generation = font_generation();
3556    if ELLIP_W_GEN.load(Ordering::Relaxed) != generation {
3557        if let Ok(mut guard) = cache.lock() {
3558            guard.clear_both();
3559        }
3560        ELLIP_W_GEN.store(generation, Ordering::Relaxed);
3561    }
3562    let key = (
3563        px.to_bits(),
3564        font_family.map(Arc::from),
3565        font_weight,
3566        font_style,
3567        letter_spacing.to_bits(),
3568        font_variation_settings.map(Arc::from),
3569        generation,
3570    );
3571    if let Some(width) = cache.lock().unwrap().get(&key).copied() {
3572        return width;
3573    }
3574    let width = {
3575        let mut eng = engine().lock().unwrap();
3576        let measurement = measure_line_with_engine(
3577            &mut eng,
3578            "…",
3579            px,
3580            font_family,
3581            font_weight,
3582            font_style,
3583            letter_spacing,
3584            font_variation_settings,
3585        );
3586        visual_width(&measurement, 0, "…".len())
3587    };
3588    cache.lock().unwrap().put(key, width);
3589    width
3590}
3591
3592#[cfg(test)]
3593mod tests {
3594    use super::*;
3595
3596    #[test]
3597    fn variation_keys_are_order_independent_and_last_wins() {
3598        let a = canonical_variation(Some("\"wght\" 400, \"wdth\" 75")).expect("canonical key");
3599        let b = canonical_variation(Some("\"wdth\" 75 , \"wght\" 400")).expect("canonical key");
3600        assert_eq!(a, b, "axis order must not change the key");
3601        let last = canonical_variation(Some("\"wght\" 400, \"wght\" 700")).expect("canonical key");
3602        let single = canonical_variation(Some("\"wght\" 700")).expect("canonical key");
3603        assert_eq!(last, single, "a repeated axis resolves to its last value");
3604        assert_ne!(
3605            canonical_variation(Some("\"wght\" 400")),
3606            canonical_variation(Some("\"wght\" 700"))
3607        );
3608        assert!(canonical_variation(None).is_none());
3609        assert!(canonical_variation(Some("")).is_none());
3610        assert!(canonical_variation(Some("!!! nonsense")).is_none());
3611    }
3612
3613    #[test]
3614    fn variation_keys_round_trip_exactly() {
3615        for source in [
3616            "\"wght\" 700",
3617            "\"wght\" 250, \"wdth\" 75, \"slnt\" -12.5",
3618            "\"opsz\" 14.25",
3619        ] {
3620            let key = canonical_variation(Some(source)).expect("canonical key");
3621            let decoded = decode_variation(Some(&key));
3622            // Independent expectations: axis names read out of the source
3623            // text, values from the same parser shaping uses, paired by
3624            // position. Decoding emits tag-sorted order, so sort likewise.
3625            let names: Vec<String> = source
3626                .split(',')
3627                .filter_map(|entry| {
3628                    let quoted = entry.trim().strip_prefix('"')?;
3629                    quoted.split_once('"').map(|(tag, _)| tag.to_string())
3630                })
3631                .collect();
3632            let values: Vec<u32> = parley::setting::FontVariation::parse_css_list(source)
3633                .filter_map(Result::ok)
3634                .map(|setting| setting.value.to_bits())
3635                .collect();
3636            let mut expected: Vec<(String, u32)> = names.into_iter().zip(values).collect();
3637            expected.sort_by(|a, b| a.0.cmp(&b.0));
3638            let got: Vec<(String, u32)> = decoded
3639                .iter()
3640                .map(|(tag, value)| {
3641                    // `skrifa::Tag`'s Debug prints `Tag(wght)`.
3642                    let name = format!("{tag:?}");
3643                    let name = name
3644                        .trim_start_matches("Tag(")
3645                        .trim_end_matches(')')
3646                        .to_string();
3647                    (name, value.to_bits())
3648                })
3649                .collect();
3650            assert_eq!(got, expected, "{source}");
3651        }
3652    }
3653
3654    /// `CacheKey::variation` is a public field, so a hand-built key need not
3655    /// be canonical. A malformed one must be ignored, not panic: panicking
3656    /// here would poison the global text-engine mutex.
3657    #[test]
3658    fn malformed_variation_keys_are_ignored_not_fatal() {
3659        assert!(decode_variation(None).is_empty());
3660        assert!(decode_variation(Some("")).is_empty());
3661        for malformed in ["wght", "abc", "0123456789ab", "zzzz", "wght!!", "\"wght\""] {
3662            assert!(
3663                decode_variation(Some(malformed)).is_empty(),
3664                "{malformed:?} must decode to nothing"
3665            );
3666        }
3667        // A valid entry followed by a truncated one keeps the valid part.
3668        let valid = canonical_variation(Some("\"wght\" 700")).expect("canonical key");
3669        let padded = format!("{valid}abc");
3670        assert_eq!(decode_variation(Some(&padded)).len(), 1);
3671    }
3672
3673    #[test]
3674    fn bidi_caret_positions_follow_visual_order() {
3675        let text = "אבג";
3676        let metrics = metrics_for_textfield(text, 32.0, None, 400, 0, 0.0, None);
3677        assert!(metrics.positions.windows(2).all(|w| w[0] > w[1]));
3678        assert!(
3679            metrics
3680                .positions
3681                .iter()
3682                .all(|position| position.is_finite())
3683        );
3684
3685        let mixed = "אב abc";
3686        let metrics = metrics_for_textfield(mixed, 32.0, None, 400, 0, 0.0, None);
3687        assert!(metrics.positions[0] > metrics.positions[1]);
3688        assert!(metrics.positions[1] > metrics.positions[2]);
3689        assert!(metrics.positions[4] < metrics.positions[5]);
3690        assert!(metrics.positions[5] < metrics.positions[6]);
3691    }
3692
3693    #[test]
3694    fn utf8_layout_and_wrapping_do_not_panic() {
3695        let text = "e\u{301} 👩‍👩‍👧‍👦 🇺🇸 אב\u{00A0}界";
3696        let result = std::panic::catch_unwind(|| {
3697            let _ = metrics_for_textfield(text, 16.0, None, 400, 0, 0.0, None);
3698            let _ = wrap_line_ranges_with_family(
3699                text,
3700                16.0,
3701                12.0,
3702                Some(2),
3703                true,
3704                None,
3705                400,
3706                0,
3707                0.0,
3708                None,
3709            );
3710        });
3711        assert!(result.is_ok());
3712    }
3713}