Files
ghostty-rs/crates/ghostty-font/src/deferred_face_coretext.rs
T
ddidderr bab49ee596 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)
2026-10-12 06:02:39 +02:00

203 lines
6.7 KiB
Rust

//! 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());
}
}