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