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}