Skip to main content

repose_text/
lib.rs

1use font_awl::FontProvider;
2use once_cell::sync::OnceCell;
3use rapidhash::{HashMapExt, RapidHashMap, fast::RapidHasher};
4use skrifa::MetadataProvider;
5use skrifa::outline::OutlinePen;
6
7pub mod fallback;
8pub mod fallback_data;
9
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::{
12    collections::{HashMap, VecDeque},
13    hash::{Hash, Hasher},
14    sync::Mutex,
15};
16use unicode_segmentation::UnicodeSegmentation;
17
18static FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
19static FALLBACK_DIRTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
20
21pub fn begin_frame() {
22    FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
23}
24
25pub fn current_frame() -> u64 {
26    FRAME_COUNTER.load(Ordering::Relaxed)
27}
28
29pub fn take_fallback_dirty() -> bool {
30    FALLBACK_DIRTY.swap(false, Ordering::Relaxed)
31}
32
33const GLYPH_CACHE_CAP: usize = 4096;
34const WRAP_CACHE_CAP: usize = 1024;
35const ELLIP_CACHE_CAP: usize = 2048;
36
37static METRICS_LRU: OnceCell<Mutex<Lru<(u64, u32, u64, u16, u8, i32, u64), TextMetrics>>> =
38    OnceCell::new();
39fn metrics_cache() -> &'static Mutex<Lru<(u64, u32, u64, u16, u8, i32, u64), TextMetrics>> {
40    METRICS_LRU.get_or_init(|| Mutex::new(Lru::new(4096)))
41}
42
43struct Lru<K, V> {
44    map: RapidHashMap<K, V>,
45    order: VecDeque<K>,
46    cap: usize,
47}
48impl<K: std::hash::Hash + Eq + Clone, V> Lru<K, V> {
49    fn new(cap: usize) -> Self {
50        Self {
51            map: RapidHashMap::new(),
52            order: VecDeque::new(),
53            cap,
54        }
55    }
56    fn get(&mut self, k: &K) -> Option<&V> {
57        if self.map.contains_key(k)
58            && let Some(pos) = self.order.iter().position(|x| x == k)
59        {
60            let key = self.order.remove(pos).unwrap();
61            self.order.push_back(key);
62        }
63        self.map.get(k)
64    }
65    fn put(&mut self, k: K, v: V) {
66        if self.map.contains_key(&k) {
67            self.map.insert(k.clone(), v);
68            if let Some(pos) = self.order.iter().position(|x| x == &k) {
69                let key = self.order.remove(pos).unwrap();
70                self.order.push_back(key);
71            }
72            return;
73        }
74        if self.map.len() >= self.cap
75            && let Some(old) = self.order.pop_front()
76        {
77            self.map.remove(&old);
78        }
79        self.order.push_back(k.clone());
80        self.map.insert(k, v);
81    }
82}
83
84static WRAP_LRU: OnceCell<
85    Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<String>, bool)>>,
86> = OnceCell::new();
87
88static WRAP_RANGES_LRU: OnceCell<
89    Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<(usize, usize)>, bool)>>,
90> = OnceCell::new();
91
92static ELLIP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32, u16, u8, i32, u64), String>>> =
93    OnceCell::new();
94
95fn wrap_cache()
96-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<String>, bool)>> {
97    WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
98}
99
100fn wrap_ranges_cache()
101-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<(usize, usize)>, bool)>> {
102    WRAP_RANGES_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
103}
104
105fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, u8, i32, u64), String>> {
106    ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
107}
108
109fn fast_hash(s: &str) -> u64 {
110    let mut h = RapidHasher::default();
111    s.len().hash(&mut h);
112    s.hash(&mut h);
113    h.finish()
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub struct GlyphKey(pub u64);
118
119/// Cache key for the renderer's glyph slug cache -> uniquely identifies a
120/// specific glyph in a specific font face.
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
122pub struct CacheKey {
123    pub font_id: u64,
124    pub glyph_id: u16,
125    pub font_size_bits: u32,
126}
127
128/// Vector path command for glyph outlines.
129#[derive(Clone, Debug)]
130pub enum Command {
131    MoveTo(f32, f32),
132    LineTo(f32, f32),
133    QuadTo(f32, f32, f32, f32),
134    CurveTo(f32, f32, f32, f32, f32, f32),
135    Close,
136}
137
138pub struct ShapedGlyph {
139    pub key: GlyphKey,
140    pub px: f32,
141    pub x: f32,
142    pub y: f32,
143    pub w: f32,
144    pub h: f32,
145    pub bearing_x: f32,
146    pub bearing_y: f32,
147    pub advance: f32,
148}
149
150pub use swash::scale::image::Content as SwashContent;
151
152pub struct GlyphBitmap {
153    pub key: GlyphKey,
154    pub w: u32,
155    pub h: u32,
156    pub content: SwashContent,
157    pub data: Vec<u8>,
158}
159
160struct FontRecord {
161    id: u64,
162    data: parley::FontData,
163    data_bytes: Vec<u8>,
164}
165
166struct Engine {
167    font_cx: parley::FontContext,
168    layout_cx: parley::LayoutContext<()>,
169    swash_cx: swash::scale::ScaleContext,
170    key_map: HashMap<GlyphKey, (u64, u16)>,
171    font_registry: Vec<FontRecord>,
172    next_font_id: u64,
173    /// Cache of rendered glyphs keyed by (font_id, glyph_id, font_size_bits).
174    /// Contains (width, height, left, top, content, data).
175    glyph_cache:
176        HashMap<(u64, u16, u32), (u32, u32, i32, i32, swash::scale::image::Content, Vec<u8>)>,
177}
178
179impl Engine {
180    fn ensure_font(&mut self, fd: &parley::FontData) -> u64 {
181        if let Some(existing) = self.font_registry.iter().find(|r| r.data == *fd) {
182            log::debug!(
183                "[font] reuse id={} len={}",
184                existing.id,
185                fd.data.as_ref().len()
186            );
187            return existing.id;
188        }
189        let id = self.next_font_id;
190        self.next_font_id += 1;
191        let bytes = fd.data.as_ref().to_vec();
192        log::debug!("[font] register id={} len={}", id, bytes.len());
193        self.font_registry.push(FontRecord {
194            id,
195            data: fd.clone(),
196            data_bytes: bytes,
197        });
198        id
199    }
200
201    fn trim_glyph_cache(&mut self) {
202        if self.glyph_cache.len() > GLYPH_CACHE_CAP {
203            let to_remove = self.glyph_cache.len() - GLYPH_CACHE_CAP;
204            let keys: Vec<_> = self.glyph_cache.keys().take(to_remove).copied().collect();
205            for k in keys {
206                self.glyph_cache.remove(&k);
207            }
208        }
209    }
210
211    fn raster_placement(
212        &mut self,
213        font_id: u64,
214        glyph_id: u16,
215        px: f32,
216    ) -> Option<(f32, f32, f32, f32)> {
217        use swash::scale::{Render, Source, StrikeWith};
218        let cache_key = (font_id, glyph_id, px.to_bits());
219        if let Some(cached) = self.glyph_cache.get(&cache_key) {
220            log::debug!(
221                "[raster_placement] HIT fid={} gid={} px={} => {}x{} {}x{}",
222                font_id,
223                glyph_id,
224                px,
225                cached.0,
226                cached.1,
227                cached.2,
228                cached.3
229            );
230            return Some((
231                cached.0 as f32,
232                cached.1 as f32,
233                cached.2 as f32,
234                cached.3 as f32,
235            ));
236        }
237        let data_bytes = self
238            .font_registry
239            .iter()
240            .find(|r| r.id == font_id)?
241            .data_bytes
242            .clone();
243        let font = swash::FontRef::from_index(&data_bytes, 0)?;
244        let mut scaler = self.swash_cx.builder(font).size(px).hint(true).build();
245        let image = Render::new(&[
246            Source::Outline,
247            Source::ColorBitmap(StrikeWith::BestFit),
248            Source::ColorOutline(0),
249        ])
250        .render(&mut scaler, glyph_id)?;
251        log::debug!(
252            "[raster_placement] MISS fid={} gid={} px={} => {}x{} {}x{}",
253            font_id,
254            glyph_id,
255            px,
256            image.placement.width,
257            image.placement.height,
258            image.placement.left,
259            image.placement.top
260        );
261        self.glyph_cache.insert(
262            cache_key,
263            (
264                image.placement.width,
265                image.placement.height,
266                image.placement.left,
267                image.placement.top,
268                image.content,
269                image.data,
270            ),
271        );
272        self.trim_glyph_cache();
273        Some((
274            image.placement.width as f32,
275            image.placement.height as f32,
276            image.placement.left as f32,
277            image.placement.top as f32,
278        ))
279    }
280}
281
282static ENGINE: OnceCell<Mutex<Engine>> = OnceCell::new();
283
284pub static FONT_PROVIDER: OnceCell<Mutex<font_awl::Provider>> = OnceCell::new();
285
286fn init_engine_sync() -> Engine {
287    let mut provider = font_awl::Provider::new();
288    provider.load_bundled_fonts();
289    #[cfg(not(target_arch = "wasm32"))]
290    if let Err(e) = provider.load_system_fonts_best_effort() {
291        log::warn!("font-awl: failed to load system fonts: {e}");
292    }
293
294    let mut font_cx = provider.new_parley_context();
295    let layout_cx = parley::LayoutContext::new();
296
297    static MATERIAL_SYMBOLS_TTF: &[u8] = include_bytes!("assets/MaterialSymbolsOutlined.ttf");
298    let blob: parley::fontique::Blob<u8> = MATERIAL_SYMBOLS_TTF.to_vec().into();
299    font_cx.collection.register_fonts(blob, None);
300
301    let _ = FONT_PROVIDER.set(Mutex::new(provider));
302
303    Engine {
304        font_cx,
305        layout_cx,
306        swash_cx: swash::scale::ScaleContext::new(),
307        key_map: HashMap::new(),
308        font_registry: Vec::new(),
309        next_font_id: 1,
310        glyph_cache: HashMap::new(),
311    }
312}
313
314#[cfg(target_arch = "wasm32")]
315pub async fn init_fonts_wasm() {
316    let mut provider = font_awl::Provider::new();
317    provider.load_bundled_fonts();
318    if let Err(e) = provider.load_web_fonts().await {
319        log::warn!("font-awl: failed to load web fonts: {e}");
320    }
321    let _ = FONT_PROVIDER.set(Mutex::new(provider));
322
323    if let Some(eng) = ENGINE.get() {
324        let mut eng = eng.lock().unwrap();
325        // Register web fonts into the existing engine's collection
326        // by re-building font_cx from the updated provider
327        if let Some(provider_lock) = FONT_PROVIDER.get() {
328            let p = provider_lock.lock().unwrap();
329            eng.font_cx = p.new_parley_context();
330        }
331    }
332}
333
334fn engine() -> &'static Mutex<Engine> {
335    ENGINE.get_or_init(|| Mutex::new(init_engine_sync()))
336}
337
338pub fn register_font_data(bytes: &[u8]) {
339    let mut eng = engine().lock().unwrap();
340    let blob: parley::fontique::Blob<u8> = bytes.to_vec().into();
341    eng.font_cx.collection.register_fonts(blob.clone(), None);
342    if let Some(provider_lock) = FONT_PROVIDER.get() {
343        let mut p = provider_lock.lock().unwrap();
344        p.collection_mut().register_fonts(blob, None);
345    }
346    // Invalidate caches so text with newly available glyphs will relayout
347    clear_caches_for_fallback();
348}
349
350pub(crate) fn clear_caches_for_fallback() {
351    if let Some(c) = METRICS_LRU.get() {
352        c.lock().unwrap().map.clear();
353        c.lock().unwrap().order.clear();
354    }
355    if let Some(c) = WRAP_LRU.get() {
356        c.lock().unwrap().map.clear();
357        c.lock().unwrap().order.clear();
358    }
359    if let Some(c) = WRAP_RANGES_LRU.get() {
360        c.lock().unwrap().map.clear();
361        c.lock().unwrap().order.clear();
362    }
363    if let Some(c) = ELLIP_LRU.get() {
364        c.lock().unwrap().map.clear();
365        c.lock().unwrap().order.clear();
366    }
367    // Also bump frame counter to signal stale
368    bump_frame_for_fallback();
369}
370
371pub(crate) fn bump_frame_for_fallback() {
372    FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
373    FALLBACK_DIRTY.store(true, Ordering::Relaxed);
374}
375
376#[cfg(target_arch = "wasm32")]
377pub fn ensure_web_fallback_initialized() {
378    crate::fallback::wasm_fallback::ensure_fallback_initialized();
379}
380
381#[cfg(not(target_arch = "wasm32"))]
382pub fn ensure_web_fallback_initialized() {}
383
384/// Load a font from a file path and register it into the global font system.
385///
386/// Returns an error if the file cannot be read.
387pub fn load_font_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
388    let bytes = std::fs::read(path)?;
389    register_font_data(&bytes);
390    Ok(())
391}
392
393/// Extract the family name from raw font bytes.
394///
395/// Tries the typographic family name first, falling back to the standard family name.
396/// Returns `None` if the font data is invalid or contains no names.
397pub fn font_family_name(bytes: &[u8]) -> Option<String> {
398    use skrifa::string::StringId;
399    let font = skrifa::FontRef::new(bytes).ok()?;
400    font.localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME)
401        .english_or_first()
402        .map(|s| s.to_string())
403        .or_else(|| {
404            font.localized_strings(StringId::FAMILY_NAME)
405                .english_or_first()
406                .map(|s| s.to_string())
407        })
408}
409
410fn key_from_pair(font_id: u64, glyph_id: u16) -> GlyphKey {
411    let mut h = RapidHasher::default();
412    font_id.hash(&mut h);
413    glyph_id.hash(&mut h);
414    GlyphKey(h.finish())
415}
416
417#[cfg(target_arch = "wasm32")]
418fn collect_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) -> Vec<u32> {
419    use parley::layout::PositionedLayoutItem;
420    let mut out = Vec::new();
421    for line in layout.lines() {
422        for item in line.items() {
423            let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
424                continue;
425            };
426            let run = glyph_run.run();
427            // parley clusters already grouped by text; if glyph id==0 => missing
428            for cluster in run.clusters() {
429                let has_missing = cluster.glyphs().any(|g| g.id == 0);
430                if has_missing {
431                    let range = cluster.text_range();
432                    // slice may be invalid if out of bounds? clamp
433                    let end = range.end.min(text.len());
434                    let start = range.start.min(end);
435                    for ch in text[start..end].chars() {
436                        out.push(ch as u32);
437                    }
438                    // Fallback: if text_range empty but still missing, push replacement
439                    if range.start == range.end {
440                        // try to guess from glyph? skip
441                    }
442                }
443            }
444        }
445    }
446    out
447}
448
449fn shape_line_inner(
450    eng: &mut Engine,
451    text: &str,
452    px: f32,
453    line_height_ratio: f32,
454    font_family: Option<&str>,
455    font_weight: u16,
456    font_style: u8,
457    letter_spacing: f32,
458    font_variation_settings: Option<&str>,
459) -> Vec<ShapedGlyph> {
460    use parley::FontWeight;
461    use parley::layout::PositionedLayoutItem;
462    use parley::style::StyleProperty;
463
464    let Engine {
465        ref mut font_cx,
466        ref mut layout_cx,
467        ..
468    } = *eng;
469    let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
470    builder.push_default(StyleProperty::FontSize(px));
471    if line_height_ratio > 0.0 {
472        builder.push_default(StyleProperty::LineHeight(
473            parley::LineHeight::FontSizeRelative(line_height_ratio),
474        ));
475    }
476    builder.push_default(StyleProperty::FontWeight(FontWeight::new(
477        font_weight as f32,
478    )));
479    builder.push_default(StyleProperty::FontStyle(match font_style {
480        1 => parley::FontStyle::Italic,
481        _ => parley::FontStyle::Normal,
482    }));
483    builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
484
485    if let Some(settings) = font_variation_settings {
486        builder.push_default(StyleProperty::FontVariations(
487            parley::style::FontVariations::from(settings),
488        ));
489    }
490
491    if let Some(family) = font_family {
492        use parley::style::{FontFamilyName, GenericFamily};
493        let names: &[FontFamilyName] = match family {
494            "monospace" => &[
495                FontFamilyName::named("JetBrains Mono"),
496                GenericFamily::Monospace.into(),
497            ],
498            "sans-serif" => &[
499                FontFamilyName::named("Open Sans"),
500                GenericFamily::SansSerif.into(),
501            ],
502            "emoji" => &[
503                FontFamilyName::named("Noto Color Emoji"),
504                GenericFamily::Emoji.into(),
505            ],
506            "serif" => &[GenericFamily::Serif.into()],
507            "cursive" => &[GenericFamily::Cursive.into()],
508            "fantasy" => &[GenericFamily::Fantasy.into()],
509            "system-ui" => &[GenericFamily::SystemUi.into()],
510            "math" => &[GenericFamily::Math.into()],
511            _ => &[FontFamilyName::named(family)],
512        };
513        builder.push(names, 0..text.len());
514    }
515
516    let mut layout = builder.build(text);
517    layout.break_all_lines(None);
518    layout.align(
519        parley::Alignment::Start,
520        parley::AlignmentOptions::default(),
521    );
522
523    // Detect unresolved codepoints for web fallback (tofu = gid 0)
524    #[cfg(target_arch = "wasm32")]
525    {
526        let unresolved = collect_unresolved_codepoints(&layout, text);
527        if !unresolved.is_empty() {
528            crate::fallback::wasm_fallback::submit_unresolved(unresolved);
529            // Ensure background task is running
530            crate::fallback::wasm_fallback::ensure_fallback_initialized();
531        }
532    }
533
534    let mut out: Vec<ShapedGlyph> = Vec::new();
535    for line in layout.lines() {
536        for item in line.items() {
537            let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
538                continue;
539            };
540            let font_data = glyph_run.run().font();
541            let fid = eng.ensure_font(font_data);
542            log::debug!(
543                "[shape] run: fid={} font_data_len={}",
544                fid,
545                font_data.data.as_ref().len()
546            );
547            for g in glyph_run.positioned_glyphs() {
548                let gid = g.id as u16;
549                let key = key_from_pair(fid, gid);
550                eng.key_map.insert(key, (fid, gid));
551
552                let (w, h, left, top) = eng
553                    .raster_placement(fid, gid, px)
554                    .unwrap_or((0.0, 0.0, 0.0, 0.0));
555
556                log::debug!(
557                    "[shape] glyph: gid={} px={} x={:.1} y={:.1} advance={:.1} bitmap={}x{} {}x{}",
558                    gid,
559                    px,
560                    g.x,
561                    g.y,
562                    g.advance,
563                    w,
564                    h,
565                    left,
566                    top,
567                );
568
569                out.push(ShapedGlyph {
570                    key,
571                    px,
572                    x: g.x,
573                    y: g.y,
574                    w,
575                    h,
576                    bearing_x: left,
577                    bearing_y: top,
578                    advance: g.advance + letter_spacing,
579                });
580            }
581        }
582    }
583    out
584}
585
586pub fn shape_line(
587    text: &str,
588    px: f32,
589    line_height_ratio: f32,
590    font_family: Option<&str>,
591    font_weight: u16,
592    font_style: u8,
593    letter_spacing: f32,
594    font_variation_settings: Option<&str>,
595) -> Vec<ShapedGlyph> {
596    let mut eng = engine().lock().unwrap();
597    shape_line_inner(
598        &mut eng,
599        text,
600        px,
601        line_height_ratio,
602        font_family,
603        font_weight,
604        font_style,
605        letter_spacing,
606        font_variation_settings,
607    )
608}
609
610pub fn rasterize(key: GlyphKey, px: f32) -> Option<GlyphBitmap> {
611    use swash::scale::{Render, Source, StrikeWith};
612    let mut eng = engine().lock().unwrap();
613    let &(fid, gid) = eng.key_map.get(&key)?;
614    let cache_key = (fid, gid, px.to_bits());
615    if let Some(cached) = eng.glyph_cache.get(&cache_key) {
616        log::debug!(
617            "[rasterize] HIT fid={} gid={} px={} => {}x{}",
618            fid,
619            gid,
620            px,
621            cached.0,
622            cached.1
623        );
624        return Some(GlyphBitmap {
625            key,
626            w: cached.0,
627            h: cached.1,
628            content: cached.4,
629            data: cached.5.clone(),
630        });
631    }
632    let data_bytes = eng
633        .font_registry
634        .iter()
635        .find(|r| r.id == fid)?
636        .data_bytes
637        .clone();
638    let font = swash::FontRef::from_index(&data_bytes, 0)?;
639    let mut scaler = eng.swash_cx.builder(font).size(px).hint(true).build();
640    let image = Render::new(&[
641        Source::Outline,
642        Source::ColorBitmap(StrikeWith::BestFit),
643        Source::ColorOutline(0),
644    ])
645    .render(&mut scaler, gid)?;
646    log::debug!(
647        "[rasterize] MISS fid={} gid={} px={} => {}x{}",
648        fid,
649        gid,
650        px,
651        image.placement.width,
652        image.placement.height
653    );
654    let bitmap = GlyphBitmap {
655        key,
656        w: image.placement.width,
657        h: image.placement.height,
658        content: image.content,
659        data: image.data,
660    };
661    eng.glyph_cache.insert(
662        cache_key,
663        (
664            bitmap.w,
665            bitmap.h,
666            image.placement.left,
667            image.placement.top,
668            bitmap.content,
669            bitmap.data.clone(),
670        ),
671    );
672    eng.trim_glyph_cache();
673    Some(bitmap)
674}
675
676pub fn lookup_cache_key(key: GlyphKey, px: f32) -> Option<CacheKey> {
677    let eng = engine().lock().unwrap();
678    let &(fid, gid) = eng.key_map.get(&key)?;
679    Some(CacheKey {
680        font_id: fid,
681        glyph_id: gid,
682        font_size_bits: px.to_bits(),
683    })
684}
685
686fn extract_outlines_for(data_bytes: &[u8], glyph_id: u16) -> Option<Box<[Command]>> {
687    let font = skrifa::FontRef::new(data_bytes).ok()?;
688    let mut pen = OutlinePenCollector(Vec::new());
689    font.outline_glyphs()
690        .get(skrifa::GlyphId::new(glyph_id as u32))?
691        .draw(skrifa::instance::Size::new(1.0), &mut pen)
692        .ok()?;
693    Some(pen.0.into_boxed_slice())
694}
695
696pub fn extract_outline_commands(cache_key: CacheKey) -> Option<Box<[Command]>> {
697    let eng = engine().lock().unwrap();
698    let record = eng
699        .font_registry
700        .iter()
701        .find(|r| r.id == cache_key.font_id)?;
702    extract_outlines_for(&record.data_bytes, cache_key.glyph_id)
703}
704
705pub fn lookup_and_extract_outline(key: GlyphKey, px: f32) -> Option<(CacheKey, Box<[Command]>)> {
706    let eng = engine().lock().unwrap();
707    let &(fid, gid) = eng.key_map.get(&key)?;
708    let record = eng.font_registry.iter().find(|r| r.id == fid)?;
709    let ck = CacheKey {
710        font_id: fid,
711        glyph_id: gid,
712        font_size_bits: px.to_bits(),
713    };
714    let cmds = extract_outlines_for(&record.data_bytes, gid)?;
715    Some((ck, cmds))
716}
717
718struct OutlinePenCollector(Vec<Command>);
719
720impl OutlinePen for OutlinePenCollector {
721    fn move_to(&mut self, x: f32, y: f32) {
722        self.0.push(Command::MoveTo(x, y));
723    }
724    fn line_to(&mut self, x: f32, y: f32) {
725        self.0.push(Command::LineTo(x, y));
726    }
727    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
728        self.0.push(Command::QuadTo(cx0, cy0, x, y));
729    }
730    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
731        self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y));
732    }
733    fn close(&mut self) {
734        self.0.push(Command::Close);
735    }
736}
737
738#[derive(Clone)]
739pub struct TextMetrics {
740    pub positions: Vec<f32>,
741    pub byte_offsets: Vec<usize>,
742}
743
744pub fn metrics_for_textfield(
745    text: &str,
746    px: f32,
747    font_family: Option<&str>,
748    font_weight: u16,
749    font_style: u8,
750    letter_spacing: f32,
751    font_variation_settings: Option<&str>,
752) -> TextMetrics {
753    let family_hash = font_family.map(fast_hash).unwrap_or(0);
754    let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
755    let key = (
756        fast_hash(text),
757        (px * 100.0) as u32,
758        family_hash,
759        font_weight,
760        font_style,
761        (letter_spacing * 100.0) as i32,
762        fvs_hash,
763    );
764    if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
765        return m;
766    }
767    let mut eng = engine().lock().unwrap();
768
769    use parley::FontWeight;
770    use parley::style::StyleProperty;
771
772    let Engine {
773        ref mut font_cx,
774        ref mut layout_cx,
775        ..
776    } = *eng;
777    let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
778    builder.push_default(StyleProperty::FontSize(px));
779    builder.push_default(StyleProperty::FontWeight(FontWeight::new(
780        font_weight as f32,
781    )));
782    builder.push_default(StyleProperty::FontStyle(match font_style {
783        1 => parley::FontStyle::Italic,
784        _ => parley::FontStyle::Normal,
785    }));
786    builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
787    if let Some(settings) = font_variation_settings {
788        builder.push_default(StyleProperty::FontVariations(
789            parley::style::FontVariations::from(settings),
790        ));
791    }
792    if let Some(family) = font_family {
793        use parley::style::{FontFamilyName, GenericFamily};
794        let names: &[FontFamilyName] = match family {
795            "monospace" => &[
796                FontFamilyName::named("JetBrains Mono"),
797                GenericFamily::Monospace.into(),
798            ],
799            "sans-serif" => &[
800                FontFamilyName::named("Open Sans"),
801                GenericFamily::SansSerif.into(),
802            ],
803            "emoji" => &[
804                FontFamilyName::named("Noto Color Emoji"),
805                GenericFamily::Emoji.into(),
806            ],
807            "serif" => &[GenericFamily::Serif.into()],
808            "cursive" => &[GenericFamily::Cursive.into()],
809            "fantasy" => &[GenericFamily::Fantasy.into()],
810            "system-ui" => &[GenericFamily::SystemUi.into()],
811            "math" => &[GenericFamily::Math.into()],
812            _ => &[FontFamilyName::named(family)],
813        };
814        builder.push(names, 0..text.len());
815    }
816
817    let mut layout = builder.build(text);
818    layout.break_all_lines(None);
819    layout.align(
820        parley::Alignment::Start,
821        parley::AlignmentOptions::default(),
822    );
823
824    #[cfg(target_arch = "wasm32")]
825    {
826        let unresolved = collect_unresolved_codepoints(&layout, text);
827        if !unresolved.is_empty() {
828            crate::fallback::wasm_fallback::submit_unresolved(unresolved);
829        }
830    }
831
832    let mut edges: Vec<(usize, f32)> = Vec::new();
833    let mut last_x = 0.0f32;
834    let mut glyph_idx = 0usize;
835    for line in layout.lines() {
836        for item in line.items() {
837            let parley::layout::PositionedLayoutItem::GlyphRun(glyph_run) = item else {
838                continue;
839            };
840            let run_offset = glyph_run.offset();
841            let run = glyph_run.run();
842            let mut cluster_offset = run_offset;
843            for cluster in run.clusters() {
844                let range = cluster.text_range();
845                for g in cluster.glyphs() {
846                    let shift = glyph_idx as f32 * letter_spacing;
847                    let x_pos = cluster_offset + g.x;
848                    let right = x_pos + shift + g.advance + letter_spacing;
849                    last_x = right.max(last_x);
850                    edges.push((range.end, right));
851                    glyph_idx += 1;
852                    cluster_offset += g.advance;
853                }
854            }
855        }
856    }
857    if edges.last().map(|e| e.0) != Some(text.len()) {
858        edges.push((text.len(), last_x));
859    }
860
861    let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
862    let mut byte_offsets = Vec::with_capacity(positions.capacity());
863    positions.push(0.0);
864    byte_offsets.push(0);
865    let mut last_byte = 0usize;
866    for (b, _) in text.grapheme_indices(true) {
867        positions
868            .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
869        byte_offsets.push(b);
870        last_byte = b;
871    }
872    if *byte_offsets.last().unwrap_or(&0) != text.len() {
873        positions.push(
874            positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
875        );
876        byte_offsets.push(text.len());
877    }
878    let m = TextMetrics {
879        positions,
880        byte_offsets,
881    };
882    metrics_cache().lock().unwrap().put(key, m.clone());
883    m
884}
885
886fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
887    let x0 = lookup_right(edges, start_b);
888    let x1 = lookup_right(edges, end_b);
889    (x1 - x0).max(0.0)
890}
891fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
892    match edges.binary_search_by_key(&b, |e| e.0) {
893        Ok(i) => edges[i].1,
894        Err(i) => {
895            if i == 0 {
896                0.0
897            } else {
898                edges[i - 1].1
899            }
900        }
901    }
902}
903
904pub fn wrap_lines(
905    text: &str,
906    px: f32,
907    max_width: f32,
908    max_lines: Option<usize>,
909    soft_wrap: bool,
910    font_weight: u16,
911    font_style: u8,
912    letter_spacing: f32,
913    font_variation_settings: Option<&str>,
914) -> (Vec<String>, bool) {
915    if text.is_empty() || max_width <= 0.0 {
916        return (vec![String::new()], false);
917    }
918    if !soft_wrap {
919        return (vec![text.to_string()], false);
920    }
921
922    let max_lines_key: u16 = match max_lines {
923        None => 0,
924        Some(n) => {
925            let n = n.min(u16::MAX as usize - 1) as u16;
926            n.saturating_add(1)
927        }
928    };
929    let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
930    let key = (
931        fast_hash(text),
932        (px * 100.0) as u32,
933        (max_width * 100.0) as u32,
934        max_lines_key,
935        soft_wrap,
936        font_weight,
937        font_style,
938        (letter_spacing * 100.0) as i32,
939        fvs_hash,
940    );
941    if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
942        return h;
943    }
944
945    let m = metrics_for_textfield(
946        text,
947        px,
948        None,
949        font_weight,
950        font_style,
951        letter_spacing,
952        font_variation_settings,
953    );
954    if let Some(&last) = m.positions.last()
955        && last <= max_width + 0.5
956    {
957        return (vec![text.to_string()], false);
958    }
959
960    let width_of = |start_b: usize, end_b: usize| -> f32 {
961        let i0 = match m.byte_offsets.binary_search(&start_b) {
962            Ok(i) | Err(i) => i,
963        };
964        let i1 = match m.byte_offsets.binary_search(&end_b) {
965            Ok(i) | Err(i) => i,
966        };
967        (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
968            .max(0.0)
969    };
970
971    let mut out: Vec<String> = Vec::new();
972    let mut truncated = false;
973
974    let mut line_start = 0usize;
975    let mut best_break = line_start;
976
977    for tok in text.split_word_bounds() {
978        let tok_start = best_break;
979        let tok_end = tok_start + tok.len();
980        let w = width_of(line_start, tok_end);
981
982        if w <= max_width + 0.5 {
983            best_break = tok_end;
984            continue;
985        }
986
987        if best_break > line_start {
988            out.push(text[line_start..best_break].trim_end().to_string());
989            line_start = best_break;
990        } else {
991            let mut cut = tok_start;
992            for g in tok.grapheme_indices(true) {
993                let next = tok_start + g.0 + g.1.len();
994                if width_of(line_start, next) <= max_width + 0.5 {
995                    cut = next;
996                } else {
997                    break;
998                }
999            }
1000            if cut == line_start
1001                && let Some((ofs, grapheme)) = tok.grapheme_indices(true).next()
1002            {
1003                cut = tok_start + ofs + grapheme.len();
1004            }
1005            out.push(text[line_start..cut].to_string());
1006            line_start = cut;
1007        }
1008
1009        if let Some(ml) = max_lines
1010            && out.len() >= ml
1011        {
1012            truncated = true;
1013            line_start = line_start.min(text.len());
1014            break;
1015        }
1016
1017        best_break = line_start;
1018
1019        if line_start < tok_end && width_of(line_start, tok_end) <= max_width + 0.5 {
1020            best_break = tok_end;
1021        }
1022    }
1023
1024    if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
1025        out.push(text[line_start..].trim_end().to_string());
1026    }
1027
1028    let res = (out, truncated);
1029
1030    wrap_cache().lock().unwrap().put(key, res.clone());
1031    res
1032}
1033
1034pub fn wrap_line_ranges(
1035    text: &str,
1036    px: f32,
1037    max_width: f32,
1038    max_lines: Option<usize>,
1039    soft_wrap: bool,
1040    font_weight: u16,
1041    font_style: u8,
1042    letter_spacing: f32,
1043    font_variation_settings: Option<&str>,
1044) -> (Vec<(usize, usize)>, bool) {
1045    if text.is_empty() || max_width <= 0.0 {
1046        return (vec![(0, 0)], false);
1047    }
1048    if !soft_wrap {
1049        let mut out = Vec::new();
1050        let mut start = 0usize;
1051        for (i, ch) in text.char_indices() {
1052            if ch == '\n' {
1053                out.push((start, i));
1054                start = i + 1;
1055            }
1056        }
1057        out.push((start, text.len()));
1058        return (out, false);
1059    }
1060
1061    let max_lines_key: u16 = match max_lines {
1062        None => 0,
1063        Some(n) => {
1064            let n = n.min(u16::MAX as usize - 1) as u16;
1065            n.saturating_add(1)
1066        }
1067    };
1068    let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1069    let key = (
1070        fast_hash(text),
1071        (px * 100.0) as u32,
1072        (max_width * 100.0) as u32,
1073        max_lines_key,
1074        soft_wrap,
1075        font_weight,
1076        font_style,
1077        (letter_spacing * 100.0) as i32,
1078        fvs_hash,
1079    );
1080    if let Some(v) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
1081        return v;
1082    }
1083
1084    let m = metrics_for_textfield(
1085        text,
1086        px,
1087        None,
1088        font_weight,
1089        font_style,
1090        letter_spacing,
1091        font_variation_settings,
1092    );
1093
1094    let width_of = |start_b: usize, end_b: usize| -> f32 {
1095        let i0 = match m.byte_offsets.binary_search(&start_b) {
1096            Ok(i) | Err(i) => i,
1097        };
1098        let i1 = match m.byte_offsets.binary_search(&end_b) {
1099            Ok(i) | Err(i) => i,
1100        };
1101        (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
1102            .max(0.0)
1103    };
1104
1105    let mut out: Vec<(usize, usize)> = Vec::new();
1106    let mut truncated = false;
1107
1108    let mut line0_start = 0usize;
1109    for (i, ch) in text.char_indices() {
1110        if ch == '\n' {
1111            let (mut ranges, tr) = wrap_one_hard_line_ranges(
1112                text,
1113                line0_start,
1114                i,
1115                max_width,
1116                max_lines.map(|ml| ml.saturating_sub(out.len())),
1117                &width_of,
1118            );
1119            out.append(&mut ranges);
1120            if tr {
1121                truncated = true;
1122                break;
1123            }
1124            line0_start = i + 1;
1125
1126            if let Some(ml) = max_lines
1127                && out.len() >= ml
1128            {
1129                truncated = true;
1130                break;
1131            }
1132        }
1133    }
1134    if !truncated {
1135        let (mut ranges, tr) = wrap_one_hard_line_ranges(
1136            text,
1137            line0_start,
1138            text.len(),
1139            max_width,
1140            max_lines.map(|ml| ml.saturating_sub(out.len())),
1141            &width_of,
1142        );
1143        out.append(&mut ranges);
1144        truncated = tr;
1145    }
1146
1147    if out.is_empty() {
1148        out.push((0, 0));
1149    }
1150
1151    let res = (out, truncated);
1152    wrap_ranges_cache().lock().unwrap().put(key, res.clone());
1153    res
1154}
1155
1156fn wrap_one_hard_line_ranges(
1157    text: &str,
1158    start: usize,
1159    end: usize,
1160    max_width: f32,
1161    max_lines: Option<usize>,
1162    width_of: &dyn Fn(usize, usize) -> f32,
1163) -> (Vec<(usize, usize)>, bool) {
1164    let mut out = Vec::new();
1165    let mut t = false;
1166
1167    if start >= end {
1168        out.push((start, start));
1169        return (out, false);
1170    }
1171
1172    if width_of(start, end) <= max_width + 0.5 {
1173        out.push((start, end));
1174        return (out, false);
1175    }
1176
1177    let mut line_start = start;
1178    let mut best_break = line_start;
1179    let mut unconsumed_start = start;
1180
1181    for tok in text[line_start..end].split_word_bounds() {
1182        let tok_abs_start = unconsumed_start;
1183        let tok_abs_end = tok_abs_start + tok.len();
1184        unconsumed_start = tok_abs_end;
1185
1186        let w = width_of(line_start, tok_abs_end);
1187        if w <= max_width + 0.5 {
1188            best_break = tok_abs_end;
1189            continue;
1190        }
1191
1192        if best_break > line_start {
1193            out.push((line_start, best_break));
1194            line_start = best_break;
1195        } else {
1196            let mut cut = tok_abs_start;
1197            for (ofs, g) in tok.grapheme_indices(true) {
1198                let next = tok_abs_start + ofs + g.len();
1199                if width_of(line_start, next) <= max_width + 0.5 {
1200                    cut = next;
1201                } else {
1202                    break;
1203                }
1204            }
1205            if cut == line_start
1206                && let Some((ofs, gr)) = tok.grapheme_indices(true).next()
1207            {
1208                cut = tok_abs_start + ofs + gr.len();
1209            }
1210            out.push((line_start, cut));
1211            line_start = cut;
1212        }
1213
1214        if let Some(ml) = max_lines
1215            && out.len() >= ml
1216        {
1217            t = true;
1218            break;
1219        }
1220
1221        best_break = line_start;
1222    }
1223
1224    if !t && line_start < end && max_lines.is_none_or(|ml| out.len() < ml) {
1225        out.push((line_start, end));
1226    }
1227
1228    (out, t)
1229}
1230
1231pub fn ellipsize_line(
1232    text: &str,
1233    px: f32,
1234    max_width: f32,
1235    font_weight: u16,
1236    font_style: u8,
1237    letter_spacing: f32,
1238    font_variation_settings: Option<&str>,
1239) -> String {
1240    if text.is_empty() || max_width <= 0.0 {
1241        return String::new();
1242    }
1243    let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1244    let key = (
1245        fast_hash(text),
1246        (px * 100.0) as u32,
1247        (max_width * 100.0) as u32,
1248        font_weight,
1249        font_style,
1250        (letter_spacing * 100.0) as i32,
1251        fvs_hash,
1252    );
1253    if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
1254        return s;
1255    }
1256    let m = metrics_for_textfield(
1257        text,
1258        px,
1259        None,
1260        font_weight,
1261        font_style,
1262        letter_spacing,
1263        font_variation_settings,
1264    );
1265    if let Some(&last) = m.positions.last()
1266        && last <= max_width + 0.5
1267    {
1268        return text.to_string();
1269    }
1270    let _el = "…";
1271    let e_w = ellipsis_width(px, letter_spacing);
1272    if e_w >= max_width {
1273        return String::new();
1274    }
1275    let mut cut_i = 0usize;
1276    for i in 0..m.positions.len() {
1277        if m.positions[i] + e_w <= max_width {
1278            cut_i = i;
1279        } else {
1280            break;
1281        }
1282    }
1283    let byte = m
1284        .byte_offsets
1285        .get(cut_i)
1286        .copied()
1287        .unwrap_or(0)
1288        .min(text.len());
1289    let mut out = String::with_capacity(byte + 3);
1290    out.push_str(&text[..byte]);
1291    out.push('…');
1292
1293    let s = out;
1294    ellip_cache().lock().unwrap().put(key, s.clone());
1295
1296    s
1297}
1298
1299fn ellipsis_width(px: f32, letter_spacing: f32) -> f32 {
1300    static ELLIP_W_LRU: OnceCell<Mutex<Lru<(u32, i32), f32>>> = OnceCell::new();
1301    let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
1302    let key = ((px * 100.0) as u32, (letter_spacing * 100.0) as i32);
1303    if let Some(w) = cache.lock().unwrap().get(&key).copied() {
1304        return w;
1305    }
1306    let w = if let Some(g) =
1307        crate::shape_line("…", px, px, None, 400, 0, letter_spacing, None).last()
1308    {
1309        g.x + g.advance
1310    } else {
1311        0.0
1312    };
1313    cache.lock().unwrap().put(key, w);
1314    w
1315}