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