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
3c8fae1089
commit
452b782149
124 files changed
+14936
-1242
No files matched your search
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
use super::hb;
|
||||
use crate::{
|
||||
face::freetype::Face,
|
||||
face::Face,
|
||||
shape::{self, Cell, DEFAULT_FEATURES, FeatureList, TextRun},
|
||||
shared_grid::SharedGrid,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user