1use font_awl::FontProvider;
2use rapidhash::{HashMapExt, RapidHashMap, fast::RapidHasher};
3use skrifa::MetadataProvider;
4use skrifa::outline::OutlinePen;
5use std::sync::OnceLock;
6
7pub mod fallback;
8pub mod fallback_data;
9pub mod unresolved;
10
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::{
13 collections::{HashMap, VecDeque},
14 hash::{Hash, Hasher},
15 sync::Mutex,
16};
17use unicode_segmentation::UnicodeSegmentation;
18
19static FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
20static FONT_GENERATION: AtomicU64 = AtomicU64::new(0);
21static FALLBACK_DIRTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
22
23pub fn begin_frame() {
24 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
25}
26
27pub fn current_frame() -> u64 {
28 FRAME_COUNTER.load(Ordering::Relaxed)
29}
30
31pub fn font_generation() -> u64 {
32 FONT_GENERATION.load(Ordering::Relaxed)
33}
34
35pub fn take_fallback_dirty() -> bool {
36 FALLBACK_DIRTY.swap(false, Ordering::SeqCst)
39}
40
41const GLYPH_CACHE_CAP: usize = 4096;
42const WRAP_CACHE_CAP: usize = 1024;
43const ELLIP_CACHE_CAP: usize = 2048;
44
45static METRICS_LRU: OnceLock<Mutex<Lru<(u64, u32, u64, u16, u8, i32, u64), TextMetrics>>> =
46 OnceLock::new();
47fn metrics_cache() -> &'static Mutex<Lru<(u64, u32, u64, u16, u8, i32, u64), TextMetrics>> {
48 METRICS_LRU.get_or_init(|| Mutex::new(Lru::new(4096)))
49}
50
51struct Lru<K, V> {
52 map: RapidHashMap<K, V>,
53 ticks: RapidHashMap<K, u64>,
54 order: VecDeque<K>,
55 cap: usize,
56 tick_counter: u64,
57}
58impl<K: std::hash::Hash + Eq + Clone, V> Lru<K, V> {
59 fn new(cap: usize) -> Self {
60 Self {
61 map: RapidHashMap::new(),
62 ticks: RapidHashMap::new(),
63 order: VecDeque::new(),
64 cap,
65 tick_counter: 0,
66 }
67 }
68 fn get(&mut self, k: &K) -> Option<&V> {
69 if self.map.contains_key(k) {
70 self.tick_counter = self.tick_counter.wrapping_add(1);
71 self.ticks.insert(k.clone(), self.tick_counter);
72 self.order.push_back(k.clone());
75 if self.order.len() > self.cap * 3 {
76 let mut pairs: Vec<(K, u64)> = self
77 .ticks
78 .iter()
79 .map(|(kk, tt)| (kk.clone(), *tt))
80 .collect();
81 pairs.sort_by_key(|(_, t)| *t);
82 self.order.clear();
83 for (kk, _) in pairs {
84 self.order.push_back(kk);
85 }
86 }
87 }
88 self.map.get(k)
89 }
90 fn put(&mut self, k: K, v: V) {
91 self.tick_counter = self.tick_counter.wrapping_add(1);
92 let is_new = !self.map.contains_key(&k);
93 self.map.insert(k.clone(), v);
94 self.ticks.insert(k.clone(), self.tick_counter);
95 if is_new {
96 self.order.push_back(k.clone());
97 } else {
98 self.order.push_back(k);
99 }
100 while self.map.len() > self.cap {
101 let victim = {
102 let mut min_key: Option<K> = None;
103 let mut min_tick = u64::MAX;
104 for (kk, tt) in &self.ticks {
105 if *tt < min_tick {
106 min_tick = *tt;
107 min_key = Some(kk.clone());
108 }
109 }
110 min_key
111 };
112 if let Some(victim) = victim {
113 self.map.remove(&victim);
114 self.ticks.remove(&victim);
115 if let Some(pos) = self.order.iter().position(|x| x == &victim) {
116 self.order.remove(pos);
117 }
118 } else {
119 break;
120 }
121 }
122 if self.order.len() > self.cap * 2 {
123 let mut seen = std::collections::HashSet::new();
124 let mut compacted = VecDeque::new();
125 let mut pairs: Vec<(K, u64)> = self
127 .ticks
128 .iter()
129 .map(|(kk, tt)| (kk.clone(), *tt))
130 .collect();
131 pairs.sort_by_key(|(_, t)| *t);
132 for (kk, _) in pairs {
133 if seen.insert(kk.clone()) {
134 compacted.push_back(kk);
135 }
136 }
137 self.order = compacted;
138 }
139 }
140 fn clear_both(&mut self) {
141 self.map.clear();
142 self.ticks.clear();
143 self.order.clear();
144 self.tick_counter = 0;
145 }
146}
147
148static WRAP_LRU: OnceLock<
149 Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<String>, bool)>>,
150> = OnceLock::new();
151
152static WRAP_RANGES_LRU: OnceLock<
153 Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<(usize, usize)>, bool)>>,
154> = OnceLock::new();
155
156static ELLIP_LRU: OnceLock<Mutex<Lru<(u64, u32, u32, u16, u8, i32, u64), String>>> =
157 OnceLock::new();
158
159fn wrap_cache()
160-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<String>, bool)>> {
161 WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
162}
163
164fn wrap_ranges_cache()
165-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<(usize, usize)>, bool)>> {
166 WRAP_RANGES_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
167}
168
169fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, u8, i32, u64), String>> {
170 ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
171}
172
173fn fast_hash(s: &str) -> u64 {
174 let mut h = RapidHasher::default();
175 s.len().hash(&mut h);
176 s.hash(&mut h);
177 h.finish()
178}
179
180#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
181pub struct GlyphKey(pub u64);
182
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
186pub struct CacheKey {
187 pub font_id: u64,
188 pub glyph_id: u32,
189 pub font_size_bits: u32,
190}
191
192static GLYPH_ID_WARNED: OnceLock<Mutex<std::collections::HashSet<(u64, u32)>>> = OnceLock::new();
193
194fn swash_glyph_id(font_id: u64, glyph_id: u32) -> u16 {
201 match u16::try_from(glyph_id) {
202 Ok(v) => v,
203 Err(_) => {
204 let warned =
205 GLYPH_ID_WARNED.get_or_init(|| Mutex::new(std::collections::HashSet::new()));
206 let is_new = warned
207 .lock()
208 .map(|mut g| g.insert((font_id, glyph_id)))
209 .unwrap_or(false);
210 if is_new {
211 log::warn!(
212 "glyph id {} (font {}) exceeds u16::MAX; falling back to .notdef (0)",
213 glyph_id,
214 font_id
215 );
216 }
217 0
218 }
219 }
220}
221
222#[derive(Clone, Debug)]
224pub enum Command {
225 MoveTo(f32, f32),
226 LineTo(f32, f32),
227 QuadTo(f32, f32, f32, f32),
228 CurveTo(f32, f32, f32, f32, f32, f32),
229 Close,
230}
231
232pub struct ShapedGlyph {
233 pub key: GlyphKey,
234 pub px: f32,
235 pub x: f32,
236 pub y: f32,
237 pub w: f32,
238 pub h: f32,
239 pub bearing_x: f32,
240 pub bearing_y: f32,
241 pub advance: f32,
242}
243
244pub use swash::scale::image::Content as SwashContent;
245
246pub struct GlyphBitmap {
247 pub key: GlyphKey,
248 pub w: u32,
249 pub h: u32,
250 pub content: SwashContent,
251 pub data: Vec<u8>,
252}
253
254struct FontRecord {
255 id: u64,
256 data: parley::FontData,
257 data_bytes: Vec<u8>,
258}
259
260struct Engine {
261 font_cx: parley::FontContext,
262 layout_cx: parley::LayoutContext<()>,
263 swash_cx: swash::scale::ScaleContext,
264 key_map: HashMap<GlyphKey, (u64, u32)>,
265 font_registry: Vec<FontRecord>,
266 next_font_id: u64,
267 glyph_cache:
270 HashMap<(u64, u32, u32), (u32, u32, i32, i32, swash::scale::image::Content, Vec<u8>)>,
271 ascent_cache: RapidHashMap<(u64, u16, u32), (f32, f32)>,
274}
275
276impl Engine {
277 fn ensure_font(&mut self, fd: &parley::FontData) -> u64 {
278 if let Some(existing) = self.font_registry.iter().find(|r| r.data == *fd) {
279 log::debug!(
280 "[font] reuse id={} len={}",
281 existing.id,
282 fd.data.as_ref().len()
283 );
284 return existing.id;
285 }
286 let id = self.next_font_id;
287 self.next_font_id += 1;
288 let bytes = fd.data.as_ref().to_vec();
289 log::debug!("[font] register id={} len={}", id, bytes.len());
290 self.font_registry.push(FontRecord {
291 id,
292 data: fd.clone(),
293 data_bytes: bytes,
294 });
295 id
296 }
297
298 fn trim_glyph_cache(&mut self) {
299 if self.glyph_cache.len() > GLYPH_CACHE_CAP {
300 let to_remove = self.glyph_cache.len() - GLYPH_CACHE_CAP;
301 let keys: Vec<_> = self.glyph_cache.keys().take(to_remove).copied().collect();
302 for k in keys {
303 self.glyph_cache.remove(&k);
304 }
305 }
306 }
307
308 fn resolve_vertical_metrics(
312 &mut self,
313 font_family: Option<&str>,
314 font_weight: u16,
315 px: f32,
316 ) -> (f32, f32) {
317 let mut candidates: Vec<&str> = Vec::new();
318 match font_family {
319 Some("monospace") => candidates.push("JetBrains Mono"),
320 Some("sans-serif") => candidates.push("Open Sans"),
321 Some("emoji") => candidates.push("Noto Color Emoji"),
322 Some(other) => candidates.push(other),
323 None => {}
324 }
325 candidates.push("Open Sans");
326 for name in candidates {
327 if let Some(m) = self.metrics_for_family(name, font_weight, px) {
328 return m;
329 }
330 }
331 (px * 0.8, px * 0.2)
332 }
333
334 fn metrics_for_family(&mut self, name: &str, font_weight: u16, px: f32) -> Option<(f32, f32)> {
337 let info = self.font_cx.collection.family_by_name(name)?;
338 let target = font_weight as f32;
339 let mut best: Option<(f32, Vec<u8>, u32)> = None;
340 for font in info.fonts() {
341 let dist = (font.weight().value() - target).abs();
342 if best.as_ref().is_some_and(|(bd, _, _)| *bd <= dist) {
343 continue;
344 }
345 let bytes: Vec<u8> = match font.source().kind() {
346 parley::fontique::SourceKind::Memory(blob) => blob.as_ref().to_vec(),
347 #[cfg(not(target_arch = "wasm32"))]
348 parley::fontique::SourceKind::Path(path) => std::fs::read(path).ok()?,
349 #[cfg(target_arch = "wasm32")]
350 parley::fontique::SourceKind::Path(_) => continue,
351 };
352 best = Some((dist, bytes, font.index()));
353 }
354 let (_, bytes, index) = best?;
355 let font = skrifa::FontRef::from_index(&bytes, index).ok()?;
356 let metrics = font.metrics(
357 skrifa::instance::Size::new(px),
358 skrifa::instance::LocationRef::default(),
359 );
360 Some((metrics.ascent.max(0.0), metrics.descent.abs().max(0.0)))
361 }
362
363 fn raster_placement(
364 &mut self,
365 font_id: u64,
366 glyph_id: u32,
367 px: f32,
368 ) -> Option<(f32, f32, f32, f32)> {
369 use swash::scale::{Render, Source, StrikeWith};
370 let cache_key = (font_id, glyph_id, px.to_bits());
371 if let Some(cached) = self.glyph_cache.get(&cache_key) {
372 log::debug!(
373 "[raster_placement] HIT fid={} gid={} px={} => {}x{} {}x{}",
374 font_id,
375 glyph_id,
376 px,
377 cached.0,
378 cached.1,
379 cached.2,
380 cached.3
381 );
382 return Some((
383 cached.0 as f32,
384 cached.1 as f32,
385 cached.2 as f32,
386 cached.3 as f32,
387 ));
388 }
389 let data_bytes = self
390 .font_registry
391 .iter()
392 .find(|r| r.id == font_id)?
393 .data_bytes
394 .clone();
395 let font = swash::FontRef::from_index(&data_bytes, 0)?;
396 let mut scaler = self.swash_cx.builder(font).size(px).hint(true).build();
397 let image = Render::new(&[
398 Source::Outline,
399 Source::ColorBitmap(StrikeWith::BestFit),
400 Source::ColorOutline(0),
401 ])
402 .render(&mut scaler, swash_glyph_id(font_id, glyph_id))?;
403 log::debug!(
404 "[raster_placement] MISS fid={} gid={} px={} => {}x{} {}x{}",
405 font_id,
406 glyph_id,
407 px,
408 image.placement.width,
409 image.placement.height,
410 image.placement.left,
411 image.placement.top
412 );
413 self.glyph_cache.insert(
414 cache_key,
415 (
416 image.placement.width,
417 image.placement.height,
418 image.placement.left,
419 image.placement.top,
420 image.content,
421 image.data,
422 ),
423 );
424 self.trim_glyph_cache();
425 Some((
426 image.placement.width as f32,
427 image.placement.height as f32,
428 image.placement.left as f32,
429 image.placement.top as f32,
430 ))
431 }
432}
433
434static ENGINE: OnceLock<Mutex<Engine>> = OnceLock::new();
435
436pub static FONT_PROVIDER: OnceLock<Mutex<font_awl::Provider>> = OnceLock::new();
437
438fn init_engine_sync() -> Engine {
439 let mut provider = font_awl::Provider::new();
440 provider.load_bundled_fonts();
441 #[cfg(not(target_arch = "wasm32"))]
442 if let Err(e) = provider.load_system_fonts_best_effort() {
443 log::warn!("font-awl: failed to load system fonts: {e}");
444 }
445
446 let mut font_cx = provider.new_parley_context();
447 #[cfg(target_arch = "wasm32")]
451 {
452 if let Some(info) = font_cx.collection.family_by_name("Noto Sans Symbols 2") {
453 let id = info.id();
454 let mut existing: Vec<parley::fontique::FamilyId> = font_cx
455 .collection
456 .generic_families(parley::fontique::GenericFamily::SansSerif)
457 .collect();
458 if !existing.contains(&id) {
459 existing.push(id);
460 font_cx.collection.set_generic_families(
461 parley::fontique::GenericFamily::SansSerif,
462 existing.into_iter(),
463 );
464 }
465 }
466 }
469 let layout_cx = parley::LayoutContext::new();
470
471 static MATERIAL_SYMBOLS_TTF: &[u8] = include_bytes!("assets/MaterialSymbolsOutlined.ttf");
472 let blob: parley::fontique::Blob<u8> = MATERIAL_SYMBOLS_TTF.to_vec().into();
473 font_cx.collection.register_fonts(blob, None);
474
475 let _ = FONT_PROVIDER.set(Mutex::new(provider));
476
477 Engine {
478 font_cx,
479 layout_cx,
480 swash_cx: swash::scale::ScaleContext::new(),
481 key_map: HashMap::new(),
482 font_registry: Vec::new(),
483 next_font_id: 1,
484 glyph_cache: HashMap::new(),
485 ascent_cache: RapidHashMap::new(),
486 }
487}
488
489#[cfg(target_arch = "wasm32")]
490pub async fn init_fonts_wasm() {
491 let mut provider = font_awl::Provider::new();
492 provider.load_bundled_fonts();
493 if let Err(e) = provider.load_web_fonts().await {
494 log::warn!("font-awl: failed to load web fonts: {e}");
495 }
496 let _ = FONT_PROVIDER.set(Mutex::new(provider));
497
498 if let Some(eng) = ENGINE.get() {
499 let mut eng = eng.lock().unwrap();
500 if let Some(provider_lock) = FONT_PROVIDER.get() {
503 let p = provider_lock.lock().unwrap();
504 eng.font_cx = p.new_parley_context();
505 }
506 }
507}
508
509fn engine() -> &'static Mutex<Engine> {
510 ENGINE.get_or_init(|| Mutex::new(init_engine_sync()))
511}
512
513pub fn register_font_data(bytes: &[u8]) {
514 let mut eng = engine().lock().unwrap();
515 let blob: parley::fontique::Blob<u8> = bytes.to_vec().into();
516 let families = eng.font_cx.collection.register_fonts(blob.clone(), None);
517 let mut is_emoji = false;
519 let mut is_symbols = false;
520 if let Some(name) = font_family_name(bytes) {
521 if name.starts_with("Noto Color Emoji") {
522 is_emoji = true;
523 } else if name.starts_with("Noto Sans Symbols") {
524 is_symbols = true;
525 }
526 }
527 if !is_emoji && !is_symbols {
528 for (fid, _) in &families {
529 if let Some(fname) = eng.font_cx.collection.family_name(*fid) {
530 if fname.starts_with("Noto Color Emoji") {
531 is_emoji = true;
532 }
533 if fname.starts_with("Noto Sans Symbols") {
534 is_symbols = true;
535 }
536 } else if let Some(info) = eng.font_cx.collection.family(*fid) {
537 let fname = info.name();
538 if fname.starts_with("Noto Color Emoji") {
539 is_emoji = true;
540 }
541 if fname.starts_with("Noto Sans Symbols") {
542 is_symbols = true;
543 }
544 }
545 }
546 }
547 if is_emoji {
548 let ids: Vec<parley::fontique::FamilyId> = families.iter().map(|(fid, _)| *fid).collect();
549 let mut existing: Vec<parley::fontique::FamilyId> = eng
550 .font_cx
551 .collection
552 .generic_families(parley::fontique::GenericFamily::Emoji)
553 .collect();
554 for id in ids.clone() {
555 if !existing.contains(&id) {
556 existing.push(id);
557 }
558 }
559 eng.font_cx
560 .collection
561 .set_generic_families(parley::fontique::GenericFamily::Emoji, existing.into_iter());
562 } else if is_symbols {
563 let ids: Vec<parley::fontique::FamilyId> = families.iter().map(|(fid, _)| *fid).collect();
564 let mut existing: Vec<parley::fontique::FamilyId> = eng
565 .font_cx
566 .collection
567 .generic_families(parley::fontique::GenericFamily::SansSerif)
568 .collect();
569 for id in ids.clone() {
570 if !existing.contains(&id) {
571 existing.push(id);
572 }
573 }
574 eng.font_cx.collection.set_generic_families(
575 parley::fontique::GenericFamily::SansSerif,
576 existing.into_iter(),
577 );
578 }
579 eng.font_cx.source_cache = parley::fontique::SourceCache::default();
581 if let Some(provider_lock) = FONT_PROVIDER.get() {
582 let mut p = provider_lock.lock().unwrap();
583 let families2 = p.collection_mut().register_fonts(blob, None);
584 if is_emoji {
586 let ids: Vec<parley::fontique::FamilyId> =
587 families2.iter().map(|(fid, _)| *fid).collect();
588 let mut existing: Vec<parley::fontique::FamilyId> = p
589 .collection_mut()
590 .generic_families(parley::fontique::GenericFamily::Emoji)
591 .collect();
592 for id in ids.clone() {
593 if !existing.contains(&id) {
594 existing.push(id);
595 }
596 }
597 p.collection_mut()
598 .set_generic_families(parley::fontique::GenericFamily::Emoji, existing.into_iter());
599 } else if is_symbols {
600 let ids: Vec<parley::fontique::FamilyId> =
601 families2.iter().map(|(fid, _)| *fid).collect();
602 let mut existing: Vec<parley::fontique::FamilyId> = p
603 .collection_mut()
604 .generic_families(parley::fontique::GenericFamily::SansSerif)
605 .collect();
606 for id in ids.clone() {
607 if !existing.contains(&id) {
608 existing.push(id);
609 }
610 }
611 p.collection_mut().set_generic_families(
612 parley::fontique::GenericFamily::SansSerif,
613 existing.into_iter(),
614 );
615 }
616 }
617 clear_caches_for_fallback_in(&mut eng);
618 #[cfg(target_arch = "wasm32")]
620 {
621 crate::unresolved::web_unresolved_registry().on_new_font_installed();
624 }
625}
626
627pub(crate) fn clear_caches_for_fallback() {
628 clear_lru_caches();
629 if let Ok(mut eng) = engine().lock() {
630 eng.ascent_cache.clear();
631 }
632 bump_frame_for_fallback();
633}
634
635pub(crate) fn clear_caches_for_fallback_in(eng: &mut Engine) {
637 clear_lru_caches();
638 eng.ascent_cache.clear();
639 bump_frame_for_fallback();
640}
641
642fn clear_lru_caches() {
643 if let Some(c) = METRICS_LRU.get()
644 && let Ok(mut g) = c.lock()
645 {
646 g.clear_both();
647 }
648 if let Some(c) = WRAP_LRU.get()
649 && let Ok(mut g) = c.lock()
650 {
651 g.clear_both();
652 }
653 if let Some(c) = WRAP_RANGES_LRU.get()
654 && let Ok(mut g) = c.lock()
655 {
656 g.clear_both();
657 }
658 if let Some(c) = ELLIP_LRU.get()
659 && let Ok(mut g) = c.lock()
660 {
661 g.clear_both();
662 }
663}
664
665pub(crate) fn bump_frame_for_fallback() {
666 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
667 FONT_GENERATION.fetch_add(1, Ordering::Relaxed);
668 FALLBACK_DIRTY.store(true, Ordering::Relaxed);
669}
670
671#[cfg(target_arch = "wasm32")]
672pub fn ensure_web_fallback_initialized() {
673 crate::fallback::wasm_fallback::ensure_fallback_initialized();
674}
675
676#[cfg(not(target_arch = "wasm32"))]
677pub fn ensure_web_fallback_initialized() {}
678
679pub fn load_font_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
683 let bytes = std::fs::read(path)?;
684 register_font_data(&bytes);
685 Ok(())
686}
687
688pub fn primary_font_vertical_metrics(
696 font_family: Option<&str>,
697 font_weight: u16,
698 px: f32,
699) -> (f32, f32) {
700 if !(px > 0.0) {
701 return (0.0, 0.0);
702 }
703 let key = (
704 font_family.map(fast_hash).unwrap_or(0),
705 font_weight,
706 (px * 100.0) as u32,
707 );
708 let mut eng = engine().lock().unwrap();
709 if let Some(&m) = eng.ascent_cache.get(&key) {
710 return m;
711 }
712 let m = eng.resolve_vertical_metrics(font_family, font_weight, px);
713 eng.ascent_cache.insert(key, m);
714 m
715}
716
717pub fn font_family_name(bytes: &[u8]) -> Option<String> {
722 use skrifa::string::StringId;
723 let font = skrifa::FontRef::new(bytes).ok()?;
724 font.localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME)
725 .english_or_first()
726 .map(|s| s.to_string())
727 .or_else(|| {
728 font.localized_strings(StringId::FAMILY_NAME)
729 .english_or_first()
730 .map(|s| s.to_string())
731 })
732}
733
734fn key_from_pair(font_id: u64, glyph_id: u32) -> GlyphKey {
735 let mut h = RapidHasher::default();
736 font_id.hash(&mut h);
737 glyph_id.hash(&mut h);
738 GlyphKey(h.finish())
739}
740
741#[cfg(target_arch = "wasm32")]
742fn collect_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) -> Vec<u32> {
743 use parley::layout::PositionedLayoutItem;
744 let mut out = Vec::new();
745 let mut total_glyphs: usize = 0;
746 for line in layout.lines() {
747 for item in line.items() {
748 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
749 continue;
750 };
751 let run = glyph_run.run();
752 for cluster in run.clusters() {
754 let glyphs: Vec<_> = cluster.glyphs().collect();
755 total_glyphs += glyphs.len();
756 let has_missing = glyphs.iter().any(|g| g.id == 0);
757 if has_missing {
758 let range = cluster.text_range();
759 let end = range.end.min(text.len());
761 let start = range.start.min(end);
762 for ch in text[start..end].chars() {
763 out.push(ch as u32);
764 }
765 if range.start == range.end {
767 }
769 }
770 }
771 }
772 }
773 if out.is_empty() && !text.is_empty() && total_glyphs == 0 {
774 if text.chars().any(|c| !c.is_whitespace()) {
775 for ch in text.chars() {
776 if !ch.is_whitespace() && ch != '\n' && ch != '\r' && ch != '\t' {
777 out.push(ch as u32);
778 }
779 }
780 }
781 }
782 out
783}
784
785fn shape_line_inner(
786 eng: &mut Engine,
787 text: &str,
788 px: f32,
789 line_height_ratio: f32,
790 font_family: Option<&str>,
791 font_weight: u16,
792 font_style: u8,
793 letter_spacing: f32,
794 font_variation_settings: Option<&str>,
795) -> Vec<ShapedGlyph> {
796 use parley::FontWeight;
797 use parley::layout::PositionedLayoutItem;
798 use parley::style::StyleProperty;
799
800 let Engine {
801 ref mut font_cx,
802 ref mut layout_cx,
803 ..
804 } = *eng;
805 let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
806 builder.push_default(StyleProperty::FontSize(px));
807 if line_height_ratio > 0.0 {
808 builder.push_default(StyleProperty::LineHeight(
809 parley::LineHeight::FontSizeRelative(line_height_ratio),
810 ));
811 }
812 builder.push_default(StyleProperty::FontWeight(FontWeight::new(
813 font_weight as f32,
814 )));
815 builder.push_default(StyleProperty::FontStyle(match font_style {
816 1 => parley::FontStyle::Italic,
817 _ => parley::FontStyle::Normal,
818 }));
819 builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
820
821 if let Some(settings) = font_variation_settings {
822 builder.push_default(StyleProperty::FontVariations(
823 parley::style::FontVariations::from(settings),
824 ));
825 }
826
827 if let Some(family) = font_family {
828 #[cfg(not(target_arch = "wasm32"))]
829 {
830 use parley::style::{FontFamilyName, GenericFamily};
831 let names: &[FontFamilyName] = match family {
832 "monospace" => &[
833 FontFamilyName::named("JetBrains Mono"),
834 GenericFamily::Monospace.into(),
835 ],
836 "sans-serif" => &[
837 FontFamilyName::named("Open Sans"),
838 GenericFamily::SansSerif.into(),
839 ],
840 "emoji" => &[
841 FontFamilyName::named("Noto Color Emoji"),
842 GenericFamily::Emoji.into(),
843 ],
844 "serif" => &[GenericFamily::Serif.into()],
845 "cursive" => &[GenericFamily::Cursive.into()],
846 "fantasy" => &[GenericFamily::Fantasy.into()],
847 "system-ui" => &[GenericFamily::SystemUi.into()],
848 "math" => &[GenericFamily::Math.into()],
849 _ => &[FontFamilyName::named(family)],
850 };
851 builder.push(names, 0..text.len());
852 }
853 #[cfg(target_arch = "wasm32")]
854 {
855 use parley::style::{FontFamilyName, GenericFamily};
856 let names: &[FontFamilyName] = match family {
857 "monospace" => &[
858 FontFamilyName::named("JetBrains Mono"),
859 GenericFamily::Monospace.into(),
860 GenericFamily::SansSerif.into(),
861 FontFamilyName::named("Noto Sans Symbols 2"),
862 FontFamilyName::named("Noto Sans Symbols2"),
863 FontFamilyName::named("Noto Sans Symbols"),
864 ],
865 "sans-serif" => &[
866 FontFamilyName::named("Open Sans"),
867 GenericFamily::SansSerif.into(),
868 GenericFamily::Emoji.into(),
869 FontFamilyName::named("Noto Color Emoji"),
870 FontFamilyName::named("Noto Sans Symbols 2"),
871 FontFamilyName::named("Noto Sans Symbols2"),
872 FontFamilyName::named("Noto Sans Symbols"),
873 ],
874 "emoji" => &[
875 FontFamilyName::named("Noto Color Emoji"),
876 GenericFamily::Emoji.into(),
877 GenericFamily::SansSerif.into(),
878 FontFamilyName::named("Noto Sans Symbols 2"),
879 ],
880 "serif" => &[
881 GenericFamily::Serif.into(),
882 GenericFamily::SansSerif.into(),
883 FontFamilyName::named("Noto Sans Symbols 2"),
884 ],
885 "cursive" => &[
886 GenericFamily::Cursive.into(),
887 GenericFamily::SansSerif.into(),
888 FontFamilyName::named("Noto Sans Symbols 2"),
889 ],
890 "fantasy" => &[
891 GenericFamily::Fantasy.into(),
892 GenericFamily::SansSerif.into(),
893 FontFamilyName::named("Noto Sans Symbols 2"),
894 ],
895 "system-ui" => &[
896 GenericFamily::SystemUi.into(),
897 GenericFamily::SansSerif.into(),
898 FontFamilyName::named("Noto Sans Symbols 2"),
899 ],
900 "math" => &[
901 GenericFamily::Math.into(),
902 GenericFamily::SansSerif.into(),
903 FontFamilyName::named("Noto Sans Symbols 2"),
904 ],
905 _ => &[FontFamilyName::named(family)],
906 };
907 builder.push(names, 0..text.len());
908 }
909 } else {
910 #[cfg(target_arch = "wasm32")]
911 {
912 use parley::style::{FontFamilyName, GenericFamily};
913 let fallback: &[FontFamilyName] = &[
914 GenericFamily::SansSerif.into(),
915 GenericFamily::Emoji.into(),
916 FontFamilyName::named("Noto Color Emoji"),
917 FontFamilyName::named("Noto Sans Symbols 2"),
918 FontFamilyName::named("Noto Sans Symbols2"),
919 FontFamilyName::named("Noto Sans Symbols"),
920 ];
921 builder.push(fallback, 0..text.len());
922 }
923 }
924
925 let mut layout = builder.build(text);
926 layout.break_all_lines(None);
927 layout.align(
928 parley::Alignment::Start,
929 parley::AlignmentOptions::default(),
930 );
931
932 #[cfg(target_arch = "wasm32")]
933 {
934 let unresolved = collect_unresolved_codepoints(&layout, text);
935 let unresolved: Vec<u32> = unresolved
937 .into_iter()
938 .filter(|cp| {
939 !((0xE000..=0xF8FF).contains(cp)
940 || (0xF0000..=0xFFFFD).contains(cp)
941 || (0x100000..=0x10FFFD).contains(cp))
942 })
943 .collect();
944 if !unresolved.is_empty() {
945 let reg = crate::unresolved::web_unresolved_registry();
946 let is_new = unresolved.iter().any(|cp| !reg.contains(*cp));
947 if is_new {
948 crate::fallback::wasm_fallback::ensure_fallback_initialized();
949 reg.add_unresolved_vec(unresolved);
950 }
951 }
952 }
953
954 let mut out: Vec<ShapedGlyph> = Vec::new();
955 for line in layout.lines() {
956 for item in line.items() {
957 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
958 continue;
959 };
960 let font_data = glyph_run.run().font();
961 let fid = eng.ensure_font(font_data);
962 log::debug!(
963 "[shape] run: fid={} font_data_len={}",
964 fid,
965 font_data.data.as_ref().len()
966 );
967 for g in glyph_run.positioned_glyphs() {
968 let gid = g.id;
969 let key = key_from_pair(fid, gid);
970 eng.key_map.insert(key, (fid, gid));
971
972 let (w, h, left, top) = eng
973 .raster_placement(fid, gid, px)
974 .unwrap_or((0.0, 0.0, 0.0, 0.0));
975
976 log::debug!(
977 "[shape] glyph: gid={} px={} x={:.1} y={:.1} advance={:.1} bitmap={}x{} {}x{}",
978 gid,
979 px,
980 g.x,
981 g.y,
982 g.advance,
983 w,
984 h,
985 left,
986 top,
987 );
988
989 out.push(ShapedGlyph {
990 key,
991 px,
992 x: g.x,
993 y: g.y,
994 w,
995 h,
996 bearing_x: left,
997 bearing_y: top,
998 advance: g.advance + letter_spacing,
999 });
1000 }
1001 }
1002 }
1003 out
1004}
1005
1006pub fn shape_line(
1007 text: &str,
1008 px: f32,
1009 line_height_ratio: f32,
1010 font_family: Option<&str>,
1011 font_weight: u16,
1012 font_style: u8,
1013 letter_spacing: f32,
1014 font_variation_settings: Option<&str>,
1015) -> Vec<ShapedGlyph> {
1016 let mut eng = engine().lock().unwrap();
1017 shape_line_inner(
1018 &mut eng,
1019 text,
1020 px,
1021 line_height_ratio,
1022 font_family,
1023 font_weight,
1024 font_style,
1025 letter_spacing,
1026 font_variation_settings,
1027 )
1028}
1029
1030pub fn rasterize(key: GlyphKey, px: f32) -> Option<GlyphBitmap> {
1031 use swash::scale::{Render, Source, StrikeWith};
1032 let mut eng = engine().lock().unwrap();
1033 let &(fid, gid) = eng.key_map.get(&key)?;
1034 let cache_key = (fid, gid, px.to_bits());
1035 if let Some(cached) = eng.glyph_cache.get(&cache_key) {
1036 log::debug!(
1037 "[rasterize] HIT fid={} gid={} px={} => {}x{}",
1038 fid,
1039 gid,
1040 px,
1041 cached.0,
1042 cached.1
1043 );
1044 return Some(GlyphBitmap {
1045 key,
1046 w: cached.0,
1047 h: cached.1,
1048 content: cached.4,
1049 data: cached.5.clone(),
1050 });
1051 }
1052 let data_bytes = eng
1053 .font_registry
1054 .iter()
1055 .find(|r| r.id == fid)?
1056 .data_bytes
1057 .clone();
1058 let font = swash::FontRef::from_index(&data_bytes, 0)?;
1059 let mut scaler = eng.swash_cx.builder(font).size(px).hint(true).build();
1060 let image = Render::new(&[
1061 Source::Outline,
1062 Source::ColorBitmap(StrikeWith::BestFit),
1063 Source::ColorOutline(0),
1064 ])
1065 .render(&mut scaler, swash_glyph_id(fid, gid))?;
1066 log::debug!(
1067 "[rasterize] MISS fid={} gid={} px={} => {}x{}",
1068 fid,
1069 gid,
1070 px,
1071 image.placement.width,
1072 image.placement.height
1073 );
1074 let bitmap = GlyphBitmap {
1075 key,
1076 w: image.placement.width,
1077 h: image.placement.height,
1078 content: image.content,
1079 data: image.data,
1080 };
1081 eng.glyph_cache.insert(
1082 cache_key,
1083 (
1084 bitmap.w,
1085 bitmap.h,
1086 image.placement.left,
1087 image.placement.top,
1088 bitmap.content,
1089 bitmap.data.clone(),
1090 ),
1091 );
1092 eng.trim_glyph_cache();
1093 Some(bitmap)
1094}
1095
1096pub fn lookup_cache_key(key: GlyphKey, px: f32) -> Option<CacheKey> {
1097 let eng = engine().lock().unwrap();
1098 let &(fid, gid) = eng.key_map.get(&key)?;
1099 Some(CacheKey {
1100 font_id: fid,
1101 glyph_id: gid,
1102 font_size_bits: px.to_bits(),
1103 })
1104}
1105
1106fn extract_outlines_for(data_bytes: &[u8], glyph_id: u32) -> Option<Box<[Command]>> {
1107 let font = skrifa::FontRef::new(data_bytes).ok()?;
1108 let mut pen = OutlinePenCollector(Vec::new());
1109 font.outline_glyphs()
1110 .get(skrifa::GlyphId::new(glyph_id))?
1111 .draw(skrifa::instance::Size::new(1.0), &mut pen)
1112 .ok()?;
1113 Some(pen.0.into_boxed_slice())
1114}
1115
1116pub fn extract_outline_commands(cache_key: CacheKey) -> Option<Box<[Command]>> {
1117 let eng = engine().lock().unwrap();
1118 let record = eng
1119 .font_registry
1120 .iter()
1121 .find(|r| r.id == cache_key.font_id)?;
1122 extract_outlines_for(&record.data_bytes, cache_key.glyph_id)
1123}
1124
1125pub fn lookup_and_extract_outline(key: GlyphKey, px: f32) -> Option<(CacheKey, Box<[Command]>)> {
1126 let eng = engine().lock().unwrap();
1127 let &(fid, gid) = eng.key_map.get(&key)?;
1128 let record = eng.font_registry.iter().find(|r| r.id == fid)?;
1129 let ck = CacheKey {
1130 font_id: fid,
1131 glyph_id: gid,
1132 font_size_bits: px.to_bits(),
1133 };
1134 let cmds = extract_outlines_for(&record.data_bytes, gid)?;
1135 Some((ck, cmds))
1136}
1137
1138struct OutlinePenCollector(Vec<Command>);
1139
1140impl OutlinePen for OutlinePenCollector {
1141 fn move_to(&mut self, x: f32, y: f32) {
1142 self.0.push(Command::MoveTo(x, y));
1143 }
1144 fn line_to(&mut self, x: f32, y: f32) {
1145 self.0.push(Command::LineTo(x, y));
1146 }
1147 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1148 self.0.push(Command::QuadTo(cx0, cy0, x, y));
1149 }
1150 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1151 self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y));
1152 }
1153 fn close(&mut self) {
1154 self.0.push(Command::Close);
1155 }
1156}
1157
1158#[derive(Clone)]
1159pub struct TextMetrics {
1160 pub positions: Vec<f32>,
1161 pub byte_offsets: Vec<usize>,
1162}
1163
1164pub fn metrics_for_textfield(
1165 text: &str,
1166 px: f32,
1167 font_family: Option<&str>,
1168 font_weight: u16,
1169 font_style: u8,
1170 letter_spacing: f32,
1171 font_variation_settings: Option<&str>,
1172) -> TextMetrics {
1173 let family_hash = font_family.map(fast_hash).unwrap_or(0);
1174 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1175 let key = (
1176 fast_hash(text),
1177 (px * 100.0) as u32,
1178 family_hash,
1179 font_weight,
1180 font_style,
1181 (letter_spacing * 100.0) as i32,
1182 fvs_hash,
1183 );
1184 if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
1185 return m;
1186 }
1187 let mut eng = engine().lock().unwrap();
1188
1189 use parley::FontWeight;
1190 use parley::style::StyleProperty;
1191
1192 let Engine {
1193 ref mut font_cx,
1194 ref mut layout_cx,
1195 ..
1196 } = *eng;
1197 let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
1198 builder.push_default(StyleProperty::FontSize(px));
1199 builder.push_default(StyleProperty::FontWeight(FontWeight::new(
1200 font_weight as f32,
1201 )));
1202 builder.push_default(StyleProperty::FontStyle(match font_style {
1203 1 => parley::FontStyle::Italic,
1204 _ => parley::FontStyle::Normal,
1205 }));
1206 builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
1207 if let Some(settings) = font_variation_settings {
1208 builder.push_default(StyleProperty::FontVariations(
1209 parley::style::FontVariations::from(settings),
1210 ));
1211 }
1212 if let Some(family) = font_family {
1213 #[cfg(not(target_arch = "wasm32"))]
1214 {
1215 use parley::style::{FontFamilyName, GenericFamily};
1216 let names: &[FontFamilyName] = match family {
1217 "monospace" => &[
1218 FontFamilyName::named("JetBrains Mono"),
1219 GenericFamily::Monospace.into(),
1220 ],
1221 "sans-serif" => &[
1222 FontFamilyName::named("Open Sans"),
1223 GenericFamily::SansSerif.into(),
1224 ],
1225 "emoji" => &[
1226 FontFamilyName::named("Noto Color Emoji"),
1227 GenericFamily::Emoji.into(),
1228 ],
1229 "serif" => &[GenericFamily::Serif.into()],
1230 "cursive" => &[GenericFamily::Cursive.into()],
1231 "fantasy" => &[GenericFamily::Fantasy.into()],
1232 "system-ui" => &[GenericFamily::SystemUi.into()],
1233 "math" => &[GenericFamily::Math.into()],
1234 _ => &[FontFamilyName::named(family)],
1235 };
1236 builder.push(names, 0..text.len());
1237 }
1238 #[cfg(target_arch = "wasm32")]
1239 {
1240 use parley::style::{FontFamilyName, GenericFamily};
1241 let names: &[FontFamilyName] = match family {
1242 "monospace" => &[
1243 FontFamilyName::named("JetBrains Mono"),
1244 GenericFamily::Monospace.into(),
1245 GenericFamily::SansSerif.into(),
1246 FontFamilyName::named("Noto Sans Symbols 2"),
1247 FontFamilyName::named("Noto Sans Symbols2"),
1248 FontFamilyName::named("Noto Sans Symbols"),
1249 ],
1250 "sans-serif" => &[
1251 FontFamilyName::named("Open Sans"),
1252 GenericFamily::SansSerif.into(),
1253 GenericFamily::Emoji.into(),
1254 FontFamilyName::named("Noto Color Emoji"),
1255 FontFamilyName::named("Noto Sans Symbols 2"),
1256 FontFamilyName::named("Noto Sans Symbols2"),
1257 FontFamilyName::named("Noto Sans Symbols"),
1258 ],
1259 "emoji" => &[
1260 FontFamilyName::named("Noto Color Emoji"),
1261 GenericFamily::Emoji.into(),
1262 GenericFamily::SansSerif.into(),
1263 FontFamilyName::named("Noto Sans Symbols 2"),
1264 ],
1265 "serif" => &[
1266 GenericFamily::Serif.into(),
1267 GenericFamily::SansSerif.into(),
1268 FontFamilyName::named("Noto Sans Symbols 2"),
1269 ],
1270 "cursive" => &[
1271 GenericFamily::Cursive.into(),
1272 GenericFamily::SansSerif.into(),
1273 FontFamilyName::named("Noto Sans Symbols 2"),
1274 ],
1275 "fantasy" => &[
1276 GenericFamily::Fantasy.into(),
1277 GenericFamily::SansSerif.into(),
1278 FontFamilyName::named("Noto Sans Symbols 2"),
1279 ],
1280 "system-ui" => &[
1281 GenericFamily::SystemUi.into(),
1282 GenericFamily::SansSerif.into(),
1283 FontFamilyName::named("Noto Sans Symbols 2"),
1284 ],
1285 "math" => &[
1286 GenericFamily::Math.into(),
1287 GenericFamily::SansSerif.into(),
1288 FontFamilyName::named("Noto Sans Symbols 2"),
1289 ],
1290 _ => &[FontFamilyName::named(family)],
1291 };
1292 builder.push(names, 0..text.len());
1293 }
1294 } else {
1295 #[cfg(target_arch = "wasm32")]
1296 {
1297 use parley::style::{FontFamilyName, GenericFamily};
1298 let fallback: &[FontFamilyName] = &[
1299 GenericFamily::SansSerif.into(),
1300 GenericFamily::Emoji.into(),
1301 FontFamilyName::named("Noto Color Emoji"),
1302 FontFamilyName::named("Noto Sans Symbols 2"),
1303 FontFamilyName::named("Noto Sans Symbols2"),
1304 FontFamilyName::named("Noto Sans Symbols"),
1305 ];
1306 builder.push(fallback, 0..text.len());
1307 }
1308 }
1309
1310 let mut layout = builder.build(text);
1311 layout.break_all_lines(None);
1312 layout.align(
1313 parley::Alignment::Start,
1314 parley::AlignmentOptions::default(),
1315 );
1316
1317 #[cfg(target_arch = "wasm32")]
1318 {
1319 let unresolved = collect_unresolved_codepoints(&layout, text);
1320 let unresolved: Vec<u32> = unresolved
1321 .into_iter()
1322 .filter(|cp| {
1323 !((0xE000..=0xF8FF).contains(cp)
1324 || (0xF0000..=0xFFFFD).contains(cp)
1325 || (0x100000..=0x10FFFD).contains(cp))
1326 })
1327 .collect();
1328 if !unresolved.is_empty() {
1329 let reg = crate::unresolved::web_unresolved_registry();
1330 let is_new = unresolved.iter().any(|cp| !reg.contains(*cp));
1331 if is_new {
1332 crate::fallback::wasm_fallback::ensure_fallback_initialized();
1333 reg.add_unresolved_vec(unresolved);
1334 }
1335 }
1336 }
1337
1338 let mut edges: Vec<(usize, f32)> = Vec::new();
1339 let mut last_x = 0.0f32;
1340 let mut glyph_idx = 0usize;
1341 for line in layout.lines() {
1342 for item in line.items() {
1343 let parley::layout::PositionedLayoutItem::GlyphRun(glyph_run) = item else {
1344 continue;
1345 };
1346 let run_offset = glyph_run.offset();
1347 let run = glyph_run.run();
1348 let mut cluster_offset = run_offset;
1349 for cluster in run.clusters() {
1350 let range = cluster.text_range();
1351 for g in cluster.glyphs() {
1352 let shift = glyph_idx as f32 * letter_spacing;
1353 let x_pos = cluster_offset + g.x;
1354 let right = x_pos + shift + g.advance + letter_spacing;
1355 last_x = right.max(last_x);
1356 edges.push((range.end, right));
1357 glyph_idx += 1;
1358 cluster_offset += g.advance;
1359 }
1360 }
1361 }
1362 }
1363 if edges.last().map(|e| e.0) != Some(text.len()) {
1364 edges.push((text.len(), last_x));
1365 }
1366
1367 let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
1368 let mut byte_offsets = Vec::with_capacity(positions.capacity());
1369 positions.push(0.0);
1370 byte_offsets.push(0);
1371 let mut last_byte = 0usize;
1372 for (b, _) in text.grapheme_indices(true) {
1373 if b == 0 {
1375 continue;
1376 }
1377 positions
1378 .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
1379 byte_offsets.push(b);
1380 last_byte = b;
1381 }
1382 if *byte_offsets.last().unwrap_or(&0) != text.len() {
1383 positions.push(
1384 positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
1385 );
1386 byte_offsets.push(text.len());
1387 }
1388 let m = TextMetrics {
1389 positions,
1390 byte_offsets,
1391 };
1392 metrics_cache().lock().unwrap().put(key, m.clone());
1393 m
1394}
1395
1396fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
1397 let x0 = lookup_right(edges, start_b);
1398 let x1 = lookup_right(edges, end_b);
1399 (x1 - x0).max(0.0)
1400}
1401fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
1402 match edges.binary_search_by_key(&b, |e| e.0) {
1403 Ok(i) => edges[i].1,
1404 Err(i) => {
1405 if i == 0 {
1406 0.0
1407 } else {
1408 edges[i - 1].1
1409 }
1410 }
1411 }
1412}
1413
1414fn wrap_single_hard_line(
1415 text: &str,
1416 px: f32,
1417 max_width: f32,
1418 max_lines: Option<usize>,
1419 font_weight: u16,
1420 font_style: u8,
1421 letter_spacing: f32,
1422 font_variation_settings: Option<&str>,
1423) -> (Vec<String>, bool) {
1424 let soft_wrap = true;
1425 let max_lines_key: u16 = match max_lines {
1426 None => 0,
1427 Some(n) => {
1428 let n = n.min(u16::MAX as usize - 1) as u16;
1429 n.saturating_add(1)
1430 }
1431 };
1432 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1433 let key = (
1434 fast_hash(text),
1435 (px * 100.0) as u32,
1436 (max_width * 100.0) as u32,
1437 max_lines_key,
1438 soft_wrap,
1439 font_weight,
1440 font_style,
1441 (letter_spacing * 100.0) as i32,
1442 fvs_hash,
1443 );
1444 if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
1445 return h;
1446 }
1447
1448 let m = metrics_for_textfield(
1449 text,
1450 px,
1451 None,
1452 font_weight,
1453 font_style,
1454 letter_spacing,
1455 font_variation_settings,
1456 );
1457 if let Some(&last) = m.positions.last()
1458 && last <= max_width + 0.5
1459 {
1460 return (vec![text.to_string()], false);
1461 }
1462
1463 let width_of = |start_b: usize, end_b: usize| -> f32 {
1464 let i0 = match m.byte_offsets.binary_search(&start_b) {
1465 Ok(i) | Err(i) => i,
1466 };
1467 let i1 = match m.byte_offsets.binary_search(&end_b) {
1468 Ok(i) | Err(i) => i,
1469 };
1470 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
1471 .max(0.0)
1472 };
1473
1474 let mut out: Vec<String> = Vec::new();
1475 let mut truncated = false;
1476
1477 let mut line_start = 0usize;
1478 let mut best_break = line_start;
1479
1480 for tok in text.split_word_bounds() {
1481 let tok_start = best_break;
1482 let tok_end = tok_start + tok.len();
1483 let w = width_of(line_start, tok_end);
1484
1485 if w <= max_width + 0.5 {
1486 best_break = tok_end;
1487 continue;
1488 }
1489
1490 if best_break > line_start {
1491 out.push(text[line_start..best_break].trim_end().to_string());
1492 line_start = best_break;
1493 } else {
1494 let mut cut = tok_start;
1495 for g in tok.grapheme_indices(true) {
1496 let next = tok_start + g.0 + g.1.len();
1497 if width_of(line_start, next) <= max_width + 0.5 {
1498 cut = next;
1499 } else {
1500 break;
1501 }
1502 }
1503 if cut == line_start
1504 && let Some((ofs, grapheme)) = tok.grapheme_indices(true).next()
1505 {
1506 cut = tok_start + ofs + grapheme.len();
1507 }
1508 out.push(text[line_start..cut].to_string());
1509 line_start = cut;
1510 }
1511
1512 if let Some(ml) = max_lines
1513 && out.len() >= ml
1514 {
1515 truncated = true;
1516 line_start = line_start.min(text.len());
1517 break;
1518 }
1519
1520 best_break = line_start;
1521
1522 if line_start < tok_end && width_of(line_start, tok_end) <= max_width + 0.5 {
1523 best_break = tok_end;
1524 }
1525 }
1526
1527 if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
1528 out.push(text[line_start..].trim_end().to_string());
1529 }
1530
1531 let res = (out, truncated);
1532
1533 wrap_cache().lock().unwrap().put(key, res.clone());
1534 res
1535}
1536
1537pub fn wrap_lines(
1538 text: &str,
1539 px: f32,
1540 max_width: f32,
1541 max_lines: Option<usize>,
1542 soft_wrap: bool,
1543 font_weight: u16,
1544 font_style: u8,
1545 letter_spacing: f32,
1546 font_variation_settings: Option<&str>,
1547) -> (Vec<String>, bool) {
1548 if text.is_empty() || max_width <= 0.0 {
1549 return (vec![String::new()], false);
1550 }
1551 if !soft_wrap {
1552 return (text.split('\n').map(|s| s.to_string()).collect(), false);
1553 }
1554
1555 let mut combined: Vec<String> = Vec::new();
1556 let mut any_truncated = false;
1557 for (seg_i, seg) in text.split('\n').enumerate() {
1558 let remaining = max_lines.map(|ml| ml.saturating_sub(combined.len()));
1559 if max_lines.is_some_and(|ml| combined.len() >= ml) {
1560 any_truncated = true;
1561 break;
1562 }
1563 let (mut lines, trunc) = wrap_single_hard_line(
1564 seg,
1565 px,
1566 max_width,
1567 remaining,
1568 font_weight,
1569 font_style,
1570 letter_spacing,
1571 font_variation_settings,
1572 );
1573 if lines.is_empty() {
1574 lines.push(String::new());
1575 }
1576 combined.append(&mut lines);
1577 any_truncated |= trunc;
1578 let _ = seg_i;
1579 }
1580 return (combined, any_truncated);
1581}
1582
1583pub fn wrap_line_ranges(
1584 text: &str,
1585 px: f32,
1586 max_width: f32,
1587 max_lines: Option<usize>,
1588 soft_wrap: bool,
1589 font_weight: u16,
1590 font_style: u8,
1591 letter_spacing: f32,
1592 font_variation_settings: Option<&str>,
1593) -> (Vec<(usize, usize)>, bool) {
1594 if text.is_empty() || max_width <= 0.0 {
1595 return (vec![(0, 0)], false);
1596 }
1597 if !soft_wrap {
1598 let mut out = Vec::new();
1599 let mut start = 0usize;
1600 for (i, ch) in text.char_indices() {
1601 if ch == '\n' {
1602 out.push((start, i));
1603 start = i + 1;
1604 }
1605 }
1606 out.push((start, text.len()));
1607 return (out, false);
1608 }
1609
1610 let max_lines_key: u16 = match max_lines {
1611 None => 0,
1612 Some(n) => {
1613 let n = n.min(u16::MAX as usize - 1) as u16;
1614 n.saturating_add(1)
1615 }
1616 };
1617 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1618 let key = (
1619 fast_hash(text),
1620 (px * 100.0) as u32,
1621 (max_width * 100.0) as u32,
1622 max_lines_key,
1623 soft_wrap,
1624 font_weight,
1625 font_style,
1626 (letter_spacing * 100.0) as i32,
1627 fvs_hash,
1628 );
1629 if let Some(v) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
1630 return v;
1631 }
1632
1633 let m = metrics_for_textfield(
1634 text,
1635 px,
1636 None,
1637 font_weight,
1638 font_style,
1639 letter_spacing,
1640 font_variation_settings,
1641 );
1642
1643 let width_of = |start_b: usize, end_b: usize| -> f32 {
1644 let i0 = match m.byte_offsets.binary_search(&start_b) {
1645 Ok(i) | Err(i) => i,
1646 };
1647 let i1 = match m.byte_offsets.binary_search(&end_b) {
1648 Ok(i) | Err(i) => i,
1649 };
1650 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
1651 .max(0.0)
1652 };
1653
1654 let mut out: Vec<(usize, usize)> = Vec::new();
1655 let mut truncated = false;
1656
1657 let mut line0_start = 0usize;
1658 for (i, ch) in text.char_indices() {
1659 if ch == '\n' {
1660 let (mut ranges, tr) = wrap_one_hard_line_ranges(
1661 text,
1662 line0_start,
1663 i,
1664 max_width,
1665 max_lines.map(|ml| ml.saturating_sub(out.len())),
1666 &width_of,
1667 );
1668 out.append(&mut ranges);
1669 if tr {
1670 truncated = true;
1671 break;
1672 }
1673 line0_start = i + 1;
1674
1675 if let Some(ml) = max_lines
1676 && out.len() >= ml
1677 {
1678 truncated = true;
1679 break;
1680 }
1681 }
1682 }
1683 if !truncated {
1684 let (mut ranges, tr) = wrap_one_hard_line_ranges(
1685 text,
1686 line0_start,
1687 text.len(),
1688 max_width,
1689 max_lines.map(|ml| ml.saturating_sub(out.len())),
1690 &width_of,
1691 );
1692 out.append(&mut ranges);
1693 truncated = tr;
1694 }
1695
1696 if out.is_empty() {
1697 out.push((0, 0));
1698 }
1699
1700 let res = (out, truncated);
1701 wrap_ranges_cache().lock().unwrap().put(key, res.clone());
1702 res
1703}
1704
1705fn wrap_one_hard_line_ranges(
1706 text: &str,
1707 start: usize,
1708 end: usize,
1709 max_width: f32,
1710 max_lines: Option<usize>,
1711 width_of: &dyn Fn(usize, usize) -> f32,
1712) -> (Vec<(usize, usize)>, bool) {
1713 let mut out = Vec::new();
1714 let mut t = false;
1715
1716 if start >= end {
1717 out.push((start, start));
1718 return (out, false);
1719 }
1720
1721 if width_of(start, end) <= max_width + 0.5 {
1722 out.push((start, end));
1723 return (out, false);
1724 }
1725
1726 let mut line_start = start;
1727 let mut best_break = line_start;
1728 let mut unconsumed_start = start;
1729
1730 for tok in text[line_start..end].split_word_bounds() {
1731 let tok_abs_start = unconsumed_start;
1732 let tok_abs_end = tok_abs_start + tok.len();
1733 unconsumed_start = tok_abs_end;
1734
1735 let w = width_of(line_start, tok_abs_end);
1736 if w <= max_width + 0.5 {
1737 best_break = tok_abs_end;
1738 continue;
1739 }
1740
1741 if best_break > line_start {
1742 out.push((line_start, best_break));
1743 line_start = best_break;
1744 } else {
1745 let mut cut = tok_abs_start;
1746 for (ofs, g) in tok.grapheme_indices(true) {
1747 let next = tok_abs_start + ofs + g.len();
1748 if width_of(line_start, next) <= max_width + 0.5 {
1749 cut = next;
1750 } else {
1751 break;
1752 }
1753 }
1754 if cut == line_start
1755 && let Some((ofs, gr)) = tok.grapheme_indices(true).next()
1756 {
1757 cut = tok_abs_start + ofs + gr.len();
1758 }
1759 out.push((line_start, cut));
1760 line_start = cut;
1761 }
1762
1763 if let Some(ml) = max_lines
1764 && out.len() >= ml
1765 {
1766 t = true;
1767 break;
1768 }
1769
1770 best_break = line_start;
1771 }
1772
1773 if !t && line_start < end && max_lines.is_none_or(|ml| out.len() < ml) {
1774 out.push((line_start, end));
1775 }
1776
1777 (out, t)
1778}
1779
1780pub fn ellipsize_line(
1781 text: &str,
1782 px: f32,
1783 max_width: f32,
1784 font_weight: u16,
1785 font_style: u8,
1786 letter_spacing: f32,
1787 font_variation_settings: Option<&str>,
1788) -> String {
1789 if text.is_empty() || max_width <= 0.0 {
1790 return String::new();
1791 }
1792 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1793 let key = (
1794 fast_hash(text),
1795 (px * 100.0) as u32,
1796 (max_width * 100.0) as u32,
1797 font_weight,
1798 font_style,
1799 (letter_spacing * 100.0) as i32,
1800 fvs_hash,
1801 );
1802 if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
1803 return s;
1804 }
1805 let m = metrics_for_textfield(
1806 text,
1807 px,
1808 None,
1809 font_weight,
1810 font_style,
1811 letter_spacing,
1812 font_variation_settings,
1813 );
1814 if let Some(&last) = m.positions.last()
1815 && last <= max_width + 0.5
1816 {
1817 return text.to_string();
1818 }
1819 let _el = "…";
1820 let e_w = ellipsis_width(px, letter_spacing);
1821 if e_w >= max_width {
1822 return String::new();
1823 }
1824 let mut cut_i = 0usize;
1825 for i in 0..m.positions.len() {
1826 if m.positions[i] + e_w <= max_width {
1827 cut_i = i;
1828 } else {
1829 break;
1830 }
1831 }
1832 let byte = m
1833 .byte_offsets
1834 .get(cut_i)
1835 .copied()
1836 .unwrap_or(0)
1837 .min(text.len());
1838 let mut out = String::with_capacity(byte + 3);
1839 out.push_str(&text[..byte]);
1840 out.push('…');
1841
1842 let s = out;
1843 ellip_cache().lock().unwrap().put(key, s.clone());
1844
1845 s
1846}
1847
1848fn ellipsis_width(px: f32, letter_spacing: f32) -> f32 {
1849 static ELLIP_W_LRU: OnceLock<Mutex<Lru<(u32, i32), f32>>> = OnceLock::new();
1850 static ELLIP_W_GEN: AtomicU64 = AtomicU64::new(0);
1855 let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
1856 let generation = font_generation();
1857 if ELLIP_W_GEN.load(Ordering::Relaxed) != generation {
1858 if let Ok(mut g) = cache.lock() {
1859 g.clear_both();
1860 }
1861 ELLIP_W_GEN.store(generation, Ordering::Relaxed);
1862 }
1863 let key = ((px * 100.0) as u32, (letter_spacing * 100.0) as i32);
1864 if let Some(w) = cache.lock().unwrap().get(&key).copied() {
1865 return w;
1866 }
1867 let w = if let Some(g) =
1868 crate::shape_line("…", px, px, None, 400, 0, letter_spacing, None).last()
1869 {
1870 g.x + g.advance
1871 } else {
1872 0.0
1873 };
1874 cache.lock().unwrap().put(key, w);
1875 w
1876}