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