Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/mac/fonts.rs
1use super::text_layout::{layout_line, layout_text};
2use crate::fonts::font_kit::{properties_to_font_kit, Rasterizer};
3use anyhow::{anyhow, bail, Result};
4use core_foundation::array::{CFArray, CFArrayRef};
5use core_foundation::base::{CFType, ItemRef, TCFType};
6use core_foundation::dictionary::CFDictionary;
7use core_foundation::string::{CFString, CFStringRef, UniChar};
8use core_graphics::display::CGSize;
9use core_graphics::font::CGGlyph;
10use core_text::font::{cascade_list_for_languages as ct_cascade_list_for_languages, CTFont};
11use core_text::font_descriptor::{
12 kCTFontFamilyNameAttribute, kCTFontLanguagesAttribute, kCTFontNameAttribute,
13 kCTFontOrientationHorizontal, CTFontDescriptor, CTFontDescriptorCopyAttribute,
14 SymbolicTraitAccessors, TraitAccessors,
15};
16use core_text::{font, font_collection, font_descriptor};
17use dashmap::{mapref::entry::Entry, DashMap};
18use font_kit::font::Font;
19use font_kit::loaders::core_text::NativeFont;
20use futures::future::BoxFuture;
21use futures::FutureExt as _;
22use itertools::Itertools as _;
23use ordered_float::OrderedFloat;
24use pathfinder_geometry::rect::RectI;
25use pathfinder_geometry::vector::{Vector2F, Vector2I};
26use std::any::Any;
27use std::collections::HashMap;
28use std::ops::Range;
29use std::sync::{
30 atomic::{AtomicUsize, Ordering},
31 Arc,
32};
33use strato_ui_core::fonts::{
34 canvas::RasterFormat, FamilyId, FontId, FontInfo, GlyphId, Metrics, Properties,
35 RasterizedGlyph, SubpixelAlignment,
36};
37use strato_ui_core::platform::{self, FontDB as _, LineStyle, TextLayoutSystem};
38use strato_ui_core::rendering;
39use strato_ui_core::text_layout::{ClipConfig, StyleAndFont, TextAlignment, TextFrame};
40 
41struct FontFamily {
42 name: String,
43 fonts: Vec<Font>,
44}
45 
46mod loader {
47 use super::*;
48 
49 // Font-kit loads fonts by copying their font file into the running memory, which
50 // is extremely inefficient. Thus, for system fonts, we load these fonts by reference
51 // through CTFontDescriptorCreateWithAttributes function and create a dummy font-kit
52 // Font interface to access its functions.
53 pub fn load_system_font(font_family: &str) -> Result<FontFamily> {
54 let Some(descriptors) = FontDB::descriptors_for_family(font_family) else {
55 bail!(
56 "could not find a non-empty font family matching one of the given names {:?}",
57 font_family
58 );
59 };
60 let mut fonts = Vec::with_capacity(descriptors.len() as usize);
61 for fontdesc in descriptors.iter() {
62 // The font size here does not affect our rendering. In CTFont, pt_size
63 // is used to calculate font metrics like ascent, descent, etc. However,
64 // font-kit creates its own layer of calculating font metrics at render time
65 // so we just need a place-holder here for getting the CTFont object. Use
66 // 16.0 here as it is consistent with https://docs.rs/core-text/19.2.0/src/core_text/font.rs.html#130
67 let font = unsafe {
68 Font::from_native_font(font::new_from_descriptor(&fontdesc, DEFAULT_FONT_SIZE))
69 };
70 
71 let glyph_id = font.glyph_for_char('m');
72 if glyph_id.is_none() {
73 return Err(anyhow!("font must contain a glyph for the 'm' character"));
74 }
75 
76 fonts.push(font);
77 }
78 Ok(FontFamily {
79 fonts,
80 name: font_family.into(),
81 })
82 }
83 
84 pub fn load_all_system_fonts() -> LoadedSystemFonts {
85 let collection = font_collection::create_for_all_families();
86 let Some(descriptors) = collection.get_descriptors() else {
87 return LoadedSystemFonts(vec![]);
88 };
89 
90 let mut fonts: Vec<(FontInfo, FontFamily)> = Vec::with_capacity(descriptors.len() as usize);
91 for descriptor in descriptors.iter() {
92 let name = match unsafe { FontDB::get_family_name(&descriptor) } {
93 Some(family) => family,
94 None => {
95 log::warn!("Failed to load the font as it does not have a valid family name.");
96 continue;
97 }
98 };
99 
100 let internal_name = match unsafe { FontDB::get_font_name(&descriptor) } {
101 Some(family) => family,
102 None => {
103 log::warn!("Failed to load the font as it does not have a valid font name.");
104 continue;
105 }
106 };
107 
108 // We should only load languages that support english
109 if !FontDB::supports_english(&descriptor) {
110 continue;
111 }
112 
113 if let Some(idx) = fonts.iter().position(|font| font.0.family_name == name) {
114 // Some font families (e.g. Osaka) could contains both monospace
115 // and variable-width fonts. To make sure the returned font family
116 // info is consistent everytime, we set is_monospace to true if the
117 // family contains one font that is monospace.
118 if descriptor.traits().symbolic_traits().is_monospace()
119 && !fonts[idx].0.is_monospace
120 {
121 // Updating here since font family names are guaranteed to be unique.
122 fonts[idx].0.is_monospace = true;
123 }
124 // Since we keep track of font families, add this font as a possible style.
125 fonts[idx].0.font_names.push(internal_name)
126 } else {
127 let font_family = match load_system_font(&name) {
128 Ok(font) => font,
129 Err(err) => {
130 log::debug!("Failed to load {}: {:?}", name.as_str(), err);
131 continue;
132 }
133 };
134 
135 fonts.push((
136 FontInfo {
137 family_name: name,
138 font_names: vec![internal_name],
139 is_monospace: descriptor.traits().symbolic_traits().is_monospace(),
140 },
141 font_family,
142 ));
143 }
144 }
145 
146 LoadedSystemFonts(fonts)
147 }
148 
149 // We use font-kit's family handle to load fonts that come with Warp as
150 // these binaries are already in memory and won't increase our memory load.
151 pub fn load_font_family_from_bytes(name: &str, font_bytes: Vec<Vec<u8>>) -> Result<FontFamily> {
152 let mut fonts = Vec::with_capacity(font_bytes.len());
153 
154 for font in font_bytes {
155 let Ok(font) = Font::from_bytes(Arc::new(font), 0) else {
156 log::info!("Unable to parse font bytes for font {name:?}");
157 continue;
158 };
159 
160 let glyph_id = font.glyph_for_char('m');
161 if glyph_id.is_none() {
162 return Err(anyhow!("font must contain a glyph for the 'm' character"));
163 }
164 fonts.push(font);
165 }
166 
167 Ok(FontFamily {
168 fonts,
169 name: name.to_owned(),
170 })
171 }
172}
173 
174struct LoadedSystemFonts(Vec<(FontInfo, FontFamily)>);
175 
176impl platform::LoadedSystemFonts for LoadedSystemFonts {
177 fn as_any(self: Box<Self>) -> Box<dyn Any> {
178 self as Box<dyn Any>
179 }
180}
181 
182pub struct FontDB {
183 next_family_id: AtomicUsize,
184 families: HashMap<FamilyId, Family>,
185 next_font_id: AtomicUsize,
186 rasterizer: Rasterizer,
187 font_names: DashMap<FontId, Arc<String>>,
188 native_fonts: DashMap<(FontId, OrderedFloat<f32>), NativeFont>,
189 fonts_by_name: DashMap<Arc<String>, FontId>,
190 fallback_fonts: DashMap<FontId, Arc<Vec<FontId>>>,
191 metrics: DashMap<FontId, Metrics>,
192 font_selections: DashMap<(FamilyId, Properties), FontId>,
193 space_advances: DashMap<FontId, Option<f64>>,
194}
195 
196struct Family {
197 name: String,
198 font_ids: Vec<FontId>,
199}
200 
201impl Default for FontDB {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206 
207const DEFAULT_FONT_SIZE: f64 = 16.0;
208 
209// Returns the horizontal advance (in points) of a single space in the given font.
210fn space_advance_width(font: &CTFont) -> Option<f64> {
211 let space_char: UniChar = ' ' as u16;
212 let mut glyph: CGGlyph = 0;
213 
214 let ok =
215 unsafe { font.get_glyphs_for_characters(&space_char as *const UniChar, &mut glyph, 1) };
216 if !ok || glyph == 0 {
217 return None;
218 }
219 
220 let mut advance = CGSize {
221 width: 0.0,
222 height: 0.0,
223 };
224 unsafe {
225 font.get_advances_for_glyphs(
226 kCTFontOrientationHorizontal,
227 &glyph as *const CGGlyph,
228 &mut advance as *mut CGSize,
229 1,
230 );
231 }
232 
233 let width = advance.width;
234 (width.is_finite() && width > 0.0).then_some(width)
235}
236 
237impl FontDB {
238 pub fn new() -> Self {
239 Self {
240 next_family_id: Default::default(),
241 families: Default::default(),
242 next_font_id: Default::default(),
243 rasterizer: Rasterizer::new(),
244 font_names: Default::default(),
245 native_fonts: Default::default(),
246 fonts_by_name: Default::default(),
247 fallback_fonts: Default::default(),
248 metrics: Default::default(),
249 font_selections: Default::default(),
250 space_advances: Default::default(),
251 }
252 }
253 
254 // This functions the same as the family_name method in core text font descriptor, but it returns
255 // None instead of panicking when the descriptor does not include the family_name attribute.
256 unsafe fn get_family_name(descriptor: &ItemRef<CTFontDescriptor>) -> Option<String> {
257 let value = CTFontDescriptorCopyAttribute(
258 descriptor.as_concrete_TypeRef(),
259 kCTFontFamilyNameAttribute,
260 );
261 if value.is_null() {
262 return None;
263 }
264 
265 let value = CFType::wrap_under_create_rule(value);
266 let s = CFString::wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
267 Some(s.to_string())
268 }
269 
270 unsafe fn get_font_name(descriptor: &ItemRef<CTFontDescriptor>) -> Option<String> {
271 let value =
272 CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), kCTFontNameAttribute);
273 if value.is_null() {
274 return None;
275 }
276 
277 let value = CFType::wrap_under_create_rule(value);
278 let s = CFString::wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
279 Some(s.to_string())
280 }
281 
282 pub fn fallback_fonts(&self, font_id: FontId) -> Vec<FontId> {
283 self.fallback_fonts
284 .get(&font_id)
285 .expect("Font fallback should not be empty")
286 .to_vec()
287 }
288 
289 // Check if the font family supports english.
290 fn supports_english(descriptor: &ItemRef<CTFontDescriptor>) -> bool {
291 unsafe {
292 let value = CTFontDescriptorCopyAttribute(
293 descriptor.as_concrete_TypeRef(),
294 kCTFontLanguagesAttribute,
295 ) as CFArrayRef;
296 
297 if value.is_null() {
298 return false;
299 }
300 
301 let languages: CFArray<CFString> = CFArray::wrap_under_create_rule(value);
302 languages.iter().any(|s| *s == "en")
303 }
304 }
305 
306 pub fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
307 self.families.get(&id).map(|s| s.name.clone())
308 }
309 
310 fn create_new_family_id(&self) -> FamilyId {
311 FamilyId(self.next_family_id.fetch_add(1, Ordering::SeqCst))
312 }
313 
314 /// Return fallback descriptors for font/language list.
315 /// Heavily inspired by crossfont's implementation:
316 /// https://github.com/alacritty/crossfont/blob/d3515de22494c6fa70d84d2a9264c10097e303bd/src/darwin/mod.rs#L288
317 fn cascade_list_for_languages(&self, ct_font: &CTFont, languages: &[&str]) -> Vec<FontId> {
318 // Convert language type &Vec<String> -> CFArray.
319 let langarr: CFArray<CFString> = {
320 let tmp: Vec<CFString> = languages
321 .iter()
322 .map(|language| CFString::new(language))
323 .collect();
324 CFArray::from_CFTypes(&tmp)
325 };
326 
327 // CFArray of CTFontDescriptorRef (again).
328 let list = ct_cascade_list_for_languages(ct_font, &langarr);
329 
330 let mut fallback_fonts: Vec<FontId> = list
331 .into_iter()
332 .filter_map(|fontdesc| self.descriptor_to_font_id(fontdesc))
333 .collect();
334 
335 // While .Apple Symbols Fallback is not a valid font. Apple Symbols is and it provides
336 // many fallback characters. This implementation is consistent with Alacritty:
337 // See: https://github.com/alacritty/crossfont/blob/d3515de22494c6fa70d84d2a9264c10097e303bd/src/darwin/mod.rs#L91
338 if let Some(font) = FontDB::descriptors_for_family("Apple Symbols")
339 .as_ref()
340 .and_then(|descriptor| descriptor.into_iter().next())
341 .and_then(|font_descriptor| self.descriptor_to_font_id(font_descriptor))
342 {
343 fallback_fonts.push(font);
344 }
345 
346 fallback_fonts
347 }
348 
349 // Get a list of CTFontDescriptors for a font family.
350 fn descriptors_for_family(name: &str) -> Option<CFArray<CTFontDescriptor>> {
351 let attributes: CFDictionary<CFString, CFType> = CFDictionary::from_CFType_pairs(&[(
352 CFString::new("NSFontFamilyAttribute"),
353 CFString::new(name).as_CFType(),
354 )]);
355 
356 let descriptor = font_descriptor::new_from_attributes(&attributes);
357 let collection_descriptors = &CFArray::from_CFTypes(&[descriptor]);
358 let collection = font_collection::new_from_descriptors(collection_descriptors);
359 
360 collection.get_descriptors()
361 }
362 
363 // Convert a CTFontDescriptor to font_id. This function does not load fallback fonts
364 // and assumes the descriptor refers to a valid system font.
365 fn descriptor_to_font_id(&self, fontdesc: ItemRef<CTFontDescriptor>) -> Option<FontId> {
366 let name = match unsafe { FontDB::get_family_name(&fontdesc) } {
367 Some(family) => family,
368 None => {
369 log::warn!("Failed to load the font as it does not have a valid family name.");
370 return None;
371 }
372 };
373 
374 let font_name = match unsafe { FontDB::get_font_name(&fontdesc) } {
375 Some(name) => name,
376 None => {
377 log::warn!("Failed to load the font as it does not have a valid name.");
378 return None;
379 }
380 };
381 
382 // We should not load fonts with name that starts with dot
383 // https://developer.apple.com/videos/play/wwdc2019/227/?time=200
384 (!name.starts_with('.')).then(|| {
385 // Check if the fallback font is in cache.
386 match self.fonts_by_name.entry(Arc::new(font_name)) {
387 Entry::Occupied(entry) => return *entry.get(),
388 Entry::Vacant(_) => (),
389 }
390 
391 // We need to push font after releasing the entry of the dashmap to prevent deadlocks.
392 self.push_font(unsafe {
393 Font::from_native_font(font::new_from_descriptor(&fontdesc, DEFAULT_FONT_SIZE))
394 })
395 })
396 }
397 
398 pub fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
399 match self.font_selections.entry((family_id, properties)) {
400 Entry::Occupied(entry) => *entry.get(),
401 Entry::Vacant(entry) => {
402 let family = &self
403 .families
404 .get(&family_id)
405 .expect("FamilyId must correspond to a valid family");
406 let candidates = family
407 .font_ids
408 .iter()
409 .map(|font_id| self.font(*font_id).properties())
410 .collect::<Vec<_>>();
411 
412 let font_id = {
413 if let Some(idx) =
414 best_font_match(&candidates, &properties_to_font_kit(properties))
415 {
416 self.font(family.font_ids[idx]).properties();
417 family.font_ids[idx]
418 } else {
419 best_font_match(&candidates, &Default::default())
420 .map(|idx| family.font_ids[idx])
421 .unwrap_or(family.font_ids[0])
422 }
423 };
424 
425 // Make sure we've loaded fallback fonts for the selected font.
426 if !self.fallback_fonts.contains_key(&font_id) {
427 self.fallback_fonts.insert(
428 font_id,
429 Arc::new(self.cascade_list_for_languages(
430 &self.font(font_id).native_font(),
431 &["en"],
432 )),
433 );
434 }
435 *entry.insert(font_id)
436 }
437 }
438 }
439 
440 pub fn font(&self, font_id: FontId) -> Arc<Font> {
441 self.rasterizer.font_for_id(font_id)
442 }
443 
444 pub fn native_font(&self, font_id: FontId, size: f32) -> NativeFont {
445 match self.native_fonts.entry((font_id, OrderedFloat(size))) {
446 Entry::Occupied(entry) => entry.get().clone(),
447 Entry::Vacant(entry) => entry
448 .insert(
449 self.rasterizer
450 .font_for_id(font_id)
451 .native_font()
452 .clone_with_font_size(size as f64),
453 )
454 .clone(),
455 }
456 }
457 
458 /// Returns the horizontal advance of a space character at the given size, or `None` if the
459 /// advance could not be measured. Uses a cached reference advance (measured at
460 /// `DEFAULT_FONT_SIZE`) scaled linearly to `size`.
461 pub fn space_advance_width(&self, font_id: FontId, size: f32) -> Option<f64> {
462 let stored = *self.space_advances.get(&font_id)?;
463 stored.map(|a| a * size as f64 / DEFAULT_FONT_SIZE)
464 }
465 
466 pub fn font_id_for_native_font(&self, native_font: NativeFont) -> FontId {
467 let postscript_name = native_font.postscript_name();
468 if let Some(font_id) = self.fonts_by_name.get(&postscript_name).as_ref() {
469 return *font_id.value();
470 }
471 
472 self.push_font(unsafe { Font::from_native_font(native_font) })
473 }
474 
475 fn push_font(&self, font: Font) -> FontId {
476 let name = Arc::new(font.postscript_name().unwrap());
477 let font_id = FontId(self.next_font_id.fetch_add(1, Ordering::SeqCst));
478 
479 let ct_font = font.native_font().clone_with_font_size(DEFAULT_FONT_SIZE);
480 let advance = space_advance_width(&ct_font);
481 
482 self.rasterizer.insert(font_id, Arc::new(font));
483 self.font_names.insert(font_id, name.clone());
484 self.fonts_by_name.insert(name, font_id);
485 self.space_advances.insert(font_id, advance);
486 font_id
487 }
488 
489 fn insert_font_family(&mut self, font_family: FontFamily) -> Result<FamilyId> {
490 if let Some(family_id) = self.family_id_for_name(&font_family.name) {
491 return Ok(family_id);
492 }
493 
494 let font_ids = font_family
495 .fonts
496 .into_iter()
497 .map(|font| self.push_font(font));
498 
499 let family_id = self.create_new_family_id();
500 self.families.insert(
501 family_id,
502 Family {
503 name: font_family.name,
504 font_ids: font_ids.collect(),
505 },
506 );
507 
508 Ok(family_id)
509 }
510}
511 
512fn best_font_match(
513 candidates: &[font_kit::properties::Properties],
514 requested: &font_kit::properties::Properties,
515) -> Option<usize> {
516 candidates
517 .iter()
518 .position(|candidate| candidate == requested)
519 .or_else(|| candidates.first().map(|_| 0))
520}
521 
522impl crate::platform::FontDB for FontDB {
523 fn load_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
524 let family = loader::load_font_family_from_bytes(name, bytes)?;
525 self.insert_font_family(family)
526 }
527 
528 fn load_from_system(&mut self, font_family: &str) -> Result<FamilyId> {
529 let family = loader::load_system_font(font_family)?;
530 self.insert_font_family(family)
531 }
532 
533 fn load_all_system_fonts(&self) -> BoxFuture<'static, Box<dyn platform::LoadedSystemFonts>> {
534 async { Box::new(loader::load_all_system_fonts()) as Box<dyn platform::LoadedSystemFonts> }
535 .boxed()
536 }
537 
538 fn process_loaded_system_fonts(
539 &mut self,
540 loaded_system_fonts: Box<dyn platform::LoadedSystemFonts>,
541 ) -> Vec<(Option<FamilyId>, crate::fonts::FontInfo)> {
542 let loaded_system_fonts: Box<LoadedSystemFonts> = loaded_system_fonts
543 .as_any()
544 .downcast()
545 .expect("should not fail to downcast to concrete type");
546 
547 loaded_system_fonts
548 .0
549 .into_iter()
550 .flat_map(|(font_info, family)| {
551 let family_id = self.insert_font_family(family).ok()?;
552 Some((Some(family_id), font_info))
553 })
554 .collect_vec()
555 }
556 
557 fn fallback_fonts(&self, _ch: char, font_id: FontId) -> Vec<FontId> {
558 self.fallback_fonts(font_id)
559 }
560 
561 fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
562 self.load_family_name_from_id(id)
563 }
564 
565 fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
566 self.select_font(family_id, properties)
567 }
568 
569 fn font_metrics(&self, font_id: FontId) -> Metrics {
570 match self.metrics.entry(font_id) {
571 Entry::Occupied(entry) => *entry.get(),
572 Entry::Vacant(entry) => *entry.insert(self.font(font_id).metrics().into()),
573 }
574 }
575 
576 fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Vector2I> {
577 Ok(self.font(font_id).advance(glyph_id)?.to_i32())
578 }
579 
580 fn glyph_raster_bounds(
581 &self,
582 font_id: FontId,
583 point_size: f32,
584 glyph_id: GlyphId,
585 scale: Vector2F,
586 glyph_config: &rendering::GlyphConfig,
587 ) -> Result<RectI> {
588 self.rasterizer
589 .glyph_raster_bounds(font_id, point_size, glyph_id, scale, glyph_config)
590 }
591 
592 fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI> {
593 Ok(self.font(font_id).typographic_bounds(glyph_id)?.to_i32())
594 }
595 
596 fn rasterize_glyph(
597 &self,
598 font_id: FontId,
599 point_size: f32,
600 glyph_id: GlyphId,
601 scale: Vector2F,
602 subpixel_alignment: SubpixelAlignment,
603 glyph_config: &rendering::GlyphConfig,
604 format: RasterFormat,
605 ) -> Result<RasterizedGlyph> {
606 self.rasterizer.rasterize_glyph(
607 font_id,
608 point_size,
609 glyph_id,
610 scale,
611 subpixel_alignment,
612 glyph_config,
613 format,
614 )
615 }
616 
617 fn glyph_for_char(&self, font: FontId, char: char) -> Option<GlyphId> {
618 self.font(font).glyph_for_char(char)
619 }
620 
621 fn family_id_for_name(&self, name: &str) -> Option<FamilyId> {
622 self.families
623 .iter()
624 .find(|(_, f)| f.name == name)
625 .map(|(id, _)| *id)
626 }
627 
628 fn text_layout_system(&self) -> &dyn TextLayoutSystem {
629 self
630 }
631}
632 
633impl crate::platform::TextLayoutSystem for FontDB {
634 fn layout_line(
635 &self,
636 text: &str,
637 line_style: LineStyle,
638 style_runs: &[(Range<usize>, StyleAndFont)],
639 _max_width: f32,
640 clip_config: ClipConfig,
641 ) -> crate::text_layout::Line {
642 layout_line(text, line_style, style_runs, self, clip_config)
643 }
644 
645 fn layout_text(
646 &self,
647 text: &str,
648 line_style: LineStyle,
649 style_runs: &[(Range<usize>, StyleAndFont)],
650 max_width: f32,
651 max_height: f32,
652 alignment: TextAlignment,
653 first_line_head_indent: Option<f32>,
654 ) -> TextFrame {
655 layout_text(
656 text,
657 line_style,
658 style_runs,
659 self,
660 max_width,
661 max_height,
662 alignment,
663 first_line_head_indent,
664 )
665 }
666}
667