1use cosmic_text::{
2 Attrs, Buffer, CacheKey, Family, FontSystem, Metrics, Shaping, Style as CosmicStyle,
3 SwashCache, SwashContent, Weight as CosmicWeight,
4};
5use once_cell::sync::OnceCell;
6use rapidhash::{HashMapExt, RapidHashMap, fast::RapidHasher};
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);
17
18pub fn begin_frame() {
20 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
21}
22
23pub fn current_frame() -> u64 {
24 FRAME_COUNTER.load(Ordering::Relaxed)
25}
26
27const WRAP_CACHE_CAP: usize = 1024;
28const ELLIP_CACHE_CAP: usize = 2048;
29
30static METRICS_LRU: OnceCell<Mutex<Lru<(u64, u32, u64, u16, u8), TextMetrics>>> = OnceCell::new();
31fn metrics_cache() -> &'static Mutex<Lru<(u64, u32, u64, u16, u8), 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) {
52 let key = self.order.remove(pos).unwrap();
53 self.order.push_back(key);
54 }
55 }
56 self.map.get(k)
57 }
58 fn put(&mut self, k: K, v: V) {
59 if self.map.contains_key(&k) {
60 self.map.insert(k.clone(), v);
61 if let Some(pos) = self.order.iter().position(|x| x == &k) {
62 let key = self.order.remove(pos).unwrap();
63 self.order.push_back(key);
64 }
65 return;
66 }
67 if self.map.len() >= self.cap
68 && let Some(old) = self.order.pop_front()
69 {
70 self.map.remove(&old);
71 }
72 self.order.push_back(k.clone());
73 self.map.insert(k, v);
74 }
75}
76
77static WRAP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8), (Vec<String>, bool)>>> =
78 OnceCell::new();
79
80static WRAP_RANGES_LRU: OnceCell<
81 Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8), (Vec<(usize, usize)>, bool)>>,
82> = OnceCell::new();
83
84static ELLIP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32, u16, u8), String>>> = OnceCell::new();
85
86fn wrap_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8), (Vec<String>, bool)>> {
87 WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
88}
89
90fn wrap_ranges_cache()
91-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8), (Vec<(usize, usize)>, bool)>> {
92 WRAP_RANGES_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
93}
94
95fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, u8), String>> {
96 ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
97}
98
99fn fast_hash(s: &str) -> u64 {
100 use std::hash::{Hash, Hasher};
101 let mut h = RapidHasher::default();
102 s.len().hash(&mut h);
103 s.hash(&mut h);
104 h.finish()
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub struct GlyphKey(pub u64);
109
110pub struct ShapedGlyph {
111 pub key: GlyphKey,
112 pub x: f32,
113 pub y: f32,
114 pub w: f32,
115 pub h: f32,
116 pub bearing_x: f32,
117 pub bearing_y: f32,
118 pub advance: f32,
119}
120
121pub struct GlyphBitmap {
122 pub key: GlyphKey,
123 pub w: u32,
124 pub h: u32,
125 pub content: SwashContent,
126 pub data: Vec<u8>, }
128
129struct Engine {
130 fs: FontSystem,
131 cache: SwashCache,
132 key_map: HashMap<GlyphKey, CacheKey>,
134}
135
136impl Engine {
137 fn get_image(&mut self, key: CacheKey) -> Option<cosmic_text::SwashImage> {
138 self.cache.get_image(&mut self.fs, key).clone()
140 }
141}
142
143static ENGINE: OnceCell<Mutex<Engine>> = OnceCell::new();
144
145fn engine() -> &'static Mutex<Engine> {
146 ENGINE.get_or_init(|| {
147 #[allow(unused_mut)]
148 let mut fs = FontSystem::new();
149
150 let cache = SwashCache::new();
151
152 {
155 static FALLBACK_TTF: &[u8] = include_bytes!("assets/OpenSans-Regular.ttf"); static FALLBACK_EMOJI_TTF: &[u8] = include_bytes!("assets/NotoColorEmoji-Regular.ttf"); static FALLBACK_SYMBOLS_TTF: &[u8] =
158 include_bytes!("assets/NotoSansSymbols2-Regular.ttf"); static MATERIAL_SYMBOLS_TTF: &[u8] =
160 include_bytes!("assets/MaterialSymbolsOutlined.ttf"); {
162 let db = fs.db_mut();
164 db.load_font_data(FALLBACK_TTF.to_vec());
165 db.set_sans_serif_family("Open Sans".to_string());
166
167 db.load_font_data(FALLBACK_SYMBOLS_TTF.to_vec());
168 db.load_font_data(FALLBACK_EMOJI_TTF.to_vec());
169 db.load_font_data(MATERIAL_SYMBOLS_TTF.to_vec());
170 }
171 }
172 Mutex::new(Engine {
173 fs,
174 cache,
175 key_map: HashMap::new(),
176 })
177 })
178}
179
180pub fn register_font_data(bytes: &'static [u8]) {
182 let mut eng = engine().lock().unwrap();
183 eng.fs.db_mut().load_font_data(bytes.to_vec());
184}
185
186fn key_from_cachekey(k: &CacheKey) -> GlyphKey {
188 let mut h = RapidHasher::default();
189 k.hash(&mut h);
190 GlyphKey(h.finish())
191}
192
193pub fn shape_line(
196 text: &str,
197 px: f32,
198 font_family: Option<&str>,
199 font_weight: u16,
200 font_style: u8,
201) -> Vec<ShapedGlyph> {
202 let mut eng = engine().lock().unwrap();
203
204 let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
206 {
207 let mut b = buf.borrow_with(&mut eng.fs);
209 b.set_size(None, None);
210 let attrs = match font_family {
211 Some(family) => Attrs::new()
212 .family(Family::Name(family))
213 .weight(CosmicWeight(font_weight))
214 .style(if font_style == 1 {
215 CosmicStyle::Italic
216 } else {
217 CosmicStyle::Normal
218 }),
219 None => Attrs::new()
220 .weight(CosmicWeight(font_weight))
221 .style(if font_style == 1 {
222 CosmicStyle::Italic
223 } else {
224 CosmicStyle::Normal
225 }),
226 };
227 b.set_text(text, &attrs, Shaping::Advanced, None);
228 b.shape_until_scroll(true);
229 }
230
231 let mut out = Vec::new();
232 for run in buf.layout_runs() {
233 for g in run.glyphs {
234 let phys = g.physical((0.0, run.line_y), 1.0);
236 let key = key_from_cachekey(&phys.cache_key);
237 eng.key_map.insert(key, phys.cache_key);
238
239 let img_opt = eng.get_image(phys.cache_key);
241 let (w, h, left, top) = if let Some(img) = img_opt.as_ref() {
242 (
243 img.placement.width as f32,
244 img.placement.height as f32,
245 img.placement.left as f32,
246 img.placement.top as f32,
247 )
248 } else {
249 (0.0, 0.0, 0.0, 0.0)
250 };
251
252 out.push(ShapedGlyph {
253 key,
254 x: g.x + g.x_offset, y: run.line_y, w,
257 h,
258 bearing_x: left,
259 bearing_y: top,
260 advance: g.w,
261 });
262 }
263 }
264 out
265}
266
267pub fn rasterize(key: GlyphKey, _px: f32) -> Option<GlyphBitmap> {
270 let mut eng = engine().lock().unwrap();
271 let &ck = eng.key_map.get(&key)?;
272
273 let img = eng.get_image(ck).as_ref()?.clone();
274 Some(GlyphBitmap {
275 key,
276 w: img.placement.width,
277 h: img.placement.height,
278 content: img.content,
279 data: img.data, })
281}
282
283pub fn lookup_cache_key(key: GlyphKey) -> Option<cosmic_text::CacheKey> {
285 let eng = engine().lock().unwrap();
286 eng.key_map.get(&key).copied()
287}
288
289pub fn extract_outline_commands(
292 cache_key: cosmic_text::CacheKey,
293) -> Option<Box<[cosmic_text::Command]>> {
294 let mut eng = engine().lock().unwrap();
295 let Engine {
296 ref mut cache,
297 ref mut fs,
298 ..
299 } = *eng;
300 cache.get_outline_commands_uncached(fs, cache_key)
301}
302
303pub fn lookup_and_extract_outline(
306 key: GlyphKey,
307) -> Option<(cosmic_text::CacheKey, Box<[cosmic_text::Command]>)> {
308 let mut eng = engine().lock().unwrap();
309 let ck = eng.key_map.get(&key).copied()?;
310 let Engine {
311 ref mut cache,
312 ref mut fs,
313 ..
314 } = *eng;
315 let cmds = cache.get_outline_commands_uncached(fs, ck)?;
316 Some((ck, cmds))
317}
318
319#[derive(Clone)]
321pub struct TextMetrics {
322 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>, }
325
326pub fn metrics_for_textfield(
329 text: &str,
330 px: f32,
331 font_family: Option<&str>,
332 font_weight: u16,
333 font_style: u8,
334) -> TextMetrics {
335 let family_hash = font_family.map(fast_hash).unwrap_or(0);
336 let key = (
337 fast_hash(text),
338 (px * 100.0) as u32,
339 family_hash,
340 font_weight,
341 font_style,
342 );
343 if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
344 return m;
345 }
346 let mut eng = engine().lock().unwrap();
347 let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
348 {
349 let mut b = buf.borrow_with(&mut eng.fs);
350 b.set_size(None, None);
351 let attrs = match font_family {
352 Some(family) => Attrs::new()
353 .family(Family::Name(family))
354 .weight(CosmicWeight(font_weight))
355 .style(if font_style == 1 {
356 CosmicStyle::Italic
357 } else {
358 CosmicStyle::Normal
359 }),
360 None => Attrs::new()
361 .weight(CosmicWeight(font_weight))
362 .style(if font_style == 1 {
363 CosmicStyle::Italic
364 } else {
365 CosmicStyle::Normal
366 }),
367 };
368 b.set_text(text, &attrs, Shaping::Advanced, None);
369 b.shape_until_scroll(true);
370 }
371 let mut edges: Vec<(usize, f32)> = Vec::new();
372 let mut last_x = 0.0f32;
373 for run in buf.layout_runs() {
374 for g in run.glyphs {
375 let right = g.x + g.w;
376 last_x = right.max(last_x);
377 edges.push((g.end, right));
378 }
379 }
380 if edges.last().map(|e| e.0) != Some(text.len()) {
381 edges.push((text.len(), last_x));
382 }
383 let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
384 let mut byte_offsets = Vec::with_capacity(positions.capacity());
385 positions.push(0.0);
386 byte_offsets.push(0);
387 let mut last_byte = 0usize;
388 for (b, _) in text.grapheme_indices(true) {
389 positions
390 .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
391 byte_offsets.push(b);
392 last_byte = b;
393 }
394 if *byte_offsets.last().unwrap_or(&0) != text.len() {
395 positions.push(
396 positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
397 );
398 byte_offsets.push(text.len());
399 }
400 let m = TextMetrics {
401 positions,
402 byte_offsets,
403 };
404 metrics_cache().lock().unwrap().put(key, m.clone());
405 m
406}
407
408fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
409 let x0 = lookup_right(edges, start_b);
410 let x1 = lookup_right(edges, end_b);
411 (x1 - x0).max(0.0)
412}
413fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
414 match edges.binary_search_by_key(&b, |e| e.0) {
415 Ok(i) => edges[i].1,
416 Err(i) => {
417 if i == 0 {
418 0.0
419 } else {
420 edges[i - 1].1
421 }
422 }
423 }
424}
425
426pub fn wrap_lines(
430 text: &str,
431 px: f32,
432 max_width: f32,
433 max_lines: Option<usize>,
434 soft_wrap: bool,
435 font_weight: u16,
436 font_style: u8,
437) -> (Vec<String>, bool) {
438 if text.is_empty() || max_width <= 0.0 {
439 return (vec![String::new()], false);
440 }
441 if !soft_wrap {
442 return (vec![text.to_string()], false);
443 }
444
445 let max_lines_key: u16 = match max_lines {
446 None => 0,
447 Some(n) => {
448 let n = n.min(u16::MAX as usize - 1) as u16;
449 n.saturating_add(1)
450 }
451 };
452 let key = (
453 fast_hash(text),
454 (px * 100.0) as u32,
455 (max_width * 100.0) as u32,
456 max_lines_key,
457 soft_wrap,
458 font_weight,
459 font_style,
460 );
461 if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
462 return h;
463 }
464
465 let m = metrics_for_textfield(text, px, None, font_weight, font_style);
467 if let Some(&last) = m.positions.last()
469 && last <= max_width + 0.5
470 {
471 return (vec![text.to_string()], false);
472 }
473
474 let width_of = |start_b: usize, end_b: usize| -> f32 {
476 let i0 = match m.byte_offsets.binary_search(&start_b) {
477 Ok(i) | Err(i) => i,
478 };
479 let i1 = match m.byte_offsets.binary_search(&end_b) {
480 Ok(i) | Err(i) => i,
481 };
482 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
483 .max(0.0)
484 };
485
486 let mut out: Vec<String> = Vec::new();
487 let mut truncated = false;
488
489 let mut line_start = 0usize; let mut best_break = line_start;
491
492 for tok in text.split_word_bounds() {
494 let tok_start = best_break;
495 let tok_end = tok_start + tok.len();
496 let w = width_of(line_start, tok_end);
497
498 if w <= max_width + 0.5 {
499 best_break = tok_end;
500 continue;
501 }
502
503 if best_break > line_start {
505 out.push(text[line_start..best_break].trim_end().to_string());
507 line_start = best_break;
508 } else {
509 let mut cut = tok_start;
511 for g in tok.grapheme_indices(true) {
512 let next = tok_start + g.0 + g.1.len();
513 if width_of(line_start, next) <= max_width + 0.5 {
514 cut = next;
515 } else {
516 break;
517 }
518 }
519 if cut == line_start {
520 if let Some((ofs, grapheme)) = tok.grapheme_indices(true).next() {
522 cut = tok_start + ofs + grapheme.len();
523 }
524 }
525 out.push(text[line_start..cut].to_string());
526 line_start = cut;
527 }
528
529 if let Some(ml) = max_lines
531 && out.len() >= ml
532 {
533 truncated = true;
534 line_start = line_start.min(text.len());
536 break;
537 }
538
539 best_break = line_start;
541
542 if line_start < tok_end {
544 if width_of(line_start, tok_end) <= max_width + 0.5 {
546 best_break = tok_end;
547 } else {
548 }
550 }
551 }
552
553 if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
555 out.push(text[line_start..].trim_end().to_string());
556 }
557
558 let res = (out, truncated);
559
560 wrap_cache().lock().unwrap().put(key, res.clone());
561 res
562}
563
564pub fn wrap_line_ranges(
571 text: &str,
572 px: f32,
573 max_width: f32,
574 max_lines: Option<usize>,
575 soft_wrap: bool,
576 font_weight: u16,
577 font_style: u8,
578) -> (Vec<(usize, usize)>, bool) {
579 if text.is_empty() || max_width <= 0.0 {
580 return (vec![(0, 0)], false);
581 }
582 if !soft_wrap {
583 let mut out = Vec::new();
585 let mut start = 0usize;
586 for (i, ch) in text.char_indices() {
587 if ch == '\n' {
588 out.push((start, i));
589 start = i + 1;
590 }
591 }
592 out.push((start, text.len()));
593 return (out, false);
594 }
595
596 let max_lines_key: u16 = match max_lines {
597 None => 0,
598 Some(n) => {
599 let n = n.min(u16::MAX as usize - 1) as u16;
600 n.saturating_add(1)
601 }
602 };
603 let key = (
604 fast_hash(text),
605 (px * 100.0) as u32,
606 (max_width * 100.0) as u32,
607 max_lines_key,
608 soft_wrap,
609 font_weight,
610 font_style,
611 );
612 if let Some(v) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
613 return v;
614 }
615
616 let m = metrics_for_textfield(text, px, None, font_weight, font_style);
618
619 let width_of = |start_b: usize, end_b: usize| -> f32 {
621 let i0 = match m.byte_offsets.binary_search(&start_b) {
622 Ok(i) | Err(i) => i,
623 };
624 let i1 = match m.byte_offsets.binary_search(&end_b) {
625 Ok(i) | Err(i) => i,
626 };
627 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
628 .max(0.0)
629 };
630
631 let mut out: Vec<(usize, usize)> = Vec::new();
632 let mut truncated = false;
633
634 let mut line0_start = 0usize;
636 for (i, ch) in text.char_indices() {
637 if ch == '\n' {
638 let (mut ranges, tr) = wrap_one_hard_line_ranges(
639 text,
640 line0_start,
641 i,
642 max_width,
643 max_lines.map(|ml| ml.saturating_sub(out.len())),
644 &width_of,
645 );
646 out.append(&mut ranges);
647 if tr {
648 truncated = true;
649 break;
650 }
651 line0_start = i + 1;
652
653 if let Some(ml) = max_lines
654 && out.len() >= ml
655 {
656 truncated = true;
657 break;
658 }
659 }
660 }
661 if !truncated {
662 let (mut ranges, tr) = wrap_one_hard_line_ranges(
663 text,
664 line0_start,
665 text.len(),
666 max_width,
667 max_lines.map(|ml| ml.saturating_sub(out.len())),
668 &width_of,
669 );
670 out.append(&mut ranges);
671 truncated = tr;
672 }
673
674 if out.is_empty() {
675 out.push((0, 0));
676 }
677
678 let res = (out, truncated);
679 wrap_ranges_cache().lock().unwrap().put(key, res.clone());
680 res
681}
682
683fn wrap_one_hard_line_ranges(
684 text: &str,
685 start: usize,
686 end: usize,
687 max_width: f32,
688 max_lines: Option<usize>,
689 width_of: &dyn Fn(usize, usize) -> f32,
690) -> (Vec<(usize, usize)>, bool) {
691 let mut out = Vec::new();
692 let mut t = false;
693
694 if start >= end {
695 out.push((start, start));
696 return (out, false);
697 }
698
699 if width_of(start, end) <= max_width + 0.5 {
701 out.push((start, end));
702 return (out, false);
703 }
704
705 let mut line_start = start;
706 let mut best_break = line_start;
707 let mut unconsumed_start = start;
708
709 for tok in text[line_start..end].split_word_bounds() {
710 let tok_abs_start = unconsumed_start;
711 let tok_abs_end = tok_abs_start + tok.len();
712 unconsumed_start = tok_abs_end;
713
714 let w = width_of(line_start, tok_abs_end);
715 if w <= max_width + 0.5 {
716 best_break = tok_abs_end;
717 continue;
718 }
719
720 if best_break > line_start {
722 out.push((line_start, best_break));
723 line_start = best_break;
724 } else {
725 let mut cut = tok_abs_start;
727 for (ofs, g) in tok.grapheme_indices(true) {
728 let next = tok_abs_start + ofs + g.len();
729 if width_of(line_start, next) <= max_width + 0.5 {
730 cut = next;
731 } else {
732 break;
733 }
734 }
735 if cut == line_start
736 && let Some((ofs, gr)) = tok.grapheme_indices(true).next()
737 {
738 cut = tok_abs_start + ofs + gr.len();
739 }
740 out.push((line_start, cut));
741 line_start = cut;
742 }
743
744 if let Some(ml) = max_lines
746 && out.len() >= ml
747 {
748 t = true;
749 break;
750 }
751
752 best_break = line_start;
753 }
754
755 if !t && line_start < end && max_lines.is_none_or(|ml| out.len() < ml) {
757 out.push((line_start, end));
758 }
759
760 (out, t)
761}
762
763pub fn ellipsize_line(
765 text: &str,
766 px: f32,
767 max_width: f32,
768 font_weight: u16,
769 font_style: u8,
770) -> String {
771 if text.is_empty() || max_width <= 0.0 {
772 return String::new();
773 }
774 let key = (
775 fast_hash(text),
776 (px * 100.0) as u32,
777 (max_width * 100.0) as u32,
778 font_weight,
779 font_style,
780 );
781 if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
782 return s;
783 }
784 let m = metrics_for_textfield(text, px, None, font_weight, font_style);
785 if let Some(&last) = m.positions.last()
786 && last <= max_width + 0.5
787 {
788 return text.to_string();
789 }
790 let _el = "…";
791 let e_w = ellipsis_width(px);
792 if e_w >= max_width {
793 return String::new();
794 }
795 let mut cut_i = 0usize;
797 for i in 0..m.positions.len() {
798 if m.positions[i] + e_w <= max_width {
799 cut_i = i;
800 } else {
801 break;
802 }
803 }
804 let byte = m
805 .byte_offsets
806 .get(cut_i)
807 .copied()
808 .unwrap_or(0)
809 .min(text.len());
810 let mut out = String::with_capacity(byte + 3);
811 out.push_str(&text[..byte]);
812 out.push('…');
813
814 let s = out;
815 ellip_cache().lock().unwrap().put(key, s.clone());
816
817 s
818}
819
820fn ellipsis_width(px: f32) -> f32 {
821 static ELLIP_W_LRU: OnceCell<Mutex<Lru<u32, f32>>> = OnceCell::new();
822 let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
823 let key = (px * 100.0) as u32;
824 if let Some(w) = cache.lock().unwrap().get(&key).copied() {
825 return w;
826 }
827 let w = if let Some(g) = crate::shape_line("…", px, None, 400, 0).last() {
828 g.x + g.advance
829 } else {
830 0.0
831 };
832 cache.lock().unwrap().put(key, w);
833 w
834}