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