feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)

Ports the macOS-specific Zig code behind cfg(target_os = "macos"):
- renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend
  (all renderer/metal/* files, shaders.metal verbatim, runtime library
  compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga;
  OpenGL remains the backend elsewhere.
- font: CoreText face, shaper and discovery with build.rs backend
  selection (coretext default; coretext-freetype / coretext-harfbuzz /
  coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple.
- input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes.
- os: macos, mach, cf_release_thread, NSProcessInfo args and macOS
  branches; config: macOS defaults, keybinds, Application Support paths.
- termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit
  handling, os_log logging.

Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings
clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot
run here. The embedded runtime/C API (embedded.zig) is the next step.

Also includes the cross-platform integration edits shared by the crash,
verification, Windows and macOS work (Cargo target-gating, thread
crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg
gates), a cfg fix so ghostty-config builds on wasm32, and an allow for
the intentional recursion in the crash overflow test.

Test Plan:
- cargo test --workspace (3224 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check --target aarch64-apple-darwin -p ghostty-renderer (with
  the documented cross env)
This commit is contained in:
ddidderr committed 2026-10-12 06:02:39 +02:00
1 parent 3c8fae1089
commit 452b782149
124 files changed
+14936 -1242

No files matched your search

+38 -2
View File
@@ -18,14 +18,50 @@ ghostty-terminal.workspace = true
# The C font libraries are unavailable on wasm32, where the web_canvas
# backend (browser Canvas API through JS imports) is used instead
# (Zig: `font/backend.zig` `Backend.default`).
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
[target.'cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))'.dependencies]
yeslogic-fontconfig-sys = "6.0.1"
[target.'cfg(not(any(target_arch = "wasm32", target_vendor = "apple", windows)))'.dependencies]
freetype-sys = "0.23.0"
harfbuzz-sys = "0.8.0"
yeslogic-fontconfig-sys = "6.0.1"
# Windows has no system freetype/harfbuzz to find via pkg-config (Zig builds
# them from source): build them from the bundled sources. Fontconfig (the
# default Zig backend there too, also built from source) must be provided
# by the Windows toolchain (vcpkg / MSYS2) when linking.
[target.'cfg(windows)'.dependencies]
freetype-sys = { version = "0.23.0", features = ["bundled"] }
harfbuzz-sys = { version = "0.8.0", features = ["bundled"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
ghostty-os.workspace = true
# Apple targets use CoreText (`coretext` backend) by default; FreeType and
# HarfBuzz are only linked for the `coretext-freetype` / `coretext-harfbuzz`
# backends (see `build.rs` and the `[features]` table).
[target.'cfg(target_vendor = "apple")'.dependencies]
# Like upstream (which builds them from source) the bundled copies are used,
# so no system library is needed on macOS.
freetype-sys = { version = "0.23.0", optional = true, features = ["bundled"] }
harfbuzz-sys = { version = "0.8.0", optional = true, default-features = false, features = ["coretext", "bundled"] }
objc2 = "0.6.5"
objc2-core-foundation = "0.3.2"
objc2-core-graphics = "0.3.2"
objc2-core-text = "0.3.2"
objc2-foundation = "0.3.2"
[features]
default = []
# CoreText for discovery, FreeType for rendering, HarfBuzz for shaping
# (Zig `-Dfont-backend=coretext_freetype`; Apple targets only).
coretext-freetype = ["dep:freetype-sys", "dep:harfbuzz-sys", "harfbuzz-sys/freetype"]
# CoreText for discovery and rendering, HarfBuzz for shaping
# (Zig `-Dfont-backend=coretext_harfbuzz`; Apple targets only).
coretext-harfbuzz = ["dep:harfbuzz-sys"]
# CoreText for discovery and rendering, no shaping
# (Zig `-Dfont-backend=coretext_noshape`; Apple targets only).
coretext-noshape = []
# This crate wraps the freetype / harfbuzz / fontconfig C libraries through
# their `-sys` crates; unsafe is denied by default and allowed on minimal,
# `// SAFETY:`-commented items only.
+60
View File
@@ -0,0 +1,60 @@
//! Emits the font backend `cfg`s (port of `font/backend.zig`'s `Backend`
//! helpers `hasFreetype`/`hasCoretext`/`hasFontconfig`/`hasHarfbuzz`).
//!
//! Backends (see `src/backend.rs`):
//! * Linux & other non-Apple, non-wasm targets: `fontconfig_freetype`.
//! * wasm32: `web_canvas`.
//! * Apple targets: `coretext` (default), or via cargo features
//! `coretext-freetype`, `coretext-harfbuzz`, `coretext-noshape`.
//!
//! Emitted cfgs:
//! * `font_freetype` - FreeType face / library are compiled.
//! * `font_fontconfig` - fontconfig discovery is compiled.
//! * `font_harfbuzz` - the HarfBuzz shaper is compiled.
//! * `font_coretext` - CoreText discovery/deferred face (Apple targets).
//! * `font_coretext_face` - the CoreText `Face` is the font face.
//! * `font_coretext_shaper` - the CoreText shaper is the shaper.
//! * `font_noshape` - the no-op shaper is the shaper.
fn main() {
println!("cargo:rerun-if-changed=build.rs");
for c in [
"font_freetype",
"font_fontconfig",
"font_harfbuzz",
"font_coretext",
"font_coretext_face",
"font_coretext_shaper",
"font_noshape",
] {
println!("cargo:rustc-check-cfg=cfg({c})");
}
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let vendor = std::env::var("CARGO_CFG_TARGET_VENDOR").unwrap_or_default();
let feat = |n: &str| std::env::var_os(format!("CARGO_FEATURE_{n}")).is_some();
let wasm = arch == "wasm32";
let apple = vendor == "apple";
let ct_ft = feat("CORETEXT_FREETYPE");
let ct_hb = feat("CORETEXT_HARFBUZZ");
let ct_noshape = feat("CORETEXT_NOSHAPE");
let on = |c: &str, v: bool| {
if v {
println!("cargo:rustc-cfg={c}");
}
};
let freetype = !wasm && (!apple || ct_ft);
let harfbuzz = !wasm && (!apple || ct_ft || ct_hb);
on("font_freetype", freetype);
on("font_fontconfig", !wasm && !apple);
on("font_harfbuzz", harfbuzz);
on("font_coretext", apple);
on("font_coretext_face", apple && !ct_ft);
// The shaper is CoreText only for the plain `coretext` backend;
// `coretext_noshape` uses the no-op shaper.
on("font_coretext_shaper", apple && !harfbuzz && !ct_noshape);
on("font_noshape", apple && !harfbuzz && ct_noshape);
}
+90 -7
View File
@@ -1,9 +1,19 @@
//! Font backend selection. Port of `font/backend.zig`.
//!
//! On Linux (and other non-wasm targets) [`OPTIONS`] selects
//! [`Backend::FontconfigFreetype`] (fontconfig discovery, FreeType rendering,
//! HarfBuzz shaping). The other variants are kept for fidelity with the Zig
//! enum and its helper predicates.
//! [`OPTIONS`] selects the backend for the target (`build.rs` emits the
//! matching `cfg`s):
//!
//! * Linux and other non-Apple, non-wasm targets:
//! [`Backend::FontconfigFreetype`] (fontconfig discovery, FreeType
//! rendering, HarfBuzz shaping).
//! * Apple targets: [`Backend::Coretext`] by default. macOS also supports
//! [`Backend::CoretextFreetype`], [`Backend::CoretextHarfbuzz`] and
//! [`Backend::CoretextNoshape`], selected with the cargo features
//! `coretext-freetype`, `coretext-harfbuzz` and `coretext-noshape`
//! (Zig: `-Dfont-backend=...`). "There is no scenario that is the default"
//! for those; they're only used by people who want to self-compile Ghostty
//! and prefer the freetype aesthetic.
//! * wasm32: [`Backend::WebCanvas`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Backend {
@@ -31,7 +41,9 @@ impl Backend {
// with `wasm_target == .browser`).
if cfg!(target_arch = "wasm32") {
Backend::WebCanvas
} else if cfg!(target_os = "macos") {
} else if cfg!(target_vendor = "apple") {
// macOS also supports "coretext_freetype" but there is no
// scenario that is the default.
Backend::Coretext
} else {
Backend::FontconfigFreetype
@@ -76,11 +88,45 @@ pub struct Options {
pub backend: Backend,
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), not(target_vendor = "apple")))]
pub const OPTIONS: Options = Options {
backend: Backend::FontconfigFreetype,
};
#[cfg(all(target_vendor = "apple", feature = "coretext-freetype"))]
pub const OPTIONS: Options = Options {
backend: Backend::CoretextFreetype,
};
#[cfg(all(
target_vendor = "apple",
feature = "coretext-harfbuzz",
not(feature = "coretext-freetype")
))]
pub const OPTIONS: Options = Options {
backend: Backend::CoretextHarfbuzz,
};
#[cfg(all(
target_vendor = "apple",
feature = "coretext-noshape",
not(feature = "coretext-freetype"),
not(feature = "coretext-harfbuzz")
))]
pub const OPTIONS: Options = Options {
backend: Backend::CoretextNoshape,
};
#[cfg(all(
target_vendor = "apple",
not(feature = "coretext-freetype"),
not(feature = "coretext-harfbuzz"),
not(feature = "coretext-noshape")
))]
pub const OPTIONS: Options = Options {
backend: Backend::Coretext,
};
#[cfg(target_arch = "wasm32")]
pub const OPTIONS: Options = Options {
backend: Backend::WebCanvas,
@@ -99,7 +145,44 @@ mod tests {
assert!(!b.has_coretext());
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn coretext_capabilities() {
let b = Backend::Coretext;
assert!(!b.has_freetype());
assert!(!b.has_fontconfig());
assert!(!b.has_harfbuzz());
assert!(b.has_coretext());
let b = Backend::CoretextFreetype;
assert!(b.has_freetype());
assert!(!b.has_fontconfig());
assert!(b.has_harfbuzz());
assert!(b.has_coretext());
let b = Backend::CoretextHarfbuzz;
assert!(!b.has_freetype());
assert!(b.has_harfbuzz());
assert!(b.has_coretext());
let b = Backend::CoretextNoshape;
assert!(!b.has_freetype());
assert!(!b.has_harfbuzz());
assert!(b.has_coretext());
}
#[test]
fn default_backend_per_target() {
let b = Backend::default_for_target();
if cfg!(target_vendor = "apple") {
assert_eq!(b, Backend::Coretext);
} else if cfg!(target_arch = "wasm32") {
assert_eq!(b, Backend::WebCanvas);
} else {
assert_eq!(b, Backend::FontconfigFreetype);
}
}
#[cfg(all(not(target_arch = "wasm32"), not(target_vendor = "apple")))]
#[test]
fn linux_backend_capabilities() {
let b = OPTIONS.backend;
@@ -19,7 +19,7 @@ use crate::{
Style,
collection::{AddOptions, Collection, Entry, GetFaceError, Index, PresentationMode, Special},
discovery::Discover,
face::{RenderOptions, freetype::RenderError as FaceRenderError},
face::{RenderError as FaceRenderError, RenderOptions},
sprite,
};
@@ -257,7 +257,7 @@ impl CodepointResolver {
monospace: false,
..Default::default()
};
let mut disco_it = disco.discover_fallback(&self.collection, &desc).ok()?;
let mut disco_it = disco.discover_fallback(&mut self.collection, &desc).ok()?;
loop {
let deferred_face = match disco_it.next() {
@@ -490,6 +490,8 @@ mod tests {
c.load_options = Some(LoadOptions::new(lib.clone()));
add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0);
// Coretext doesn't support Noto's format
#[cfg(not(font_coretext))]
add(&mut c, &lib, embedded::EMOJI, Style::Regular, 12.0);
add(&mut c, &lib, embedded::EMOJI_TEXT, Style::Regular, 12.0);
@@ -515,7 +517,8 @@ mod tests {
.get_index(0x270C, Style::Regular, Some(Presentation::Text))
.unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(2, idx.idx);
let text_idx = if cfg!(font_coretext) { 1 } else { 2 };
assert_eq!(text_idx, idx.idx);
}
{
let idx = r
+3 -2
View File
@@ -124,7 +124,7 @@ pub enum GetFaceError {
#[error("face load failed: {0}")]
Load(#[from] crate::deferred_face::LoadError),
#[error("face error: {0}")]
Face(#[from] face::freetype::FaceError),
Face(#[from] face::FaceError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
@@ -158,7 +158,7 @@ enum SyntheticError {
#[error(transparent)]
GetFace(#[from] GetFaceError),
#[error(transparent)]
Face(#[from] face::freetype::FaceError),
Face(#[from] face::FaceError),
}
impl Collection {
@@ -1270,6 +1270,7 @@ mod tests {
assert!(!c.has_codepoint(idx, '🥸' as u32, PresentationMode::Any));
}
#[cfg(not(font_coretext))]
#[test]
fn has_codepoint_emoji_default_graphical() {
let lib = Library::init().unwrap();
+165
View File
@@ -0,0 +1,165 @@
//! Small helpers over the CoreText / CoreFoundation bindings shared by the
//! CoreText face, shaper and discovery (the Zig `macos` package conveniences
//! they rely on). Apple targets only.
#![allow(unsafe_code, clippy::trivially_copy_pass_by_ref)]
use std::{ops::Deref, ptr::NonNull};
use objc2_core_foundation::{CFData, CFRetained, CFString};
use objc2_core_text::{CTFont, CTFontTableOptions};
use crate::face::coretext_math::utf16_units;
/// A retained CoreFoundation object that may be moved/shared across threads.
///
/// CoreText font objects (`CTFont`, `CTFontDescriptor`) are immutable and
/// documented as thread safe by Apple ("CTFont ... are thread-safe"), but the
/// generated bindings conservatively do not mark them `Send`/`Sync`.
pub struct CfBox<T: ?Sized>(pub(crate) CFRetained<T>);
// SAFETY: see the type docs; the wrapped objects are immutable CoreText
// objects, safe to use from any thread.
unsafe impl<T: ?Sized> Send for CfBox<T> {}
// SAFETY: as above.
unsafe impl<T: ?Sized> Sync for CfBox<T> {}
impl<T: ?Sized> CfBox<T> {
pub(crate) fn new(v: CFRetained<T>) -> Self {
CfBox(v)
}
}
impl<T: ?Sized> Deref for CfBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T: objc2_core_foundation::Type> Clone for CfBox<T> {
fn clone(&self) -> Self {
CfBox(self.0.clone())
}
}
/// Convert a `CFString` to a Rust `String`.
pub(crate) fn cf_to_string(s: &CFString) -> String {
s.to_string()
}
/// Copy the sfnt table with the given tag, if present.
pub(crate) fn font_table(font: &CTFont, tag: &[u8; 4]) -> Option<CFRetained<CFData>> {
// SAFETY: plain CoreText call with valid arguments.
unsafe { font.table(u32::from_be_bytes(*tag), CTFontTableOptions::NoOptions) }
}
/// Copy the bytes of the sfnt table with the given tag, if present.
pub(crate) fn font_table_bytes(font: &CTFont, tag: &[u8; 4]) -> Option<Vec<u8>> {
font_table(font, tag).map(|d| d.to_vec())
}
/// `CTFontGetGlyphsForCharacters` for a single UTF-32 codepoint. Returns the
/// glyph ID of the first UTF-16 unit and whether *all* units mapped to glyphs
/// (the C function's return value).
pub(crate) fn glyphs_for_codepoint(font: &CTFont, cp: u32) -> (u16, bool) {
// Turn UTF-32 into UTF-16 for the CT API
let (mut unichars, len) = utf16_units(cp);
let mut glyphs = [0u16; 2];
// SAFETY: both buffers have at least `len` (<= 2) elements and outlive
// the call.
let ok = unsafe {
font.glyphs_for_characters(
NonNull::new_unchecked(unichars.as_mut_ptr()),
NonNull::new_unchecked(glyphs.as_mut_ptr()),
len as isize,
)
};
(glyphs[0], ok)
}
/// A font variation axis as reported by CoreText.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Axis {
pub name: String,
pub id: u32,
pub min: f64,
pub max: f64,
pub default: f64,
}
/// The variation axes of a font (`kCTFontVariationAxesAttribute`), or `None`
/// if the font has none.
pub(crate) fn variation_axes(font: &CTFont) -> Option<Vec<Axis>> {
use objc2_core_foundation::{CFArray, CFDictionary, CFNumber, CFType};
use objc2_core_text::{
kCTFontVariationAxisDefaultValueKey,
kCTFontVariationAxisIdentifierKey,
kCTFontVariationAxisMaximumValueKey,
kCTFontVariationAxisMinimumValueKey,
kCTFontVariationAxisNameKey,
};
// SAFETY: plain CoreText call.
let axes = unsafe { font.variation_axes() }?;
// SAFETY: the documented element type of the axes array is CFDictionary.
let axes: &CFArray<CFDictionary> = unsafe { axes.cast_unchecked() };
let mut out = Vec::with_capacity(axes.len());
for i in 0..axes.len() {
let Some(dict) = axes.get(i) else { continue };
// SAFETY: the values stored under these keys are CFNumber / CFString
// as documented for the variation axis keys.
let (names, nums) = unsafe {
(
dict.cast_unchecked::<CFString, CFString>(),
dict.cast_unchecked::<CFString, CFNumber>(),
)
};
// SAFETY: the keys are valid static CFStrings.
let (k_name, k_id, k_min, k_max, k_def) = unsafe {
(
kCTFontVariationAxisNameKey,
kCTFontVariationAxisIdentifierKey,
kCTFontVariationAxisMinimumValueKey,
kCTFontVariationAxisMaximumValueKey,
kCTFontVariationAxisDefaultValueKey,
)
};
let _ = std::marker::PhantomData::<CFType>;
let name = names.get(k_name).map(|s| s.to_string()).unwrap_or_default();
let id = nums
.get(k_id)
.and_then(|n| n.as_i32())
.map_or(0, |v| v as u32);
let min = nums.get(k_min).and_then(|n| n.as_f64()).unwrap_or(0.0);
let max = nums.get(k_max).and_then(|n| n.as_f64()).unwrap_or(0.0);
let default = nums.get(k_def).and_then(|n| n.as_f64()).unwrap_or(0.0);
out.push(Axis {
name,
id,
min,
max,
default,
});
}
Some(out)
}
/// The current value of each variation axis of the font instance
/// (`kCTFontVariationAttribute`), keyed by axis id.
pub(crate) fn variation_values(font: &CTFont) -> Option<std::collections::HashMap<u32, f64>> {
use objc2_core_foundation::{CFDictionary, CFNumber};
// SAFETY: plain CoreText call.
let values = unsafe { font.variation() }?;
// SAFETY: the variation dictionary maps CFNumber axis ids to CFNumber
// values.
let values: &CFDictionary<CFNumber, CFNumber> = unsafe { values.cast_unchecked() };
let (keys, vals) = values.to_vecs();
Some(
keys.iter()
.zip(vals.iter())
.filter_map(|(k, v)| Some((k.as_i32()? as u32, v.as_f64()?)))
.collect(),
)
}
+1 -1
View File
@@ -20,7 +20,7 @@ pub enum LoadError {
#[error(transparent)]
Fontconfig(#[from] fc::Error),
#[error(transparent)]
Face(#[from] face::freetype::FaceError),
Face(#[from] face::FaceError),
}
/// Fontconfig specific data. This is only present if building with fontconfig.
@@ -0,0 +1,202 @@
//! A deferred face represents a single font face with all the information
//! necessary to load it, but defers loading the full face until it is
//! needed.
//!
//! This allows us to have many fallback fonts to look for glyphs, but
//! only load them if they're really needed.
//!
//! Port of `font/DeferredFace.zig` (CoreText variant; Apple targets).
#![allow(unsafe_code)]
use objc2_core_foundation::CFRetained;
use objc2_core_text::{CTFont, CTFontSymbolicTraits};
use crate::{
Face,
Library,
Presentation,
coretext_util::{self as util, CfBox},
face::{self, Options, Variation},
};
#[derive(Debug, thiserror::Error)]
pub enum LoadError {
#[error(transparent)]
Face(#[from] face::FaceError),
/// `coretext_freetype`: the font has no file we could hand to FreeType.
#[error("font has no file")]
FontHasNoFile,
/// `coretext_freetype`: the font path could not be decoded.
#[error("font path can't be decoded")]
FontPathCantDecode,
}
/// CoreText specific data.
pub struct CoreText {
/// The initialized font
pub font: CfBox<CTFont>,
/// Variations to apply to this font. We apply the variations to the
/// search descriptor but sometimes when the font collection is
/// made the variation axes are reset so we have to reapply them.
pub variations: Vec<Variation>,
}
#[derive(Default)]
pub struct DeferredFace {
/// CoreText
pub ct: Option<CoreText>,
}
impl DeferredFace {
pub fn from_coretext(font: CFRetained<CTFont>, variations: Vec<Variation>) -> DeferredFace {
DeferredFace {
ct: Some(CoreText {
font: CfBox::new(font),
variations,
}),
}
}
/// Returns the family name of the font.
pub fn family_name(&self) -> Result<String, LoadError> {
if let Some(ct) = &self.ct {
// SAFETY: plain CoreText call.
return Ok(util::cf_to_string(&*unsafe { ct.font.family_name() }));
}
Ok(String::new())
}
/// Returns the name of this face.
pub fn name(&self) -> Result<String, LoadError> {
if let Some(ct) = &self.ct {
// SAFETY: plain CoreText call.
return Ok(util::cf_to_string(&*unsafe { ct.font.display_name() }));
}
Ok(String::new())
}
/// Load the deferred font face.
#[cfg(not(font_freetype))]
pub fn load(&mut self, _lib: &Library, opts: Options) -> Result<Face, LoadError> {
let ct = self
.ct
.as_ref()
.expect("deferred face has no coretext data");
let mut face = Face::init_font_copy(&ct.font, opts)?;
face.set_variations(&ct.variations, opts)?;
Ok(face)
}
/// Load the deferred font face (`coretext_freetype`: FreeType renders the
/// file CoreText found).
#[cfg(font_freetype)]
pub fn load(&mut self, lib: &Library, opts: Options) -> Result<Face, LoadError> {
use objc2_core_foundation::{CFString, CFURL};
use objc2_core_text::kCTFontURLAttribute;
let ct = self
.ct
.as_ref()
.expect("deferred face has no coretext data");
// Get the URL for the font so we can get the filepath
// SAFETY: valid static CFString key.
let url =
unsafe { ct.font.attribute(kCTFontURLAttribute) }.ok_or(LoadError::FontHasNoFile)?;
let url = url
.downcast::<CFURL>()
.map_err(|_| LoadError::FontHasNoFile)?;
// Get the path from the URL
let path = url.path().ok_or(LoadError::FontHasNoFile)?;
// URL decode the path
let blank = CFString::from_str("");
let decoded =
CFURL::new_string_by_replacing_percent_escapes(None, Some(&path), Some(&blank))
.ok_or(LoadError::FontPathCantDecode)?;
let path = util::cf_to_string(&decoded);
// Face index 0 is not always correct. We don't ship this configuration
// in a release build. Users should use the pure CoreText builds.
let mut face = Face::init_file(lib, &path, 0, opts)?;
face.set_variations(&ct.variations, opts)?;
Ok(face)
}
/// Returns true if this face can satisfy the given codepoint and
/// presentation. If presentation is null, then it just checks if the
/// codepoint is present at all.
///
/// This does not require the face to be loaded (the CT font is used).
pub fn has_codepoint(&self, cp: u32, p: Option<Presentation>) -> bool {
// If we are using coretext, we check the loaded CT font.
let Some(ct) = &self.ct else {
// This is unreachable because discovery mechanisms terminate, and
// if we're not using a discovery mechanism, the face MUST be loaded.
unreachable!("deferred face without coretext data");
};
// This presentation check isn't as detailed as isColorGlyph
// because forced presentation modes are only used for emoji and
// emoji should always have color glyphs set. This can be
// more correct by using the isColorGlyph logic but I'd want
// to find a font that actually requires this so we can write
// a test for it before changing it.
if let Some(desired_p) = p {
// SAFETY: plain CoreText call.
let traits = unsafe { ct.font.symbolic_traits() };
let actual_p = if traits.contains(CTFontSymbolicTraits::TraitColorGlyphs) {
Presentation::Emoji
} else {
Presentation::Text
};
if actual_p != desired_p {
return false;
}
}
util::glyphs_for_codepoint(&ct.font, cp).1
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discovery::{Descriptor, Discover};
#[test]
fn coretext() {
// Get a deferred face from CoreText
let mut def = {
let disco = Discover::init();
let mut it = disco
.discover(&Descriptor {
family: Some("Monaco".into()),
size: 12.0,
..Default::default()
})
.unwrap();
it.next().unwrap().unwrap()
};
// Verify we can get the name
let n = def.name().unwrap();
assert_ne!("", n);
// Load it and verify it works
let lib = Library::init().unwrap();
let face = def
.load(&lib, Options::new(face::DesiredSize::new(12.0)))
.unwrap();
assert!(face.glyph_index(' ' as u32).is_some());
}
#[test]
fn empty_deferred_face_has_empty_names() {
let def = DeferredFace::default();
assert_eq!("", def.family_name().unwrap());
assert_eq!("", def.name().unwrap());
}
}
+140 -122
View File
@@ -1,144 +1,160 @@
//! Font discovery. Port of `font/discovery.zig` (fontconfig backend only;
//! the CoreText discoverer is macOS-only and out of scope).
//! Font discovery. Port of `font/discovery.zig`: the fontconfig backend
//! (Linux, in this file) and the CoreText backend (Apple targets, in
//! `discovery_coretext.rs`).
use std::sync::Arc;
use crate::{
collection::Collection,
deferred_face::{DeferredFace, Fontconfig as DeferredFontconfig},
face::Variation,
fontconfig as fc,
};
/// Discover implementation for the compile options.
pub type Discover = Fontconfig;
#[cfg(font_fontconfig)]
pub use fc_impl::*;
#[cfg(font_coretext)]
#[path = "discovery_coretext.rs"]
mod coretext_impl;
#[cfg(font_coretext)]
pub use coretext_impl::*;
/// Descriptor is used to search for fonts (canonical definition in `ghostty-config`).
pub use ghostty_config::font_types::Descriptor;
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this. (`Descriptor.toFcPattern`)
pub fn to_fc_pattern(desc: &Descriptor) -> fc::Pattern {
let mut pat = fc::Pattern::create();
if let Some(family) = &desc.family {
assert!(pat.add_string(fc::Property::Family, family));
}
if let Some(style) = &desc.style {
assert!(pat.add_string(fc::Property::Style, style));
}
if desc.codepoint > 0 {
let mut cs = fc::CharSet::create();
assert!(cs.add_char(desc.codepoint));
assert!(pat.add_charset(fc::Property::Charset, &cs));
}
if desc.size > 0.0 {
assert!(pat.add_integer(fc::Property::Size, desc.size.round() as i32));
}
if desc.bold {
assert!(pat.add_integer(fc::Property::Weight, fc::Weight::Bold as i32));
}
if desc.italic {
assert!(pat.add_integer(fc::Property::Slant, fc::Slant::Italic as i32));
#[cfg(font_fontconfig)]
mod fc_impl {
use std::sync::Arc;
use super::Descriptor;
use crate::{
collection::Collection,
deferred_face::{DeferredFace, Fontconfig as DeferredFontconfig},
face::Variation,
fontconfig as fc,
};
/// Discover implementation for the compile options.
pub type Discover = Fontconfig;
/// The error type of [`DiscoverIterator::next`].
pub type NextError = fc::Error;
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this. (`Descriptor.toFcPattern`)
pub fn to_fc_pattern(desc: &Descriptor) -> fc::Pattern {
let mut pat = fc::Pattern::create();
if let Some(family) = &desc.family {
assert!(pat.add_string(fc::Property::Family, family));
}
if let Some(style) = &desc.style {
assert!(pat.add_string(fc::Property::Style, style));
}
if desc.codepoint > 0 {
let mut cs = fc::CharSet::create();
assert!(cs.add_char(desc.codepoint));
assert!(pat.add_charset(fc::Property::Charset, &cs));
}
if desc.size > 0.0 {
assert!(pat.add_integer(fc::Property::Size, desc.size.round() as i32));
}
if desc.bold {
assert!(pat.add_integer(fc::Property::Weight, fc::Weight::Bold as i32));
}
if desc.italic {
assert!(pat.add_integer(fc::Property::Slant, fc::Slant::Italic as i32));
}
// For fontconfig, we always add monospace in the pattern. Since
// fontconfig sorts by closeness to the pattern, this doesn't fully
// exclude non-monospace but helps prefer it.
assert!(pat.add_integer(fc::Property::Spacing, fc::Spacing::Mono as i32));
pat
}
// For fontconfig, we always add monospace in the pattern. Since
// fontconfig sorts by closeness to the pattern, this doesn't fully
// exclude non-monospace but helps prefer it.
assert!(pat.add_integer(fc::Property::Spacing, fc::Spacing::Mono as i32));
#[derive(Clone)]
pub struct Fontconfig {
fc_config: Arc<fc::Config>,
}
pat
}
#[derive(Debug, thiserror::Error)]
pub enum DiscoverError {
#[error("fontconfig failed")]
FontConfigFailed,
#[error(transparent)]
Fontconfig(#[from] fc::Error),
}
#[derive(Clone)]
pub struct Fontconfig {
fc_config: Arc<fc::Config>,
}
impl Fontconfig {
pub fn init() -> Fontconfig {
// safe to call multiple times and concurrently
fc::init();
Fontconfig {
fc_config: Arc::new(
fc::Config::init_load_config_and_fonts()
.expect("FcInitLoadConfigAndFonts returned null"),
),
}
}
#[derive(Debug, thiserror::Error)]
pub enum DiscoverError {
#[error("fontconfig failed")]
FontConfigFailed,
#[error(transparent)]
Fontconfig(#[from] fc::Error),
}
/// Discover fonts from a descriptor. This returns an iterator that can
/// be used to build up the deferred fonts.
pub fn discover(&self, desc: &Descriptor) -> Result<DiscoverIterator, DiscoverError> {
// Build our pattern that we'll search for
let mut pat = to_fc_pattern(desc);
assert!(self.fc_config.substitute_with_pat_pattern(&mut pat));
pat.default_substitute();
impl Fontconfig {
pub fn init() -> Fontconfig {
// safe to call multiple times and concurrently
fc::init();
Fontconfig {
fc_config: Arc::new(
fc::Config::init_load_config_and_fonts()
.expect("FcInitLoadConfigAndFonts returned null"),
),
// Search
let set = self
.fc_config
.font_sort(&mut pat, false)
.map_err(|_| DiscoverError::FontConfigFailed)?;
Ok(DiscoverIterator {
config: Arc::clone(&self.fc_config),
pattern: pat,
set,
variations: desc.variations.clone(),
i: 0,
})
}
pub fn discover_fallback(
&self,
_collection: &mut Collection,
desc: &Descriptor,
) -> Result<DiscoverIterator, DiscoverError> {
self.discover(desc)
}
}
/// Discover fonts from a descriptor. This returns an iterator that can
/// be used to build up the deferred fonts.
pub fn discover(&self, desc: &Descriptor) -> Result<DiscoverIterator, DiscoverError> {
// Build our pattern that we'll search for
let mut pat = to_fc_pattern(desc);
assert!(self.fc_config.substitute_with_pat_pattern(&mut pat));
pat.default_substitute();
// Search
let set = self
.fc_config
.font_sort(&mut pat, false)
.map_err(|_| DiscoverError::FontConfigFailed)?;
Ok(DiscoverIterator {
config: Arc::clone(&self.fc_config),
pattern: pat,
set,
variations: desc.variations.clone(),
i: 0,
})
pub struct DiscoverIterator {
config: Arc<fc::Config>,
pattern: fc::Pattern,
set: fc::FontSet,
variations: Vec<Variation>,
i: usize,
}
pub fn discover_fallback(
&self,
_collection: &Collection,
desc: &Descriptor,
) -> Result<DiscoverIterator, DiscoverError> {
self.discover(desc)
}
}
impl DiscoverIterator {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<DeferredFace>, fc::Error> {
if self.i >= self.set.len() {
return Ok(None);
}
pub struct DiscoverIterator {
config: Arc<fc::Config>,
pattern: fc::Pattern,
set: fc::FontSet,
variations: Vec<Variation>,
i: usize,
}
// Get the copied pattern from our fontset that has the
// attributes configured for rendering.
let font_pattern =
self.config
.font_render_prepare(&mut self.pattern, &self.set, self.i)?;
impl DiscoverIterator {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<DeferredFace>, fc::Error> {
if self.i >= self.set.len() {
return Ok(None);
// The charset and langset must exist (they are used for lookups).
font_pattern.charset_has_char(0)?;
font_pattern.langset_has_lang("")?;
// Increment after we return
self.i += 1;
Ok(Some(DeferredFace::from_fontconfig(DeferredFontconfig {
pattern: font_pattern,
variations: self.variations.clone(),
})))
}
// Get the copied pattern from our fontset that has the
// attributes configured for rendering.
let font_pattern = self
.config
.font_render_prepare(&mut self.pattern, &self.set, self.i)?;
// The charset and langset must exist (they are used for lookups).
font_pattern.charset_has_char(0)?;
font_pattern.langset_has_lang("")?;
// Increment after we return
self.i += 1;
Ok(Some(DeferredFace::from_fontconfig(DeferredFontconfig {
pattern: font_pattern,
variations: self.variations.clone(),
})))
}
}
@@ -178,6 +194,7 @@ mod tests {
assert_ne!(a.hashcode(), b.hashcode());
}
#[cfg(font_fontconfig)]
#[test]
fn fontconfig() {
let fc = Fontconfig::init();
@@ -190,6 +207,7 @@ mod tests {
.unwrap();
}
#[cfg(font_fontconfig)]
#[test]
fn fontconfig_codepoint() {
let fc = Fontconfig::init();
@@ -0,0 +1,525 @@
//! CoreText font discovery (Apple targets). Port of the `CoreText` struct in
//! `font/discovery.zig` plus `Descriptor.toCoreTextDescriptor`.
#![allow(unsafe_code)]
use std::ptr;
use objc2_core_foundation::{
CFArray,
CFCharacterSet,
CFDictionary,
CFNumber,
CFRange,
CFRetained,
CFString,
CFType,
kCFNull,
};
use objc2_core_text::{
CTFont,
CTFontCollection,
CTFontDescriptor,
kCTFontCharacterSetAttribute,
kCTFontFamilyNameAttribute,
kCTFontSizeAttribute,
kCTFontStyleNameAttribute,
kCTFontSymbolicTrait,
kCTFontTraitsAttribute,
};
use super::Descriptor;
use crate::{
Style,
collection::{Collection, Index},
coretext_util::{self as util, CfBox},
deferred_face::DeferredFace,
face::{
Variation,
coretext_math::{self as math, Score},
},
opentype,
};
/// Discover implementation for the compile options.
pub type Discover = CoreText;
/// The error type of [`DiscoverIterator::next`]. CoreText iteration can't
/// fail, so this is uninhabited.
#[derive(Debug, thiserror::Error)]
pub enum NextError {}
#[derive(Debug, thiserror::Error)]
pub enum DiscoverError {
#[error("coretext descriptor creation failed")]
DescriptorFailed,
}
/// Retain a value as a `CFType`.
fn as_type<T: AsRef<CFType> + objc2_core_foundation::Type>(v: &T) -> CFRetained<CFType> {
let r: &CFType = v.as_ref();
objc2_core_foundation::Type::retain(r)
}
/// Convert to Core Text font descriptor to use for lookup or
/// conversion to a specific font (`Descriptor.toCoreTextDescriptor`).
pub fn to_coretext_descriptor(
desc: &Descriptor,
) -> Result<CFRetained<CTFontDescriptor>, DiscoverError> {
let mut keys: Vec<&CFString> = Vec::new();
// Owned values; we collect references below.
let mut values: Vec<CFRetained<CFType>> = Vec::new();
// Family
if let Some(family) = &desc.family {
let family = CFString::from_str(family);
// SAFETY: valid static key.
keys.push(unsafe { kCTFontFamilyNameAttribute });
values.push(as_type(&*family));
}
// Style
if let Some(style) = &desc.style {
let style = CFString::from_str(style);
// SAFETY: valid static key.
keys.push(unsafe { kCTFontStyleNameAttribute });
values.push(as_type(&*style));
}
// Codepoint support
if desc.codepoint > 0 {
// SAFETY: plain CoreFoundation call with a valid range.
let cs = unsafe {
CFCharacterSet::with_characters_in_range(
None,
CFRange {
location: desc.codepoint as isize,
length: 1,
},
)
}
.ok_or(DiscoverError::DescriptorFailed)?;
// SAFETY: valid static key.
keys.push(unsafe { kCTFontCharacterSetAttribute });
values.push(as_type(&*cs));
}
// Set our size attribute if set
if desc.size > 0.0 {
let size = CFNumber::new_i32(desc.size.round() as i32);
// SAFETY: valid static key.
keys.push(unsafe { kCTFontSizeAttribute });
values.push(as_type(&*size));
}
// Build our traits. If we set any, then we store it in the attributes
// otherwise we do nothing.
let traits = math::symbolic_traits_value(desc.bold, desc.italic, desc.monospace);
if traits > 0 {
// Setting traits is a pain. We have to create a nested dictionary
// of the symbolic traits value, and set that in our attributes.
let traits_num = CFNumber::new_i32(traits as i32);
// SAFETY: valid static key.
let k = unsafe { kCTFontSymbolicTrait };
let nested =
CFDictionary::<CFString, CFType>::from_slices(&[k], &[&*as_type(&*traits_num)]);
// SAFETY: valid static key.
keys.push(unsafe { kCTFontTraitsAttribute });
values.push(as_type(&*nested));
}
let value_refs: Vec<&CFType> = values.iter().map(|v| &**v).collect();
let attrs = CFDictionary::<CFString, CFType>::from_slices(&keys, &value_refs);
// SAFETY: valid attributes dictionary.
Ok(unsafe { CTFontDescriptor::with_attributes(attrs.as_opaque()) })
}
/// The CoreText discoverer. It has no state.
#[derive(Debug, Clone, Default)]
pub struct CoreText;
impl CoreText {
pub fn init() -> CoreText {
// Required for the "interface" but does nothing for CoreText.
CoreText
}
/// Discover fonts from a descriptor. This returns an iterator that can
/// be used to build up the deferred fonts.
pub fn discover(&self, desc: &Descriptor) -> Result<DiscoverIterator, DiscoverError> {
// Build our pattern that we'll search for
let ct_desc = to_coretext_descriptor(desc)?;
// Our descriptors have to be in an array
let desc_arr = CFArray::from_objects(&[&*ct_desc]);
// Build our collection
// SAFETY: valid array, no options.
let set =
unsafe { CTFontCollection::with_font_descriptors(Some(desc_arr.as_opaque()), None) };
// SAFETY: plain CoreText call.
let list = unsafe { set.matching_font_descriptors() };
// Copy and sort our descriptors
let mut items: Vec<CFRetained<CTFontDescriptor>> = match list {
Some(list) => {
// SAFETY: the matching list contains CTFontDescriptors.
let typed: &CFArray<CTFontDescriptor> = unsafe { list.cast_unchecked() };
typed.to_vec()
}
None => Vec::new(),
};
sort_matching_descriptors(desc, &mut items);
Ok(DiscoverIterator {
list: items.into_iter().map(CfBox::new).collect(),
variations: desc.variations.clone(),
i: 0,
})
}
pub fn discover_fallback(
&self,
collection: &mut Collection,
desc: &Descriptor,
) -> Result<DiscoverIterator, DiscoverError> {
// If we have a codepoint within the CJK unified ideographs block
// then we fallback to macOS to find a font that supports it because
// there isn't a better way manually with CoreText that I can find that
// properly takes into account system locale.
//
// References:
// - http://unicode.org/charts/PDF/U4E00.pdf
// - https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/platform/fonts/LocaleInFonts.md#unified-han-ideographs
if (0x4E00..=0x9FFF).contains(&desc.codepoint)
&& let Some(han) = self.discover_codepoint(collection, desc)
{
return Ok(DiscoverIterator {
list: vec![CfBox::new(han)],
variations: desc.variations.clone(),
i: 0,
});
}
let it = self.discover(desc)?;
// If our normal discovery doesn't find anything and we have a specific
// codepoint, then fallback to using CTFontCreateForString to find a
// matching font CoreText wants to use. See:
// https://github.com/ghostty-org/ghostty/issues/2499
if it.list.is_empty()
&& desc.codepoint > 0
&& let Some(ct_desc) = self.discover_codepoint(collection, desc)
{
return Ok(DiscoverIterator {
list: vec![CfBox::new(ct_desc)],
variations: desc.variations.clone(),
i: 0,
});
}
Ok(it)
}
/// Discover a font for a specific codepoint using the CoreText
/// CTFontCreateForString API.
#[cfg(font_freetype)]
#[allow(clippy::unused_self)]
fn discover_codepoint(
&self,
_collection: &mut Collection,
_desc: &Descriptor,
) -> Option<CFRetained<CTFontDescriptor>> {
// If we have freetype, we can't use CoreText to find a font
// that supports a specific codepoint because we need to
// have a CoreText font to be able to do so.
None
}
/// Discover a font for a specific codepoint using the CoreText
/// CTFontCreateForString API.
#[cfg(not(font_freetype))]
#[allow(clippy::unused_self)]
fn discover_codepoint(
&self,
collection: &mut Collection,
desc: &Descriptor,
) -> Option<CFRetained<CTFontDescriptor>> {
assert!(desc.codepoint > 0);
// Get our original font. This is dependent on the requested style
// from the descriptor.
let original_style = {
// In all the styles below, we try to match it but if we don't
// we always fall back to some other option. The order matters
// here.
if desc.bold && desc.italic && collection.count(Style::BoldItalic) > 0 {
Style::BoldItalic
} else if desc.bold && collection.count(Style::Bold) > 0 {
Style::Bold
} else if desc.italic && collection.count(Style::Italic) > 0 {
Style::Italic
} else {
Style::Regular
}
};
let original = collection
.get_face(Index {
style: original_style,
idx: 0,
})
.ok()?;
// We need a CFString for the character.
let ch = char::from_u32(desc.codepoint)?;
let str = CFString::from_str(ch.encode_utf8(&mut [0u8; 4]));
// Get our range length for CTFontCreateForString. It looks like
// the range uses UTF-16 codepoints and not UTF-32 codepoints.
let range_len = math::utf16_units(desc.codepoint).1;
// Get our font
// SAFETY: valid string and range.
let font = unsafe {
original.ct_font().for_string(
&str,
CFRange {
location: 0,
length: range_len as isize,
},
)
};
// Do not allow the last resort font to go through. This is the
// last font used by CoreText if it can't find anything else and
// only contains replacement characters.
// SAFETY: plain CoreText call.
let name = util::cf_to_string(&*unsafe { font.post_script_name() });
// If the name is "LastResort" then we don't want to use it.
if name == "LastResort" {
return None;
}
// Get the descriptor
// SAFETY: plain CoreText call.
Some(unsafe { font.font_descriptor() })
}
}
fn sort_matching_descriptors(desc: &Descriptor, list: &mut [CFRetained<CTFontDescriptor>]) {
// Compute the scores once. Higher score is "less" (earlier).
let mut scored: Vec<(u32, CFRetained<CTFontDescriptor>)> = list
.iter()
.map(|d| (score(desc, d).int(), d.clone()))
.collect();
scored.sort_by_key(|a| std::cmp::Reverse(a.0));
for (slot, (_, d)) in list.iter_mut().zip(scored) {
*slot = d;
}
}
/// Score a CoreText descriptor against the requested descriptor
/// (`Score.score`).
fn score(desc: &Descriptor, ct_desc: &CTFontDescriptor) -> Score {
let mut this = Score::default();
// We always load the font if we can since some things can only be
// inspected on the font itself.
// SAFETY: valid descriptor, null matrix is allowed.
let font: CFRetained<CTFont> =
unsafe { CTFont::with_font_descriptor(ct_desc, 12.0, ptr::null()) };
// We prefer fonts with more glyphs, all else being equal.
// SAFETY: plain CoreText call.
this.glyph_count = u16::try_from(unsafe { font.glyph_count() }).unwrap_or(u16::MAX);
// If we're searching for a codepoint, then we
// prioritize fonts that have that codepoint.
if desc.codepoint > 0 {
this.codepoint = util::glyphs_for_codepoint(&font, desc.codepoint).1;
}
// Get our symbolic traits for the descriptor so we can
// compare boolean attributes like bold, monospace, etc.
let symbolic_traits: u32 = (|| {
// SAFETY: valid static key.
let traits = unsafe { ct_desc.attribute(kCTFontTraitsAttribute) }?;
let traits = traits.downcast::<CFDictionary>().ok()?;
// SAFETY: the traits dictionary maps CFString keys to CFNumber values.
let traits: &CFDictionary<CFString, CFNumber> = unsafe { traits.cast_unchecked() };
// SAFETY: valid static key.
let symbolic = traits.get(unsafe { kCTFontSymbolicTrait })?;
symbolic.as_i32().map(|v| v as u32)
})()
.unwrap_or(0);
this.monospace = symbolic_traits & math::TRAIT_MONOSPACE != 0;
// We try to derived data from the font itself, which is generally
// more reliable than only using the symbolic traits for this.
let (is_bold, is_italic) = {
// We start with initial guesses based on the symbolic traits,
// but refine these with more information if we can get it.
let mut is_italic = symbolic_traits & math::TRAIT_ITALIC != 0;
let mut is_bold = symbolic_traits & math::TRAIT_BOLD != 0;
// Read the 'head' table out of the font data if it's available.
if let Some(head) = util::font_table_bytes(&font, b"head").and_then(|d| {
opentype::Head::init(&d)
.inspect_err(|e| log::warn!("error parsing head table: {e}"))
.ok()
}) {
is_bold = is_bold || (head.macStyle & 1 == 1);
is_italic = is_italic || (head.macStyle & 2 == 2);
}
// Read the 'OS/2' table out of the font data if it's available.
if let Some(os2) = util::font_table_bytes(&font, b"OS/2").and_then(|d| {
opentype::OS2::init(&d)
.inspect_err(|e| log::warn!("error parsing OS/2 table: {e}"))
.ok()
}) {
is_bold = is_bold || os2.fsSelection.bold;
is_italic = is_italic || os2.fsSelection.italic;
}
// Check if we have variation axes in our descriptor, if we
// do then we can derive weight italic-ness or both from them.
if let Some(axes) = util::variation_axes(&font) {
// Copy the variation values for this instance of the font.
// if there are none then we just skip.
if let Some(values) = util::variation_values(&font) {
let list = axes.into_iter().map(|axis| {
let val = values.get(&axis.id).copied().unwrap_or(axis.default);
(axis.name, val)
});
(is_bold, is_italic) = math::bold_italic_from_axes(is_bold, is_italic, list);
}
}
(is_bold, is_italic)
};
this.bold = desc.bold == is_bold;
this.italic = desc.italic == is_italic;
// Get the style string from the font.
let style_str: String = (|| {
// SAFETY: valid static key.
let style = unsafe { ct_desc.attribute(kCTFontStyleNameAttribute) }?;
style.downcast::<CFString>().ok().map(|s| s.to_string())
})()
.unwrap_or_default();
// The first string in this slice will be used for the exact match,
// and for the fuzzy match, all matching substrings will increase
// the rank.
let desired_styles = math::desired_styles(desc.style.as_deref(), desc.bold, desc.italic);
let (exact, fuzzy) = math::style_scores(&style_str, &desired_styles);
this.exact_style = exact;
this.fuzzy_style = fuzzy;
this
}
pub struct DiscoverIterator {
list: Vec<CfBox<CTFontDescriptor>>,
variations: Vec<Variation>,
i: usize,
}
impl DiscoverIterator {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<DeferredFace>, NextError> {
if self.i >= self.list.len() {
return Ok(None);
}
// Get our descriptor. We need to remove the character set
// limitation because we may have used that to filter but we
// don't want it anymore because it'll restrict the characters
// available.
let desc = {
// We create a copy, overwriting the character set attribute.
// SAFETY: reading the extern static constant.
let null: &CFType = match unsafe { kCFNull } {
Some(n) => n.as_ref(),
None => unreachable!("kCFNull is never null"),
};
// SAFETY: valid static key.
let key = unsafe { kCTFontCharacterSetAttribute };
let attrs = CFDictionary::<CFString, CFType>::from_slices(&[key], &[null]);
// SAFETY: valid dictionary.
unsafe { self.list[self.i].copy_with_attributes(attrs.as_opaque()) }
};
// Create our font. We need a size to initialize it so we use size
// 12 but we will alter the size later.
// SAFETY: valid descriptor, null matrix is allowed.
let font = unsafe { CTFont::with_font_descriptor(&desc, 12.0, ptr::null()) };
// Increment after we return
self.i += 1;
Ok(Some(DeferredFace::from_coretext(
font,
self.variations.clone(),
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coretext() {
let ct = CoreText::init();
let mut it = ct
.discover(&Descriptor {
family: Some("Monaco".into()),
size: 12.0,
..Default::default()
})
.unwrap();
let face = it.next().unwrap().unwrap();
assert!(face.has_codepoint('A' as u32, None));
}
#[test]
fn coretext_codepoint() {
let ct = CoreText::init();
let mut it = ct
.discover(&Descriptor {
codepoint: 'A' as u32,
size: 12.0,
..Default::default()
})
.unwrap();
// The first result should have the codepoint. Later ones may not
// because the results are sorted.
let face = it.next().unwrap().unwrap();
assert!(face.has_codepoint('A' as u32, None));
// Should have other codepoints too
assert!(face.has_codepoint('B' as u32, None));
}
#[test]
fn coretext_sorting() {
let ct = CoreText::init();
let mut it = ct
.discover(&Descriptor {
family: Some("Menlo".into()),
size: 12.0,
..Default::default()
})
.unwrap();
// Exercise the scoring/sorting path; the first result for the
// regular style must be the regular face.
let first = it.next().unwrap().unwrap();
assert!(first.name().unwrap().contains("Menlo"));
}
}
+17 -4
View File
@@ -1,6 +1,7 @@
//! Face types shared by all face implementations. Port of `font/face.zig`.
//!
//! The concrete FreeType face lives in [`freetype`] (`font/face/freetype.zig`).
//! The concrete FreeType face lives in [`freetype`] (`font/face/freetype.zig`),
//! the CoreText face (Apple targets) in [`coretext`] (`font/face/coretext.zig`).
// `(a + b) / 2` is kept verbatim from upstream (f64::midpoint rounds differently).
#![allow(clippy::manual_midpoint)]
@@ -9,14 +10,23 @@ use std::sync::Arc;
use crate::metrics::Metrics;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(font_coretext_face)]
pub mod coretext;
pub mod coretext_math;
#[cfg(font_freetype)]
pub mod freetype;
// The pure (non-JS) parts of the web_canvas face compile everywhere so they
// can be unit tested natively; the JS-backed `Face` is wasm32-only.
pub mod web_canvas;
#[cfg(not(target_arch = "wasm32"))]
pub use freetype::Face;
#[cfg(all(font_coretext_face, font_harfbuzz))]
pub use coretext::HbFont;
#[cfg(font_coretext_face)]
pub use coretext::{Face, FaceError, RenderError};
#[cfg(font_freetype)]
pub use freetype::HbFont;
#[cfg(font_freetype)]
pub use freetype::{Face, FaceError, RenderError};
pub use ghostty_config::types_b::FreetypeLoadFlags;
#[cfg(target_arch = "wasm32")]
pub use web_canvas::Face;
@@ -24,6 +34,9 @@ pub use web_canvas::Face;
/// If a DPI can't be calculated, this DPI is used. This is probably
/// wrong on modern devices so it is highly recommended you get the DPI
/// using whatever platform method you can.
#[cfg(target_os = "macos")]
pub const DEFAULT_DPI: u16 = 72;
#[cfg(not(target_os = "macos"))]
pub const DEFAULT_DPI: u16 = 96;
pub const FREETYPE_LOAD_FLAGS_DEFAULT: FreetypeLoadFlags = FreetypeLoadFlags {
+884
View File
@@ -0,0 +1,884 @@
//! CoreText font face (macOS / iOS). Port of `font/face/coretext.zig`.
//!
//! Compile-checked on Linux with `cargo check --target aarch64-apple-darwin`;
//! it cannot be run here. The platform-independent math (metrics priority,
//! glyph bearings/canvas size, synthetic bold width) lives in
//! [`super::coretext_math`] and is unit tested everywhere.
#![allow(unsafe_code)]
use std::ptr::{self, NonNull};
use objc2_core_foundation::{CFData, CFNumber, CFRetained, CFString, CGAffineTransform, CGPoint};
use objc2_core_graphics::{
CGBitmapContextCreate,
CGColorSpace,
CGContext,
CGImageAlphaInfo,
CGImageByteOrderInfo,
CGTextDrawingMode,
kCGColorSpaceDisplayP3,
kCGColorSpaceLinearGray,
};
use objc2_core_text::{CTFont, CTFontOrientation, CTFontSymbolicTraits};
use super::{
DesiredSize,
FontData,
Options,
RenderOptions,
Variation,
coretext_math::{self as math, CtMetrics, Measurements, Rect, synthetic_bold_line_width},
};
use crate::{
atlas::{self, Atlas},
coretext_util::{self as util, CfBox},
glyph::Glyph,
library::Library,
metrics::FaceMetrics,
opentype,
};
#[derive(Debug, thiserror::Error)]
pub enum FaceError {
/// CoreText could not create a font from the given data.
#[error("font initialization failed")]
FontInitFailure,
/// The `SVG ` table is malformed.
#[error("invalid SVG table")]
InvalidSvgTable,
/// The face has no backing file (used by the `coretext_freetype` backend).
#[error("font has no file")]
FontHasNoFile,
/// The font file path could not be decoded.
#[error("font path can't be decoded")]
FontPathCantDecode,
/// Creating a font variation failed.
#[error("variation failed")]
VariationFailed,
}
#[derive(Debug, thiserror::Error)]
pub enum RenderError {
#[error(transparent)]
Atlas(#[from] atlas::Error),
/// The atlas format does not match the glyph's color depth.
#[error("invalid atlas format")]
InvalidAtlasFormat,
/// A CoreGraphics color space or bitmap context could not be created.
#[error("coregraphics resource creation failed")]
ContextFailed,
/// The glyph index doesn't fit a CoreText `CGGlyph` (16 bits).
#[error("invalid glyph index")]
InvalidGlyph,
}
/// A harfbuzz font owned by a [`Face`] (only used by the
/// `coretext_harfbuzz` backend).
#[cfg(font_harfbuzz)]
pub struct HbFont(NonNull<harfbuzz_sys::hb_font_t>);
// SAFETY: hb_font_t is thread-safe for shaping when not mutated; Ghostty uses
// it behind the collection lock exactly like upstream.
#[cfg(font_harfbuzz)]
unsafe impl Send for HbFont {}
// SAFETY: see above.
#[cfg(font_harfbuzz)]
unsafe impl Sync for HbFont {}
#[cfg(font_harfbuzz)]
impl HbFont {
/// The raw `hb_font_t` pointer (valid while the `Face` lives).
pub fn as_ptr(&self) -> *mut harfbuzz_sys::hb_font_t {
self.0.as_ptr()
}
}
#[cfg(font_harfbuzz)]
impl Drop for HbFont {
fn drop(&mut self) {
// SAFETY: we own one reference to the hb font.
unsafe { harfbuzz_sys::hb_font_destroy(self.0.as_ptr()) };
}
}
pub struct Face {
/// Our font face
font: CfBox<CTFont>,
/// Harfbuzz font corresponding to this face. We only use this
/// if we're using Harfbuzz.
#[cfg(font_harfbuzz)]
hb_font: HbFont,
/// Set quirks.disableDefaultFontFeatures
pub quirks_disable_default_font_features: bool,
/// True if this font face should be rasterized with a synthetic bold
/// effect. This is used for fonts that don't have a bold variant.
pub synthetic_bold: Option<f64>,
/// If the face can possibly be colored, then this is the state
/// used to check for color information. This is None if the font
/// can't possibly be colored (i.e. doesn't have SVG, sbix, etc
/// tables).
color: Option<ColorState>,
/// The current size this font is set to.
pub size: DesiredSize,
}
impl std::fmt::Debug for Face {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Face")
.field("size", &self.size)
.field("synthetic_bold", &self.synthetic_bold)
.finish_non_exhaustive()
}
}
/// The matrix applied to a regular font to auto-italicize it.
fn italic_skew() -> CGAffineTransform {
CGAffineTransform {
a: 1.0,
b: 0.0,
c: math::ITALIC_SKEW_C, // approx. tan(15)
d: 1.0,
tx: 0.0,
ty: 0.0,
}
}
impl Face {
/// Initialize a CoreText-based font from a TTF/TTC in memory.
pub fn init(_lib: &Library, source: FontData, opts: Options) -> Result<Face, FaceError> {
let data = match source {
FontData::Static(s) => CFData::from_static_bytes(s),
FontData::Shared(s) => CFData::from_bytes(&s),
};
// SAFETY: `data` is a valid CFData.
let desc = unsafe { objc2_core_text::CTFontManagerCreateFontDescriptorFromData(&data) }
.ok_or(FaceError::FontInitFailure)?;
// SAFETY: valid descriptor, null matrix is allowed.
let ct_font = unsafe { CTFont::with_font_descriptor(&desc, 12.0, ptr::null()) };
Self::init_font_copy(&ct_font, opts)
}
/// Initialize a CoreText-based face from another initialized font face
/// but with a new size. This is often how CoreText fonts are initialized
/// because the font is loaded at a default size during discovery, and then
/// adjusted to the final size for final load.
pub fn init_font_copy(base: &CTFont, opts: Options) -> Result<Face, FaceError> {
// Create a copy. The copyWithAttributes docs say the size is in points,
// but we need to scale the points by the DPI and to do that we use our
// function called "pixels".
// SAFETY: null matrix and no descriptor are allowed.
let ct_font =
unsafe { base.copy_with_attributes(f64::from(opts.size.pixels()), ptr::null(), None) };
Self::init_font(ct_font, opts)
}
/// Initialize a face with a CTFont. This takes ownership of the CTFont.
pub fn init_font(ct_font: CFRetained<CTFont>, opts: Options) -> Result<Face, FaceError> {
// SAFETY: plain CoreText call.
let traits = unsafe { ct_font.symbolic_traits() };
#[cfg(font_harfbuzz)]
let hb_font = {
// SAFETY: valid CTFont; hb takes its own reference.
let p = unsafe { harfbuzz_sys::coretext::hb_coretext_font_create(&ct_font) };
let hb = NonNull::new(p)
.map(HbFont)
.ok_or(FaceError::FontInitFailure)?;
// 26.6 fixed point scale
let scale = (f64::from(opts.size.pixels()) * 64.0).round() as i32;
// SAFETY: valid hb font.
unsafe { harfbuzz_sys::hb_font_set_scale(hb.as_ptr(), scale, scale) };
hb
};
let color = if traits.contains(CTFontSymbolicTraits::TraitColorGlyphs) {
Some(ColorState::new(&ct_font)?)
} else {
None
};
let result = Face {
font: CfBox::new(ct_font),
#[cfg(font_harfbuzz)]
hb_font,
quirks_disable_default_font_features: false,
synthetic_bold: None,
color,
size: opts.size,
};
// In debug mode, we output information about available variation axes,
// if they exist.
if cfg!(debug_assertions)
&& let Some(axes) = util::variation_axes(&result.font)
{
log::debug!("variation axes font={}", result.name());
for axis in axes {
log::debug!(
"variation axis: name={} id={} min={} max={} def={}",
axis.name,
String::from_utf8_lossy(&axis.id.to_be_bytes()),
axis.min,
axis.max,
axis.default,
);
}
}
Ok(result)
}
/// Return a new face that is the same as this but has a transformation
/// matrix applied to italicize it.
pub fn synthetic_italic(&self, opts: Options) -> Result<Face, FaceError> {
let skew = italic_skew();
// SAFETY: `skew` outlives the call.
let ct_font = unsafe { self.font.copy_with_attributes(0.0, &raw const skew, None) };
Self::init_font(ct_font, opts)
}
/// Return a new face that is the same as this but applies a synthetic
/// bold effect to it. This is useful for fonts that don't have a bold
/// variant.
pub fn synthetic_bold(&self, opts: Options) -> Result<Face, FaceError> {
// SAFETY: null matrix and no descriptor are allowed.
let ct_font = unsafe { self.font.copy_with_attributes(0.0, ptr::null(), None) };
let mut face = Self::init_font(ct_font, opts)?;
face.synthetic_bold = Some(synthetic_bold_line_width(f64::from(opts.size.points)));
Ok(face)
}
/// Returns the font name (the family name).
pub fn name(&self) -> String {
// SAFETY: plain CoreText call.
util::cf_to_string(&*unsafe { self.font.family_name() })
}
/// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc.
pub fn set_size(&mut self, opts: Options) -> Result<(), FaceError> {
// We just create a copy and replace ourself
let face = Self::init_font_copy(&self.font, opts)?;
*self = face;
Ok(())
}
/// Set the variation axes for this font. This will modify this font
/// in-place.
pub fn set_variations(&mut self, vs: &[Variation], opts: Options) -> Result<(), FaceError> {
// If we have no variations, we don't need to do anything.
if vs.is_empty() {
return Ok(());
}
// Create a new font descriptor with all the variations set.
// SAFETY: plain CoreText call.
let mut desc = unsafe { self.font.font_descriptor() };
for v in vs {
let id = CFNumber::new_i32(v.id.0 as i32);
// SAFETY: valid number and descriptor.
desc = unsafe { desc.copy_with_variation(&id, v.value) };
}
// Put our current size in the opts so that we don't change size.
let mut new_opts = opts;
new_opts.size = self.size;
// Initialize a font based on these attributes.
// SAFETY: null matrix is allowed.
let ct_font = unsafe {
self.font
.copy_with_attributes(0.0, ptr::null(), Some(&desc))
};
let face = Self::init_font(ct_font, new_opts)?;
*self = face;
Ok(())
}
/// Returns true if the face has any glyphs that are colorized.
/// To determine if an individual glyph is colorized you must use
/// `is_color_glyph`.
pub fn has_color(&self) -> bool {
self.color.is_some()
}
/// Returns true if the given glyph ID is colorized.
pub fn is_color_glyph(&self, glyph_id: u32) -> bool {
match &self.color {
Some(c) => c.is_color_glyph(glyph_id),
None => false,
}
}
/// Returns the glyph index for the given Unicode code point. If this
/// face doesn't support this glyph, None is returned.
pub fn glyph_index(&self, cp: u32) -> Option<u32> {
let (glyph, ok) = util::glyphs_for_codepoint(&self.font, cp);
if !ok {
return None;
}
Some(u32::from(glyph))
}
/// Render a glyph using the glyph index. The rendered glyph is stored in
/// the given texture atlas.
pub fn render_glyph(
&self,
atlas: &mut Atlas,
glyph_index: u32,
opts: RenderOptions,
) -> Result<Glyph, RenderError> {
let mut glyph = [u16::try_from(glyph_index).map_err(|_| RenderError::InvalidGlyph)?];
// SAFETY: one valid glyph in the buffer.
let ct_rect = unsafe {
self.font.bounding_rects_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(glyph.as_mut_ptr()),
ptr::null_mut(),
1,
)
};
// This is in a coordinate space with (0.0, 0.0)
// in the bottom left and +Y pointing up.
let rect = Rect {
x: ct_rect.origin.x,
y: ct_rect.origin.y,
width: ct_rect.size.width,
height: ct_rect.size.height,
};
// Determine whether this is a color glyph.
let is_color = self.is_color_glyph(glyph_index);
// And whether it's (probably) a bitmap (sbix).
let sbix = is_color && self.color.as_ref().is_some_and(|c| c.sbix);
let Some(layout) = math::glyph_layout(rect, sbix, self.synthetic_bold, &opts) else {
return Ok(Glyph {
width: 0,
height: 0,
offset_x: 0,
offset_y: 0,
atlas_x: 0,
atlas_y: 0,
});
};
// Settings that are specific to if we are rendering text or emoji.
let depth = math::bitmap_depth(is_color);
let (space, context_opts) = if is_color {
// SAFETY: valid static CFString constant.
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceDisplayP3 }));
(
space,
CGImageByteOrderInfo::Order32Little.0 | CGImageAlphaInfo::PremultipliedFirst.0,
)
} else {
// SAFETY: valid static CFString constant.
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceLinearGray }));
(space, CGImageAlphaInfo::Only.0)
};
let space = space.ok_or(RenderError::ContextFailed)?;
// This is just a safety check.
if u32::from(atlas.format.depth()) != depth {
log::warn!(
"font atlas color depth doesn't equal font color depth atlas={} font={}",
atlas.format.depth(),
depth,
);
return Err(RenderError::InvalidAtlasFormat);
}
// Our buffer for rendering. We could cache this but glyph rasterization
// usually stabilizes pretty quickly and is very infrequent so I think
// the allocation overhead is acceptable compared to the cost of
// caching it forever or having to deal with a cache lifetime.
let width = layout.px_width as usize;
let height = layout.px_height as usize;
let mut buf = vec![0u8; width * height * depth as usize];
// SAFETY: `buf` is `width * height * depth` bytes, matching the
// bytes-per-row and dimensions given; it outlives the context (the
// context is dropped before `buf` at the end of this function).
let ctx = unsafe {
CGBitmapContextCreate(
buf.as_mut_ptr().cast(),
width,
height,
8,
width * depth as usize,
Some(&space),
context_opts,
)
}
.ok_or(RenderError::ContextFailed)?;
let c = Some(&*ctx);
// Perform an initial fill. This ensures that we don't have any
// uninitialized pixels in the bitmap.
if is_color {
CGContext::set_rgb_fill_color(c, 0.0, 0.0, 0.0, 0.0);
} else {
CGContext::set_gray_fill_color(c, 0.0, 0.0);
}
CGContext::fill_rect(
c,
objc2_core_foundation::CGRect::new(
CGPoint::new(0.0, 0.0),
objc2_core_foundation::CGSize::new(width as f64, height as f64),
),
);
// "Font smoothing" is what we call "thickening", it's an attempt
// to compensate for optical thinning of fonts, but at this point
// it's just something that makes the text look closer to system
// applications if users want that.
CGContext::set_allows_font_smoothing(c, true);
CGContext::set_should_smooth_fonts(c, opts.thicken);
// Subpixel positioning allows glyphs to be placed at non-integer
// coordinates. We need this for our alignment.
CGContext::set_allows_font_subpixel_positioning(c, true);
CGContext::set_should_subpixel_position_fonts(c, true);
// We don't want subpixel quantization, since we very carefully
// manage the position of our glyphs ourselves, and dont want to
// mess that up.
CGContext::set_allows_font_subpixel_quantization(c, false);
CGContext::set_should_subpixel_quantize_fonts(c, false);
// Anti-aliasing is self explanatory.
CGContext::set_allows_antialiasing(c, true);
CGContext::set_should_antialias(c, true);
// Set our color for drawing
if is_color {
CGContext::set_rgb_fill_color(c, 1.0, 1.0, 1.0, 1.0);
CGContext::set_rgb_stroke_color(c, 1.0, 1.0, 1.0, 1.0);
} else {
let strength = f64::from(opts.thicken_strength);
CGContext::set_gray_fill_color(c, strength / 255.0, 1.0);
CGContext::set_gray_stroke_color(c, strength / 255.0, 1.0);
}
// If we are drawing with synthetic bold then use a fill stroke
// which strokes the outlines of the glyph making a more bold look.
if let Some(line_width) = self.synthetic_bold {
CGContext::set_text_drawing_mode(c, CGTextDrawingMode::FillStroke);
CGContext::set_line_width(c, line_width);
}
// Translate our drawing context so that when we draw our
// glyph the bottom/left edge is at the correct sub-pixel
// position. The bottom/left edges are guaranteed to be at
// exactly [0, 0] relative to this because when we call to
// `drawGlyphs`, we pass the negated bearings.
CGContext::translate_ctm(
c,
layout.frac_x + f64::from(layout.canvas_padding),
layout.frac_y + f64::from(layout.canvas_padding),
);
// Scale the drawing context so that when we draw
// our glyph it's stretched to the constrained size.
CGContext::scale_ctm(c, layout.scale_x, layout.scale_y);
// Draw our glyph.
//
// We offset the position by the negated bearings so that the
// glyph is drawn at exactly [0, 0], which is then offset to
// the appropriate fractional position by the translation we
// did before scaling.
let mut position = CGPoint::new(-layout.rect_x, -layout.rect_y);
// SAFETY: one glyph and one position, valid context.
unsafe {
self.font.draw_glyphs(
NonNull::new_unchecked(glyph.as_mut_ptr()),
NonNull::from(&mut position),
1,
&ctx,
);
}
// The context must be released before we read the buffer.
drop(ctx);
// Write our rasterized glyph to the atlas.
let region = atlas.reserve(layout.px_width, layout.px_height)?;
atlas.set(region, &buf);
Ok(Glyph {
width: layout.px_width,
height: layout.px_height,
offset_x: layout.offset_x(),
offset_y: layout.offset_y(),
atlas_x: region.x,
atlas_y: region.y,
})
}
/// Get the `FaceMetrics` for this face.
pub fn get_metrics(&self) -> FaceMetrics {
let ct_font: &CTFont = &self.font;
// Read the 'head' table out of the font data.
//
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
// the table format is byte-identical to the 'head' table, so if we
// can't find 'head' we try 'bhed' instead before failing.
//
// ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
let head = util::font_table_bytes(ct_font, b"head")
.or_else(|| util::font_table_bytes(ct_font, b"bhed"))
.and_then(|d| {
opentype::Head::init(&d)
.inspect_err(|e| log::warn!("error parsing head table: {e}"))
.ok()
});
// Read the 'post' table out of the font data.
let post = util::font_table_bytes(ct_font, b"post").and_then(|d| {
opentype::Post::init(&d)
.inspect_err(|e| log::warn!("error parsing post table: {e}"))
.ok()
});
// Read the 'OS/2' table out of the font data if it's available.
let os2 = util::font_table_bytes(ct_font, b"OS/2").and_then(|d| {
opentype::OS2::init(&d)
.inspect_err(|e| log::warn!("error parsing OS/2 table: {e}"))
.ok()
});
// Read the 'hhea' table out of the font data.
let hhea = util::font_table_bytes(ct_font, b"hhea").and_then(|d| {
opentype::Hhea::init(&d)
.inspect_err(|e| log::warn!("error parsing hhea table: {e}"))
.ok()
});
// SAFETY: plain CoreText getters.
let (units_per_em, px_per_em, ct) = unsafe {
(
head.map_or_else(
|| f64::from(ct_font.units_per_em()),
|h| f64::from(h.unitsPerEm),
),
ct_font.size(),
CtMetrics {
ascent: ct_font.ascent(),
descent: ct_font.descent(),
leading: ct_font.leading(),
cap_height: ct_font.cap_height(),
x_height: ct_font.x_height(),
},
)
};
// Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take
// whatever is widest.
//
// ASCII height is calculated as the height of the overall bounding
// box of the same characters.
let (cell_width, ascii_height) = {
// All the visible ASCII chars
let mut unichars: Vec<u16> = (32u16..127).collect();
let n = unichars.len();
// Get our glyph IDs for the ASCII chars
let mut glyphs = vec![0u16; n];
// SAFETY: both buffers have `n` elements.
unsafe {
ct_font.glyphs_for_characters(
NonNull::new_unchecked(unichars.as_mut_ptr()),
NonNull::new_unchecked(glyphs.as_mut_ptr()),
n as isize,
);
}
// Get all our advances
let mut advances = vec![objc2_core_foundation::CGSize::new(0.0, 0.0); n];
// SAFETY: buffers have `n` elements.
unsafe {
ct_font.advances_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(glyphs.as_mut_ptr()),
advances.as_mut_ptr(),
n as isize,
);
}
// Find the maximum advance
let max = advances.iter().fold(0.0f64, |m, a| a.width.max(m));
// Get the overall bounding rect for the glyphs
// SAFETY: `glyphs` has `n` elements.
let rect = unsafe {
ct_font.bounding_rects_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(glyphs.as_mut_ptr()),
ptr::null_mut(),
n as isize,
)
};
(max, rect.size.height)
};
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
let ic_width = self.glyph_index('水' as u32).and_then(|glyph| {
let mut g = [glyph as u16];
// SAFETY: one valid glyph.
let (advance, bounds) = unsafe {
(
ct_font.advances_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(g.as_mut_ptr()),
ptr::null_mut(),
1,
),
ct_font.bounding_rects_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(g.as_mut_ptr()),
ptr::null_mut(),
1,
),
)
};
// If the advance of the glyph is less than the width of the actual
// glyph then we just treat it as invalid since it's probably wrong
// and using it for size normalization will instead make the font
// way too big.
//
// This can sometimes happen if there's a CJK font that has been
// patched with the nerd fonts patcher and it butchers the advance
// values so the advance ends up half the width of the actual glyph.
if !math::ic_width_is_valid(bounds.size.width, advance) {
log::warn!(
"(getMetrics) Width of glyph '水' for font \"{}\" is greater than its advance ({} > {}), discarding ic_width metric.",
self.name(),
bounds.size.width,
advance,
);
return None;
}
Some(advance)
});
math::face_metrics(
px_per_em,
units_per_em,
hhea.as_ref(),
os2.as_ref(),
post.as_ref(),
&ct,
Measurements {
cell_width,
ascii_height,
ic_width,
},
)
}
/// Copy the font table data for the given tag.
pub fn copy_table(&self, tag: &[u8; 4]) -> Result<Option<Vec<u8>>, FaceError> {
Ok(util::font_table_bytes(&self.font, tag))
}
/// The underlying CTFont (for shapers).
pub(crate) fn ct_font(&self) -> &CTFont {
&self.font
}
/// The harfbuzz font corresponding to this face (for the HarfBuzz shaper).
#[cfg(font_harfbuzz)]
pub fn hb_font(&self) -> &HbFont {
&self.hb_font
}
/// The `FontHasNoFile`-style path of this face's file (for the
/// `coretext_freetype` backend this is done by the deferred face).
pub fn font_descriptor(&self) -> CFRetained<objc2_core_text::CTFontDescriptor> {
// SAFETY: plain CoreText call.
unsafe { self.font.font_descriptor() }
}
}
/// The state associated with a font face that may have colorized glyphs.
/// This is used to determine if a specific glyph ID is colorized.
struct ColorState {
/// True if there is an sbix font table. For now, the mere presence
/// of an sbix font table causes us to assume the glyph is colored.
/// We can improve this later.
sbix: bool,
/// The SVG font table data (if any), which we can use to determine
/// if a glyph is present in the SVG table.
svg: Option<SvgTable>,
}
/// An owned copy of the `SVG ` table and the glyph range it covers.
struct SvgTable {
data: Vec<u8>,
}
impl ColorState {
fn new(f: &CTFont) -> Result<ColorState, FaceError> {
// sbix is true if the table exists in the font data at all.
// In the future we probably want to actually parse it and
// check for glyphs.
let sbix = util::font_table(f, b"sbix").is_some_and(|d| !d.is_empty());
// Read the SVG table out of the font data.
let svg = match util::font_table_bytes(f, b"SVG ") {
None => None,
Some(data) => {
// Validate the table up front like Zig.
opentype::SVG::init(&data).map_err(|_| FaceError::InvalidSvgTable)?;
Some(SvgTable { data })
}
};
Ok(ColorState { sbix, svg })
}
/// Returns true if the given glyph ID is colored.
fn is_color_glyph(&self, glyph_id: u32) -> bool {
// Our font system uses 32-bit glyph IDs for special values but
// actual fonts only contain 16-bit glyph IDs so if we can't cast
// into it it must be false.
let Ok(glyph_u16) = u16::try_from(glyph_id) else {
return false;
};
// sbix is always true for now
if self.sbix {
return true;
}
// if we have svg data, check it
if let Some(svg) = &self.svg
&& let Ok(table) = opentype::SVG::init(&svg.data)
&& table.has_glyph(glyph_u16)
{
return true;
}
false
}
}
/// Create a CTFont by name at the given size (test/dev helper equivalent of
/// `FontDescriptor.createWithNameAndSize` + `Font.createWithFontDescriptor`).
pub fn font_with_name(name: &str, size: f64) -> CFRetained<CTFont> {
let cf_name = CFString::from_str(name);
// SAFETY: valid name; null matrix is allowed.
unsafe { CTFont::with_name(&cf_name, size, ptr::null()) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{embedded, metrics::Metrics};
fn opts(points: f32) -> Options {
Options::new(DesiredSize::new(points))
}
fn render_ascii(face: &Face) {
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
for i in 32u32..127 {
let g = face.glyph_index(i).unwrap();
face.render_glyph(
&mut atlas,
g,
RenderOptions::new(Metrics::calc(face.get_metrics())),
)
.unwrap();
}
}
#[test]
fn monaco_ascii() {
let font = font_with_name("Monaco", 12.0);
let face = Face::init_font_copy(&font, opts(12.0)).unwrap();
render_ascii(&face);
}
#[test]
fn name() {
let font = font_with_name("Menlo", 12.0);
let face = Face::init_font_copy(&font, opts(12.0)).unwrap();
assert_eq!(face.name(), "Menlo");
}
#[test]
fn emoji() {
let font = font_with_name("Apple Color Emoji", 12.0);
let face = Face::init_font_copy(&font, opts(18.0)).unwrap();
let id = face.glyph_index('🥸' as u32).unwrap();
assert!(face.is_color_glyph(id));
}
#[test]
fn in_memory() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::REGULAR), opts(12.0)).unwrap();
render_ascii(&face);
}
#[test]
fn variable() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::VARIABLE), opts(12.0)).unwrap();
render_ascii(&face);
}
#[test]
fn variable_set_variation() {
let lib = Library::init().unwrap();
let mut face = Face::init(&lib, FontData::Static(embedded::VARIABLE), opts(12.0)).unwrap();
face.set_variations(
&[Variation {
id: super::super::VariationId::init(b"wght"),
value: 400.0,
}],
opts(12.0),
)
.unwrap();
render_ascii(&face);
}
#[test]
fn svg_font_table() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::JULIA_MONO), opts(12.0)).unwrap();
let table = face.copy_table(b"SVG ").unwrap().unwrap();
assert_ne!(table.len(), 0);
}
#[test]
fn glyph_index_colored_vs_text() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::JULIA_MONO), opts(12.0)).unwrap();
let glyph = face.glyph_index('A' as u32).unwrap();
assert_eq!(glyph, 4);
assert!(!face.is_color_glyph(glyph));
let glyph = face.glyph_index(0xE800).unwrap();
assert_eq!(glyph, 11482);
assert!(face.is_color_glyph(glyph));
}
}
@@ -0,0 +1,956 @@
//! Platform-independent math extracted from the CoreText face
//! (`font/face/coretext.zig`) so it can be unit tested on every platform.
//!
//! The CoreText face itself only exists on Apple targets; everything here is
//! plain arithmetic on values that the face reads from CoreText / the
//! OpenType tables:
//!
//! * [`face_metrics`]: the `getMetrics` table-priority logic (OS/2 typo
//! metrics vs. hhea vs. OS/2 win metrics, underline/strikethrough
//! fallbacks, cap/ex heights).
//! * [`synthetic_bold_line_width`]: the synthetic bold stroke width.
//! * [`glyph_layout`]: the bearing / canvas-size computation of
//! `renderGlyph`.
use super::{Constraint, ConstraintSize, GlyphSize, RenderOptions};
use crate::{
metrics::FaceMetrics,
opentype::{Hhea, OS2, Post},
};
/// The matrix applied to a regular font to auto-italicize it
/// (`Face.italic_skew`): `a=1, b=0, c=tan(15deg) (approx), d=1, tx=ty=0`.
pub const ITALIC_SKEW_C: f64 = 0.267949;
/// Metrics CoreText itself reports (used as fallbacks when a table is
/// missing). `descent` is CoreText's positive-down value.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CtMetrics {
pub ascent: f64,
/// `CTFontGetDescent`: positive distance below the baseline.
pub descent: f64,
pub leading: f64,
pub cap_height: f64,
pub x_height: f64,
}
/// Measurements taken by drawing/measuring glyphs.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Measurements {
/// The widest advance of the visible ASCII characters.
pub cell_width: f64,
/// Height of the overall bounding box of the visible ASCII characters.
pub ascii_height: f64,
/// Advance of U+6C34 (None if absent or considered invalid).
pub ic_width: Option<f64>,
}
/// The line width used for synthetic bold given the font size in points.
///
/// To determine our synthetic bold line width we get a multiplier from the
/// font size in points. This is a heuristic that is based on the fact that
/// a line width of 1 looks good at a certain point size. We want to scale
/// that up roughly linearly with the font size.
pub fn synthetic_bold_line_width(points: f64) -> f64 {
(points / 14.0).max(1.0)
}
/// Returns `(ascent, descent, line_gap)` in pixels, +Y up (`descent` is
/// normally negative), following the table priority of `getMetrics`.
pub fn vertical_metrics(
hhea: Option<&Hhea>,
os2: Option<&OS2>,
px_per_unit: f64,
ct: &CtMetrics,
) -> (f64, f64, f64) {
// If we couldn't get the hhea table, rely on metrics from CoreText.
let Some(hhea) = hhea else {
return (ct.ascent, -ct.descent, ct.leading);
};
let hhea_ascent = f64::from(hhea.ascender);
let hhea_descent = f64::from(hhea.descender);
let hhea_line_gap = f64::from(hhea.lineGap);
// If our font has no OS/2 table, then we just
// blindly use the metrics from the hhea table.
let Some(os2) = os2 else {
return (
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
);
};
let os2_ascent = f64::from(os2.sTypoAscender);
let os2_descent = f64::from(os2.sTypoDescender);
let os2_line_gap = f64::from(os2.sTypoLineGap);
// If the font says to use typo metrics, trust it.
if os2.fsSelection.use_typo_metrics {
return (
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
);
}
// Otherwise we prefer the height metrics from 'hhea' if they
// are available, or else OS/2 sTypo* metrics, and if all else
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if hhea.ascender != 0 || hhea.descender != 0 {
return (
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
);
}
if os2_ascent != 0.0 || os2_descent != 0.0 {
return (
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
);
}
let win_ascent = f64::from(os2.usWinAscent);
let win_descent = f64::from(os2.usWinDescent);
(
win_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender
// and hhea.Descender, so we flip its sign to fix this.
-win_descent * px_per_unit,
0.0,
)
}
/// Compute the `FaceMetrics` from the (optionally present) OpenType tables,
/// CoreText's own metrics and the measurements. `px_per_em` is
/// `CTFontGetSize` and `units_per_em` the `head` table value (or
/// `CTFontGetUnitsPerEm` if there is no `head`/`bhed` table).
pub fn face_metrics(
px_per_em: f64,
units_per_em: f64,
hhea: Option<&Hhea>,
os2: Option<&OS2>,
post: Option<&Post>,
ct: &CtMetrics,
measured: Measurements,
) -> FaceMetrics {
let px_per_unit = px_per_em / units_per_em;
let (ascent, descent, line_gap) = vertical_metrics(hhea, os2, px_per_unit, ct);
let (underline_position, underline_thickness) = match post {
None => (None, None),
Some(post) => {
// Some fonts have degenerate 'post' tables where the underline
// thickness (and often position) are 0. We consider them null
// if this is the case and use our own fallbacks when we calculate.
let has_broken_underline = post.underlineThickness == 0;
// If the underline position isn't 0 then we do use it,
// even if the thickness is't properly specified.
let pos = if has_broken_underline && post.underlinePosition == 0 {
None
} else {
Some(f64::from(post.underlinePosition) * px_per_unit)
};
let thick = if has_broken_underline {
None
} else {
Some(f64::from(post.underlineThickness) * px_per_unit)
};
(pos, thick)
}
};
// Similar logic to the underline above.
let (strikethrough_position, strikethrough_thickness) = match os2 {
None => (None, None),
Some(os2) => {
let has_broken_strikethrough = os2.yStrikeoutSize == 0;
let pos = if has_broken_strikethrough && os2.yStrikeoutPosition == 0 {
None
} else {
Some(f64::from(os2.yStrikeoutPosition) * px_per_unit)
};
let thick = if has_broken_strikethrough {
None
} else {
Some(f64::from(os2.yStrikeoutSize) * px_per_unit)
};
(pos, thick)
}
};
// We fall back to whatever CoreText does if the
// OS/2 table doesn't specify a cap or ex height.
let (cap_height, ex_height) = match os2 {
None => (ct.cap_height, ct.x_height),
Some(os2) => (
os2.sCapHeight
.map_or(ct.cap_height, |v| f64::from(v) * px_per_unit),
os2.sxHeight
.map_or(ct.x_height, |v| f64::from(v) * px_per_unit),
),
};
FaceMetrics {
px_per_em,
cell_width: measured.cell_width,
ascent,
descent,
line_gap,
underline_position,
underline_thickness,
strikethrough_position,
strikethrough_thickness,
cap_height: Some(cap_height),
ex_height: Some(ex_height),
ascii_height: Some(measured.ascii_height),
ic_width: measured.ic_width,
}
}
/// Is the advance of the "水" glyph usable as the ic width? If the advance
/// is less than the width of the actual glyph then it is probably wrong
/// (e.g. a CJK font patched by the nerd fonts patcher) and using it for size
/// normalization would make the font way too big.
pub fn ic_width_is_valid(glyph_width: f64, advance: f64) -> bool {
glyph_width <= advance || glyph_width.is_nan() || advance.is_nan()
}
/// A CoreText bounding rect (origin is bottom-left, +Y up).
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
/// The result of laying out a glyph for rasterization.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlyphLayout {
/// Whole pixel bearings of the final glyph.
pub px_x: i32,
pub px_y: i32,
/// Canvas size (includes padding and the fractional offset).
pub px_width: u32,
pub px_height: u32,
/// Extra pixels around the glyph for font smoothing.
pub canvas_padding: u32,
/// Fractional part of the pixel bearings, added as an offset when
/// rasterizing.
pub frac_x: f64,
pub frac_y: f64,
/// Scale applied to the context so the glyph is stretched to the
/// constrained size.
pub scale_x: f64,
pub scale_y: f64,
/// The (possibly bold-adjusted) bounding rect origin; the glyph is drawn
/// at the negated origin.
pub rect_x: f64,
pub rect_y: f64,
}
impl GlyphLayout {
/// `Glyph.offset_y`: distance from the bottom of the cell to the top of
/// the glyph's bounding box.
pub fn offset_y(&self) -> i32 {
self.px_y + self.px_height as i32
}
/// `Glyph.offset_x`: distance from the left of the cell to the left of
/// the glyph's bounding box.
pub fn offset_x(&self) -> i32 {
self.px_x
}
}
/// The bearing / size computation from `renderGlyph`.
///
/// `rect` is `CTFontGetBoundingRectsForGlyphs` for the glyph. Returns `None`
/// if the glyph is too small to render (the caller then returns a zero-sized
/// `Glyph`).
pub fn glyph_layout(
mut rect: Rect,
is_sbix: bool,
synthetic_bold: Option<f64>,
opts: &RenderOptions,
) -> Option<GlyphLayout> {
// If we're rendering a synthetic bold then we will gain 50% of
// the line width on every edge, which means we should increase
// our width and height by the line width and subtract half from
// our origin points.
//
// We don't add extra size if it's a sbix color font though,
// since bitmaps aren't affected by synthetic bold.
if !is_sbix && let Some(line_width) = synthetic_bold {
rect.width += line_width;
rect.height += line_width;
rect.x -= line_width / 2.0;
rect.y -= line_width / 2.0;
}
// If our rect is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
if rect.width < 0.25 || rect.height < 0.25 {
return None;
}
let metrics = &opts.grid_metrics;
let cell_width = f64::from(metrics.cell_width);
let cell_height = f64::from(metrics.cell_height);
// Next we apply any constraints to get the final size of the glyph.
let constraint: &Constraint = &opts.constraint;
// We need to add the baseline position before passing to the constrain
// function since it operates on cell-relative positions, not baseline.
let cell_baseline = f64::from(metrics.cell_baseline);
let glyph_size = constraint.constrain(
GlyphSize {
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y + cell_baseline,
},
metrics,
opts.constraint_width,
);
let mut x = glyph_size.x;
let mut y = glyph_size.y;
let mut width = glyph_size.width;
let mut height = glyph_size.height;
// We center all glyphs within the pixel-rounded and adjusted
// cell width if it's larger than the face width, so that they
// aren't weirdly off to the left.
//
// We don't do this if the glyph has a stretch constraint,
// since in that case the position was already calculated with the
// new cell width in mind.
if constraint.size != ConstraintSize::Stretch {
// We add half the difference to re-center.
let dx = (cell_width - metrics.face_width) / 2.0;
x += dx;
if dx < 0.0 {
// For negative diff (cell narrower than advance), we remove the
// integer part and only keep the fractional adjustment needed
// for consistent subpixel positioning.
x -= dx.trunc();
}
}
// If this is a bitmap glyph, it will always render as full pixels,
// not fractional pixels, so we need to quantize its position and
// size accordingly to align to full pixels so we get good results.
if is_sbix {
width = cell_width - (cell_width - width - x).round() - x.round();
height = cell_height - (cell_height - height - y).round() - y.round();
x = x.round();
y = y.round();
}
// We make an assumption that font smoothing ("thicken")
// adds no more than 1 extra pixel to any edge. We don't
// add extra size if it's a sbix color font though, since
// bitmaps aren't affected by smoothing.
let canvas_padding: u32 = u32::from(opts.thicken && !is_sbix);
// Our whole-pixel bearings for the final glyph.
// The fractional portion will be included in the rasterized position.
let px_x = (x.floor() as i32) - canvas_padding as i32;
let px_y = (y.floor() as i32) - canvas_padding as i32;
// We keep track of the fractional part of the pixel bearings, which
// we will add as an offset when rasterizing to make sure we get the
// correct sub-pixel position.
let frac_x = x - x.floor();
let frac_y = y - y.floor();
// Add the fractional pixel to the width and height and take
// the ceiling to get a canvas size that will definitely fit
// our drawn glyph, including the fractional offset and font smoothing.
let px_width = ((width + frac_x).ceil() as u32) + (2 * canvas_padding);
let px_height = ((height + frac_y).ceil() as u32) + (2 * canvas_padding);
Some(GlyphLayout {
px_x,
px_y,
px_width,
px_height,
canvas_padding,
frac_x,
frac_y,
scale_x: width / rect.width,
scale_y: height / rect.height,
rect_x: rect.x,
rect_y: rect.y,
})
}
/// `kCTFontTraitBold` bit of the symbolic traits.
pub const TRAIT_BOLD: u32 = 1 << 1;
/// `kCTFontTraitItalic` bit of the symbolic traits.
pub const TRAIT_ITALIC: u32 = 1;
/// `kCTFontTraitMonoSpace` bit of the symbolic traits.
pub const TRAIT_MONOSPACE: u32 = 1 << 10;
/// `kCTFontTraitColorGlyphs` bit of the symbolic traits.
pub const TRAIT_COLOR_GLYPHS: u32 = 1 << 13;
/// The symbolic traits value for a descriptor (0 if no trait is set, in
/// which case the traits attribute is not added; see
/// `Descriptor.toCoreTextDescriptor`).
pub fn symbolic_traits_value(bold: bool, italic: bool, monospace: bool) -> u32 {
(if bold { TRAIT_BOLD } else { 0 })
| (if italic { TRAIT_ITALIC } else { 0 })
| (if monospace { TRAIT_MONOSPACE } else { 0 })
}
/// Number of bytes per pixel and whether the glyph is rendered in color,
/// for the atlas format check in `renderGlyph`.
pub fn bitmap_depth(is_color: bool) -> u32 {
if is_color { 4 } else { 1 }
}
/// Decode a UTF-32 codepoint into UTF-16 code units
/// (`stringGetSurrogatePairForLongCharacter`). Returns the units and how many
/// are valid (1 or 2). Codepoints outside the Unicode range yield a single
/// replacement unit.
pub fn utf16_units(cp: u32) -> ([u16; 2], usize) {
match char::from_u32(cp) {
Some(c) => {
let mut buf = [0u16; 2];
let n = c.encode_utf16(&mut buf).len();
(buf, n)
}
None => ([0xFFFD, 0], 1),
}
}
/// The score used to sort CoreText discovery results, as a plain integer
/// (`Score.int()` in discovery.zig). Fields from least to most significant:
/// `glyph_count: u16`, `fuzzy_style: u8`, `bold`, `italic`, `exact_style`,
/// `monospace`, `codepoint` (1 bit each). Higher is better.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Score {
pub glyph_count: u16,
pub fuzzy_style: u8,
pub bold: bool,
pub italic: bool,
pub exact_style: bool,
pub monospace: bool,
pub codepoint: bool,
}
impl Score {
pub fn int(self) -> u32 {
u32::from(self.glyph_count)
| (u32::from(self.fuzzy_style) << 16)
| (u32::from(self.bold) << 24)
| (u32::from(self.italic) << 25)
| (u32::from(self.exact_style) << 26)
| (u32::from(self.monospace) << 27)
| (u32::from(self.codepoint) << 28)
}
}
/// The style-name strings the discovery score matches against (the first
/// is used for the exact match, all are used for the fuzzy match).
pub fn desired_styles(style: Option<&str>, bold: bool, italic: bool) -> Vec<&str> {
if let Some(s) = style {
return vec![s];
}
// If we don't have an explicitly desired style name, we base
// it on the bold and italic properties, this isn't ideal since
// fonts may use style names other than these, but it helps in
// some edge cases.
if bold {
if italic {
return vec!["bold italic", "bold", "italic", "oblique"];
}
return vec!["bold", "upright"];
} else if italic {
return vec!["italic", "regular", "oblique"];
}
vec!["regular", "upright"]
}
/// Compute `(exact_style, fuzzy_style)` for a font's style string against
/// the desired styles (see `Score.score`).
pub fn style_scores(style_str: &str, desired: &[&str]) -> (bool, u8) {
let exact = style_str.eq_ignore_ascii_case(desired[0]);
// Our "fuzzy match" score is 0 if the desired style isn't present
// in the string, otherwise we give higher priority for styles that
// have fewer characters not in the desired_styles list.
let mut fuzzy: u8 = style_str.len().min(255) as u8;
let lower = style_str.to_ascii_lowercase();
for s in desired {
if lower.contains(&s.to_ascii_lowercase()) {
fuzzy = fuzzy.saturating_sub(s.len().min(255) as u8);
}
}
(exact, u8::MAX.saturating_sub(fuzzy))
}
/// Derive `(is_bold, is_italic)` for a variation axis list entry, following
/// the thresholds in `Score.score`. `axes` yields `(name, value)`.
pub fn bold_italic_from_axes(
mut is_bold: bool,
mut is_italic: bool,
axes: impl IntoIterator<Item = (String, f64)>,
) -> (bool, bool) {
// If we see the 'ital' value then we ignore 'slnt'.
let mut ital_seen = false;
for (name, val) in axes {
if name == "wght" {
// Somewhat subjective threshold, we consider fonts
// bold if they have a 'wght' set greater than 600.
is_bold = val > 600.0;
continue;
}
if name == "ital" {
is_italic = val > 0.5;
ital_seen = true;
continue;
}
if !ital_seen && name == "slnt" {
// Arbitrary threshold of anything more than a 5
// degree clockwise slant is considered italic.
is_italic = val <= -5.0;
}
}
(is_bold, is_italic)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
face::{DesiredSize, RenderOptions},
metrics::Metrics,
opentype::{
os2::FSSelection,
sfnt::{Fixed, Version16Dot16},
},
};
fn hhea(ascender: i16, descender: i16, line_gap: i16) -> Hhea {
Hhea {
majorVersion: 1,
minorVersion: 0,
ascender,
descender,
lineGap: line_gap,
advanceWidthMax: 0,
minLeftSideBearing: 0,
minRightSideBearing: 0,
xMaxExtent: 0,
caretSlopeRise: 0,
caretSlopeRun: 0,
caretOffset: 0,
reserved0: 0,
reserved1: 0,
reserved2: 0,
reserved3: 0,
metricDataFormat: 0,
numberOfHMetrics: 0,
}
}
fn os2(typo: (i16, i16, i16), win: (u16, u16), use_typo: bool) -> OS2 {
OS2 {
version: 4,
xAvgCharWidth: 0,
usWeightClass: 400,
usWidthClass: 5,
fsType: 0,
ySubscriptXSize: 0,
ySubscriptYSize: 0,
ySubscriptXOffset: 0,
ySubscriptYOffset: 0,
ySuperscriptXSize: 0,
ySuperscriptYSize: 0,
ySuperscriptXOffset: 0,
ySuperscriptYOffset: 0,
yStrikeoutSize: 50,
yStrikeoutPosition: 300,
sFamilyClass: 0,
panose: [0; 10],
ulUnicodeRange1: 0,
ulUnicodeRange2: 0,
ulUnicodeRange3: 0,
ulUnicodeRange4: 0,
achVendID: [0; 4],
fsSelection: FSSelection {
use_typo_metrics: use_typo,
..FSSelection::from_u16(0)
},
usFirstCharIndex: 0,
usLastCharIndex: 0,
sTypoAscender: typo.0,
sTypoDescender: typo.1,
sTypoLineGap: typo.2,
usWinAscent: win.0,
usWinDescent: win.1,
ulCodePageRange1: None,
ulCodePageRange2: None,
sxHeight: Some(500),
sCapHeight: Some(700),
usDefaultChar: None,
usBreakChar: None,
usMaxContext: None,
usLowerOpticalPointSize: None,
usUpperOpticalPointSize: None,
}
}
fn post(pos: i16, thick: i16) -> Post {
Post {
version: Version16Dot16::from_u32(0x0002_0000),
italicAngle: Fixed(0),
underlinePosition: pos,
underlineThickness: thick,
isFixedPitch: 1,
minMemType42: 0,
maxMemType42: 0,
minMemType1: 0,
maxMemType1: 0,
}
}
#[test]
fn symbolic_traits() {
assert_eq!(symbolic_traits_value(false, false, false), 0);
assert_eq!(symbolic_traits_value(true, false, false), 2);
assert_eq!(symbolic_traits_value(false, true, false), 1);
assert_eq!(symbolic_traits_value(true, true, true), 2 | 1 | 1024);
}
#[test]
fn bold_line_width() {
assert_eq!(synthetic_bold_line_width(12.0), 1.0);
assert_eq!(synthetic_bold_line_width(14.0), 1.0);
assert_eq!(synthetic_bold_line_width(28.0), 2.0);
}
#[test]
fn vertical_no_hhea_uses_coretext() {
let ct = CtMetrics {
ascent: 10.0,
descent: 3.0,
leading: 1.0,
..Default::default()
};
assert_eq!(vertical_metrics(None, None, 0.01, &ct), (10.0, -3.0, 1.0));
}
#[test]
fn vertical_hhea_only() {
let h = hhea(800, -200, 100);
let (a, d, g) = vertical_metrics(Some(&h), None, 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (8.0, -2.0, 1.0));
}
#[test]
fn vertical_typo_when_flagged() {
let h = hhea(800, -200, 100);
let o = os2((900, -300, 0), (1000, 400), true);
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (9.0, -3.0, 0.0));
}
#[test]
fn vertical_prefers_hhea_then_typo_then_win() {
let o = os2((900, -300, 20), (1000, 400), false);
// hhea non-zero wins over typo.
let h = hhea(800, -200, 100);
let (a, ..) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!(a, 8.0);
// hhea zero -> typo.
let h = hhea(0, 0, 0);
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (9.0, -3.0, 0.2));
// both zero -> win metrics with flipped descent sign.
let o = os2((0, 0, 20), (1000, 400), false);
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (10.0, -4.0, 0.0));
}
#[test]
fn metrics_underline_and_strikeout() {
let h = hhea(800, -200, 0);
let o = os2((0, 0, 0), (0, 0), false);
let p = post(-100, 50);
let m = face_metrics(
10.0,
1000.0,
Some(&h),
Some(&o),
Some(&p),
&CtMetrics::default(),
Measurements {
cell_width: 6.0,
ascii_height: 9.0,
ic_width: Some(12.0),
},
);
assert_eq!(m.underline_position, Some(-1.0));
assert_eq!(m.underline_thickness, Some(0.5));
assert_eq!(m.strikethrough_position, Some(3.0));
assert_eq!(m.strikethrough_thickness, Some(0.5));
assert_eq!(m.cap_height, Some(7.0));
assert_eq!(m.ex_height, Some(5.0));
assert_eq!(m.cell_width, 6.0);
assert_eq!(m.ascii_height, Some(9.0));
assert_eq!(m.ic_width, Some(12.0));
}
#[test]
fn metrics_broken_underline_is_none() {
let p = post(0, 0);
let m = face_metrics(
10.0,
1000.0,
None,
None,
Some(&p),
&CtMetrics {
cap_height: 7.0,
x_height: 5.0,
..Default::default()
},
Measurements::default(),
);
assert_eq!(m.underline_position, None);
assert_eq!(m.underline_thickness, None);
// position set but thickness broken: keep position only
let p = post(-100, 0);
let m = face_metrics(
10.0,
1000.0,
None,
None,
Some(&p),
&CtMetrics::default(),
Measurements::default(),
);
assert_eq!(m.underline_position, Some(-1.0));
assert_eq!(m.underline_thickness, None);
assert_eq!(m.strikethrough_position, None);
// no OS/2: CoreText cap/ex heights.
assert_eq!(m.cap_height, Some(0.0));
}
#[test]
fn ic_width_validity() {
assert!(ic_width_is_valid(10.0, 12.0));
assert!(ic_width_is_valid(12.0, 12.0));
assert!(!ic_width_is_valid(13.0, 12.0));
}
fn opts() -> RenderOptions {
let mut m = Metrics::init();
m.cell_width = 10;
m.cell_height = 20;
m.cell_baseline = 4;
m.face_width = 10.0;
m.face_height = 20.0;
let _ = DesiredSize::new(12.0);
RenderOptions::new(m)
}
#[test]
fn layout_tiny_glyph_is_none() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 0.2,
height: 5.0,
};
assert!(glyph_layout(r, false, None, &opts()).is_none());
}
#[test]
fn layout_basic() {
let r = Rect {
x: 1.5,
y: -2.25,
width: 6.0,
height: 10.0,
};
let l = glyph_layout(r, false, None, &opts()).unwrap();
assert_eq!(l.px_x, 1);
// y is cell-relative: -2.25 + cell_baseline (4) = 1.75
assert_eq!(l.px_y, 1);
assert!((l.frac_x - 0.5).abs() < 1e-9);
assert!((l.frac_y - 0.75).abs() < 1e-9);
assert_eq!(l.px_width, 7);
assert_eq!(l.px_height, 11);
assert_eq!(l.canvas_padding, 0);
assert_eq!(l.scale_x, 1.0);
assert_eq!(l.offset_y(), 12);
assert_eq!(l.offset_x(), 1);
}
#[test]
fn layout_thicken_pads_canvas() {
let r = Rect {
x: 1.0,
y: 0.0,
width: 5.0,
height: 5.0,
};
let mut o = opts();
o.thicken = true;
let l = glyph_layout(r, false, None, &o).unwrap();
assert_eq!(l.canvas_padding, 1);
assert_eq!(l.px_x, 0);
assert_eq!(l.px_width, 7);
// sbix glyphs never pad
let l = glyph_layout(r, true, None, &o).unwrap();
assert_eq!(l.canvas_padding, 0);
}
#[test]
fn layout_synthetic_bold_grows_rect() {
let r = Rect {
x: 2.0,
y: 2.0,
width: 4.0,
height: 4.0,
};
let l = glyph_layout(r, false, Some(2.0), &opts()).unwrap();
assert_eq!(l.rect_x, 1.0);
assert_eq!(l.rect_y, 1.0);
assert_eq!(l.px_width, 6);
// sbix ignores synthetic bold
let l = glyph_layout(r, true, Some(2.0), &opts()).unwrap();
assert_eq!(l.rect_x, 2.0);
}
#[test]
fn layout_centers_in_wider_cell() {
let mut o = opts();
o.grid_metrics.cell_width = 12;
o.grid_metrics.face_width = 10.0;
let r = Rect {
x: 0.0,
y: 0.0,
width: 5.0,
height: 5.0,
};
let l = glyph_layout(r, false, None, &o).unwrap();
// dx = (12 - 10)/2 = 1
assert_eq!(l.px_x, 1);
}
#[test]
fn utf16() {
assert_eq!(utf16_units('A' as u32), (['A' as u16, 0], 1));
let (u, n) = utf16_units(0x1F978);
assert_eq!(n, 2);
assert_eq!(u, [0xD83E, 0xDD78]);
assert_eq!(utf16_units(0xD800).1, 1);
}
#[test]
fn score_ordering() {
let a = Score {
codepoint: true,
..Default::default()
};
let b = Score {
monospace: true,
glyph_count: u16::MAX,
fuzzy_style: 255,
bold: true,
italic: true,
exact_style: true,
..Default::default()
};
assert!(a.int() > b.int());
let c = Score {
glyph_count: 5,
..Default::default()
};
let d = Score {
glyph_count: 7,
..Default::default()
};
assert!(d.int() > c.int());
}
#[test]
fn desired_style_lists() {
assert_eq!(desired_styles(Some("Light"), true, true), vec!["Light"]);
assert_eq!(
desired_styles(None, true, true),
vec!["bold italic", "bold", "italic", "oblique"]
);
assert_eq!(desired_styles(None, true, false), vec!["bold", "upright"]);
assert_eq!(
desired_styles(None, false, true),
vec!["italic", "regular", "oblique"]
);
assert_eq!(
desired_styles(None, false, false),
vec!["regular", "upright"]
);
}
#[test]
fn style_score_fuzzy_and_exact() {
let d = desired_styles(None, false, false);
let (exact, fuzzy) = style_scores("Regular", &d);
assert!(exact);
assert_eq!(fuzzy, 255);
let (exact, fuzzy) = style_scores("Semi Regular Extra", &d);
assert!(!exact);
// len 18, "regular" (7) subtracted => 11 => 255 - 11
assert_eq!(fuzzy, 255 - 11);
let (_, fuzzy) = style_scores("Light", &d);
assert_eq!(fuzzy, 255 - 5);
}
#[test]
fn axes_bold_italic() {
let r = bold_italic_from_axes(
false,
false,
[("wght".to_owned(), 700.0), ("slnt".to_owned(), -10.0)],
);
assert_eq!(r, (true, true));
// ital seen => slnt ignored
let r = bold_italic_from_axes(
true,
true,
[("ital".to_owned(), 0.0), ("slnt".to_owned(), -10.0)],
);
assert_eq!(r, (true, false));
let r = bold_italic_from_axes(false, false, [("wght".to_owned(), 600.0)]);
assert_eq!(r, (false, false));
}
}
+13 -6
View File
@@ -1,7 +1,9 @@
//! Font engine: metrics, atlas, sprite font, faces (FreeType), discovery
//! (fontconfig), collections, codepoint resolution, shaping (HarfBuzz) and
//! the shared glyph grid. Port of `ZIG/src/font/` for the Linux backend
//! (`fontconfig_freetype`).
//! Font engine: metrics, atlas, sprite font, faces (FreeType / CoreText),
//! discovery (fontconfig / CoreText), collections, codepoint resolution,
//! shaping (HarfBuzz / CoreText) and the shared glyph grid. Port of
//! `ZIG/src/font/`. The backend is selected per target (see `backend.rs` and
//! `build.rs`): `fontconfig_freetype` on Linux, `coretext` on Apple targets,
//! `web_canvas` on wasm32.
#![cfg_attr(test, allow(clippy::unwrap_used))]
@@ -13,7 +15,12 @@ pub mod codepoint_resolver;
#[cfg(not(target_arch = "wasm32"))]
pub mod collection;
pub mod config_types;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(font_coretext)]
pub mod coretext_util;
#[cfg(font_fontconfig)]
pub mod deferred_face;
#[cfg(font_coretext)]
#[path = "deferred_face_coretext.rs"]
pub mod deferred_face;
#[cfg(target_arch = "wasm32")]
#[path = "deferred_face_web_canvas.rs"]
@@ -23,7 +30,7 @@ pub mod discovery;
#[cfg(not(target_arch = "wasm32"))]
pub mod embedded;
pub mod face;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(font_fontconfig)]
pub mod fontconfig;
pub mod glyph;
#[cfg(not(target_arch = "wasm32"))]
+24 -4
View File
@@ -1,24 +1,31 @@
//! A library represents the shared state that the underlying font
//! library implementation(s) require per-process. Port of `font/library.zig`
//! (the `FreetypeLibrary` variant).
//! library implementation(s) require per-process. Port of `font/library.zig`.
//!
//! * FreeType backends: the `FT_Library` (plus a face-creation mutex).
//! * Backends without a library (CoreText, Canvas): an empty library
//! (Zig: `NoopLibrary`).
use std::sync::{Arc, Mutex};
#[cfg(font_freetype)]
use crate::face::freetype::ft;
#[derive(Debug, thiserror::Error)]
pub enum LibraryError {
#[cfg(font_freetype)]
#[error("freetype error {0}")]
Freetype(i32),
}
/// Shared FreeType library state. Cheap to clone.
/// Shared font library state. Cheap to clone.
#[derive(Clone)]
pub struct Library {
#[cfg_attr(not(font_freetype), allow(dead_code))]
pub(crate) inner: Arc<LibraryInner>,
}
pub(crate) struct LibraryInner {
#[cfg(font_freetype)]
pub(crate) lib: ft::Lib,
/// Mutex to be held any time the library is
@@ -27,6 +34,7 @@ pub(crate) struct LibraryInner {
}
impl Library {
#[cfg(font_freetype)]
pub fn init() -> Result<Library, LibraryError> {
let lib = ft::Lib::new().map_err(LibraryError::Freetype)?;
Ok(Library {
@@ -37,8 +45,20 @@ impl Library {
})
}
/// The CoreText backend has no per-process library state.
#[cfg(not(font_freetype))]
#[allow(clippy::unnecessary_wraps)]
pub fn init() -> Result<Library, LibraryError> {
Ok(Library {
inner: Arc::new(LibraryInner {
mutex: Mutex::new(()),
}),
})
}
/// Lock the library mutex (poisoning is ignored: the guarded state is
/// only FreeType's own).
/// only the font library's own).
#[cfg_attr(not(font_freetype), allow(dead_code))]
pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, ()> {
self.inner
.mutex
+14 -5
View File
@@ -1,20 +1,29 @@
//! Shaping types shared by all shapers. Port of `font/shape.zig`.
//!
//! The shaper implementation for the Linux backend is HarfBuzz
//! ([`crate::shaper::harfbuzz::Shaper`]).
//! (`crate::shaper::harfbuzz::Shaper`), for the macOS backend CoreText
//! (`crate::shaper::coretext::Shaper`).
#[cfg(font_coretext_shaper)]
pub use crate::shaper::coretext;
#[cfg(font_coretext_shaper)]
pub use crate::shaper::coretext::{ShapeError, ShapeRunError, Shaper};
pub use crate::shaper::feature::{DEFAULT_FEATURES, Feature, FeatureList};
#[cfg(font_harfbuzz)]
pub use crate::shaper::harfbuzz;
/// Shaper implementation for our compile options (backend
/// `fontconfig_freetype` => HarfBuzz).
#[cfg(not(target_arch = "wasm32"))]
pub use crate::shaper::harfbuzz::Shaper;
/// `fontconfig_freetype`, `coretext_freetype`, `coretext_harfbuzz` =>
/// HarfBuzz; `coretext` => CoreText; `coretext_noshape` => no-op).
#[cfg(font_harfbuzz)]
pub use crate::shaper::harfbuzz::{ShapeError, ShapeRunError, Shaper};
#[cfg(font_noshape)]
pub use crate::shaper::noop::{ShapeError, ShapeRunError, Shaper};
/// Shaper implementation for the `web_canvas` backend (wasm32).
#[cfg(target_arch = "wasm32")]
pub use crate::shaper::web_canvas::Shaper;
#[cfg(not(target_arch = "wasm32"))]
pub use crate::shaper::{
cache::Cache,
harfbuzz,
noop,
run::{RunIterator, RunIteratorHook, RunOptions, TextRun},
};
@@ -0,0 +1,81 @@
//! Dedicated thread for releasing CoreFoundation objects.
//!
//! Pools of CF objects (strings, attributed strings, typesetters, lines,
//! fonts) are sent here so that their (excessively slow) release callback
//! logic doesn't block the shaping/renderer thread. Equivalent of
//! `os/cf_release_thread.zig` for the shaper's needs: a mailbox of owned
//! pools and a thread that drops them.
//!
//! TODO(integration): this is a self-contained version living in the font
//! crate; it can be replaced by `ghostty_os::cf_release_thread` once the os
//! crate's port lands and the font crate depends on it.
#![allow(unsafe_code)]
use std::{
sync::mpsc::{Sender, channel},
thread::JoinHandle,
};
use objc2_core_foundation::{CFRetained, CFType};
use crate::coretext_util::CfBox;
/// An owned CF object queued for release.
pub(crate) type Pooled = CfBox<CFType>;
/// Convert any retained CF object into a pool entry.
pub(crate) fn pooled<T: objc2_core_foundation::Type>(v: CFRetained<T>) -> Pooled {
// SAFETY: every CF object is a CFType; the retain count is carried over
// by the cast and released exactly once when the `Pooled` drops.
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFType>(v) })
}
pub(crate) struct ReleaseThread {
tx: Option<Sender<Vec<Pooled>>>,
handle: Option<JoinHandle<()>>,
}
impl ReleaseThread {
pub(crate) fn new() -> Option<ReleaseThread> {
let (tx, rx) = channel::<Vec<Pooled>>();
let handle = std::thread::Builder::new()
.name("cf_release".into())
.spawn(move || {
// Dropping each pool releases every object in it.
while let Ok(pool) = rx.recv() {
drop(pool);
}
log::debug!("cf release thread exited");
})
.inspect_err(|e| log::warn!("error spawning cf release thread: {e}"))
.ok()?;
Some(ReleaseThread {
tx: Some(tx),
handle: Some(handle),
})
}
/// Send a pool for release. If the thread is gone the pool is released
/// inline.
pub(crate) fn release(&self, pool: Vec<Pooled>) {
if pool.is_empty() {
return;
}
if let Some(tx) = &self.tx
&& let Err(e) = tx.send(pool)
{
// Receiver gone: release here.
drop(e.0);
}
}
}
impl Drop for ReleaseThread {
fn drop(&mut self) {
// Closing the channel stops the thread after it drains the mailbox.
self.tx.take();
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
+499
View File
@@ -0,0 +1,499 @@
//! CoreText shaper (Apple targets). Port of `font/shaper/coretext.zig`.
//!
//! CoreText shaping differs in subtle ways from HarfBuzz so it may result
//! in inconsistent rendering across platforms. But it also fixes many
//! issues (some macOS specific):
//!
//! - Theta hat offset is incorrect in HarfBuzz but correct by default
//! on macOS applications using CoreText. (See:
//! <https://github.com/harfbuzz/harfbuzz/discussions/4525>)
//!
//! - Hyphens (U+2010) can be synthesized by CoreText but not by HarfBuzz.
//! See: <https://github.com/mitchellh/ghostty/issues/1643>
//!
//! The API mirrors [`crate::shaper::harfbuzz::Shaper`]: the run iterator
//! drives `prepare` / `add_codepoint` / `finalize` through
//! [`RunIteratorHook`](crate::shaper::run::RunIteratorHook) and the caller
//! shapes with [`Shaper::shape_run`]. The cluster/ligature heuristics live
//! in [`super::coretext_cells`] (unit tested on every platform).
#![allow(unsafe_code)]
use std::ptr::{self, NonNull};
use objc2_core_foundation::{
CFArray,
CFAttributedString,
CFDictionary,
CFNumber,
CFRange,
CFRetained,
CFString,
CFType,
CGPoint,
CGSize,
};
use objc2_core_text::{
CTFont,
CTFontDescriptor,
CTLine,
CTRun,
CTRunStatus,
CTTypesetter,
kCTFontAttributeName,
kCTFontFeatureSettingsAttribute,
kCTFontOpenTypeFeatureTag,
kCTFontOpenTypeFeatureValue,
kCTTypesetterOptionForcedEmbeddingLevel,
};
use super::{
cf_release::{Pooled, ReleaseThread, pooled},
coretext_cells::{self as cells, CtRun, RunGlyph, RunState},
};
use crate::{
coretext_util::CfBox,
face::Face,
shape::{self, Cell, DEFAULT_FEATURES, Feature, FeatureList, TextRun},
shared_grid::SharedGrid,
};
#[derive(Debug, thiserror::Error)]
pub enum ShapeError {
#[error("coretext object creation failed")]
CoreTextFailed,
}
/// Errors from [`Shaper::shape_run`].
#[derive(Debug, thiserror::Error)]
pub enum ShapeRunError {
#[error(transparent)]
GetFace(#[from] crate::collection::GetFaceError),
#[error(transparent)]
Shape(#[from] ShapeError),
}
/// Create a CoreFoundation Dictionary suitable for settings the font
/// features of a CoreText font (`makeFeaturesDict`).
fn make_features_dict(feats: &[Feature]) -> CfBox<CFDictionary> {
let mut items: Vec<CFRetained<CFDictionary<CFString, CFType>>> = Vec::new();
for feat in feats {
// Keys can only be ASCII.
let key = CFString::from_str(&String::from_utf8_lossy(&feat.tag));
let value = CFNumber::new_i32(feat.value as i32);
// SAFETY: valid static keys.
let (k_tag, k_val) = unsafe { (kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue) };
let dict = CFDictionary::<CFString, CFType>::from_slices(
&[k_tag, k_val],
&[key.as_ref(), value.as_ref()],
);
items.push(dict);
}
let item_refs: Vec<&CFDictionary<CFString, CFType>> = items.iter().map(|d| &**d).collect();
let list = CFArray::from_objects(&item_refs);
// SAFETY: valid static key.
let k = unsafe { kCTFontFeatureSettingsAttribute };
let dict = CFDictionary::<CFString, CFType>::from_slices(&[k], &[list.as_ref()]);
// SAFETY: the attribute dictionary is used as an untyped CFDictionary.
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(dict) })
}
/// Shaper that uses CoreText.
pub struct Shaper {
/// The string used for shaping the current run.
run_state: RunState,
/// CoreFoundation Dictionary which represents our font feature settings.
features: CfBox<CFDictionary>,
/// A version of the features dictionary with the default features excluded.
features_no_default: CfBox<CFDictionary>,
/// The shared memory used for shaping results.
cell_buf: Vec<Cell>,
/// Cached attributes dict for creating CTTypesetter objects.
/// The values in this never change so we can avoid overhead
/// by just creating it once and saving it for reuse.
typesetter_attr_dict: CfBox<CFDictionary>,
/// List where we cache fonts, so we don't have to remake them for
/// every single shaping operation.
///
/// Fonts are cached as attribute dictionaries to be applied directly to
/// attributed strings.
cached_fonts: Vec<Option<CfBox<CFDictionary>>>,
/// The grid that our cached fonts correspond to.
/// If the grid changes then we need to reset our cache.
cached_font_grid: usize,
/// The list of CoreFoundation objects to release on the dedicated
/// release thread. This is built up over the course of shaping and
/// sent to the release thread when `end_frame` is called.
cf_release_pool: Vec<Pooled>,
/// Dedicated thread for releasing CoreFoundation objects. Some objects,
/// such as those produced by CoreText, have excessively slow release
/// callback logic.
cf_release_thread: Option<ReleaseThread>,
}
impl Shaper {
/// The cell buffer holds the shaped results.
pub fn new(opts: &shape::Options) -> Result<Shaper, ShapeError> {
let mut feature_list = FeatureList::default();
for feature_str in &opts.features {
feature_list.append_from_string(feature_str);
}
// We need to construct two attrs dictionaries for font features;
// one without the default features included, and one with them.
let feats = &feature_list.features;
let mut feats_df: Vec<Feature> = DEFAULT_FEATURES.to_vec();
feats_df.extend_from_slice(feats);
let features = make_features_dict(&feats_df);
let features_no_default = make_features_dict(feats);
// For now we only support LTR text. If we shape RTL text then
// rendering will be very wrong so we need to explicitly force
// LTR no matter what.
//
// See: https://github.com/mitchellh/ghostty/issues/1737
// See: https://github.com/mitchellh/ghostty/issues/1442
//
// We used to do this by setting the writing direction attribute
// on the attributed string we used, but it seems like that will
// still allow some weird results, for example a single space at
// the end of a line composed of RTL characters will be cause it
// to output a run containing just that space, BEFORE it outputs
// the rest of the line as a separate run, very weirdly with the
// "right to left" flag set in the single space run's run status...
//
// So instead what we do is use a CTTypesetter to create our line,
// using the kCTTypesetterOptionForcedEmbeddingLevel attribute to
// force CoreText not to try doing any sort of BiDi, instead just
// treat all text as embedding level 0 (left to right).
let typesetter_attr_dict = {
let num = CFNumber::new_i32(0);
// SAFETY: valid static key.
let k = unsafe { kCTTypesetterOptionForcedEmbeddingLevel };
let d = CFDictionary::<CFString, CFType>::from_slices(&[k], &[num.as_ref()]);
// SAFETY: used as an untyped CFDictionary.
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(d) })
};
Ok(Shaper {
run_state: RunState::default(),
features,
features_no_default,
cell_buf: Vec::new(),
typesetter_attr_dict,
cached_fonts: Vec::new(),
cached_font_grid: 0,
cf_release_pool: Vec::new(),
cf_release_thread: ReleaseThread::new(),
})
}
/// Send the release pool to the release thread.
pub fn end_frame(&mut self) {
if self.cf_release_pool.is_empty() {
return;
}
// Get all the items in the pool as an owned vec so we can
// send it to the dedicated release thread.
let items = std::mem::take(&mut self.cf_release_pool);
match &self.cf_release_thread {
Some(t) => t.release(items),
// No thread: release inline.
None => drop(items),
}
}
// ---- RunIterator hooks ----
pub fn prepare(&mut self) {
self.run_state.reset();
}
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
self.run_state.add_codepoint(cp, cluster);
}
#[allow(clippy::unused_self)]
pub fn finalize(&mut self) {}
/// Convenience: shape `text` as a single run where every item is a
/// `(codepoint, cluster)` pair.
pub fn shape_text(
&mut self,
face: Option<&Face>,
text: &[(u32, u32)],
) -> Result<&[Cell], ShapeError> {
self.prepare();
for &(cp, cluster) in text {
self.add_codepoint(cp, cluster);
}
self.finalize();
self.shape(face)
}
/// Shape the given text run (`shape(run)` in Zig). The run must be the
/// immediately previous run that was iterated since it shares state with
/// the shaper.
///
/// Note that this will accumulate garbage in the release pool. The
/// caller must ensure you're properly calling `end_frame` to release
/// all the objects.
pub fn shape_run(
&mut self,
grid: &SharedGrid,
run: &TextRun,
) -> Result<&[Cell], ShapeRunError> {
// Special fonts aren't shaped and their codepoint == glyph so we
// can just return the codepoints as-is.
if run.font_index.special().is_some() {
cells::special_cells(&self.run_state.codepoints, &mut self.cell_buf);
return Ok(&self.cell_buf);
}
let attr_dict = self.get_font(grid, run.font_index)?;
Ok(self.shape_with_attrs(&attr_dict)?)
}
/// Shape the current run (as built through the hooks) with the given
/// face (uncached), or pass `None` for a special-case font (sprite).
pub fn shape(&mut self, face: Option<&Face>) -> Result<&[Cell], ShapeError> {
let Some(face) = face else {
cells::special_cells(&self.run_state.codepoints, &mut self.cell_buf);
return Ok(&self.cell_buf);
};
let attr_dict = self.make_attr_dict(face);
self.shape_with_attrs(&attr_dict)
}
fn shape_with_attrs(&mut self, attr_dict: &CFDictionary) -> Result<&[Cell], ShapeError> {
let state = &self.run_state;
// Make room for the attributed string, CTTypesetter, and CTLine.
self.cf_release_pool.reserve(4);
// SAFETY: `unichars` is a valid buffer of `len` UTF-16 units.
let str = unsafe {
CFString::with_characters(None, state.unichars.as_ptr(), state.unichars.len() as isize)
}
.ok_or(ShapeError::CoreTextFailed)?;
// Create an attributed string from our string
// SAFETY: valid string and attribute dictionary.
let attr_str = unsafe { CFAttributedString::new(None, Some(&str), Some(attr_dict)) }
.ok_or(ShapeError::CoreTextFailed)?;
// Create a typesetter from the attributed string and the cached
// attr dict. (See comment in new for more info on the attr dict.)
// SAFETY: valid string and options dictionary.
let typesetter = unsafe {
CTTypesetter::with_attributed_string_and_options(
&attr_str,
Some(&self.typesetter_attr_dict),
)
}
.ok_or(ShapeError::CoreTextFailed)?;
// Create a line from the typesetter
// SAFETY: a zero length range means the whole string.
let line: CFRetained<CTLine> = unsafe {
typesetter.line(CFRange {
location: 0,
length: 0,
})
};
// CoreText, despite our insistence with an enforced embedding level,
// may sometimes output runs that are non-monotonic.
//
// CoreText may generate multiple runs even though our input to
// CoreText is already split into runs by our own run iterator.
// The runs as far as I can tell are always sequential to each
// other so we can iterate over them and just append to our
// cell buffer.
// SAFETY: plain CoreText call.
let glyph_runs = unsafe { line.glyph_runs() };
// SAFETY: the glyph runs array contains CTRun objects.
let glyph_runs: &CFArray<CTRun> = unsafe { glyph_runs.cast_unchecked() };
let mut runs: Vec<CtRun> = Vec::with_capacity(glyph_runs.len());
for i in 0..glyph_runs.len() {
let Some(ctrun) = glyph_runs.get(i) else {
continue;
};
runs.push(read_run(&ctrun));
}
cells::cells_from_runs(&self.run_state.codepoints, &runs, &mut self.cell_buf);
// The objects are released on the release thread (see `end_frame`).
self.cf_release_pool.push(pooled(str));
self.cf_release_pool.push(pooled(attr_str));
self.cf_release_pool.push(pooled(typesetter));
self.cf_release_pool.push(pooled(line));
Ok(&self.cell_buf)
}
/// Build an attr dict (kCTFontAttributeName -> font with our features)
/// for the given face.
fn make_attr_dict(&mut self, face: &Face) -> CfBox<CFDictionary> {
let attrs = if face.quirks_disable_default_font_features {
&self.features_no_default
} else {
&self.features
};
// SAFETY: valid attributes dictionary.
let desc = unsafe { CTFontDescriptor::with_attributes(attrs) };
// SAFETY: valid descriptor, null matrix is allowed.
let copied: CFRetained<CTFont> = unsafe {
face.ct_font()
.copy_with_attributes(0.0, ptr::null(), Some(&desc))
};
// Get our font and use that get the attributes to set for the
// attributed string so the whole string uses the same font.
// SAFETY: valid static key.
let k = unsafe { kCTFontAttributeName };
let d = CFDictionary::<CFString, CFType>::from_slices(&[k], &[copied.as_ref()]);
self.cf_release_pool.push(pooled(desc));
self.cf_release_pool.push(pooled(copied));
// SAFETY: used as an untyped CFDictionary.
CfBox::new(unsafe { CFRetained::cast_unchecked::<CFDictionary>(d) })
}
/// Get an attr dict for a font from a specific index.
/// These items are cached.
fn get_font(
&mut self,
grid: &SharedGrid,
index: crate::collection::Index,
) -> Result<CfBox<CFDictionary>, ShapeRunError> {
// If this grid doesn't match the one we've cached fonts for,
// then we reset the cache list since it's no longer valid.
// We use an integer rather than keeping the pointer because we
// don't want anyone to inadvertently use the pointer.
let grid_id = ptr::from_ref(grid) as usize;
if grid_id != self.cached_font_grid {
if self.cached_font_grid > 0 {
// Put all the currently cached fonts in to
// the release pool before clearing the list.
for ft in self.cached_fonts.drain(..).flatten() {
self.cf_release_pool.push(pooled(ft.0));
}
}
self.cached_font_grid = grid_id;
}
let index_int = index.int() as usize;
// The cached fonts are indexed directly by the font index, since
// this number is usually low. Therefore, we set any index we haven't
// seen to null.
if self.cached_fonts.len() <= index_int {
self.cached_fonts.resize_with(index_int + 1, || None);
}
// If we have it, return the cached attr dict.
if let Some(cached) = &self.cached_fonts[index_int] {
return Ok(cached.clone());
}
// The CoreText shaper relies on CoreText and CoreText claims
// that CTFonts are threadsafe. See:
// https://developer.apple.com/documentation/coretext/
//
// Quote:
// All individual functions in Core Text are thread-safe. Font
// objects (CTFont, CTFontDescriptor, and associated objects) can
// be used simultaneously by multiple operations, work queues, or
// threads. However, the layout objects (CTTypesetter, CTFramesetter,
// CTRun, CTLine, CTFrame, and associated objects) should be used in
// a single operation, work queue, or thread.
//
// Because of this, we only hold the grid lock to grab the face and
// set it up, then release it.
let attr_dict = {
let mut guard = grid.write();
let face = guard.resolver.collection.get_face(index)?;
self.make_attr_dict(face)
};
self.cached_fonts[index_int] = Some(attr_dict.clone());
Ok(attr_dict)
}
}
impl Drop for Shaper {
fn drop(&mut self) {
// Release whatever is left in the pool on the release thread
// (it is joined when the thread handle drops).
let items = std::mem::take(&mut self.cf_release_pool);
if !items.is_empty() {
log::debug!("releasing {} remaining CF objects", items.len());
}
let cached = std::mem::take(&mut self.cached_fonts);
let mut all = items;
all.extend(cached.into_iter().flatten().map(|d| pooled(d.0)));
if let Some(t) = &self.cf_release_thread {
t.release(all);
}
}
}
/// Copy a CTRun's glyphs, advances, positions and string indices
/// (`getGlyphsPtr() orelse getGlyphs(alloc)` etc.).
fn read_run(ctrun: &CTRun) -> CtRun {
// SAFETY: plain CoreText getters on a valid run.
let (status, n) = unsafe { (ctrun.status(), ctrun.glyph_count() as usize) };
let non_ltr = status.intersects(CTRunStatus::NonMonotonic | CTRunStatus::RightToLeft);
if n == 0 {
return CtRun {
non_ltr,
glyphs: Vec::new(),
};
}
let full = CFRange {
location: 0,
length: 0,
};
let mut glyphs = vec![0u16; n];
let mut advances = vec![CGSize::new(0.0, 0.0); n];
let mut positions = vec![CGPoint::new(0.0, 0.0); n];
let mut indices = vec![0isize; n];
// SAFETY: each buffer holds `n` elements, which is the glyph count of the
// run; a zero-length range copies everything.
unsafe {
ctrun.glyphs(full, NonNull::new_unchecked(glyphs.as_mut_ptr()));
ctrun.advances(full, NonNull::new_unchecked(advances.as_mut_ptr()));
ctrun.positions(full, NonNull::new_unchecked(positions.as_mut_ptr()));
ctrun.string_indices(full, NonNull::new_unchecked(indices.as_mut_ptr()));
}
CtRun {
non_ltr,
glyphs: (0..n)
.map(|i| RunGlyph {
glyph: glyphs[i],
advance_width: advances[i].width,
x: positions[i].x,
y: positions[i].y,
index: indices[i] as usize,
})
.collect(),
}
}
@@ -0,0 +1,400 @@
//! Platform-independent part of the CoreText shaper
//! (`font/shaper/coretext.zig`): building the UTF-16 string / codepoint
//! table for a run, and converting CoreText glyph runs to shaped cells.
//!
//! The CoreText calls themselves live in `shaper/coretext.rs` (Apple
//! targets); this module takes plain data so the (subtle) cluster /
//! ligature heuristics can be unit tested on every platform.
use crate::shape::Cell;
/// One entry of the codepoint table. Surrogate pairs occupy two entries so
/// that indices match the UTF-16 string handed to CoreText; the second one
/// has `codepoint == 0`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Codepoint {
pub codepoint: u32,
pub cluster: u32,
}
/// The state of the current run (`RunState`).
#[derive(Debug, Default)]
pub struct RunState {
pub codepoints: Vec<Codepoint>,
pub unichars: Vec<u16>,
}
impl RunState {
pub fn reset(&mut self) {
self.codepoints.clear();
self.unichars.clear();
}
/// `RunIteratorHook.addCodepoint`.
pub fn add_codepoint(&mut self, cp: u32, cluster: u32) {
// Build our UTF-16 string for CoreText
let (units, len) = crate::face::coretext_math::utf16_units(cp);
self.unichars.extend_from_slice(&units[..len]);
// Build our reverse lookup table for codepoints to clusters
self.codepoints.push(Codepoint {
codepoint: cp,
cluster,
});
// If the UTF-16 codepoint is a pair then we need to insert
// a dummy entry so that the CTRunGetStringIndices() function
// maps correctly.
if len == 2 {
self.codepoints.push(Codepoint {
codepoint: 0,
cluster,
});
}
}
}
/// Special fonts aren't shaped and their codepoint == glyph so we can just
/// return the codepoints as-is.
pub fn special_cells(codepoints: &[Codepoint], out: &mut Vec<Cell>) {
out.clear();
for entry in codepoints {
// We use null codepoints to pad out our list so indices match
// the UTF-16 string we constructed for CoreText. We don't want
// to emit these if this is a special font, since they're not
// part of the original run.
if entry.codepoint == 0 {
continue;
}
out.push(Cell {
x: entry.cluster as u16,
glyph_index: entry.codepoint,
..Cell::default()
});
}
}
/// One glyph of a CoreText run (the zipped glyphs / advances / positions /
/// string indices arrays).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RunGlyph {
pub glyph: u16,
pub advance_width: f64,
pub x: f64,
pub y: f64,
/// Index into the UTF-16 string (and therefore the codepoint table).
pub index: usize,
}
/// One CoreText glyph run of the line.
#[derive(Debug, Clone, Default)]
pub struct CtRun {
/// True if the run status has `non_monotonic` or `right_to_left`.
pub non_ltr: bool,
pub glyphs: Vec<RunGlyph>,
}
#[derive(Debug, Clone, Copy, Default)]
struct Offset {
cluster: u32,
x: f64,
}
/// Convert CoreText runs to cells (the loop at the end of `Shaper.shape`).
pub fn cells_from_runs(codepoints: &[Codepoint], runs: &[CtRun], out: &mut Vec<Cell>) {
// This keeps track of the current x offset (sum of advance.width) and
// the furthest cluster we've seen so far (max).
let mut run_offset = Offset::default();
// This keeps track of the cell starting x and cluster.
let mut cell_offset = Offset::default();
out.clear();
// CoreText, despite our insistence with an enforced embedding level,
// may sometimes output runs that are non-monotonic. In order to fix
// this, we check the run status for each run and if any aren't ltr
// we set this to true, which indicates that we must sort our buffer.
let mut non_ltr = false;
// CoreText may generate multiple runs even though our input to
// CoreText is already split into runs by our own run iterator.
// The runs as far as I can tell are always sequential to each
// other so we can iterate over them and just append to our
// cell buffer.
for run in runs {
if run.non_ltr {
non_ltr = true;
}
for g in &run.glyphs {
let index = g.index;
// Our cluster is also our cell X position. If the cluster changes
// then we need to reset our current cell offsets.
let cluster = codepoints[index].cluster;
if cell_offset.cluster != cluster {
// We previously asserted that the new cluster is greater
// than cell_offset.cluster, but this isn't always true.
// See e.g. the "shape Chakma vowel sign with ligature
// (vowel sign renders first)" test.
let is_after_glyph_from_current_or_next_clusters = cluster <= run_offset.cluster;
let is_first_codepoint_in_cluster = {
let mut i = index;
let mut result = true;
while i > 0 {
i -= 1;
let codepoint = codepoints[i];
// Skip surrogate pair padding
if codepoint.codepoint == 0 {
continue;
}
result = codepoint.cluster != cluster;
break;
}
result
};
// We need to reset the `cell_offset` at the start of a new
// cluster, but we do that conditionally if the codepoint
// `is_first_codepoint_in_cluster` and the cluster is not
// `is_after_glyph_from_current_or_next_clusters`, which is
// a heuristic to detect ligatures and avoid positioning
// glyphs that mark ligatures incorrectly. The idea is that
// if the first codepoint in a cluster doesn't appear in
// the stream, it's very likely that it combined with
// codepoints from a previous cluster into a ligature.
// Then, the subsequent codepoints are very likely marking
// glyphs that are placed relative to that ligature, so if
// we were to reset the `cell_offset` to align it with the
// grid, the positions would be off. The
// `!is_after_glyph_from_current_or_next_clusters` check is
// needed in case these marking glyphs come from a later
// cluster but are rendered first (see the Chakma and
// Bengali tests). In that case when we get to the
// codepoint that `is_first_codepoint_in_cluster`, but in a
// cluster that
// `is_after_glyph_from_current_or_next_clusters`, we don't
// want to reset to the grid and cause the positions to be
// off. (Note that we could go back and align the cells to
// the grid starting from the one from the cluster that
// rendered out of order, but that is more complicated so
// we don't do that for now. Also, it's TBD if there are
// exceptions to this heuristic for detecting ligatures,
// but using the logging below seems to show it works
// well.)
if is_first_codepoint_in_cluster && !is_after_glyph_from_current_or_next_clusters {
cell_offset = Offset {
cluster,
x: run_offset.x,
};
}
}
let x_offset = g.x - cell_offset.x;
out.push(Cell {
x: cell_offset.cluster as u16,
x_offset: x_offset.round() as i16,
y_offset: g.y.round() as i16,
glyph_index: u32::from(g.glyph),
});
// Add our advances to keep track of our run offsets.
// Advances apply to the NEXT cell.
run_offset.x += g.advance_width;
run_offset.cluster = run_offset.cluster.max(cluster);
}
}
// If our buffer contains some non-ltr sections we need to sort it :/
if non_ltr {
// This is EXCEPTIONALLY rare. Only happens for languages with
// complex shaping which we don't even really support properly
// right now, so are very unlikely to be used heavily by users
// of Ghostty.
//
// Zig's `std.mem.sort` is not stable but Rust's `sort_by_key` is;
// equal keys keep their relative order which is a valid result.
out.sort_by_key(|c| c.x);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state(cps: &[(u32, u32)]) -> RunState {
let mut s = RunState::default();
for &(cp, cl) in cps {
s.add_codepoint(cp, cl);
}
s
}
fn glyph(glyph: u16, adv: f64, x: f64, y: f64, index: usize) -> RunGlyph {
RunGlyph {
glyph,
advance_width: adv,
x,
y,
index,
}
}
#[test]
fn add_codepoint_bmp() {
let s = state(&[(0x41, 0), (0x42, 1)]);
assert_eq!(s.unichars, vec![0x41, 0x42]);
assert_eq!(s.codepoints.len(), 2);
}
#[test]
fn add_codepoint_surrogate_pair_pads_table() {
let s = state(&[(0x1F978, 0), (0x41, 2)]);
assert_eq!(s.unichars, vec![0xD83E, 0xDD78, 0x41]);
assert_eq!(
s.codepoints,
vec![
Codepoint {
codepoint: 0x1F978,
cluster: 0
},
Codepoint {
codepoint: 0,
cluster: 0
},
Codepoint {
codepoint: 0x41,
cluster: 2
},
]
);
}
#[test]
fn special_skips_padding() {
let s = state(&[(0x1F978, 3), (0x41, 5)]);
let mut out = Vec::new();
special_cells(&s.codepoints, &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[0].x, 3);
assert_eq!(out[0].glyph_index, 0x1F978);
assert_eq!(out[1].x, 5);
assert_eq!(out[1].glyph_index, 0x41);
}
#[test]
fn simple_run_one_glyph_per_cluster() {
let s = state(&[(0x41, 0), (0x42, 1), (0x43, 2)]);
let run = CtRun {
non_ltr: false,
glyphs: vec![
glyph(10, 8.0, 0.0, 0.0, 0),
glyph(11, 8.0, 8.0, 0.0, 1),
glyph(12, 8.0, 16.0, 0.0, 2),
],
};
let mut out = Vec::new();
cells_from_runs(&s.codepoints, &[run], &mut out);
assert_eq!(out.len(), 3);
for (i, c) in out.iter().enumerate() {
assert_eq!(c.x as usize, i);
assert_eq!(c.x_offset, 0);
assert_eq!(c.y_offset, 0);
assert_eq!(c.glyph_index, 10 + i as u32);
}
}
#[test]
fn ligature_glyph_spans_clusters() {
// "fi" shaped to one glyph (index 0) -> a single cell at cluster 0;
// the following char starts a new cell at cluster 2.
let s = state(&[(0x66, 0), (0x69, 1), (0x41, 2)]);
let run = CtRun {
non_ltr: false,
glyphs: vec![glyph(50, 16.0, 0.0, 0.0, 0), glyph(11, 8.0, 16.0, 0.0, 2)],
};
let mut out = Vec::new();
cells_from_runs(&s.codepoints, &[run], &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[0].x, 0);
assert_eq!(out[1].x, 2);
assert_eq!(out[1].x_offset, 0);
}
#[test]
fn mark_after_ligature_keeps_ligature_cell() {
// Clusters 0,1 ligate; a mark in cluster 2 positioned relative to
// the ligature: first codepoint of cluster 2 isn't "first" in the
// stream (it's index 2, preceded by cluster 1) so it IS first in
// cluster; cluster 2 > run_offset.cluster(0) so offset resets.
let s = state(&[(0x66, 0), (0x69, 1), (0x301, 2)]);
let run = CtRun {
non_ltr: false,
glyphs: vec![glyph(50, 16.0, 0.0, 0.0, 0), glyph(99, 0.0, 14.0, 3.0, 2)],
};
let mut out = Vec::new();
cells_from_runs(&s.codepoints, &[run], &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[1].x, 2);
// cell offset reset to run_offset.x = 16 => x_offset = 14 - 16
assert_eq!(out[1].x_offset, -2);
assert_eq!(out[1].y_offset, 3);
}
#[test]
fn glyph_from_earlier_cluster_does_not_reset_offset() {
// Glyph for cluster 1 comes after glyph for cluster 2 (rendered out
// of order): cluster 1 <= run_offset.cluster (2) => no reset, the
// cell stays at the cluster 2 offset.
let s = state(&[(0x41, 0), (0x42, 1), (0x43, 2)]);
let run = CtRun {
non_ltr: false,
glyphs: vec![
glyph(1, 8.0, 0.0, 0.0, 0),
glyph(3, 8.0, 8.0, 0.0, 2),
glyph(2, 8.0, 16.0, 0.0, 1),
],
};
let mut out = Vec::new();
cells_from_runs(&s.codepoints, &[run], &mut out);
assert_eq!(out.len(), 3);
// cluster 2's glyph: reset (2 > 0): offset x = 8
assert_eq!(out[1].x, 2);
// cluster 1 glyph after cluster 2: 1 <= 2 so no reset, stays at 2
assert_eq!(out[2].x, 2);
assert_eq!(out[2].x_offset, 8);
}
#[test]
fn non_ltr_sorts_by_x() {
let s = state(&[(0x41, 0), (0x42, 1)]);
let run = CtRun {
non_ltr: true,
glyphs: vec![glyph(2, 8.0, 0.0, 0.0, 1), glyph(1, 8.0, 8.0, 0.0, 0)],
};
let mut out = Vec::new();
cells_from_runs(&s.codepoints, &[run], &mut out);
assert!(out.windows(2).all(|w| w[0].x <= w[1].x));
}
#[test]
fn surrogate_padding_skipped_when_looking_back() {
// Emoji (pair) at cluster 0 followed by 'A' at cluster 1. The 'A'
// glyph looks back past the padding entry to the real codepoint.
let s = state(&[(0x1F978, 0), (0x41, 1)]);
let run = CtRun {
non_ltr: false,
glyphs: vec![glyph(7, 16.0, 0.0, 0.0, 0), glyph(8, 8.0, 16.0, 0.0, 2)],
};
let mut out = Vec::new();
cells_from_runs(&s.codepoints, &[run], &mut out);
assert_eq!(out[1].x, 1);
assert_eq!(out[1].x_offset, 0);
}
}
+1 -1
View File
@@ -12,7 +12,7 @@
use super::hb;
use crate::{
face::freetype::Face,
face::Face,
shape::{self, Cell, DEFAULT_FEATURES, FeatureList, TextRun},
shared_grid::SharedGrid,
};
+1 -1
View File
@@ -6,7 +6,7 @@ use std::ptr::NonNull;
use harfbuzz_sys as sys;
use crate::face::freetype::HbFont;
use crate::face::HbFont;
/// A HarfBuzz feature (`hb_feature_t`) applied globally.
pub type Feature = sys::hb_feature_t;
+8 -3
View File
@@ -2,10 +2,15 @@
#[cfg(not(target_arch = "wasm32"))]
pub mod cache;
#[cfg(font_coretext_shaper)]
mod cf_release;
#[cfg(font_coretext_shaper)]
pub mod coretext;
pub mod coretext_cells;
pub mod feature;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(font_harfbuzz)]
pub mod harfbuzz;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(font_harfbuzz)]
mod hb;
#[cfg(not(target_arch = "wasm32"))]
pub mod noop;
@@ -14,5 +19,5 @@ pub mod run;
pub mod web_canvas;
#[cfg(all(test, not(target_arch = "wasm32")))]
#[cfg(all(test, font_harfbuzz, font_fontconfig))]
pub(crate) mod test_util;
+35 -5
View File
@@ -3,10 +3,25 @@
//! font's glyph index.
use crate::{
face::freetype::Face,
shape::{self, Cell},
face::Face,
shape::{self, Cell, TextRun},
shared_grid::SharedGrid,
};
/// Never produced (the no-op shaper can't fail to initialize); exists so the
/// API matches the other shapers (`shape::ShapeError`).
#[derive(Debug, thiserror::Error)]
pub enum ShapeError {}
/// Errors from [`Shaper::shape_run`].
#[derive(Debug, thiserror::Error)]
pub enum ShapeRunError {
#[error(transparent)]
GetFace(#[from] crate::collection::GetFaceError),
#[error(transparent)]
Shape(#[from] ShapeError),
}
#[derive(Debug, Clone, Copy)]
struct Codepoint {
codepoint: u32,
@@ -24,8 +39,9 @@ pub struct Shaper {
}
impl Shaper {
pub fn new(_opts: &shape::Options) -> Shaper {
Shaper::default()
#[allow(clippy::unnecessary_wraps)]
pub fn new(_opts: &shape::Options) -> Result<Shaper, ShapeError> {
Ok(Shaper::default())
}
pub fn end_frame(&self) {}
@@ -45,6 +61,20 @@ impl Shaper {
pub fn finalize(&mut self) {}
/// Shape the given text run (looks the face up in `grid`).
pub fn shape_run(
&mut self,
grid: &SharedGrid,
run: &TextRun,
) -> Result<&[Cell], ShapeRunError> {
if run.font_index.special().is_none() {
let mut guard = grid.write();
let face = guard.resolver.collection.get_face(run.font_index)?;
return Ok(self.shape(Some(face)));
}
Ok(self.shape(None))
}
/// Shape the current run. `face` is `None` for special fonts (sprite),
/// whose codepoint == glyph index.
///
@@ -76,7 +106,7 @@ mod tests {
#[test]
fn noop_special_passthrough() {
let mut s = Shaper::new(&shape::Options::default());
let mut s = Shaper::new(&shape::Options::default()).unwrap();
s.prepare();
s.add_codepoint(0x41, 0);
s.add_codepoint(0x42, 2);
+14
View File
@@ -160,6 +160,7 @@ pub trait RunIteratorHook {
fn finalize(&mut self);
}
#[cfg(font_harfbuzz)]
impl RunIteratorHook for crate::shaper::harfbuzz::Shaper {
fn prepare(&mut self) {
Self::prepare(self);
@@ -172,6 +173,19 @@ impl RunIteratorHook for crate::shaper::harfbuzz::Shaper {
}
}
#[cfg(font_coretext_shaper)]
impl RunIteratorHook for crate::shaper::coretext::Shaper {
fn prepare(&mut self) {
Self::prepare(self);
}
fn add_codepoint(&mut self, cp: u32, cluster: u32) {
Self::add_codepoint(self, cp, cluster);
}
fn finalize(&mut self) {
Self::finalize(self);
}
}
impl RunIteratorHook for crate::shaper::noop::Shaper {
fn prepare(&mut self) {
Self::prepare(self);
+48 -19
View File
@@ -33,7 +33,7 @@ use crate::{
codepoint_map::CodepointMap,
codepoint_resolver::{CodepointResolver, StyleStatus},
collection::{self, AddOptions, Collection, LoadOptions, SizeAdjustment},
discovery::{Descriptor, Discover, Fontconfig},
discovery::{Descriptor, Discover},
embedded,
face::{self, DesiredSize, FontData},
library::{Library, LibraryError},
@@ -47,7 +47,7 @@ pub enum RefError {
#[error(transparent)]
Discover(#[from] crate::discovery::DiscoverError),
#[error("font discovery failed: {0}")]
DiscoverNext(#[from] crate::fontconfig::Error),
DiscoverNext(#[from] crate::discovery::NextError),
#[error(transparent)]
Add(#[from] collection::AddError),
#[error(transparent)]
@@ -57,7 +57,7 @@ pub enum RefError {
#[error(transparent)]
GetFace(#[from] collection::GetFaceError),
#[error(transparent)]
Face(#[from] face::freetype::FaceError),
Face(#[from] face::FaceError),
#[error(transparent)]
Grid(#[from] shared_grid::InitError),
#[error(transparent)]
@@ -321,20 +321,49 @@ impl SharedGridSet {
fallback(Style::Regular, SizeAdjustment::None),
)?;
// Emoji fallback. (Zig skips this on macOS, which is out of scope.)
c.add(
Face::init(&self.font_lib, FontData::Static(embedded::EMOJI), face_opts)?,
// No size adjustment for emojis.
fallback(Style::Regular, SizeAdjustment::None),
)?;
c.add(
Face::init(
&self.font_lib,
FontData::Static(embedded::EMOJI_TEXT),
face_opts,
)?,
fallback(Style::Regular, SizeAdjustment::None),
)?;
// On macOS, always search for and add the Apple Emoji font
// as our preferred emoji font for fallback. We do this in case
// people add other emoji fonts to their system, we always want to
// prefer the official one. Users can override this by explicitly
// specifying a font-family for emoji.
#[cfg(font_coretext)]
{
let disco = Self::discover(inner);
let mut disco_it = disco.discover(&Descriptor {
family: Some("Apple Color Emoji".into()),
..Default::default()
})?;
if let Some(face) = disco_it.next()? {
c.add_deferred(
face,
AddOptions {
style: Style::Regular,
fallback: true,
// No size adjustment for emojis.
size_adjustment: SizeAdjustment::None,
},
)?;
}
}
// Emoji fallback. We don't include this on Mac since Mac is expected
// to always have the Apple Emoji available on the system.
#[cfg(not(font_coretext))]
{
c.add(
Face::init(&self.font_lib, FontData::Static(embedded::EMOJI), face_opts)?,
// No size adjustment for emojis.
fallback(Style::Regular, SizeAdjustment::None),
)?;
c.add(
Face::init(
&self.font_lib,
FontData::Static(embedded::EMOJI_TEXT),
face_opts,
)?,
fallback(Style::Regular, SizeAdjustment::None),
)?;
}
Ok(c)
}
@@ -361,8 +390,8 @@ impl SharedGridSet {
/// Initialize once and return the font discovery mechanism. This remains
/// initialized throughout the lifetime of the application because some
/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit.
fn discover(inner: &mut Inner) -> &Fontconfig {
inner.font_discover.get_or_insert_with(Fontconfig::init)
fn discover(inner: &mut Inner) -> &Discover {
inner.font_discover.get_or_insert_with(Discover::init)
}
}