Skip to main content

open_gpui_wgpu/
cosmic_text_system.rs

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 open_gpui::{
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 open_gpui_collections::HashMap;
13
14use itertools::Itertools;
15use parking_lot::RwLock;
16use smallvec::SmallVec;
17use std::{borrow::Cow, 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    /// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
48    loaded_fonts: Vec<LoadedFont>,
49    /// Caches the `FontId`s associated with a specific family to avoid iterating the font database
50    /// for every font face in a family.
51    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    /// resolved at load time so `layout_line` shares one chain across faces.
60    /// `Arc` keeps clone cheap on the per-run hot path.
61    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();
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        // recurse with `fallbacks = None` so a fallback family cannot pull in
235        // another chain. missing fallback families are dropped so a typo in
236        // settings still lets the primary family load.
237        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 = open_gpui::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            // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
289            let allowed_bad_font_names = [
290                "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
291                "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                // Convert from RGBA to BGRA.
354                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                    // We must always return RGBA data when subpixel rendering is requested.
362                    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            // There seems to be a bug in Swash where the B and R values are swapped.
404            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    /// This is used when cosmic_text has chosen a fallback font instead of using the requested
418    /// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
419    /// yet have an entry for this fallback font, and so one is added.
420    ///
421    /// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
422    /// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
423    /// current use of this field is for the *input* of `layout_line`, and so it's fine to use
424    /// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
425    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        let mut attrs_list = AttrsList::new(&Attrs::new());
458        let mut offs = 0;
459        for run in font_runs {
460            let run_end = offs + run.len;
461
462            let loaded_font = self.loaded_font(run.font_id);
463            let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
464                log::warn!(
465                    "font face not found in database for font_id {:?}",
466                    run.font_id
467                );
468                offs = run_end;
469                continue;
470            };
471            let Some(first_family) = face.families.first() else {
472                log::warn!(
473                    "font face has no family names for font_id {:?}",
474                    run.font_id
475                );
476                offs = run_end;
477                continue;
478            };
479
480            let primary_family_name: SharedString = first_family.0.clone().into();
481            let primary_stretch = face.stretch;
482            let primary_style = face.style;
483            let primary_weight = face.weight;
484            let primary_features = loaded_font.features.clone();
485            let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain);
486
487            // build one `Attrs` per slot up front. each clone of span attrs
488            // would otherwise re-allocate the `font_features` Vec.
489            let primary_attrs = Attrs::new()
490                .metadata(run.font_id.0)
491                .family(Family::Name(&primary_family_name))
492                .stretch(primary_stretch)
493                .style(primary_style)
494                .weight(primary_weight)
495                .font_features(primary_features.clone());
496            let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain
497                .iter()
498                .map(|(fb_id, fb_name)| {
499                    Attrs::new()
500                        .metadata(fb_id.0)
501                        .family(Family::Name(fb_name))
502                        .stretch(primary_stretch)
503                        .style(primary_style)
504                        .weight(primary_weight)
505                        .font_features(primary_features.clone())
506                })
507                .collect();
508
509            let spans = if fallback_chain.is_empty() {
510                let mut spans = SmallVec::<[RunSpan; 4]>::new();
511                spans.push(RunSpan {
512                    start: offs,
513                    end: run_end,
514                    slot: None,
515                    font_id: run.font_id,
516                });
517                spans
518            } else {
519                let loaded_fonts = &self.loaded_fonts;
520                let covers = |id: FontId, ch: char| charmap_covers(loaded_fonts, id, ch);
521                compute_run_spans(text, offs, run.len, run.font_id, &fallback_chain, &covers)
522            };
523
524            for span in spans {
525                let attrs = match span.slot {
526                    None => &primary_attrs,
527                    Some(ix) => &fallback_attrs[ix],
528                };
529                attrs_list.add_span(span.start..span.end, attrs);
530            }
531            offs = run_end;
532        }
533
534        let line = ShapeLine::new(
535            &mut self.font_system,
536            text,
537            &attrs_list,
538            cosmic_text::Shaping::Advanced,
539            4,
540        );
541        let mut layout_lines = Vec::with_capacity(1);
542        line.layout_to_buffer(
543            &mut self.scratch,
544            f32::from(font_size),
545            None, // We do our own wrapping
546            cosmic_text::Wrap::None,
547            Ellipsize::None,
548            None,
549            &mut layout_lines,
550            None,
551            cosmic_text::Hinting::Disabled,
552        );
553
554        let Some(layout) = layout_lines.first() else {
555            return LineLayout {
556                font_size,
557                width: Pixels::ZERO,
558                ascent: Pixels::ZERO,
559                descent: Pixels::ZERO,
560                runs: Vec::new(),
561                len: text.len(),
562            };
563        };
564
565        let mut runs: Vec<ShapedRun> = Vec::new();
566        for glyph in &layout.glyphs {
567            let mut font_id = FontId(glyph.metadata);
568            let mut loaded_font = self.loaded_font(font_id);
569            if loaded_font.font.id() != glyph.font_id {
570                match self.font_id_for_cosmic_id(glyph.font_id) {
571                    std::result::Result::Ok(resolved_id) => {
572                        font_id = resolved_id;
573                        loaded_font = self.loaded_font(font_id);
574                    }
575                    Err(error) => {
576                        log::warn!(
577                            "failed to resolve cosmic font id {:?}: {error:#}",
578                            glyph.font_id
579                        );
580                        continue;
581                    }
582                }
583            }
584            let is_emoji = loaded_font.is_known_emoji_font;
585
586            // HACK: Prevent crash caused by variation selectors.
587            if glyph.glyph_id == 3 && is_emoji {
588                continue;
589            }
590
591            let shaped_glyph = ShapedGlyph {
592                id: GlyphId(glyph.glyph_id as u32),
593                position: point(glyph.x.into(), glyph.y.into()),
594                index: glyph.start,
595                is_emoji,
596            };
597
598            if let Some(last_run) = runs
599                .last_mut()
600                .filter(|last_run| last_run.font_id == font_id)
601            {
602                last_run.glyphs.push(shaped_glyph);
603            } else {
604                runs.push(ShapedRun {
605                    font_id,
606                    glyphs: vec![shaped_glyph],
607                });
608            }
609        }
610
611        LineLayout {
612            font_size,
613            width: layout.w.into(),
614            ascent: layout.max_ascent.into(),
615            descent: layout.max_descent.into(),
616            runs,
617            len: text.len(),
618        }
619    }
620}
621
622#[cfg(feature = "font-kit")]
623fn find_best_match(
624    font: &Font,
625    candidates: &[FontId],
626    state: &CosmicTextSystemState,
627) -> Result<usize> {
628    let candidate_properties = candidates
629        .iter()
630        .map(|font_id| {
631            let database_id = state.loaded_font(*font_id).font.id();
632            let face_info = state
633                .font_system
634                .db()
635                .face(database_id)
636                .context("font face not found in database")?;
637            Ok(face_info_into_properties(face_info))
638        })
639        .collect::<Result<SmallVec<[_; 4]>>>()?;
640
641    let ix = find_best_font_kit_match(&candidate_properties, &font_into_properties(font))
642        .context("requested font family contains no font matching the other parameters")?;
643
644    Ok(ix)
645}
646
647#[cfg(not(feature = "font-kit"))]
648fn find_best_match(
649    font: &Font,
650    candidates: &[FontId],
651    state: &CosmicTextSystemState,
652) -> Result<usize> {
653    if candidates.is_empty() {
654        anyhow::bail!("requested font family contains no font matching the other parameters");
655    }
656    if candidates.len() == 1 {
657        return Ok(0);
658    }
659
660    let target_weight = font.weight.0;
661    let target_italic = matches!(
662        font.style,
663        open_gpui::FontStyle::Italic | open_gpui::FontStyle::Oblique
664    );
665
666    let mut best_index = 0;
667    let mut best_score = u32::MAX;
668
669    for (index, font_id) in candidates.iter().enumerate() {
670        let database_id = state.loaded_font(*font_id).font.id();
671        let face_info = state
672            .font_system
673            .db()
674            .face(database_id)
675            .context("font face not found in database")?;
676
677        let is_italic = matches!(
678            face_info.style,
679            cosmic_text::Style::Italic | cosmic_text::Style::Oblique
680        );
681        let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
682        let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
683        let score = style_penalty + weight_diff;
684
685        if score < best_score {
686            best_score = score;
687            best_index = index;
688        }
689    }
690
691    Ok(best_index)
692}
693
694/// one contiguous slice of a `FontRun` that maps to a single slot. `slot` is
695/// `None` for the primary font and `Some(ix)` for `fallback_chain[ix]`.
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697struct RunSpan {
698    start: usize,
699    end: usize,
700    slot: Option<usize>,
701    font_id: FontId,
702}
703
704/// walks `text[run_offset..run_offset + run_len]` and groups codepoints into
705/// spans. inheriting codepoints stay in the current span so shaping clusters
706/// like emoji zwj sequences and combining marks are not torn apart.
707fn compute_run_spans(
708    text: &str,
709    run_offset: usize,
710    run_len: usize,
711    primary: FontId,
712    fallback_chain: &[(FontId, SharedString)],
713    covers: &impl Fn(FontId, char) -> bool,
714) -> SmallVec<[RunSpan; 4]> {
715    let mut spans = SmallVec::new();
716    let run_end = run_offset + run_len;
717    if run_end <= run_offset {
718        return spans;
719    }
720    if fallback_chain.is_empty() {
721        spans.push(RunSpan {
722            start: run_offset,
723            end: run_end,
724            slot: None,
725            font_id: primary,
726        });
727        return spans;
728    }
729    let run_text = &text[run_offset..run_end];
730    let mut span_start = run_offset;
731    let mut span_slot: Option<usize> = None;
732    let mut span_font_id = primary;
733    for (grapheme_idx, grapheme) in run_text.grapheme_indices(true) {
734        let abs = run_offset + grapheme_idx;
735        let ch = grapheme.chars().next().unwrap_or('\0');
736        let next_slot = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);
737        if next_slot == span_slot {
738            continue;
739        }
740        if abs > span_start {
741            spans.push(RunSpan {
742                start: span_start,
743                end: abs,
744                slot: span_slot,
745                font_id: span_font_id,
746            });
747        }
748        span_start = abs;
749        span_slot = next_slot;
750        span_font_id = slot_font_id(next_slot, primary, fallback_chain);
751    }
752    if span_start < run_end {
753        spans.push(RunSpan {
754            start: span_start,
755            end: run_end,
756            slot: span_slot,
757            font_id: span_font_id,
758        });
759    }
760    spans
761}
762
763fn slot_font_id(
764    slot: Option<usize>,
765    primary: FontId,
766    fallback_chain: &[(FontId, SharedString)],
767) -> FontId {
768    match slot {
769        None => primary,
770        Some(ix) => fallback_chain[ix].0,
771    }
772}
773
774fn pick_covering_slot(
775    ch: char,
776    current: Option<usize>,
777    primary: FontId,
778    fallback_chain: &[(FontId, SharedString)],
779    covers: &impl Fn(FontId, char) -> bool,
780) -> Option<usize> {
781    if (ch as u32) <= 0x7F {
782        return None;
783    }
784    if covers(primary, ch) {
785        return None;
786    }
787    let current_id = slot_font_id(current, primary, fallback_chain);
788    if covers(current_id, ch) {
789        return current;
790    }
791    for (ix, (fb_id, _)) in fallback_chain.iter().enumerate() {
792        if covers(*fb_id, ch) {
793            return Some(ix);
794        }
795    }
796    None
797}
798
799fn charmap_covers(loaded_fonts: &[LoadedFont], id: FontId, ch: char) -> bool {
800    loaded_fonts
801        .get(id.0)
802        .is_some_and(|loaded| loaded.font.as_swash().charmap().map(ch) != 0)
803}
804
805fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
806    let mut result = CosmicFontFeatures::new();
807    for feature in features.0.iter() {
808        let name_bytes: [u8; 4] = feature
809            .0
810            .as_bytes()
811            .try_into()
812            .context("Incorrect feature flag format")?;
813
814        let tag = cosmic_text::FeatureTag::new(&name_bytes);
815
816        result.set(tag, feature.1);
817    }
818    Ok(result)
819}
820
821#[cfg(feature = "font-kit")]
822fn find_best_font_kit_match(
823    candidates: &[font_kit::properties::Properties],
824    query: &font_kit::properties::Properties,
825) -> Option<usize> {
826    use font_kit::properties::{Stretch, Style, Weight};
827
828    let mut matching_set = (0..candidates.len()).collect::<SmallVec<[_; 8]>>();
829    if matching_set.is_empty() {
830        return None;
831    }
832
833    let matching_stretch = if matching_set
834        .iter()
835        .any(|&index| candidates[index].stretch == query.stretch)
836    {
837        query.stretch
838    } else if query.stretch <= Stretch::NORMAL {
839        matching_set
840            .iter()
841            .filter(|&index| candidates[*index].stretch < query.stretch)
842            .min_by(|&a, &b| {
843                stretch_distance(candidates[*a].stretch, query.stretch)
844                    .total_cmp(&stretch_distance(candidates[*b].stretch, query.stretch))
845            })
846            .or_else(|| {
847                matching_set.iter().min_by(|&a, &b| {
848                    stretch_distance(candidates[*a].stretch, query.stretch)
849                        .total_cmp(&stretch_distance(candidates[*b].stretch, query.stretch))
850                })
851            })
852            .map(|index| candidates[*index].stretch)?
853    } else {
854        matching_set
855            .iter()
856            .filter(|&index| candidates[*index].stretch > query.stretch)
857            .min_by(|&a, &b| {
858                stretch_distance(candidates[*a].stretch, query.stretch)
859                    .total_cmp(&stretch_distance(candidates[*b].stretch, query.stretch))
860            })
861            .or_else(|| {
862                matching_set.iter().min_by(|&a, &b| {
863                    stretch_distance(candidates[*a].stretch, query.stretch)
864                        .total_cmp(&stretch_distance(candidates[*b].stretch, query.stretch))
865                })
866            })
867            .map(|index| candidates[*index].stretch)?
868    };
869    matching_set.retain(|index| candidates[*index].stretch == matching_stretch);
870
871    let style_preference = match query.style {
872        Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
873        Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
874        Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
875    };
876    let matching_style = *style_preference.iter().find(|&query_style| {
877        matching_set
878            .iter()
879            .any(|&index| candidates[index].style == *query_style)
880    })?;
881    matching_set.retain(|index| candidates[*index].style == matching_style);
882
883    let matching_weight = if matching_set
884        .iter()
885        .any(|&index| candidates[index].weight == query.weight)
886    {
887        query.weight
888    } else if query.weight >= Weight(400.0)
889        && query.weight < Weight(450.0)
890        && matching_set
891            .iter()
892            .any(|&index| candidates[index].weight == Weight(500.0))
893    {
894        Weight(500.0)
895    } else if query.weight >= Weight(450.0)
896        && query.weight <= Weight(500.0)
897        && matching_set
898            .iter()
899            .any(|&index| candidates[index].weight == Weight(400.0))
900    {
901        Weight(400.0)
902    } else if query.weight <= Weight(500.0) {
903        matching_set
904            .iter()
905            .filter(|&index| candidates[*index].weight <= query.weight)
906            .min_by(|&a, &b| {
907                weight_distance(candidates[*a].weight, query.weight)
908                    .total_cmp(&weight_distance(candidates[*b].weight, query.weight))
909            })
910            .or_else(|| {
911                matching_set.iter().min_by(|&a, &b| {
912                    weight_distance(candidates[*a].weight, query.weight)
913                        .total_cmp(&weight_distance(candidates[*b].weight, query.weight))
914                })
915            })
916            .map(|index| candidates[*index].weight)?
917    } else {
918        matching_set
919            .iter()
920            .filter(|&index| candidates[*index].weight >= query.weight)
921            .min_by(|&a, &b| {
922                weight_distance(candidates[*a].weight, query.weight)
923                    .total_cmp(&weight_distance(candidates[*b].weight, query.weight))
924            })
925            .or_else(|| {
926                matching_set.iter().min_by(|&a, &b| {
927                    weight_distance(candidates[*a].weight, query.weight)
928                        .total_cmp(&weight_distance(candidates[*b].weight, query.weight))
929                })
930            })
931            .map(|index| candidates[*index].weight)?
932    };
933    matching_set.retain(|index| candidates[*index].weight == matching_weight);
934    matching_set.into_iter().next()
935}
936
937#[cfg(feature = "font-kit")]
938fn stretch_distance(a: font_kit::properties::Stretch, b: font_kit::properties::Stretch) -> f32 {
939    (a.0 - b.0).abs()
940}
941
942#[cfg(feature = "font-kit")]
943fn weight_distance(a: font_kit::properties::Weight, b: font_kit::properties::Weight) -> f32 {
944    (a.0 - b.0).abs()
945}
946
947#[cfg(feature = "font-kit")]
948fn font_into_properties(font: &open_gpui::Font) -> font_kit::properties::Properties {
949    font_kit::properties::Properties {
950        style: match font.style {
951            open_gpui::FontStyle::Normal => font_kit::properties::Style::Normal,
952            open_gpui::FontStyle::Italic => font_kit::properties::Style::Italic,
953            open_gpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
954        },
955        weight: font_kit::properties::Weight(font.weight.0),
956        stretch: Default::default(),
957    }
958}
959
960#[cfg(feature = "font-kit")]
961fn face_info_into_properties(
962    face_info: &cosmic_text::fontdb::FaceInfo,
963) -> font_kit::properties::Properties {
964    font_kit::properties::Properties {
965        style: match face_info.style {
966            cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
967            cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
968            cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
969        },
970        weight: font_kit::properties::Weight(face_info.weight.0.into()),
971        stretch: match face_info.stretch {
972            cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
973            cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
974            cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
975            cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
976            cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
977            cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
978            cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
979            cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
980            cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
981        },
982    }
983}
984
985fn check_is_known_emoji_font(postscript_name: &str) -> bool {
986    // TODO: Include other common emoji fonts
987    postscript_name == "NotoColorEmoji"
988}
989
990#[cfg(test)]
991mod tests {
992    use super::*;
993
994    #[cfg(feature = "font-kit")]
995    fn props(
996        style: font_kit::properties::Style,
997        weight: f32,
998        stretch: font_kit::properties::Stretch,
999    ) -> font_kit::properties::Properties {
1000        font_kit::properties::Properties {
1001            style,
1002            weight: font_kit::properties::Weight(weight),
1003            stretch,
1004        }
1005    }
1006
1007    fn fid(i: usize) -> FontId {
1008        FontId(i)
1009    }
1010
1011    fn chain(ids: &[usize]) -> SmallVec<[(FontId, SharedString); 4]> {
1012        ids.iter()
1013            .map(|&i| (fid(i), SharedString::from(format!("fb{i}"))))
1014            .collect()
1015    }
1016
1017    fn span(start: usize, end: usize, slot: Option<usize>, font_id: FontId) -> RunSpan {
1018        RunSpan {
1019            start,
1020            end,
1021            slot,
1022            font_id,
1023        }
1024    }
1025
1026    #[cfg(feature = "font-kit")]
1027    #[test]
1028    fn font_kit_match_returns_none_for_empty_candidates() {
1029        use font_kit::properties::{Stretch, Style};
1030
1031        assert_eq!(
1032            find_best_font_kit_match(&[], &props(Style::Normal, 400.0, Stretch::NORMAL)),
1033            None
1034        );
1035    }
1036
1037    #[cfg(feature = "font-kit")]
1038    #[test]
1039    fn font_kit_match_prefers_stretch_before_style_and_weight() {
1040        use font_kit::properties::{Stretch, Style};
1041
1042        let candidates = [
1043            props(Style::Italic, 400.0, Stretch::EXPANDED),
1044            props(Style::Normal, 900.0, Stretch::CONDENSED),
1045            props(Style::Oblique, 700.0, Stretch::NORMAL),
1046        ];
1047
1048        assert_eq!(
1049            find_best_font_kit_match(
1050                &candidates,
1051                &props(Style::Normal, 400.0, Stretch::SEMI_CONDENSED)
1052            ),
1053            Some(1)
1054        );
1055    }
1056
1057    #[cfg(feature = "font-kit")]
1058    #[test]
1059    fn font_kit_match_uses_css_style_preference_order() {
1060        use font_kit::properties::{Stretch, Style};
1061
1062        let candidates = [
1063            props(Style::Normal, 400.0, Stretch::NORMAL),
1064            props(Style::Oblique, 400.0, Stretch::NORMAL),
1065            props(Style::Italic, 400.0, Stretch::NORMAL),
1066        ];
1067
1068        assert_eq!(
1069            find_best_font_kit_match(&candidates, &props(Style::Italic, 400.0, Stretch::NORMAL)),
1070            Some(2)
1071        );
1072        assert_eq!(
1073            find_best_font_kit_match(
1074                &candidates[..2],
1075                &props(Style::Italic, 400.0, Stretch::NORMAL)
1076            ),
1077            Some(1)
1078        );
1079    }
1080
1081    #[cfg(feature = "font-kit")]
1082    #[test]
1083    fn font_kit_match_keeps_css_weight_edge_cases() {
1084        use font_kit::properties::{Stretch, Style};
1085
1086        let candidates = [
1087            props(Style::Normal, 300.0, Stretch::NORMAL),
1088            props(Style::Normal, 400.0, Stretch::NORMAL),
1089            props(Style::Normal, 500.0, Stretch::NORMAL),
1090        ];
1091
1092        assert_eq!(
1093            find_best_font_kit_match(&candidates, &props(Style::Normal, 425.0, Stretch::NORMAL)),
1094            Some(2)
1095        );
1096        assert_eq!(
1097            find_best_font_kit_match(&candidates, &props(Style::Normal, 475.0, Stretch::NORMAL)),
1098            Some(1)
1099        );
1100    }
1101
1102    #[test]
1103    fn primary_wins_over_current_fallback_when_primary_covers() {
1104        let primary = fid(0);
1105        let fb = chain(&[1, 2]);
1106        let covers = |id: FontId, _: char| id == fid(0) || id == fid(1);
1107        assert_eq!(
1108            pick_covering_slot('a', Some(0), primary, &fb, &covers),
1109            None
1110        );
1111    }
1112
1113    #[test]
1114    fn primary_preferred_over_fallback_when_both_cover() {
1115        let primary = fid(0);
1116        let fb = chain(&[1]);
1117        let covers = |_: FontId, _: char| true;
1118        assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
1119    }
1120
1121    #[test]
1122    fn falls_through_chain_in_order() {
1123        let primary = fid(0);
1124        let fb = chain(&[1, 2, 3]);
1125        // only fallback 2 at index 1 covers.
1126        let covers = |id: FontId, _: char| id == fid(2);
1127        assert_eq!(
1128            pick_covering_slot('字', None, primary, &fb, &covers),
1129            Some(1)
1130        );
1131    }
1132
1133    #[test]
1134    fn no_coverage_returns_primary() {
1135        let primary = fid(0);
1136        let fb = chain(&[1, 2]);
1137        let covers = |_: FontId, _: char| false;
1138        // nothing covers. return `None` so the `cosmic-text` built in script
1139        // fallback can take over during shaping.
1140        assert_eq!(
1141            pick_covering_slot('\u{1F600}', Some(1), primary, &fb, &covers),
1142            None
1143        );
1144    }
1145
1146    #[test]
1147    fn empty_chain_always_returns_primary() {
1148        let primary = fid(0);
1149        let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
1150        let covers = |_: FontId, _: char| false;
1151        assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
1152    }
1153
1154    #[test]
1155    fn slot_font_id_resolution() {
1156        let primary = fid(7);
1157        let fb = chain(&[10, 20]);
1158        assert_eq!(slot_font_id(None, primary, &fb), fid(7));
1159        assert_eq!(slot_font_id(Some(0), primary, &fb), fid(10));
1160        assert_eq!(slot_font_id(Some(1), primary, &fb), fid(20));
1161    }
1162
1163    #[test]
1164    fn run_spans_with_no_chain_emit_one_primary_span() {
1165        let primary = fid(0);
1166        let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
1167        let covers = |_: FontId, _: char| false;
1168        let text = "hello";
1169        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1170        assert_eq!(spans.as_slice(), &[span(0, text.len(), None, primary)]);
1171    }
1172
1173    #[test]
1174    fn run_spans_use_byte_offsets_for_multibyte_chars() {
1175        let primary = fid(0);
1176        let fb = chain(&[1]);
1177        // primary covers ascii. fallback covers cjk.
1178        let covers = |id: FontId, ch: char| {
1179            if id == primary {
1180                ch.is_ascii()
1181            } else {
1182                !ch.is_ascii()
1183            }
1184        };
1185        let text = "a字b";
1186        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1187        // '字' is 3 bytes so split is at 1 then 4.
1188        assert_eq!(
1189            spans.as_slice(),
1190            &[
1191                span(0, 1, None, primary),
1192                span(1, 4, Some(0), fid(1)),
1193                span(4, 5, None, primary),
1194            ]
1195        );
1196    }
1197
1198    #[test]
1199    fn run_spans_respect_run_offset() {
1200        let primary = fid(0);
1201        let fb = chain(&[1]);
1202        let covers = |id: FontId, ch: char| {
1203            if id == primary {
1204                ch.is_ascii()
1205            } else {
1206                !ch.is_ascii()
1207            }
1208        };
1209        // outer text has a prefix that is not part of this run.
1210        let text = "xx字y";
1211        let run_offset = 2;
1212        let run_len = text.len() - run_offset;
1213        let spans = compute_run_spans(text, run_offset, run_len, primary, &fb, &covers);
1214        assert_eq!(
1215            spans.as_slice(),
1216            &[span(2, 5, Some(0), fid(1)), span(5, 6, None, primary)]
1217        );
1218    }
1219
1220    #[test]
1221    fn run_spans_keep_combining_marks_with_base_in_fallback() {
1222        let primary = fid(0);
1223        let fb = chain(&[1]);
1224        // primary covers ascii only. fallback covers the base char.
1225        // combining mark must stay in the fallback span even when fallback
1226        // does not advertise coverage of it.
1227        let covers = |id: FontId, ch: char| {
1228            if id == primary {
1229                ch.is_ascii()
1230            } else {
1231                ch == '\u{0905}'
1232            }
1233        };
1234        // \u{0905} devanagari short a + \u{0902} candrabindu mark.
1235        let text = "\u{0905}\u{0902}";
1236        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1237        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1238    }
1239
1240    #[test]
1241    fn run_spans_keep_zwj_inside_emoji_cluster() {
1242        let primary = fid(0);
1243        let fb = chain(&[1]);
1244        // only fallback covers the emoji codepoints. zwj must not split.
1245        let covers = |id: FontId, ch: char| id == fid(1) && ch != '\u{200D}';
1246        // family zwj sequence woman zwj girl.
1247        let text = "\u{1F469}\u{200D}\u{1F467}";
1248        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1249        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1250    }
1251
1252    #[test]
1253    fn run_spans_collapse_adjacent_same_slot() {
1254        let primary = fid(0);
1255        let fb = chain(&[1]);
1256        let covers = |id: FontId, ch: char| {
1257            if id == primary {
1258                ch.is_ascii()
1259            } else {
1260                !ch.is_ascii()
1261            }
1262        };
1263        let text = "字字字";
1264        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1265        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1266    }
1267
1268    #[test]
1269    fn run_spans_empty_run_returns_no_spans() {
1270        let primary = fid(0);
1271        let fb = chain(&[1]);
1272        let covers = |_: FontId, _: char| true;
1273        let spans = compute_run_spans("anything", 3, 0, primary, &fb, &covers);
1274        assert!(spans.is_empty());
1275    }
1276}