1use font_awl::FontProvider;
2use once_cell::sync::OnceCell;
3use rapidhash::{HashMapExt, RapidHashMap, fast::RapidHasher};
4use skrifa::MetadataProvider;
5use skrifa::outline::OutlinePen;
6
7pub mod fallback;
8pub mod fallback_data;
9
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::{
12 collections::{HashMap, VecDeque},
13 hash::{Hash, Hasher},
14 sync::Mutex,
15};
16use unicode_segmentation::UnicodeSegmentation;
17
18static FRAME_COUNTER: AtomicU64 = AtomicU64::new(0);
19static FALLBACK_DIRTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
20
21pub fn begin_frame() {
22 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
23}
24
25pub fn current_frame() -> u64 {
26 FRAME_COUNTER.load(Ordering::Relaxed)
27}
28
29pub fn take_fallback_dirty() -> bool {
30 FALLBACK_DIRTY.swap(false, Ordering::Relaxed)
31}
32
33const GLYPH_CACHE_CAP: usize = 4096;
34const WRAP_CACHE_CAP: usize = 1024;
35const ELLIP_CACHE_CAP: usize = 2048;
36
37static METRICS_LRU: OnceCell<Mutex<Lru<(u64, u32, u64, u16, u8, i32, u64), TextMetrics>>> =
38 OnceCell::new();
39fn metrics_cache() -> &'static Mutex<Lru<(u64, u32, u64, u16, u8, i32, u64), TextMetrics>> {
40 METRICS_LRU.get_or_init(|| Mutex::new(Lru::new(4096)))
41}
42
43struct Lru<K, V> {
44 map: RapidHashMap<K, V>,
45 order: VecDeque<K>,
46 cap: usize,
47}
48impl<K: std::hash::Hash + Eq + Clone, V> Lru<K, V> {
49 fn new(cap: usize) -> Self {
50 Self {
51 map: RapidHashMap::new(),
52 order: VecDeque::new(),
53 cap,
54 }
55 }
56 fn get(&mut self, k: &K) -> Option<&V> {
57 if self.map.contains_key(k)
58 && let Some(pos) = self.order.iter().position(|x| x == k)
59 {
60 let key = self.order.remove(pos).unwrap();
61 self.order.push_back(key);
62 }
63 self.map.get(k)
64 }
65 fn put(&mut self, k: K, v: V) {
66 if self.map.contains_key(&k) {
67 self.map.insert(k.clone(), v);
68 if let Some(pos) = self.order.iter().position(|x| x == &k) {
69 let key = self.order.remove(pos).unwrap();
70 self.order.push_back(key);
71 }
72 return;
73 }
74 if self.map.len() >= self.cap
75 && let Some(old) = self.order.pop_front()
76 {
77 self.map.remove(&old);
78 }
79 self.order.push_back(k.clone());
80 self.map.insert(k, v);
81 }
82}
83
84static WRAP_LRU: OnceCell<
85 Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<String>, bool)>>,
86> = OnceCell::new();
87
88static WRAP_RANGES_LRU: OnceCell<
89 Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<(usize, usize)>, bool)>>,
90> = OnceCell::new();
91
92static ELLIP_LRU: OnceCell<Mutex<Lru<(u64, u32, u32, u16, u8, i32, u64), String>>> =
93 OnceCell::new();
94
95fn wrap_cache()
96-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<String>, bool)>> {
97 WRAP_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
98}
99
100fn wrap_ranges_cache()
101-> &'static Mutex<Lru<(u64, u32, u32, u16, bool, u16, u8, i32, u64), (Vec<(usize, usize)>, bool)>> {
102 WRAP_RANGES_LRU.get_or_init(|| Mutex::new(Lru::new(WRAP_CACHE_CAP)))
103}
104
105fn ellip_cache() -> &'static Mutex<Lru<(u64, u32, u32, u16, u8, i32, u64), String>> {
106 ELLIP_LRU.get_or_init(|| Mutex::new(Lru::new(ELLIP_CACHE_CAP)))
107}
108
109fn fast_hash(s: &str) -> u64 {
110 let mut h = RapidHasher::default();
111 s.len().hash(&mut h);
112 s.hash(&mut h);
113 h.finish()
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub struct GlyphKey(pub u64);
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
122pub struct CacheKey {
123 pub font_id: u64,
124 pub glyph_id: u16,
125 pub font_size_bits: u32,
126}
127
128#[derive(Clone, Debug)]
130pub enum Command {
131 MoveTo(f32, f32),
132 LineTo(f32, f32),
133 QuadTo(f32, f32, f32, f32),
134 CurveTo(f32, f32, f32, f32, f32, f32),
135 Close,
136}
137
138pub struct ShapedGlyph {
139 pub key: GlyphKey,
140 pub px: f32,
141 pub x: f32,
142 pub y: f32,
143 pub w: f32,
144 pub h: f32,
145 pub bearing_x: f32,
146 pub bearing_y: f32,
147 pub advance: f32,
148}
149
150pub use swash::scale::image::Content as SwashContent;
151
152pub struct GlyphBitmap {
153 pub key: GlyphKey,
154 pub w: u32,
155 pub h: u32,
156 pub content: SwashContent,
157 pub data: Vec<u8>,
158}
159
160struct FontRecord {
161 id: u64,
162 data: parley::FontData,
163 data_bytes: Vec<u8>,
164}
165
166struct Engine {
167 font_cx: parley::FontContext,
168 layout_cx: parley::LayoutContext<()>,
169 swash_cx: swash::scale::ScaleContext,
170 key_map: HashMap<GlyphKey, (u64, u16)>,
171 font_registry: Vec<FontRecord>,
172 next_font_id: u64,
173 glyph_cache:
176 HashMap<(u64, u16, u32), (u32, u32, i32, i32, swash::scale::image::Content, Vec<u8>)>,
177}
178
179impl Engine {
180 fn ensure_font(&mut self, fd: &parley::FontData) -> u64 {
181 if let Some(existing) = self.font_registry.iter().find(|r| r.data == *fd) {
182 log::debug!(
183 "[font] reuse id={} len={}",
184 existing.id,
185 fd.data.as_ref().len()
186 );
187 return existing.id;
188 }
189 let id = self.next_font_id;
190 self.next_font_id += 1;
191 let bytes = fd.data.as_ref().to_vec();
192 log::debug!("[font] register id={} len={}", id, bytes.len());
193 self.font_registry.push(FontRecord {
194 id,
195 data: fd.clone(),
196 data_bytes: bytes,
197 });
198 id
199 }
200
201 fn trim_glyph_cache(&mut self) {
202 if self.glyph_cache.len() > GLYPH_CACHE_CAP {
203 let to_remove = self.glyph_cache.len() - GLYPH_CACHE_CAP;
204 let keys: Vec<_> = self.glyph_cache.keys().take(to_remove).copied().collect();
205 for k in keys {
206 self.glyph_cache.remove(&k);
207 }
208 }
209 }
210
211 fn raster_placement(
212 &mut self,
213 font_id: u64,
214 glyph_id: u16,
215 px: f32,
216 ) -> Option<(f32, f32, f32, f32)> {
217 use swash::scale::{Render, Source, StrikeWith};
218 let cache_key = (font_id, glyph_id, px.to_bits());
219 if let Some(cached) = self.glyph_cache.get(&cache_key) {
220 log::debug!(
221 "[raster_placement] HIT fid={} gid={} px={} => {}x{} {}x{}",
222 font_id,
223 glyph_id,
224 px,
225 cached.0,
226 cached.1,
227 cached.2,
228 cached.3
229 );
230 return Some((
231 cached.0 as f32,
232 cached.1 as f32,
233 cached.2 as f32,
234 cached.3 as f32,
235 ));
236 }
237 let data_bytes = self
238 .font_registry
239 .iter()
240 .find(|r| r.id == font_id)?
241 .data_bytes
242 .clone();
243 let font = swash::FontRef::from_index(&data_bytes, 0)?;
244 let mut scaler = self.swash_cx.builder(font).size(px).hint(true).build();
245 let image = Render::new(&[
246 Source::Outline,
247 Source::ColorBitmap(StrikeWith::BestFit),
248 Source::ColorOutline(0),
249 ])
250 .render(&mut scaler, glyph_id)?;
251 log::debug!(
252 "[raster_placement] MISS fid={} gid={} px={} => {}x{} {}x{}",
253 font_id,
254 glyph_id,
255 px,
256 image.placement.width,
257 image.placement.height,
258 image.placement.left,
259 image.placement.top
260 );
261 self.glyph_cache.insert(
262 cache_key,
263 (
264 image.placement.width,
265 image.placement.height,
266 image.placement.left,
267 image.placement.top,
268 image.content,
269 image.data,
270 ),
271 );
272 self.trim_glyph_cache();
273 Some((
274 image.placement.width as f32,
275 image.placement.height as f32,
276 image.placement.left as f32,
277 image.placement.top as f32,
278 ))
279 }
280}
281
282static ENGINE: OnceCell<Mutex<Engine>> = OnceCell::new();
283
284pub static FONT_PROVIDER: OnceCell<Mutex<font_awl::Provider>> = OnceCell::new();
285
286fn init_engine_sync() -> Engine {
287 let mut provider = font_awl::Provider::new();
288 provider.load_bundled_fonts();
289 #[cfg(not(target_arch = "wasm32"))]
290 if let Err(e) = provider.load_system_fonts_best_effort() {
291 log::warn!("font-awl: failed to load system fonts: {e}");
292 }
293
294 let mut font_cx = provider.new_parley_context();
295 let layout_cx = parley::LayoutContext::new();
296
297 static MATERIAL_SYMBOLS_TTF: &[u8] = include_bytes!("assets/MaterialSymbolsOutlined.ttf");
298 let blob: parley::fontique::Blob<u8> = MATERIAL_SYMBOLS_TTF.to_vec().into();
299 font_cx.collection.register_fonts(blob, None);
300
301 let _ = FONT_PROVIDER.set(Mutex::new(provider));
302
303 Engine {
304 font_cx,
305 layout_cx,
306 swash_cx: swash::scale::ScaleContext::new(),
307 key_map: HashMap::new(),
308 font_registry: Vec::new(),
309 next_font_id: 1,
310 glyph_cache: HashMap::new(),
311 }
312}
313
314#[cfg(target_arch = "wasm32")]
315pub async fn init_fonts_wasm() {
316 let mut provider = font_awl::Provider::new();
317 provider.load_bundled_fonts();
318 if let Err(e) = provider.load_web_fonts().await {
319 log::warn!("font-awl: failed to load web fonts: {e}");
320 }
321 let _ = FONT_PROVIDER.set(Mutex::new(provider));
322
323 if let Some(eng) = ENGINE.get() {
324 let mut eng = eng.lock().unwrap();
325 if let Some(provider_lock) = FONT_PROVIDER.get() {
328 let p = provider_lock.lock().unwrap();
329 eng.font_cx = p.new_parley_context();
330 }
331 }
332}
333
334fn engine() -> &'static Mutex<Engine> {
335 ENGINE.get_or_init(|| Mutex::new(init_engine_sync()))
336}
337
338pub fn register_font_data(bytes: &[u8]) {
339 let mut eng = engine().lock().unwrap();
340 let blob: parley::fontique::Blob<u8> = bytes.to_vec().into();
341 eng.font_cx.collection.register_fonts(blob.clone(), None);
342 if let Some(provider_lock) = FONT_PROVIDER.get() {
343 let mut p = provider_lock.lock().unwrap();
344 p.collection_mut().register_fonts(blob, None);
345 }
346 clear_caches_for_fallback();
348}
349
350pub(crate) fn clear_caches_for_fallback() {
351 if let Some(c) = METRICS_LRU.get() {
352 c.lock().unwrap().map.clear();
353 c.lock().unwrap().order.clear();
354 }
355 if let Some(c) = WRAP_LRU.get() {
356 c.lock().unwrap().map.clear();
357 c.lock().unwrap().order.clear();
358 }
359 if let Some(c) = WRAP_RANGES_LRU.get() {
360 c.lock().unwrap().map.clear();
361 c.lock().unwrap().order.clear();
362 }
363 if let Some(c) = ELLIP_LRU.get() {
364 c.lock().unwrap().map.clear();
365 c.lock().unwrap().order.clear();
366 }
367 bump_frame_for_fallback();
369}
370
371pub(crate) fn bump_frame_for_fallback() {
372 FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
373 FALLBACK_DIRTY.store(true, Ordering::Relaxed);
374}
375
376#[cfg(target_arch = "wasm32")]
377pub fn ensure_web_fallback_initialized() {
378 crate::fallback::wasm_fallback::ensure_fallback_initialized();
379}
380
381#[cfg(not(target_arch = "wasm32"))]
382pub fn ensure_web_fallback_initialized() {}
383
384pub fn load_font_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
388 let bytes = std::fs::read(path)?;
389 register_font_data(&bytes);
390 Ok(())
391}
392
393pub fn font_family_name(bytes: &[u8]) -> Option<String> {
398 use skrifa::string::StringId;
399 let font = skrifa::FontRef::new(bytes).ok()?;
400 font.localized_strings(StringId::TYPOGRAPHIC_FAMILY_NAME)
401 .english_or_first()
402 .map(|s| s.to_string())
403 .or_else(|| {
404 font.localized_strings(StringId::FAMILY_NAME)
405 .english_or_first()
406 .map(|s| s.to_string())
407 })
408}
409
410fn key_from_pair(font_id: u64, glyph_id: u16) -> GlyphKey {
411 let mut h = RapidHasher::default();
412 font_id.hash(&mut h);
413 glyph_id.hash(&mut h);
414 GlyphKey(h.finish())
415}
416
417#[cfg(target_arch = "wasm32")]
418fn collect_unresolved_codepoints(layout: &parley::Layout<()>, text: &str) -> Vec<u32> {
419 use parley::layout::PositionedLayoutItem;
420 let mut out = Vec::new();
421 for line in layout.lines() {
422 for item in line.items() {
423 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
424 continue;
425 };
426 let run = glyph_run.run();
427 for cluster in run.clusters() {
429 let has_missing = cluster.glyphs().any(|g| g.id == 0);
430 if has_missing {
431 let range = cluster.text_range();
432 let end = range.end.min(text.len());
434 let start = range.start.min(end);
435 for ch in text[start..end].chars() {
436 out.push(ch as u32);
437 }
438 if range.start == range.end {
440 }
442 }
443 }
444 }
445 }
446 out
447}
448
449fn shape_line_inner(
450 eng: &mut Engine,
451 text: &str,
452 px: f32,
453 line_height_ratio: f32,
454 font_family: Option<&str>,
455 font_weight: u16,
456 font_style: u8,
457 letter_spacing: f32,
458 font_variation_settings: Option<&str>,
459) -> Vec<ShapedGlyph> {
460 use parley::FontWeight;
461 use parley::layout::PositionedLayoutItem;
462 use parley::style::StyleProperty;
463
464 let Engine {
465 ref mut font_cx,
466 ref mut layout_cx,
467 ..
468 } = *eng;
469 let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
470 builder.push_default(StyleProperty::FontSize(px));
471 if line_height_ratio > 0.0 {
472 builder.push_default(StyleProperty::LineHeight(
473 parley::LineHeight::FontSizeRelative(line_height_ratio),
474 ));
475 }
476 builder.push_default(StyleProperty::FontWeight(FontWeight::new(
477 font_weight as f32,
478 )));
479 builder.push_default(StyleProperty::FontStyle(match font_style {
480 1 => parley::FontStyle::Italic,
481 _ => parley::FontStyle::Normal,
482 }));
483 builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
484
485 if let Some(settings) = font_variation_settings {
486 builder.push_default(StyleProperty::FontVariations(
487 parley::style::FontVariations::from(settings),
488 ));
489 }
490
491 if let Some(family) = font_family {
492 use parley::style::{FontFamilyName, GenericFamily};
493 let names: &[FontFamilyName] = match family {
494 "monospace" => &[
495 FontFamilyName::named("JetBrains Mono"),
496 GenericFamily::Monospace.into(),
497 ],
498 "sans-serif" => &[
499 FontFamilyName::named("Open Sans"),
500 GenericFamily::SansSerif.into(),
501 ],
502 "emoji" => &[
503 FontFamilyName::named("Noto Color Emoji"),
504 GenericFamily::Emoji.into(),
505 ],
506 "serif" => &[GenericFamily::Serif.into()],
507 "cursive" => &[GenericFamily::Cursive.into()],
508 "fantasy" => &[GenericFamily::Fantasy.into()],
509 "system-ui" => &[GenericFamily::SystemUi.into()],
510 "math" => &[GenericFamily::Math.into()],
511 _ => &[FontFamilyName::named(family)],
512 };
513 builder.push(names, 0..text.len());
514 }
515
516 let mut layout = builder.build(text);
517 layout.break_all_lines(None);
518 layout.align(
519 parley::Alignment::Start,
520 parley::AlignmentOptions::default(),
521 );
522
523 #[cfg(target_arch = "wasm32")]
525 {
526 let unresolved = collect_unresolved_codepoints(&layout, text);
527 if !unresolved.is_empty() {
528 crate::fallback::wasm_fallback::submit_unresolved(unresolved);
529 crate::fallback::wasm_fallback::ensure_fallback_initialized();
531 }
532 }
533
534 let mut out: Vec<ShapedGlyph> = Vec::new();
535 for line in layout.lines() {
536 for item in line.items() {
537 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
538 continue;
539 };
540 let font_data = glyph_run.run().font();
541 let fid = eng.ensure_font(font_data);
542 log::debug!(
543 "[shape] run: fid={} font_data_len={}",
544 fid,
545 font_data.data.as_ref().len()
546 );
547 for g in glyph_run.positioned_glyphs() {
548 let gid = g.id as u16;
549 let key = key_from_pair(fid, gid);
550 eng.key_map.insert(key, (fid, gid));
551
552 let (w, h, left, top) = eng
553 .raster_placement(fid, gid, px)
554 .unwrap_or((0.0, 0.0, 0.0, 0.0));
555
556 log::debug!(
557 "[shape] glyph: gid={} px={} x={:.1} y={:.1} advance={:.1} bitmap={}x{} {}x{}",
558 gid,
559 px,
560 g.x,
561 g.y,
562 g.advance,
563 w,
564 h,
565 left,
566 top,
567 );
568
569 out.push(ShapedGlyph {
570 key,
571 px,
572 x: g.x,
573 y: g.y,
574 w,
575 h,
576 bearing_x: left,
577 bearing_y: top,
578 advance: g.advance + letter_spacing,
579 });
580 }
581 }
582 }
583 out
584}
585
586pub fn shape_line(
587 text: &str,
588 px: f32,
589 line_height_ratio: f32,
590 font_family: Option<&str>,
591 font_weight: u16,
592 font_style: u8,
593 letter_spacing: f32,
594 font_variation_settings: Option<&str>,
595) -> Vec<ShapedGlyph> {
596 let mut eng = engine().lock().unwrap();
597 shape_line_inner(
598 &mut eng,
599 text,
600 px,
601 line_height_ratio,
602 font_family,
603 font_weight,
604 font_style,
605 letter_spacing,
606 font_variation_settings,
607 )
608}
609
610pub fn rasterize(key: GlyphKey, px: f32) -> Option<GlyphBitmap> {
611 use swash::scale::{Render, Source, StrikeWith};
612 let mut eng = engine().lock().unwrap();
613 let &(fid, gid) = eng.key_map.get(&key)?;
614 let cache_key = (fid, gid, px.to_bits());
615 if let Some(cached) = eng.glyph_cache.get(&cache_key) {
616 log::debug!(
617 "[rasterize] HIT fid={} gid={} px={} => {}x{}",
618 fid,
619 gid,
620 px,
621 cached.0,
622 cached.1
623 );
624 return Some(GlyphBitmap {
625 key,
626 w: cached.0,
627 h: cached.1,
628 content: cached.4,
629 data: cached.5.clone(),
630 });
631 }
632 let data_bytes = eng
633 .font_registry
634 .iter()
635 .find(|r| r.id == fid)?
636 .data_bytes
637 .clone();
638 let font = swash::FontRef::from_index(&data_bytes, 0)?;
639 let mut scaler = eng.swash_cx.builder(font).size(px).hint(true).build();
640 let image = Render::new(&[
641 Source::Outline,
642 Source::ColorBitmap(StrikeWith::BestFit),
643 Source::ColorOutline(0),
644 ])
645 .render(&mut scaler, gid)?;
646 log::debug!(
647 "[rasterize] MISS fid={} gid={} px={} => {}x{}",
648 fid,
649 gid,
650 px,
651 image.placement.width,
652 image.placement.height
653 );
654 let bitmap = GlyphBitmap {
655 key,
656 w: image.placement.width,
657 h: image.placement.height,
658 content: image.content,
659 data: image.data,
660 };
661 eng.glyph_cache.insert(
662 cache_key,
663 (
664 bitmap.w,
665 bitmap.h,
666 image.placement.left,
667 image.placement.top,
668 bitmap.content,
669 bitmap.data.clone(),
670 ),
671 );
672 eng.trim_glyph_cache();
673 Some(bitmap)
674}
675
676pub fn lookup_cache_key(key: GlyphKey, px: f32) -> Option<CacheKey> {
677 let eng = engine().lock().unwrap();
678 let &(fid, gid) = eng.key_map.get(&key)?;
679 Some(CacheKey {
680 font_id: fid,
681 glyph_id: gid,
682 font_size_bits: px.to_bits(),
683 })
684}
685
686fn extract_outlines_for(data_bytes: &[u8], glyph_id: u16) -> Option<Box<[Command]>> {
687 let font = skrifa::FontRef::new(data_bytes).ok()?;
688 let mut pen = OutlinePenCollector(Vec::new());
689 font.outline_glyphs()
690 .get(skrifa::GlyphId::new(glyph_id as u32))?
691 .draw(skrifa::instance::Size::new(1.0), &mut pen)
692 .ok()?;
693 Some(pen.0.into_boxed_slice())
694}
695
696pub fn extract_outline_commands(cache_key: CacheKey) -> Option<Box<[Command]>> {
697 let eng = engine().lock().unwrap();
698 let record = eng
699 .font_registry
700 .iter()
701 .find(|r| r.id == cache_key.font_id)?;
702 extract_outlines_for(&record.data_bytes, cache_key.glyph_id)
703}
704
705pub fn lookup_and_extract_outline(key: GlyphKey, px: f32) -> Option<(CacheKey, Box<[Command]>)> {
706 let eng = engine().lock().unwrap();
707 let &(fid, gid) = eng.key_map.get(&key)?;
708 let record = eng.font_registry.iter().find(|r| r.id == fid)?;
709 let ck = CacheKey {
710 font_id: fid,
711 glyph_id: gid,
712 font_size_bits: px.to_bits(),
713 };
714 let cmds = extract_outlines_for(&record.data_bytes, gid)?;
715 Some((ck, cmds))
716}
717
718struct OutlinePenCollector(Vec<Command>);
719
720impl OutlinePen for OutlinePenCollector {
721 fn move_to(&mut self, x: f32, y: f32) {
722 self.0.push(Command::MoveTo(x, y));
723 }
724 fn line_to(&mut self, x: f32, y: f32) {
725 self.0.push(Command::LineTo(x, y));
726 }
727 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
728 self.0.push(Command::QuadTo(cx0, cy0, x, y));
729 }
730 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
731 self.0.push(Command::CurveTo(cx0, cy0, cx1, cy1, x, y));
732 }
733 fn close(&mut self) {
734 self.0.push(Command::Close);
735 }
736}
737
738#[derive(Clone)]
739pub struct TextMetrics {
740 pub positions: Vec<f32>,
741 pub byte_offsets: Vec<usize>,
742}
743
744pub fn metrics_for_textfield(
745 text: &str,
746 px: f32,
747 font_family: Option<&str>,
748 font_weight: u16,
749 font_style: u8,
750 letter_spacing: f32,
751 font_variation_settings: Option<&str>,
752) -> TextMetrics {
753 let family_hash = font_family.map(fast_hash).unwrap_or(0);
754 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
755 let key = (
756 fast_hash(text),
757 (px * 100.0) as u32,
758 family_hash,
759 font_weight,
760 font_style,
761 (letter_spacing * 100.0) as i32,
762 fvs_hash,
763 );
764 if let Some(m) = metrics_cache().lock().unwrap().get(&key).cloned() {
765 return m;
766 }
767 let mut eng = engine().lock().unwrap();
768
769 use parley::FontWeight;
770 use parley::style::StyleProperty;
771
772 let Engine {
773 ref mut font_cx,
774 ref mut layout_cx,
775 ..
776 } = *eng;
777 let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
778 builder.push_default(StyleProperty::FontSize(px));
779 builder.push_default(StyleProperty::FontWeight(FontWeight::new(
780 font_weight as f32,
781 )));
782 builder.push_default(StyleProperty::FontStyle(match font_style {
783 1 => parley::FontStyle::Italic,
784 _ => parley::FontStyle::Normal,
785 }));
786 builder.push_default(StyleProperty::LetterSpacing(letter_spacing));
787 if let Some(settings) = font_variation_settings {
788 builder.push_default(StyleProperty::FontVariations(
789 parley::style::FontVariations::from(settings),
790 ));
791 }
792 if let Some(family) = font_family {
793 use parley::style::{FontFamilyName, GenericFamily};
794 let names: &[FontFamilyName] = match family {
795 "monospace" => &[
796 FontFamilyName::named("JetBrains Mono"),
797 GenericFamily::Monospace.into(),
798 ],
799 "sans-serif" => &[
800 FontFamilyName::named("Open Sans"),
801 GenericFamily::SansSerif.into(),
802 ],
803 "emoji" => &[
804 FontFamilyName::named("Noto Color Emoji"),
805 GenericFamily::Emoji.into(),
806 ],
807 "serif" => &[GenericFamily::Serif.into()],
808 "cursive" => &[GenericFamily::Cursive.into()],
809 "fantasy" => &[GenericFamily::Fantasy.into()],
810 "system-ui" => &[GenericFamily::SystemUi.into()],
811 "math" => &[GenericFamily::Math.into()],
812 _ => &[FontFamilyName::named(family)],
813 };
814 builder.push(names, 0..text.len());
815 }
816
817 let mut layout = builder.build(text);
818 layout.break_all_lines(None);
819 layout.align(
820 parley::Alignment::Start,
821 parley::AlignmentOptions::default(),
822 );
823
824 #[cfg(target_arch = "wasm32")]
825 {
826 let unresolved = collect_unresolved_codepoints(&layout, text);
827 if !unresolved.is_empty() {
828 crate::fallback::wasm_fallback::submit_unresolved(unresolved);
829 }
830 }
831
832 let mut edges: Vec<(usize, f32)> = Vec::new();
833 let mut last_x = 0.0f32;
834 let mut glyph_idx = 0usize;
835 for line in layout.lines() {
836 for item in line.items() {
837 let parley::layout::PositionedLayoutItem::GlyphRun(glyph_run) = item else {
838 continue;
839 };
840 let run_offset = glyph_run.offset();
841 let run = glyph_run.run();
842 let mut cluster_offset = run_offset;
843 for cluster in run.clusters() {
844 let range = cluster.text_range();
845 for g in cluster.glyphs() {
846 let shift = glyph_idx as f32 * letter_spacing;
847 let x_pos = cluster_offset + g.x;
848 let right = x_pos + shift + g.advance + letter_spacing;
849 last_x = right.max(last_x);
850 edges.push((range.end, right));
851 glyph_idx += 1;
852 cluster_offset += g.advance;
853 }
854 }
855 }
856 }
857 if edges.last().map(|e| e.0) != Some(text.len()) {
858 edges.push((text.len(), last_x));
859 }
860
861 let mut positions = Vec::with_capacity(text.graphemes(true).count() + 1);
862 let mut byte_offsets = Vec::with_capacity(positions.capacity());
863 positions.push(0.0);
864 byte_offsets.push(0);
865 let mut last_byte = 0usize;
866 for (b, _) in text.grapheme_indices(true) {
867 positions
868 .push(positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, b));
869 byte_offsets.push(b);
870 last_byte = b;
871 }
872 if *byte_offsets.last().unwrap_or(&0) != text.len() {
873 positions.push(
874 positions.last().copied().unwrap_or(0.0) + width_between(&edges, last_byte, text.len()),
875 );
876 byte_offsets.push(text.len());
877 }
878 let m = TextMetrics {
879 positions,
880 byte_offsets,
881 };
882 metrics_cache().lock().unwrap().put(key, m.clone());
883 m
884}
885
886fn width_between(edges: &[(usize, f32)], start_b: usize, end_b: usize) -> f32 {
887 let x0 = lookup_right(edges, start_b);
888 let x1 = lookup_right(edges, end_b);
889 (x1 - x0).max(0.0)
890}
891fn lookup_right(edges: &[(usize, f32)], b: usize) -> f32 {
892 match edges.binary_search_by_key(&b, |e| e.0) {
893 Ok(i) => edges[i].1,
894 Err(i) => {
895 if i == 0 {
896 0.0
897 } else {
898 edges[i - 1].1
899 }
900 }
901 }
902}
903
904pub fn wrap_lines(
905 text: &str,
906 px: f32,
907 max_width: f32,
908 max_lines: Option<usize>,
909 soft_wrap: bool,
910 font_weight: u16,
911 font_style: u8,
912 letter_spacing: f32,
913 font_variation_settings: Option<&str>,
914) -> (Vec<String>, bool) {
915 if text.is_empty() || max_width <= 0.0 {
916 return (vec![String::new()], false);
917 }
918 if !soft_wrap {
919 return (vec![text.to_string()], false);
920 }
921
922 let max_lines_key: u16 = match max_lines {
923 None => 0,
924 Some(n) => {
925 let n = n.min(u16::MAX as usize - 1) as u16;
926 n.saturating_add(1)
927 }
928 };
929 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
930 let key = (
931 fast_hash(text),
932 (px * 100.0) as u32,
933 (max_width * 100.0) as u32,
934 max_lines_key,
935 soft_wrap,
936 font_weight,
937 font_style,
938 (letter_spacing * 100.0) as i32,
939 fvs_hash,
940 );
941 if let Some(h) = wrap_cache().lock().unwrap().get(&key).cloned() {
942 return h;
943 }
944
945 let m = metrics_for_textfield(
946 text,
947 px,
948 None,
949 font_weight,
950 font_style,
951 letter_spacing,
952 font_variation_settings,
953 );
954 if let Some(&last) = m.positions.last()
955 && last <= max_width + 0.5
956 {
957 return (vec![text.to_string()], false);
958 }
959
960 let width_of = |start_b: usize, end_b: usize| -> f32 {
961 let i0 = match m.byte_offsets.binary_search(&start_b) {
962 Ok(i) | Err(i) => i,
963 };
964 let i1 = match m.byte_offsets.binary_search(&end_b) {
965 Ok(i) | Err(i) => i,
966 };
967 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
968 .max(0.0)
969 };
970
971 let mut out: Vec<String> = Vec::new();
972 let mut truncated = false;
973
974 let mut line_start = 0usize;
975 let mut best_break = line_start;
976
977 for tok in text.split_word_bounds() {
978 let tok_start = best_break;
979 let tok_end = tok_start + tok.len();
980 let w = width_of(line_start, tok_end);
981
982 if w <= max_width + 0.5 {
983 best_break = tok_end;
984 continue;
985 }
986
987 if best_break > line_start {
988 out.push(text[line_start..best_break].trim_end().to_string());
989 line_start = best_break;
990 } else {
991 let mut cut = tok_start;
992 for g in tok.grapheme_indices(true) {
993 let next = tok_start + g.0 + g.1.len();
994 if width_of(line_start, next) <= max_width + 0.5 {
995 cut = next;
996 } else {
997 break;
998 }
999 }
1000 if cut == line_start
1001 && let Some((ofs, grapheme)) = tok.grapheme_indices(true).next()
1002 {
1003 cut = tok_start + ofs + grapheme.len();
1004 }
1005 out.push(text[line_start..cut].to_string());
1006 line_start = cut;
1007 }
1008
1009 if let Some(ml) = max_lines
1010 && out.len() >= ml
1011 {
1012 truncated = true;
1013 line_start = line_start.min(text.len());
1014 break;
1015 }
1016
1017 best_break = line_start;
1018
1019 if line_start < tok_end && width_of(line_start, tok_end) <= max_width + 0.5 {
1020 best_break = tok_end;
1021 }
1022 }
1023
1024 if line_start < text.len() && max_lines.is_none_or(|ml| out.len() < ml) {
1025 out.push(text[line_start..].trim_end().to_string());
1026 }
1027
1028 let res = (out, truncated);
1029
1030 wrap_cache().lock().unwrap().put(key, res.clone());
1031 res
1032}
1033
1034pub fn wrap_line_ranges(
1035 text: &str,
1036 px: f32,
1037 max_width: f32,
1038 max_lines: Option<usize>,
1039 soft_wrap: bool,
1040 font_weight: u16,
1041 font_style: u8,
1042 letter_spacing: f32,
1043 font_variation_settings: Option<&str>,
1044) -> (Vec<(usize, usize)>, bool) {
1045 if text.is_empty() || max_width <= 0.0 {
1046 return (vec![(0, 0)], false);
1047 }
1048 if !soft_wrap {
1049 let mut out = Vec::new();
1050 let mut start = 0usize;
1051 for (i, ch) in text.char_indices() {
1052 if ch == '\n' {
1053 out.push((start, i));
1054 start = i + 1;
1055 }
1056 }
1057 out.push((start, text.len()));
1058 return (out, false);
1059 }
1060
1061 let max_lines_key: u16 = match max_lines {
1062 None => 0,
1063 Some(n) => {
1064 let n = n.min(u16::MAX as usize - 1) as u16;
1065 n.saturating_add(1)
1066 }
1067 };
1068 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1069 let key = (
1070 fast_hash(text),
1071 (px * 100.0) as u32,
1072 (max_width * 100.0) as u32,
1073 max_lines_key,
1074 soft_wrap,
1075 font_weight,
1076 font_style,
1077 (letter_spacing * 100.0) as i32,
1078 fvs_hash,
1079 );
1080 if let Some(v) = wrap_ranges_cache().lock().unwrap().get(&key).cloned() {
1081 return v;
1082 }
1083
1084 let m = metrics_for_textfield(
1085 text,
1086 px,
1087 None,
1088 font_weight,
1089 font_style,
1090 letter_spacing,
1091 font_variation_settings,
1092 );
1093
1094 let width_of = |start_b: usize, end_b: usize| -> f32 {
1095 let i0 = match m.byte_offsets.binary_search(&start_b) {
1096 Ok(i) | Err(i) => i,
1097 };
1098 let i1 = match m.byte_offsets.binary_search(&end_b) {
1099 Ok(i) | Err(i) => i,
1100 };
1101 (m.positions.get(i1).copied().unwrap_or(0.0) - m.positions.get(i0).copied().unwrap_or(0.0))
1102 .max(0.0)
1103 };
1104
1105 let mut out: Vec<(usize, usize)> = Vec::new();
1106 let mut truncated = false;
1107
1108 let mut line0_start = 0usize;
1109 for (i, ch) in text.char_indices() {
1110 if ch == '\n' {
1111 let (mut ranges, tr) = wrap_one_hard_line_ranges(
1112 text,
1113 line0_start,
1114 i,
1115 max_width,
1116 max_lines.map(|ml| ml.saturating_sub(out.len())),
1117 &width_of,
1118 );
1119 out.append(&mut ranges);
1120 if tr {
1121 truncated = true;
1122 break;
1123 }
1124 line0_start = i + 1;
1125
1126 if let Some(ml) = max_lines
1127 && out.len() >= ml
1128 {
1129 truncated = true;
1130 break;
1131 }
1132 }
1133 }
1134 if !truncated {
1135 let (mut ranges, tr) = wrap_one_hard_line_ranges(
1136 text,
1137 line0_start,
1138 text.len(),
1139 max_width,
1140 max_lines.map(|ml| ml.saturating_sub(out.len())),
1141 &width_of,
1142 );
1143 out.append(&mut ranges);
1144 truncated = tr;
1145 }
1146
1147 if out.is_empty() {
1148 out.push((0, 0));
1149 }
1150
1151 let res = (out, truncated);
1152 wrap_ranges_cache().lock().unwrap().put(key, res.clone());
1153 res
1154}
1155
1156fn wrap_one_hard_line_ranges(
1157 text: &str,
1158 start: usize,
1159 end: usize,
1160 max_width: f32,
1161 max_lines: Option<usize>,
1162 width_of: &dyn Fn(usize, usize) -> f32,
1163) -> (Vec<(usize, usize)>, bool) {
1164 let mut out = Vec::new();
1165 let mut t = false;
1166
1167 if start >= end {
1168 out.push((start, start));
1169 return (out, false);
1170 }
1171
1172 if width_of(start, end) <= max_width + 0.5 {
1173 out.push((start, end));
1174 return (out, false);
1175 }
1176
1177 let mut line_start = start;
1178 let mut best_break = line_start;
1179 let mut unconsumed_start = start;
1180
1181 for tok in text[line_start..end].split_word_bounds() {
1182 let tok_abs_start = unconsumed_start;
1183 let tok_abs_end = tok_abs_start + tok.len();
1184 unconsumed_start = tok_abs_end;
1185
1186 let w = width_of(line_start, tok_abs_end);
1187 if w <= max_width + 0.5 {
1188 best_break = tok_abs_end;
1189 continue;
1190 }
1191
1192 if best_break > line_start {
1193 out.push((line_start, best_break));
1194 line_start = best_break;
1195 } else {
1196 let mut cut = tok_abs_start;
1197 for (ofs, g) in tok.grapheme_indices(true) {
1198 let next = tok_abs_start + ofs + g.len();
1199 if width_of(line_start, next) <= max_width + 0.5 {
1200 cut = next;
1201 } else {
1202 break;
1203 }
1204 }
1205 if cut == line_start
1206 && let Some((ofs, gr)) = tok.grapheme_indices(true).next()
1207 {
1208 cut = tok_abs_start + ofs + gr.len();
1209 }
1210 out.push((line_start, cut));
1211 line_start = cut;
1212 }
1213
1214 if let Some(ml) = max_lines
1215 && out.len() >= ml
1216 {
1217 t = true;
1218 break;
1219 }
1220
1221 best_break = line_start;
1222 }
1223
1224 if !t && line_start < end && max_lines.is_none_or(|ml| out.len() < ml) {
1225 out.push((line_start, end));
1226 }
1227
1228 (out, t)
1229}
1230
1231pub fn ellipsize_line(
1232 text: &str,
1233 px: f32,
1234 max_width: f32,
1235 font_weight: u16,
1236 font_style: u8,
1237 letter_spacing: f32,
1238 font_variation_settings: Option<&str>,
1239) -> String {
1240 if text.is_empty() || max_width <= 0.0 {
1241 return String::new();
1242 }
1243 let fvs_hash = font_variation_settings.map(fast_hash).unwrap_or(0);
1244 let key = (
1245 fast_hash(text),
1246 (px * 100.0) as u32,
1247 (max_width * 100.0) as u32,
1248 font_weight,
1249 font_style,
1250 (letter_spacing * 100.0) as i32,
1251 fvs_hash,
1252 );
1253 if let Some(s) = ellip_cache().lock().unwrap().get(&key).cloned() {
1254 return s;
1255 }
1256 let m = metrics_for_textfield(
1257 text,
1258 px,
1259 None,
1260 font_weight,
1261 font_style,
1262 letter_spacing,
1263 font_variation_settings,
1264 );
1265 if let Some(&last) = m.positions.last()
1266 && last <= max_width + 0.5
1267 {
1268 return text.to_string();
1269 }
1270 let _el = "…";
1271 let e_w = ellipsis_width(px, letter_spacing);
1272 if e_w >= max_width {
1273 return String::new();
1274 }
1275 let mut cut_i = 0usize;
1276 for i in 0..m.positions.len() {
1277 if m.positions[i] + e_w <= max_width {
1278 cut_i = i;
1279 } else {
1280 break;
1281 }
1282 }
1283 let byte = m
1284 .byte_offsets
1285 .get(cut_i)
1286 .copied()
1287 .unwrap_or(0)
1288 .min(text.len());
1289 let mut out = String::with_capacity(byte + 3);
1290 out.push_str(&text[..byte]);
1291 out.push('…');
1292
1293 let s = out;
1294 ellip_cache().lock().unwrap().put(key, s.clone());
1295
1296 s
1297}
1298
1299fn ellipsis_width(px: f32, letter_spacing: f32) -> f32 {
1300 static ELLIP_W_LRU: OnceCell<Mutex<Lru<(u32, i32), f32>>> = OnceCell::new();
1301 let cache = ELLIP_W_LRU.get_or_init(|| Mutex::new(Lru::new(64)));
1302 let key = ((px * 100.0) as u32, (letter_spacing * 100.0) as i32);
1303 if let Some(w) = cache.lock().unwrap().get(&key).copied() {
1304 return w;
1305 }
1306 let w = if let Some(g) =
1307 crate::shape_line("…", px, px, None, 400, 0, letter_spacing, None).last()
1308 {
1309 g.x + g.advance
1310 } else {
1311 0.0
1312 };
1313 cache.lock().unwrap().put(key, w);
1314 w
1315}