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 #[cfg(target_os = "windows")]
182 fn destroy(&self) {}
183}
184
185impl CosmicTextSystemState {
186 #[profiling::function]
187 fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
188 let db = self.font_system.db_mut();
189 for bytes in fonts {
190 match bytes {
191 Cow::Borrowed(embedded_font) => {
192 db.load_font_data(embedded_font.to_vec());
193 }
194 Cow::Owned(bytes) => {
195 db.load_font_data(bytes);
196 }
197 }
198 }
199 Ok(())
200 }
201
202 // todo(linux) handle `FontFeatures`
203 #[profiling::function]
204 fn load_family(
205 &mut self,
206 name: &str,
207 _features: &FontFeatures,
208 ) -> Result<SmallVec<[FontId; 4]>> {
209 // TODO: Determine the proper system UI font.
210 let name = if name == ".SystemUIFont" {
211 "Zed Plex Sans"
212 } else {
213 name
214 };
215
216 let mut font_ids = SmallVec::new();
217 let families = self
218 .font_system
219 .db()
220 .faces()
221 .filter(|face| face.families.iter().any(|family| *name == family.0))
222 .map(|face| (face.id, face.post_script_name.clone()))
223 .collect::<SmallVec<[_; 4]>>();
224
225 for (font_id, postscript_name) in families {
226 let font = self
227 .font_system
228 .get_font(font_id)
229 .ok_or_else(|| anyhow!("Could not load font"))?;
230
231 // HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
232 let allowed_bad_font_names = [
233 "SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
234 "Segoe Fluent Icons",
235 ];
236
237 if font.as_swash().charmap().map('m') == 0
238 && !allowed_bad_font_names.contains(&postscript_name.as_str())
239 {
240 self.font_system.db_mut().remove_face(font.id());
241 continue;
242 };
243
244 let font_id = FontId(self.loaded_fonts_store.len());
245 font_ids.push(font_id);
246 self.loaded_fonts_store.push(font);
247 self.postscript_names.insert(font_id, postscript_name);
248 }
249
250 Ok(font_ids)
251 }
252
253 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
254 let width = self.loaded_fonts_store[font_id.0]
255 .as_swash()
256 .glyph_metrics(&[])
257 .advance_width(glyph_id.0 as u16);
258 let height = self.loaded_fonts_store[font_id.0]
259 .as_swash()
260 .glyph_metrics(&[])
261 .advance_height(glyph_id.0 as u16);
262 Ok(Size { width, height })
263 }
264
265 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
266 let glyph_id = self.loaded_fonts_store[font_id.0]
267 .as_swash()
268 .charmap()
269 .map(ch);
270 if glyph_id == 0 {
271 None
272 } else {
273 Some(GlyphId(glyph_id.into()))
274 }
275 }
276
277 fn is_emoji(&self, font_id: FontId) -> bool {
278 // TODO: Include other common emoji fonts
279 self.postscript_names
280 .get(&font_id)
281 .map_or(false, |postscript_name| postscript_name == "NotoColorEmoji")
282 }
283
284 fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
285 let font = &self.loaded_fonts_store[params.font_id.0];
286 let font_system = &mut self.font_system;
287 let image = self
288 .swash_cache
289 .get_image(
290 font_system,
291 CacheKey::new(
292 font.id(),
293 params.glyph_id.0 as u16,
294 (params.font_size * params.scale_factor).into(),
295 (0.0, 0.0),
296 cosmic_text::CacheKeyFlags::empty(),
297 )
298 .0,
299 )
300 .clone()
301 .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
302 Ok(Bounds {
303 origin: point(image.placement.left.into(), (-image.placement.top).into()),
304 size: size(image.placement.width.into(), image.placement.height.into()),
305 })
306 }
307
308 #[profiling::function]
309 fn rasterize_glyph(
310 &mut self,
311 params: &RenderGlyphParams,
312 glyph_bounds: Bounds<DevicePixels>,
313 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
314 if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
315 Err(anyhow!("glyph bounds are empty"))
316 } else {
317 // todo(linux) handle subpixel variants
318 let bitmap_size = glyph_bounds.size;
319 let font = &self.loaded_fonts_store[params.font_id.0];
320 let font_system = &mut self.font_system;
321 let mut image = self
322 .swash_cache
323 .get_image(
324 font_system,
325 CacheKey::new(
326 font.id(),
327 params.glyph_id.0 as u16,
328 (params.font_size * params.scale_factor).into(),
329 (0.0, 0.0),
330 cosmic_text::CacheKeyFlags::empty(),
331 )
332 .0,
333 )
334 .clone()
335 .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
336
337 if params.is_emoji {
338 // Convert from RGBA to BGRA.
339 for pixel in image.data.chunks_exact_mut(4) {
340 pixel.swap(0, 2);
341 }
342 }
343
344 Ok((bitmap_size, image.data))
345 }
346 }
347
348 fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
349 if let Some(ix) = self
350 .loaded_fonts_store
351 .iter()
352 .position(|font| font.id() == id)
353 {
354 FontId(ix)
355 } else {
356 // This matches the behavior of the mac text system
357 let font = self.font_system.get_font(id).unwrap();
358 let face = self
359 .font_system
360 .db()
361 .faces()
362 .find(|info| info.id == id)
363 .unwrap();
364
365 let font_id = FontId(self.loaded_fonts_store.len());
366 self.loaded_fonts_store.push(font);
367 self.postscript_names
368 .insert(font_id, face.post_script_name.clone());
369
370 font_id
371 }
372 }
373
374 #[profiling::function]
375 fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
376 let mut attrs_list = AttrsList::new(Attrs::new());
377 let mut offs = 0;
378 for run in font_runs {
379 let font = &self.loaded_fonts_store[run.font_id.0];
380 let font = self.font_system.db().face(font.id()).unwrap();
381 attrs_list.add_span(
382 offs..(offs + run.len),
383 Attrs::new()
384 .family(Family::Name(&font.families.first().unwrap().0))
385 .stretch(font.stretch)
386 .style(font.style)
387 .weight(font.weight),
388 );
389 offs += run.len;
390 }
391 let mut line = ShapeLine::new_in_buffer(
392 &mut self.scratch,
393 &mut self.font_system,
394 text,
395 &attrs_list,
396 cosmic_text::Shaping::Advanced,
397 4,
398 );
399
400 let mut layout = Vec::with_capacity(1);
401 line.layout_to_buffer(
402 &mut self.scratch,
403 font_size.0,
404 None, // We do our own wrapping
405 cosmic_text::Wrap::None,
406 None,
407 &mut layout,
408 None,
409 );
410
411 let mut runs = Vec::new();
412 let layout = layout.first().unwrap();
413 for glyph in &layout.glyphs {
414 let font_id = glyph.font_id;
415 let font_id = self.font_id_for_cosmic_id(font_id);
416 let is_emoji = self.is_emoji(font_id);
417 let mut glyphs = SmallVec::new();
418
419 // HACK: Prevent crash caused by variation selectors.
420 if glyph.glyph_id == 3 && is_emoji {
421 continue;
422 }
423
424 // 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
425 glyphs.push(ShapedGlyph {
426 id: GlyphId(glyph.glyph_id as u32),
427 position: point(glyph.x.into(), glyph.y.into()),
428 index: glyph.start,
429 is_emoji,
430 });
431
432 runs.push(crate::ShapedRun { font_id, glyphs });
433 }
434
435 LineLayout {
436 font_size,
437 width: layout.w.into(),
438 ascent: layout.max_ascent.into(),
439 descent: layout.max_descent.into(),
440 runs,
441 len: text.len(),
442 }
443 }
444}
445
446impl From<RectF> for Bounds<f32> {
447 fn from(rect: RectF) -> Self {
448 Bounds {
449 origin: point(rect.origin_x(), rect.origin_y()),
450 size: size(rect.width(), rect.height()),
451 }
452 }
453}
454
455impl From<RectI> for Bounds<DevicePixels> {
456 fn from(rect: RectI) -> Self {
457 Bounds {
458 origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
459 size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
460 }
461 }
462}
463
464impl From<Vector2I> for Size<DevicePixels> {
465 fn from(value: Vector2I) -> Self {
466 size(value.x().into(), value.y().into())
467 }
468}
469
470impl From<RectI> for Bounds<i32> {
471 fn from(rect: RectI) -> Self {
472 Bounds {
473 origin: point(rect.origin_x(), rect.origin_y()),
474 size: size(rect.width(), rect.height()),
475 }
476 }
477}
478
479impl From<Point<u32>> for Vector2I {
480 fn from(size: Point<u32>) -> Self {
481 Vector2I::new(size.x as i32, size.y as i32)
482 }
483}
484
485impl From<Vector2F> for Size<f32> {
486 fn from(vec: Vector2F) -> Self {
487 size(vec.x(), vec.y())
488 }
489}
490
491impl From<FontWeight> for cosmic_text::Weight {
492 fn from(value: FontWeight) -> Self {
493 cosmic_text::Weight(value.0 as u16)
494 }
495}
496
497impl From<FontStyle> for cosmic_text::Style {
498 fn from(style: FontStyle) -> Self {
499 match style {
500 FontStyle::Normal => cosmic_text::Style::Normal,
501 FontStyle::Italic => cosmic_text::Style::Italic,
502 FontStyle::Oblique => cosmic_text::Style::Oblique,
503 }
504 }
505}
506
507fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
508 font_kit::properties::Properties {
509 style: match font.style {
510 crate::FontStyle::Normal => font_kit::properties::Style::Normal,
511 crate::FontStyle::Italic => font_kit::properties::Style::Italic,
512 crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
513 },
514 weight: font_kit::properties::Weight(font.weight.0),
515 stretch: Default::default(),
516 }
517}
518
519fn face_info_into_properties(
520 face_info: &cosmic_text::fontdb::FaceInfo,
521) -> font_kit::properties::Properties {
522 font_kit::properties::Properties {
523 style: match face_info.style {
524 cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
525 cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
526 cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
527 },
528 // both libs use the same values for weight
529 weight: font_kit::properties::Weight(face_info.weight.0.into()),
530 stretch: match face_info.stretch {
531 cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
532 cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
533 cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
534 cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
535 cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
536 cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
537 cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
538 cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
539 cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
540 },
541 }
542}