feat(font): add the FreeType-only font backend (font-backend-freetype)
Ghostty can be built with -Dfont-backend=freetype, which uses FreeType without any font discovery. The Rust port only had fontconfig+freetype (default on Linux) and the CoreText variants. The new cargo feature `font-backend-freetype` on ghostty-font (forwarded by ghostty-cli and the root crate; non-Apple, non-wasm targets) behaves like Zig's mode: font-family discovery, fallback search and codepoint-map overrides are skipped (embedded fonts only); +list-fonts and +show-face print Zig's "built without a font discovery mechanism" message and exit 1; +version reports `.freetype`. The default backend is unchanged. Also registers the Rust-only benchmark tooling extensions (--time, --handler=full, ghostty-synthetic +sgr) in DEVIANT_FIXES.md. Note: `--all-features` now selects this backend for the workspace. Test Plan: - cargo test -p ghostty-font --features font-backend-freetype (198) - cargo build --features font-backend-freetype && ghostty +list-fonts
This commit is contained in:
1 parent
ebd603f3da
commit
ff1169abed
18 files changed
+327
-12
No files matched your search
@@ -13,6 +13,9 @@
|
||||
//! (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.
|
||||
//! * Non-Apple, non-wasm targets with the cargo feature
|
||||
//! `font-backend-freetype`: [`Backend::Freetype`] (Zig
|
||||
//! `-Dfont-backend=freetype`: no font discovery).
|
||||
//! * wasm32: [`Backend::WebCanvas`].
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -50,6 +53,19 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Zig tag name (`@tagName`) of the backend.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Backend::Freetype => "freetype",
|
||||
Backend::FontconfigFreetype => "fontconfig_freetype",
|
||||
Backend::Coretext => "coretext",
|
||||
Backend::CoretextFreetype => "coretext_freetype",
|
||||
Backend::CoretextHarfbuzz => "coretext_harfbuzz",
|
||||
Backend::CoretextNoshape => "coretext_noshape",
|
||||
Backend::WebCanvas => "web_canvas",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_freetype(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
@@ -88,7 +104,20 @@ pub struct Options {
|
||||
pub backend: Backend,
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_vendor = "apple")))]
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
not(target_vendor = "apple"),
|
||||
feature = "font-backend-freetype"
|
||||
))]
|
||||
pub const OPTIONS: Options = Options {
|
||||
backend: Backend::Freetype,
|
||||
};
|
||||
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
not(target_vendor = "apple"),
|
||||
not(feature = "font-backend-freetype")
|
||||
))]
|
||||
pub const OPTIONS: Options = Options {
|
||||
backend: Backend::FontconfigFreetype,
|
||||
};
|
||||
@@ -182,13 +211,45 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_vendor = "apple")))]
|
||||
#[test]
|
||||
fn freetype_capabilities() {
|
||||
let b = Backend::Freetype;
|
||||
assert!(b.has_freetype());
|
||||
assert!(!b.has_fontconfig());
|
||||
assert!(b.has_harfbuzz());
|
||||
assert!(!b.has_coretext());
|
||||
assert_eq!(b.name(), "freetype");
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
not(target_vendor = "apple"),
|
||||
not(feature = "font-backend-freetype")
|
||||
))]
|
||||
#[test]
|
||||
fn linux_backend_capabilities() {
|
||||
let b = OPTIONS.backend;
|
||||
assert_eq!(b, Backend::FontconfigFreetype);
|
||||
assert!(b.has_freetype());
|
||||
assert!(b.has_fontconfig());
|
||||
assert!(b.has_harfbuzz());
|
||||
assert!(!b.has_coretext());
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(target_arch = "wasm32"),
|
||||
not(target_vendor = "apple"),
|
||||
feature = "font-backend-freetype"
|
||||
))]
|
||||
#[test]
|
||||
fn freetype_only_backend_selected() {
|
||||
let b = OPTIONS.backend;
|
||||
assert_eq!(b, Backend::Freetype);
|
||||
assert!(b.has_freetype());
|
||||
assert!(!b.has_fontconfig());
|
||||
assert!(b.has_harfbuzz());
|
||||
assert!(!b.has_coretext());
|
||||
// No discovery mechanism, as in Zig (`Discover == void`).
|
||||
const { assert!(!crate::HAS_DISCOVERY) };
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(font_discovery)]
|
||||
use crate::collection::{AddOptions, Entry};
|
||||
use crate::{
|
||||
Atlas,
|
||||
CodepointMap,
|
||||
@@ -17,7 +19,7 @@ use crate::{
|
||||
Presentation,
|
||||
SpriteFace,
|
||||
Style,
|
||||
collection::{AddOptions, Collection, Entry, GetFaceError, Index, PresentationMode, Special},
|
||||
collection::{Collection, GetFaceError, Index, PresentationMode, Special},
|
||||
discovery::Discover,
|
||||
face::{RenderError as FaceRenderError, RenderOptions},
|
||||
sprite,
|
||||
@@ -88,6 +90,7 @@ pub struct CodepointResolver {
|
||||
|
||||
/// The descriptor cache is used to cache the descriptor to font face
|
||||
/// mapping for codepoint maps.
|
||||
#[cfg_attr(not(font_discovery), allow(dead_code))]
|
||||
descriptor_cache: DescriptorCache,
|
||||
|
||||
/// Set this to a non-null value to enable sprite glyph drawing. If this
|
||||
@@ -108,6 +111,7 @@ pub struct CodepointResolver {
|
||||
///
|
||||
/// As in upstream, descriptors are compared by their `hashcode()` only.
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(not(font_discovery), allow(dead_code))]
|
||||
struct DescriptorCache(HashMap<u64, Option<Index>>);
|
||||
|
||||
impl CodepointResolver {
|
||||
@@ -240,6 +244,27 @@ impl CodepointResolver {
|
||||
.get_index(cp, Style::Regular, PresentationMode::Any)
|
||||
}
|
||||
|
||||
/// Without a discovery mechanism (the `freetype` backend, Zig
|
||||
/// `Discover == void`) there is no fallback search.
|
||||
#[cfg(not(font_discovery))]
|
||||
#[allow(clippy::unused_self)]
|
||||
fn discover_fallback(
|
||||
&mut self,
|
||||
_cp: u32,
|
||||
_style: Style,
|
||||
_p: PresentationMode,
|
||||
) -> Option<Index> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Without a discovery mechanism, codepoint overrides cannot find fonts.
|
||||
#[cfg(not(font_discovery))]
|
||||
#[allow(clippy::unused_self)]
|
||||
fn get_index_codepoint_override(&mut self, _cp: u32) -> Option<Index> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(font_discovery)]
|
||||
fn discover_fallback(
|
||||
&mut self,
|
||||
cp: u32,
|
||||
@@ -309,6 +334,7 @@ impl CodepointResolver {
|
||||
|
||||
/// Checks if the codepoint is in the map of codepoint overrides,
|
||||
/// finds the override font, and returns it.
|
||||
#[cfg(font_discovery)]
|
||||
fn get_index_codepoint_override(&mut self, cp: u32) -> Option<Index> {
|
||||
// Get our codepoint map. If we have no map set then we have no
|
||||
// codepoint overrides and we're done.
|
||||
@@ -459,7 +485,7 @@ mod tests {
|
||||
use crate::{
|
||||
Face,
|
||||
Library,
|
||||
collection::{LoadOptions, SizeAdjustment},
|
||||
collection::{AddOptions, LoadOptions, SizeAdjustment},
|
||||
embedded,
|
||||
face::{DesiredSize, Options},
|
||||
};
|
||||
@@ -534,6 +560,27 @@ mod tests {
|
||||
assert!(r.get_index(0x1FB00, Style::Regular, None).is_none());
|
||||
}
|
||||
|
||||
/// `freetype` backend (no discovery): codepoint overrides and fallback
|
||||
/// searches are disabled, so an unmapped codepoint is simply not found.
|
||||
#[cfg(not(font_discovery))]
|
||||
#[test]
|
||||
fn freetype_backend_has_no_discovery_fallback_or_override() {
|
||||
const { assert!(!crate::HAS_DISCOVERY) };
|
||||
let lib = Library::init().unwrap();
|
||||
let mut c = Collection::new();
|
||||
c.load_options = Some(LoadOptions::new(lib.clone()));
|
||||
add(&mut c, &lib, embedded::REGULAR, Style::Regular, 12.0);
|
||||
|
||||
let mut r = CodepointResolver::new(c);
|
||||
assert!(r.discover.is_none());
|
||||
assert!(r.get_index_codepoint_override('A' as u32).is_none());
|
||||
// Not in the embedded font, and there is no discovery to ask.
|
||||
assert!(r.get_index(0x4E2D, Style::Regular, None).is_none());
|
||||
assert!(r.get_index(0x4E2D, Style::Bold, None).is_none());
|
||||
// Embedded codepoints still resolve.
|
||||
assert!(r.get_index('A' as u32, Style::Regular, None).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_index_disabled_font_style() {
|
||||
let lib = Library::init().unwrap();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! Port of `font/DeferredFace.zig` (`freetype` backend: no discovery, so
|
||||
//! there is no deferred data and every face must already be loaded).
|
||||
|
||||
use crate::{
|
||||
Face,
|
||||
Library,
|
||||
Presentation,
|
||||
face::{self, Options},
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LoadError {
|
||||
#[error(transparent)]
|
||||
Face(#[from] face::FaceError),
|
||||
}
|
||||
|
||||
/// With no discovery mechanism there is no data (all fields are `void` in
|
||||
/// Zig for the `freetype` backend).
|
||||
#[derive(Default)]
|
||||
pub struct DeferredFace {}
|
||||
|
||||
impl DeferredFace {
|
||||
/// Returns the family name of the font.
|
||||
pub fn family_name(&self) -> Result<String, LoadError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
/// Returns the name of this face.
|
||||
pub fn name(&self) -> Result<String, LoadError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
/// Load the deferred font face.
|
||||
pub fn load(&mut self, _lib: &Library, _opts: Options) -> Result<Face, LoadError> {
|
||||
// Unreachable because we must be already loaded or have the
|
||||
// proper configuration for one of the other deferred mechanisms.
|
||||
unreachable!("deferred face without a discovery mechanism");
|
||||
}
|
||||
|
||||
/// Returns true if this face can satisfy the given codepoint and
|
||||
/// presentation.
|
||||
pub fn has_codepoint(&self, _cp: u32, _p: Option<Presentation>) -> bool {
|
||||
// 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 a discovery mechanism");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,42 @@ pub use fc_impl::*;
|
||||
mod coretext_impl;
|
||||
#[cfg(font_coretext)]
|
||||
pub use coretext_impl::*;
|
||||
|
||||
/// The `freetype` backend has no discovery mechanism: Zig's
|
||||
/// `Discover = void`. This is an uninhabited stand-in so that generic code
|
||||
/// type-checks; it can never be constructed, and the callers check
|
||||
/// [`crate::HAS_DISCOVERY`] (or `cfg(font_discovery)`) first.
|
||||
#[cfg(not(any(font_fontconfig, font_coretext)))]
|
||||
pub struct Discover(std::convert::Infallible);
|
||||
|
||||
#[cfg(not(any(font_fontconfig, font_coretext)))]
|
||||
pub type DiscoverError = std::convert::Infallible;
|
||||
#[cfg(not(any(font_fontconfig, font_coretext)))]
|
||||
pub type NextError = std::convert::Infallible;
|
||||
|
||||
#[cfg(not(any(font_fontconfig, font_coretext)))]
|
||||
pub struct DiscoverIterator(std::convert::Infallible);
|
||||
|
||||
#[cfg(not(any(font_fontconfig, font_coretext)))]
|
||||
impl Discover {
|
||||
#[must_use]
|
||||
pub fn init() -> Discover {
|
||||
unreachable!("no font discovery mechanism in this build")
|
||||
}
|
||||
|
||||
pub fn discover(&self, _desc: &Descriptor) -> Result<DiscoverIterator, DiscoverError> {
|
||||
match self.0 {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(font_fontconfig, font_coretext)))]
|
||||
impl DiscoverIterator {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Result<Option<crate::deferred_face::DeferredFace>, NextError> {
|
||||
match self.0 {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Descriptor is used to search for fonts (canonical definition in `ghostty-config`).
|
||||
pub use ghostty_config::font_types::Descriptor;
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ pub mod config_types;
|
||||
pub mod coretext_util;
|
||||
#[cfg(font_fontconfig)]
|
||||
pub mod deferred_face;
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(font_fontconfig), not(font_coretext)))]
|
||||
#[path = "deferred_face_freetype.rs"]
|
||||
pub mod deferred_face;
|
||||
#[cfg(font_coretext)]
|
||||
#[path = "deferred_face_coretext.rs"]
|
||||
pub mod deferred_face;
|
||||
@@ -48,6 +51,11 @@ pub mod sprite;
|
||||
|
||||
pub use atlas::Atlas;
|
||||
pub use backend::{Backend, OPTIONS as options};
|
||||
|
||||
/// Whether a font discovery mechanism exists in this build (Zig:
|
||||
/// `font.Discover != void`; false for the `freetype` and `web_canvas`
|
||||
/// backends).
|
||||
pub const HAS_DISCOVERY: bool = cfg!(font_discovery);
|
||||
pub use codepoint_map::CodepointMap;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use codepoint_resolver::CodepointResolver;
|
||||
|
||||
@@ -19,5 +19,5 @@ pub mod run;
|
||||
|
||||
pub mod web_canvas;
|
||||
|
||||
#[cfg(all(test, font_harfbuzz, font_fontconfig))]
|
||||
#[cfg(all(test, font_harfbuzz))]
|
||||
pub(crate) mod test_util;
|
||||
@@ -23,7 +23,6 @@ use crate::{
|
||||
Style,
|
||||
codepoint_resolver::CodepointResolver,
|
||||
collection::{AddOptions, Collection, LoadOptions, SizeAdjustment},
|
||||
discovery::{Descriptor, Fontconfig},
|
||||
embedded,
|
||||
face::{DesiredSize, FontData, Options as FaceOptions},
|
||||
shape,
|
||||
@@ -102,7 +101,18 @@ pub(crate) fn test_shaper_with_font(font_req: TestFont) -> TestShaper {
|
||||
|
||||
/// Returns None (the test is skipped, like `error.SkipZigTest`) if the font
|
||||
/// can't be discovered.
|
||||
#[cfg(not(font_fontconfig))]
|
||||
pub(crate) fn test_shaper_with_discovered_font(_font_req: &str) -> Option<TestShaper> {
|
||||
// No fontconfig discovery in this backend (for example `freetype`).
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns None (the test is skipped, like `error.SkipZigTest`) if the font
|
||||
/// can't be discovered.
|
||||
#[cfg(font_fontconfig)]
|
||||
pub(crate) fn test_shaper_with_discovered_font(font_req: &str) -> Option<TestShaper> {
|
||||
use crate::discovery::{Descriptor, Fontconfig};
|
||||
|
||||
let lib = Library::init().unwrap();
|
||||
let mut c = Collection::new();
|
||||
c.load_options = Some(LoadOptions::new(lib.clone()));
|
||||
|
||||
@@ -27,13 +27,15 @@ use ghostty_config::{
|
||||
types_b::FreetypeLoadFlags,
|
||||
};
|
||||
|
||||
#[cfg(font_discovery)]
|
||||
use crate::discovery::Discover;
|
||||
use crate::{
|
||||
Face,
|
||||
Style,
|
||||
codepoint_map::CodepointMap,
|
||||
codepoint_resolver::{CodepointResolver, StyleStatus},
|
||||
collection::{self, AddOptions, Collection, LoadOptions, SizeAdjustment},
|
||||
discovery::{Descriptor, Discover},
|
||||
discovery::Descriptor,
|
||||
embedded,
|
||||
face::{self, DesiredSize, FontData},
|
||||
library::{Library, LibraryError},
|
||||
@@ -45,7 +47,9 @@ use crate::{
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RefError {
|
||||
#[error(transparent)]
|
||||
#[cfg(font_discovery)]
|
||||
Discover(#[from] crate::discovery::DiscoverError),
|
||||
#[cfg(font_discovery)]
|
||||
#[error("font discovery failed: {0}")]
|
||||
DiscoverNext(#[from] crate::discovery::NextError),
|
||||
#[error(transparent)]
|
||||
@@ -80,6 +84,7 @@ struct Inner {
|
||||
/// Font discovery mechanism. Initialized once and kept for the
|
||||
/// lifetime of the set because some font discovery mechanisms
|
||||
/// (i.e. fontconfig) are unsafe to reinit.
|
||||
#[cfg(font_discovery)]
|
||||
font_discover: Option<Discover>,
|
||||
}
|
||||
|
||||
@@ -98,6 +103,7 @@ impl SharedGridSet {
|
||||
font_lib: Library::init()?,
|
||||
inner: Mutex::new(Inner {
|
||||
map: HashMap::new(),
|
||||
#[cfg(font_discovery)]
|
||||
font_discover: None,
|
||||
}),
|
||||
})
|
||||
@@ -158,7 +164,10 @@ impl SharedGridSet {
|
||||
// Init our resolver which just requires setting fields.
|
||||
let mut resolver = CodepointResolver::new(c);
|
||||
resolver.styles = styles;
|
||||
resolver.discover = Some(Self::discover(&mut inner).clone());
|
||||
#[cfg(font_discovery)]
|
||||
{
|
||||
resolver.discover = Some(Self::discover(&mut inner).clone());
|
||||
}
|
||||
resolver.codepoint_map = Some(key.codepoint_map.clone());
|
||||
|
||||
let grid = Arc::new(SharedGrid::new(resolver)?);
|
||||
@@ -174,6 +183,7 @@ impl SharedGridSet {
|
||||
|
||||
/// Builds the Collection for the given configuration key and
|
||||
/// initial font size.
|
||||
#[cfg_attr(not(font_discovery), allow(unused_variables))]
|
||||
fn collection(
|
||||
&self,
|
||||
inner: &mut Inner,
|
||||
@@ -191,7 +201,9 @@ impl SharedGridSet {
|
||||
c.load_options = Some(load_options.clone());
|
||||
c.metric_modifiers.clone_from(&key.metric_modifiers);
|
||||
|
||||
// Search for fonts
|
||||
// Search for fonts. Without a discovery mechanism (the `freetype`
|
||||
// backend) there is nothing to search with.
|
||||
#[cfg(font_discovery)]
|
||||
{
|
||||
let disco = Self::discover(inner);
|
||||
for style in Style::ALL {
|
||||
@@ -392,6 +404,7 @@ 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.
|
||||
#[cfg(font_discovery)]
|
||||
fn discover(inner: &mut Inner) -> &Discover {
|
||||
inner.font_discover.get_or_insert_with(Discover::init)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user