repose_text/
lib.rs

1use ahash::{AHashMap, AHasher};
2use cosmic_text::{
3    Attrs, Buffer, CacheKey, FontSystem, Metrics, Shaping, SwashCache, SwashContent,
4};
5use once_cell::sync::OnceCell;
6use std::{
7    collections::{HashMap, VecDeque},
8    hash::{Hash, Hasher},
9    sync::Mutex,
10};
11use unicode_segmentation::UnicodeSegmentation;
12
13const WRAP_CACHE_CAP: usize = 1024;
14const ELLIP_CACHE_CAP: usize = 2048;
15
16static METRICS_LRU: OnceCell<Mutex<Lru<(u64, u32), TextMetrics>>> = OnceCell::new();
17fn metrics_cache() -> &'static Mutex<Lru<(u64, u32), TextMetrics>> {
18    METRICS_LRU.get_or_init(|| Mutex::new(Lru::new(4096)))
19}
20
21struct Lru<K, V> {
22    map: AHashMap<K, V>,
23    order: VecDeque<K>,
24    cap: usize,
25}
26impl<K: std::hash::Hash + Eq + Clone, V> Lru<K, V> {
27    fn new(cap: usize) -> Self {
28        Self {
29            map: AHashMap::new(),
30            order: VecDeque::new(),
31            cap,
32        }
33    }
34    fn get(&mut self, k: &K) -> Option<&V> {
35        if self.map.contains_key(k) {
36            // move to back
37            if let Some(pos) = self.order.iter().position(|x| x == k) {
38                let key = self.order.remove(pos).unwrap();
39                self.order.push_back(key);
40            }
41        }
42        self.map.get(k)
43    }
44    fn put(&mut self, k: K, v: V) {
45        if self.map.contains_key(&k) {
46            self.map.insert(k.clone(), v);
47            if let Some(pos) = self.order.iter().position(|x| x == &k) {
48                let key = self.order.remove(pos).unwrap();
49                self.order.push_back(key);
50            }
51            return;
52        }
53        if self.map.len() >= self.cap
54            && let Some(old) = self.order.pop_front()
55        {
56            self.map.remove(&old);
57        }
58        self.order.push_back(k.clone());
59        self.map.insert(k, v);
60    }
61}
62
63static WRAP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<String>, bool)>>> =
64    OnceCell::new();
65static ELLIP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32), String>>> = OnceCell::new();
66
67fn wrap_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<String>, bool)>> {
68    WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
69}
70fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32), String>> {
71    ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
72}
73
74fn fast_hash(s: &str) -> u64 {
75    use std::hash::{Hash, Hasher};
76    let mut h = AHasher::default();
77    s.hash(&mut h);
78    h.finish()
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub struct GlyphKey(pub u64);
83
84pub struct ShapedGlyph {
85    pub key: GlyphKey,
86    pub x: f32,
87    pub y: f32,
88    pub w: f32,
89    pub h: f32,
90    pub bearing_x: f32,
91    pub bearing_y: f32,
92    pub advance: f32,
93}
94
95pub struct GlyphBitmap {
96    pub key: GlyphKey,
97    pub w: u32,
98    pub h: u32,
99    pub content: SwashContent,
100    pub data: Vec<u8>, // Mask: A8; Color/Subpixel: RGBA8
101}
102
103struct Engine {
104    fs: FontSystem,
105    cache: SwashCache,
106    // Map our compact atlas key -> full cosmic_text CacheKey
107    key_map: HashMap<GlyphKey, CacheKey>,
108}
109
110impl Engine {
111    fn get_image(&mut self, key: CacheKey) -> Option<cosmic_text::SwashImage> {
112        // inside this method we may freely borrow both fields
113        self.cache.get_image(&mut self.fs, key).clone()
114    }
115}
116
117static ENGINE: OnceCell<Mutex<Engine>> = OnceCell::new();
118
119fn engine() -> &'static Mutex<Engine> {
120    ENGINE.get_or_init(|| {
121        #[allow(unused_mut)]
122        let mut fs = FontSystem::new();
123
124        let cache = SwashCache::new();
125
126        #[cfg(any(target_os = "android", target_arch = "wasm32"))]
127        // Until cosmic-text has android/web font loading support
128        {
129            static FALLBACK_TTF: &[u8] = include_bytes!("assets/OpenSans-Regular.ttf"); // GFonts, OFL licensed
130            static FALLBACK_EMOJI_TTF: &[u8] = include_bytes!("assets/NotoColorEmoji-Regular.ttf"); // GFonts, OFL licensed
131            static FALLBACK_SYMBOLS_TTF: &[u8] =
132                include_bytes!("assets/NotoSansSymbols2-Regular.ttf"); // GFonts, OFL licensed
133            {
134                // Register fallback font data into font DB
135                let db = fs.db_mut();
136                db.load_font_data(FALLBACK_TTF.to_vec());
137                db.set_sans_serif_family("Open Sans".to_string());
138
139                db.load_font_data(FALLBACK_SYMBOLS_TTF.to_vec());
140                db.load_font_data(FALLBACK_EMOJI_TTF.to_vec());
141            }
142        }
143        Mutex::new(Engine {
144            fs,
145            cache,
146            key_map: HashMap::new(),
147        })
148    })
149}
150
151// Utility: stable u64 key from a CacheKey using its Hash impl
152fn key_from_cachekey(k: &CacheKey) -> GlyphKey {
153    let mut h = AHasher::default();
154    k.hash(&mut h);
155    GlyphKey(h.finish())
156}
157
158// Shape a single-line string (no wrapping). Returns positioned glyphs relative to baseline y=0.
159pub fn shape_line(text: &str, px: f32) -> Vec<ShapedGlyph> {
160    let mut eng = engine().lock().unwrap();
161
162    // Construct a temporary buffer each call; FontSystem and caches are retained globally
163    let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
164    {
165        // Borrow with FS for ergonomic setters (no FS arg)
166        let mut b = buf.borrow_with(&mut eng.fs);
167        b.set_size(None, None);
168        b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
169        b.shape_until_scroll(true);
170    }
171
172    let mut out = Vec::new();
173    for run in buf.layout_runs() {
174        for g in run.glyphs {
175            // Compute physical glyph: gives cache_key and integer pixel position
176            let phys = g.physical((0.0, run.line_y), 1.0);
177            let key = key_from_cachekey(&phys.cache_key);
178            eng.key_map.insert(key, phys.cache_key);
179
180            // Query raster cache to get placement for metrics
181            let img_opt = eng.get_image(phys.cache_key);
182            let (w, h, left, top) = if let Some(img) = img_opt.as_ref() {
183                (
184                    img.placement.width as f32,
185                    img.placement.height as f32,
186                    img.placement.left as f32,
187                    img.placement.top as f32,
188                )
189            } else {
190                (0.0, 0.0, 0.0, 0.0)
191            };
192
193            out.push(ShapedGlyph {
194                key,
195                x: g.x + g.x_offset, // visual x
196                y: run.line_y,       // baseline y
197                w,
198                h,
199                bearing_x: left,
200                bearing_y: top,
201                advance: g.w,
202            });
203        }
204    }
205    out
206}
207
208// Rasterize a glyph mask (A8) or color/subpixel (RGBA8) for a given shaped key.
209// Returns owned pixels to avoid borrowing from the cache.
210pub fn rasterize(key: GlyphKey, _px: f32) -> Option<GlyphBitmap> {
211    let mut eng = engine().lock().unwrap();
212    let &ck = eng.key_map.get(&key)?;
213
214    let img = eng.get_image(ck).as_ref()?.clone();
215    Some(GlyphBitmap {
216        key,
217        w: img.placement.width,
218        h: img.placement.height,
219        content: img.content,
220        data: img.data, // already a Vec<u8>
221    })
222}
223
224// Text metrics for TextField: positions per grapheme boundary and byte offsets.
225#[derive(Clone)]
226pub struct TextMetrics {
227    pub positions: Vec<f32>,      // cumulative advance per boundary (len == n+1)
228    pub byte_offsets: Vec<usize>, // byte index per boundary (len == n+1)
229}
230
231/// Computes caret mapping using shaping (no wrapping).
232pub fn metrics_for_textfield(text: &str, px: f32) -> TextMetrics {
233    let key = (fast_hash(text), (px * 100.0) as u32);
234    if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
235        return m;
236    }
237    let mut eng = engine().lock().unwrap();
238    let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
239    {
240        let mut b = buf.borrow_with(&mut eng.fs);
241        b.set_size(None, None);
242        b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
243        b.shape_until_scroll(true);
244    }
245    let mut edges: Vec<(usize, f32)> = Vec::new();
246    let mut last_x = 0.0f32;
247    for run in buf.layout_runs() {
248        for g in run.glyphs {
249            let right = g.x + g.w;
250            last_x = right.max(last_x);
251            edges.push((g.end, right));
252        }
253    }
254    if edges.last().map(|e| e.0) != Some(text.len()) {
255        edges.push((text.len(), last_x));
256    }
257    let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
258    let mut byte_offsets = Vec::with_capacity(positions.capacity());
259    positions.push(0.0);
260    byte_offsets.push(0);
261    let mut last_byte = 0usize;
262    for (b, _) in text.grapheme_indices(true) {
263        positions
264            .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
265        byte_offsets.push(b);
266        last_byte = b;
267    }
268    if *byte_offsets.last().unwrap_or(&0) != text.len() {
269        positions.push(
270            positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
271        );
272        byte_offsets.push(text.len());
273    }
274    let m = TextMetrics {
275        positions,
276        byte_offsets,
277    };
278    metrics_cache().lock().unwrap().put(key, m.clone());
279    m
280}
281
282fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
283    let x0 = lookup_right(edges, start_b);
284    let x1 = lookup_right(edges, end_b);
285    (x1 - x0).max(0.0)
286}
287fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
288    match edges.binary_search_by_key(&b, |e| e.0) {
289        Ok(i) => edges[i].1,
290        Err(i) => {
291            if i == 0 {
292                0.0
293            } else {
294                edges[i - 1].1
295            }
296        }
297    }
298}
299
300/// Greedy wrap into lines that fit max_width. Prefers breaking at whitespace,
301/// falls back to grapheme boundaries. If max_lines is Some and we truncate,
302/// caller can choose to ellipsize the last visible line.
303pub fn wrap_lines(
304    text: &str,
305    px: f32,
306    max_width: f32,
307    max_lines: Option<usize>,
308    soft_wrap: bool,
309) -> (Vec<String>, bool) {
310    if text.is_empty() || max_width <= 0.0 {
311        return (vec![String::new()], false);
312    }
313    if !soft_wrap {
314        return (vec![text.to_string()], false);
315    }
316
317    let max_lines_key: u16 = match max_lines {
318        None => 0,
319        Some(n) => {
320            let n = n.min(u16::MAX as usize - 1) as u16;
321            n.saturating_add(1)
322        }
323    };
324    let key = (
325        fast_hash(text),
326        (px * 100.0) as u32,
327        (max_width * 100.0) as u32,
328        max_lines_key,
329        soft_wrap,
330    );
331    if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
332        return h;
333    }
334
335    // Shape once and reuse positions/byte mapping.
336    let m = metrics_for_textfield(text, px);
337    // Fast path: fits
338    if let Some(&last) = m.positions.last()
339        && last <= max_width + 0.5
340    {
341        return (vec![text.to_string()], false);
342    }
343
344    // Helper: width of substring [start..end] in bytes
345    let width_of = |start_b: usize, end_b: usize| -> f32 {
346        let i0 = match m.byte_offsets.binary_search(&start_b) {
347            Ok(i) | Err(i) => i,
348        };
349        let i1 = match m.byte_offsets.binary_search(&end_b) {
350            Ok(i) | Err(i) => i,
351        };
352        (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
353            .max(0.0)
354    };
355
356    let mut out: Vec<String> = Vec::new();
357    let mut truncated = false;
358
359    let mut line_start = 0usize; // byte index
360    let mut best_break = line_start;
361    let mut last_w = 0.0;
362
363    // Iterate word boundaries (keep whitespace tokens so they factor widths)
364    for tok in text.split_word_bounds() {
365        let tok_start = best_break;
366        let tok_end = tok_start + tok.len();
367        let w = width_of(line_start, tok_end);
368
369        if w <= max_width + 0.5 {
370            best_break = tok_end;
371            last_w = w;
372            continue;
373        }
374
375        // Need to break the line before tok_end.
376        if best_break > line_start {
377            // Break at last good boundary
378            out.push(text[line_start..best_break].trim_end().to_string());
379            line_start = best_break;
380        } else {
381            // Token itself too wide: force break inside token at grapheme boundaries
382            let mut cut = tok_start;
383            for g in tok.grapheme_indices(true) {
384                let next = tok_start + g.0 + g.1.len();
385                if width_of(line_start, next) <= max_width + 0.5 {
386                    cut = next;
387                } else {
388                    break;
389                }
390            }
391            if cut == line_start {
392                // nothing fits; fall back to single grapheme
393                if let Some((ofs, grapheme)) = tok.grapheme_indices(true).next() {
394                    cut = tok_start + ofs + grapheme.len();
395                }
396            }
397            out.push(text[line_start..cut].to_string());
398            line_start = cut;
399        }
400
401        // Check max_lines
402        if let Some(ml) = max_lines
403            && out.len() >= ml
404        {
405            truncated = true;
406            // Stop; caller may ellipsize the last line
407            line_start = line_start.min(text.len());
408            break;
409        }
410
411        // Reset best_break for new line
412        best_break = line_start;
413        last_w = 0.0;
414
415        // Re-consider current token if not fully consumed
416        if line_start < tok_end {
417            // recompute width with the remaining token portion
418            if width_of(line_start, tok_end) <= max_width + 0.5 {
419                best_break = tok_end;
420                last_w = width_of(line_start, best_break);
421            } else {
422                // will be handled in next iterations (or forced again)
423            }
424        }
425    }
426
427    // Push tail if allowed
428    if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
429        out.push(text[line_start..].trim_end().to_string());
430    }
431
432    let res = (out, truncated);
433
434    wrap_cache().lock().unwrap().put(key, res.clone());
435    res
436}
437
438/// Return a string truncated to fit max_width at the given px size, appending '…' if truncated.
439pub fn ellipsize_line(text: &str, px: f32, max_width: f32) -> String {
440    if text.is_empty() || max_width <= 0.0 {
441        return String::new();
442    }
443    let key = (
444        fast_hash(text),
445        (px * 100.0) as u32,
446        (max_width * 100.0) as u32,
447    );
448    if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
449        return s;
450    }
451    let m = metrics_for_textfield(text, px);
452    if let Some(&last) = m.positions.last()
453        && last <= max_width + 0.5
454    {
455        return text.to_string();
456    }
457    let el = "…";
458    let e_w = ellipsis_width(px);
459    if e_w >= max_width {
460        return String::new();
461    }
462    // Find last grapheme index whose width + ellipsis fits
463    let mut cut_i = 0usize;
464    for i in 0..m.positions.len() {
465        if m.positions[i] + e_w <= max_width {
466            cut_i = i;
467        } else {
468            break;
469        }
470    }
471    let byte = m
472        .byte_offsets
473        .get(cut_i)
474        .copied()
475        .unwrap_or(0)
476        .min(text.len());
477    let mut out = String::with_capacity(byte + 3);
478    out.push_str(&text[..byte]);
479    out.push('…');
480
481    let s = out;
482    ellip_cache().lock().unwrap().put(key, s.clone());
483
484    s
485}
486
487fn ellipsis_width(px: f32) -> f32 {
488    static ELLIP_W_LRU: OnceCell<Mutex<Lru<u32, f32>>> = OnceCell::new();
489    let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
490    let key = (px * 100.0) as u32;
491    if let Some(w) = cache.lock().unwrap().get(&key).copied() {
492        return w;
493    }
494    let w = if let Some(g) = crate::shape_line("…", px).last() {
495        g.x + g.advance
496    } else {
497        0.0
498    };
499    cache.lock().unwrap().put(key, w);
500    w
501}