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