feat(font): add run iterator, shared grid set and config integration

Completes the font engine port:
- shaper/run.rs ports run.zig: the RunIterator over terminal rows that
  splits text runs at style/face boundaries, selection and cursor,
  handles graphemes and ligatures; Shaper::shape_run shapes a run.
- shared_grid_set.rs ports SharedGridSet.zig: grids keyed by a config
  derived key, handed out as Arc<SharedGrid>.
- Font-related types now have one canonical definition in ghostty-config
  (Variation, Descriptor, CodepointMap, metric modifiers, freetype load
  flags, synthetic style, shaping break) which the font crate re-exports.
  MetricModifier::apply_i32 now saturates to -i32::MAX like Zig.
- The 23 terminal-driven harfbuzz shaper tests are ported, printing via
  Screen until Terminal lands.

Test Plan:
- cargo test -p ghostty-font (157 passed, 3 ignored - also skipped in
  Zig); Devanagari/Chakma/Bengali tests return early without system fonts
- cargo test -p ghostty-config (255 passed)
This commit is contained in:
ddidderr committed 2026-10-11 22:38:55 +02:00
1 parent b64a2886ec
commit 7b408257c4
19 files changed
+3204 -554

No files matched your search

+32 -100
View File
@@ -1,7 +1,6 @@
//! Font discovery. Port of `font/discovery.zig` (fontconfig backend only;
//! the CoreText discoverer is macOS-only and out of scope).
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::collection::Collection;
@@ -12,111 +11,44 @@ use crate::fontconfig as fc;
/// Discover implementation for the compile options.
pub type Discover = Fontconfig;
/// Descriptor is used to search for fonts. The only required field
/// is "family". The rest are ignored unless they're set to a non-zero
/// value.
#[derive(Debug, Clone, Default)]
pub struct Descriptor {
/// Font family to search for. This can be a fully qualified font
/// name such as "Fira Code", "monospace", "serif", etc.
///
/// On systems that use fontconfig (Linux), this can be a full
/// fontconfig pattern, such as "Fira Code-14:bold".
pub family: Option<String>,
/// Descriptor is used to search for fonts (canonical definition in `ghostty-config`).
pub use ghostty_config::font_types::Descriptor;
/// Specific font style to search for. This will filter the style
/// string the font advertises. The "bold/italic" booleans later in this
/// struct filter by the style trait the font has, not the string, so
/// these can be used in conjunction or not.
pub style: Option<String>,
/// A codepoint that this font must be able to render.
pub codepoint: u32,
/// Font size in points that the font should support. For conversion
/// to pixels, we will use 72 DPI for Mac and 96 DPI for everything else.
/// (If pixel conversion is necessary, i.e. emoji fonts)
pub size: f32,
/// True if we want to search specifically for a font that supports
/// specific styles.
pub bold: bool,
pub italic: bool,
pub monospace: bool,
/// Variation axes to apply to the font. This also impacts searching
/// for fonts since fonts with the ability to set these variations
/// will be preferred, but not guaranteed.
pub variations: Vec<Variation>,
}
impl Hash for Descriptor {
/// Hash the descriptor with the given hasher. Covers the same fields
/// as the Zig implementation.
fn hash<H: Hasher>(&self, state: &mut H) {
self.family.hash(state);
self.style.hash(state);
self.codepoint.hash(state);
self.size.to_bits().hash(state);
self.bold.hash(state);
self.italic.hash(state);
self.monospace.hash(state);
self.variations.len().hash(state);
for variation in &self.variations {
variation.id.hash(state);
// This is not correct, but we don't currently depend on the
// hash value being different based on decimal values of variations.
(variation.value as i64).hash(state);
}
/// 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));
}
}
impl Descriptor {
/// Returns a hash code that can be used to uniquely identify this
/// action.
pub fn hashcode(&self) -> u64 {
// `DefaultHasher::new()` uses fixed keys, so this is deterministic.
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
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));
}
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this.
pub fn to_fc_pattern(&self) -> fc::Pattern {
let mut pat = fc::Pattern::create();
if let Some(family) = &self.family {
assert!(pat.add_string(fc::Property::Family, family));
}
if let Some(style) = &self.style {
assert!(pat.add_string(fc::Property::Style, style));
}
if self.codepoint > 0 {
let mut cs = fc::CharSet::create();
assert!(cs.add_char(self.codepoint));
assert!(pat.add_charset(fc::Property::Charset, &cs));
}
if self.size > 0.0 {
assert!(pat.add_integer(fc::Property::Size, self.size.round() as i32));
}
if self.bold {
assert!(pat.add_integer(fc::Property::Weight, fc::Weight::Bold as i32));
}
if self.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));
// 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
}
pat
}
#[derive(Clone)]
pub struct Fontconfig {
fc_config: Arc<fc::Config>,
}
@@ -145,7 +77,7 @@ impl Fontconfig {
/// 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 = desc.to_fc_pattern();
let mut pat = to_fc_pattern(desc);
assert!(self.fc_config.substitute_with_pat_pattern(&mut pat));
pat.default_substitute();