Skip to main content

rgpui_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 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            Ellipsize::None,
562            None,
563            &mut layout_lines,
564            None,
565            cosmic_text::Hinting::Disabled,
566        );
567
568        let Some(layout) = layout_lines.first() else {
569            return LineLayout {
570                font_size,
571                width: Pixels::ZERO,
572                ascent: Pixels::ZERO,
573                descent: Pixels::ZERO,
574                runs: Vec::new(),
575                len: text.len(),
576            };
577        };
578
579        let mut runs: Vec<ShapedRun> = Vec::new();
580        for glyph in &layout.glyphs {
581            let mut font_id = FontId(glyph.metadata);
582            let mut loaded_font = self.loaded_font(font_id);
583            if loaded_font.font.id() != glyph.font_id {
584                match self.font_id_for_cosmic_id(glyph.font_id) {
585                    std::result::Result::Ok(resolved_id) => {
586                        font_id = resolved_id;
587                        loaded_font = self.loaded_font(font_id);
588                    }
589                    Err(error) => {
590                        log::warn!(
591                            "failed to resolve cosmic font id {:?}: {error:#}",
592                            glyph.font_id
593                        );
594                        continue;
595                    }
596                }
597            }
598            let is_emoji = loaded_font.is_known_emoji_font;
599
600            // HACK: 防止因变体选择器导致的崩溃。
601            if glyph.glyph_id == 3 && is_emoji {
602                continue;
603            }
604
605            let shaped_glyph = ShapedGlyph {
606                id: GlyphId(glyph.glyph_id as u32),
607                position: point(glyph.x.into(), glyph.y.into()),
608                index: glyph.start,
609                is_emoji,
610            };
611
612            if let Some(last_run) = runs
613                .last_mut()
614                .filter(|last_run| last_run.font_id == font_id)
615            {
616                last_run.glyphs.push(shaped_glyph);
617            } else {
618                runs.push(ShapedRun {
619                    font_id,
620                    glyphs: vec![shaped_glyph],
621                });
622            }
623        }
624
625        LineLayout {
626            font_size,
627            width: layout.w.into(),
628            ascent: layout.max_ascent.into(),
629            descent: layout.max_descent.into(),
630            runs,
631            len: text.len(),
632        }
633    }
634}
635
636#[cfg(feature = "font-kit")]
637/// 在候选字体中找到最佳匹配
638fn find_best_match(
639    font: &Font,
640    candidates: &[FontId],
641    state: &CosmicTextSystemState,
642) -> Result<usize> {
643    let candidate_properties = candidates
644        .iter()
645        .map(|font_id| {
646            let database_id = state.loaded_font(*font_id).font.id();
647            let face_info = state
648                .font_system
649                .db()
650                .face(database_id)
651                .context("font face not found in database")?;
652            Ok(face_info_into_properties(face_info))
653        })
654        .collect::<Result<SmallVec<[_; 4]>>>()?;
655
656    let ix =
657        font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
658            .context("requested font family contains no font matching the other parameters")?;
659
660    Ok(ix)
661}
662
663#[cfg(not(feature = "font-kit"))]
664fn find_best_match(
665    font: &Font,
666    candidates: &[FontId],
667    state: &CosmicTextSystemState,
668) -> Result<usize> {
669    if candidates.is_empty() {
670        anyhow::bail!("requested font family contains no font matching the other parameters");
671    }
672    if candidates.len() == 1 {
673        return Ok(0);
674    }
675
676    let target_weight = font.weight.0;
677    let target_italic = matches!(
678        font.style,
679        rgpui::FontStyle::Italic | rgpui::FontStyle::Oblique
680    );
681
682    let mut best_index = 0;
683    let mut best_score = u32::MAX;
684
685    for (index, font_id) in candidates.iter().enumerate() {
686        let database_id = state.loaded_font(*font_id).font.id();
687        let face_info = state
688            .font_system
689            .db()
690            .face(database_id)
691            .context("font face not found in database")?;
692
693        let is_italic = matches!(
694            face_info.style,
695            cosmic_text::Style::Italic | cosmic_text::Style::Oblique
696        );
697        let style_penalty: u32 = if is_italic == target_italic { 0 } else { 1000 };
698        let weight_diff = (face_info.weight.0 as i32 - target_weight as i32).unsigned_abs();
699        let score = style_penalty + weight_diff;
700
701        if score < best_score {
702            best_score = score;
703            best_index = index;
704        }
705    }
706
707    Ok(best_index)
708}
709
710/// `FontRun` 的一个连续切片,映射到单个槽位。`slot` 为
711/// `None` 表示主字体,`Some(ix)` 表示 `fallback_chain[ix]`。
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713struct RunSpan {
714    start: usize,
715    end: usize,
716    slot: Option<usize>,
717    font_id: FontId,
718}
719
720/// 遍历 `text[run_offset..run_offset + run_len]` 并将码点分组为
721/// span。继承码点保留在当前 span 中,以便像 emoji zwj 序列
722/// 和组合标记这样的字形簇不会被拆分。
723fn compute_run_spans(
724    text: &str,
725    run_offset: usize,
726    run_len: usize,
727    primary: FontId,
728    fallback_chain: &[(FontId, SharedString)],
729    covers: &impl Fn(FontId, char) -> bool,
730) -> SmallVec<[RunSpan; 4]> {
731    let mut spans = SmallVec::new();
732    let run_end = run_offset + run_len;
733    if run_end <= run_offset {
734        return spans;
735    }
736    if fallback_chain.is_empty() {
737        spans.push(RunSpan {
738            start: run_offset,
739            end: run_end,
740            slot: None,
741            font_id: primary,
742        });
743        return spans;
744    }
745    let run_text = &text[run_offset..run_end];
746    let mut span_start = run_offset;
747    let mut span_slot: Option<usize> = None;
748    let mut span_font_id = primary;
749    for (grapheme_idx, grapheme) in run_text.grapheme_indices(true) {
750        let abs = run_offset + grapheme_idx;
751        let ch = grapheme.chars().next().unwrap_or('\0');
752        let next_slot = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);
753        if next_slot == span_slot {
754            continue;
755        }
756        if abs > span_start {
757            spans.push(RunSpan {
758                start: span_start,
759                end: abs,
760                slot: span_slot,
761                font_id: span_font_id,
762            });
763        }
764        span_start = abs;
765        span_slot = next_slot;
766        span_font_id = slot_font_id(next_slot, primary, fallback_chain);
767    }
768    if span_start < run_end {
769        spans.push(RunSpan {
770            start: span_start,
771            end: run_end,
772            slot: span_slot,
773            font_id: span_font_id,
774        });
775    }
776    spans
777}
778
779/// 根据槽位获取字体 ID
780fn slot_font_id(
781    slot: Option<usize>,
782    primary: FontId,
783    fallback_chain: &[(FontId, SharedString)],
784) -> FontId {
785    match slot {
786        None => primary,
787        Some(ix) => fallback_chain[ix].0,
788    }
789}
790
791/// 选择能覆盖指定字符的槽位
792fn pick_covering_slot(
793    ch: char,
794    current: Option<usize>,
795    primary: FontId,
796    fallback_chain: &[(FontId, SharedString)],
797    covers: &impl Fn(FontId, char) -> bool,
798) -> Option<usize> {
799    if (ch as u32) <= 0x7F {
800        return None;
801    }
802    if covers(primary, ch) {
803        return None;
804    }
805    let current_id = slot_font_id(current, primary, fallback_chain);
806    if covers(current_id, ch) {
807        return current;
808    }
809    for (ix, (fb_id, _)) in fallback_chain.iter().enumerate() {
810        if covers(*fb_id, ch) {
811            return Some(ix);
812        }
813    }
814    None
815}
816
817/// 检查字体的字符映射是否覆盖指定字符
818fn charmap_covers(loaded_fonts: &[LoadedFont], id: FontId, ch: char) -> bool {
819    loaded_fonts
820        .get(id.0)
821        .is_some_and(|loaded| loaded.font.as_swash().charmap().map(ch) != 0)
822}
823
824/// 将 gpui 字体特性转换为 cosmic-text 字体特性
825fn cosmic_font_features(features: &FontFeatures) -> Result<CosmicFontFeatures> {
826    let mut result = CosmicFontFeatures::new();
827    for feature in features.0.iter() {
828        let name_bytes: [u8; 4] = feature
829            .0
830            .as_bytes()
831            .try_into()
832            .context("Incorrect feature flag format")?;
833
834        let tag = cosmic_text::FeatureTag::new(&name_bytes);
835
836        result.set(tag, feature.1);
837    }
838    Ok(result)
839}
840
841#[cfg(feature = "font-kit")]
842fn font_into_properties(font: &rgpui::Font) -> font_kit::properties::Properties {
843    font_kit::properties::Properties {
844        style: match font.style {
845            rgpui::FontStyle::Normal => font_kit::properties::Style::Normal,
846            rgpui::FontStyle::Italic => font_kit::properties::Style::Italic,
847            rgpui::FontStyle::Oblique => font_kit::properties::Style::Oblique,
848        },
849        weight: font_kit::properties::Weight(font.weight.0),
850        stretch: Default::default(),
851    }
852}
853
854#[cfg(feature = "font-kit")]
855fn face_info_into_properties(
856    face_info: &cosmic_text::fontdb::FaceInfo,
857) -> font_kit::properties::Properties {
858    font_kit::properties::Properties {
859        style: match face_info.style {
860            cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
861            cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
862            cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
863        },
864        weight: font_kit::properties::Weight(face_info.weight.0.into()),
865        stretch: match face_info.stretch {
866            cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
867            cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
868            cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
869            cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
870            cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
871            cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
872            cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
873            cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
874            cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
875        },
876    }
877}
878
879/// 检查是否为已知的 emoji 字体
880fn check_is_known_emoji_font(postscript_name: &str) -> bool {
881    // TODO: 包含其他常见的 emoji 字体
882    postscript_name == "NotoColorEmoji"
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888
889    /// 创建 FontId 辅助函数
890    fn fid(i: usize) -> FontId {
891        FontId(i)
892    }
893
894    /// 创建回退链辅助函数
895    fn chain(ids: &[usize]) -> SmallVec<[(FontId, SharedString); 4]> {
896        ids.iter()
897            .map(|&i| (fid(i), SharedString::from(format!("fb{i}"))))
898            .collect()
899    }
900
901    /// 创建 RunSpan 辅助函数
902    fn span(start: usize, end: usize, slot: Option<usize>, font_id: FontId) -> RunSpan {
903        RunSpan {
904            start,
905            end,
906            slot,
907            font_id,
908        }
909    }
910
911    #[test]
912    /// 当主字体覆盖字符时,优先于当前回退字体
913    fn primary_wins_over_current_fallback_when_primary_covers() {
914        let primary = fid(0);
915        let fb = chain(&[1, 2]);
916        let covers = |id: FontId, _: char| id == fid(0) || id == fid(1);
917        assert_eq!(
918            pick_covering_slot('a', Some(0), primary, &fb, &covers),
919            None
920        );
921    }
922
923    #[test]
924    /// 当主字体和回退字体都能覆盖时,优先选择主字体
925    fn primary_preferred_over_fallback_when_both_cover() {
926        let primary = fid(0);
927        let fb = chain(&[1]);
928        let covers = |_: FontId, _: char| true;
929        assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
930    }
931
932    #[test]
933    /// 按顺序遍历回退链
934    fn falls_through_chain_in_order() {
935        let primary = fid(0);
936        let fb = chain(&[1, 2, 3]);
937        // only fallback 2 at index 1 covers.
938        let covers = |id: FontId, _: char| id == fid(2);
939        assert_eq!(
940            pick_covering_slot('字', None, primary, &fb, &covers),
941            Some(1)
942        );
943    }
944
945    #[test]
946    /// 无覆盖时返回主字体
947    fn no_coverage_returns_primary() {
948        let primary = fid(0);
949        let fb = chain(&[1, 2]);
950        let covers = |_: FontId, _: char| false;
951        // nothing covers. return `None` so the `cosmic-text` built in script
952        // fallback can take over during shaping.
953        assert_eq!(
954            pick_covering_slot('\u{1F600}', Some(1), primary, &fb, &covers),
955            None
956        );
957    }
958
959    #[test]
960    /// 空链始终返回主字体
961    fn empty_chain_always_returns_primary() {
962        let primary = fid(0);
963        let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
964        let covers = |_: FontId, _: char| false;
965        assert_eq!(pick_covering_slot('a', None, primary, &fb, &covers), None);
966    }
967
968    #[test]
969    /// 槽位字体 ID 解析
970    fn slot_font_id_resolution() {
971        let primary = fid(7);
972        let fb = chain(&[10, 20]);
973        assert_eq!(slot_font_id(None, primary, &fb), fid(7));
974        assert_eq!(slot_font_id(Some(0), primary, &fb), fid(10));
975        assert_eq!(slot_font_id(Some(1), primary, &fb), fid(20));
976    }
977
978    #[test]
979    /// 无回退链时发射单个主字体 span
980    fn run_spans_with_no_chain_emit_one_primary_span() {
981        let primary = fid(0);
982        let fb: SmallVec<[(FontId, SharedString); 4]> = SmallVec::new();
983        let covers = |_: FontId, _: char| false;
984        let text = "hello";
985        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
986        assert_eq!(spans.as_slice(), &[span(0, text.len(), None, primary)]);
987    }
988
989    #[test]
990    /// 多字节字符使用字节偏移
991    fn run_spans_use_byte_offsets_for_multibyte_chars() {
992        let primary = fid(0);
993        let fb = chain(&[1]);
994        // primary covers ascii. fallback covers cjk.
995        let covers = |id: FontId, ch: char| {
996            if id == primary {
997                ch.is_ascii()
998            } else {
999                !ch.is_ascii()
1000            }
1001        };
1002        let text = "a字b";
1003        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1004        // '字' is 3 bytes so split is at 1 then 4.
1005        assert_eq!(
1006            spans.as_slice(),
1007            &[
1008                span(0, 1, None, primary),
1009                span(1, 4, Some(0), fid(1)),
1010                span(4, 5, None, primary),
1011            ]
1012        );
1013    }
1014
1015    #[test]
1016    /// span 尊重运行偏移
1017    fn run_spans_respect_run_offset() {
1018        let primary = fid(0);
1019        let fb = chain(&[1]);
1020        let covers = |id: FontId, ch: char| {
1021            if id == primary {
1022                ch.is_ascii()
1023            } else {
1024                !ch.is_ascii()
1025            }
1026        };
1027        // outer text has a prefix that is not part of this run.
1028        let text = "xx字y";
1029        let run_offset = 2;
1030        let run_len = text.len() - run_offset;
1031        let spans = compute_run_spans(text, run_offset, run_len, primary, &fb, &covers);
1032        assert_eq!(
1033            spans.as_slice(),
1034            &[span(2, 5, Some(0), fid(1)), span(5, 6, None, primary)]
1035        );
1036    }
1037
1038    #[test]
1039    /// 组合标记与基础字符保持在回退 span 中
1040    fn run_spans_keep_combining_marks_with_base_in_fallback() {
1041        let primary = fid(0);
1042        let fb = chain(&[1]);
1043        // primary covers ascii only. fallback covers the base char.
1044        // combining mark must stay in the fallback span even when fallback
1045        // does not advertise coverage of it.
1046        let covers = |id: FontId, ch: char| {
1047            if id == primary {
1048                ch.is_ascii()
1049            } else {
1050                ch == '\u{0905}'
1051            }
1052        };
1053        // \u{0905} devanagari short a + \u{0902} candrabindu mark.
1054        let text = "\u{0905}\u{0902}";
1055        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1056        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1057    }
1058
1059    #[test]
1060    /// emoji 簇内的 ZWJ 保持不分割
1061    fn run_spans_keep_zwj_inside_emoji_cluster() {
1062        let primary = fid(0);
1063        let fb = chain(&[1]);
1064        // only fallback covers the emoji codepoints. zwj must not split.
1065        let covers = |id: FontId, ch: char| id == fid(1) && ch != '\u{200D}';
1066        // family zwj sequence woman zwj girl.
1067        let text = "\u{1F469}\u{200D}\u{1F467}";
1068        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1069        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1070    }
1071
1072    #[test]
1073    /// 相邻相同槽位合并
1074    fn run_spans_collapse_adjacent_same_slot() {
1075        let primary = fid(0);
1076        let fb = chain(&[1]);
1077        let covers = |id: FontId, ch: char| {
1078            if id == primary {
1079                ch.is_ascii()
1080            } else {
1081                !ch.is_ascii()
1082            }
1083        };
1084        let text = "字字字";
1085        let spans = compute_run_spans(text, 0, text.len(), primary, &fb, &covers);
1086        assert_eq!(spans.as_slice(), &[span(0, text.len(), Some(0), fid(1))]);
1087    }
1088
1089    #[test]
1090    /// 空运行返回空 span
1091    fn run_spans_empty_run_returns_no_spans() {
1092        let primary = fid(0);
1093        let fb = chain(&[1]);
1094        let covers = |_: FontId, _: char| true;
1095        let spans = compute_run_spans("anything", 3, 0, primary, &fb, &covers);
1096        assert!(spans.is_empty());
1097    }
1098}