Skip to main content

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