Files
galaxy/crates/galaxyui/src/platform/mac/fonts.rs
T

657 lines
23 KiB
Rust

use super::text_layout::{layout_line, layout_text};
use crate::fonts::font_kit::{properties_to_font_kit, Rasterizer};
use anyhow::{anyhow, bail, Result};
use core_foundation::array::{CFArray, CFArrayRef};
use core_foundation::base::{CFType, ItemRef, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::string::{CFString, CFStringRef, UniChar};
use core_graphics::display::CGSize;
use core_graphics::font::CGGlyph;
use core_text::font::{cascade_list_for_languages as ct_cascade_list_for_languages, CTFont};
use core_text::font_descriptor::{
kCTFontFamilyNameAttribute, kCTFontLanguagesAttribute, kCTFontNameAttribute,
kCTFontOrientationHorizontal, CTFontDescriptor, CTFontDescriptorCopyAttribute,
SymbolicTraitAccessors, TraitAccessors,
};
use core_text::{font, font_collection, font_descriptor};
use dashmap::{mapref::entry::Entry, DashMap};
use font_kit::font::Font;
use font_kit::loaders::core_text::NativeFont;
use futures::future::BoxFuture;
use futures::FutureExt as _;
use itertools::Itertools as _;
use ordered_float::OrderedFloat;
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::vector::{Vector2F, Vector2I};
use std::any::Any;
use std::collections::HashMap;
use std::ops::Range;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use galaxyui_core::fonts::{
canvas::RasterFormat, FamilyId, FontId, FontInfo, GlyphId, Metrics, Properties,
RasterizedGlyph, SubpixelAlignment,
};
use galaxyui_core::platform::{self, FontDB as _, LineStyle, TextLayoutSystem};
use galaxyui_core::rendering;
use galaxyui_core::text_layout::{ClipConfig, StyleAndFont, TextAlignment, TextFrame};
struct FontFamily {
name: String,
fonts: Vec<Font>,
}
mod loader {
use super::*;
// Font-kit loads fonts by copying their font file into the running memory, which
// is extremely inefficient. Thus, for system fonts, we load these fonts by reference
// through CTFontDescriptorCreateWithAttributes function and create a dummy font-kit
// Font interface to access its functions.
pub fn load_system_font(font_family: &str) -> Result<FontFamily> {
let Some(descriptors) = FontDB::descriptors_for_family(font_family) else {
bail!(
"could not find a non-empty font family matching one of the given names {:?}",
font_family
);
};
let mut fonts = Vec::with_capacity(descriptors.len() as usize);
for fontdesc in descriptors.into_iter() {
// The font size here does not affect our rendering. In CTFont, pt_size
// is used to calculate font metrics like ascent, descent, etc. However,
// font-kit creates its own layer of calculating font metrics at render time
// so we just need a place-holder here for getting the CTFont object. Use
// 16.0 here as it is consistent with https://docs.rs/core-text/19.2.0/src/core_text/font.rs.html#130
let font = Font::from_ct_font(font::new_from_descriptor(&fontdesc, DEFAULT_FONT_SIZE));
let glyph_id = font.glyph_for_char('m');
if glyph_id.is_none() {
return Err(anyhow!("font must contain a glyph for the 'm' character"));
}
fonts.push(font);
}
Ok(FontFamily {
fonts,
name: font_family.into(),
})
}
pub fn load_all_system_fonts() -> LoadedSystemFonts {
let collection = font_collection::create_for_all_families();
let Some(descriptors) = collection.get_descriptors() else {
return LoadedSystemFonts(vec![]);
};
let mut fonts: Vec<(FontInfo, FontFamily)> = Vec::with_capacity(descriptors.len() as usize);
for descriptor in descriptors.iter() {
let name = match unsafe { FontDB::get_family_name(&descriptor) } {
Some(family) => family,
None => {
log::warn!("Failed to load the font as it does not have a valid family name.");
continue;
}
};
let internal_name = match unsafe { FontDB::get_font_name(&descriptor) } {
Some(family) => family,
None => {
log::warn!("Failed to load the font as it does not have a valid font name.");
continue;
}
};
// We should only load languages that support english
if !FontDB::supports_english(&descriptor) {
continue;
}
if let Some(idx) = fonts.iter().position(|font| font.0.family_name == name) {
// Some font families (e.g. Osaka) could contains both monospace
// and variable-width fonts. To make sure the returned font family
// info is consistent everytime, we set is_monospace to true if the
// family contains one font that is monospace.
if descriptor.traits().symbolic_traits().is_monospace()
&& !fonts[idx].0.is_monospace
{
// Updating here since font family names are guaranteed to be unique.
fonts[idx].0.is_monospace = true;
}
// Since we keep track of font families, add this font as a possible style.
fonts[idx].0.font_names.push(internal_name)
} else {
let font_family = match load_system_font(&name) {
Ok(font) => font,
Err(err) => {
log::debug!("Failed to load {}: {:?}", name.as_str(), err);
continue;
}
};
fonts.push((
FontInfo {
family_name: name,
font_names: vec![internal_name],
is_monospace: descriptor.traits().symbolic_traits().is_monospace(),
},
font_family,
));
}
}
LoadedSystemFonts(fonts)
}
// We use font-kit's family handle to load fonts that come with Warp as
// these binaries are already in memory and won't increase our memory load.
pub fn load_font_family_from_bytes(name: &str, font_bytes: Vec<Vec<u8>>) -> Result<FontFamily> {
let mut fonts = Vec::with_capacity(font_bytes.len());
for font in font_bytes {
let Ok(font) = Font::from_bytes(Arc::new(font), 0) else {
log::info!("Unable to parse font bytes for font {name:?}");
continue;
};
let glyph_id = font.glyph_for_char('m');
if glyph_id.is_none() {
return Err(anyhow!("font must contain a glyph for the 'm' character"));
}
fonts.push(font);
}
Ok(FontFamily {
fonts,
name: name.to_owned(),
})
}
}
struct LoadedSystemFonts(Vec<(FontInfo, FontFamily)>);
impl platform::LoadedSystemFonts for LoadedSystemFonts {
fn as_any(self: Box<Self>) -> Box<dyn Any> {
self as Box<dyn Any>
}
}
pub struct FontDB {
next_family_id: AtomicUsize,
families: HashMap<FamilyId, Family>,
next_font_id: AtomicUsize,
rasterizer: Rasterizer,
font_names: DashMap<FontId, Arc<String>>,
native_fonts: DashMap<(FontId, OrderedFloat<f32>), NativeFont>,
fonts_by_name: DashMap<Arc<String>, FontId>,
fallback_fonts: DashMap<FontId, Arc<Vec<FontId>>>,
metrics: DashMap<FontId, Metrics>,
font_selections: DashMap<(FamilyId, Properties), FontId>,
space_advances: DashMap<FontId, Option<f64>>,
}
struct Family {
name: String,
font_ids: Vec<FontId>,
}
impl Default for FontDB {
fn default() -> Self {
Self::new()
}
}
const DEFAULT_FONT_SIZE: f64 = 16.0;
// Returns the horizontal advance (in points) of a single space in the given font.
fn space_advance_width(font: &CTFont) -> Option<f64> {
let space_char: UniChar = ' ' as u16;
let mut glyph: CGGlyph = 0;
let ok =
unsafe { font.get_glyphs_for_characters(&space_char as *const UniChar, &mut glyph, 1) };
if !ok || glyph == 0 {
return None;
}
let mut advance = CGSize {
width: 0.0,
height: 0.0,
};
unsafe {
font.get_advances_for_glyphs(
kCTFontOrientationHorizontal,
&glyph as *const CGGlyph,
&mut advance as *mut CGSize,
1,
);
}
let width = advance.width;
(width.is_finite() && width > 0.0).then_some(width)
}
impl FontDB {
pub fn new() -> Self {
Self {
next_family_id: Default::default(),
families: Default::default(),
next_font_id: Default::default(),
rasterizer: Rasterizer::new(),
font_names: Default::default(),
native_fonts: Default::default(),
fonts_by_name: Default::default(),
fallback_fonts: Default::default(),
metrics: Default::default(),
font_selections: Default::default(),
space_advances: Default::default(),
}
}
// This functions the same as the family_name method in core text font descriptor, but it returns
// None instead of panicking when the descriptor does not include the family_name attribute.
unsafe fn get_family_name(descriptor: &ItemRef<CTFontDescriptor>) -> Option<String> {
let value = CTFontDescriptorCopyAttribute(
descriptor.as_concrete_TypeRef(),
kCTFontFamilyNameAttribute,
);
if value.is_null() {
return None;
}
let value = CFType::wrap_under_create_rule(value);
let s = CFString::wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
Some(s.to_string())
}
unsafe fn get_font_name(descriptor: &ItemRef<CTFontDescriptor>) -> Option<String> {
let value =
CTFontDescriptorCopyAttribute(descriptor.as_concrete_TypeRef(), kCTFontNameAttribute);
if value.is_null() {
return None;
}
let value = CFType::wrap_under_create_rule(value);
let s = CFString::wrap_under_get_rule(value.as_CFTypeRef() as CFStringRef);
Some(s.to_string())
}
pub fn fallback_fonts(&self, font_id: FontId) -> Vec<FontId> {
self.fallback_fonts
.get(&font_id)
.expect("Font fallback should not be empty")
.to_vec()
}
// Check if the font family supports english.
fn supports_english(descriptor: &ItemRef<CTFontDescriptor>) -> bool {
unsafe {
let value = CTFontDescriptorCopyAttribute(
descriptor.as_concrete_TypeRef(),
kCTFontLanguagesAttribute,
) as CFArrayRef;
if value.is_null() {
return false;
}
let languages: CFArray<CFString> = CFArray::wrap_under_create_rule(value);
languages.iter().any(|s| *s == "en")
}
}
pub fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
self.families.get(&id).map(|s| s.name.clone())
}
fn create_new_family_id(&self) -> FamilyId {
FamilyId(self.next_family_id.fetch_add(1, Ordering::SeqCst))
}
/// Return fallback descriptors for font/language list.
/// Heavily inspired by crossfont's implementation:
/// https://github.com/alacritty/crossfont/blob/d3515de22494c6fa70d84d2a9264c10097e303bd/src/darwin/mod.rs#L288
fn cascade_list_for_languages(&self, ct_font: &CTFont, languages: &[&str]) -> Vec<FontId> {
// Convert language type &Vec<String> -> CFArray.
let langarr: CFArray<CFString> = {
let tmp: Vec<CFString> = languages
.iter()
.map(|language| CFString::new(language))
.collect();
CFArray::from_CFTypes(&tmp)
};
// CFArray of CTFontDescriptorRef (again).
let list = ct_cascade_list_for_languages(ct_font, &langarr);
let mut fallback_fonts: Vec<FontId> = list
.into_iter()
.filter_map(|fontdesc| self.descriptor_to_font_id(fontdesc))
.collect();
// While .Apple Symbols Fallback is not a valid font. Apple Symbols is and it provides
// many fallback characters. This implementation is consistent with Alacritty:
// See: https://github.com/alacritty/crossfont/blob/d3515de22494c6fa70d84d2a9264c10097e303bd/src/darwin/mod.rs#L91
if let Some(font) = FontDB::descriptors_for_family("Apple Symbols")
.as_ref()
.and_then(|descriptor| descriptor.into_iter().next())
.and_then(|font_descriptor| self.descriptor_to_font_id(font_descriptor))
{
fallback_fonts.push(font);
}
fallback_fonts
}
// Get a list of CTFontDescriptors for a font family.
fn descriptors_for_family(name: &str) -> Option<CFArray<CTFontDescriptor>> {
let attributes: CFDictionary<CFString, CFType> = CFDictionary::from_CFType_pairs(&[(
CFString::new("NSFontFamilyAttribute"),
CFString::new(name).as_CFType(),
)]);
let descriptor = font_descriptor::new_from_attributes(&attributes);
let collection_descriptors = &CFArray::from_CFTypes(&[descriptor]);
let collection = font_collection::new_from_descriptors(collection_descriptors);
collection.get_descriptors()
}
// Convert a CTFontDescriptor to font_id. This function does not load fallback fonts
// and assumes the descriptor refers to a valid system font.
fn descriptor_to_font_id(&self, fontdesc: ItemRef<CTFontDescriptor>) -> Option<FontId> {
let name = match unsafe { FontDB::get_family_name(&fontdesc) } {
Some(family) => family,
None => {
log::warn!("Failed to load the font as it does not have a valid family name.");
return None;
}
};
let font_name = match unsafe { FontDB::get_font_name(&fontdesc) } {
Some(name) => name,
None => {
log::warn!("Failed to load the font as it does not have a valid name.");
return None;
}
};
// We should not load fonts with name that starts with dot
// https://developer.apple.com/videos/play/wwdc2019/227/?time=200
(!name.starts_with('.')).then(|| {
// Check if the fallback font is in cache.
match self.fonts_by_name.entry(Arc::new(font_name)) {
Entry::Occupied(entry) => return *entry.get(),
Entry::Vacant(_) => (),
}
// We need to push font after releasing the entry of the dashmap to prevent deadlocks.
self.push_font(Font::from_ct_font(font::new_from_descriptor(
&fontdesc,
DEFAULT_FONT_SIZE,
)))
})
}
pub fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
match self.font_selections.entry((family_id, properties)) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let family = &self
.families
.get(&family_id)
.expect("FamilyId must correspond to a valid family");
let candidates = family
.font_ids
.iter()
.map(|font_id| self.font(*font_id).properties())
.collect::<Vec<_>>();
let font_id = {
if let Ok(idx) = font_kit::matching::find_best_match(
&candidates,
&properties_to_font_kit(properties),
) {
self.font(family.font_ids[idx]).properties();
family.font_ids[idx]
} else {
font_kit::matching::find_best_match(&candidates, &Default::default())
.map(|idx| family.font_ids[idx])
.unwrap_or(family.font_ids[0])
}
};
// Make sure we've loaded fallback fonts for the selected font.
if !self.fallback_fonts.contains_key(&font_id) {
self.fallback_fonts.insert(
font_id,
Arc::new(self.cascade_list_for_languages(
&self.font(font_id).native_font(),
&["en"],
)),
);
}
*entry.insert(font_id)
}
}
}
pub fn font(&self, font_id: FontId) -> Arc<Font> {
self.rasterizer.font_for_id(font_id)
}
pub fn native_font(&self, font_id: FontId, size: f32) -> NativeFont {
match self.native_fonts.entry((font_id, OrderedFloat(size))) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => entry
.insert(
self.rasterizer
.font_for_id(font_id)
.native_font()
.clone_with_font_size(size as f64),
)
.clone(),
}
}
/// Returns the horizontal advance of a space character at the given size, or `None` if the
/// advance could not be measured. Uses a cached reference advance (measured at
/// `DEFAULT_FONT_SIZE`) scaled linearly to `size`.
pub fn space_advance_width(&self, font_id: FontId, size: f32) -> Option<f64> {
let stored = *self.space_advances.get(&font_id)?;
stored.map(|a| a * size as f64 / DEFAULT_FONT_SIZE)
}
pub fn font_id_for_native_font(&self, native_font: NativeFont) -> FontId {
let postscript_name = native_font.postscript_name();
if let Some(font_id) = self.fonts_by_name.get(&postscript_name).as_ref() {
return *font_id.value();
}
self.push_font(Font::from_ct_font(native_font))
}
fn push_font(&self, font: Font) -> FontId {
let name = Arc::new(font.postscript_name().unwrap());
let font_id = FontId(self.next_font_id.fetch_add(1, Ordering::SeqCst));
let ct_font = font.native_font().clone_with_font_size(DEFAULT_FONT_SIZE);
let advance = space_advance_width(&ct_font);
self.rasterizer.insert(font_id, Arc::new(font));
self.font_names.insert(font_id, name.clone());
self.fonts_by_name.insert(name, font_id);
self.space_advances.insert(font_id, advance);
font_id
}
fn insert_font_family(&mut self, font_family: FontFamily) -> Result<FamilyId> {
if let Some(family_id) = self.family_id_for_name(&font_family.name) {
return Ok(family_id);
}
let font_ids = font_family
.fonts
.into_iter()
.map(|font| self.push_font(font));
let family_id = self.create_new_family_id();
self.families.insert(
family_id,
Family {
name: font_family.name,
font_ids: font_ids.collect(),
},
);
Ok(family_id)
}
}
impl crate::platform::FontDB for FontDB {
fn load_from_bytes(&mut self, name: &str, bytes: Vec<Vec<u8>>) -> Result<FamilyId> {
let family = loader::load_font_family_from_bytes(name, bytes)?;
self.insert_font_family(family)
}
fn load_from_system(&mut self, font_family: &str) -> Result<FamilyId> {
let family = loader::load_system_font(font_family)?;
self.insert_font_family(family)
}
fn load_all_system_fonts(&self) -> BoxFuture<'static, Box<dyn platform::LoadedSystemFonts>> {
async { Box::new(loader::load_all_system_fonts()) as Box<dyn platform::LoadedSystemFonts> }
.boxed()
}
fn process_loaded_system_fonts(
&mut self,
loaded_system_fonts: Box<dyn platform::LoadedSystemFonts>,
) -> Vec<(Option<FamilyId>, crate::fonts::FontInfo)> {
let loaded_system_fonts: Box<LoadedSystemFonts> = loaded_system_fonts
.as_any()
.downcast()
.expect("should not fail to downcast to concrete type");
loaded_system_fonts
.0
.into_iter()
.flat_map(|(font_info, family)| {
let family_id = self.insert_font_family(family).ok()?;
Some((Some(family_id), font_info))
})
.collect_vec()
}
fn fallback_fonts(&self, _ch: char, font_id: FontId) -> Vec<FontId> {
self.fallback_fonts(font_id)
}
fn load_family_name_from_id(&self, id: FamilyId) -> Option<String> {
self.load_family_name_from_id(id)
}
fn select_font(&self, family_id: FamilyId, properties: Properties) -> FontId {
self.select_font(family_id, properties)
}
fn font_metrics(&self, font_id: FontId) -> Metrics {
match self.metrics.entry(font_id) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => *entry.insert(self.font(font_id).metrics().into()),
}
}
fn glyph_advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Vector2I> {
Ok(self.font(font_id).advance(glyph_id)?.to_i32())
}
fn glyph_raster_bounds(
&self,
font_id: FontId,
point_size: f32,
glyph_id: GlyphId,
scale: Vector2F,
glyph_config: &rendering::GlyphConfig,
) -> Result<RectI> {
self.rasterizer
.glyph_raster_bounds(font_id, point_size, glyph_id, scale, glyph_config)
}
fn glyph_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<RectI> {
Ok(self.font(font_id).typographic_bounds(glyph_id)?.to_i32())
}
fn rasterize_glyph(
&self,
font_id: FontId,
point_size: f32,
glyph_id: GlyphId,
scale: Vector2F,
subpixel_alignment: SubpixelAlignment,
glyph_config: &rendering::GlyphConfig,
format: RasterFormat,
) -> Result<RasterizedGlyph> {
self.rasterizer.rasterize_glyph(
font_id,
point_size,
glyph_id,
scale,
subpixel_alignment,
glyph_config,
format,
)
}
fn glyph_for_char(&self, font: FontId, char: char) -> Option<GlyphId> {
self.font(font).glyph_for_char(char)
}
fn family_id_for_name(&self, name: &str) -> Option<FamilyId> {
self.families
.iter()
.find(|(_, f)| f.name == name)
.map(|(id, _)| *id)
}
fn text_layout_system(&self) -> &dyn TextLayoutSystem {
self
}
}
impl crate::platform::TextLayoutSystem for FontDB {
fn layout_line(
&self,
text: &str,
line_style: LineStyle,
style_runs: &[(Range<usize>, StyleAndFont)],
_max_width: f32,
clip_config: ClipConfig,
) -> crate::text_layout::Line {
layout_line(text, line_style, style_runs, self, clip_config)
}
fn layout_text(
&self,
text: &str,
line_style: LineStyle,
style_runs: &[(Range<usize>, StyleAndFont)],
max_width: f32,
max_height: f32,
alignment: TextAlignment,
first_line_head_indent: Option<f32>,
) -> TextFrame {
layout_text(
text,
line_style,
style_runs,
self,
max_width,
max_height,
alignment,
first_line_head_indent,
)
}
}