text_system.rs

  1use crate::{
  2    point, size, Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle,
  3    FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams,
  4    ShapedGlyph, SharedString, Size, SUBPIXEL_VARIANTS,
  5};
  6use anyhow::{anyhow, Context, Ok, Result};
  7use collections::HashMap;
  8use cosmic_text::{
  9    Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontSystem, ShapeBuffer, ShapeLine,
 10    SwashCache,
 11};
 12
 13use itertools::Itertools;
 14use parking_lot::RwLock;
 15use pathfinder_geometry::{
 16    rect::{RectF, RectI},
 17    vector::{Vector2F, Vector2I},
 18};
 19use smallvec::SmallVec;
 20use std::{borrow::Cow, sync::Arc};
 21
 22pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
 23
 24struct CosmicTextSystemState {
 25    swash_cache: SwashCache,
 26    font_system: FontSystem,
 27    scratch: ShapeBuffer,
 28    /// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
 29    loaded_fonts_store: Vec<Arc<CosmicTextFont>>,
 30    /// Caches the `FontId`s associated with a specific family to avoid iterating the font database
 31    /// for every font face in a family.
 32    font_ids_by_family_cache: HashMap<SharedString, SmallVec<[FontId; 4]>>,
 33    /// The name of each font associated with the given font id
 34    postscript_names: HashMap<FontId, String>,
 35}
 36
 37impl CosmicTextSystem {
 38    pub(crate) fn new() -> Self {
 39        let mut font_system = FontSystem::new();
 40
 41        // todo(linux) make font loading non-blocking
 42        font_system.db_mut().load_system_fonts();
 43
 44        Self(RwLock::new(CosmicTextSystemState {
 45            font_system,
 46            swash_cache: SwashCache::new(),
 47            scratch: ShapeBuffer::default(),
 48            loaded_fonts_store: Vec::new(),
 49            font_ids_by_family_cache: HashMap::default(),
 50            postscript_names: HashMap::default(),
 51        }))
 52    }
 53}
 54
 55impl Default for CosmicTextSystem {
 56    fn default() -> Self {
 57        Self::new()
 58    }
 59}
 60
 61impl PlatformTextSystem for CosmicTextSystem {
 62    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
 63        self.0.write().add_fonts(fonts)
 64    }
 65
 66    fn all_font_names(&self) -> Vec<String> {
 67        let mut result = self
 68            .0
 69            .read()
 70            .font_system
 71            .db()
 72            .faces()
 73            .filter_map(|face| face.families.first().map(|family| family.0.clone()))
 74            .collect_vec();
 75        result.sort();
 76        result.dedup();
 77        result
 78    }
 79
 80    fn font_id(&self, font: &Font) -> Result<FontId> {
 81        // todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
 82        let mut state = self.0.write();
 83
 84        let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
 85            font_ids.as_slice()
 86        } else {
 87            let font_ids = state.load_family(&font.family, &font.features)?;
 88            state
 89                .font_ids_by_family_cache
 90                .insert(font.family.clone(), font_ids);
 91            state.font_ids_by_family_cache[&font.family].as_ref()
 92        };
 93
 94        // todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
 95        let candidate_properties = candidates
 96            .iter()
 97            .map(|font_id| {
 98                let database_id = state.loaded_fonts_store[font_id.0].id();
 99                let face_info = state.font_system.db().face(database_id).expect("");
100                face_info_into_properties(face_info)
101            })
102            .collect::<SmallVec<[_; 4]>>();
103
104        let ix =
105            font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
106                .context("requested font family contains no font matching the other parameters")?;
107
108        Ok(candidates[ix])
109    }
110
111    fn font_metrics(&self, font_id: FontId) -> FontMetrics {
112        let metrics = self.0.read().loaded_fonts_store[font_id.0]
113            .as_swash()
114            .metrics(&[]);
115
116        FontMetrics {
117            units_per_em: metrics.units_per_em as u32,
118            ascent: metrics.ascent,
119            descent: -metrics.descent, // todo(linux) confirm this is correct
120            line_gap: metrics.leading,
121            underline_position: metrics.underline_offset,
122            underline_thickness: metrics.stroke_size,
123            cap_height: metrics.cap_height,
124            x_height: metrics.x_height,
125            // todo(linux): Compute this correctly
126            bounding_box: Bounds {
127                origin: point(0.0, 0.0),
128                size: size(metrics.max_width, metrics.ascent + metrics.descent),
129            },
130        }
131    }
132
133    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
134        let lock = self.0.read();
135        let glyph_metrics = lock.loaded_fonts_store[font_id.0]
136            .as_swash()
137            .glyph_metrics(&[]);
138        let glyph_id = glyph_id.0 as u16;
139        // todo(linux): Compute this correctly
140        // see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620
141        Ok(Bounds {
142            origin: point(0.0, 0.0),
143            size: size(
144                glyph_metrics.advance_width(glyph_id),
145                glyph_metrics.advance_height(glyph_id),
146            ),
147        })
148    }
149
150    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
151        self.0.read().advance(font_id, glyph_id)
152    }
153
154    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
155        self.0.read().glyph_for_char(font_id, ch)
156    }
157
158    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
159        self.0.write().raster_bounds(params)
160    }
161
162    fn rasterize_glyph(
163        &self,
164        params: &RenderGlyphParams,
165        raster_bounds: Bounds<DevicePixels>,
166    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
167        self.0.write().rasterize_glyph(params, raster_bounds)
168    }
169
170    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
171        self.0.write().layout_line(text, font_size, runs)
172    }
173}
174
175impl CosmicTextSystemState {
176    #[profiling::function]
177    fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
178        let db = self.font_system.db_mut();
179        for bytes in fonts {
180            match bytes {
181                Cow::Borrowed(embedded_font) => {
182                    db.load_font_data(embedded_font.to_vec());
183                }
184                Cow::Owned(bytes) => {
185                    db.load_font_data(bytes);
186                }
187            }
188        }
189        Ok(())
190    }
191
192    // todo(linux) handle `FontFeatures`
193    #[profiling::function]
194    fn load_family(
195        &mut self,
196        name: &str,
197        _features: &FontFeatures,
198    ) -> Result<SmallVec<[FontId; 4]>> {
199        // TODO: Determine the proper system UI font.
200        let name = if name == ".SystemUIFont" {
201            "Zed Plex Sans"
202        } else {
203            name
204        };
205
206        let mut font_ids = SmallVec::new();
207        let families = self
208            .font_system
209            .db()
210            .faces()
211            .filter(|face| face.families.iter().any(|family| *name == family.0))
212            .map(|face| (face.id, face.post_script_name.clone()))
213            .collect::<SmallVec<[_; 4]>>();
214
215        for (font_id, postscript_name) in families {
216            let font = self
217                .font_system
218                .get_font(font_id)
219                .ok_or_else(|| anyhow!("Could not load font"))?;
220
221            // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
222            let allowed_bad_font_names = [
223                "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
224                "Segoe Fluent Icons",
225            ];
226
227            if font.as_swash().charmap().map('m') == 0
228                && !allowed_bad_font_names.contains(&postscript_name.as_str())
229            {
230                self.font_system.db_mut().remove_face(font.id());
231                continue;
232            };
233
234            let font_id = FontId(self.loaded_fonts_store.len());
235            font_ids.push(font_id);
236            self.loaded_fonts_store.push(font);
237            self.postscript_names.insert(font_id, postscript_name);
238        }
239
240        Ok(font_ids)
241    }
242
243    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
244        let width = self.loaded_fonts_store[font_id.0]
245            .as_swash()
246            .glyph_metrics(&[])
247            .advance_width(glyph_id.0 as u16);
248        let height = self.loaded_fonts_store[font_id.0]
249            .as_swash()
250            .glyph_metrics(&[])
251            .advance_height(glyph_id.0 as u16);
252        Ok(Size { width, height })
253    }
254
255    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
256        let glyph_id = self.loaded_fonts_store[font_id.0]
257            .as_swash()
258            .charmap()
259            .map(ch);
260        if glyph_id == 0 {
261            None
262        } else {
263            Some(GlyphId(glyph_id.into()))
264        }
265    }
266
267    fn is_emoji(&self, font_id: FontId) -> bool {
268        // TODO: Include other common emoji fonts
269        self.postscript_names
270            .get(&font_id)
271            .map_or(false, |postscript_name| postscript_name == "NotoColorEmoji")
272    }
273
274    fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
275        let font = &self.loaded_fonts_store[params.font_id.0];
276        let image = self
277            .swash_cache
278            .get_image(
279                &mut self.font_system,
280                CacheKey::new(
281                    font.id(),
282                    params.glyph_id.0 as u16,
283                    (params.font_size * params.scale_factor).into(),
284                    (0.0, 0.0),
285                    cosmic_text::CacheKeyFlags::empty(),
286                )
287                .0,
288            )
289            .clone()
290            .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
291        Ok(Bounds {
292            origin: point(image.placement.left.into(), (-image.placement.top).into()),
293            size: size(image.placement.width.into(), image.placement.height.into()),
294        })
295    }
296
297    #[profiling::function]
298    fn rasterize_glyph(
299        &mut self,
300        params: &RenderGlyphParams,
301        glyph_bounds: Bounds<DevicePixels>,
302    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
303        if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
304            Err(anyhow!("glyph bounds are empty"))
305        } else {
306            let bitmap_size = glyph_bounds.size;
307            let font = &self.loaded_fonts_store[params.font_id.0];
308            let subpixel_shift = params
309                .subpixel_variant
310                .map(|v| v as f32 / (SUBPIXEL_VARIANTS as f32 * params.scale_factor));
311            let mut image = self
312                .swash_cache
313                .get_image(
314                    &mut self.font_system,
315                    CacheKey::new(
316                        font.id(),
317                        params.glyph_id.0 as u16,
318                        (params.font_size * params.scale_factor).into(),
319                        (subpixel_shift.x, subpixel_shift.y.trunc()),
320                        cosmic_text::CacheKeyFlags::empty(),
321                    )
322                    .0,
323                )
324                .clone()
325                .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
326
327            if params.is_emoji {
328                // Convert from RGBA to BGRA.
329                for pixel in image.data.chunks_exact_mut(4) {
330                    pixel.swap(0, 2);
331                }
332            }
333
334            Ok((bitmap_size, image.data))
335        }
336    }
337
338    fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
339        if let Some(ix) = self
340            .loaded_fonts_store
341            .iter()
342            .position(|font| font.id() == id)
343        {
344            FontId(ix)
345        } else {
346            // This matches the behavior of the mac text system
347            let font = self.font_system.get_font(id).unwrap();
348            let face = self
349                .font_system
350                .db()
351                .faces()
352                .find(|info| info.id == id)
353                .unwrap();
354
355            let font_id = FontId(self.loaded_fonts_store.len());
356            self.loaded_fonts_store.push(font);
357            self.postscript_names
358                .insert(font_id, face.post_script_name.clone());
359
360            font_id
361        }
362    }
363
364    #[profiling::function]
365    fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
366        let mut attrs_list = AttrsList::new(Attrs::new());
367        let mut offs = 0;
368        for run in font_runs {
369            let font = &self.loaded_fonts_store[run.font_id.0];
370            let font = self.font_system.db().face(font.id()).unwrap();
371            attrs_list.add_span(
372                offs..(offs + run.len),
373                Attrs::new()
374                    .family(Family::Name(&font.families.first().unwrap().0))
375                    .stretch(font.stretch)
376                    .style(font.style)
377                    .weight(font.weight),
378            );
379            offs += run.len;
380        }
381        let mut line = ShapeLine::new_in_buffer(
382            &mut self.scratch,
383            &mut self.font_system,
384            text,
385            &attrs_list,
386            cosmic_text::Shaping::Advanced,
387            4,
388        );
389
390        let mut layout = Vec::with_capacity(1);
391        line.layout_to_buffer(
392            &mut self.scratch,
393            font_size.0,
394            None, // We do our own wrapping
395            cosmic_text::Wrap::None,
396            None,
397            &mut layout,
398            None,
399        );
400
401        let mut runs = Vec::new();
402        let layout = layout.first().unwrap();
403        for glyph in &layout.glyphs {
404            let font_id = glyph.font_id;
405            let font_id = self.font_id_for_cosmic_id(font_id);
406            let is_emoji = self.is_emoji(font_id);
407            let mut glyphs = SmallVec::new();
408
409            // HACK: Prevent crash caused by variation selectors.
410            if glyph.glyph_id == 3 && is_emoji {
411                continue;
412            }
413
414            // todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction
415            glyphs.push(ShapedGlyph {
416                id: GlyphId(glyph.glyph_id as u32),
417                position: point(glyph.x.into(), glyph.y.into()),
418                index: glyph.start,
419                is_emoji,
420            });
421
422            runs.push(crate::ShapedRun { font_id, glyphs });
423        }
424
425        LineLayout {
426            font_size,
427            width: layout.w.into(),
428            ascent: layout.max_ascent.into(),
429            descent: layout.max_descent.into(),
430            runs,
431            len: text.len(),
432        }
433    }
434}
435
436impl From<RectF> for Bounds<f32> {
437    fn from(rect: RectF) -> Self {
438        Bounds {
439            origin: point(rect.origin_x(), rect.origin_y()),
440            size: size(rect.width(), rect.height()),
441        }
442    }
443}
444
445impl From<RectI> for Bounds<DevicePixels> {
446    fn from(rect: RectI) -> Self {
447        Bounds {
448            origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
449            size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
450        }
451    }
452}
453
454impl From<Vector2I> for Size<DevicePixels> {
455    fn from(value: Vector2I) -> Self {
456        size(value.x().into(), value.y().into())
457    }
458}
459
460impl From<RectI> for Bounds<i32> {
461    fn from(rect: RectI) -> Self {
462        Bounds {
463            origin: point(rect.origin_x(), rect.origin_y()),
464            size: size(rect.width(), rect.height()),
465        }
466    }
467}
468
469impl From<Point<u32>> for Vector2I {
470    fn from(size: Point<u32>) -> Self {
471        Vector2I::new(size.x as i32, size.y as i32)
472    }
473}
474
475impl From<Vector2F> for Size<f32> {
476    fn from(vec: Vector2F) -> Self {
477        size(vec.x(), vec.y())
478    }
479}
480
481impl From<FontWeight> for cosmic_text::Weight {
482    fn from(value: FontWeight) -> Self {
483        cosmic_text::Weight(value.0 as u16)
484    }
485}
486
487impl From<FontStyle> for cosmic_text::Style {
488    fn from(style: FontStyle) -> Self {
489        match style {
490            FontStyle::Normal => cosmic_text::Style::Normal,
491            FontStyle::Italic => cosmic_text::Style::Italic,
492            FontStyle::Oblique => cosmic_text::Style::Oblique,
493        }
494    }
495}
496
497fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
498    font_kit::properties::Properties {
499        style: match font.style {
500            crate::FontStyle::Normal => font_kit::properties::Style::Normal,
501            crate::FontStyle::Italic => font_kit::properties::Style::Italic,
502            crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
503        },
504        weight: font_kit::properties::Weight(font.weight.0),
505        stretch: Default::default(),
506    }
507}
508
509fn face_info_into_properties(
510    face_info: &cosmic_text::fontdb::FaceInfo,
511) -> font_kit::properties::Properties {
512    font_kit::properties::Properties {
513        style: match face_info.style {
514            cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
515            cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
516            cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
517        },
518        // both libs use the same values for weight
519        weight: font_kit::properties::Weight(face_info.weight.0.into()),
520        stretch: match face_info.stretch {
521            cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
522            cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
523            cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
524            cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
525            cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
526            cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
527            cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
528            cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
529            cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
530        },
531    }
532}