fonts: Port macOS font code to use objc2-* crates (#41711)

This change moves Servo's macOS font code away from using our homegrown
`core-*` crates and toward the more general-purpose `objc2-*` crates.
Development of these crates is more active and they use automatic code
generation to have more complete coverage of the relevant platform APIs.
In
addition, this means that it is easier to understand Servo's code if you
are familiar with the platform APIs as the `objc2` crate are a more
direct Rust wrapper over them. In comparison, our wrappers had more
batteries-included behavior that was less flexible.

This change:
- is the first step toward more flexible font fallback on macOS (#41426)
- means we can now remove our manually FFI bindings for font variation
code.

Testing: This should not change behavior and macOS is currently untested
via WPT on the Ci.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson
2026-02-05 21:55:47 +01:00
committed by GitHub
parent 9e02b0ce65
commit 1db3ad5bd4
7 changed files with 293 additions and 188 deletions

63
Cargo.lock generated
View File

@@ -932,6 +932,15 @@ dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.3",
]
[[package]]
name = "blocking"
version = "1.6.2"
@@ -2817,9 +2826,6 @@ dependencies = [
"bitflags 2.10.0",
"byteorder",
"content-security-policy",
"core-foundation 0.9.4",
"core-graphics",
"core-text",
"dwrote",
"euclid",
"fonts_traits",
@@ -2833,6 +2839,10 @@ dependencies = [
"memmap2",
"net_traits",
"num-traits",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-text",
"paint_api",
"parking_lot",
"profile_traits",
@@ -6013,7 +6023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
"objc2-core-data",
@@ -6043,7 +6053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -6055,7 +6065,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -6067,7 +6077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -6079,7 +6089,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"dispatch2",
"libc",
"objc2 0.6.3",
]
@@ -6090,10 +6102,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"dispatch2",
"libc",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-io-surface",
"objc2-metal 0.3.2",
]
[[package]]
@@ -6102,7 +6117,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
@@ -6114,12 +6129,26 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-contacts",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"libc",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
@@ -6144,7 +6173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"dispatch",
"libc",
"objc2 0.5.2",
@@ -6190,7 +6219,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -6203,7 +6232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -6226,7 +6255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
@@ -6261,7 +6290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-cloud-kit",
"objc2-core-data",
@@ -6281,7 +6310,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
@@ -6293,7 +6322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
@@ -11222,7 +11251,7 @@ dependencies = [
"android-activity",
"atomic-waker",
"bitflags 2.10.0",
"block2",
"block2 0.5.1",
"bytemuck",
"calloop",
"cfg_aliases",

View File

@@ -124,6 +124,10 @@ num-bigint-dig = "0.8"
num-derive = "0.4.2"
num-traits = "0.2"
num_cpus = "1.17.0"
objc2 = "0.6.3"
objc2-core-foundation = "0.3.2"
objc2-core-graphics = "0.3.2"
objc2-core-text = "0.3.2"
ocb3 = "0.1.0"
openxr = "0.20"
paint_api = { path = "components/shared/paint" }

View File

@@ -56,9 +56,10 @@ webrender_api = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
byteorder = { workspace = true }
core-foundation = "0.9"
core-graphics = "0.23"
core-text = "20.1"
objc2 = { workspace = true }
objc2-core-foundation = { workspace = true }
objc2-core-graphics = { workspace = true }
objc2-core-text = { workspace = true }
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
freetype-sys = { workspace = true }

View File

@@ -3,17 +3,20 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use std::path::Path;
use std::sync::OnceLock;
use app_units::Au;
use core_foundation::base::TCFType;
use core_foundation::data::CFData;
use core_foundation::number::CFNumber;
use core_foundation::string::{CFString, CFStringRef};
use core_foundation::url::{CFURL, kCFURLPOSIXPathStyle};
use core_graphics::display::CFDictionary;
use core_text::font_descriptor::{kCTFontURLAttribute, kCTFontVariationAttribute};
use fonts_traits::FontIdentifier;
use objc2_core_foundation::{
CFData, CFDictionary, CFNumber, CFRetained, CFString, CFType, CFURL, CGFloat,
};
use objc2_core_text::{
CTFont, CTFontDescriptor, CTFontManagerCreateFontDescriptorFromData, kCTFontURLAttribute,
kCTFontVariationAttribute, kCTFontVariationAxisDefaultValueKey,
kCTFontVariationAxisIdentifierKey, kCTFontVariationAxisMaximumValueKey,
kCTFontVariationAxisMinimumValueKey,
};
use parking_lot::RwLock;
use webrender_api::FontVariation;
@@ -130,28 +133,31 @@ impl CoreTextFontCache {
// The only way to reliably load the correct font from a TTC bundle on
// macOS is to create the font using a descriptor with both the PostScript
// name and path.
let cf_name = CFString::new(&local_font_identifier.postscript_name);
let descriptor = core_text::font_descriptor::new_from_postscript_name(&cf_name);
let postscript_name = CFString::from_str(&local_font_identifier.postscript_name);
let descriptor =
unsafe { CTFontDescriptor::with_name_and_size(&postscript_name, 0.0) };
let cf_path = CFString::new(&local_font_identifier.path);
let url_attribute = unsafe { CFString::wrap_under_get_rule(kCTFontURLAttribute) };
let attributes = CFDictionary::from_CFType_pairs(&[(
url_attribute,
CFURL::from_file_system_path(cf_path, kCFURLPOSIXPathStyle, false),
)]);
descriptor.create_copy_with_attributes(attributes.to_untyped())
let cf_url =
CFURL::from_file_path(Path::new(&local_font_identifier.path.to_string()))?;
let attributes: CFRetained<CFDictionary<CFString, CFType>> =
CFDictionary::from_slices(
&[unsafe { kCTFontURLAttribute }],
&[cf_url.as_ref()],
);
unsafe { descriptor.copy_with_attributes(attributes.as_opaque()) }
},
FontIdentifier::Web(_) => {
let data = data
.expect("Should always have FontData for web fonts")
.clone();
let cf_data = CFData::from_arc(Arc::new(data));
core_text::font_manager::create_font_descriptor_with_data(cf_data)
let data = CFData::from_bytes(data.as_ref());
unsafe { CTFontManagerCreateFontDescriptorFromData(&data)? }
},
};
let ctfont = core_text::font::new_from_descriptor(&descriptor.ok()?, clamped_pt_size);
let ctfont = unsafe {
CTFont::with_font_descriptor(&descriptor, clamped_pt_size as CGFloat, std::ptr::null())
};
Some(PlatformFont::new_with_ctfont(ctfont, synthetic_bold))
}
@@ -197,52 +203,61 @@ impl CoreTextFontCache {
return platform_font;
}
let cftype_variations: Vec<_> = variations
let variation_keys: Vec<_> = variations
.iter()
.map(|variation| {
(
CFNumber::from(variation.tag as i64),
CFNumber::from(variation.value as f64),
)
})
.map(|variation| CFNumber::new_i64(variation.tag as i64))
.collect();
let values_dict = CFDictionary::from_CFType_pairs(&cftype_variations);
let variation_values: Vec<_> = variations
.iter()
.map(|variation| CFNumber::new_f32(variation.value))
.collect();
let values_dict = CFDictionary::<CFNumber, CFNumber>::from_slices(
&variation_keys
.iter()
.map(CFRetained::as_ref)
.collect::<Vec<_>>(),
&variation_values
.iter()
.map(CFRetained::as_ref)
.collect::<Vec<_>>(),
);
let variation_attribute =
unsafe { CFString::wrap_under_get_rule(kCTFontVariationAttribute) };
let attrs_dict = CFDictionary::from_CFType_pairs(&[(variation_attribute, values_dict)]);
let ct_var_font_desc = platform_font
.ctfont
.copy_descriptor()
.create_copy_with_attributes(attrs_dict.to_untyped())
.unwrap();
let attributes = CFDictionary::<CFString, CFType>::from_slices(
&[unsafe { kCTFontVariationAttribute }],
&[values_dict.as_ref()],
);
let descriptor_with_variations = unsafe {
platform_font
.ctfont
.font_descriptor()
.copy_with_attributes(attributes.as_opaque())
};
let ctfont = core_text::font::new_from_descriptor(&ct_var_font_desc, pt_size);
let ctfont = unsafe {
CTFont::with_font_descriptor(&descriptor_with_variations, pt_size, std::ptr::null())
};
PlatformFont::new_with_ctfont_and_variations(ctfont, variations, synthetic_bold)
}
fn get_variation_axis_information(
platform_font: &PlatformFont,
) -> Option<Vec<VariationAxisInformation>> {
let variation_axes = unsafe { platform_font.ctfont.variation_axes()? };
let traits = unsafe { variation_axes.cast_unchecked::<CFDictionary<CFString, CFNumber>>() };
Some(
platform_font
.ctfont
.get_variation_axes()?
traits
.iter()
.filter_map(|axes| {
let tag = unsafe { axes.find(kCTFontVariationAxisIdentifierKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let max_value = unsafe { axes.find(kCTFontVariationAxisMaximumValueKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let min_value = unsafe { axes.find(kCTFontVariationAxisMinimumValueKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let default_value = unsafe { axes.find(kCTFontVariationAxisDefaultValueKey) }
.and_then(|tag| tag.downcast::<CFNumber>())?;
let tag = unsafe { axes.get(kCTFontVariationAxisIdentifierKey) }?;
let max_value = unsafe { axes.get(kCTFontVariationAxisMaximumValueKey) }?;
let min_value = unsafe { axes.get(kCTFontVariationAxisMinimumValueKey) }?;
let default_value = unsafe { axes.get(kCTFontVariationAxisDefaultValueKey) }?;
Some(VariationAxisInformation {
tag: tag.to_i64()?,
max_value: max_value.to_f64()?,
min_value: min_value.to_f64()?,
default_value: default_value.to_f64()?,
tag: tag.as_i64()?,
max_value: max_value.as_f64()?,
min_value: min_value.as_f64()?,
default_value: default_value.as_f64()?,
})
})
.collect(),
@@ -257,10 +272,3 @@ struct VariationAxisInformation {
min_value: f64,
default_value: f64,
}
unsafe extern "C" {
static kCTFontVariationAxisDefaultValueKey: CFStringRef;
static kCTFontVariationAxisIdentifierKey: CFStringRef;
static kCTFontVariationAxisMaximumValueKey: CFStringRef;
static kCTFontVariationAxisMinimumValueKey: CFStringRef;
}

View File

@@ -4,21 +4,23 @@
use std::cmp::Ordering;
use std::ops::Range;
use std::ptr::NonNull;
use std::{fmt, ptr};
/// Implementation of Quartz (CoreGraphics) fonts.
use app_units::Au;
use byteorder::{BigEndian, ByteOrder};
use core_foundation::data::CFData;
use core_foundation::string::UniChar;
use core_graphics::font::CGGlyph;
use core_text::font::CTFont;
use core_text::font_descriptor::{
CTFontTraits, SymbolicTraitAccessors, TraitAccessors, kCTFontDefaultOrientation,
};
use euclid::default::{Point2D, Rect, Size2D};
use fonts_traits::{FontIdentifier, LocalFontIdentifier};
use log::debug;
use objc2_core_foundation::{
CFData, CFDictionary, CFIndex, CFNumber, CFRetained, CFString, CFType,
};
use objc2_core_graphics::CGGlyph;
use objc2_core_text::{
CTFont, CTFontOrientation, CTFontSymbolicTraits, CTFontTableOptions, CTFontTableTag,
kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait,
};
use skrifa::Tag;
use style::values::computed::font::{FontStretch, FontStyle, FontWeight};
use webrender_api::{FontInstanceFlags, FontVariation};
@@ -33,7 +35,7 @@ const KERN_PAIR_LEN: usize = 6;
#[derive(Clone)]
pub struct FontTable {
data: CFData,
data: CFRetained<CFData>,
}
// assumes 72 points per inch, and 96 px per inch
@@ -42,20 +44,20 @@ fn pt_to_px(pt: f64) -> f64 {
}
impl FontTable {
pub(crate) fn wrap(data: CFData) -> FontTable {
pub(crate) fn wrap(data: CFRetained<CFData>) -> FontTable {
FontTable { data }
}
}
impl FontTableMethods for FontTable {
fn buffer(&self) -> &[u8] {
self.data.bytes()
unsafe { self.data.as_bytes_unchecked() }
}
}
#[derive(Clone, Debug)]
pub struct PlatformFont {
pub(crate) ctfont: CTFont,
pub(crate) ctfont: CFRetained<CTFont>,
variations: Vec<FontVariation>,
h_kern_subtable: Option<CachedKernTable>,
synthetic_bold: bool,
@@ -73,19 +75,23 @@ unsafe impl Sync for PlatformFont {}
unsafe impl Send for PlatformFont {}
impl PlatformFont {
pub(crate) fn new_with_ctfont(ctfont: CTFont, synthetic_bold: bool) -> Self {
pub(crate) fn new_with_ctfont(ctfont: CFRetained<CTFont>, synthetic_bold: bool) -> Self {
Self::new_with_ctfont_and_variations(ctfont, vec![], synthetic_bold)
}
#[expect(unsafe_code)]
pub(crate) fn new_with_ctfont_and_variations(
ctfont: CTFont,
ctfont: CFRetained<CTFont>,
variations: Vec<FontVariation>,
synthetic_bold: bool,
) -> PlatformFont {
let ctfont_is_bold = ctfont.all_traits().weight() >= FontWeight::BOLD_THRESHOLD;
let is_variable_font = ctfont
.get_variation_axes()
.is_some_and(|arr| !arr.is_empty());
let ctfont_is_bold = unsafe {
ctfont
.symbolic_traits()
.contains(CTFontSymbolicTraits::TraitBold)
};
let is_variable_font =
unsafe { ctfont.variation_axes().is_some_and(|arr| !arr.is_empty()) };
let synthetic_bold = !ctfont_is_bold && !is_variable_font && synthetic_bold;
@@ -124,6 +130,7 @@ impl PlatformFont {
/// Cache all the data needed for basic horizontal kerning. This is used only as a fallback or
/// fast path (when the GPOS table is missing or unnecessary) so it needn't handle every case.
#[expect(unsafe_code)]
fn load_h_kern_subtable(&mut self) {
if self.h_kern_subtable.is_some() {
return;
@@ -175,7 +182,7 @@ impl PlatformFont {
}
let pt_per_font_unit =
self.ctfont.pt_size() / self.ctfont.units_per_em() as f64;
unsafe { self.ctfont.size() / self.ctfont.units_per_em() as f64 };
result.px_per_font_unit = pt_to_px(pt_per_font_unit);
}
start = end;
@@ -191,7 +198,7 @@ impl PlatformFont {
// <https://github.com/servo/webrender/blob/main/wr_glyph_rasterizer/src/rasterizer.rs#L1006>
fn get_extra_strikes(&self, strike_scale: f64) -> usize {
if self.synthetic_bold {
let mut bold_offset = self.ctfont.pt_size() / 48.0;
let mut bold_offset = unsafe { self.ctfont.size() } / 48.0;
if bold_offset < 1.0 {
bold_offset = 0.25 + 0.75 * bold_offset;
}
@@ -270,8 +277,7 @@ impl PlatformFontMethods for PlatformFont {
}
fn descriptor(&self) -> FontTemplateDescriptor {
let traits = self.ctfont.all_traits();
FontTemplateDescriptor::new(traits.weight(), traits.stretch(), traits.style())
font_template_descriptor_from_ctfont_attributes(unsafe { self.ctfont.traits() })
}
fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
@@ -281,15 +287,15 @@ impl PlatformFontMethods for PlatformFont {
// of the buffer to CTFontGetGlyphsForCharacters, but passing the actual number of encoded
// code units ensures that the resulting glyph is always placed in the first slot in the output
// buffer.
let mut characters: [UniChar; 2] = [0, 0];
let mut characters: [u16; 2] = [0, 0];
let encoded_characters = codepoint.encode_utf16(&mut characters);
let mut glyphs: [CGGlyph; 2] = [0, 0];
let result = unsafe {
self.ctfont.get_glyphs_for_characters(
encoded_characters.as_ptr(),
glyphs.as_mut_ptr(),
encoded_characters.len() as isize,
self.ctfont.glyphs_for_characters(
NonNull::from(&mut encoded_characters[0]),
NonNull::from(&mut glyphs[0]),
encoded_characters.len() as CFIndex,
)
};
@@ -311,11 +317,10 @@ impl PlatformFontMethods for PlatformFont {
}
fn glyph_h_advance(&self, glyph: GlyphId) -> Option<FractionalPixel> {
let glyphs = [glyph as CGGlyph];
let mut advance = unsafe {
self.ctfont.get_advances_for_glyphs(
kCTFontDefaultOrientation,
&glyphs[0],
self.ctfont.advances_for_glyphs(
CTFontOrientation::Default,
NonNull::from(&mut (glyph as CGGlyph)),
ptr::null_mut(),
1,
)
@@ -350,14 +355,14 @@ impl PlatformFontMethods for PlatformFont {
// TODO(mrobinson): Gecko first tries to get metrics from the SFNT tables via
// HarfBuzz and only afterward falls back to platform APIs. We should do something
// similar here. This will likely address issue #201 mentioned below.
let ascent = self.ctfont.ascent();
let descent = self.ctfont.descent();
let leading = self.ctfont.leading();
let x_height = self.ctfont.x_height();
let underline_thickness = self.ctfont.underline_thickness();
let ascent = unsafe { self.ctfont.ascent() };
let descent = unsafe { self.ctfont.descent() };
let leading = unsafe { self.ctfont.leading() };
let x_height = unsafe { self.ctfont.x_height() };
let underline_thickness = unsafe { self.ctfont.underline_thickness() };
let line_gap = (ascent + descent + leading + 0.5).floor();
let max_advance = Au::from_f64_px(self.ctfont.bounding_box().size.width);
let max_advance = Au::from_f64_px(unsafe { self.ctfont.bounding_box().size.width });
let zero_horizontal_advance = self
.glyph_index('0')
.and_then(|idx| self.glyph_h_advance(idx))
@@ -374,6 +379,7 @@ impl PlatformFontMethods for PlatformFont {
.map(Au::from_f64_px)
.unwrap_or(average_advance);
let pt_size = unsafe { self.ctfont.size() };
let metrics = FontMetrics {
underline_size: Au::from_f64_px(underline_thickness),
// TODO(Issue #201): underline metrics are not reliable. Have to pull out of font table
@@ -381,7 +387,7 @@ impl PlatformFontMethods for PlatformFont {
//
// see also: https://bugs.webkit.org/show_bug.cgi?id=16768
// see also: https://bugreports.qt-project.org/browse/QTBUG-13364
underline_offset: Au::from_f64_px(self.ctfont.underline_position()),
underline_offset: Au::from_f64_px(unsafe { self.ctfont.underline_position() }),
// There is no way to get these from CoreText or CoreGraphics APIs, so
// derive them from the other font metrics. These should eventually be
// found in the font tables directly when #201 is fixed.
@@ -389,7 +395,7 @@ impl PlatformFontMethods for PlatformFont {
strikeout_offset: Au::from_f64_px((x_height + underline_thickness) / 2.0),
leading: Au::from_f64_px(leading),
x_height: Au::from_f64_px(x_height),
em_size: Au::from_f64_px(self.ctfont.pt_size()),
em_size: Au::from_f64_px(pt_size),
ascent: Au::from_f64_px(ascent),
descent: Au::from_f64_px(descent),
max_advance,
@@ -399,18 +405,17 @@ impl PlatformFontMethods for PlatformFont {
ic_horizontal_advance,
space_advance,
};
debug!(
"Font metrics (@{} pt): {:?}",
self.ctfont.pt_size(),
metrics
);
debug!("Font metrics (@{pt_size} pt): {metrics:?}");
metrics
}
fn table_for_tag(&self, tag: Tag) -> Option<FontTable> {
let tag_u32 = u32::from_be_bytes(tag.to_be_bytes());
let result: Option<CFData> = self.ctfont.get_font_table(tag_u32);
result.map(FontTable::wrap)
unsafe {
self.ctfont
.table(tag_u32 as CTFontTableTag, CTFontTableOptions::NoOptions)
}
.map(FontTable::wrap)
}
/// Get the necessary [`FontInstanceFlags`]` for this font.
@@ -435,9 +440,15 @@ impl PlatformFontMethods for PlatformFont {
}
fn typographic_bounds(&self, glyph_id: GlyphId) -> Rect<f32> {
let rect = self
.ctfont
.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph_id as u16]);
let rect = unsafe {
self.ctfont.bounding_rects_for_glyphs(
CTFontOrientation::Default,
NonNull::from(&mut (glyph_id as CGGlyph)),
std::ptr::null_mut(),
1,
)
};
Rect::new(
Point2D::new(rect.origin.x as f32, rect.origin.y as f32),
Size2D::new(rect.size.width as f32, rect.size.height as f32),
@@ -449,49 +460,57 @@ impl PlatformFontMethods for PlatformFont {
}
}
pub(super) trait CoreTextFontTraitsMapping {
fn weight(&self) -> FontWeight;
fn style(&self) -> FontStyle;
fn stretch(&self) -> FontStretch;
}
pub(crate) fn font_template_descriptor_from_ctfont_attributes(
traits: CFRetained<CFDictionary>,
) -> FontTemplateDescriptor {
let traits = unsafe { traits.cast_unchecked::<CFString, CFType>() };
impl CoreTextFontTraitsMapping for CTFontTraits {
fn weight(&self) -> FontWeight {
// From https://developer.apple.com/documentation/coretext/kctfontweighttrait?language=objc
// > The value returned is a CFNumberRef representing a float value between -1.0 and
// > 1.0 for normalized weight. The value of 0.0 corresponds to the regular or
// > medium font weight.
let mapping = [(-1., 0.), (0., 400.), (1., 1000.)];
let get_f64_trait = |key| {
traits
.get(key)
.and_then(|value| value.downcast::<CFNumber>().ok()?.as_f64())
};
let mapped_weight = map_platform_values_to_style_values(&mapping, self.normalized_weight());
FontWeight::from_float(mapped_weight as f32)
}
// From https://developer.apple.com/documentation/coretext/kctfontweighttrait?language=objc
// > The value returned is a CFNumberRef representing a float value between -1.0 and
// > 1.0 for normalized weight. The value of 0.0 corresponds to the regular or
// > medium font weight.
let mapping = [(-1., 0.), (0., 400.), (1., 1000.)];
let font_weight = get_f64_trait(unsafe { kCTFontWeightTrait }).unwrap_or(400.);
let weight =
FontWeight::from_float(map_platform_values_to_style_values(&mapping, font_weight) as f32);
fn style(&self) -> FontStyle {
let slant = self.normalized_slant();
if slant == 0. && self.symbolic_traits().is_italic() {
return FontStyle::ITALIC;
let font_slant = get_f64_trait(unsafe { kCTFontSlantTrait }).unwrap_or(0.);
let style = if font_slant == 0. {
let symbolic_traits = traits
.get(unsafe { kCTFontSymbolicTrait })
.and_then(|value| value.downcast::<CFNumber>().ok()?.as_i64())
.map(|value| CTFontSymbolicTraits::from_bits_retain(value as u32));
match symbolic_traits {
Some(symbolic_traits)
if symbolic_traits.contains(CTFontSymbolicTraits::TraitItalic) =>
{
FontStyle::ITALIC
},
_ => FontStyle::NORMAL,
}
if slant == 0. {
return FontStyle::NORMAL;
}
} else {
// From https://developer.apple.com/documentation/coretext/kctfontslanttrait?language=objc
// > The value returned is a CFNumberRef object representing a float value
// > between -1.0 and 1.0 for normalized slant angle. The value of 0.0
// > corresponds to 0 degrees clockwise rotation from the vertical and 1.0
// > corresponds to 30 degrees clockwise rotation.
let mapping = [(-1., -30.), (0., 0.), (1., 30.)];
let mapped_slant = map_platform_values_to_style_values(&mapping, slant);
FontStyle::oblique(mapped_slant as f32)
}
FontStyle::oblique(map_platform_values_to_style_values(&mapping, font_slant) as f32)
};
fn stretch(&self) -> FontStretch {
// From https://developer.apple.com/documentation/coretext/kctfontwidthtrait?language=objc
// > This value corresponds to the relative interglyph spacing for a given font.
// > The value returned is a CFNumberRef object representing a float between -1.0
// > and 1.0. The value of 0.0 corresponds to regular glyph spacing, and negative
// > values represent condensed glyph spacing.
FontStretch::from_percentage(self.normalized_width() as f32 + 1.0)
}
// From https://developer.apple.com/documentation/coretext/kctfontwidthtrait?language=objc
// > This value corresponds to the relative interglyph spacing for a given font.
// > The value returned is a CFNumberRef object representing a float between -1.0
// > and 1.0. The value of 0.0 corresponds to regular glyph spacing, and negative
// > values represent condensed glyph spacing.
let font_stretch = get_f64_trait(unsafe { kCTFontWidthTrait }).unwrap_or(0.);
let stretch = FontStretch::from_percentage(font_stretch as f32 + 1.0);
FontTemplateDescriptor::new(weight, stretch, style)
}

View File

@@ -2,59 +2,102 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::ffi::c_void;
use base::text::{UnicodeBlock, UnicodeBlockMethod, unicode_plane};
use fonts_traits::LocalFontIdentifier;
use log::debug;
use objc2_core_foundation::{CFDictionary, CFRetained, CFSet, CFString, CFType, CFURL};
use objc2_core_text::{
CTFontDescriptor, CTFontManagerCopyAvailableFontFamilyNames, kCTFontFamilyNameAttribute,
kCTFontNameAttribute, kCTFontTraitsAttribute, kCTFontURLAttribute,
};
use style::Atom;
use style::values::computed::font::GenericFontFamily;
use unicode_script::Script;
use crate::platform::add_noto_fallback_families;
use crate::platform::font::CoreTextFontTraitsMapping;
use crate::platform::font::font_template_descriptor_from_ctfont_attributes;
use crate::{
EmojiPresentationPreference, FallbackFontSelectionOptions, FontIdentifier, FontTemplate,
FontTemplateDescriptor, LowercaseFontFamilyName,
LowercaseFontFamilyName,
};
pub(crate) fn for_each_available_family<F>(mut callback: F)
where
F: FnMut(String),
{
let family_names = core_text::font_collection::get_family_names();
let family_names = unsafe { CTFontManagerCopyAvailableFontFamilyNames() };
let family_names = unsafe { family_names.cast_unchecked::<CFString>() };
for family_name in family_names.iter() {
callback(family_name.to_string());
}
}
fn font_template_for_local_font_descriptor(
family_descriptor: CFRetained<CTFontDescriptor>,
) -> Option<FontTemplate> {
let url = unsafe {
family_descriptor
.attribute(kCTFontURLAttribute)?
.downcast::<CFURL>()
.ok()?
};
let font_name = unsafe {
family_descriptor
.attribute(kCTFontNameAttribute)?
.downcast::<CFString>()
.ok()?
};
let traits = unsafe {
family_descriptor
.attribute(kCTFontTraitsAttribute)?
.downcast::<CFDictionary>()
.ok()?
};
let identifier = LocalFontIdentifier {
postscript_name: Atom::from(font_name.to_string()),
path: Atom::from(url.to_file_path()?.to_str()?),
};
Some(FontTemplate::new(
FontIdentifier::Local(identifier),
font_template_descriptor_from_ctfont_attributes(traits),
None,
None,
))
}
pub(crate) fn for_each_variation<F>(family_name: &str, mut callback: F)
where
F: FnMut(FontTemplate),
{
debug!("Looking for faces of family: {}", family_name);
let family_collection = core_text::font_collection::create_for_family(family_name);
if let Some(family_collection) = family_collection {
if let Some(family_descriptors) = family_collection.get_descriptors() {
for family_descriptor in family_descriptors.iter() {
let path = family_descriptor.font_path();
let path = match path.as_ref().and_then(|path| path.to_str()) {
Some(path) => path,
None => continue,
};
let traits = family_descriptor.traits();
let descriptor =
FontTemplateDescriptor::new(traits.weight(), traits.stretch(), traits.style());
let identifier = LocalFontIdentifier {
postscript_name: Atom::from(family_descriptor.font_name()),
path: Atom::from(path),
};
callback(FontTemplate::new(
FontIdentifier::Local(identifier),
descriptor,
None,
None,
));
}
let specified_attributes: CFRetained<CFDictionary<CFString, CFType>> =
CFDictionary::from_slices(
&[unsafe { kCTFontFamilyNameAttribute }],
&[CFString::from_str(family_name).as_ref()],
);
let wildcard_descriptor =
unsafe { CTFontDescriptor::with_attributes(specified_attributes.as_ref()) };
let values = [unsafe { kCTFontFamilyNameAttribute }];
let values = values.as_ptr().cast::<*const c_void>().cast_mut();
let mandatory_attributes = unsafe { CFSet::new(None, values, 1, std::ptr::null()) };
let Some(mandatory_attributes) = mandatory_attributes else {
return;
};
let matched_descriptors =
unsafe { wildcard_descriptor.matching_font_descriptors(Some(&mandatory_attributes)) };
let Some(matched_descriptors) = matched_descriptors else {
return;
};
let matched_descriptors = unsafe { matched_descriptors.cast_unchecked::<CTFontDescriptor>() };
for family_descriptor in matched_descriptors.iter() {
if let Some(font_template) = font_template_for_local_font_descriptor(family_descriptor) {
callback(font_template)
}
}
}

View File

@@ -154,6 +154,7 @@ skip = [
"socket2",
# duplicated by winit
"block2",
"objc2-app-kit",
"objc2-foundation",
"objc2",