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