1use anyhow::{Context as _, Ok, Result};
2use cosmic_text::{
3 Attrs, AttrsList, Ellipsize, Family, Font as CosmicTextFont,
4 FontFeatures as CosmicFontFeatures, FontSystem, ShapeBuffer, ShapeLine,
5};
6use rgpui::{
7 Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun, GlyphId,
8 LineLayout, Pixels, PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X,
9 SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point,
10 size,
11};
12use std::collections::HashMap;
13
14use itertools::Itertools;
15use parking_lot::RwLock;
16use smallvec::SmallVec;
17use std::{borrow::Cow, ops::Range, sync::Arc};
18use swash::{
19 scale::{Render, ScaleContext, Source, StrikeWith},
20 zeno::{Format, Vector},
21};
22use unicode_segmentation::UnicodeSegmentation;
23
24pub struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27struct FontKey {
28 family: SharedString,
29 features: FontFeatures,
30 fallbacks: Option<FontFallbacks>,
31}
32
33impl FontKey {
34 fn new(family: SharedString, features: FontFeatures, fallbacks: Option<FontFallbacks>) -> Self {
35 Self {
36 family,
37 features,
38 fallbacks,
39 }
40 }
41}
42
43struct CosmicTextSystemState {
44 font_system: FontSystem,
45 scratch: ShapeBuffer,
46 swash_scale_context: ScaleContext,
47 loaded_fonts: Vec<LoadedFont>,
49 font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
52 system_font_fallback: String,
53}
54
55struct LoadedFont {
56 font: Arc<CosmicTextFont>,
57 features: CosmicFontFeatures,
58 is_known_emoji_font: bool,
59 user_fallback_chain: Arc<[(FontId, SharedString)]>,
62}
63
64impl CosmicTextSystem {
65 pub fn new(system_font_fallback: &str) -> Self {
66 let font_system = FontSystem::new();
67
68 Self(RwLock::new(CosmicTextSystemState {
69 font_system,
70 scratch: ShapeBuffer::default(),
71 swash_scale_context: ScaleContext::new(),
72 loaded_fonts: Vec::new(),
73 font_ids_by_family_cache: HashMap::default(),
74 system_font_fallback: system_font_fallback.to_string(),
75 }))
76 }
77
78 pub fn new_without_system_fonts(system_font_fallback: &str) -> Self {
79 let font_system = FontSystem::new_with_locale_and_db(
80 "en-US".to_string(),
81 cosmic_text::fontdb::Database::new(),
82 );
83
84 Self(RwLock::new(CosmicTextSystemState {
85 font_system,
86 scratch: ShapeBuffer::default(),
87 swash_scale_context: ScaleContext::new(),
88 loaded_fonts: Vec::new(),
89 font_ids_by_family_cache: HashMap::default(),
90 system_font_fallback: system_font_fallback.to_string(),
91 }))
92 }
93}
94
95impl PlatformTextSystem for CosmicTextSystem {
96 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
97 self.0.write().add_fonts(fonts)
98 }
99
100 fn all_font_names(&self) -> Vec<String> {
101 let mut result = self
102 .0
103 .read()
104 .font_system
105 .db()
106 .faces()
107 .filter_map(|face| face.families.first().map(|family| family.0.clone()))
108 .collect_vec();
109 result.sort_unstable();
110 result.dedup();
111 result
112 }
113
114 fn font_id(&self, font: &Font) -> Result<FontId> {
115 let mut state = self.0.write();
116 let key = FontKey::new(
117 font.family.clone(),
118 font.features.clone(),
119 font.fallbacks.clone(),
120 );
121 let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
122 font_ids.as_slice()
123 } else {
124 let font_ids =
125 state.load_family(&font.family, &font.features, font.fallbacks.as_ref())?;
126 state.font_ids_by_family_cache.insert(key.clone(), font_ids);
127 state.font_ids_by_family_cache[&key].as_ref()
128 };
129
130 let ix = find_best_match(font, candidates, &state)?;
131
132 Ok(candidates[ix])
133 }
134
135 fn font_metrics(&self, font_id: FontId) -> FontMetrics {
136 let metrics = self
137 .0
138 .read()
139 .loaded_font(font_id)
140 .font
141 .as_swash()
142 .metrics(&[]);
143
144 FontMetrics {
145 units_per_em: metrics.units_per_em as u32,
146 ascent: metrics.ascent,
147 descent: -metrics.descent,
148 line_gap: metrics.leading,
149 underline_position: metrics.underline_offset,
150 underline_thickness: metrics.stroke_size,
151 cap_height: metrics.cap_height,
152 x_height: metrics.x_height,
153 bounding_box: Bounds {
154 origin: point(0.0, 0.0),
155 size: size(metrics.max_width, metrics.ascent + metrics.descent),
156 },
157 }
158 }
159
160 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
161 let lock = self.0.read();
162 let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
163 let glyph_id = glyph_id.0 as u16;
164 Ok(Bounds {
165 origin: point(0.0, 0.0),
166 size: size(
167 glyph_metrics.advance_width(glyph_id),
168 glyph_metrics.advance_height(glyph_id),
169 ),
170 })
171 }
172
173 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
174 self.0.read().advance(font_id, glyph_id)
175 }
176
177 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
178 self.0.read().glyph_for_char(font_id, ch)
179 }
180
181 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
182 self.0.write().raster_bounds(params)
183 }
184
185 fn rasterize_glyph(
186 &self,
187 params: &RenderGlyphParams,
188 raster_bounds: Bounds<DevicePixels>,
189 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
190 self.0.write().rasterize_glyph(params, raster_bounds)
191 }
192
193 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
194 self.0.write().layout_line(text, font_size, runs)
195 }
196
197 fn recommended_rendering_mode(
198 &self,
199 _font_id: FontId,
200 _font_size: Pixels,
201 ) -> TextRenderingMode {
202 TextRenderingMode::Subpixel
203 }
204}
205
206impl CosmicTextSystemState {
207 fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
208 &self.loaded_fonts[font_id.0]
209 }
210
211 #[profiling::function]
212 fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
213 let db = self.font_system.db_mut();
214 for bytes in fonts {
215 match bytes {
216 Cow::Borrowed(embedded_font) => {
217 db.load_font_data(embedded_font.to_vec());
218 }
219 Cow::Owned(bytes) => {
220 db.load_font_data(bytes);
221 }
222 }
223 }
224 Ok(())
225 }
226
227 #[profiling::function]
228 fn load_family(
229 &mut self,
230 name: &str,
231 features: &FontFeatures,
232 fallbacks: Option<&FontFallbacks>,
233 ) -> Result<SmallVec<[FontId; 4]>> {
234 let user_fallback_chain: Arc<[(FontId, SharedString)]> = match fallbacks {
238 Some(fallbacks) if !fallbacks.fallback_list().is_empty() => {
239 let mut chain: Vec<(FontId, SharedString)> = Vec::new();
240 for fallback_name in fallbacks.fallback_list() {
241 let fb_key = FontKey::new(
242 SharedString::from(fallback_name.clone()),
243 features.clone(),
244 None,
245 );
246 let fb_ids = if let Some(cached) = self.font_ids_by_family_cache.get(&fb_key) {
247 cached.clone()
248 } else {
249 let loaded = self.load_family(fallback_name, features, None)?;
250 self.font_ids_by_family_cache
251 .insert(fb_key.clone(), loaded.clone());
252 loaded
253 };
254 let Some(&fb_id) = fb_ids.first() else {
255 continue;
256 };
257 let db_id = self.loaded_fonts[fb_id.0].font.id();
258 if let Some(face) = self.font_system.db().face(db_id)
259 && let Some(family) = face.families.first()
260 {
261 chain.push((fb_id, SharedString::from(family.0.clone())));
262 }
263 }
264 Arc::from(chain)
265 }
266 _ => Arc::from(Vec::new()),
267 };
268
269 let name = rgpui::font_name_with_fallbacks(name, &self.system_font_fallback);
270
271 let families = self
272 .font_system
273 .db()
274 .faces()
275 .filter(|face| face.families.iter().any(|family| *name == family.0))
276 .map(|face| (face.id, face.post_script_name.clone()))
277 .collect::<SmallVec<[_; 4]>>();
278
279 let cosmic_features = cosmic_font_features(features)?;
280
281 let mut loaded_font_ids = SmallVec::new();
282 for (font_id, postscript_name) in families {
283 let font = self
284 .font_system
285 .get_font(font_id, cosmic_text::Weight::NORMAL)
286 .context("Could not load font")?;
287
288 let allowed_bad_font_names = [
290 "SegoeFluentIcons", "Segoe Fluent Icons",
292 ];
293
294 if font.as_swash().charmap().map('m') == 0
295 && !allowed_bad_font_names.contains(&postscript_name.as_str())
296 {
297 self.font_system.db_mut().remove_face(font.id());
298 continue;
299 };
300
301 let font_id = FontId(self.loaded_fonts.len());
302 loaded_font_ids.push(font_id);
303 self.loaded_fonts.push(LoadedFont {
304 font,
305 features: cosmic_features.clone(),
306 is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
307 user_fallback_chain: Arc::clone(&user_fallback_chain),
308 });
309 }
310
311 Ok(loaded_font_ids)
312 }
313
314 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
315 let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
316 Ok(Size {
317 width: glyph_metrics.advance_width(glyph_id.0 as u16),
318 height: glyph_metrics.advance_height(glyph_id.0 as u16),
319 })
320 }
321
322 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
323 let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
324 if glyph_id == 0 {
325 None
326 } else {
327 Some(GlyphId(glyph_id.into()))
328 }
329 }
330
331 fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
332 let image = self.render_glyph_image(params)?;
333 Ok(Bounds {
334 origin: point(image.placement.left.into(), (-image.placement.top).into()),
335 size: size(image.placement.width.into(), image.placement.height.into()),
336 })
337 }
338
339 #[profiling::function]
340 fn rasterize_glyph(
341 &mut self,
342 params: &RenderGlyphParams,
343 glyph_bounds: Bounds<DevicePixels>,
344 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
345 if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
346 anyhow::bail!("glyph bounds are empty");
347 }
348
349 let mut image = self.render_glyph_image(params)?;
350 let bitmap_size = glyph_bounds.size;
351 match image.content {
352 swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
353 for pixel in image.data.chunks_exact_mut(4) {
355 pixel.swap(0, 2);
356 }
357 Ok((bitmap_size, image.data))
358 }
359 swash::scale::image::Content::Mask => {
360 if params.subpixel_rendering {
361 let expanded = image.data.iter().flat_map(|&a| [a, a, a, a]).collect();
363 Ok((bitmap_size, expanded))
364 } else {
365 Ok((bitmap_size, image.data))
366 }
367 }
368 }
369 }
370
371 fn render_glyph_image(
372 &mut self,
373 params: &RenderGlyphParams,
374 ) -> Result<swash::scale::image::Image> {
375 let loaded_font = &self.loaded_fonts[params.font_id.0];
376 let font_ref = loaded_font.font.as_swash();
377 let pixel_size = f32::from(params.font_size);
378
379 let subpixel_offset = Vector::new(
380 params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
381 params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
382 );
383
384 let mut scaler = self
385 .swash_scale_context
386 .builder(font_ref)
387 .size(pixel_size * params.scale_factor)
388 .hint(true)
389 .build();
390
391 let sources: &[Source] = if params.is_emoji {
392 &[
393 Source::ColorOutline(0),
394 Source::ColorBitmap(StrikeWith::BestFit),
395 Source::Outline,
396 ]
397 } else {
398 &[Source::Bitmap(StrikeWith::ExactSize), Source::Outline]
399 };
400
401 let mut renderer = Render::new(sources);
402 if params.subpixel_rendering {
403 renderer
405 .format(Format::subpixel_bgra())
406 .offset(subpixel_offset);
407 } else {
408 renderer.format(Format::Alpha).offset(subpixel_offset);
409 }
410
411 let glyph_id: u16 = params.glyph_id.0.try_into()?;
412 renderer
413 .render(&mut scaler, glyph_id)
414 .with_context(|| format!("unable to render glyph via swash for {params:?}"))
415 }
416
417 fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> Result<FontId> {
426 if let Some(ix) = self
427 .loaded_fonts
428 .iter()
429 .position(|loaded_font| loaded_font.font.id() == id)
430 {
431 Ok(FontId(ix))
432 } else {
433 let font = self
434 .font_system
435 .get_font(id, cosmic_text::Weight::NORMAL)
436 .context("failed to get fallback font from cosmic-text font system")?;
437 let face = self
438 .font_system
439 .db()
440 .face(id)
441 .context("fallback font face not found in cosmic-text database")?;
442
443 let font_id = FontId(self.loaded_fonts.len());
444 self.loaded_fonts.push(LoadedFont {
445 font,
446 features: CosmicFontFeatures::new(),
447 is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
448 user_fallback_chain: Arc::from(Vec::new()),
449 });
450
451 Ok(font_id)
452 }
453 }
454
455 #[profiling::function]
456 fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
457 if contains_paragraph_separator(text) {
458 self.layout_line_with_separators(text, font_size, font_runs)
459 } else {
460 self.layout_line_no_separators(text, font_size, font_runs)
461 }
462 }
463
464 fn layout_line_with_separators(
465 &mut self,
466 text: &str,
467 font_size: Pixels,
468 font_runs: &[FontRun],
469 ) -> LineLayout {
470 let mut layout = LineLayout {
471 font_size,
472 len: text.len(),
473 ..Default::default()
474 };
475 let mut paragraph_start = 0;
476
477 for (separator_start, separator) in text
478 .char_indices()
479 .filter(|(_, character)| is_paragraph_separator(*character))
480 {
481 let separator_end = separator_start + separator.len_utf8();
482 self.shape_segment(
483 text,
484 paragraph_start..separator_start,
485 font_size,
486 font_runs,
487 &mut layout,
488 );
489 self.shape_segment(
490 text,
491 separator_start..separator_end,
492 font_size,
493 font_runs,
494 &mut layout,
495 );
496 paragraph_start = separator_end;
497 }
498
499 self.shape_segment(
500 text,
501 paragraph_start..text.len(),
502 font_size,
503 font_runs,
504 &mut layout,
505 );
506
507 layout
508 }
509
510 fn shape_segment(
511 &mut self,
512 text: &str,
513 range: Range<usize>,
514 font_size: Pixels,
515 font_runs: &[FontRun],
516 layout: &mut LineLayout,
517 ) {
518 if range.is_empty() {
519 return;
520 }
521
522 let segment_font_runs = clip_font_runs(font_runs, range.clone());
523 let segment =
524 self.layout_line_no_separators(&text[range.clone()], font_size, &segment_font_runs);
525
526 let mut segment_runs = segment.runs;
527 for run in &mut segment_runs {
528 for glyph in &mut run.glyphs {
529 glyph.index += range.start;
530 glyph.position.x += layout.width;
531 }
532 }
533
534 for mut run in segment_runs {
535 if let Some(same_run) = layout
536 .runs
537 .last_mut()
538 .filter(|last| last.font_id == run.font_id)
539 {
540 same_run.glyphs.append(&mut run.glyphs);
541 } else {
542 layout.runs.push(run);
543 }
544 }
545
546 layout.width += segment.width;
547 layout.ascent = layout.ascent.max(segment.ascent);
548 layout.descent = layout.descent.max(segment.descent);
549 }
550
551 fn layout_line_no_separators(
552 &mut self,
553 text: &str,
554 font_size: Pixels,
555 font_runs: &[FontRun],
556 ) -> LineLayout {
557 let mut attrs_list = AttrsList::new(&Attrs::new());
558 let mut offs = 0;
559 for run in font_runs {
560 let run_end = offs + run.len;
561
562 let loaded_font = self.loaded_font(run.font_id);
563 let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
564 log::warn!(
565 "font face not found in database for font_id {:?}",
566 run.font_id
567 );
568 offs = run_end;
569 continue;
570 };
571 let Some(first_family) = face.families.first() else {
572 log::warn!(
573 "font face has no family names for font_id {:?}",
574 run.font_id
575 );
576 offs = run_end;
577 continue;
578 };
579
580 let primary_family_name: SharedString = first_family.0.clone().into();
581 let primary_stretch = face.stretch;
582 let primary_style = face.style;
583 let primary_weight = face.weight;
584 let primary_features = loaded_font.features.clone();
585 let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain);
586
587 let primary_attrs = Attrs::new()
590 .metadata(run.font_id.0)
591 .family(Family::Name(&primary_family_name))
592 .stretch(primary_stretch)
593 .style(primary_style)
594 .weight(primary_weight)
595 .font_features(primary_features.clone());
596 let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain
597 .iter()
598 .map(|(fb_id, fb_name)| {
599 Attrs::new()
600 .metadata(fb_id.0)
601 .family(Family::Name(fb_name))
602 .stretch(primary_stretch)
603 .style(primary_style)
604 .weight(primary_weight)
605 .font_features(primary_features.clone())
606 })
607 .collect();
608
609 let spans = if fallback_chain.is_empty() {
610 let mut spans = SmallVec::<[RunSpan; 4]>::new();
611 spans.push(RunSpan {
612 start: offs,
613 end: run_end,
614 slot: None,
615 font_id: run.font_id,
616 });
617 spans
618 } else {
619 let loaded_fonts = &self.loaded_fonts;
620 let covers = |id: FontId, ch: char| charmap_covers(loaded_fonts, id, ch);
621 compute_run_spans(text, offs, run.len, run.font_id, &fallback_chain, &covers)
622 };
623
624 for span in spans {
625 let attrs = match span.slot {
626 None => &primary_attrs,
627 Some(ix) => &fallback_attrs[ix],
628 };
629 attrs_list.add_span(span.start..span.end, attrs);
630 }
631 offs = run_end;
632 }
633
634 let line = ShapeLine::new(
635 &mut self.font_system,
636 text,
637 &attrs_list,
638 cosmic_text::Shaping::Advanced,
639 4,
640 );
641 let mut layout_lines = Vec::with_capacity(1);
642 line.layout_to_buffer(
643 &mut self.scratch,
644 f32::from(font_size),
645 None, cosmic_text::Wrap::None,
647 Ellipsize::None,
648 None,
649 &mut layout_lines,
650 None,
651 cosmic_text::Hinting::Disabled,
652 );
653
654 let Some(layout) = layout_lines.first() else {
655 return LineLayout {
656 font_size,
657 width: Pixels::ZERO,
658 ascent: Pixels::ZERO,
659 descent: Pixels::ZERO,
660 runs: Vec::new(),
661 len: text.len(),
662 };
663 };
664
665 let mut runs: Vec<ShapedRun> = Vec::new();
666 for glyph in &layout.glyphs {
667 let mut font_id = FontId(glyph.metadata);
668 let mut loaded_font = self.loaded_font(font_id);
669 if loaded_font.font.id() != glyph.font_id {
670 match self.font_id_for_cosmic_id(glyph.font_id) {
671 std::result::Result::Ok(resolved_id) => {
672 font_id = resolved_id;
673 loaded_font = self.loaded_font(font_id);
674 }
675 Err(error) => {
676 log::warn!(
677 "failed to resolve cosmic font id {:?}: {error:#}",
678 glyph.font_id
679 );
680 continue;
681 }
682 }
683 }
684 let is_emoji = loaded_font.is_known_emoji_font;
685
686 if glyph.glyph_id == 3 && is_emoji {
688 continue;
689 }
690
691 let shaped_glyph = ShapedGlyph {
692 id: GlyphId(glyph.glyph_id as u32),
693 position: point(glyph.x.into(), glyph.y.into()),
694 index: glyph.start,
695 is_emoji,
696 };
697
698 if let Some(last_run) = runs
699 .last_mut()
700 .filter(|last_run| last_run.font_id == font_id)
701 {
702 last_run.glyphs.push(shaped_glyph);
703 } else {
704 runs.push(ShapedRun {
705 font_id,
706 glyphs: vec![shaped_glyph],
707 });
708 }
709 }
710
711 LineLayout {
712 font_size,
713 width: layout.w.into(),
714 ascent: layout.max_ascent.into(),
715 descent: layout.max_descent.into(),
716 runs,
717 len: text.len(),
718 }
719 }
720}
721
722#[inline(always)]
723fn is_paragraph_separator(character: char) -> bool {
724 unicode_bidi::bidi_class(character) == unicode_bidi::BidiClass::B
725}
726
727fn contains_paragraph_separator(text: &str) -> bool {
728 if text
729 .bytes()
730 .any(|byte| matches!(byte, b'\n' | b'\r' | 0x1c | 0x1d | 0x1e))
731 {
732 return true;
733 }
734
735 !text.is_ascii() && text.chars().any(is_paragraph_separator)
736}
737
738fn clip_font_runs(font_runs: &[FontRun], range: Range<usize>) -> SmallVec<[FontRun; 4]> {
739 let mut clipped = SmallVec::new();
740 let mut offs = 0;
741 for run in font_runs {
742 let run_start = offs;
743 offs += run.len;
744 if offs <= range.start {
745 continue;
746 }
747 if run_start >= range.end {
748 break;
749 }
750 let start = run_start.max(range.start);
751 let end = offs.min(range.end);
752 if start < end {
753 clipped.push(FontRun {
754 len: end - start,
755 font_id: run.font_id,
756 });
757 }
758 }
759 clipped
760}
761
762#[cfg(feature = "font-kit")]
763fn find_best_match(
764 font: &Font,
765 candidates: &[FontId],
766 state: &CosmicTextSystemState,
767) -> Result<usize> {
768 let candidate_properties = candidates
769 .iter()
770 .map(|font_id| {
771 let database_id = state.loaded_font(*font_id).font.id();
772 let face_info = state
773 .font_system
774 .db()
775 .face(database_id)
776 .context("font face not found in database")?;
777 Ok(face_info_into_properties(face_info))
778 })
779 .collect::<Result<SmallVec<[_; 4]>>>()?;
780
781 let ix =
782 font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
783 .context("requested font family contains no font matching the other parameters")?;
784
785 Ok(ix)
786}
787
788#[cfg(not(feature = "font-kit"))]
789fn find_best_match(
790 font: &Font,
791 candidates: &[FontId],
792 state: &CosmicTextSystemState,
793) -> Result<usize> {
794 if candidates.is_empty() {
795 anyhow::bail!("requested font family contains no font matching the other parameters");
796 }
797 if candidates.len() == 1 {
798 return Ok(0);
799 }
800
801 let target_weight = font.weight.0;
802 let target_italic = matches!(
803 font.style,
804 rgpui::FontStyle::Italic | rgpui::FontStyle::Oblique
805 );
806
807 let mut best_index = 0;
808 let mut best_score = u32::MAX;
809
810 for (index, font_id) in candidates.iter().enumerate() {
811 let database_id = state.loaded_font(*font_id).font.id();
812 let face_info = state
813 .font_system
814 .db()
815 .face(database_id)
816 .context("font face not found in database")?;
817
818 let is_italic = matches!(
819 face_info.style,
820 cosmic_text::Style::Italic | cosmic_text::Style::Oblique
821 );
822 let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
823 let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
824 let score = style_penalty + weight_diff;
825
826 if score < best_score {
827 best_score = score;
828 best_index = index;
829 }
830 }
831
832 Ok(best_index)
833}
834
835#[derive(Debug, Clone, Copy, PartialEq, Eq)]
838struct RunSpan {
839 start: usize,
840 end: usize,
841 slot: Option<usize>,
842 font_id: FontId,
843}
844
845fn compute_run_spans(
849 text: &str,
850 run_offset: usize,
851 run_len: usize,
852 primary: FontId,
853 fallback_chain: &[(FontId, SharedString)],
854 covers: &impl Fn(FontId, char) -> bool,
855) -> SmallVec<[RunSpan; 4]> {
856 let mut spans = SmallVec::new();
857 let run_end = run_offset + run_len;
858 if run_end <= run_offset {
859 return spans;
860 }
861 if fallback_chain.is_empty() {
862 spans.push(RunSpan {
863 start: run_offset,
864 end: run_end,
865 slot: None,
866 font_id: primary,
867 });
868 return spans;
869 }
870 let run_text = &text[run_offset..run_end];
871 let mut span_start = run_offset;
872 let mut span_slot: Option<usize> = None;
873 let mut span_font_id = primary;
874 for (grapheme_idx, grapheme) in run_text.grapheme_indices(true) {
875 let abs = run_offset + grapheme_idx;
876 let ch = grapheme.chars().next().unwrap_or('\0');
877 let next_slot = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);
878 if next_slot == span_slot {
879 continue;
880 }
881 if abs > span_start {
882 spans.push(RunSpan {
883 start: span_start,
884 end: abs,
885 slot: span_slot,
886 font_id: span_font_id,
887 });
888 }
889 span_start = abs;
890 span_slot = next_slot;
891 span_font_id = slot_font_id(next_slot, primary, fallback_chain);
892 }
893 if span_start < run_end {
894 spans.push(RunSpan {
895 start: span_start,
896 end: run_end,
897 slot: span_slot,
898 font_id: span_font_id,
899 });
900 }
901 spans
902}
903
904fn slot_font_id(
905 slot: Option<usize>,
906 primary: FontId,
907 fallback_chain: &[(FontId, SharedString)],
908) -> FontId {
909 match slot {
910 None => primary,
911 Some(ix) => fallback_chain[ix].0,
912 }
913}
914
915fn pick_covering_slot(
916 ch: char,
917 current: Option<usize>,
918 primary: FontId,
919 fallback_chain: &[(FontId, SharedString)],
920 covers: &impl Fn(FontId, char) -> bool,
921) -> Option<usize> {
922 if (ch as u32) <= 0x7F {
923 return None;
924 }
925 if covers(primary, ch) {
926 return None;
927 }
928 let current_id = slot_font_id(current, primary, fallback_chain);
929 if covers(current_id, ch) {
930 return current;
931 }
932 for (ix, (fb_id, _)) in fallback_chain.iter().enumerate() {
933 if covers(*fb_id, ch) {
934 return Some(ix);
935 }
936 }
937 None
938}
939
940fn charmap_covers(loaded_fonts: &[LoadedFont], id: FontId, ch: char) -> bool {
941 loaded_fonts
942 .get(id.0)
943 .is_some_and(|loaded| loaded.font.as_swash().charmap().map(ch) != 0)
944}
945
946fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
947 let mut result = CosmicFontFeatures::new();
948 for feature in features.0.iter() {
949 let name_bytes: [u8; 4] = feature
950 .0
951 .as_bytes()
952 .try_into()
953 .context("Incorrect feature flag format")?;
954
955 let tag = cosmic_text::FeatureTag::new(&name_bytes);
956
957 result.set(tag, feature.1);
958 }
959 Ok(result)
960}
961
962#[cfg(feature = "font-kit")]
963fn font_into_properties(font: &rgpui::Font) -> font_kit::properties::Properties {
964 font_kit::properties::Properties {
965 style: match font.style {
966 rgpui::FontStyle::Normal => font_kit::properties::Style::Normal,
967 rgpui::FontStyle::Italic => font_kit::properties::Style::Italic,
968 rgpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
969 },
970 weight: font_kit::properties::Weight(font.weight.0),
971 stretch: Default::default(),
972 }
973}
974
975#[cfg(feature = "font-kit")]
976fn face_info_into_properties(
977 face_info: &cosmic_text::fontdb::FaceInfo,
978) -> font_kit::properties::Properties {
979 font_kit::properties::Properties {
980 style: match face_info.style {
981 cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
982 cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
983 cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
984 },
985 weight: font_kit::properties::Weight(face_info.weight.0.into()),
986 stretch: match face_info.stretch {
987 cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
988 cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
989 cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
990 cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
991 cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
992 cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
993 cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
994 cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
995 cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
996 },
997 }
998}
999
1000fn check_is_known_emoji_font(postscript_name: &str) -> bool {
1001 postscript_name == "NotoColorEmoji"
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 fn fid(i: usize) -> FontId {
1010 FontId(i)
1011 }
1012
1013 fn chain(ids: &[usize]) -> SmallVec<[(FontId, SharedString); 4]> {
1014 ids.iter()
1015 .map(|&i| (fid(i), SharedString::from(format!("fb{i}"))))
1016 .collect()
1017 }
1018
1019 fn span(start: usize, end: usize, slot: Option<usize>, font_id: FontId) -> RunSpan {
1020 RunSpan {
1021 start,
1022 end,
1023 slot,
1024 font_id,
1025 }
1026 }
1027
1028 const IBM_PLEX: &[u8] =
1029 include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
1030
1031 const SEPARATORS: &[char] = &[
1034 '\u{000a}', '\u{000d}', '\u{001c}', '\u{001d}', '\u{001e}', '\u{0085}', '\u{2029}',
1035 ];
1036
1037 fn text_system() -> Result<CosmicTextSystem> {
1038 let text_system = CosmicTextSystem::new_without_system_fonts("IBM Plex Sans");
1039 text_system.add_fonts(vec![Cow::Borrowed(IBM_PLEX)])?;
1040 Ok(text_system)
1041 }
1042
1043 fn layout_text(text_system: &CosmicTextSystem, text: &str) -> Result<LineLayout> {
1044 let font_id = text_system.font_id(&rgpui::font("IBM Plex Sans"))?;
1045 let runs = [FontRun {
1046 len: text.len(),
1047 font_id,
1048 }];
1049 Ok(text_system.layout_line(text, rgpui::px(14.0), &runs))
1050 }
1051
1052 #[test]
1055 fn shape_text_with_mixed_direction_paragraphs() -> Result<()> {
1056 let platform_text_system = Arc::new(text_system()?);
1057 let text_system = Arc::new(rgpui::TextSystem::new(platform_text_system));
1058 let window_text_system = rgpui::WindowTextSystem::new(text_system);
1059
1060 let text: SharedString = "first line\n\u{05d0}\u{001c}A".into();
1061 let runs = [rgpui::TextRun {
1062 len: text.len(),
1063 font: rgpui::font("IBM Plex Sans"),
1064 ..Default::default()
1065 }];
1066
1067 let lines = window_text_system.shape_text(text, rgpui::px(14.0), &runs, None, None)?;
1068
1069 assert_eq!(lines.len(), 2);
1070 assert_eq!(lines[1].len(), "\u{05d0}\u{001c}A".len());
1071 assert!(lines[1].width() > Pixels::ZERO);
1072 Ok(())
1073 }
1074
1075 #[test]
1076 fn layout_line_with_mixed_direction_paragraphs() -> Result<()> {
1077 let text_system = text_system()?;
1078
1079 for separator in SEPARATORS {
1080 for text in [
1081 format!("\u{05d0}{separator}A"),
1082 format!("A{separator}\u{05d0}"),
1083 ] {
1084 let layout = layout_text(&text_system, &text)?;
1085
1086 assert_eq!(layout.len, text.len(), "{text:?}");
1087 assert!(layout.width > Pixels::ZERO, "{text:?}");
1088 assert!(
1089 layout.runs.iter().any(|run| !run.glyphs.is_empty()),
1090 "{text:?}"
1091 );
1092 }
1093 }
1094
1095 Ok(())
1096 }
1097
1098 #[test]
1099 fn layout_line_with_separators_at_line_edges() -> Result<()> {
1100 let text_system = text_system()?;
1101
1102 for text in [
1103 "\u{001c}",
1104 "\u{001c}\u{001c}",
1105 "\u{001c}\u{05d0}",
1106 "\u{05d0}\u{001c}",
1107 "\u{05d0}\u{001c}\u{001c}A",
1108 "\u{001c}\u{05d0}\u{001c}A\u{001c}",
1109 ] {
1110 let layout = layout_text(&text_system, text)?;
1111 assert_eq!(layout.len, text.len(), "{text:?}");
1112 }
1113
1114 Ok(())
1115 }
1116
1117 #[test]
1121 fn layout_line_keeps_indices_and_positions_ordered_across_paragraphs() -> Result<()> {
1122 let text_system = text_system()?;
1123 let text = "ab\u{001c}cd\u{2029}ef";
1124 let layout = layout_text(&text_system, text)?;
1125
1126 let glyphs: Vec<_> = layout.runs.iter().flat_map(|run| &run.glyphs).collect();
1127 assert!(!glyphs.is_empty());
1128
1129 for glyph in &glyphs {
1130 assert!(glyph.index < text.len(), "{:?}", glyph.index);
1131 assert!(text.is_char_boundary(glyph.index), "{:?}", glyph.index);
1132 }
1133 for pair in glyphs.windows(2) {
1134 assert!(pair[0].index < pair[1].index);
1135 assert!(pair[0].position.x <= pair[1].position.x);
1136 }
1137
1138 assert!(layout.width > layout_text(&text_system, "ab")?.width);
1141 Ok(())
1142 }
1143
1144 #[test]
1147 fn layout_line_with_font_run_straddling_a_separator() -> Result<()> {
1148 let text_system = text_system()?;
1149 let font_id = text_system.font_id(&rgpui::font("IBM Plex Sans"))?;
1150 let text = "ab\u{001c}\u{05d0}\u{05d1}";
1151
1152 let runs = [
1154 FontRun {
1155 len: "ab\u{001c}\u{05d0}".len(),
1156 font_id,
1157 },
1158 FontRun {
1159 len: "\u{05d1}".len(),
1160 font_id,
1161 },
1162 ];
1163 let layout = text_system.layout_line(text, rgpui::px(14.0), &runs);
1164
1165 assert_eq!(layout.len, text.len());
1166 assert!(layout.width > Pixels::ZERO);
1167 Ok(())
1168 }
1169
1170 #[test]
1173 fn layout_line_without_separators_takes_fast_path() -> Result<()> {
1174 let text_system = text_system()?;
1175
1176 for text in [
1177 "hello world",
1178 "\u{05d0}\u{05d1}\u{05d2}",
1179 "mixed \u{05d0}\u{05d1}",
1180 ] {
1181 assert!(!contains_paragraph_separator(text), "{text:?}");
1182 let layout = layout_text(&text_system, text)?;
1183 assert_eq!(layout.len, text.len(), "{text:?}");
1184 assert!(layout.width > Pixels::ZERO, "{text:?}");
1185 }
1186
1187 Ok(())
1188 }
1189
1190 #[test]
1191 fn paragraph_separator_detection() {
1192 for separator in SEPARATORS {
1193 assert!(is_paragraph_separator(*separator), "{separator:?}");
1194 assert!(contains_paragraph_separator(&format!("a{separator}b")));
1195 }
1196
1197 for text in [
1198 "",
1199 "plain ascii",
1200 "\u{05d0}",
1201 "tab\there",
1202 "emoji \u{1f600}",
1203 ] {
1204 assert!(!contains_paragraph_separator(text), "{text:?}");
1205 }
1206 }
1207
1208 #[test]
1209 fn font_runs_are_clipped_to_segment() {
1210 let runs = [
1211 FontRun {
1212 len: 3,
1213 font_id: fid(1),
1214 },
1215 FontRun {
1216 len: 4,
1217 font_id: fid(2),
1218 },
1219 ];
1220
1221 assert_eq!(clip_font_runs(&runs, 0..7).as_slice(), &runs);
1222 assert_eq!(
1223 clip_font_runs(&runs, 2..5).as_slice(),
1224 &[
1225 FontRun {
1226 len: 1,
1227 font_id: fid(1)
1228 },
1229 FontRun {
1230 len: 2,
1231 font_id: fid(2)
1232 },
1233 ]
1234 );
1235 assert_eq!(
1236 clip_font_runs(&runs, 3..7).as_slice(),
1237 &[FontRun {
1238 len: 4,
1239 font_id: fid(2)
1240 }]
1241 );
1242 assert!(clip_font_runs(&runs, 5..5).is_empty());
1243 }
1244
1245 #[test]
1246 fn primary_wins_over_current_fallback_when_primary_covers() {
1247 let primary = fid(0);
1248 let fb = chain(&[1, 2]);
1249 let covers = |id: FontId, _: char| id == fid(0) || id == fid(1);
1250 assert_eq!(
1251 pick_covering_slot('a', Some(0), primary, &fb, &covers),
1252 None
1253 );
1254 }
1255
1256 #[test]
1257 fn primary_preferred_over_fallback_when_both_cover() {
1258 let primary = fid(0);
1259 let fb = chain(&[1]);
1260 let covers = |_: FontId, _: char| true;
1261 assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
1262 }
1263
1264 #[test]
1265 fn falls_through_chain_in_order() {
1266 let primary = fid(0);
1267 let fb = chain(&[1, 2, 3]);
1268 let covers = |id: FontId, _: char| id == fid(2);
1270 assert_eq!(
1271 pick_covering_slot('瀛?, None, primary, &fb, &covers),
1272 Some(1)
1273 );
1274 }
1275
1276 #[test]
1277 fn no_coverage_returns_primary() {
1278 let primary = fid(0);
1279 let fb = chain(&[1, 2]);
1280 let covers = |_: FontId, _: char| false;
1281 assert_eq!(
1284 pick_covering_slot('\u{1F600}', Some(1), primary, &fb, &covers),
1285 None
1286 );
1287 }
1288
1289 #[test]
1290 fn empty_chain_always_returns_primary() {
1291 let primary = fid(0);
1292 let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
1293 let covers = |_: FontId, _: char| false;
1294 assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
1295 }
1296
1297 #[test]
1298 fn slot_font_id_resolution() {
1299 let primary = fid(7);
1300 let fb = chain(&[10, 20]);
1301 assert_eq!(slot_font_id(None, primary, &fb), fid(7));
1302 assert_eq!(slot_font_id(Some(0), primary, &fb), fid(10));
1303 assert_eq!(slot_font_id(Some(1), primary, &fb), fid(20));
1304 }
1305
1306 #[test]
1307 fn run_spans_with_no_chain_emit_one_primary_span() {
1308 let primary = fid(0);
1309 let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
1310 let covers = |_: FontId, _: char| false;
1311 let text = "hello";
1312 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1313 assert_eq!(spans.as_slice(), &[span(0, text.len(), None, primary)]);
1314 }
1315
1316 #[test]
1317 fn run_spans_use_byte_offsets_for_multibyte_chars() {
1318 let primary = fid(0);
1319 let fb = chain(&[1]);
1320 let covers = |id: FontId, ch: char| {
1322 if id == primary {
1323 ch.is_ascii()
1324 } else {
1325 !ch.is_ascii()
1326 }
1327 };
1328 let text = "a瀛梑";
1329 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1330 assert_eq!(
1332 spans.as_slice(),
1333 &[
1334 span(0, 1, None, primary),
1335 span(1, 4, Some(0), fid(1)),
1336 span(4, 5, None, primary),
1337 ]
1338 );
1339 }
1340
1341 #[test]
1342 fn run_spans_respect_run_offset() {
1343 let primary = fid(0);
1344 let fb = chain(&[1]);
1345 let covers = |id: FontId, ch: char| {
1346 if id == primary {
1347 ch.is_ascii()
1348 } else {
1349 !ch.is_ascii()
1350 }
1351 };
1352 let text = "xx瀛梱";
1354 let run_offset = 2;
1355 let run_len = text.len() - run_offset;
1356 let spans = compute_run_spans(text, run_offset, run_len, primary, &fb, &covers);
1357 assert_eq!(
1358 spans.as_slice(),
1359 &[span(2, 5, Some(0), fid(1)), span(5, 6, None, primary)]
1360 );
1361 }
1362
1363 #[test]
1364 fn run_spans_keep_combining_marks_with_base_in_fallback() {
1365 let primary = fid(0);
1366 let fb = chain(&[1]);
1367 let covers = |id: FontId, ch: char| {
1371 if id == primary {
1372 ch.is_ascii()
1373 } else {
1374 ch == '\u{0905}'
1375 }
1376 };
1377 let text = "\u{0905}\u{0902}";
1379 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1380 assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1381 }
1382
1383 #[test]
1384 fn run_spans_keep_zwj_inside_emoji_cluster() {
1385 let primary = fid(0);
1386 let fb = chain(&[1]);
1387 let covers = |id: FontId, ch: char| id == fid(1) && ch != '\u{200D}';
1389 let text = "\u{1F469}\u{200D}\u{1F467}";
1391 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1392 assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1393 }
1394
1395 #[test]
1396 fn run_spans_collapse_adjacent_same_slot() {
1397 let primary = fid(0);
1398 let fb = chain(&[1]);
1399 let covers = |id: FontId, ch: char| {
1400 if id == primary {
1401 ch.is_ascii()
1402 } else {
1403 !ch.is_ascii()
1404 }
1405 };
1406 let text = "测试文本";
1407 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1408 assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1409 }
1410
1411 #[test]
1412 fn run_spans_empty_run_returns_no_spans() {
1413 let primary = fid(0);
1414 let fb = chain(&[1]);
1415 let covers = |_: FontId, _: char| true;
1416 let spans = compute_run_spans("anything", 3, 0, primary, &fb, &covers);
1417 assert!(spans.is_empty());
1418 }
1419}