1use std::collections::HashMap;
12use std::fmt;
13use std::hash::{Hash, Hasher};
14use std::sync::{Arc, OnceLock};
15
16use parley::fontique::{Blob, Collection, CollectionOptions, FamilyId, GenericFamily, SourceCache};
17use parley::{
18 Alignment, AlignmentOptions, FontContext, FontFamily, FontWeight, Layout, LayoutContext, LineHeight,
19 PositionedLayoutItem, StyleProperty,
20};
21use ravenrs_core::{Color, FontRef, GlyphRun, Point, PositionedGlyph, Scene, Size};
22
23static INTER_REGULAR: &[u8] = include_bytes!("../fonts/Inter-Regular.ttf");
24static INTER_SEMIBOLD: &[u8] = include_bytes!("../fonts/Inter-SemiBold.ttf");
25static RAVEN_ICONS: &[u8] = include_bytes!("../fonts/RavenIcons.ttf");
27#[cfg(feature = "mono")]
29static FIRA_MONO: &[u8] = include_bytes!("../fonts/FiraMono-Medium.ttf");
30
31#[cfg(not(feature = "mono"))]
33const BUILTIN: [&[u8]; 3] = [INTER_REGULAR, INTER_SEMIBOLD, RAVEN_ICONS];
34#[cfg(feature = "mono")]
35const BUILTIN: [&[u8]; 4] = [INTER_REGULAR, INTER_SEMIBOLD, RAVEN_ICONS, FIRA_MONO];
36
37pub const ICON_FAMILY: &str = "RavenIcons";
39
40pub const DEFAULT_FAMILY: &str = "Inter";
42
43#[cfg(feature = "mono")]
46pub const MONO_FAMILY: &str = "Fira Mono";
47
48pub fn builtin_font_number(bytes: &[u8]) -> Option<u8> {
51 BUILTIN.iter().position(|font| *font == bytes).map(|i| i as u8)
52}
53
54pub fn builtin_font(number: u8) -> Option<FontRef> {
56 static FONTS: OnceLock<[FontRef; BUILTIN.len()]> = OnceLock::new();
57 let fonts = FONTS.get_or_init(|| {
58 BUILTIN.map(|bytes| {
59 let blob = Blob::new(Arc::new(bytes));
60 FontRef { id: blob.id(), data: Arc::new(blob), index: 0 }
61 })
62 });
63 fonts.get(usize::from(number)).cloned()
64}
65
66pub fn inter_font(semibold: bool) -> &'static [u8] {
69 if semibold { INTER_SEMIBOLD } else { INTER_REGULAR }
70}
71
72pub fn fold(c: char) -> impl Iterator<Item = char> {
75 c.to_lowercase().map(|c| match c {
76 'á' | 'à' | 'â' | 'ã' | 'ä' | 'å' => 'a',
77 'é' | 'è' | 'ê' | 'ë' => 'e',
78 'í' | 'ì' | 'î' | 'ï' => 'i',
79 'ó' | 'ò' | 'ô' | 'õ' | 'ö' => 'o',
80 'ú' | 'ù' | 'û' | 'ü' => 'u',
81 'ç' => 'c',
82 'ñ' => 'n',
83 '\u{a0}' => ' ',
85 other => other,
86 })
87}
88
89#[derive(Clone, Debug, PartialEq)]
91pub struct TextAttrs {
92 pub size: f32,
94 pub line_height: f32,
96 pub weight: f32,
98 pub letter_spacing: f32,
100 pub family: Option<String>,
102}
103
104impl Default for TextAttrs {
105 fn default() -> Self {
106 Self { size: 15.0, line_height: 22.0, weight: 400.0, letter_spacing: 0.0, family: None }
107 }
108}
109
110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
112pub struct TextStats {
113 pub shapes: u64,
115}
116
117const CACHE_GENERATION: usize = 8192;
119const CACHE_LONGEST_TEXT: usize = 4096;
121
122fn cache_key(text: &str, attrs: &TextAttrs, fit: Option<f32>) -> u64 {
125 let mut hash = std::hash::DefaultHasher::new();
126 text.hash(&mut hash);
127 attrs.size.to_bits().hash(&mut hash);
128 attrs.line_height.to_bits().hash(&mut hash);
129 attrs.weight.to_bits().hash(&mut hash);
130 attrs.letter_spacing.to_bits().hash(&mut hash);
131 attrs.family.hash(&mut hash);
132 fit.map(f32::to_bits).hash(&mut hash);
133 hash.finish()
134}
135
136#[derive(Clone)]
137struct CachedLayout {
138 text: Box<str>,
140 attrs: TextAttrs,
141 layout: TextLayout,
142}
143
144#[derive(Default)]
147struct LayoutCache {
148 current: HashMap<u64, CachedLayout>,
149 previous: HashMap<u64, CachedLayout>,
150}
151
152impl LayoutCache {
153 fn get(&mut self, key: u64, text: &str, attrs: &TextAttrs) -> Option<TextLayout> {
154 let matches = |entry: &CachedLayout| &*entry.text == text && entry.attrs == *attrs;
155 if let Some(entry) = self.current.get(&key).filter(|entry| matches(entry)) {
156 return Some(entry.layout.clone());
157 }
158 let entry = self.previous.get(&key).filter(|entry| matches(entry))?.clone();
159 let layout = entry.layout.clone();
160 self.insert(key, entry);
161 Some(layout)
162 }
163
164 fn put(&mut self, key: u64, text: &str, attrs: &TextAttrs, layout: &TextLayout) {
165 if text.len() <= CACHE_LONGEST_TEXT {
166 let entry = CachedLayout { text: text.into(), attrs: attrs.clone(), layout: layout.clone() };
167 self.insert(key, entry);
168 }
169 }
170
171 fn insert(&mut self, key: u64, entry: CachedLayout) {
172 if self.current.len() >= CACHE_GENERATION {
173 self.previous = std::mem::take(&mut self.current);
174 }
175 self.current.insert(key, entry);
176 }
177
178 fn clear(&mut self) {
179 self.current.clear();
180 self.previous.clear();
181 }
182}
183
184pub struct TextSystem {
186 fonts: FontContext,
187 layouts: LayoutContext<()>,
188 stats: TextStats,
189 cache: LayoutCache,
190}
191
192impl fmt::Debug for TextSystem {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.debug_struct("TextSystem").finish_non_exhaustive()
195 }
196}
197
198impl Default for TextSystem {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204impl TextSystem {
205 pub fn new() -> Self {
207 let collection = Collection::new(CollectionOptions { shared: false, system_fonts: false });
208 let mut system = Self {
209 fonts: FontContext { collection, source_cache: SourceCache::default() },
210 layouts: LayoutContext::new(),
211 stats: TextStats::default(),
212 cache: LayoutCache::default(),
213 };
214 let mut families = Vec::new();
215 for bytes in [INTER_REGULAR, INTER_SEMIBOLD] {
216 families.extend(system.register_static_font(bytes));
217 }
218 families.dedup();
219 system.register_static_font(RAVEN_ICONS);
220 for generic in [GenericFamily::SansSerif, GenericFamily::SystemUi, GenericFamily::UiSansSerif] {
221 system.fonts.collection.set_generic_families(generic, families.iter().copied());
222 }
223 #[cfg(feature = "mono")]
224 {
225 let mono = system.register_static_font(FIRA_MONO);
226 for generic in [GenericFamily::Monospace, GenericFamily::UiMonospace] {
227 system.fonts.collection.set_generic_families(generic, mono.iter().copied());
228 }
229 }
230 system
231 }
232
233 fn register_static_font(&mut self, bytes: &'static [u8]) -> Vec<FamilyId> {
234 let blob = Blob::new(Arc::new(bytes));
235 self.fonts.collection.register_fonts(blob, None).into_iter().map(|(id, _)| id).collect()
236 }
237
238 pub fn register_font(&mut self, bytes: Vec<u8>) -> Vec<String> {
240 self.cache.clear();
242 let ids: Vec<FamilyId> = self
243 .fonts
244 .collection
245 .register_fonts(Blob::from(bytes), None)
246 .into_iter()
247 .map(|(id, _)| id)
248 .collect();
249 ids.into_iter().filter_map(|id| self.fonts.collection.family_name(id).map(str::to_owned)).collect()
250 }
251
252 pub fn stats(&self) -> TextStats {
254 self.stats
255 }
256
257 pub fn layout(&mut self, text: &str, attrs: &TextAttrs) -> TextLayout {
259 let key = cache_key(text, attrs, None);
260 if let Some(layout) = self.cache.get(key, text, attrs) {
261 return layout;
262 }
263 let layout = self.shape(text, attrs);
264 self.cache.put(key, text, attrs, &layout);
265 layout
266 }
267
268 fn shape(&mut self, text: &str, attrs: &TextAttrs) -> TextLayout {
269 self.stats.shapes += 1;
270 let mut builder = self.layouts.ranged_builder(&mut self.fonts, text, 1.0, false);
271 let family = attrs.family.as_deref().unwrap_or(DEFAULT_FAMILY);
272 builder.push_default(StyleProperty::FontFamily(FontFamily::Source(family.into())));
273 builder.push_default(StyleProperty::FontSize(attrs.size));
274 builder.push_default(StyleProperty::FontWeight(FontWeight::new(attrs.weight)));
275 builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(attrs.line_height)));
276 if attrs.letter_spacing != 0.0 {
277 builder.push_default(StyleProperty::LetterSpacing(attrs.letter_spacing));
278 }
279 let layout = builder.build(text);
280 TextLayout { layout, broken_at: None }
281 }
282
283 pub fn layout_ellipsized(&mut self, text: &str, attrs: &TextAttrs, max_width: f32) -> TextLayout {
286 let key = cache_key(text, attrs, Some(max_width));
287 if let Some(layout) = self.cache.get(key, text, attrs) {
288 return layout;
289 }
290 let layout = self.fit(text, attrs, max_width);
291 self.cache.put(key, text, attrs, &layout);
292 layout
293 }
294
295 fn fit(&mut self, text: &str, attrs: &TextAttrs, max_width: f32) -> TextLayout {
296 let mut whole = self.layout(text, attrs);
297 if whole.measure(None).width <= max_width {
298 return whole;
299 }
300 let ends: Vec<usize> = text.char_indices().map(|(i, _)| i).collect();
301 let cut = |count: usize| {
302 let end = ends.get(count).copied().unwrap_or(text.len());
303 format!("{}…", text[..end].trim_end())
304 };
305 let (mut low, mut high) = (0, ends.len());
307 while low < high {
308 let middle = (low + high).div_ceil(2);
309 if self.layout(&cut(middle), attrs).measure(None).width <= max_width {
310 low = middle;
311 } else {
312 high = middle - 1;
313 }
314 }
315 let mut fitted = self.layout(&cut(low), attrs);
316 fitted.measure(None);
317 fitted
318 }
319}
320
321#[derive(Clone, Debug, PartialEq)]
323pub struct TextLine {
324 pub range: std::ops::Range<usize>,
326 pub width: f32,
328 pub top: f32,
330 pub baseline: f32,
332 pub height: f32,
334}
335
336#[derive(Clone, Debug, PartialEq)]
338pub struct ShapedGlyph {
339 pub id: u32,
341 pub x: f32,
343 pub advance: f32,
345 pub text: std::ops::Range<usize>,
347}
348
349#[derive(Clone)]
351pub struct TextLayout {
352 layout: Layout<()>,
353 broken_at: Option<Option<f32>>,
355}
356
357impl fmt::Debug for TextLayout {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 f.debug_struct("TextLayout")
360 .field("width", &self.layout.width())
361 .field("height", &self.layout.height())
362 .finish()
363 }
364}
365
366impl TextLayout {
367 pub fn measure(&mut self, max_width: Option<f32>) -> Size {
369 if self.broken_at != Some(max_width) {
370 self.layout.break_all_lines(max_width);
371 self.layout.align(Alignment::Start, AlignmentOptions::default());
372 self.broken_at = Some(max_width);
373 }
374 Size::new(self.layout.width(), self.layout.height())
375 }
376
377 pub fn lines(&self) -> Vec<TextLine> {
380 self.layout
381 .lines()
382 .map(|line| {
383 let m = line.metrics();
384 TextLine {
385 range: line.text_range(),
386 width: (m.advance - m.hanging_advance).max(0.0),
387 top: m.block_min_coord,
388 baseline: m.baseline,
389 height: m.block_max_coord - m.block_min_coord,
390 }
391 })
392 .collect()
393 }
394
395 pub fn glyphs(&self) -> Vec<ShapedGlyph> {
398 let mut glyphs = Vec::new();
399 for line in self.layout.lines() {
400 for item in line.items() {
401 let PositionedLayoutItem::GlyphRun(run) = item else { continue };
402 let mut x = run.offset();
403 for cluster in run.run().visual_clusters() {
404 let text = cluster.text_range();
405 for g in cluster.glyphs() {
406 glyphs.push(ShapedGlyph {
407 id: g.id,
408 x: x + g.x,
409 advance: g.advance,
410 text: text.clone(),
411 });
412 x += g.advance;
413 }
414 }
415 }
416 }
417 glyphs
418 }
419
420 pub fn paint(&self, origin: Point, color: Color, scene: &mut Scene) {
423 for line in self.layout.lines() {
424 for item in line.items() {
425 let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
426 continue;
427 };
428 let run = glyph_run.run();
429 let font = run.font();
430 let glyphs = glyph_run
431 .positioned_glyphs()
432 .map(|g| PositionedGlyph { id: g.id, x: origin.x + g.x, y: origin.y + g.y })
433 .collect();
434 scene.push_glyphs(GlyphRun {
435 font: FontRef {
436 id: font.data.id(),
437 data: Arc::new(font.data.clone()),
438 index: font.index,
439 },
440 size: run.font_size(),
441 color,
442 glyphs,
443 });
444 }
445 }
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use ravenrs_core::Primitive;
452
453 use super::*;
454
455 #[test]
456 fn shapes_with_embedded_font() {
457 let mut ts = TextSystem::new();
458 let mut layout = ts.layout("Welcome to RavenRS", &TextAttrs::default());
459 let size = layout.measure(None);
460 assert!(size.width > 100.0 && size.width < 200.0, "unexpected width {}", size.width);
461 assert_eq!(size.height, 22.0);
462 }
463
464 #[test]
465 fn reports_lines_with_ranges_and_baselines() {
466 let mut ts = TextSystem::new();
467 let text = "one two three four five six";
468 let mut layout = ts.layout(text, &TextAttrs::default());
469 layout.measure(Some(60.0));
470 let lines = layout.lines();
471 assert!(lines.len() > 1);
472 assert_eq!(lines[0].range.start, 0);
473 assert_eq!(lines.last().map(|l| l.range.end), Some(text.len()));
474 for pair in lines.windows(2) {
475 assert_eq!(pair[0].range.end, pair[1].range.start, "lines cover the text without gaps");
476 assert!(pair[1].baseline > pair[0].baseline);
477 }
478 assert!(lines.iter().all(|l| l.width <= 60.0 + 0.5 && l.height == 22.0));
479 assert!(text[lines[0].range.clone()].ends_with(' '), "trailing space stays in the range");
480 }
481
482 #[test]
483 fn the_same_text_is_shaped_once() {
484 let mut ts = TextSystem::new();
485 let attrs = TextAttrs::default();
486 let mut first = ts.layout("Repeated text", &attrs);
487 let mut again = ts.layout("Repeated text", &attrs);
488 assert_eq!(ts.stats().shapes, 1);
489 assert_eq!(first.measure(None), again.measure(None));
490 ts.layout("Other text", &attrs);
491 ts.layout("Repeated text", &TextAttrs { weight: 600.0, ..TextAttrs::default() });
492 assert_eq!(ts.stats().shapes, 3, "other text and other attributes are other layouts");
493 }
494
495 #[test]
496 fn breaking_a_cached_layout_leaves_the_next_one_alone() {
497 let mut ts = TextSystem::new();
498 let text = "one two three four five six";
499 let mut narrow = ts.layout(text, &TextAttrs::default());
500 let narrow = narrow.measure(Some(60.0));
501 let wide = ts.layout(text, &TextAttrs::default()).measure(None);
502 assert!(narrow.height > wide.height, "the cache keeps no one's line breaks");
503 }
504
505 #[test]
506 fn cutting_text_to_fit_is_kept_per_width() {
507 let mut ts = TextSystem::new();
508 let attrs = TextAttrs::default();
509 let text = "A long sentence that will not fit in the space it was given";
510 ts.layout_ellipsized(text, &attrs, 100.0);
511 let shapes = ts.stats().shapes;
512 let mut fitted = ts.layout_ellipsized(text, &attrs, 100.0);
513 assert_eq!(ts.stats().shapes, shapes, "the bisection does not run again");
514 assert!(fitted.measure(None).width <= 100.0);
515 let mut wider = ts.layout_ellipsized(text, &attrs, 200.0);
516 assert!(wider.measure(None).width > fitted.measure(None).width);
517 }
518
519 #[test]
520 fn a_new_font_empties_the_cache() {
521 let mut ts = TextSystem::new();
522 ts.layout("Text", &TextAttrs::default());
523 ts.register_font(INTER_REGULAR.to_vec());
524 ts.layout("Text", &TextAttrs::default());
525 assert_eq!(ts.stats().shapes, 2);
526 }
527
528 #[test]
529 fn wraps_when_narrow() {
530 let mut ts = TextSystem::new();
531 let mut layout = ts.layout("one two three four five six", &TextAttrs::default());
532 let wide = layout.measure(None);
533 let narrow = layout.measure(Some(60.0));
534 assert!(narrow.height > wide.height);
535 assert!(narrow.width <= 60.0 + 0.5);
536 }
537
538 #[test]
539 fn semibold_is_wider_than_regular() {
540 let mut ts = TextSystem::new();
541 let regular = ts.layout("RavenRS", &TextAttrs::default()).measure(None).width;
542 let bold =
543 ts.layout("RavenRS", &TextAttrs { weight: 600.0, ..TextAttrs::default() }).measure(None).width;
544 assert!(bold > regular, "semibold face should be selected ({bold} vs {regular})");
545 }
546
547 #[test]
548 fn icon_font_is_registered() {
549 let mut ts = TextSystem::new();
550 let attrs = TextAttrs {
551 family: Some(ICON_FAMILY.into()),
552 size: 16.0,
553 line_height: 16.0,
554 ..Default::default()
555 };
556 let mut layout = ts.layout("\u{e151}", &attrs);
558 let size = layout.measure(None);
559 assert!((size.width - 16.0).abs() < 1.0, "icon glyph should be one em wide, got {}", size.width);
560 }
561
562 #[test]
563 fn paints_one_glyph_per_character_offset_by_origin() {
564 let mut ts = TextSystem::new();
565 let mut layout = ts.layout("Raven", &TextAttrs::default());
566 layout.measure(None);
567 let mut scene = Scene::new();
568 layout.paint(Point::new(10.0, 20.0), Color::WHITE, &mut scene);
569 let Some(Primitive::Glyphs(run)) = scene.primitives().first() else {
570 panic!("expected a glyph run");
571 };
572 assert_eq!(run.glyphs.len(), 5);
573 assert!(run.glyphs[0].x >= 10.0);
574 assert!(run.glyphs[0].y > 20.0, "glyph y is the baseline below the origin");
575 assert!(!run.font.bytes().is_empty());
576 }
577}