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

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

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

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

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

No files matched your search

+884
View File
@@ -0,0 +1,884 @@
//! CoreText font face (macOS / iOS). Port of `font/face/coretext.zig`.
//!
//! Compile-checked on Linux with `cargo check --target aarch64-apple-darwin`;
//! it cannot be run here. The platform-independent math (metrics priority,
//! glyph bearings/canvas size, synthetic bold width) lives in
//! [`super::coretext_math`] and is unit tested everywhere.
#![allow(unsafe_code)]
use std::ptr::{self, NonNull};
use objc2_core_foundation::{CFData, CFNumber, CFRetained, CFString, CGAffineTransform, CGPoint};
use objc2_core_graphics::{
CGBitmapContextCreate,
CGColorSpace,
CGContext,
CGImageAlphaInfo,
CGImageByteOrderInfo,
CGTextDrawingMode,
kCGColorSpaceDisplayP3,
kCGColorSpaceLinearGray,
};
use objc2_core_text::{CTFont, CTFontOrientation, CTFontSymbolicTraits};
use super::{
DesiredSize,
FontData,
Options,
RenderOptions,
Variation,
coretext_math::{self as math, CtMetrics, Measurements, Rect, synthetic_bold_line_width},
};
use crate::{
atlas::{self, Atlas},
coretext_util::{self as util, CfBox},
glyph::Glyph,
library::Library,
metrics::FaceMetrics,
opentype,
};
#[derive(Debug, thiserror::Error)]
pub enum FaceError {
/// CoreText could not create a font from the given data.
#[error("font initialization failed")]
FontInitFailure,
/// The `SVG ` table is malformed.
#[error("invalid SVG table")]
InvalidSvgTable,
/// The face has no backing file (used by the `coretext_freetype` backend).
#[error("font has no file")]
FontHasNoFile,
/// The font file path could not be decoded.
#[error("font path can't be decoded")]
FontPathCantDecode,
/// Creating a font variation failed.
#[error("variation failed")]
VariationFailed,
}
#[derive(Debug, thiserror::Error)]
pub enum RenderError {
#[error(transparent)]
Atlas(#[from] atlas::Error),
/// The atlas format does not match the glyph's color depth.
#[error("invalid atlas format")]
InvalidAtlasFormat,
/// A CoreGraphics color space or bitmap context could not be created.
#[error("coregraphics resource creation failed")]
ContextFailed,
/// The glyph index doesn't fit a CoreText `CGGlyph` (16 bits).
#[error("invalid glyph index")]
InvalidGlyph,
}
/// A harfbuzz font owned by a [`Face`] (only used by the
/// `coretext_harfbuzz` backend).
#[cfg(font_harfbuzz)]
pub struct HbFont(NonNull<harfbuzz_sys::hb_font_t>);
// SAFETY: hb_font_t is thread-safe for shaping when not mutated; Ghostty uses
// it behind the collection lock exactly like upstream.
#[cfg(font_harfbuzz)]
unsafe impl Send for HbFont {}
// SAFETY: see above.
#[cfg(font_harfbuzz)]
unsafe impl Sync for HbFont {}
#[cfg(font_harfbuzz)]
impl HbFont {
/// The raw `hb_font_t` pointer (valid while the `Face` lives).
pub fn as_ptr(&self) -> *mut harfbuzz_sys::hb_font_t {
self.0.as_ptr()
}
}
#[cfg(font_harfbuzz)]
impl Drop for HbFont {
fn drop(&mut self) {
// SAFETY: we own one reference to the hb font.
unsafe { harfbuzz_sys::hb_font_destroy(self.0.as_ptr()) };
}
}
pub struct Face {
/// Our font face
font: CfBox<CTFont>,
/// Harfbuzz font corresponding to this face. We only use this
/// if we're using Harfbuzz.
#[cfg(font_harfbuzz)]
hb_font: HbFont,
/// Set quirks.disableDefaultFontFeatures
pub quirks_disable_default_font_features: bool,
/// True if this font face should be rasterized with a synthetic bold
/// effect. This is used for fonts that don't have a bold variant.
pub synthetic_bold: Option<f64>,
/// If the face can possibly be colored, then this is the state
/// used to check for color information. This is None if the font
/// can't possibly be colored (i.e. doesn't have SVG, sbix, etc
/// tables).
color: Option<ColorState>,
/// The current size this font is set to.
pub size: DesiredSize,
}
impl std::fmt::Debug for Face {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Face")
.field("size", &self.size)
.field("synthetic_bold", &self.synthetic_bold)
.finish_non_exhaustive()
}
}
/// The matrix applied to a regular font to auto-italicize it.
fn italic_skew() -> CGAffineTransform {
CGAffineTransform {
a: 1.0,
b: 0.0,
c: math::ITALIC_SKEW_C, // approx. tan(15)
d: 1.0,
tx: 0.0,
ty: 0.0,
}
}
impl Face {
/// Initialize a CoreText-based font from a TTF/TTC in memory.
pub fn init(_lib: &Library, source: FontData, opts: Options) -> Result<Face, FaceError> {
let data = match source {
FontData::Static(s) => CFData::from_static_bytes(s),
FontData::Shared(s) => CFData::from_bytes(&s),
};
// SAFETY: `data` is a valid CFData.
let desc = unsafe { objc2_core_text::CTFontManagerCreateFontDescriptorFromData(&data) }
.ok_or(FaceError::FontInitFailure)?;
// SAFETY: valid descriptor, null matrix is allowed.
let ct_font = unsafe { CTFont::with_font_descriptor(&desc, 12.0, ptr::null()) };
Self::init_font_copy(&ct_font, opts)
}
/// Initialize a CoreText-based face from another initialized font face
/// but with a new size. This is often how CoreText fonts are initialized
/// because the font is loaded at a default size during discovery, and then
/// adjusted to the final size for final load.
pub fn init_font_copy(base: &CTFont, opts: Options) -> Result<Face, FaceError> {
// Create a copy. The copyWithAttributes docs say the size is in points,
// but we need to scale the points by the DPI and to do that we use our
// function called "pixels".
// SAFETY: null matrix and no descriptor are allowed.
let ct_font =
unsafe { base.copy_with_attributes(f64::from(opts.size.pixels()), ptr::null(), None) };
Self::init_font(ct_font, opts)
}
/// Initialize a face with a CTFont. This takes ownership of the CTFont.
pub fn init_font(ct_font: CFRetained<CTFont>, opts: Options) -> Result<Face, FaceError> {
// SAFETY: plain CoreText call.
let traits = unsafe { ct_font.symbolic_traits() };
#[cfg(font_harfbuzz)]
let hb_font = {
// SAFETY: valid CTFont; hb takes its own reference.
let p = unsafe { harfbuzz_sys::coretext::hb_coretext_font_create(&ct_font) };
let hb = NonNull::new(p)
.map(HbFont)
.ok_or(FaceError::FontInitFailure)?;
// 26.6 fixed point scale
let scale = (f64::from(opts.size.pixels()) * 64.0).round() as i32;
// SAFETY: valid hb font.
unsafe { harfbuzz_sys::hb_font_set_scale(hb.as_ptr(), scale, scale) };
hb
};
let color = if traits.contains(CTFontSymbolicTraits::TraitColorGlyphs) {
Some(ColorState::new(&ct_font)?)
} else {
None
};
let result = Face {
font: CfBox::new(ct_font),
#[cfg(font_harfbuzz)]
hb_font,
quirks_disable_default_font_features: false,
synthetic_bold: None,
color,
size: opts.size,
};
// In debug mode, we output information about available variation axes,
// if they exist.
if cfg!(debug_assertions)
&& let Some(axes) = util::variation_axes(&result.font)
{
log::debug!("variation axes font={}", result.name());
for axis in axes {
log::debug!(
"variation axis: name={} id={} min={} max={} def={}",
axis.name,
String::from_utf8_lossy(&axis.id.to_be_bytes()),
axis.min,
axis.max,
axis.default,
);
}
}
Ok(result)
}
/// Return a new face that is the same as this but has a transformation
/// matrix applied to italicize it.
pub fn synthetic_italic(&self, opts: Options) -> Result<Face, FaceError> {
let skew = italic_skew();
// SAFETY: `skew` outlives the call.
let ct_font = unsafe { self.font.copy_with_attributes(0.0, &raw const skew, None) };
Self::init_font(ct_font, opts)
}
/// Return a new face that is the same as this but applies a synthetic
/// bold effect to it. This is useful for fonts that don't have a bold
/// variant.
pub fn synthetic_bold(&self, opts: Options) -> Result<Face, FaceError> {
// SAFETY: null matrix and no descriptor are allowed.
let ct_font = unsafe { self.font.copy_with_attributes(0.0, ptr::null(), None) };
let mut face = Self::init_font(ct_font, opts)?;
face.synthetic_bold = Some(synthetic_bold_line_width(f64::from(opts.size.points)));
Ok(face)
}
/// Returns the font name (the family name).
pub fn name(&self) -> String {
// SAFETY: plain CoreText call.
util::cf_to_string(&*unsafe { self.font.family_name() })
}
/// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc.
pub fn set_size(&mut self, opts: Options) -> Result<(), FaceError> {
// We just create a copy and replace ourself
let face = Self::init_font_copy(&self.font, opts)?;
*self = face;
Ok(())
}
/// Set the variation axes for this font. This will modify this font
/// in-place.
pub fn set_variations(&mut self, vs: &[Variation], opts: Options) -> Result<(), FaceError> {
// If we have no variations, we don't need to do anything.
if vs.is_empty() {
return Ok(());
}
// Create a new font descriptor with all the variations set.
// SAFETY: plain CoreText call.
let mut desc = unsafe { self.font.font_descriptor() };
for v in vs {
let id = CFNumber::new_i32(v.id.0 as i32);
// SAFETY: valid number and descriptor.
desc = unsafe { desc.copy_with_variation(&id, v.value) };
}
// Put our current size in the opts so that we don't change size.
let mut new_opts = opts;
new_opts.size = self.size;
// Initialize a font based on these attributes.
// SAFETY: null matrix is allowed.
let ct_font = unsafe {
self.font
.copy_with_attributes(0.0, ptr::null(), Some(&desc))
};
let face = Self::init_font(ct_font, new_opts)?;
*self = face;
Ok(())
}
/// Returns true if the face has any glyphs that are colorized.
/// To determine if an individual glyph is colorized you must use
/// `is_color_glyph`.
pub fn has_color(&self) -> bool {
self.color.is_some()
}
/// Returns true if the given glyph ID is colorized.
pub fn is_color_glyph(&self, glyph_id: u32) -> bool {
match &self.color {
Some(c) => c.is_color_glyph(glyph_id),
None => false,
}
}
/// Returns the glyph index for the given Unicode code point. If this
/// face doesn't support this glyph, None is returned.
pub fn glyph_index(&self, cp: u32) -> Option<u32> {
let (glyph, ok) = util::glyphs_for_codepoint(&self.font, cp);
if !ok {
return None;
}
Some(u32::from(glyph))
}
/// Render a glyph using the glyph index. The rendered glyph is stored in
/// the given texture atlas.
pub fn render_glyph(
&self,
atlas: &mut Atlas,
glyph_index: u32,
opts: RenderOptions,
) -> Result<Glyph, RenderError> {
let mut glyph = [u16::try_from(glyph_index).map_err(|_| RenderError::InvalidGlyph)?];
// SAFETY: one valid glyph in the buffer.
let ct_rect = unsafe {
self.font.bounding_rects_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(glyph.as_mut_ptr()),
ptr::null_mut(),
1,
)
};
// This is in a coordinate space with (0.0, 0.0)
// in the bottom left and +Y pointing up.
let rect = Rect {
x: ct_rect.origin.x,
y: ct_rect.origin.y,
width: ct_rect.size.width,
height: ct_rect.size.height,
};
// Determine whether this is a color glyph.
let is_color = self.is_color_glyph(glyph_index);
// And whether it's (probably) a bitmap (sbix).
let sbix = is_color && self.color.as_ref().is_some_and(|c| c.sbix);
let Some(layout) = math::glyph_layout(rect, sbix, self.synthetic_bold, &opts) else {
return Ok(Glyph {
width: 0,
height: 0,
offset_x: 0,
offset_y: 0,
atlas_x: 0,
atlas_y: 0,
});
};
// Settings that are specific to if we are rendering text or emoji.
let depth = math::bitmap_depth(is_color);
let (space, context_opts) = if is_color {
// SAFETY: valid static CFString constant.
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceDisplayP3 }));
(
space,
CGImageByteOrderInfo::Order32Little.0 | CGImageAlphaInfo::PremultipliedFirst.0,
)
} else {
// SAFETY: valid static CFString constant.
let space = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceLinearGray }));
(space, CGImageAlphaInfo::Only.0)
};
let space = space.ok_or(RenderError::ContextFailed)?;
// This is just a safety check.
if u32::from(atlas.format.depth()) != depth {
log::warn!(
"font atlas color depth doesn't equal font color depth atlas={} font={}",
atlas.format.depth(),
depth,
);
return Err(RenderError::InvalidAtlasFormat);
}
// Our buffer for rendering. We could cache this but glyph rasterization
// usually stabilizes pretty quickly and is very infrequent so I think
// the allocation overhead is acceptable compared to the cost of
// caching it forever or having to deal with a cache lifetime.
let width = layout.px_width as usize;
let height = layout.px_height as usize;
let mut buf = vec![0u8; width * height * depth as usize];
// SAFETY: `buf` is `width * height * depth` bytes, matching the
// bytes-per-row and dimensions given; it outlives the context (the
// context is dropped before `buf` at the end of this function).
let ctx = unsafe {
CGBitmapContextCreate(
buf.as_mut_ptr().cast(),
width,
height,
8,
width * depth as usize,
Some(&space),
context_opts,
)
}
.ok_or(RenderError::ContextFailed)?;
let c = Some(&*ctx);
// Perform an initial fill. This ensures that we don't have any
// uninitialized pixels in the bitmap.
if is_color {
CGContext::set_rgb_fill_color(c, 0.0, 0.0, 0.0, 0.0);
} else {
CGContext::set_gray_fill_color(c, 0.0, 0.0);
}
CGContext::fill_rect(
c,
objc2_core_foundation::CGRect::new(
CGPoint::new(0.0, 0.0),
objc2_core_foundation::CGSize::new(width as f64, height as f64),
),
);
// "Font smoothing" is what we call "thickening", it's an attempt
// to compensate for optical thinning of fonts, but at this point
// it's just something that makes the text look closer to system
// applications if users want that.
CGContext::set_allows_font_smoothing(c, true);
CGContext::set_should_smooth_fonts(c, opts.thicken);
// Subpixel positioning allows glyphs to be placed at non-integer
// coordinates. We need this for our alignment.
CGContext::set_allows_font_subpixel_positioning(c, true);
CGContext::set_should_subpixel_position_fonts(c, true);
// We don't want subpixel quantization, since we very carefully
// manage the position of our glyphs ourselves, and dont want to
// mess that up.
CGContext::set_allows_font_subpixel_quantization(c, false);
CGContext::set_should_subpixel_quantize_fonts(c, false);
// Anti-aliasing is self explanatory.
CGContext::set_allows_antialiasing(c, true);
CGContext::set_should_antialias(c, true);
// Set our color for drawing
if is_color {
CGContext::set_rgb_fill_color(c, 1.0, 1.0, 1.0, 1.0);
CGContext::set_rgb_stroke_color(c, 1.0, 1.0, 1.0, 1.0);
} else {
let strength = f64::from(opts.thicken_strength);
CGContext::set_gray_fill_color(c, strength / 255.0, 1.0);
CGContext::set_gray_stroke_color(c, strength / 255.0, 1.0);
}
// If we are drawing with synthetic bold then use a fill stroke
// which strokes the outlines of the glyph making a more bold look.
if let Some(line_width) = self.synthetic_bold {
CGContext::set_text_drawing_mode(c, CGTextDrawingMode::FillStroke);
CGContext::set_line_width(c, line_width);
}
// Translate our drawing context so that when we draw our
// glyph the bottom/left edge is at the correct sub-pixel
// position. The bottom/left edges are guaranteed to be at
// exactly [0, 0] relative to this because when we call to
// `drawGlyphs`, we pass the negated bearings.
CGContext::translate_ctm(
c,
layout.frac_x + f64::from(layout.canvas_padding),
layout.frac_y + f64::from(layout.canvas_padding),
);
// Scale the drawing context so that when we draw
// our glyph it's stretched to the constrained size.
CGContext::scale_ctm(c, layout.scale_x, layout.scale_y);
// Draw our glyph.
//
// We offset the position by the negated bearings so that the
// glyph is drawn at exactly [0, 0], which is then offset to
// the appropriate fractional position by the translation we
// did before scaling.
let mut position = CGPoint::new(-layout.rect_x, -layout.rect_y);
// SAFETY: one glyph and one position, valid context.
unsafe {
self.font.draw_glyphs(
NonNull::new_unchecked(glyph.as_mut_ptr()),
NonNull::from(&mut position),
1,
&ctx,
);
}
// The context must be released before we read the buffer.
drop(ctx);
// Write our rasterized glyph to the atlas.
let region = atlas.reserve(layout.px_width, layout.px_height)?;
atlas.set(region, &buf);
Ok(Glyph {
width: layout.px_width,
height: layout.px_height,
offset_x: layout.offset_x(),
offset_y: layout.offset_y(),
atlas_x: region.x,
atlas_y: region.y,
})
}
/// Get the `FaceMetrics` for this face.
pub fn get_metrics(&self) -> FaceMetrics {
let ct_font: &CTFont = &self.font;
// Read the 'head' table out of the font data.
//
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
// the table format is byte-identical to the 'head' table, so if we
// can't find 'head' we try 'bhed' instead before failing.
//
// ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
let head = util::font_table_bytes(ct_font, b"head")
.or_else(|| util::font_table_bytes(ct_font, b"bhed"))
.and_then(|d| {
opentype::Head::init(&d)
.inspect_err(|e| log::warn!("error parsing head table: {e}"))
.ok()
});
// Read the 'post' table out of the font data.
let post = util::font_table_bytes(ct_font, b"post").and_then(|d| {
opentype::Post::init(&d)
.inspect_err(|e| log::warn!("error parsing post table: {e}"))
.ok()
});
// Read the 'OS/2' table out of the font data if it's available.
let os2 = util::font_table_bytes(ct_font, b"OS/2").and_then(|d| {
opentype::OS2::init(&d)
.inspect_err(|e| log::warn!("error parsing OS/2 table: {e}"))
.ok()
});
// Read the 'hhea' table out of the font data.
let hhea = util::font_table_bytes(ct_font, b"hhea").and_then(|d| {
opentype::Hhea::init(&d)
.inspect_err(|e| log::warn!("error parsing hhea table: {e}"))
.ok()
});
// SAFETY: plain CoreText getters.
let (units_per_em, px_per_em, ct) = unsafe {
(
head.map_or_else(
|| f64::from(ct_font.units_per_em()),
|h| f64::from(h.unitsPerEm),
),
ct_font.size(),
CtMetrics {
ascent: ct_font.ascent(),
descent: ct_font.descent(),
leading: ct_font.leading(),
cap_height: ct_font.cap_height(),
x_height: ct_font.x_height(),
},
)
};
// Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take
// whatever is widest.
//
// ASCII height is calculated as the height of the overall bounding
// box of the same characters.
let (cell_width, ascii_height) = {
// All the visible ASCII chars
let mut unichars: Vec<u16> = (32u16..127).collect();
let n = unichars.len();
// Get our glyph IDs for the ASCII chars
let mut glyphs = vec![0u16; n];
// SAFETY: both buffers have `n` elements.
unsafe {
ct_font.glyphs_for_characters(
NonNull::new_unchecked(unichars.as_mut_ptr()),
NonNull::new_unchecked(glyphs.as_mut_ptr()),
n as isize,
);
}
// Get all our advances
let mut advances = vec![objc2_core_foundation::CGSize::new(0.0, 0.0); n];
// SAFETY: buffers have `n` elements.
unsafe {
ct_font.advances_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(glyphs.as_mut_ptr()),
advances.as_mut_ptr(),
n as isize,
);
}
// Find the maximum advance
let max = advances.iter().fold(0.0f64, |m, a| a.width.max(m));
// Get the overall bounding rect for the glyphs
// SAFETY: `glyphs` has `n` elements.
let rect = unsafe {
ct_font.bounding_rects_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(glyphs.as_mut_ptr()),
ptr::null_mut(),
n as isize,
)
};
(max, rect.size.height)
};
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
let ic_width = self.glyph_index('水' as u32).and_then(|glyph| {
let mut g = [glyph as u16];
// SAFETY: one valid glyph.
let (advance, bounds) = unsafe {
(
ct_font.advances_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(g.as_mut_ptr()),
ptr::null_mut(),
1,
),
ct_font.bounding_rects_for_glyphs(
CTFontOrientation::Horizontal,
NonNull::new_unchecked(g.as_mut_ptr()),
ptr::null_mut(),
1,
),
)
};
// If the advance of the glyph is less than the width of the actual
// glyph then we just treat it as invalid since it's probably wrong
// and using it for size normalization will instead make the font
// way too big.
//
// This can sometimes happen if there's a CJK font that has been
// patched with the nerd fonts patcher and it butchers the advance
// values so the advance ends up half the width of the actual glyph.
if !math::ic_width_is_valid(bounds.size.width, advance) {
log::warn!(
"(getMetrics) Width of glyph '水' for font \"{}\" is greater than its advance ({} > {}), discarding ic_width metric.",
self.name(),
bounds.size.width,
advance,
);
return None;
}
Some(advance)
});
math::face_metrics(
px_per_em,
units_per_em,
hhea.as_ref(),
os2.as_ref(),
post.as_ref(),
&ct,
Measurements {
cell_width,
ascii_height,
ic_width,
},
)
}
/// Copy the font table data for the given tag.
pub fn copy_table(&self, tag: &[u8; 4]) -> Result<Option<Vec<u8>>, FaceError> {
Ok(util::font_table_bytes(&self.font, tag))
}
/// The underlying CTFont (for shapers).
pub(crate) fn ct_font(&self) -> &CTFont {
&self.font
}
/// The harfbuzz font corresponding to this face (for the HarfBuzz shaper).
#[cfg(font_harfbuzz)]
pub fn hb_font(&self) -> &HbFont {
&self.hb_font
}
/// The `FontHasNoFile`-style path of this face's file (for the
/// `coretext_freetype` backend this is done by the deferred face).
pub fn font_descriptor(&self) -> CFRetained<objc2_core_text::CTFontDescriptor> {
// SAFETY: plain CoreText call.
unsafe { self.font.font_descriptor() }
}
}
/// The state associated with a font face that may have colorized glyphs.
/// This is used to determine if a specific glyph ID is colorized.
struct ColorState {
/// True if there is an sbix font table. For now, the mere presence
/// of an sbix font table causes us to assume the glyph is colored.
/// We can improve this later.
sbix: bool,
/// The SVG font table data (if any), which we can use to determine
/// if a glyph is present in the SVG table.
svg: Option<SvgTable>,
}
/// An owned copy of the `SVG ` table and the glyph range it covers.
struct SvgTable {
data: Vec<u8>,
}
impl ColorState {
fn new(f: &CTFont) -> Result<ColorState, FaceError> {
// sbix is true if the table exists in the font data at all.
// In the future we probably want to actually parse it and
// check for glyphs.
let sbix = util::font_table(f, b"sbix").is_some_and(|d| !d.is_empty());
// Read the SVG table out of the font data.
let svg = match util::font_table_bytes(f, b"SVG ") {
None => None,
Some(data) => {
// Validate the table up front like Zig.
opentype::SVG::init(&data).map_err(|_| FaceError::InvalidSvgTable)?;
Some(SvgTable { data })
}
};
Ok(ColorState { sbix, svg })
}
/// Returns true if the given glyph ID is colored.
fn is_color_glyph(&self, glyph_id: u32) -> bool {
// Our font system uses 32-bit glyph IDs for special values but
// actual fonts only contain 16-bit glyph IDs so if we can't cast
// into it it must be false.
let Ok(glyph_u16) = u16::try_from(glyph_id) else {
return false;
};
// sbix is always true for now
if self.sbix {
return true;
}
// if we have svg data, check it
if let Some(svg) = &self.svg
&& let Ok(table) = opentype::SVG::init(&svg.data)
&& table.has_glyph(glyph_u16)
{
return true;
}
false
}
}
/// Create a CTFont by name at the given size (test/dev helper equivalent of
/// `FontDescriptor.createWithNameAndSize` + `Font.createWithFontDescriptor`).
pub fn font_with_name(name: &str, size: f64) -> CFRetained<CTFont> {
let cf_name = CFString::from_str(name);
// SAFETY: valid name; null matrix is allowed.
unsafe { CTFont::with_name(&cf_name, size, ptr::null()) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{embedded, metrics::Metrics};
fn opts(points: f32) -> Options {
Options::new(DesiredSize::new(points))
}
fn render_ascii(face: &Face) {
let mut atlas = Atlas::new(512, atlas::Format::Grayscale);
for i in 32u32..127 {
let g = face.glyph_index(i).unwrap();
face.render_glyph(
&mut atlas,
g,
RenderOptions::new(Metrics::calc(face.get_metrics())),
)
.unwrap();
}
}
#[test]
fn monaco_ascii() {
let font = font_with_name("Monaco", 12.0);
let face = Face::init_font_copy(&font, opts(12.0)).unwrap();
render_ascii(&face);
}
#[test]
fn name() {
let font = font_with_name("Menlo", 12.0);
let face = Face::init_font_copy(&font, opts(12.0)).unwrap();
assert_eq!(face.name(), "Menlo");
}
#[test]
fn emoji() {
let font = font_with_name("Apple Color Emoji", 12.0);
let face = Face::init_font_copy(&font, opts(18.0)).unwrap();
let id = face.glyph_index('🥸' as u32).unwrap();
assert!(face.is_color_glyph(id));
}
#[test]
fn in_memory() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::REGULAR), opts(12.0)).unwrap();
render_ascii(&face);
}
#[test]
fn variable() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::VARIABLE), opts(12.0)).unwrap();
render_ascii(&face);
}
#[test]
fn variable_set_variation() {
let lib = Library::init().unwrap();
let mut face = Face::init(&lib, FontData::Static(embedded::VARIABLE), opts(12.0)).unwrap();
face.set_variations(
&[Variation {
id: super::super::VariationId::init(b"wght"),
value: 400.0,
}],
opts(12.0),
)
.unwrap();
render_ascii(&face);
}
#[test]
fn svg_font_table() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::JULIA_MONO), opts(12.0)).unwrap();
let table = face.copy_table(b"SVG ").unwrap().unwrap();
assert_ne!(table.len(), 0);
}
#[test]
fn glyph_index_colored_vs_text() {
let lib = Library::init().unwrap();
let face = Face::init(&lib, FontData::Static(embedded::JULIA_MONO), opts(12.0)).unwrap();
let glyph = face.glyph_index('A' as u32).unwrap();
assert_eq!(glyph, 4);
assert!(!face.is_color_glyph(glyph));
let glyph = face.glyph_index(0xE800).unwrap();
assert_eq!(glyph, 11482);
assert!(face.is_color_glyph(glyph));
}
}
@@ -0,0 +1,956 @@
//! Platform-independent math extracted from the CoreText face
//! (`font/face/coretext.zig`) so it can be unit tested on every platform.
//!
//! The CoreText face itself only exists on Apple targets; everything here is
//! plain arithmetic on values that the face reads from CoreText / the
//! OpenType tables:
//!
//! * [`face_metrics`]: the `getMetrics` table-priority logic (OS/2 typo
//! metrics vs. hhea vs. OS/2 win metrics, underline/strikethrough
//! fallbacks, cap/ex heights).
//! * [`synthetic_bold_line_width`]: the synthetic bold stroke width.
//! * [`glyph_layout`]: the bearing / canvas-size computation of
//! `renderGlyph`.
use super::{Constraint, ConstraintSize, GlyphSize, RenderOptions};
use crate::{
metrics::FaceMetrics,
opentype::{Hhea, OS2, Post},
};
/// The matrix applied to a regular font to auto-italicize it
/// (`Face.italic_skew`): `a=1, b=0, c=tan(15deg) (approx), d=1, tx=ty=0`.
pub const ITALIC_SKEW_C: f64 = 0.267949;
/// Metrics CoreText itself reports (used as fallbacks when a table is
/// missing). `descent` is CoreText's positive-down value.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CtMetrics {
pub ascent: f64,
/// `CTFontGetDescent`: positive distance below the baseline.
pub descent: f64,
pub leading: f64,
pub cap_height: f64,
pub x_height: f64,
}
/// Measurements taken by drawing/measuring glyphs.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Measurements {
/// The widest advance of the visible ASCII characters.
pub cell_width: f64,
/// Height of the overall bounding box of the visible ASCII characters.
pub ascii_height: f64,
/// Advance of U+6C34 (None if absent or considered invalid).
pub ic_width: Option<f64>,
}
/// The line width used for synthetic bold given the font size in points.
///
/// To determine our synthetic bold line width we get a multiplier from the
/// font size in points. This is a heuristic that is based on the fact that
/// a line width of 1 looks good at a certain point size. We want to scale
/// that up roughly linearly with the font size.
pub fn synthetic_bold_line_width(points: f64) -> f64 {
(points / 14.0).max(1.0)
}
/// Returns `(ascent, descent, line_gap)` in pixels, +Y up (`descent` is
/// normally negative), following the table priority of `getMetrics`.
pub fn vertical_metrics(
hhea: Option<&Hhea>,
os2: Option<&OS2>,
px_per_unit: f64,
ct: &CtMetrics,
) -> (f64, f64, f64) {
// If we couldn't get the hhea table, rely on metrics from CoreText.
let Some(hhea) = hhea else {
return (ct.ascent, -ct.descent, ct.leading);
};
let hhea_ascent = f64::from(hhea.ascender);
let hhea_descent = f64::from(hhea.descender);
let hhea_line_gap = f64::from(hhea.lineGap);
// If our font has no OS/2 table, then we just
// blindly use the metrics from the hhea table.
let Some(os2) = os2 else {
return (
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
);
};
let os2_ascent = f64::from(os2.sTypoAscender);
let os2_descent = f64::from(os2.sTypoDescender);
let os2_line_gap = f64::from(os2.sTypoLineGap);
// If the font says to use typo metrics, trust it.
if os2.fsSelection.use_typo_metrics {
return (
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
);
}
// Otherwise we prefer the height metrics from 'hhea' if they
// are available, or else OS/2 sTypo* metrics, and if all else
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if hhea.ascender != 0 || hhea.descender != 0 {
return (
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
);
}
if os2_ascent != 0.0 || os2_descent != 0.0 {
return (
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
);
}
let win_ascent = f64::from(os2.usWinAscent);
let win_descent = f64::from(os2.usWinDescent);
(
win_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender
// and hhea.Descender, so we flip its sign to fix this.
-win_descent * px_per_unit,
0.0,
)
}
/// Compute the `FaceMetrics` from the (optionally present) OpenType tables,
/// CoreText's own metrics and the measurements. `px_per_em` is
/// `CTFontGetSize` and `units_per_em` the `head` table value (or
/// `CTFontGetUnitsPerEm` if there is no `head`/`bhed` table).
pub fn face_metrics(
px_per_em: f64,
units_per_em: f64,
hhea: Option<&Hhea>,
os2: Option<&OS2>,
post: Option<&Post>,
ct: &CtMetrics,
measured: Measurements,
) -> FaceMetrics {
let px_per_unit = px_per_em / units_per_em;
let (ascent, descent, line_gap) = vertical_metrics(hhea, os2, px_per_unit, ct);
let (underline_position, underline_thickness) = match post {
None => (None, None),
Some(post) => {
// Some fonts have degenerate 'post' tables where the underline
// thickness (and often position) are 0. We consider them null
// if this is the case and use our own fallbacks when we calculate.
let has_broken_underline = post.underlineThickness == 0;
// If the underline position isn't 0 then we do use it,
// even if the thickness is't properly specified.
let pos = if has_broken_underline && post.underlinePosition == 0 {
None
} else {
Some(f64::from(post.underlinePosition) * px_per_unit)
};
let thick = if has_broken_underline {
None
} else {
Some(f64::from(post.underlineThickness) * px_per_unit)
};
(pos, thick)
}
};
// Similar logic to the underline above.
let (strikethrough_position, strikethrough_thickness) = match os2 {
None => (None, None),
Some(os2) => {
let has_broken_strikethrough = os2.yStrikeoutSize == 0;
let pos = if has_broken_strikethrough && os2.yStrikeoutPosition == 0 {
None
} else {
Some(f64::from(os2.yStrikeoutPosition) * px_per_unit)
};
let thick = if has_broken_strikethrough {
None
} else {
Some(f64::from(os2.yStrikeoutSize) * px_per_unit)
};
(pos, thick)
}
};
// We fall back to whatever CoreText does if the
// OS/2 table doesn't specify a cap or ex height.
let (cap_height, ex_height) = match os2 {
None => (ct.cap_height, ct.x_height),
Some(os2) => (
os2.sCapHeight
.map_or(ct.cap_height, |v| f64::from(v) * px_per_unit),
os2.sxHeight
.map_or(ct.x_height, |v| f64::from(v) * px_per_unit),
),
};
FaceMetrics {
px_per_em,
cell_width: measured.cell_width,
ascent,
descent,
line_gap,
underline_position,
underline_thickness,
strikethrough_position,
strikethrough_thickness,
cap_height: Some(cap_height),
ex_height: Some(ex_height),
ascii_height: Some(measured.ascii_height),
ic_width: measured.ic_width,
}
}
/// Is the advance of the "水" glyph usable as the ic width? If the advance
/// is less than the width of the actual glyph then it is probably wrong
/// (e.g. a CJK font patched by the nerd fonts patcher) and using it for size
/// normalization would make the font way too big.
pub fn ic_width_is_valid(glyph_width: f64, advance: f64) -> bool {
glyph_width <= advance || glyph_width.is_nan() || advance.is_nan()
}
/// A CoreText bounding rect (origin is bottom-left, +Y up).
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
/// The result of laying out a glyph for rasterization.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlyphLayout {
/// Whole pixel bearings of the final glyph.
pub px_x: i32,
pub px_y: i32,
/// Canvas size (includes padding and the fractional offset).
pub px_width: u32,
pub px_height: u32,
/// Extra pixels around the glyph for font smoothing.
pub canvas_padding: u32,
/// Fractional part of the pixel bearings, added as an offset when
/// rasterizing.
pub frac_x: f64,
pub frac_y: f64,
/// Scale applied to the context so the glyph is stretched to the
/// constrained size.
pub scale_x: f64,
pub scale_y: f64,
/// The (possibly bold-adjusted) bounding rect origin; the glyph is drawn
/// at the negated origin.
pub rect_x: f64,
pub rect_y: f64,
}
impl GlyphLayout {
/// `Glyph.offset_y`: distance from the bottom of the cell to the top of
/// the glyph's bounding box.
pub fn offset_y(&self) -> i32 {
self.px_y + self.px_height as i32
}
/// `Glyph.offset_x`: distance from the left of the cell to the left of
/// the glyph's bounding box.
pub fn offset_x(&self) -> i32 {
self.px_x
}
}
/// The bearing / size computation from `renderGlyph`.
///
/// `rect` is `CTFontGetBoundingRectsForGlyphs` for the glyph. Returns `None`
/// if the glyph is too small to render (the caller then returns a zero-sized
/// `Glyph`).
pub fn glyph_layout(
mut rect: Rect,
is_sbix: bool,
synthetic_bold: Option<f64>,
opts: &RenderOptions,
) -> Option<GlyphLayout> {
// If we're rendering a synthetic bold then we will gain 50% of
// the line width on every edge, which means we should increase
// our width and height by the line width and subtract half from
// our origin points.
//
// We don't add extra size if it's a sbix color font though,
// since bitmaps aren't affected by synthetic bold.
if !is_sbix && let Some(line_width) = synthetic_bold {
rect.width += line_width;
rect.height += line_width;
rect.x -= line_width / 2.0;
rect.y -= line_width / 2.0;
}
// If our rect is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
if rect.width < 0.25 || rect.height < 0.25 {
return None;
}
let metrics = &opts.grid_metrics;
let cell_width = f64::from(metrics.cell_width);
let cell_height = f64::from(metrics.cell_height);
// Next we apply any constraints to get the final size of the glyph.
let constraint: &Constraint = &opts.constraint;
// We need to add the baseline position before passing to the constrain
// function since it operates on cell-relative positions, not baseline.
let cell_baseline = f64::from(metrics.cell_baseline);
let glyph_size = constraint.constrain(
GlyphSize {
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y + cell_baseline,
},
metrics,
opts.constraint_width,
);
let mut x = glyph_size.x;
let mut y = glyph_size.y;
let mut width = glyph_size.width;
let mut height = glyph_size.height;
// We center all glyphs within the pixel-rounded and adjusted
// cell width if it's larger than the face width, so that they
// aren't weirdly off to the left.
//
// We don't do this if the glyph has a stretch constraint,
// since in that case the position was already calculated with the
// new cell width in mind.
if constraint.size != ConstraintSize::Stretch {
// We add half the difference to re-center.
let dx = (cell_width - metrics.face_width) / 2.0;
x += dx;
if dx < 0.0 {
// For negative diff (cell narrower than advance), we remove the
// integer part and only keep the fractional adjustment needed
// for consistent subpixel positioning.
x -= dx.trunc();
}
}
// If this is a bitmap glyph, it will always render as full pixels,
// not fractional pixels, so we need to quantize its position and
// size accordingly to align to full pixels so we get good results.
if is_sbix {
width = cell_width - (cell_width - width - x).round() - x.round();
height = cell_height - (cell_height - height - y).round() - y.round();
x = x.round();
y = y.round();
}
// We make an assumption that font smoothing ("thicken")
// adds no more than 1 extra pixel to any edge. We don't
// add extra size if it's a sbix color font though, since
// bitmaps aren't affected by smoothing.
let canvas_padding: u32 = u32::from(opts.thicken && !is_sbix);
// Our whole-pixel bearings for the final glyph.
// The fractional portion will be included in the rasterized position.
let px_x = (x.floor() as i32) - canvas_padding as i32;
let px_y = (y.floor() as i32) - canvas_padding as i32;
// We keep track of the fractional part of the pixel bearings, which
// we will add as an offset when rasterizing to make sure we get the
// correct sub-pixel position.
let frac_x = x - x.floor();
let frac_y = y - y.floor();
// Add the fractional pixel to the width and height and take
// the ceiling to get a canvas size that will definitely fit
// our drawn glyph, including the fractional offset and font smoothing.
let px_width = ((width + frac_x).ceil() as u32) + (2 * canvas_padding);
let px_height = ((height + frac_y).ceil() as u32) + (2 * canvas_padding);
Some(GlyphLayout {
px_x,
px_y,
px_width,
px_height,
canvas_padding,
frac_x,
frac_y,
scale_x: width / rect.width,
scale_y: height / rect.height,
rect_x: rect.x,
rect_y: rect.y,
})
}
/// `kCTFontTraitBold` bit of the symbolic traits.
pub const TRAIT_BOLD: u32 = 1 << 1;
/// `kCTFontTraitItalic` bit of the symbolic traits.
pub const TRAIT_ITALIC: u32 = 1;
/// `kCTFontTraitMonoSpace` bit of the symbolic traits.
pub const TRAIT_MONOSPACE: u32 = 1 << 10;
/// `kCTFontTraitColorGlyphs` bit of the symbolic traits.
pub const TRAIT_COLOR_GLYPHS: u32 = 1 << 13;
/// The symbolic traits value for a descriptor (0 if no trait is set, in
/// which case the traits attribute is not added; see
/// `Descriptor.toCoreTextDescriptor`).
pub fn symbolic_traits_value(bold: bool, italic: bool, monospace: bool) -> u32 {
(if bold { TRAIT_BOLD } else { 0 })
| (if italic { TRAIT_ITALIC } else { 0 })
| (if monospace { TRAIT_MONOSPACE } else { 0 })
}
/// Number of bytes per pixel and whether the glyph is rendered in color,
/// for the atlas format check in `renderGlyph`.
pub fn bitmap_depth(is_color: bool) -> u32 {
if is_color { 4 } else { 1 }
}
/// Decode a UTF-32 codepoint into UTF-16 code units
/// (`stringGetSurrogatePairForLongCharacter`). Returns the units and how many
/// are valid (1 or 2). Codepoints outside the Unicode range yield a single
/// replacement unit.
pub fn utf16_units(cp: u32) -> ([u16; 2], usize) {
match char::from_u32(cp) {
Some(c) => {
let mut buf = [0u16; 2];
let n = c.encode_utf16(&mut buf).len();
(buf, n)
}
None => ([0xFFFD, 0], 1),
}
}
/// The score used to sort CoreText discovery results, as a plain integer
/// (`Score.int()` in discovery.zig). Fields from least to most significant:
/// `glyph_count: u16`, `fuzzy_style: u8`, `bold`, `italic`, `exact_style`,
/// `monospace`, `codepoint` (1 bit each). Higher is better.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Score {
pub glyph_count: u16,
pub fuzzy_style: u8,
pub bold: bool,
pub italic: bool,
pub exact_style: bool,
pub monospace: bool,
pub codepoint: bool,
}
impl Score {
pub fn int(self) -> u32 {
u32::from(self.glyph_count)
| (u32::from(self.fuzzy_style) << 16)
| (u32::from(self.bold) << 24)
| (u32::from(self.italic) << 25)
| (u32::from(self.exact_style) << 26)
| (u32::from(self.monospace) << 27)
| (u32::from(self.codepoint) << 28)
}
}
/// The style-name strings the discovery score matches against (the first
/// is used for the exact match, all are used for the fuzzy match).
pub fn desired_styles(style: Option<&str>, bold: bool, italic: bool) -> Vec<&str> {
if let Some(s) = style {
return vec![s];
}
// If we don't have an explicitly desired style name, we base
// it on the bold and italic properties, this isn't ideal since
// fonts may use style names other than these, but it helps in
// some edge cases.
if bold {
if italic {
return vec!["bold italic", "bold", "italic", "oblique"];
}
return vec!["bold", "upright"];
} else if italic {
return vec!["italic", "regular", "oblique"];
}
vec!["regular", "upright"]
}
/// Compute `(exact_style, fuzzy_style)` for a font's style string against
/// the desired styles (see `Score.score`).
pub fn style_scores(style_str: &str, desired: &[&str]) -> (bool, u8) {
let exact = style_str.eq_ignore_ascii_case(desired[0]);
// Our "fuzzy match" score is 0 if the desired style isn't present
// in the string, otherwise we give higher priority for styles that
// have fewer characters not in the desired_styles list.
let mut fuzzy: u8 = style_str.len().min(255) as u8;
let lower = style_str.to_ascii_lowercase();
for s in desired {
if lower.contains(&s.to_ascii_lowercase()) {
fuzzy = fuzzy.saturating_sub(s.len().min(255) as u8);
}
}
(exact, u8::MAX.saturating_sub(fuzzy))
}
/// Derive `(is_bold, is_italic)` for a variation axis list entry, following
/// the thresholds in `Score.score`. `axes` yields `(name, value)`.
pub fn bold_italic_from_axes(
mut is_bold: bool,
mut is_italic: bool,
axes: impl IntoIterator<Item = (String, f64)>,
) -> (bool, bool) {
// If we see the 'ital' value then we ignore 'slnt'.
let mut ital_seen = false;
for (name, val) in axes {
if name == "wght" {
// Somewhat subjective threshold, we consider fonts
// bold if they have a 'wght' set greater than 600.
is_bold = val > 600.0;
continue;
}
if name == "ital" {
is_italic = val > 0.5;
ital_seen = true;
continue;
}
if !ital_seen && name == "slnt" {
// Arbitrary threshold of anything more than a 5
// degree clockwise slant is considered italic.
is_italic = val <= -5.0;
}
}
(is_bold, is_italic)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
face::{DesiredSize, RenderOptions},
metrics::Metrics,
opentype::{
os2::FSSelection,
sfnt::{Fixed, Version16Dot16},
},
};
fn hhea(ascender: i16, descender: i16, line_gap: i16) -> Hhea {
Hhea {
majorVersion: 1,
minorVersion: 0,
ascender,
descender,
lineGap: line_gap,
advanceWidthMax: 0,
minLeftSideBearing: 0,
minRightSideBearing: 0,
xMaxExtent: 0,
caretSlopeRise: 0,
caretSlopeRun: 0,
caretOffset: 0,
reserved0: 0,
reserved1: 0,
reserved2: 0,
reserved3: 0,
metricDataFormat: 0,
numberOfHMetrics: 0,
}
}
fn os2(typo: (i16, i16, i16), win: (u16, u16), use_typo: bool) -> OS2 {
OS2 {
version: 4,
xAvgCharWidth: 0,
usWeightClass: 400,
usWidthClass: 5,
fsType: 0,
ySubscriptXSize: 0,
ySubscriptYSize: 0,
ySubscriptXOffset: 0,
ySubscriptYOffset: 0,
ySuperscriptXSize: 0,
ySuperscriptYSize: 0,
ySuperscriptXOffset: 0,
ySuperscriptYOffset: 0,
yStrikeoutSize: 50,
yStrikeoutPosition: 300,
sFamilyClass: 0,
panose: [0; 10],
ulUnicodeRange1: 0,
ulUnicodeRange2: 0,
ulUnicodeRange3: 0,
ulUnicodeRange4: 0,
achVendID: [0; 4],
fsSelection: FSSelection {
use_typo_metrics: use_typo,
..FSSelection::from_u16(0)
},
usFirstCharIndex: 0,
usLastCharIndex: 0,
sTypoAscender: typo.0,
sTypoDescender: typo.1,
sTypoLineGap: typo.2,
usWinAscent: win.0,
usWinDescent: win.1,
ulCodePageRange1: None,
ulCodePageRange2: None,
sxHeight: Some(500),
sCapHeight: Some(700),
usDefaultChar: None,
usBreakChar: None,
usMaxContext: None,
usLowerOpticalPointSize: None,
usUpperOpticalPointSize: None,
}
}
fn post(pos: i16, thick: i16) -> Post {
Post {
version: Version16Dot16::from_u32(0x0002_0000),
italicAngle: Fixed(0),
underlinePosition: pos,
underlineThickness: thick,
isFixedPitch: 1,
minMemType42: 0,
maxMemType42: 0,
minMemType1: 0,
maxMemType1: 0,
}
}
#[test]
fn symbolic_traits() {
assert_eq!(symbolic_traits_value(false, false, false), 0);
assert_eq!(symbolic_traits_value(true, false, false), 2);
assert_eq!(symbolic_traits_value(false, true, false), 1);
assert_eq!(symbolic_traits_value(true, true, true), 2 | 1 | 1024);
}
#[test]
fn bold_line_width() {
assert_eq!(synthetic_bold_line_width(12.0), 1.0);
assert_eq!(synthetic_bold_line_width(14.0), 1.0);
assert_eq!(synthetic_bold_line_width(28.0), 2.0);
}
#[test]
fn vertical_no_hhea_uses_coretext() {
let ct = CtMetrics {
ascent: 10.0,
descent: 3.0,
leading: 1.0,
..Default::default()
};
assert_eq!(vertical_metrics(None, None, 0.01, &ct), (10.0, -3.0, 1.0));
}
#[test]
fn vertical_hhea_only() {
let h = hhea(800, -200, 100);
let (a, d, g) = vertical_metrics(Some(&h), None, 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (8.0, -2.0, 1.0));
}
#[test]
fn vertical_typo_when_flagged() {
let h = hhea(800, -200, 100);
let o = os2((900, -300, 0), (1000, 400), true);
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (9.0, -3.0, 0.0));
}
#[test]
fn vertical_prefers_hhea_then_typo_then_win() {
let o = os2((900, -300, 20), (1000, 400), false);
// hhea non-zero wins over typo.
let h = hhea(800, -200, 100);
let (a, ..) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!(a, 8.0);
// hhea zero -> typo.
let h = hhea(0, 0, 0);
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (9.0, -3.0, 0.2));
// both zero -> win metrics with flipped descent sign.
let o = os2((0, 0, 20), (1000, 400), false);
let (a, d, g) = vertical_metrics(Some(&h), Some(&o), 0.01, &CtMetrics::default());
assert_eq!((a, d, g), (10.0, -4.0, 0.0));
}
#[test]
fn metrics_underline_and_strikeout() {
let h = hhea(800, -200, 0);
let o = os2((0, 0, 0), (0, 0), false);
let p = post(-100, 50);
let m = face_metrics(
10.0,
1000.0,
Some(&h),
Some(&o),
Some(&p),
&CtMetrics::default(),
Measurements {
cell_width: 6.0,
ascii_height: 9.0,
ic_width: Some(12.0),
},
);
assert_eq!(m.underline_position, Some(-1.0));
assert_eq!(m.underline_thickness, Some(0.5));
assert_eq!(m.strikethrough_position, Some(3.0));
assert_eq!(m.strikethrough_thickness, Some(0.5));
assert_eq!(m.cap_height, Some(7.0));
assert_eq!(m.ex_height, Some(5.0));
assert_eq!(m.cell_width, 6.0);
assert_eq!(m.ascii_height, Some(9.0));
assert_eq!(m.ic_width, Some(12.0));
}
#[test]
fn metrics_broken_underline_is_none() {
let p = post(0, 0);
let m = face_metrics(
10.0,
1000.0,
None,
None,
Some(&p),
&CtMetrics {
cap_height: 7.0,
x_height: 5.0,
..Default::default()
},
Measurements::default(),
);
assert_eq!(m.underline_position, None);
assert_eq!(m.underline_thickness, None);
// position set but thickness broken: keep position only
let p = post(-100, 0);
let m = face_metrics(
10.0,
1000.0,
None,
None,
Some(&p),
&CtMetrics::default(),
Measurements::default(),
);
assert_eq!(m.underline_position, Some(-1.0));
assert_eq!(m.underline_thickness, None);
assert_eq!(m.strikethrough_position, None);
// no OS/2: CoreText cap/ex heights.
assert_eq!(m.cap_height, Some(0.0));
}
#[test]
fn ic_width_validity() {
assert!(ic_width_is_valid(10.0, 12.0));
assert!(ic_width_is_valid(12.0, 12.0));
assert!(!ic_width_is_valid(13.0, 12.0));
}
fn opts() -> RenderOptions {
let mut m = Metrics::init();
m.cell_width = 10;
m.cell_height = 20;
m.cell_baseline = 4;
m.face_width = 10.0;
m.face_height = 20.0;
let _ = DesiredSize::new(12.0);
RenderOptions::new(m)
}
#[test]
fn layout_tiny_glyph_is_none() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 0.2,
height: 5.0,
};
assert!(glyph_layout(r, false, None, &opts()).is_none());
}
#[test]
fn layout_basic() {
let r = Rect {
x: 1.5,
y: -2.25,
width: 6.0,
height: 10.0,
};
let l = glyph_layout(r, false, None, &opts()).unwrap();
assert_eq!(l.px_x, 1);
// y is cell-relative: -2.25 + cell_baseline (4) = 1.75
assert_eq!(l.px_y, 1);
assert!((l.frac_x - 0.5).abs() < 1e-9);
assert!((l.frac_y - 0.75).abs() < 1e-9);
assert_eq!(l.px_width, 7);
assert_eq!(l.px_height, 11);
assert_eq!(l.canvas_padding, 0);
assert_eq!(l.scale_x, 1.0);
assert_eq!(l.offset_y(), 12);
assert_eq!(l.offset_x(), 1);
}
#[test]
fn layout_thicken_pads_canvas() {
let r = Rect {
x: 1.0,
y: 0.0,
width: 5.0,
height: 5.0,
};
let mut o = opts();
o.thicken = true;
let l = glyph_layout(r, false, None, &o).unwrap();
assert_eq!(l.canvas_padding, 1);
assert_eq!(l.px_x, 0);
assert_eq!(l.px_width, 7);
// sbix glyphs never pad
let l = glyph_layout(r, true, None, &o).unwrap();
assert_eq!(l.canvas_padding, 0);
}
#[test]
fn layout_synthetic_bold_grows_rect() {
let r = Rect {
x: 2.0,
y: 2.0,
width: 4.0,
height: 4.0,
};
let l = glyph_layout(r, false, Some(2.0), &opts()).unwrap();
assert_eq!(l.rect_x, 1.0);
assert_eq!(l.rect_y, 1.0);
assert_eq!(l.px_width, 6);
// sbix ignores synthetic bold
let l = glyph_layout(r, true, Some(2.0), &opts()).unwrap();
assert_eq!(l.rect_x, 2.0);
}
#[test]
fn layout_centers_in_wider_cell() {
let mut o = opts();
o.grid_metrics.cell_width = 12;
o.grid_metrics.face_width = 10.0;
let r = Rect {
x: 0.0,
y: 0.0,
width: 5.0,
height: 5.0,
};
let l = glyph_layout(r, false, None, &o).unwrap();
// dx = (12 - 10)/2 = 1
assert_eq!(l.px_x, 1);
}
#[test]
fn utf16() {
assert_eq!(utf16_units('A' as u32), (['A' as u16, 0], 1));
let (u, n) = utf16_units(0x1F978);
assert_eq!(n, 2);
assert_eq!(u, [0xD83E, 0xDD78]);
assert_eq!(utf16_units(0xD800).1, 1);
}
#[test]
fn score_ordering() {
let a = Score {
codepoint: true,
..Default::default()
};
let b = Score {
monospace: true,
glyph_count: u16::MAX,
fuzzy_style: 255,
bold: true,
italic: true,
exact_style: true,
..Default::default()
};
assert!(a.int() > b.int());
let c = Score {
glyph_count: 5,
..Default::default()
};
let d = Score {
glyph_count: 7,
..Default::default()
};
assert!(d.int() > c.int());
}
#[test]
fn desired_style_lists() {
assert_eq!(desired_styles(Some("Light"), true, true), vec!["Light"]);
assert_eq!(
desired_styles(None, true, true),
vec!["bold italic", "bold", "italic", "oblique"]
);
assert_eq!(desired_styles(None, true, false), vec!["bold", "upright"]);
assert_eq!(
desired_styles(None, false, true),
vec!["italic", "regular", "oblique"]
);
assert_eq!(
desired_styles(None, false, false),
vec!["regular", "upright"]
);
}
#[test]
fn style_score_fuzzy_and_exact() {
let d = desired_styles(None, false, false);
let (exact, fuzzy) = style_scores("Regular", &d);
assert!(exact);
assert_eq!(fuzzy, 255);
let (exact, fuzzy) = style_scores("Semi Regular Extra", &d);
assert!(!exact);
// len 18, "regular" (7) subtracted => 11 => 255 - 11
assert_eq!(fuzzy, 255 - 11);
let (_, fuzzy) = style_scores("Light", &d);
assert_eq!(fuzzy, 255 - 5);
}
#[test]
fn axes_bold_italic() {
let r = bold_italic_from_axes(
false,
false,
[("wght".to_owned(), 700.0), ("slnt".to_owned(), -10.0)],
);
assert_eq!(r, (true, true));
// ital seen => slnt ignored
let r = bold_italic_from_axes(
true,
true,
[("ital".to_owned(), 0.0), ("slnt".to_owned(), -10.0)],
);
assert_eq!(r, (true, false));
let r = bold_italic_from_axes(false, false, [("wght".to_owned(), 600.0)]);
assert_eq!(r, (false, false));
}
}