1use ahash::{AHashMap, AHasher};
2use cosmic_text::{
3 Attrs, Buffer, CacheKey, FontSystem, Metrics, Shaping, SwashCache, SwashContent,
4};
5use once_cell::sync::OnceCell;
6use std::{
7 collections::{HashMap, VecDeque},
8 hash::{Hash, Hasher},
9 sync::Mutex,
10};
11use unicode_segmentation::UnicodeSegmentation;
12
13const WRAP_CACHE_CAP: usize = 1024;
14const ELLIP_CACHE_CAP: usize = 2048;
15
16static METRICS_LRU: OnceCell<Mutex<Lru<(u64, u32), TextMetrics>>> = OnceCell::new();
17fn metrics_cache() -> &'static Mutex<Lru<(u64, u32), TextMetrics>> {
18 METRICS_LRU.get_or_init(|| Mutex::new(Lru::new(4096)))
19}
20
21struct Lru<K, V> {
22 map: AHashMap<K, V>,
23 order: VecDeque<K>,
24 cap: usize,
25}
26impl<K: std::hash::Hash + Eq + Clone, V> Lru<K, V> {
27 fn new(cap: usize) -> Self {
28 Self {
29 map: AHashMap::new(),
30 order: VecDeque::new(),
31 cap,
32 }
33 }
34 fn get(&mut self, k: &K) -> Option<&V> {
35 if self.map.contains_key(k) {
36 if let Some(pos) = self.order.iter().position(|x| x == k) {
38 let key = self.order.remove(pos).unwrap();
39 self.order.push_back(key);
40 }
41 }
42 self.map.get(k)
43 }
44 fn put(&mut self, k: K, v: V) {
45 if self.map.contains_key(&k) {
46 self.map.insert(k.clone(), v);
47 if let Some(pos) = self.order.iter().position(|x| x == &k) {
48 let key = self.order.remove(pos).unwrap();
49 self.order.push_back(key);
50 }
51 return;
52 }
53 if self.map.len() >= self.cap
54 && let Some(old) = self.order.pop_front()
55 {
56 self.map.remove(&old);
57 }
58 self.order.push_back(k.clone());
59 self.map.insert(k, v);
60 }
61}
62
63static WRAP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<String>, bool)>>> =
64 OnceCell::new();
65static ELLIP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32), String>>> = OnceCell::new();
66
67fn wrap_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, bool), (Vec<String>, bool)>> {
68 WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
69}
70fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32), String>> {
71 ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
72}
73
74fn fast_hash(s: &str) -> u64 {
75 use std::hash::{Hash, Hasher};
76 let mut h = AHasher::default();
77 s.hash(&mut h);
78 h.finish()
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub struct GlyphKey(pub u64);
83
84pub struct ShapedGlyph {
85 pub key: GlyphKey,
86 pub x: f32,
87 pub y: f32,
88 pub w: f32,
89 pub h: f32,
90 pub bearing_x: f32,
91 pub bearing_y: f32,
92 pub advance: f32,
93}
94
95pub struct GlyphBitmap {
96 pub key: GlyphKey,
97 pub w: u32,
98 pub h: u32,
99 pub content: SwashContent,
100 pub data: Vec<u8>, }
102
103struct Engine {
104 fs: FontSystem,
105 cache: SwashCache,
106 key_map: HashMap<GlyphKey, CacheKey>,
108}
109
110impl Engine {
111 fn get_image(&mut self, key: CacheKey) -> Option<cosmic_text::SwashImage> {
112 self.cache.get_image(&mut self.fs, key).clone()
114 }
115}
116
117static ENGINE: OnceCell<Mutex<Engine>> = OnceCell::new();
118
119fn engine() -> &'static Mutex<Engine> {
120 ENGINE.get_or_init(|| {
121 #[allow(unused_mut)]
122 let mut fs = FontSystem::new();
123
124 let cache = SwashCache::new();
125
126 #[cfg(any(target_os = "android", target_arch = "wasm32"))]
127 {
129 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] =
132 include_bytes!("assets/NotoSansSymbols2-Regular.ttf"); {
134 let db = fs.db_mut();
136 db.load_font_data(FALLBACK_TTF.to_vec());
137 db.set_sans_serif_family("Open Sans".to_string());
138
139 db.load_font_data(FALLBACK_SYMBOLS_TTF.to_vec());
140 db.load_font_data(FALLBACK_EMOJI_TTF.to_vec());
141 }
142 }
143 Mutex::new(Engine {
144 fs,
145 cache,
146 key_map: HashMap::new(),
147 })
148 })
149}
150
151fn key_from_cachekey(k: &CacheKey) -> GlyphKey {
153 let mut h = AHasher::default();
154 k.hash(&mut h);
155 GlyphKey(h.finish())
156}
157
158pub fn shape_line(text: &str, px: f32) -> Vec<ShapedGlyph> {
160 let mut eng = engine().lock().unwrap();
161
162 let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
164 {
165 let mut b = buf.borrow_with(&mut eng.fs);
167 b.set_size(None, None);
168 b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
169 b.shape_until_scroll(true);
170 }
171
172 let mut out = Vec::new();
173 for run in buf.layout_runs() {
174 for g in run.glyphs {
175 let phys = g.physical((0.0, run.line_y), 1.0);
177 let key = key_from_cachekey(&phys.cache_key);
178 eng.key_map.insert(key, phys.cache_key);
179
180 let img_opt = eng.get_image(phys.cache_key);
182 let (w, h, left, top) = if let Some(img) = img_opt.as_ref() {
183 (
184 img.placement.width as f32,
185 img.placement.height as f32,
186 img.placement.left as f32,
187 img.placement.top as f32,
188 )
189 } else {
190 (0.0, 0.0, 0.0, 0.0)
191 };
192
193 out.push(ShapedGlyph {
194 key,
195 x: g.x + g.x_offset, y: run.line_y, w,
198 h,
199 bearing_x: left,
200 bearing_y: top,
201 advance: g.w,
202 });
203 }
204 }
205 out
206}
207
208pub fn rasterize(key: GlyphKey, _px: f32) -> Option<GlyphBitmap> {
211 let mut eng = engine().lock().unwrap();
212 let &ck = eng.key_map.get(&key)?;
213
214 let img = eng.get_image(ck).as_ref()?.clone();
215 Some(GlyphBitmap {
216 key,
217 w: img.placement.width,
218 h: img.placement.height,
219 content: img.content,
220 data: img.data, })
222}
223
224#[derive(Clone)]
226pub struct TextMetrics {
227 pub positions: Vec<f32>, pub byte_offsets: Vec<usize>, }
230
231pub fn metrics_for_textfield(text: &str, px: f32) -> TextMetrics {
233 let key = (fast_hash(text), (px * 100.0) as u32);
234 if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
235 return m;
236 }
237 let mut eng = engine().lock().unwrap();
238 let mut buf = Buffer::new(&mut eng.fs, Metrics::new(px, px * 1.3));
239 {
240 let mut b = buf.borrow_with(&mut eng.fs);
241 b.set_size(None, None);
242 b.set_text(text, &Attrs::new(), Shaping::Advanced, None);
243 b.shape_until_scroll(true);
244 }
245 let mut edges: Vec<(usize, f32)> = Vec::new();
246 let mut last_x = 0.0f32;
247 for run in buf.layout_runs() {
248 for g in run.glyphs {
249 let right = g.x + g.w;
250 last_x = right.max(last_x);
251 edges.push((g.end, right));
252 }
253 }
254 if edges.last().map(|e| e.0) != Some(text.len()) {
255 edges.push((text.len(), last_x));
256 }
257 let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
258 let mut byte_offsets = Vec::with_capacity(positions.capacity());
259 positions.push(0.0);
260 byte_offsets.push(0);
261 let mut last_byte = 0usize;
262 for (b, _) in text.grapheme_indices(true) {
263 positions
264 .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
265 byte_offsets.push(b);
266 last_byte = b;
267 }
268 if *byte_offsets.last().unwrap_or(&0) != text.len() {
269 positions.push(
270 positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
271 );
272 byte_offsets.push(text.len());
273 }
274 let m = TextMetrics {
275 positions,
276 byte_offsets,
277 };
278 metrics_cache().lock().unwrap().put(key, m.clone());
279 m
280}
281
282fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
283 let x0 = lookup_right(edges, start_b);
284 let x1 = lookup_right(edges, end_b);
285 (x1 - x0).max(0.0)
286}
287fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
288 match edges.binary_search_by_key(&b, |e| e.0) {
289 Ok(i) => edges[i].1,
290 Err(i) => {
291 if i == 0 {
292 0.0
293 } else {
294 edges[i - 1].1
295 }
296 }
297 }
298}
299
300pub fn wrap_lines(
304 text: &str,
305 px: f32,
306 max_width: f32,
307 max_lines: Option<usize>,
308 soft_wrap: bool,
309) -> (Vec<String>, bool) {
310 if text.is_empty() || max_width <= 0.0 {
311 return (vec![String::new()], false);
312 }
313 if !soft_wrap {
314 return (vec![text.to_string()], false);
315 }
316
317 let max_lines_key: u16 = match max_lines {
318 None => 0,
319 Some(n) => {
320 let n = n.min(u16::MAX as usize - 1) as u16;
321 n.saturating_add(1)
322 }
323 };
324 let key = (
325 fast_hash(text),
326 (px * 100.0) as u32,
327 (max_width * 100.0) as u32,
328 max_lines_key,
329 soft_wrap,
330 );
331 if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
332 return h;
333 }
334
335 let m = metrics_for_textfield(text, px);
337 if let Some(&last) = m.positions.last()
339 && last <= max_width + 0.5
340 {
341 return (vec![text.to_string()], false);
342 }
343
344 let width_of = |start_b: usize, end_b: usize| -> f32 {
346 let i0 = match m.byte_offsets.binary_search(&start_b) {
347 Ok(i) | Err(i) => i,
348 };
349 let i1 = match m.byte_offsets.binary_search(&end_b) {
350 Ok(i) | Err(i) => i,
351 };
352 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
353 .max(0.0)
354 };
355
356 let mut out: Vec<String> = Vec::new();
357 let mut truncated = false;
358
359 let mut line_start = 0usize; let mut best_break = line_start;
361 let mut last_w = 0.0;
362
363 for tok in text.split_word_bounds() {
365 let tok_start = best_break;
366 let tok_end = tok_start + tok.len();
367 let w = width_of(line_start, tok_end);
368
369 if w <= max_width + 0.5 {
370 best_break = tok_end;
371 last_w = w;
372 continue;
373 }
374
375 if best_break > line_start {
377 out.push(text[line_start..best_break].trim_end().to_string());
379 line_start = best_break;
380 } else {
381 let mut cut = tok_start;
383 for g in tok.grapheme_indices(true) {
384 let next = tok_start + g.0 + g.1.len();
385 if width_of(line_start, next) <= max_width + 0.5 {
386 cut = next;
387 } else {
388 break;
389 }
390 }
391 if cut == line_start {
392 if let Some((ofs, grapheme)) = tok.grapheme_indices(true).next() {
394 cut = tok_start + ofs + grapheme.len();
395 }
396 }
397 out.push(text[line_start..cut].to_string());
398 line_start = cut;
399 }
400
401 if let Some(ml) = max_lines
403 && out.len() >= ml
404 {
405 truncated = true;
406 line_start = line_start.min(text.len());
408 break;
409 }
410
411 best_break = line_start;
413 last_w = 0.0;
414
415 if line_start < tok_end {
417 if width_of(line_start, tok_end) <= max_width + 0.5 {
419 best_break = tok_end;
420 last_w = width_of(line_start, best_break);
421 } else {
422 }
424 }
425 }
426
427 if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
429 out.push(text[line_start..].trim_end().to_string());
430 }
431
432 let res = (out, truncated);
433
434 wrap_cache().lock().unwrap().put(key, res.clone());
435 res
436}
437
438pub fn ellipsize_line(text: &str, px: f32, max_width: f32) -> String {
440 if text.is_empty() || max_width <= 0.0 {
441 return String::new();
442 }
443 let key = (
444 fast_hash(text),
445 (px * 100.0) as u32,
446 (max_width * 100.0) as u32,
447 );
448 if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
449 return s;
450 }
451 let m = metrics_for_textfield(text, px);
452 if let Some(&last) = m.positions.last()
453 && last <= max_width + 0.5
454 {
455 return text.to_string();
456 }
457 let el = "…";
458 let e_w = ellipsis_width(px);
459 if e_w >= max_width {
460 return String::new();
461 }
462 let mut cut_i = 0usize;
464 for i in 0..m.positions.len() {
465 if m.positions[i] + e_w <= max_width {
466 cut_i = i;
467 } else {
468 break;
469 }
470 }
471 let byte = m
472 .byte_offsets
473 .get(cut_i)
474 .copied()
475 .unwrap_or(0)
476 .min(text.len());
477 let mut out = String::with_capacity(byte + 3);
478 out.push_str(&text[..byte]);
479 out.push('…');
480
481 let s = out;
482 ellip_cache().lock().unwrap().put(key, s.clone());
483
484 s
485}
486
487fn ellipsis_width(px: f32) -> f32 {
488 static ELLIP_W_LRU: OnceCell<Mutex<Lru<u32, f32>>> = OnceCell::new();
489 let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
490 let key = (px * 100.0) as u32;
491 if let Some(w) = cache.lock().unwrap().get(&key).copied() {
492 return w;
493 }
494 let w = if let Some(g) = crate::shape_line("…", px).last() {
495 g.x + g.advance
496 } else {
497 0.0
498 };
499 cache.lock().unwrap().put(key, w);
500 w
501}