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