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();
78
79static WRAP_RANGES_LRU: OnceCell<
80    Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<(usize, usize)>, bool)>>,
81> = OnceCell::new();
82
83static ELLIP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32), String>>> = OnceCell::new();
84
85fn wrap_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<String>, bool)>> {
86    WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
87}
88
89fn wrap_ranges_cache()
90-> &'static Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<(usize, usize)>, bool)>> {
91    WRAP_RANGES_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
92}
93
94fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32), String>> {
95    ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
96}
97
98fn fast_hash(s: &str) -> u64 {
99    use std::hash::{Hash, Hasher};
100    let mut h = AHasher::default();
101    s.hash(&mut h);
102    h.finish()
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub struct GlyphKey(pub u64);
107
108pub struct ShapedGlyph {
109    pub key: GlyphKey,
110    pub x: f32,
111    pub y: f32,
112    pub w: f32,
113    pub h: f32,
114    pub bearing_x: f32,
115    pub bearing_y: f32,
116    pub advance: f32,
117}
118
119pub struct GlyphBitmap {
120    pub key: GlyphKey,
121    pub w: u32,
122    pub h: u32,
123    pub content: SwashContent,
124    pub data: Vec<u8>, // Mask: A8; Color/Subpixel: RGBA8
125}
126
127struct Engine {
128    fs: FontSystem,
129    cache: SwashCache,
130    // Map our compact atlas key -> full cosmic_text CacheKey
131    key_map: HashMap<GlyphKey, CacheKey>,
132}
133
134impl Engine {
135    fn get_image(&mut self, key: CacheKey) -> Option<cosmic_text::SwashImage> {
136        // inside this method we may freely borrow both fields
137        self.cache.get_image(&mut self.fs, key).clone()
138    }
139}
140
141static ENGINE: OnceCell<Mutex<Engine>> = OnceCell::new();
142
143fn engine() -> &'static Mutex<Engine> {
144    ENGINE.get_or_init(|| {
145        #[allow(unused_mut)]
146        let mut fs = FontSystem::new();
147
148        let cache = SwashCache::new();
149
150        #[cfg(any(target_os = "android", target_arch = "wasm32"))]
151        // Until cosmic-text has android/web font loading support
152        {
153            static FALLBACK_TTF: &[u8] = include_bytes!("assets/OpenSans-Regular.ttf"); // GFonts, OFL licensed
154            static FALLBACK_EMOJI_TTF: &[u8] = include_bytes!("assets/NotoColorEmoji-Regular.ttf"); // GFonts, OFL licensed
155            static FALLBACK_SYMBOLS_TTF: &[u8] =
156                include_bytes!("assets/NotoSansSymbols2-Regular.ttf"); // GFonts, OFL licensed
157            {
158                // Register fallback font data into font DB
159                let db = fs.db_mut();
160                db.load_font_data(FALLBACK_TTF.to_vec());
161                db.set_sans_serif_family("Open Sans".to_string());
162
163                db.load_font_data(FALLBACK_SYMBOLS_TTF.to_vec());
164                db.load_font_data(FALLBACK_EMOJI_TTF.to_vec());
165            }
166        }
167        Mutex::new(Engine {
168            fs,
169            cache,
170            key_map: HashMap::new(),
171        })
172    })
173}
174
175// Utility: stable u64 key from a CacheKey using its Hash impl
176fn key_from_cachekey(k: &CacheKey) -> GlyphKey {
177    let mut h = AHasher::default();
178    k.hash(&mut h);
179    GlyphKey(h.finish())
180}
181
182// Shape a single-line string (no wrapping). Returns positioned glyphs relative to baseline y=0.
183pub fn shape_line(text: &str, px: f32) -> Vec<ShapedGlyph> {
184    let mut eng = engine().lock().unwrap();
185
186    // Construct a temporary buffer each call; FontSystem and caches are retained globally
187    let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
188    {
189        // Borrow with FS for ergonomic setters (no FS arg)
190        let mut b = buf.borrow_with(&mut eng.fs);
191        b.set_size(None, None);
192        b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
193        b.shape_until_scroll(true);
194    }
195
196    let mut out = Vec::new();
197    for run in buf.layout_runs() {
198        for g in run.glyphs {
199            // Compute physical glyph: gives cache_key and integer pixel position
200            let phys = g.physical((0.0, run.line_y), 1.0);
201            let key = key_from_cachekey(&phys.cache_key);
202            eng.key_map.insert(key, phys.cache_key);
203
204            // Query raster cache to get placement for metrics
205            let img_opt = eng.get_image(phys.cache_key);
206            let (w, h, left, top) = if let Some(img) = img_opt.as_ref() {
207                (
208                    img.placement.width as f32,
209                    img.placement.height as f32,
210                    img.placement.left as f32,
211                    img.placement.top as f32,
212                )
213            } else {
214                (0.0, 0.0, 0.0, 0.0)
215            };
216
217            out.push(ShapedGlyph {
218                key,
219                x: g.x + g.x_offset, // visual x
220                y: run.line_y,       // baseline y
221                w,
222                h,
223                bearing_x: left,
224                bearing_y: top,
225                advance: g.w,
226            });
227        }
228    }
229    out
230}
231
232// Rasterize a glyph mask (A8) or color/subpixel (RGBA8) for a given shaped key.
233// Returns owned pixels to avoid borrowing from the cache.
234pub fn rasterize(key: GlyphKey, _px: f32) -> Option<GlyphBitmap> {
235    let mut eng = engine().lock().unwrap();
236    let &ck = eng.key_map.get(&key)?;
237
238    let img = eng.get_image(ck).as_ref()?.clone();
239    Some(GlyphBitmap {
240        key,
241        w: img.placement.width,
242        h: img.placement.height,
243        content: img.content,
244        data: img.data, // already a Vec<u8>
245    })
246}
247
248// Text metrics for TextField: positions per grapheme boundary and byte offsets.
249#[derive(Clone)]
250pub struct TextMetrics {
251    pub positions: Vec<f32>,      // cumulative advance per boundary (len == n+1)
252    pub byte_offsets: Vec<usize>, // byte index per boundary (len == n+1)
253}
254
255/// Computes caret mapping using shaping (no wrapping).
256pub fn metrics_for_textfield(text: &str, px: f32) -> TextMetrics {
257    let key = (fast_hash(text), (px * 100.0) as u32);
258    if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
259        return m;
260    }
261    let mut eng = engine().lock().unwrap();
262    let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
263    {
264        let mut b = buf.borrow_with(&mut eng.fs);
265        b.set_size(None, None);
266        b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
267        b.shape_until_scroll(true);
268    }
269    let mut edges: Vec<(usize, f32)> = Vec::new();
270    let mut last_x = 0.0f32;
271    for run in buf.layout_runs() {
272        for g in run.glyphs {
273            let right = g.x + g.w;
274            last_x = right.max(last_x);
275            edges.push((g.end, right));
276        }
277    }
278    if edges.last().map(|e| e.0) != Some(text.len()) {
279        edges.push((text.len(), last_x));
280    }
281    let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
282    let mut byte_offsets = Vec::with_capacity(positions.capacity());
283    positions.push(0.0);
284    byte_offsets.push(0);
285    let mut last_byte = 0usize;
286    for (b, _) in text.grapheme_indices(true) {
287        positions
288            .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
289        byte_offsets.push(b);
290        last_byte = b;
291    }
292    if *byte_offsets.last().unwrap_or(&0) != text.len() {
293        positions.push(
294            positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
295        );
296        byte_offsets.push(text.len());
297    }
298    let m = TextMetrics {
299        positions,
300        byte_offsets,
301    };
302    metrics_cache().lock().unwrap().put(key, m.clone());
303    m
304}
305
306fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
307    let x0 = lookup_right(edges, start_b);
308    let x1 = lookup_right(edges, end_b);
309    (x1 - x0).max(0.0)
310}
311fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
312    match edges.binary_search_by_key(&b, |e| e.0) {
313        Ok(i) => edges[i].1,
314        Err(i) => {
315            if i == 0 {
316                0.0
317            } else {
318                edges[i - 1].1
319            }
320        }
321    }
322}
323
324/// Greedy wrap into lines that fit max_width. Prefers breaking at whitespace,
325/// falls back to grapheme boundaries. If max_lines is Some and we truncate,
326/// caller can choose to ellipsize the last visible line.
327pub fn wrap_lines(
328    text: &str,
329    px: f32,
330    max_width: f32,
331    max_lines: Option<usize>,
332    soft_wrap: bool,
333) -> (Vec<String>, bool) {
334    if text.is_empty() || max_width <= 0.0 {
335        return (vec![String::new()], false);
336    }
337    if !soft_wrap {
338        return (vec![text.to_string()], false);
339    }
340
341    let max_lines_key: u16 = match max_lines {
342        None => 0,
343        Some(n) => {
344            let n = n.min(u16::MAX as usize - 1) as u16;
345            n.saturating_add(1)
346        }
347    };
348    let key = (
349        fast_hash(text),
350        (px * 100.0) as u32,
351        (max_width * 100.0) as u32,
352        max_lines_key,
353        soft_wrap,
354    );
355    if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
356        return h;
357    }
358
359    // Shape once and reuse positions/byte mapping.
360    let m = metrics_for_textfield(text, px);
361    // Fast path: fits
362    if let Some(&last) = m.positions.last()
363        && last <= max_width + 0.5
364    {
365        return (vec![text.to_string()], false);
366    }
367
368    // Helper: width of substring [start..end] in bytes
369    let width_of = |start_b: usize, end_b: usize| -> f32 {
370        let i0 = match m.byte_offsets.binary_search(&start_b) {
371            Ok(i) | Err(i) => i,
372        };
373        let i1 = match m.byte_offsets.binary_search(&end_b) {
374            Ok(i) | Err(i) => i,
375        };
376        (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
377            .max(0.0)
378    };
379
380    let mut out: Vec<String> = Vec::new();
381    let mut truncated = false;
382
383    let mut line_start = 0usize; // byte index
384    let mut best_break = line_start;
385    let mut last_w = 0.0;
386
387    // Iterate word boundaries (keep whitespace tokens so they factor widths)
388    for tok in text.split_word_bounds() {
389        let tok_start = best_break;
390        let tok_end = tok_start + tok.len();
391        let w = width_of(line_start, tok_end);
392
393        if w <= max_width + 0.5 {
394            best_break = tok_end;
395            last_w = w;
396            continue;
397        }
398
399        // Need to break the line before tok_end.
400        if best_break > line_start {
401            // Break at last good boundary
402            out.push(text[line_start..best_break].trim_end().to_string());
403            line_start = best_break;
404        } else {
405            // Token itself too wide: force break inside token at grapheme boundaries
406            let mut cut = tok_start;
407            for g in tok.grapheme_indices(true) {
408                let next = tok_start + g.0 + g.1.len();
409                if width_of(line_start, next) <= max_width + 0.5 {
410                    cut = next;
411                } else {
412                    break;
413                }
414            }
415            if cut == line_start {
416                // nothing fits; fall back to single grapheme
417                if let Some((ofs, grapheme)) = tok.grapheme_indices(true).next() {
418                    cut = tok_start + ofs + grapheme.len();
419                }
420            }
421            out.push(text[line_start..cut].to_string());
422            line_start = cut;
423        }
424
425        // Check max_lines
426        if let Some(ml) = max_lines
427            && out.len() >= ml
428        {
429            truncated = true;
430            // Stop; caller may ellipsize the last line
431            line_start = line_start.min(text.len());
432            break;
433        }
434
435        // Reset best_break for new line
436        best_break = line_start;
437        last_w = 0.0;
438
439        // Re-consider current token if not fully consumed
440        if line_start < tok_end {
441            // recompute width with the remaining token portion
442            if width_of(line_start, tok_end) <= max_width + 0.5 {
443                best_break = tok_end;
444                last_w = width_of(line_start, best_break);
445            } else {
446                // will be handled in next iterations (or forced again)
447            }
448        }
449    }
450
451    // Push tail if allowed
452    if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
453        out.push(text[line_start..].trim_end().to_string());
454    }
455
456    let res = (out, truncated);
457
458    wrap_cache().lock().unwrap().put(key, res.clone());
459    res
460}
461
462/// Like `wrap_lines`, but returns byte ranges into the original `text`
463/// for each visual line. This is required for multi-line editing so
464/// caret/selection mapping stays correct.
465///
466/// Ranges are half-open `[start, end)`, and never include the '\n' char
467/// (hard line breaks end a range at the '\n' byte index).
468pub fn wrap_line_ranges(
469    text: &str,
470    px: f32,
471    max_width: f32,
472    max_lines: Option<usize>,
473    soft_wrap: bool,
474) -> (Vec<(usize, usize)>, bool) {
475    if text.is_empty() || max_width <= 0.0 {
476        return (vec![(0, 0)], false);
477    }
478    if !soft_wrap {
479        // Hard lines only (split on '\n' but no width wrapping)
480        let mut out = Vec::new();
481        let mut start = 0usize;
482        for (i, ch) in text.char_indices() {
483            if ch == '\n' {
484                out.push((start, i));
485                start = i + 1;
486            }
487        }
488        out.push((start, text.len()));
489        return (out, false);
490    }
491
492    let max_lines_key: u16 = match max_lines {
493        None => 0,
494        Some(n) => {
495            let n = n.min(u16::MAX as usize - 1) as u16;
496            n.saturating_add(1)
497        }
498    };
499    let key = (
500        fast_hash(text),
501        (px * 100.0) as u32,
502        (max_width * 100.0) as u32,
503        max_lines_key,
504        soft_wrap,
505    );
506    if let Some(v) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
507        return v;
508    }
509
510    // Shape once for width queries (whole string)
511    let m = metrics_for_textfield(text, px);
512
513    // Helper: width of substring [start..end] in bytes using m
514    let width_of = |start_b: usize, end_b: usize| -> f32 {
515        let i0 = match m.byte_offsets.binary_search(&start_b) {
516            Ok(i) | Err(i) => i,
517        };
518        let i1 = match m.byte_offsets.binary_search(&end_b) {
519            Ok(i) | Err(i) => i,
520        };
521        (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
522            .max(0.0)
523    };
524
525    let mut out: Vec<(usize, usize)> = Vec::new();
526    let mut truncated = false;
527
528    // Process hard lines split by '\n' while preserving original indices.
529    let mut line0_start = 0usize;
530    for (i, ch) in text.char_indices() {
531        if ch == '\n' {
532            let (mut ranges, tr) = wrap_one_hard_line_ranges(
533                text,
534                line0_start,
535                i,
536                max_width,
537                max_lines.map(|ml| ml.saturating_sub(out.len())),
538                &width_of,
539            );
540            out.append(&mut ranges);
541            if tr {
542                truncated = true;
543                break;
544            }
545            line0_start = i + 1;
546
547            if let Some(ml) = max_lines {
548                if out.len() >= ml {
549                    truncated = true;
550                    break;
551                }
552            }
553        }
554    }
555    if !truncated {
556        let (mut ranges, tr) = wrap_one_hard_line_ranges(
557            text,
558            line0_start,
559            text.len(),
560            max_width,
561            max_lines.map(|ml| ml.saturating_sub(out.len())),
562            &width_of,
563        );
564        out.append(&mut ranges);
565        truncated = tr;
566    }
567
568    if out.is_empty() {
569        out.push((0, 0));
570    }
571
572    let res = (out, truncated);
573    wrap_ranges_cache().lock().unwrap().put(key, res.clone());
574    res
575}
576
577fn wrap_one_hard_line_ranges(
578    text: &str,
579    start: usize,
580    end: usize,
581    max_width: f32,
582    max_lines: Option<usize>,
583    width_of: &dyn Fn(usize, usize) -> f32,
584) -> (Vec<(usize, usize)>, bool) {
585    let mut out = Vec::new();
586    let mut t = false;
587
588    if start >= end {
589        out.push((start, start));
590        return (out, false);
591    }
592
593    // Fast path: whole line fits
594    if width_of(start, end) <= max_width + 0.5 {
595        out.push((start, end));
596        return (out, false);
597    }
598
599    let mut line_start = start;
600    let mut best_break = line_start;
601    let mut unconsumed_start = start;
602
603    for tok in text[line_start..end].split_word_bounds() {
604        let tok_abs_start = unconsumed_start;
605        let tok_abs_end = tok_abs_start + tok.len();
606        unconsumed_start = tok_abs_end;
607
608        let w = width_of(line_start, tok_abs_end);
609        if w <= max_width + 0.5 {
610            best_break = tok_abs_end;
611            continue;
612        }
613
614        // Need break before tok_abs_end.
615        if best_break > line_start {
616            out.push((line_start, best_break));
617            line_start = best_break;
618        } else {
619            // Token too wide: force break at grapheme boundaries
620            let mut cut = tok_abs_start;
621            for (ofs, g) in tok.grapheme_indices(true) {
622                let next = tok_abs_start + ofs + g.len();
623                if width_of(line_start, next) <= max_width + 0.5 {
624                    cut = next;
625                } else {
626                    break;
627                }
628            }
629            if cut == line_start {
630                if let Some((ofs, gr)) = tok.grapheme_indices(true).next() {
631                    cut = tok_abs_start + ofs + gr.len();
632                }
633            }
634            out.push((line_start, cut));
635            line_start = cut;
636        }
637
638        // Max lines check
639        if let Some(ml) = max_lines {
640            if out.len() >= ml {
641                t = true;
642                break;
643            }
644        }
645
646        best_break = line_start;
647    }
648
649    // Tail
650    if !t && line_start < end && max_lines.is_none_or(|ml| out.len() < ml) {
651        out.push((line_start, end));
652    }
653
654    (out, t)
655}
656
657/// Return a string truncated to fit max_width at the given px size, appending '…' if truncated.
658pub fn ellipsize_line(text: &str, px: f32, max_width: f32) -> String {
659    if text.is_empty() || max_width <= 0.0 {
660        return String::new();
661    }
662    let key = (
663        fast_hash(text),
664        (px * 100.0) as u32,
665        (max_width * 100.0) as u32,
666    );
667    if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
668        return s;
669    }
670    let m = metrics_for_textfield(text, px);
671    if let Some(&last) = m.positions.last()
672        && last <= max_width + 0.5
673    {
674        return text.to_string();
675    }
676    let el = "…";
677    let e_w = ellipsis_width(px);
678    if e_w >= max_width {
679        return String::new();
680    }
681    // Find last grapheme index whose width + ellipsis fits
682    let mut cut_i = 0usize;
683    for i in 0..m.positions.len() {
684        if m.positions[i] + e_w <= max_width {
685            cut_i = i;
686        } else {
687            break;
688        }
689    }
690    let byte = m
691        .byte_offsets
692        .get(cut_i)
693        .copied()
694        .unwrap_or(0)
695        .min(text.len());
696    let mut out = String::with_capacity(byte + 3);
697    out.push_str(&text[..byte]);
698    out.push('…');
699
700    let s = out;
701    ellip_cache().lock().unwrap().put(key, s.clone());
702
703    s
704}
705
706fn ellipsis_width(px: f32) -> f32 {
707    static ELLIP_W_LRU: OnceCell<Mutex<Lru<u32, f32>>> = OnceCell::new();
708    let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
709    let key = (px * 100.0) as u32;
710    if let Some(w) = cache.lock().unwrap().get(&key).copied() {
711        return w;
712    }
713    let w = if let Some(g) = crate::shape_line("…", px).last() {
714        g.x + g.advance
715    } else {
716        0.0
717    };
718    cache.lock().unwrap().put(key, w);
719    w
720}