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
14static FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
16
17pub 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 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>, }
115
116struct Engine {
117 fs: FontSystem,
118 cache: SwashCache,
119 key_map: HashMap<GlyphKey, CacheKey>,
121}
122
123impl Engine {
124 fn get_image(&mut self, key: CacheKey) -> Option<cosmic_text::SwashImage> {
125 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 {
142 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] =
145 include_bytes!("assets/NotoSansSymbols2-Regular.ttf"); {
147 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
164fn key_from_cachekey(k: &CacheKey) -> GlyphKey {
166 let mut h = AHasher::default();
167 k.hash(&mut h);
168 GlyphKey(h.finish())
169}
170
171pub fn shape_line(text: &str, px: f32) -> Vec<ShapedGlyph> {
173 let mut eng = engine().lock().unwrap();
174
175 let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
177 {
178 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 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 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, y: run.line_y, w,
211 h,
212 bearing_x: left,
213 bearing_y: top,
214 advance: g.w,
215 });
216 }
217 }
218 out
219}
220
221pub 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, })
235}
236
237#[derive(Clone)]
239pub struct TextMetrics {
240 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>, }
243
244pub 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
313pub 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 let m = metrics_for_textfield(text, px);
350 if let Some(&last) = m.positions.last()
352 && last <= max_width + 0.5
353 {
354 return (vec![text.to_string()], false);
355 }
356
357 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; let mut best_break = line_start;
374 let mut last_w = 0.0;
375
376 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 if best_break > line_start {
390 out.push(text[line_start..best_break].trim_end().to_string());
392 line_start = best_break;
393 } else {
394 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 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 if let Some(ml) = max_lines
416 && out.len() >= ml
417 {
418 truncated = true;
419 line_start = line_start.min(text.len());
421 break;
422 }
423
424 best_break = line_start;
426 last_w = 0.0;
427
428 if line_start < tok_end {
430 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 }
437 }
438 }
439
440 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
451pub 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 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}