Skip to main content

rgpui_wgpu/
cosmic_text_system.rs

1use anyhow::{Context as _, Ok, Result};
2use cosmic_text::{
3    Attrs, AttrsList, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
4    FontSystem, ShapeBuffer, ShapeLine,
5};
6use rgpui::{
7    Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,
8    FxHashMap, GlyphId, LineLayout, Pixels, PlatformTextSystem, RenderGlyphParams,
9    SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size,
10    TextRenderingMode, point, size,
11};
12
13use itertools::Itertools;
14use parking_lot::RwLock;
15use smallvec::SmallVec;
16use std::{borrow::Cow, sync::Arc};
17use swash::{
18    scale::{Render, ScaleContext, Source, StrikeWith},
19    zeno::{Format, Vector},
20};
21use unicode_segmentation::UnicodeSegmentation;
22
23/// 基于 cosmic-text 的文本系统实现
24pub struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
25
26pub type HashMap<K, V> = FxHashMap<K, V>;
27
28/// 字体唯一标识键
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30struct FontKey {
31    family: SharedString,
32    features: FontFeatures,
33    fallbacks: Option<FontFallbacks>,
34}
35
36impl FontKey {
37    /// 创建字体键
38    fn new(family: SharedString, features: FontFeatures, fallbacks: Option<FontFallbacks>) -> Self {
39        Self {
40            family,
41            features,
42            fallbacks,
43        }
44    }
45}
46
47/// 文本系统内部状态
48struct CosmicTextSystemState {
49    font_system: FontSystem,
50    scratch: ShapeBuffer,
51    swash_scale_context: ScaleContext,
52    /// 包含所有已加载的字体,包括所有字形面。通过 `FontId` 索引。
53    loaded_fonts: Vec<LoadedFont>,
54    /// 缓存特定字体族关联的 `FontId`,避免为字体数据库中的每个字体面迭代查询。
55    font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
56    system_font_fallback: String,
57}
58
59/// 已加载的字体信息
60struct LoadedFont {
61    font: Arc<CosmicTextFont>,
62    features: CosmicFontFeatures,
63    is_known_emoji_font: bool,
64    /// 在加载时解析,以便 `layout_line` 跨字面共享一条链。
65    /// `Arc` 使每次运行热路径上的克隆保持廉价。
66    user_fallback_chain: Arc<[(FontId, SharedString)]>,
67}
68
69impl CosmicTextSystem {
70    /// 创建新的文本系统,加载系统字体
71    pub fn new(system_font_fallback: &str) -> Self {
72        let font_system = FontSystem::new();
73
74        Self(RwLock::new(CosmicTextSystemState {
75            font_system,
76            scratch: ShapeBuffer::default(),
77            swash_scale_context: ScaleContext::new(),
78            loaded_fonts: Vec::new(),
79            font_ids_by_family_cache: HashMap::default(),
80            system_font_fallback: system_font_fallback.to_string(),
81        }))
82    }
83
84    /// 创建不加载系统字体的文本系统
85    pub fn new_without_system_fonts(system_font_fallback: &str) -> Self {
86        let font_system = FontSystem::new_with_locale_and_db(
87            "en-US".to_string(),
88            cosmic_text::fontdb::Database::new(),
89        );
90
91        Self(RwLock::new(CosmicTextSystemState {
92            font_system,
93            scratch: ShapeBuffer::default(),
94            swash_scale_context: ScaleContext::new(),
95            loaded_fonts: Vec::new(),
96            font_ids_by_family_cache: HashMap::default(),
97            system_font_fallback: system_font_fallback.to_string(),
98        }))
99    }
100}
101
102impl PlatformTextSystem for CosmicTextSystem {
103    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
104        self.0.write().add_fonts(fonts)
105    }
106
107    fn all_font_names(&self) -> Vec<String> {
108        let mut result = self
109            .0
110            .read()
111            .font_system
112            .db()
113            .faces()
114            .filter_map(|face| face.families.first().map(|family| family.0.clone()))
115            .collect_vec();
116        result.sort();
117        result.dedup();
118        result
119    }
120
121    fn font_id(&self, font: &Font) -> Result<FontId> {
122        let mut state = self.0.write();
123        let key = FontKey::new(
124            font.family.clone(),
125            font.features.clone(),
126            font.fallbacks.clone(),
127        );
128        let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
129            font_ids.as_slice()
130        } else {
131            let font_ids =
132                state.load_family(&font.family, &font.features, font.fallbacks.as_ref())?;
133            state.font_ids_by_family_cache.insert(key.clone(), font_ids);
134            state.font_ids_by_family_cache[&key].as_ref()
135        };
136
137        let ix = find_best_match(font, candidates, &state)?;
138
139        Ok(candidates[ix])
140    }
141
142    fn font_metrics(&self, font_id: FontId) -> FontMetrics {
143        let metrics = self
144            .0
145            .read()
146            .loaded_font(font_id)
147            .font
148            .as_swash()
149            .metrics(&[]);
150
151        FontMetrics {
152            units_per_em: metrics.units_per_em as u32,
153            ascent: metrics.ascent,
154            descent: -metrics.descent,
155            line_gap: metrics.leading,
156            underline_position: metrics.underline_offset,
157            underline_thickness: metrics.stroke_size,
158            cap_height: metrics.cap_height,
159            x_height: metrics.x_height,
160            bounding_box: Bounds {
161                origin: point(0.0, 0.0),
162                size: size(metrics.max_width, metrics.ascent + metrics.descent),
163            },
164        }
165    }
166
167    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
168        let lock = self.0.read();
169        let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
170        let glyph_id = glyph_id.0 as u16;
171        Ok(Bounds {
172            origin: point(0.0, 0.0),
173            size: size(
174                glyph_metrics.advance_width(glyph_id),
175                glyph_metrics.advance_height(glyph_id),
176            ),
177        })
178    }
179
180    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
181        self.0.read().advance(font_id, glyph_id)
182    }
183
184    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
185        self.0.read().glyph_for_char(font_id, ch)
186    }
187
188    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
189        self.0.write().raster_bounds(params)
190    }
191
192    fn rasterize_glyph(
193        &self,
194        params: &RenderGlyphParams,
195        raster_bounds: Bounds<DevicePixels>,
196    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
197        self.0.write().rasterize_glyph(params, raster_bounds)
198    }
199
200    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
201        self.0.write().layout_line(text, font_size, runs)
202    }
203
204    fn recommended_rendering_mode(
205        &self,
206        _font_id: FontId,
207        _font_size: Pixels,
208    ) -> TextRenderingMode {
209        TextRenderingMode::Subpixel
210    }
211}
212
213impl CosmicTextSystemState {
214    /// 获取已加载字体的引用
215    fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
216        &self.loaded_fonts[font_id.0]
217    }
218
219    #[profiling::function]
220    /// 添加字体数据到字体数据库
221    fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
222        let db = self.font_system.db_mut();
223        for bytes in fonts {
224            match bytes {
225                Cow::Borrowed(embedded_font) => {
226                    db.load_font_data(embedded_font.to_vec());
227                }
228                Cow::Owned(bytes) => {
229                    db.load_font_data(bytes);
230                }
231            }
232        }
233        Ok(())
234    }
235
236    #[profiling::function]
237    /// 加载字体族及其回退链
238    fn load_family(
239        &mut self,
240        name: &str,
241        features: &FontFeatures,
242        fallbacks: Option<&FontFallbacks>,
243    ) -> Result<SmallVec<[FontId; 4]>> {
244        // 使用 `fallbacks = None` 递归,以便回退字体族无法拉入另一个链。
245        // 缺失的回退字体族会被丢弃,因此设置中的拼写错误仍允许主字体族加载。
246        let user_fallback_chain: Arc<[(FontId, SharedString)]> = match fallbacks {
247            Some(fallbacks) if !fallbacks.fallback_list().is_empty() => {
248                let mut chain: Vec<(FontId, SharedString)> = Vec::new();
249                for fallback_name in fallbacks.fallback_list() {
250                    let fb_key = FontKey::new(
251                        SharedString::from(fallback_name.clone()),
252                        features.clone(),
253                        None,
254                    );
255                    let fb_ids = if let Some(cached) = self.font_ids_by_family_cache.get(&fb_key) {
256                        cached.clone()
257                    } else {
258                        let loaded = self.load_family(fallback_name, features, None)?;
259                        self.font_ids_by_family_cache
260                            .insert(fb_key.clone(), loaded.clone());
261                        loaded
262                    };
263                    let Some(&fb_id) = fb_ids.first() else {
264                        continue;
265                    };
266                    let db_id = self.loaded_fonts[fb_id.0].font.id();
267                    if let Some(face) = self.font_system.db().face(db_id)
268                        && let Some(family) = face.families.first()
269                    {
270                        chain.push((fb_id, SharedString::from(family.0.clone())));
271                    }
272                }
273                Arc::from(chain)
274            }
275            _ => Arc::from(Vec::new()),
276        };
277
278        let name = rgpui::font_name_with_fallbacks(name, &self.system_font_fallback);
279
280        let families = self
281            .font_system
282            .db()
283            .faces()
284            .filter(|face| face.families.iter().any(|family| *name == family.0))
285            .map(|face| (face.id, face.post_script_name.clone()))
286            .collect::<SmallVec<[_; 4]>>();
287
288        let cosmic_features = cosmic_font_features(features)?;
289
290        let mut loaded_font_ids = SmallVec::new();
291        for (font_id, postscript_name) in families {
292            let font = self
293                .font_system
294                .get_font(font_id, cosmic_text::Weight::NORMAL)
295                .context("Could not load font")?;
296
297            //  HACK: 允许 storybook 运行并渲染 Windows 标题栏图标。我们应该实现更好的字体回退。
298            let allowed_bad_font_names = [
299                "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
300                "Segoe Fluent Icons",
301            ];
302
303            if font.as_swash().charmap().map('m') == 0
304                && !allowed_bad_font_names.contains(&postscript_name.as_str())
305            {
306                self.font_system.db_mut().remove_face(font.id());
307                continue;
308            };
309
310            let font_id = FontId(self.loaded_fonts.len());
311            loaded_font_ids.push(font_id);
312            self.loaded_fonts.push(LoadedFont {
313                font,
314                features: cosmic_features.clone(),
315                is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
316                user_fallback_chain: Arc::clone(&user_fallback_chain),
317            });
318        }
319
320        Ok(loaded_font_ids)
321    }
322
323    /// 获取字形的Advance(前进)尺寸
324    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
325        let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
326        Ok(Size {
327            width: glyph_metrics.advance_width(glyph_id.0 as u16),
328            height: glyph_metrics.advance_height(glyph_id.0 as u16),
329        })
330    }
331
332    /// 获取字符对应的字形 ID
333    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
334        let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
335        if glyph_id == 0 {
336            None
337        } else {
338            Some(GlyphId(glyph_id.into()))
339        }
340    }
341
342    /// 计算字形光栅化后的边界
343    fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
344        let image = self.render_glyph_image(params)?;
345        Ok(Bounds {
346            origin: point(image.placement.left.into(), (-image.placement.top).into()),
347            size: size(image.placement.width.into(), image.placement.height.into()),
348        })
349    }
350
351    #[profiling::function]
352    /// 光栅化字形,返回尺寸和像素数据
353    fn rasterize_glyph(
354        &mut self,
355        params: &RenderGlyphParams,
356        glyph_bounds: Bounds<DevicePixels>,
357    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
358        if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
359            anyhow::bail!("glyph bounds are empty");
360        }
361
362        let mut image = self.render_glyph_image(params)?;
363        let bitmap_size = glyph_bounds.size;
364        match image.content {
365            swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
366                // 将 RGBA 转换为 BGRA。
367                for pixel in image.data.chunks_exact_mut(4) {
368                    pixel.swap(0, 2);
369                }
370                Ok((bitmap_size, image.data))
371            }
372            swash::scale::image::Content::Mask => {
373                if params.subpixel_rendering {
374                    // 当请求子像素渲染时,我们必须始终返回 RGBA 数据。
375                    let expanded = image.data.iter().flat_map(|&a| [a, a, a, a]).collect();
376                    Ok((bitmap_size, expanded))
377                } else {
378                    Ok((bitmap_size, image.data))
379                }
380            }
381        }
382    }
383
384    /// 渲染字形图像
385    fn render_glyph_image(
386        &mut self,
387        params: &RenderGlyphParams,
388    ) -> Result<swash::scale::image::Image> {
389        let loaded_font = &self.loaded_fonts[params.font_id.0];
390        let font_ref = loaded_font.font.as_swash();
391        let pixel_size = f32::from(params.font_size);
392
393        let subpixel_offset = Vector::new(
394            params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
395            params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
396        );
397
398        let mut scaler = self
399            .swash_scale_context
400            .builder(font_ref)
401            .size(pixel_size * params.scale_factor)
402            .hint(true)
403            .build();
404
405        let sources: &[Source] = if params.is_emoji {
406            &[
407                Source::ColorOutline(0),
408                Source::ColorBitmap(StrikeWith::BestFit),
409                Source::Outline,
410            ]
411        } else {
412            &[Source::Bitmap(StrikeWith::ExactSize), Source::Outline]
413        };
414
415        let mut renderer = Render::new(sources);
416        if params.subpixel_rendering {
417            // Swash 中似乎存在一个 bug,B 和 R 值被交换了。
418            renderer
419                .format(Format::subpixel_bgra())
420                .offset(subpixel_offset);
421        } else {
422            renderer.format(Format::Alpha).offset(subpixel_offset);
423        }
424
425        let glyph_id: u16 = params.glyph_id.0.try_into()?;
426        renderer
427            .render(&mut scaler, glyph_id)
428            .with_context(|| format!("unable to render glyph via swash for {params:?}"))
429    }
430
431    /// 当 cosmic_text 选择使用回退字体而非请求的字体时(通常用于处理某些 Unicode 字符),
432    /// 会使用此方法。发生这种情况时,`loaded_fonts` 可能还没有这个回退字体的条目,
433    /// 因此会添加一个。
434    ///
435    /// 注意:调用者不应在会检索对应 `LoadedFont.features` 的地方使用此 `FontId`,
436    /// 因为它会有一个任意选择或空的值。当前此字段的唯一用途是作为 `layout_line` 的*输入*,
437    /// 因此在计算 `layout_line` 的*输出*时使用 `font_id_for_cosmic_id` 是没问题的。
438    fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> Result<FontId> {
439        if let Some(ix) = self
440            .loaded_fonts
441            .iter()
442            .position(|loaded_font| loaded_font.font.id() == id)
443        {
444            Ok(FontId(ix))
445        } else {
446            let font = self
447                .font_system
448                .get_font(id, cosmic_text::Weight::NORMAL)
449                .context("failed to get fallback font from cosmic-text font system")?;
450            let face = self
451                .font_system
452                .db()
453                .face(id)
454                .context("fallback font face not found in cosmic-text database")?;
455
456            let font_id = FontId(self.loaded_fonts.len());
457            self.loaded_fonts.push(LoadedFont {
458                font,
459                features: CosmicFontFeatures::new(),
460                is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
461                user_fallback_chain: Arc::from(Vec::new()),
462            });
463
464            Ok(font_id)
465        }
466    }
467
468    #[profiling::function]
469    /// 排版文本行,返回布局信息
470    fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
471        let mut attrs_list = AttrsList::new(&Attrs::new());
472        let mut offs = 0;
473        for run in font_runs {
474            let run_end = offs + run.len;
475
476            let loaded_font = self.loaded_font(run.font_id);
477            let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
478                log::warn!(
479                    "font face not found in database for font_id {:?}",
480                    run.font_id
481                );
482                offs = run_end;
483                continue;
484            };
485            let Some(first_family) = face.families.first() else {
486                log::warn!(
487                    "font face has no family names for font_id {:?}",
488                    run.font_id
489                );
490                offs = run_end;
491                continue;
492            };
493
494            let primary_family_name: SharedString = first_family.0.clone().into();
495            let primary_stretch = face.stretch;
496            let primary_style = face.style;
497            let primary_weight = face.weight;
498            let primary_features = loaded_font.features.clone();
499            let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain);
500
501            // 预先为每个槽位构建一个 `Attrs`。否则每个 span 属性的克隆
502            // 都会重新分配 `font_features` Vec。
503            let primary_attrs = Attrs::new()
504                .metadata(run.font_id.0)
505                .family(Family::Name(&primary_family_name))
506                .stretch(primary_stretch)
507                .style(primary_style)
508                .weight(primary_weight)
509                .font_features(primary_features.clone());
510            let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain
511                .iter()
512                .map(|(fb_id, fb_name)| {
513                    Attrs::new()
514                        .metadata(fb_id.0)
515                        .family(Family::Name(fb_name))
516                        .stretch(primary_stretch)
517                        .style(primary_style)
518                        .weight(primary_weight)
519                        .font_features(primary_features.clone())
520                })
521                .collect();
522
523            let spans = if fallback_chain.is_empty() {
524                let mut spans = SmallVec::<[RunSpan; 4]>::new();
525                spans.push(RunSpan {
526                    start: offs,
527                    end: run_end,
528                    slot: None,
529                    font_id: run.font_id,
530                });
531                spans
532            } else {
533                let loaded_fonts = &self.loaded_fonts;
534                let covers = |id: FontId, ch: char| charmap_covers(loaded_fonts, id, ch);
535                compute_run_spans(text, offs, run.len, run.font_id, &fallback_chain, &covers)
536            };
537
538            for span in spans {
539                let attrs = match span.slot {
540                    None => &primary_attrs,
541                    Some(ix) => &fallback_attrs[ix],
542                };
543                attrs_list.add_span(span.start..span.end, attrs);
544            }
545            offs = run_end;
546        }
547
548        let line = ShapeLine::new(
549            &mut self.font_system,
550            text,
551            &attrs_list,
552            cosmic_text::Shaping::Advanced,
553            4,
554        );
555        let mut layout_lines = Vec::with_capacity(1);
556        line.layout_to_buffer(
557            &mut self.scratch,
558            f32::from(font_size),
559            None, // 我们自己处理换行
560            cosmic_text::Wrap::None,
561            None,
562            &mut layout_lines,
563            None,
564            cosmic_text::Hinting::Disabled,
565        );
566
567        let Some(layout) = layout_lines.first() else {
568            return LineLayout {
569                font_size,
570                width: Pixels::ZERO,
571                ascent: Pixels::ZERO,
572                descent: Pixels::ZERO,
573                runs: Vec::new(),
574                len: text.len(),
575            };
576        };
577
578        let mut runs: Vec<ShapedRun> = Vec::new();
579        for glyph in &layout.glyphs {
580            let mut font_id = FontId(glyph.metadata);
581            let mut loaded_font = self.loaded_font(font_id);
582            if loaded_font.font.id() != glyph.font_id {
583                match self.font_id_for_cosmic_id(glyph.font_id) {
584                    std::result::Result::Ok(resolved_id) => {
585                        font_id = resolved_id;
586                        loaded_font = self.loaded_font(font_id);
587                    }
588                    Err(error) => {
589                        log::warn!(
590                            "failed to resolve cosmic font id {:?}: {error:#}",
591                            glyph.font_id
592                        );
593                        continue;
594                    }
595                }
596            }
597            let is_emoji = loaded_font.is_known_emoji_font;
598
599            // HACK: 防止因变体选择器导致的崩溃。
600            if glyph.glyph_id == 3 && is_emoji {
601                continue;
602            }
603
604            let shaped_glyph = ShapedGlyph {
605                id: GlyphId(glyph.glyph_id as u32),
606                position: point(glyph.x.into(), glyph.y.into()),
607                index: glyph.start,
608                is_emoji,
609            };
610
611            if let Some(last_run) = runs
612                .last_mut()
613                .filter(|last_run| last_run.font_id == font_id)
614            {
615                last_run.glyphs.push(shaped_glyph);
616            } else {
617                runs.push(ShapedRun {
618                    font_id,
619                    glyphs: vec![shaped_glyph],
620                });
621            }
622        }
623
624        LineLayout {
625            font_size,
626            width: layout.w.into(),
627            ascent: layout.max_ascent.into(),
628            descent: layout.max_descent.into(),
629            runs,
630            len: text.len(),
631        }
632    }
633}
634
635#[cfg(feature = "font-kit")]
636/// 在候选字体中找到最佳匹配
637fn find_best_match(
638    font: &Font,
639    candidates: &[FontId],
640    state: &CosmicTextSystemState,
641) -> Result<usize> {
642    let candidate_properties = candidates
643        .iter()
644        .map(|font_id| {
645            let database_id = state.loaded_font(*font_id).font.id();
646            let face_info = state
647                .font_system
648                .db()
649                .face(database_id)
650                .context("font face not found in database")?;
651            Ok(face_info_into_properties(face_info))
652        })
653        .collect::<Result<SmallVec<[_; 4]>>>()?;
654
655    let ix =
656        font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
657            .context("requested font family contains no font matching the other parameters")?;
658
659    Ok(ix)
660}
661
662#[cfg(not(feature = "font-kit"))]
663fn find_best_match(
664    font: &Font,
665    candidates: &[FontId],
666    state: &CosmicTextSystemState,
667) -> Result<usize> {
668    if candidates.is_empty() {
669        anyhow::bail!("requested font family contains no font matching the other parameters");
670    }
671    if candidates.len() == 1 {
672        return Ok(0);
673    }
674
675    let target_weight = font.weight.0;
676    let target_italic = matches!(
677        font.style,
678        rgpui::FontStyle::Italic | rgpui::FontStyle::Oblique
679    );
680
681    let mut best_index = 0;
682    let mut best_score = u32::MAX;
683
684    for (index, font_id) in candidates.iter().enumerate() {
685        let database_id = state.loaded_font(*font_id).font.id();
686        let face_info = state
687            .font_system
688            .db()
689            .face(database_id)
690            .context("font face not found in database")?;
691
692        let is_italic = matches!(
693            face_info.style,
694            cosmic_text::Style::Italic | cosmic_text::Style::Oblique
695        );
696        let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
697        let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
698        let score = style_penalty + weight_diff;
699
700        if score < best_score {
701            best_score = score;
702            best_index = index;
703        }
704    }
705
706    Ok(best_index)
707}
708
709/// `FontRun` 的一个连续切片,映射到单个槽位。`slot` 为
710/// `None` 表示主字体,`Some(ix)` 表示 `fallback_chain[ix]`。
711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
712struct RunSpan {
713    start: usize,
714    end: usize,
715    slot: Option<usize>,
716    font_id: FontId,
717}
718
719/// 遍历 `text[run_offset..run_offset + run_len]` 并将码点分组为
720/// span。继承码点保留在当前 span 中,以便像 emoji zwj 序列
721/// 和组合标记这样的字形簇不会被拆分。
722fn compute_run_spans(
723    text: &str,
724    run_offset: usize,
725    run_len: usize,
726    primary: FontId,
727    fallback_chain: &[(FontId, SharedString)],
728    covers: &impl Fn(FontId, char) -> bool,
729) -> SmallVec<[RunSpan; 4]> {
730    let mut spans = SmallVec::new();
731    let run_end = run_offset + run_len;
732    if run_end <= run_offset {
733        return spans;
734    }
735    if fallback_chain.is_empty() {
736        spans.push(RunSpan {
737            start: run_offset,
738            end: run_end,
739            slot: None,
740            font_id: primary,
741        });
742        return spans;
743    }
744    let run_text = &text[run_offset..run_end];
745    let mut span_start = run_offset;
746    let mut span_slot: Option<usize> = None;
747    let mut span_font_id = primary;
748    for (grapheme_idx, grapheme) in run_text.grapheme_indices(true) {
749        let abs = run_offset + grapheme_idx;
750        let ch = grapheme.chars().next().unwrap_or('\0');
751        let next_slot = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);
752        if next_slot == span_slot {
753            continue;
754        }
755        if abs > span_start {
756            spans.push(RunSpan {
757                start: span_start,
758                end: abs,
759                slot: span_slot,
760                font_id: span_font_id,
761            });
762        }
763        span_start = abs;
764        span_slot = next_slot;
765        span_font_id = slot_font_id(next_slot, primary, fallback_chain);
766    }
767    if span_start < run_end {
768        spans.push(RunSpan {
769            start: span_start,
770            end: run_end,
771            slot: span_slot,
772            font_id: span_font_id,
773        });
774    }
775    spans
776}
777
778/// 根据槽位获取字体 ID
779fn slot_font_id(
780    slot: Option<usize>,
781    primary: FontId,
782    fallback_chain: &[(FontId, SharedString)],
783) -> FontId {
784    match slot {
785        None => primary,
786        Some(ix) => fallback_chain[ix].0,
787    }
788}
789
790/// 选择能覆盖指定字符的槽位
791fn pick_covering_slot(
792    ch: char,
793    current: Option<usize>,
794    primary: FontId,
795    fallback_chain: &[(FontId, SharedString)],
796    covers: &impl Fn(FontId, char) -> bool,
797) -> Option<usize> {
798    if (ch as u32) <= 0x7F {
799        return None;
800    }
801    if covers(primary, ch) {
802        return None;
803    }
804    let current_id = slot_font_id(current, primary, fallback_chain);
805    if covers(current_id, ch) {
806        return current;
807    }
808    for (ix, (fb_id, _)) in fallback_chain.iter().enumerate() {
809        if covers(*fb_id, ch) {
810            return Some(ix);
811        }
812    }
813    None
814}
815
816/// 检查字体的字符映射是否覆盖指定字符
817fn charmap_covers(loaded_fonts: &[LoadedFont], id: FontId, ch: char) -> bool {
818    loaded_fonts
819        .get(id.0)
820        .is_some_and(|loaded| loaded.font.as_swash().charmap().map(ch) != 0)
821}
822
823/// 将 gpui 字体特性转换为 cosmic-text 字体特性
824fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
825    let mut result = CosmicFontFeatures::new();
826    for feature in features.0.iter() {
827        let name_bytes: [u8; 4] = feature
828            .0
829            .as_bytes()
830            .try_into()
831            .context("Incorrect feature flag format")?;
832
833        let tag = cosmic_text::FeatureTag::new(&name_bytes);
834
835        result.set(tag, feature.1);
836    }
837    Ok(result)
838}
839
840#[cfg(feature = "font-kit")]
841fn font_into_properties(font: &rgpui::Font) -> font_kit::properties::Properties {
842    font_kit::properties::Properties {
843        style: match font.style {
844            rgpui::FontStyle::Normal => font_kit::properties::Style::Normal,
845            rgpui::FontStyle::Italic => font_kit::properties::Style::Italic,
846            rgpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
847        },
848        weight: font_kit::properties::Weight(font.weight.0),
849        stretch: Default::default(),
850    }
851}
852
853#[cfg(feature = "font-kit")]
854fn face_info_into_properties(
855    face_info: &cosmic_text::fontdb::FaceInfo,
856) -> font_kit::properties::Properties {
857    font_kit::properties::Properties {
858        style: match face_info.style {
859            cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
860            cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
861            cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
862        },
863        weight: font_kit::properties::Weight(face_info.weight.0.into()),
864        stretch: match face_info.stretch {
865            cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
866            cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
867            cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
868            cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
869            cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
870            cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
871            cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
872            cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
873            cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
874        },
875    }
876}
877
878/// 检查是否为已知的 emoji 字体
879fn check_is_known_emoji_font(postscript_name: &str) -> bool {
880    // TODO: 包含其他常见的 emoji 字体
881    postscript_name == "NotoColorEmoji"
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    /// 创建 FontId 辅助函数
889    fn fid(i: usize) -> FontId {
890        FontId(i)
891    }
892
893    /// 创建回退链辅助函数
894    fn chain(ids: &[usize]) -> SmallVec<[(FontId, SharedString); 4]> {
895        ids.iter()
896            .map(|&i| (fid(i), SharedString::from(format!("fb{i}"))))
897            .collect()
898    }
899
900    /// 创建 RunSpan 辅助函数
901    fn span(start: usize, end: usize, slot: Option<usize>, font_id: FontId) -> RunSpan {
902        RunSpan {
903            start,
904            end,
905            slot,
906            font_id,
907        }
908    }
909
910    #[test]
911    /// 当主字体覆盖字符时,优先于当前回退字体
912    fn primary_wins_over_current_fallback_when_primary_covers() {
913        let primary = fid(0);
914        let fb = chain(&[1, 2]);
915        let covers = |id: FontId, _: char| id == fid(0) || id == fid(1);
916        assert_eq!(
917            pick_covering_slot('a', Some(0), primary, &fb, &covers),
918            None
919        );
920    }
921
922    #[test]
923    /// 当主字体和回退字体都能覆盖时,优先选择主字体
924    fn primary_preferred_over_fallback_when_both_cover() {
925        let primary = fid(0);
926        let fb = chain(&[1]);
927        let covers = |_: FontId, _: char| true;
928        assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
929    }
930
931    #[test]
932    /// 按顺序遍历回退链
933    fn falls_through_chain_in_order() {
934        let primary = fid(0);
935        let fb = chain(&[1, 2, 3]);
936        // only fallback 2 at index 1 covers.
937        let covers = |id: FontId, _: char| id == fid(2);
938        assert_eq!(
939            pick_covering_slot('字', None, primary, &fb, &covers),
940            Some(1)
941        );
942    }
943
944    #[test]
945    /// 无覆盖时返回主字体
946    fn no_coverage_returns_primary() {
947        let primary = fid(0);
948        let fb = chain(&[1, 2]);
949        let covers = |_: FontId, _: char| false;
950        // nothing covers. return `None` so the `cosmic-text` built in script
951        // fallback can take over during shaping.
952        assert_eq!(
953            pick_covering_slot('\u{1F600}', Some(1), primary, &fb, &covers),
954            None
955        );
956    }
957
958    #[test]
959    /// 空链始终返回主字体
960    fn empty_chain_always_returns_primary() {
961        let primary = fid(0);
962        let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
963        let covers = |_: FontId, _: char| false;
964        assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
965    }
966
967    #[test]
968    /// 槽位字体 ID 解析
969    fn slot_font_id_resolution() {
970        let primary = fid(7);
971        let fb = chain(&[10, 20]);
972        assert_eq!(slot_font_id(None, primary, &fb), fid(7));
973        assert_eq!(slot_font_id(Some(0), primary, &fb), fid(10));
974        assert_eq!(slot_font_id(Some(1), primary, &fb), fid(20));
975    }
976
977    #[test]
978    /// 无回退链时发射单个主字体 span
979    fn run_spans_with_no_chain_emit_one_primary_span() {
980        let primary = fid(0);
981        let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
982        let covers = |_: FontId, _: char| false;
983        let text = "hello";
984        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
985        assert_eq!(spans.as_slice(), &[span(0, text.len(), None, primary)]);
986    }
987
988    #[test]
989    /// 多字节字符使用字节偏移
990    fn run_spans_use_byte_offsets_for_multibyte_chars() {
991        let primary = fid(0);
992        let fb = chain(&[1]);
993        // primary covers ascii. fallback covers cjk.
994        let covers = |id: FontId, ch: char| {
995            if id == primary {
996                ch.is_ascii()
997            } else {
998                !ch.is_ascii()
999            }
1000        };
1001        let text = "a字b";
1002        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1003        // '字' is 3 bytes so split is at 1 then 4.
1004        assert_eq!(
1005            spans.as_slice(),
1006            &[
1007                span(0, 1, None, primary),
1008                span(1, 4, Some(0), fid(1)),
1009                span(4, 5, None, primary),
1010            ]
1011        );
1012    }
1013
1014    #[test]
1015    /// span 尊重运行偏移
1016    fn run_spans_respect_run_offset() {
1017        let primary = fid(0);
1018        let fb = chain(&[1]);
1019        let covers = |id: FontId, ch: char| {
1020            if id == primary {
1021                ch.is_ascii()
1022            } else {
1023                !ch.is_ascii()
1024            }
1025        };
1026        // outer text has a prefix that is not part of this run.
1027        let text = "xx字y";
1028        let run_offset = 2;
1029        let run_len = text.len() - run_offset;
1030        let spans = compute_run_spans(text, run_offset, run_len, primary, &fb, &covers);
1031        assert_eq!(
1032            spans.as_slice(),
1033            &[span(2, 5, Some(0), fid(1)), span(5, 6, None, primary)]
1034        );
1035    }
1036
1037    #[test]
1038    /// 组合标记与基础字符保持在回退 span 中
1039    fn run_spans_keep_combining_marks_with_base_in_fallback() {
1040        let primary = fid(0);
1041        let fb = chain(&[1]);
1042        // primary covers ascii only. fallback covers the base char.
1043        // combining mark must stay in the fallback span even when fallback
1044        // does not advertise coverage of it.
1045        let covers = |id: FontId, ch: char| {
1046            if id == primary {
1047                ch.is_ascii()
1048            } else {
1049                ch == '\u{0905}'
1050            }
1051        };
1052        // \u{0905} devanagari short a + \u{0902} candrabindu mark.
1053        let text = "\u{0905}\u{0902}";
1054        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1055        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1056    }
1057
1058    #[test]
1059    /// emoji 簇内的 ZWJ 保持不分割
1060    fn run_spans_keep_zwj_inside_emoji_cluster() {
1061        let primary = fid(0);
1062        let fb = chain(&[1]);
1063        // only fallback covers the emoji codepoints. zwj must not split.
1064        let covers = |id: FontId, ch: char| id == fid(1) && ch != '\u{200D}';
1065        // family zwj sequence woman zwj girl.
1066        let text = "\u{1F469}\u{200D}\u{1F467}";
1067        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1068        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1069    }
1070
1071    #[test]
1072    /// 相邻相同槽位合并
1073    fn run_spans_collapse_adjacent_same_slot() {
1074        let primary = fid(0);
1075        let fb = chain(&[1]);
1076        let covers = |id: FontId, ch: char| {
1077            if id == primary {
1078                ch.is_ascii()
1079            } else {
1080                !ch.is_ascii()
1081            }
1082        };
1083        let text = "字字字";
1084        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1085        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1086    }
1087
1088    #[test]
1089    /// 空运行返回空 span
1090    fn run_spans_empty_run_returns_no_spans() {
1091        let primary = fid(0);
1092        let fb = chain(&[1]);
1093        let covers = |_: FontId, _: char| true;
1094        let spans = compute_run_spans("anything", 3, 0, primary, &fb, &covers);
1095        assert!(spans.is_empty());
1096    }
1097}