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:
1 parent
76771bed0d
commit
bab49ee596
124 files changed
+14936
-1242
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user