1use anyhow::{Context as _, Ok, Result};
2use collections::HashMap;
3use cosmic_text::{
4 Attrs, AttrsList, Ellipsize, Family, Font as CosmicTextFont,
5 FontFeatures as CosmicFontFeatures, FontSystem, ShapeBuffer, ShapeLine, Stretch, Style, Weight,
6};
7use gpui::{
8 Bounds, DevicePixels, FallbackFontClass, Font, FontFallbacks, FontFeatures, FontId,
9 FontMetrics, FontRun, GlyphId, IsZero as _, LineLayout, MissingGlyph, MissingGlyphSink, Pixels,
10 PlatformTextSystem, RenderGlyphParams, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ShapedGlyph,
11 ShapedRun, SharedString, Size, TextRenderingMode, point, size,
12};
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
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44struct LoadedFontKey {
45 database_id: cosmic_text::fontdb::ID,
46 font: FontKey,
47}
48
49struct CosmicTextSystemState {
50 font_system: FontSystem,
51 scratch: ShapeBuffer,
52 swash_scale_context: ScaleContext,
53 pending_glyph_images: HashMap<RenderGlyphParams, swash::scale::image::Image>,
54 loaded_fonts: Vec<LoadedFont>,
56 loaded_font_ids_by_key: HashMap<LoadedFontKey, FontId>,
57 font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
60 system_font_fallback: String,
61 missing_glyph_sink: Option<Arc<dyn MissingGlyphSink>>,
62}
63
64struct LoadedFont {
65 font: Arc<CosmicTextFont>,
66 features: CosmicFontFeatures,
67 is_known_emoji_font: bool,
68 user_fallback_chain: Arc<[(FontId, SharedString)]>,
71}
72
73struct FontMatchProperties {
74 primary_family_name: SharedString,
75 stretch: Stretch,
76 style: Style,
77 weight: Weight,
78 features: CosmicFontFeatures,
79 fallback_chain: Arc<[(FontId, SharedString)]>,
80}
81
82impl FontMatchProperties {
83 fn attributes<'a>(&'a self, font_id: FontId, family_name: &'a str) -> Attrs<'a> {
84 Attrs::new()
85 .metadata(font_id.0)
86 .family(Family::Name(family_name))
87 .stretch(self.stretch)
88 .style(self.style)
89 .weight(self.weight)
90 .font_features(self.features.clone())
91 }
92}
93
94impl CosmicTextSystem {
95 pub fn font_weight_and_style(
97 &self,
98 font_id: FontId,
99 ) -> Result<(gpui::FontWeight, gpui::FontStyle)> {
100 let state = self.0.read();
101 let font = state
102 .loaded_fonts
103 .get(font_id.0)
104 .context("invalid font ID")?;
105 let face = state
106 .font_system
107 .db()
108 .face(font.font.id())
109 .context("font face not found")?;
110 let style = match face.style {
111 cosmic_text::Style::Normal => gpui::FontStyle::Normal,
112 cosmic_text::Style::Italic => gpui::FontStyle::Italic,
113 cosmic_text::Style::Oblique => gpui::FontStyle::Oblique,
114 };
115 Ok((gpui::FontWeight(face.weight.0 as f32), style))
116 }
117
118 pub fn missing_glyphs(
121 &self,
122 text: &str,
123 font_runs: &[FontRun],
124 missing_text_indices: impl IntoIterator<Item = usize>,
125 ) -> Vec<MissingGlyph> {
126 self.0
127 .read()
128 .missing_glyphs(text, font_runs, missing_text_indices)
129 }
130
131 pub fn new(system_font_fallback: &str) -> Self {
132 let font_system = FontSystem::new();
133
134 Self(RwLock::new(CosmicTextSystemState {
135 font_system,
136 scratch: ShapeBuffer::default(),
137 swash_scale_context: ScaleContext::new(),
138 pending_glyph_images: HashMap::default(),
139 loaded_fonts: Vec::new(),
140 loaded_font_ids_by_key: HashMap::default(),
141 font_ids_by_family_cache: HashMap::default(),
142 system_font_fallback: system_font_fallback.to_string(),
143 missing_glyph_sink: None,
144 }))
145 }
146
147 pub fn new_without_system_fonts(system_font_fallback: &str) -> Self {
148 let font_system = FontSystem::new_with_locale_and_db(
149 "en-US".to_string(),
150 cosmic_text::fontdb::Database::new(),
151 );
152
153 Self(RwLock::new(CosmicTextSystemState {
154 font_system,
155 scratch: ShapeBuffer::default(),
156 swash_scale_context: ScaleContext::new(),
157 pending_glyph_images: HashMap::default(),
158 loaded_fonts: Vec::new(),
159 loaded_font_ids_by_key: HashMap::default(),
160 font_ids_by_family_cache: HashMap::default(),
161 system_font_fallback: system_font_fallback.to_string(),
162 missing_glyph_sink: None,
163 }))
164 }
165}
166
167impl PlatformTextSystem for CosmicTextSystem {
168 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
169 self.0.write().add_fonts(fonts)
170 }
171
172 fn set_missing_glyph_sink(&self, sink: Option<Arc<dyn MissingGlyphSink>>) {
173 self.0.write().missing_glyph_sink = sink;
174 }
175
176 fn all_font_names(&self) -> Vec<String> {
177 let mut result = self
178 .0
179 .read()
180 .font_system
181 .db()
182 .faces()
183 .filter_map(|face| face.families.first().map(|family| family.0.clone()))
184 .collect_vec();
185 result.sort_unstable();
186 result.dedup();
187 result
188 }
189
190 fn font_id(&self, font: &Font) -> Result<FontId> {
191 let mut state = self.0.write();
192 let key = FontKey::new(
193 font.family.clone(),
194 font.features.clone(),
195 font.fallbacks.clone(),
196 );
197 let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
198 font_ids.as_slice()
199 } else {
200 let font_ids =
201 state.load_family(&font.family, &font.features, font.fallbacks.as_ref())?;
202 state.font_ids_by_family_cache.insert(key.clone(), font_ids);
203 state.font_ids_by_family_cache[&key].as_ref()
204 };
205
206 let ix = find_best_match(font, candidates, &state)?;
207
208 Ok(candidates[ix])
209 }
210
211 fn prewarm_fonts(&self, font_ids: &[FontId]) {
212 self.0.write().prewarm_fonts(font_ids);
213 }
214
215 fn font_metrics(&self, font_id: FontId) -> FontMetrics {
216 let metrics = self
217 .0
218 .read()
219 .loaded_font(font_id)
220 .font
221 .as_swash()
222 .metrics(&[]);
223
224 FontMetrics {
225 units_per_em: metrics.units_per_em as u32,
226 ascent: metrics.ascent,
227 descent: -metrics.descent,
228 line_gap: metrics.leading,
229 underline_position: metrics.underline_offset,
230 underline_thickness: metrics.stroke_size,
231 cap_height: metrics.cap_height,
232 x_height: metrics.x_height,
233 bounding_box: Bounds {
234 origin: point(0.0, 0.0),
235 size: size(metrics.max_width, metrics.ascent + metrics.descent),
236 },
237 }
238 }
239
240 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
241 let lock = self.0.read();
242 let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
243 let glyph_id = glyph_id.0 as u16;
244 Ok(Bounds {
245 origin: point(0.0, 0.0),
246 size: size(
247 glyph_metrics.advance_width(glyph_id),
248 glyph_metrics.advance_height(glyph_id),
249 ),
250 })
251 }
252
253 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
254 self.0.read().advance(font_id, glyph_id)
255 }
256
257 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
258 self.0.read().glyph_for_char(font_id, ch)
259 }
260
261 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
262 self.0.write().raster_bounds(params)
263 }
264
265 fn rasterize_glyph(
266 &self,
267 params: &RenderGlyphParams,
268 raster_bounds: Bounds<DevicePixels>,
269 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
270 self.0.write().rasterize_glyph(params, raster_bounds)
271 }
272
273 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
274 self.0.write().layout_line(text, font_size, runs)
275 }
276
277 fn recommended_rendering_mode(
278 &self,
279 _font_id: FontId,
280 _font_size: Pixels,
281 ) -> TextRenderingMode {
282 TextRenderingMode::Subpixel
283 }
284}
285
286impl CosmicTextSystemState {
287 fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
288 &self.loaded_fonts[font_id.0]
289 }
290
291 fn font_match_properties(&self, font_id: FontId) -> Option<FontMatchProperties> {
292 let loaded_font = self.loaded_font(font_id);
293 let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
294 log::warn!("font face not found in database for font_id {:?}", font_id);
295 return None;
296 };
297 let Some(first_family) = face.families.first() else {
298 log::warn!("font face has no family names for font_id {:?}", font_id);
299 return None;
300 };
301
302 Some(FontMatchProperties {
303 primary_family_name: first_family.0.clone().into(),
304 stretch: face.stretch,
305 style: face.style,
306 weight: face.weight,
307 features: loaded_font.features.clone(),
308 fallback_chain: Arc::clone(&loaded_font.user_fallback_chain),
309 })
310 }
311
312 fn prewarm_fonts(&mut self, font_ids: &[FontId]) {
313 for &font_id in font_ids {
314 let Some(properties) = self.font_match_properties(font_id) else {
315 continue;
316 };
317 let primary_attributes =
318 properties.attributes(font_id, &properties.primary_family_name);
319 self.font_system.get_font_matches(&primary_attributes);
320
321 for (fallback_id, fallback_name) in &*properties.fallback_chain {
322 let fallback_attributes = properties.attributes(*fallback_id, fallback_name);
323 self.font_system.get_font_matches(&fallback_attributes);
324 }
325 }
326 }
327
328 #[profiling::function]
329 fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
330 self.font_ids_by_family_cache.clear();
331 let db = self.font_system.db_mut();
332 for bytes in fonts {
333 db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new(bytes)));
334 }
335 Ok(())
336 }
337
338 #[profiling::function]
339 fn load_family(
340 &mut self,
341 name: &str,
342 features: &FontFeatures,
343 fallbacks: Option<&FontFallbacks>,
344 ) -> Result<SmallVec<[FontId; 4]>> {
345 let loaded_font_key = FontKey::new(
346 SharedString::from(name.to_owned()),
347 features.clone(),
348 fallbacks.cloned(),
349 );
350
351 let user_fallback_chain: Arc<[(FontId, SharedString)]> = match fallbacks {
355 Some(fallbacks) if !fallbacks.fallback_list().is_empty() => {
356 let mut chain: Vec<(FontId, SharedString)> = Vec::new();
357 for fallback_name in fallbacks.fallback_list() {
358 let fb_key = FontKey::new(
359 SharedString::from(fallback_name.clone()),
360 features.clone(),
361 None,
362 );
363 let fb_ids = if let Some(cached) = self.font_ids_by_family_cache.get(&fb_key) {
364 cached.clone()
365 } else {
366 let loaded = self.load_family(fallback_name, features, None)?;
367 self.font_ids_by_family_cache
368 .insert(fb_key.clone(), loaded.clone());
369 loaded
370 };
371 let Some(&fb_id) = fb_ids.first() else {
372 continue;
373 };
374 let db_id = self.loaded_fonts[fb_id.0].font.id();
375 if let Some(face) = self.font_system.db().face(db_id)
376 && let Some(family) = face.families.first()
377 {
378 chain.push((fb_id, SharedString::from(family.0.clone())));
379 }
380 }
381 Arc::from(chain)
382 }
383 _ => Arc::from(Vec::new()),
384 };
385
386 let name = gpui::font_name_with_fallbacks(name, &self.system_font_fallback);
387
388 let families = self
389 .font_system
390 .db()
391 .faces()
392 .filter(|face| face.families.iter().any(|family| *name == family.0))
393 .map(|face| (face.id, face.post_script_name.clone()))
394 .collect::<SmallVec<[_; 4]>>();
395
396 let cosmic_features = cosmic_font_features(features)?;
397
398 let mut loaded_font_ids = SmallVec::new();
399 for (database_id, postscript_name) in families {
400 let key = LoadedFontKey {
401 database_id,
402 font: loaded_font_key.clone(),
403 };
404 if let Some(&font_id) = self.loaded_font_ids_by_key.get(&key) {
405 self.loaded_fonts[font_id.0].user_fallback_chain = Arc::clone(&user_fallback_chain);
406 loaded_font_ids.push(font_id);
407 continue;
408 }
409
410 let font = self
411 .font_system
412 .get_font(database_id, cosmic_text::Weight::NORMAL)
413 .context("Could not load font")?;
414
415 let allowed_bad_font_names = [
417 "SegoeFluentIcons", "Segoe Fluent Icons",
419 ];
420
421 if font.as_swash().charmap().map('m') == 0
430 && !allowed_bad_font_names.contains(&postscript_name.as_str())
431 {
432 continue;
433 };
434
435 let font_id = FontId(self.loaded_fonts.len());
436 loaded_font_ids.push(font_id);
437 self.loaded_fonts.push(LoadedFont {
438 font,
439 features: cosmic_features.clone(),
440 is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
441 user_fallback_chain: Arc::clone(&user_fallback_chain),
442 });
443 self.loaded_font_ids_by_key.insert(key, font_id);
444 }
445
446 Ok(loaded_font_ids)
447 }
448
449 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
450 let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
451 Ok(Size {
452 width: glyph_metrics.advance_width(glyph_id.0 as u16),
453 height: glyph_metrics.advance_height(glyph_id.0 as u16),
454 })
455 }
456
457 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
458 let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
459 if glyph_id == 0 {
460 None
461 } else {
462 Some(GlyphId(glyph_id.into()))
463 }
464 }
465
466 fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
467 let image = self.render_glyph_image(params)?;
468 let bounds = Bounds {
469 origin: point(image.placement.left.into(), (-image.placement.top).into()),
470 size: size(image.placement.width.into(), image.placement.height.into()),
471 };
472 if !bounds.is_zero() {
473 self.pending_glyph_images.insert(params.clone(), image);
474 }
475 Ok(bounds)
476 }
477
478 #[profiling::function]
479 fn rasterize_glyph(
480 &mut self,
481 params: &RenderGlyphParams,
482 glyph_bounds: Bounds<DevicePixels>,
483 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
484 if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
485 anyhow::bail!("glyph bounds are empty");
486 }
487
488 let mut image = match self.pending_glyph_images.remove(params) {
489 Some(image) => image,
490 None => self.render_glyph_image(params)?,
491 };
492 let bitmap_size = glyph_bounds.size;
493 match image.content {
494 swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
495 for pixel in image.data.chunks_exact_mut(4) {
497 pixel.swap(0, 2);
498 }
499 Ok((bitmap_size, image.data))
500 }
501 swash::scale::image::Content::Mask => {
502 if params.subpixel_rendering {
503 let expanded = image.data.iter().flat_map(|&a| [a, a, a, a]).collect();
505 Ok((bitmap_size, expanded))
506 } else {
507 Ok((bitmap_size, image.data))
508 }
509 }
510 }
511 }
512
513 fn render_glyph_image(
514 &mut self,
515 params: &RenderGlyphParams,
516 ) -> Result<swash::scale::image::Image> {
517 let loaded_font = &self.loaded_fonts[params.font_id.0];
518 let font_ref = loaded_font.font.as_swash();
519 let pixel_size = f32::from(params.font_size);
520
521 let subpixel_offset = Vector::new(
522 params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
523 params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
524 );
525
526 let mut scaler = self
527 .swash_scale_context
528 .builder(font_ref)
529 .size(pixel_size * params.scale_factor)
530 .hint(true)
531 .build();
532
533 let sources: &[Source] = if params.is_emoji {
534 &[
535 Source::ColorOutline(0),
536 Source::ColorBitmap(StrikeWith::BestFit),
537 Source::Outline,
538 ]
539 } else {
540 &[Source::Bitmap(StrikeWith::ExactSize), Source::Outline]
541 };
542
543 let mut renderer = Render::new(sources);
544 if params.subpixel_rendering {
545 renderer
547 .format(Format::subpixel_bgra())
548 .offset(subpixel_offset);
549 } else {
550 renderer.format(Format::Alpha).offset(subpixel_offset);
551 }
552
553 let glyph_id: u16 = params.glyph_id.0.try_into()?;
554 renderer
555 .render(&mut scaler, glyph_id)
556 .with_context(|| format!("unable to render glyph via swash for {params:?}"))
557 }
558
559 fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> Result<FontId> {
568 if let Some(ix) = self
569 .loaded_fonts
570 .iter()
571 .position(|loaded_font| loaded_font.font.id() == id)
572 {
573 Ok(FontId(ix))
574 } else {
575 let font = self
576 .font_system
577 .get_font(id, cosmic_text::Weight::NORMAL)
578 .context("failed to get fallback font from cosmic-text font system")?;
579 let face = self
580 .font_system
581 .db()
582 .face(id)
583 .context("fallback font face not found in cosmic-text database")?;
584
585 let font_id = FontId(self.loaded_fonts.len());
586 self.loaded_fonts.push(LoadedFont {
587 font,
588 features: CosmicFontFeatures::new(),
589 is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
590 user_fallback_chain: Arc::from(Vec::new()),
591 });
592
593 Ok(font_id)
594 }
595 }
596
597 #[profiling::function]
598 fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
599 if contains_paragraph_separator(text) {
600 self.layout_line_with_separators(text, font_size, font_runs)
601 } else {
602 self.layout_line_no_separators(text, font_size, font_runs)
603 }
604 }
605
606 fn layout_line_with_separators(
607 &mut self,
608 text: &str,
609 font_size: Pixels,
610 font_runs: &[FontRun],
611 ) -> LineLayout {
612 let mut layout = LineLayout {
613 font_size,
614 len: text.len(),
615 ..Default::default()
616 };
617 let mut paragraph_start = 0;
618
619 for (separator_start, separator) in text
620 .char_indices()
621 .filter(|(_, character)| is_paragraph_separator(*character))
622 {
623 let separator_end = separator_start + separator.len_utf8();
624 self.shape_segment(
625 text,
626 paragraph_start..separator_start,
627 font_size,
628 font_runs,
629 &mut layout,
630 );
631 self.shape_segment(
632 text,
633 separator_start..separator_end,
634 font_size,
635 font_runs,
636 &mut layout,
637 );
638 paragraph_start = separator_end;
639 }
640
641 self.shape_segment(
642 text,
643 paragraph_start..text.len(),
644 font_size,
645 font_runs,
646 &mut layout,
647 );
648
649 layout
650 }
651
652 fn shape_segment(
653 &mut self,
654 text: &str,
655 range: Range<usize>,
656 font_size: Pixels,
657 font_runs: &[FontRun],
658 layout: &mut LineLayout,
659 ) {
660 if range.is_empty() {
661 return;
662 }
663
664 let segment_font_runs = clip_font_runs(font_runs, range.clone());
665 let segment =
666 self.layout_line_no_separators(&text[range.clone()], font_size, &segment_font_runs);
667
668 let mut segment_runs = segment.runs;
669 for run in &mut segment_runs {
670 for glyph in &mut run.glyphs {
671 glyph.index += range.start;
672 glyph.position.x += layout.width;
673 }
674 }
675
676 for mut run in segment_runs {
677 if let Some(same_run) = layout
678 .runs
679 .last_mut()
680 .filter(|last| last.font_id == run.font_id)
681 {
682 same_run.glyphs.append(&mut run.glyphs);
683 } else {
684 layout.runs.push(run);
685 }
686 }
687
688 layout.width += segment.width;
689 layout.ascent = layout.ascent.max(segment.ascent);
690 layout.descent = layout.descent.max(segment.descent);
691 }
692
693 fn layout_line_no_separators(
694 &mut self,
695 text: &str,
696 font_size: Pixels,
697 font_runs: &[FontRun],
698 ) -> LineLayout {
699 let mut attrs_list = AttrsList::new(&Attrs::new());
700 let mut offs = 0;
701 for run in font_runs {
702 let run_end = offs + run.len;
703
704 let Some(properties) = self.font_match_properties(run.font_id) else {
705 offs = run_end;
706 continue;
707 };
708
709 let primary_attrs = properties.attributes(run.font_id, &properties.primary_family_name);
710 let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = properties
711 .fallback_chain
712 .iter()
713 .map(|(font_id, family_name)| properties.attributes(*font_id, family_name))
714 .collect();
715
716 let spans = if properties.fallback_chain.is_empty() {
717 let mut spans = SmallVec::<[RunSpan; 4]>::new();
718 spans.push(RunSpan {
719 start: offs,
720 end: run_end,
721 slot: None,
722 font_id: run.font_id,
723 });
724 spans
725 } else {
726 let loaded_fonts = &self.loaded_fonts;
727 let covers = |id: FontId, ch: char| charmap_covers(loaded_fonts, id, ch);
728 compute_run_spans(
729 text,
730 offs,
731 run.len,
732 run.font_id,
733 &properties.fallback_chain,
734 &covers,
735 )
736 };
737
738 for span in spans {
739 let attrs = match span.slot {
740 None => &primary_attrs,
741 Some(ix) => &fallback_attrs[ix],
742 };
743 attrs_list.add_span(span.start..span.end, attrs);
744 }
745 offs = run_end;
746 }
747
748 let line = ShapeLine::new(
749 &mut self.font_system,
750 text,
751 &attrs_list,
752 cosmic_text::Shaping::Advanced,
753 4,
754 );
755 let mut layout_lines = Vec::with_capacity(1);
756 line.layout_to_buffer(
757 &mut self.scratch,
758 f32::from(font_size),
759 None, cosmic_text::Wrap::None,
761 Ellipsize::None,
762 None,
763 &mut layout_lines,
764 None,
765 cosmic_text::Hinting::Disabled,
766 );
767
768 let Some(layout) = layout_lines.first() else {
769 return LineLayout {
770 font_size,
771 width: Pixels::ZERO,
772 ascent: Pixels::ZERO,
773 descent: Pixels::ZERO,
774 runs: Vec::new(),
775 len: text.len(),
776 };
777 };
778
779 let missing_glyphs = self.missing_glyph_sink.as_ref().map(|_| {
780 self.missing_glyphs(
781 text,
782 font_runs,
783 layout
784 .glyphs
785 .iter()
786 .filter(|glyph| glyph.glyph_id == 0)
787 .map(|glyph| glyph.start),
788 )
789 });
790
791 let mut runs: Vec<ShapedRun> = Vec::new();
792 for glyph in &layout.glyphs {
793 let mut font_id = FontId(glyph.metadata);
794 let mut loaded_font = self.loaded_font(font_id);
795 if loaded_font.font.id() != glyph.font_id {
796 match self.font_id_for_cosmic_id(glyph.font_id) {
797 std::result::Result::Ok(resolved_id) => {
798 font_id = resolved_id;
799 loaded_font = self.loaded_font(font_id);
800 }
801 Err(error) => {
802 log::warn!(
803 "failed to resolve cosmic font id {:?}: {error:#}",
804 glyph.font_id
805 );
806 continue;
807 }
808 }
809 }
810 let is_emoji = loaded_font.is_known_emoji_font;
811
812 if glyph.glyph_id == 3 && is_emoji {
814 continue;
815 }
816
817 let shaped_glyph = ShapedGlyph {
818 id: GlyphId(glyph.glyph_id as u32),
819 position: point(glyph.x.into(), glyph.y.into()),
820 index: glyph.start,
821 is_emoji,
822 };
823
824 if let Some(last_run) = runs
825 .last_mut()
826 .filter(|last_run| last_run.font_id == font_id)
827 {
828 last_run.glyphs.push(shaped_glyph);
829 } else {
830 runs.push(ShapedRun {
831 font_id,
832 glyphs: vec![shaped_glyph],
833 });
834 }
835 }
836
837 if let Some((sink, missing_glyphs)) = self.missing_glyph_sink.as_ref().zip(missing_glyphs) {
838 sink.report(missing_glyphs);
839 }
840
841 LineLayout {
842 font_size,
843 width: layout.w.into(),
844 ascent: layout.max_ascent.into(),
845 descent: layout.max_descent.into(),
846 runs,
847 len: text.len(),
848 }
849 }
850
851 fn missing_glyphs(
852 &self,
853 text: &str,
854 font_runs: &[FontRun],
855 missing_text_indices: impl IntoIterator<Item = usize>,
856 ) -> Vec<MissingGlyph> {
857 let mut missing_text_indices = missing_text_indices.into_iter().peekable();
858 if missing_text_indices.peek().is_none() {
859 return Vec::new();
860 }
861 let mut missing_text_indices = missing_text_indices.collect::<Vec<_>>();
862 missing_text_indices.sort_unstable();
863 missing_text_indices.dedup();
864
865 let mut font_run_index = 0;
866 let mut font_run_end = font_runs.first().map_or(0, |font_run| font_run.len);
867 let mut missing_glyphs = Vec::new();
868 let mut missing_index = 0;
869 for (grapheme_start, grapheme) in text.grapheme_indices(true) {
870 let grapheme_end = grapheme_start + grapheme.len();
871 while missing_text_indices
872 .get(missing_index)
873 .is_some_and(|text_index| *text_index < grapheme_start)
874 {
875 missing_index += 1;
876 }
877 let Some(&text_index) = missing_text_indices.get(missing_index) else {
878 break;
879 };
880 if text_index >= grapheme_end {
881 continue;
882 }
883
884 while font_run_end <= text_index && font_run_index + 1 < font_runs.len() {
885 font_run_index += 1;
886 let Some(font_run) = font_runs.get(font_run_index) else {
887 break;
888 };
889 font_run_end += font_run.len;
890 }
891 let font_class = self.fallback_font_class(
892 font_runs
893 .get(font_run_index)
894 .or_else(|| font_runs.last())
895 .map(|font_run| font_run.font_id),
896 );
897 missing_glyphs.push(MissingGlyph::new(grapheme.into(), font_class));
898 while missing_text_indices
899 .get(missing_index)
900 .is_some_and(|text_index| *text_index < grapheme_end)
901 {
902 missing_index += 1;
903 }
904 }
905 missing_glyphs
906 }
907
908 fn fallback_font_class(&self, font_id: Option<FontId>) -> FallbackFontClass {
909 let Some(font_id) = font_id else {
910 return FallbackFontClass::Proportional;
911 };
912 let loaded_font = self.loaded_font(font_id);
913 let is_monospace = self
914 .font_system
915 .db()
916 .face(loaded_font.font.id())
917 .is_some_and(|face| face.monospaced);
918 if is_monospace {
919 FallbackFontClass::Monospace
920 } else {
921 FallbackFontClass::Proportional
922 }
923 }
924}
925
926#[inline(always)]
927fn is_paragraph_separator(character: char) -> bool {
928 unicode_bidi::bidi_class(character) == unicode_bidi::BidiClass::B
929}
930
931fn contains_paragraph_separator(text: &str) -> bool {
932 if text
933 .bytes()
934 .any(|byte| matches!(byte, b'\n' | b'\r' | 0x1c | 0x1d | 0x1e))
935 {
936 return true;
937 }
938
939 !text.is_ascii() && text.chars().any(is_paragraph_separator)
940}
941
942fn clip_font_runs(font_runs: &[FontRun], range: Range<usize>) -> SmallVec<[FontRun; 4]> {
943 let mut clipped = SmallVec::new();
944 let mut offs = 0;
945 for run in font_runs {
946 let run_start = offs;
947 offs += run.len;
948 if offs <= range.start {
949 continue;
950 }
951 if run_start >= range.end {
952 break;
953 }
954 let start = run_start.max(range.start);
955 let end = offs.min(range.end);
956 if start < end {
957 clipped.push(FontRun {
958 len: end - start,
959 font_id: run.font_id,
960 });
961 }
962 }
963 clipped
964}
965
966#[cfg(feature = "font-kit")]
967fn find_best_match(
968 font: &Font,
969 candidates: &[FontId],
970 state: &CosmicTextSystemState,
971) -> Result<usize> {
972 let candidate_properties = candidates
973 .iter()
974 .map(|font_id| {
975 let database_id = state.loaded_font(*font_id).font.id();
976 let face_info = state
977 .font_system
978 .db()
979 .face(database_id)
980 .context("font face not found in database")?;
981 Ok(face_info_into_properties(face_info))
982 })
983 .collect::<Result<SmallVec<[_; 4]>>>()?;
984
985 let ix =
986 font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
987 .context("requested font family contains no font matching the other parameters")?;
988
989 Ok(ix)
990}
991
992#[cfg(not(feature = "font-kit"))]
993fn find_best_match(
994 font: &Font,
995 candidates: &[FontId],
996 state: &CosmicTextSystemState,
997) -> Result<usize> {
998 if candidates.is_empty() {
999 anyhow::bail!("requested font family contains no font matching the other parameters");
1000 }
1001 if candidates.len() == 1 {
1002 return Ok(0);
1003 }
1004
1005 let target_weight = font.weight.0;
1006 let target_italic = matches!(
1007 font.style,
1008 gpui::FontStyle::Italic | gpui::FontStyle::Oblique
1009 );
1010
1011 let mut best_index = 0;
1012 let mut best_score = u32::MAX;
1013
1014 for (index, font_id) in candidates.iter().enumerate() {
1015 let database_id = state.loaded_font(*font_id).font.id();
1016 let face_info = state
1017 .font_system
1018 .db()
1019 .face(database_id)
1020 .context("font face not found in database")?;
1021
1022 let is_italic = matches!(
1023 face_info.style,
1024 cosmic_text::Style::Italic | cosmic_text::Style::Oblique
1025 );
1026 let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
1027 let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
1028 let score = style_penalty + weight_diff;
1029
1030 if score < best_score {
1031 best_score = score;
1032 best_index = index;
1033 }
1034 }
1035
1036 Ok(best_index)
1037}
1038
1039#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1042struct RunSpan {
1043 start: usize,
1044 end: usize,
1045 slot: Option<usize>,
1046 font_id: FontId,
1047}
1048
1049fn compute_run_spans(
1053 text: &str,
1054 run_offset: usize,
1055 run_len: usize,
1056 primary: FontId,
1057 fallback_chain: &[(FontId, SharedString)],
1058 covers: &impl Fn(FontId, char) -> bool,
1059) -> SmallVec<[RunSpan; 4]> {
1060 let mut spans = SmallVec::new();
1061 let run_end = run_offset + run_len;
1062 if run_end <= run_offset {
1063 return spans;
1064 }
1065 if fallback_chain.is_empty() {
1066 spans.push(RunSpan {
1067 start: run_offset,
1068 end: run_end,
1069 slot: None,
1070 font_id: primary,
1071 });
1072 return spans;
1073 }
1074 let run_text = &text[run_offset..run_end];
1075 let mut span_start = run_offset;
1076 let mut span_slot: Option<usize> = None;
1077 let mut span_font_id = primary;
1078 for (grapheme_idx, grapheme) in run_text.grapheme_indices(true) {
1079 let abs = run_offset + grapheme_idx;
1080 let ch = grapheme.chars().next().unwrap_or('\0');
1081 let next_slot = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);
1082 if next_slot == span_slot {
1083 continue;
1084 }
1085 if abs > span_start {
1086 spans.push(RunSpan {
1087 start: span_start,
1088 end: abs,
1089 slot: span_slot,
1090 font_id: span_font_id,
1091 });
1092 }
1093 span_start = abs;
1094 span_slot = next_slot;
1095 span_font_id = slot_font_id(next_slot, primary, fallback_chain);
1096 }
1097 if span_start < run_end {
1098 spans.push(RunSpan {
1099 start: span_start,
1100 end: run_end,
1101 slot: span_slot,
1102 font_id: span_font_id,
1103 });
1104 }
1105 spans
1106}
1107
1108fn slot_font_id(
1109 slot: Option<usize>,
1110 primary: FontId,
1111 fallback_chain: &[(FontId, SharedString)],
1112) -> FontId {
1113 match slot {
1114 None => primary,
1115 Some(ix) => fallback_chain[ix].0,
1116 }
1117}
1118
1119fn pick_covering_slot(
1120 ch: char,
1121 current: Option<usize>,
1122 primary: FontId,
1123 fallback_chain: &[(FontId, SharedString)],
1124 covers: &impl Fn(FontId, char) -> bool,
1125) -> Option<usize> {
1126 if (ch as u32) <= 0x7F {
1127 return None;
1128 }
1129 if covers(primary, ch) {
1130 return None;
1131 }
1132 let current_id = slot_font_id(current, primary, fallback_chain);
1133 if covers(current_id, ch) {
1134 return current;
1135 }
1136
1137 fallback_chain
1138 .iter()
1139 .position(|(fb_id, _)| covers(*fb_id, ch))
1140}
1141
1142fn charmap_covers(loaded_fonts: &[LoadedFont], id: FontId, ch: char) -> bool {
1143 loaded_fonts
1144 .get(id.0)
1145 .is_some_and(|loaded| loaded.font.as_swash().charmap().map(ch) != 0)
1146}
1147
1148fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
1149 let mut result = CosmicFontFeatures::new();
1150 for feature in features.0.iter() {
1151 let name_bytes: [u8; 4] = feature
1152 .0
1153 .as_bytes()
1154 .try_into()
1155 .context("Incorrect feature flag format")?;
1156
1157 let tag = cosmic_text::FeatureTag::new(&name_bytes);
1158
1159 result.set(tag, feature.1);
1160 }
1161 Ok(result)
1162}
1163
1164#[cfg(feature = "font-kit")]
1165fn font_into_properties(font: &gpui::Font) -> font_kit::properties::Properties {
1166 font_kit::properties::Properties {
1167 style: match font.style {
1168 gpui::FontStyle::Normal => font_kit::properties::Style::Normal,
1169 gpui::FontStyle::Italic => font_kit::properties::Style::Italic,
1170 gpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
1171 },
1172 weight: font_kit::properties::Weight(font.weight.0),
1173 stretch: Default::default(),
1174 }
1175}
1176
1177#[cfg(feature = "font-kit")]
1178fn face_info_into_properties(
1179 face_info: &cosmic_text::fontdb::FaceInfo,
1180) -> font_kit::properties::Properties {
1181 font_kit::properties::Properties {
1182 style: match face_info.style {
1183 cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
1184 cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
1185 cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
1186 },
1187 weight: font_kit::properties::Weight(face_info.weight.0.into()),
1188 stretch: match face_info.stretch {
1189 cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
1190 cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
1191 cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
1192 cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
1193 cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
1194 cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
1195 cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
1196 cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
1197 cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
1198 },
1199 }
1200}
1201
1202fn check_is_known_emoji_font(postscript_name: &str) -> bool {
1203 postscript_name == "NotoColorEmoji"
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210
1211 #[cfg(feature = "test-support")]
1212 use std::{cell::RefCell, rc::Rc};
1213 use std::{
1214 sync::mpsc,
1215 thread,
1216 time::{Duration, Instant},
1217 };
1218
1219 #[test]
1220 fn all_font_names_tracks_available_families() -> Result<()> {
1221 let text_system = gpui::TextSystem::new(Arc::new(
1222 CosmicTextSystem::new_without_system_fonts("IBM Plex Sans"),
1223 ));
1224 assert!(text_system.all_font_names().is_empty());
1225
1226 text_system.add_fonts(vec![Cow::Borrowed(include_bytes!(
1227 "../../../assets/fonts/lilex/Lilex-Regular.ttf"
1228 ))])?;
1229 assert_eq!(text_system.all_font_names(), ["Lilex"]);
1230
1231 text_system.add_fonts(vec![
1232 Cow::Borrowed(IBM_PLEX),
1233 Cow::Borrowed(include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf")),
1234 ])?;
1235 assert_eq!(text_system.all_font_names(), ["IBM Plex Sans", "Lilex"]);
1236 Ok(())
1237 }
1238
1239 fn fid(i: usize) -> FontId {
1240 FontId(i)
1241 }
1242
1243 fn chain(ids: &[usize]) -> SmallVec<[(FontId, SharedString); 4]> {
1244 ids.iter()
1245 .map(|&i| (fid(i), SharedString::from(format!("fb{i}"))))
1246 .collect()
1247 }
1248
1249 fn span(start: usize, end: usize, slot: Option<usize>, font_id: FontId) -> RunSpan {
1250 RunSpan {
1251 start,
1252 end,
1253 slot,
1254 font_id,
1255 }
1256 }
1257
1258 const IBM_PLEX: &[u8] =
1259 include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
1260 const LILEX: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf");
1261
1262 const SEPARATORS: &[char] = &[
1265 '\u{000a}', '\u{000d}', '\u{001c}', '\u{001d}', '\u{001e}', '\u{0085}', '\u{2029}',
1266 ];
1267
1268 fn text_system() -> Result<CosmicTextSystem> {
1269 let text_system = CosmicTextSystem::new_without_system_fonts("IBM Plex Sans");
1270 text_system.add_fonts(vec![Cow::Borrowed(IBM_PLEX)])?;
1271 Ok(text_system)
1272 }
1273
1274 #[test]
1275 fn cached_font_resolution_does_not_wait_for_cold_lookups() -> Result<()> {
1276 for family in [".ZedSans", "Missing Font"] {
1277 let platform_text_system = Arc::new(text_system()?);
1278 let text_system = gpui::TextSystem::new(platform_text_system.clone());
1279 let cached = gpui::font(family);
1280 let expected = platform_text_system.font_id(&gpui::font(".ZedSans"))?;
1281 assert_eq!(text_system.resolve_font(&cached), expected);
1282 let (sender, receiver) = mpsc::channel();
1283
1284 thread::scope(|scope| {
1285 let backend = platform_text_system.0.read();
1286 let deadline = Instant::now() + Duration::from_secs(5);
1287 let cold = scope.spawn(|| text_system.resolve_font(&cached.clone().bold()));
1288 while !platform_text_system.0.is_locked_exclusive() && Instant::now() < deadline {
1289 thread::yield_now();
1290 }
1291 let cold_waiting = platform_text_system.0.is_locked_exclusive();
1292 let warm = scope.spawn(|| sender.send(text_system.resolve_font(&cached)));
1293 let result =
1294 receiver.recv_timeout(deadline.saturating_duration_since(Instant::now()));
1295 drop(backend);
1296 let cold_result = cold.join();
1297 let warm_result = warm.join();
1298
1299 assert!(cold_waiting);
1300 assert_eq!(result.expect("cached lookup blocked"), expected);
1301 assert_eq!(cold_result.expect("cold lookup panicked"), expected);
1302 warm_result
1303 .expect("warm lookup panicked")
1304 .expect("receiver dropped");
1305 });
1306 }
1307 Ok(())
1308 }
1309
1310 #[test]
1311 fn adding_fonts_retries_cached_font_failures() -> Result<()> {
1312 let platform_text_system = Arc::new(CosmicTextSystem::new_without_system_fonts("Lilex"));
1313 let text_system = gpui::TextSystem::new(platform_text_system.clone());
1314 text_system.add_fonts(vec![Cow::Borrowed(IBM_PLEX)])?;
1315 let original_fallback = platform_text_system.font_id(&gpui::font(".ZedSans"))?;
1316 let requests = [
1317 gpui::Font::default(),
1318 gpui::font("Lilex"),
1319 gpui::font("Missing Font"),
1320 ];
1321 for requested in &requests {
1322 assert_eq!(text_system.resolve_font(requested), original_fallback);
1323 }
1324 assert_eq!(
1325 text_system.get_font_for_id(original_fallback),
1326 Some(gpui::font(".ZedSans"))
1327 );
1328
1329 text_system.add_fonts(vec![Cow::Borrowed(LILEX)])?;
1330 for (requested, resolved) in requests.iter().zip([".SystemUIFont", "Lilex", ".ZedMono"]) {
1331 let installed = platform_text_system.font_id(&gpui::font(resolved))?;
1332 assert_ne!(installed, original_fallback);
1333 assert_eq!(text_system.resolve_font(requested), installed);
1334 }
1335 Ok(())
1336 }
1337
1338 #[test]
1339 fn font_properties_describe_the_selected_face() -> Result<()> {
1340 let text_system = text_system()?;
1341 let regular = gpui::font("IBM Plex Sans");
1342 let regular_id = text_system.font_id(®ular)?;
1343 for (weight, style) in [
1344 (gpui::FontWeight::MEDIUM, gpui::FontStyle::Normal),
1345 (gpui::FontWeight::BOLD, gpui::FontStyle::Italic),
1346 (gpui::FontWeight::NORMAL, gpui::FontStyle::Oblique),
1347 ] {
1348 let requested = Font {
1349 weight,
1350 style,
1351 ..regular.clone()
1352 };
1353 let font_id = text_system.font_id(&requested)?;
1354 assert_eq!(font_id, regular_id);
1355 assert_eq!(
1356 text_system.font_weight_and_style(font_id)?,
1357 (gpui::FontWeight::NORMAL, gpui::FontStyle::Normal)
1358 );
1359 }
1360 Ok(())
1361 }
1362
1363 fn layout_text(text_system: &CosmicTextSystem, text: &str) -> Result<LineLayout> {
1364 let font_id = text_system.font_id(&gpui::font("IBM Plex Sans"))?;
1365 let runs = [FontRun {
1366 len: text.len(),
1367 font_id,
1368 }];
1369 Ok(text_system.layout_line(text, gpui::px(14.0), &runs))
1370 }
1371
1372 #[test]
1375 fn shape_text_with_mixed_direction_paragraphs() -> Result<()> {
1376 let platform_text_system = Arc::new(text_system()?);
1377 let text_system = Arc::new(gpui::TextSystem::new(platform_text_system));
1378 let window_text_system = gpui::WindowTextSystem::new(text_system);
1379
1380 let text: SharedString = "first line\n\u{05d0}\u{001c}A".into();
1381 let runs = [gpui::TextRun {
1382 len: text.len(),
1383 font: gpui::font("IBM Plex Sans"),
1384 ..Default::default()
1385 }];
1386
1387 let lines = window_text_system.shape_text(text, gpui::px(14.0), &runs, None, None)?;
1388
1389 assert_eq!(lines.len(), 2);
1390 assert_eq!(lines[1].len(), "\u{05d0}\u{001c}A".len());
1391 assert!(lines[1].width() > Pixels::ZERO);
1392 Ok(())
1393 }
1394
1395 #[cfg(feature = "test-support")]
1396 #[test]
1397 fn reports_graphemes_that_exhaust_font_fallback() -> Result<()> {
1398 let platform_text_system = Arc::new(text_system()?);
1399 let dispatcher = gpui::TestDispatcher::new(0);
1400 let cx =
1401 gpui::TestAppContext::build_with_text_system(dispatcher, None, platform_text_system);
1402 let observed = Rc::new(RefCell::new(Vec::new()));
1403 let _subscription = cx.update(|cx| {
1404 let observed = observed.clone();
1405 cx.on_missing_glyphs(move |missing_glyphs, _| {
1406 observed.borrow_mut().extend_from_slice(missing_glyphs);
1407 })
1408 });
1409 let text: SharedString = "界".into();
1410
1411 cx.update(|cx| {
1412 let text_system = gpui::WindowTextSystem::new(cx.text_system().clone());
1413 let runs = [gpui::TextRun {
1414 len: text.len(),
1415 font: gpui::font("IBM Plex Sans"),
1416 ..Default::default()
1417 }];
1418 text_system.shape_line(text, gpui::px(14.0), &runs, None);
1419 });
1420 cx.run_until_parked();
1421
1422 let observed = observed.borrow();
1423 assert_eq!(observed.len(), 1);
1424 assert_eq!(observed[0].grapheme(), "界");
1425 assert_eq!(
1426 observed[0].font_class(),
1427 gpui::FallbackFontClass::Proportional
1428 );
1429 Ok(())
1430 }
1431
1432 #[test]
1433 fn combines_missing_glyphs_from_one_grapheme() -> Result<()> {
1434 let text_system = text_system()?;
1435 let font_id = text_system.font_id(&gpui::font("IBM Plex Sans"))?;
1436 let text = "x\u{0301}";
1437 let runs = [FontRun {
1438 len: text.len(),
1439 font_id,
1440 }];
1441
1442 let missing_glyphs = text_system
1443 .0
1444 .read()
1445 .missing_glyphs(text, &runs, [0, "x".len()]);
1446
1447 assert_eq!(missing_glyphs.len(), 1);
1448 assert_eq!(missing_glyphs[0].grapheme(), text);
1449 Ok(())
1450 }
1451
1452 #[test]
1453 fn adding_fonts_invalidates_cached_line_layouts() -> Result<()> {
1454 let platform_text_system = Arc::new(text_system()?);
1455 let text_system = Arc::new(gpui::TextSystem::new(platform_text_system.clone()));
1456 let window_text_system = gpui::WindowTextSystem::new(text_system.clone());
1457 let text: SharedString = "cached text".into();
1458 let runs = [gpui::TextRun {
1459 len: text.len(),
1460 font: gpui::font("IBM Plex Sans"),
1461 ..Default::default()
1462 }];
1463
1464 let first_layout = window_text_system.shape_line(text.clone(), gpui::px(14.0), &runs, None);
1465 let cached_layout =
1466 window_text_system.shape_line(text.clone(), gpui::px(14.0), &runs, None);
1467 assert!(std::ptr::eq::<LineLayout>(
1468 &**first_layout,
1469 &**cached_layout
1470 ));
1471 let loaded_font_count = platform_text_system.0.read().loaded_fonts.len();
1472
1473 text_system.add_fonts(vec![Cow::Borrowed(LILEX)])?;
1474
1475 let refreshed_layout = window_text_system.shape_line(text, gpui::px(14.0), &runs, None);
1476 assert!(!std::ptr::eq::<LineLayout>(
1477 &**first_layout,
1478 &**refreshed_layout
1479 ));
1480 assert_eq!(
1481 platform_text_system.0.read().loaded_fonts.len(),
1482 loaded_font_count
1483 );
1484 Ok(())
1485 }
1486
1487 #[test]
1488 fn layout_line_with_mixed_direction_paragraphs() -> Result<()> {
1489 let text_system = text_system()?;
1490
1491 for separator in SEPARATORS {
1492 for text in [
1493 format!("\u{05d0}{separator}A"),
1494 format!("A{separator}\u{05d0}"),
1495 ] {
1496 let layout = layout_text(&text_system, &text)?;
1497
1498 assert_eq!(layout.len, text.len(), "{text:?}");
1499 assert!(layout.width > Pixels::ZERO, "{text:?}");
1500 assert!(
1501 layout.runs.iter().any(|run| !run.glyphs.is_empty()),
1502 "{text:?}"
1503 );
1504 }
1505 }
1506
1507 Ok(())
1508 }
1509
1510 #[test]
1511 fn layout_line_with_separators_at_line_edges() -> Result<()> {
1512 let text_system = text_system()?;
1513
1514 for text in [
1515 "\u{001c}",
1516 "\u{001c}\u{001c}",
1517 "\u{001c}\u{05d0}",
1518 "\u{05d0}\u{001c}",
1519 "\u{05d0}\u{001c}\u{001c}A",
1520 "\u{001c}\u{05d0}\u{001c}A\u{001c}",
1521 ] {
1522 let layout = layout_text(&text_system, text)?;
1523 assert_eq!(layout.len, text.len(), "{text:?}");
1524 }
1525
1526 Ok(())
1527 }
1528
1529 #[test]
1533 fn layout_line_keeps_indices_and_positions_ordered_across_paragraphs() -> Result<()> {
1534 let text_system = text_system()?;
1535 let text = "ab\u{001c}cd\u{2029}ef";
1536 let layout = layout_text(&text_system, text)?;
1537
1538 let glyphs: Vec<_> = layout.runs.iter().flat_map(|run| &run.glyphs).collect();
1539 assert!(!glyphs.is_empty());
1540
1541 for glyph in &glyphs {
1542 assert!(glyph.index < text.len(), "{:?}", glyph.index);
1543 assert!(text.is_char_boundary(glyph.index), "{:?}", glyph.index);
1544 }
1545 for pair in glyphs.windows(2) {
1546 assert!(pair[0].index < pair[1].index);
1547 assert!(pair[0].position.x <= pair[1].position.x);
1548 }
1549
1550 assert!(layout.width > layout_text(&text_system, "ab")?.width);
1553 Ok(())
1554 }
1555
1556 #[test]
1559 fn layout_line_with_font_run_straddling_a_separator() -> Result<()> {
1560 let text_system = text_system()?;
1561 let font_id = text_system.font_id(&gpui::font("IBM Plex Sans"))?;
1562 let text = "ab\u{001c}\u{05d0}\u{05d1}";
1563
1564 let runs = [
1566 FontRun {
1567 len: "ab\u{001c}\u{05d0}".len(),
1568 font_id,
1569 },
1570 FontRun {
1571 len: "\u{05d1}".len(),
1572 font_id,
1573 },
1574 ];
1575 let layout = text_system.layout_line(text, gpui::px(14.0), &runs);
1576
1577 assert_eq!(layout.len, text.len());
1578 assert!(layout.width > Pixels::ZERO);
1579 Ok(())
1580 }
1581
1582 #[test]
1585 fn layout_line_without_separators_takes_fast_path() -> Result<()> {
1586 let text_system = text_system()?;
1587
1588 for text in [
1589 "hello world",
1590 "\u{05d0}\u{05d1}\u{05d2}",
1591 "mixed \u{05d0}\u{05d1}",
1592 ] {
1593 assert!(!contains_paragraph_separator(text), "{text:?}");
1594 let layout = layout_text(&text_system, text)?;
1595 assert_eq!(layout.len, text.len(), "{text:?}");
1596 assert!(layout.width > Pixels::ZERO, "{text:?}");
1597 }
1598
1599 Ok(())
1600 }
1601
1602 #[test]
1608 fn index_for_position_past_line_width() -> Result<()> {
1609 let text_system = Arc::new(gpui::TextSystem::new(Arc::new(text_system()?)));
1610 let window_text_system = gpui::WindowTextSystem::new(text_system);
1611 let text: SharedString = "Warning: this will delete files\u{200b}".into();
1612 let runs = [gpui::TextRun {
1613 len: text.len(),
1614 font: gpui::font("IBM Plex Sans"),
1615 ..Default::default()
1616 }];
1617 let lines =
1618 window_text_system.shape_text(text, gpui::px(14.), &runs, Some(gpui::px(4.)), None)?;
1619 let line = &lines[0];
1620 let width = line.unwrapped_layout.width;
1621 let boundary_glyph = |row: usize| {
1622 let boundary = line.wrap_boundaries()[row];
1623 &line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix]
1624 };
1625 let row = (1..line.wrap_boundaries().len())
1626 .find(|row| boundary_glyph(*row).position.x > width)
1627 .expect("trailing zero-width space should be a wrap boundary past the line width");
1628 let row_start_x = f32::from(boundary_glyph(row - 1).position.x);
1629 let row_end = boundary_glyph(row);
1630
1631 let mut x = f32::from(width) - row_start_x;
1632 while x + row_start_x < f32::from(width) {
1633 x = x.next_up();
1634 }
1635 assert!(gpui::px(x + row_start_x) < row_end.position.x);
1636
1637 let line_height = gpui::px(20.);
1638 let position = gpui::point(gpui::px(x), line_height * row as f32 + gpui::px(1.));
1639 assert_eq!(
1640 line.index_for_position(position, line_height),
1641 Err(row_end.index)
1642 );
1643 Ok(())
1644 }
1645
1646 #[test]
1647 fn paragraph_separator_detection() {
1648 for separator in SEPARATORS {
1649 assert!(is_paragraph_separator(*separator), "{separator:?}");
1650 assert!(contains_paragraph_separator(&format!("a{separator}b")));
1651 }
1652
1653 for text in [
1654 "",
1655 "plain ascii",
1656 "\u{05d0}",
1657 "tab\there",
1658 "emoji \u{1f600}",
1659 ] {
1660 assert!(!contains_paragraph_separator(text), "{text:?}");
1661 }
1662 }
1663
1664 #[test]
1665 fn font_runs_are_clipped_to_segment() {
1666 let runs = [
1667 FontRun {
1668 len: 3,
1669 font_id: fid(1),
1670 },
1671 FontRun {
1672 len: 4,
1673 font_id: fid(2),
1674 },
1675 ];
1676
1677 assert_eq!(clip_font_runs(&runs, 0..7).as_slice(), &runs);
1678 assert_eq!(
1679 clip_font_runs(&runs, 2..5).as_slice(),
1680 &[
1681 FontRun {
1682 len: 1,
1683 font_id: fid(1)
1684 },
1685 FontRun {
1686 len: 2,
1687 font_id: fid(2)
1688 },
1689 ]
1690 );
1691 assert_eq!(
1692 clip_font_runs(&runs, 3..7).as_slice(),
1693 &[FontRun {
1694 len: 4,
1695 font_id: fid(2)
1696 }]
1697 );
1698 assert!(clip_font_runs(&runs, 5..5).is_empty());
1699 }
1700
1701 #[test]
1702 fn primary_wins_over_current_fallback_when_primary_covers() {
1703 let primary = fid(0);
1704 let fb = chain(&[1, 2]);
1705 let covers = |id: FontId, _: char| id == fid(0) || id == fid(1);
1706 assert_eq!(
1707 pick_covering_slot('a', Some(0), primary, &fb, &covers),
1708 None
1709 );
1710 }
1711
1712 #[test]
1713 fn primary_preferred_over_fallback_when_both_cover() {
1714 let primary = fid(0);
1715 let fb = chain(&[1]);
1716 let covers = |_: FontId, _: char| true;
1717 assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
1718 }
1719
1720 #[test]
1721 fn falls_through_chain_in_order() {
1722 let primary = fid(0);
1723 let fb = chain(&[1, 2, 3]);
1724 let covers = |id: FontId, _: char| id == fid(2);
1726 assert_eq!(
1727 pick_covering_slot('字', None, primary, &fb, &covers),
1728 Some(1)
1729 );
1730 }
1731
1732 #[test]
1733 fn no_coverage_returns_primary() {
1734 let primary = fid(0);
1735 let fb = chain(&[1, 2]);
1736 let covers = |_: FontId, _: char| false;
1737 assert_eq!(
1740 pick_covering_slot('\u{1F600}', Some(1), primary, &fb, &covers),
1741 None
1742 );
1743 }
1744
1745 #[test]
1746 fn empty_chain_always_returns_primary() {
1747 let primary = fid(0);
1748 let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
1749 let covers = |_: FontId, _: char| false;
1750 assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
1751 }
1752
1753 #[test]
1754 fn slot_font_id_resolution() {
1755 let primary = fid(7);
1756 let fb = chain(&[10, 20]);
1757 assert_eq!(slot_font_id(None, primary, &fb), fid(7));
1758 assert_eq!(slot_font_id(Some(0), primary, &fb), fid(10));
1759 assert_eq!(slot_font_id(Some(1), primary, &fb), fid(20));
1760 }
1761
1762 #[test]
1763 fn run_spans_with_no_chain_emit_one_primary_span() {
1764 let primary = fid(0);
1765 let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
1766 let covers = |_: FontId, _: char| false;
1767 let text = "hello";
1768 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1769 assert_eq!(spans.as_slice(), &[span(0, text.len(), None, primary)]);
1770 }
1771
1772 #[test]
1773 fn run_spans_use_byte_offsets_for_multibyte_chars() {
1774 let primary = fid(0);
1775 let fb = chain(&[1]);
1776 let covers = |id: FontId, ch: char| {
1778 if id == primary {
1779 ch.is_ascii()
1780 } else {
1781 !ch.is_ascii()
1782 }
1783 };
1784 let text = "a字b";
1785 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1786 assert_eq!(
1788 spans.as_slice(),
1789 &[
1790 span(0, 1, None, primary),
1791 span(1, 4, Some(0), fid(1)),
1792 span(4, 5, None, primary),
1793 ]
1794 );
1795 }
1796
1797 #[test]
1798 fn run_spans_respect_run_offset() {
1799 let primary = fid(0);
1800 let fb = chain(&[1]);
1801 let covers = |id: FontId, ch: char| {
1802 if id == primary {
1803 ch.is_ascii()
1804 } else {
1805 !ch.is_ascii()
1806 }
1807 };
1808 let text = "xx字y";
1810 let run_offset = 2;
1811 let run_len = text.len() - run_offset;
1812 let spans = compute_run_spans(text, run_offset, run_len, primary, &fb, &covers);
1813 assert_eq!(
1814 spans.as_slice(),
1815 &[span(2, 5, Some(0), fid(1)), span(5, 6, None, primary)]
1816 );
1817 }
1818
1819 #[test]
1820 fn run_spans_keep_combining_marks_with_base_in_fallback() {
1821 let primary = fid(0);
1822 let fb = chain(&[1]);
1823 let covers = |id: FontId, ch: char| {
1827 if id == primary {
1828 ch.is_ascii()
1829 } else {
1830 ch == '\u{0905}'
1831 }
1832 };
1833 let text = "\u{0905}\u{0902}";
1835 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1836 assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1837 }
1838
1839 #[test]
1840 fn run_spans_keep_zwj_inside_emoji_cluster() {
1841 let primary = fid(0);
1842 let fb = chain(&[1]);
1843 let covers = |id: FontId, ch: char| id == fid(1) && ch != '\u{200D}';
1845 let text = "\u{1F469}\u{200D}\u{1F467}";
1847 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1848 assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1849 }
1850
1851 #[test]
1852 fn run_spans_collapse_adjacent_same_slot() {
1853 let primary = fid(0);
1854 let fb = chain(&[1]);
1855 let covers = |id: FontId, ch: char| {
1856 if id == primary {
1857 ch.is_ascii()
1858 } else {
1859 !ch.is_ascii()
1860 }
1861 };
1862 let text = "字字字";
1863 let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1864 assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1865 }
1866
1867 #[test]
1868 fn run_spans_empty_run_returns_no_spans() {
1869 let primary = fid(0);
1870 let fb = chain(&[1]);
1871 let covers = |_: FontId, _: char| true;
1872 let spans = compute_run_spans("anything", 3, 0, primary, &fb, &covers);
1873 assert!(spans.is_empty());
1874 }
1875}